
PipelineTest.pudu
Pudu100 lines5.8 KB
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 Timeout141516fn 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}272829fn 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}353637fn 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