Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-resilience

Resilience pipelines for Pudu: retry, circuit breaker, timeout, fallback, hedging, rate limiting, and chaos injection

0.1.0Apache-2.01

InstallClose

PipelineTest.pudu

Pudu100 lines5.8 KB

GitHub ↗
1/** @Test.Pipeline.Suite — composition order, validation, nesting, and description */2module PuduLangResilience.PipelineTest34import Std.Io as Io5import Std.Result as Result6import Std.Sync as Sync7import Std.Test as Test8import PuduLangResilience.Context as Context9import PuduLangResilience.Pipeline as Pipeline10import PuduLangResilience as Resilience11import PuduLangResilience.Retry as Retry12import PuduLangResilience.Strategy as Strategy13import PuduLangResilience.Timeout as Timeout1415/// A strategy that records its name before and after the callback.16fn tracing(label: Str, trail: &Sync.Cell[Array[Str]]) -> Strategy.Strategy[Int, Str] {17  let sink = *trail18  Strategy.custom(Some(label), "Trace", fn(_runtime: Strategy.Runtime, context: Context.Context, callback: Strategy.Callback[Int, Str]) -> Resilience.Outcome[Int, Str] {19      let before = match Sync.get(&sink) { case Ok(list) => list case Err(_) => [] }20      let _entered = Sync.set(&sink, before.push(label + ">"))21      let outcome = callback(context)22      let after = match Sync.get(&sink) { case Ok(list) => list case Err(_) => [] }23      let _left = Sync.set(&sink, after.push("<" + label))24      outcome25    })26}2728/// The pipeline the strategies make; stops the suite when they are refused.29fn built(strategies: Array[Strategy.Strategy[Int, Str]]) -> Pipeline.Pipeline[Int, Str] {30  match Pipeline.build(strategies) {31    case Ok(pipeline) => pipeline32    case Err(invalid) => panic(Pipeline.explain(&invalid))33  }34}3536/// Runs the suite.37fn main() -> Int {38  let trail: Sync.Cell[Array[Str]] = Sync.cell([])39  let ordered = built([tracing("outer", &trail), tracing("inner", &trail)])40  let answered = Pipeline.execute(&ordered, fn(_context: Context.Context) -> Resilience.Outcome[Int, Str] { Ok(9) })4142  let empty: Pipeline.Pipeline[Int, Str] = Pipeline.empty()43  let emptyCalls = Sync.counter(0)44  let plain = Pipeline.execute(&empty, fn(_context: Context.Context) -> Resilience.Outcome[Int, Str] {45      let _counted = Sync.increment(&emptyCalls, 1)46      Resilience.raise("once")47    })48  let lifted = Pipeline.run(&empty, fn(_context: Context.Context) -> Result[Int, Str] { Err("plain error") })49  let liftedOk = Pipeline.run(&empty, fn(_context: Context.Context) -> Result[Int, Str] { Ok(3) })50  let keyed = Pipeline.runWith(&empty, Context.keyed("orders"), fn(context: Context.Context) -> Result[Int, Str] {51      match context.operationKey { case Some(key) => Ok(key.length()) case None => Err("no key") }52    })5354  let innerTrail: Sync.Cell[Array[Str]] = Sync.cell([])55  let inner = match Pipeline.buildWith(&Pipeline.Options{..Pipeline.defaults(), name: Some("inner-pipeline")}, [tracing("nested", &innerTrail)]) {56    case Ok(pipeline) => pipeline57    case Err(invalid) => panic(Pipeline.explain(&invalid))58  }59  let outer = built([tracing("host", &innerTrail), Pipeline.asStrategy(&inner)])60  let _nestedRun = Pipeline.execute(&outer, fn(_context: Context.Context) -> Resilience.Outcome[Int, Str] { Ok(1) })6162  let duplicate = Pipeline.build([Retry.strategy(Retry.Options{..Retry.defaults(), name: Some("same")}), Timeout.strategy(Timeout.Options{..Timeout.defaults(), name: Some("same")})])63  let unnamedTwice = Pipeline.build([Retry.strategy(Retry.defaults()), Retry.strategy(Retry.defaults())])64  let collected = Pipeline.build([Retry.strategy(Retry.Options{..Retry.defaults(), maxRetryAttempts: 0}), Timeout.strategy(Timeout.after(1))])6566  let described = Pipeline.describe(&match Pipeline.buildWith(&Pipeline.Options{..Pipeline.defaults(), name: Some("orders"), instance: Some("eu")}, [67        Retry.strategy(Retry.Options{..Retry.defaults(), name: Some("retry"), backoff: Retry.Exponential, useJitter: true}),68        Timeout.strategy(Timeout.after(500)),69        Pipeline.asStrategy(&inner)70      ]) {71      case Ok(pipeline) => pipeline72      case Err(invalid) => panic(Pipeline.explain(&invalid))73    })7475  let checks = Test.suite("Pipeline", &[76      Test.equals("the first strategy is the outermost", &Sync.get(&trail), &Ok(["outer>", "inner>", "<inner", "<outer"])),77      Test.equals("the callback's outcome passes out through every layer", &answered, &Ok(9)),78      Test.equals("an empty pipeline runs the callback once, as given", &(plain, Sync.count(&emptyCalls)), &(Resilience.raise("once"), Ok(1))),79      Test.equals("a plain error becomes a raised failure", &lifted, &Resilience.raise("plain error")),80      Test.equals("a plain value passes through", &liftedOk, &Ok(3)),81      Test.equals("the given context reaches the callback", &keyed, &Ok(6)),82      Test.equals("a nested pipeline runs as one layer", &Sync.get(&innerTrail), &Ok(["host>", "nested>", "<nested", "<host"])),83      Test.equals("a strategy name used twice is refused", &Result.err(duplicate), &Some(Pipeline.Invalid{problems: ["the strategy name 'same' is used more than once"]})),84      Test.that("unnamed strategies may repeat", Result.isOk(&unnamedTwice)),85      Test.equals("problems from every strategy are collected", &Result.err(collected), &Some(Pipeline.Invalid{problems: ["Retry: maxRetryAttempts must be at least 1", "Timeout: timeout must be from 10 to 86400000 ms"]})),86      Test.equals("a description names the pipeline", &(described.name, described.instance), &(Some("orders"), Some("eu"))),87      Test.equals("a description lists each strategy outermost first", &described.strategies, &[88          Pipeline.Described{name: Some("retry"), kind: "Retry", summary: "3 retries, exponential backoff from 2000 ms with jitter"},89          Pipeline.Described{name: None, kind: "Timeout", summary: "500 ms"},90          Pipeline.Described{name: Some("inner-pipeline"), kind: "Pipeline", summary: "1 strategies"}91        ]),92      Test.equals("an invalid pipeline explains itself", &Pipeline.explain(&Pipeline.Invalid{problems: ["a", "b"]}), &"The pipeline is invalid: a; b")93    ])94  let ran = Test.run(&checks)95  for failure in Test.failuresOf(&ran) {96    let _reported = Io.writeErrorLine(failure)97  }98  Test.report(&ran)99}100