
ChaosTest.pudu
Pudu120 lines8.3 KB
1/** @Test.Chaos.Suite — injected faults, outcomes, latency, and behavior */2module PuduLangResilience.ChaosTest34import Std.Io as Io5import Std.Sync as Sync6import Std.Test as Test7import PuduLangResilience.Chaos.Behavior as Behavior8import PuduLangResilience.Chaos as Chaos9import PuduLangResilience.Chaos.Fault as Fault10import PuduLangResilience.Chaos.Latency as Latency11import PuduLangResilience.Chaos.Outcome as Outcome12import PuduLangResilience.Chaos.Weighted as Weighted13import PuduLangResilience.Clock as Clock14import PuduLangResilience.Context as Context15import PuduLangResilience.Pipeline as Pipeline16import PuduLangResilience.Randomizer as Randomizer17import PuduLangResilience as Resilience18import PuduLangResilience.Strategy as Strategy192021fn chaotic(layers: Array[Strategy.Strategy[Int, Str]], clock: &Clock.Manual, draw: Decimal) -> Pipeline.Pipeline[Int, Str] {22 let settings = Pipeline.Options{..Pipeline.defaults(), clock: Clock.ofManual(clock), randomizer: Randomizer.fixed(draw)}23 match Pipeline.buildWith(&settings, layers) {24 case Ok(pipeline) => pipeline25 case Err(invalid) => panic(Pipeline.explain(&invalid))26 }27}282930fn counted(calls: &Sync.Counter) -> Strategy.Callback[Int, Str] {31 let counter = *calls32 fn(_context: Context.Context) -> Resilience.Outcome[Int, Str] {33 let _counted = Sync.increment(&counter, 1)34 Ok(1)35 }36}373839fn main() -> Int {40 let clock = Clock.manual(0)41 let calls = Sync.counter(0)42 let fault = Fault.injecting(Resilience.Raised("injected"), Chaos.atRate(0.6d))43 let injected = Pipeline.execute(&chaotic([Fault.strategy(fault)], &clock, 0.5d), counted(&calls))44 let skipped = Pipeline.execute(&chaotic([Fault.strategy(Fault.Options{..fault, injection: Chaos.atRate(0.4d)})], &clock, 0.5d), counted(&calls))45 let disabled = Pipeline.execute(&chaotic([Fault.strategy(Fault.Options{..fault, injection: Chaos.Injection{..Chaos.always(), enabled: false}})], &clock, 0.5d), counted(&calls))46 let byKey = Chaos.Injection{..Chaos.always(), enabledGenerator: Some(fn(context: Context.Context) -> Bool { context.operationKey == Some("chaos") })}47 let keyedOn = Pipeline.executeWith(&chaotic([Fault.strategy(Fault.Options{..fault, injection: byKey})], &clock, 0.5d), Context.keyed("chaos"), counted(&calls))48 let keyedOff = Pipeline.executeWith(&chaotic([Fault.strategy(Fault.Options{..fault, injection: byKey})], &clock, 0.5d), Context.keyed("calm"), counted(&calls))49 let generatedRate = Chaos.Injection{..Chaos.defaults(), rateGenerator: Some(fn(_context: Context.Context) -> Decimal { 2d })}50 let clampedRate = Pipeline.execute(&chaotic([Fault.strategy(Fault.Options{..fault, injection: generatedRate})], &clock, 0.99d), counted(&calls))51 let weighted = Fault.Options{..Fault.defaults(), injection: Chaos.always(), generator: Some(Weighted.choose(&[(1, Resilience.Raised("rare")), (0, Resilience.Raised("never")), (3, Resilience.TimedOut(5))]))}52 let heavy = Pipeline.execute(&chaotic([Fault.strategy(weighted)], &clock, 0.5d), counted(&calls))53 let light = Pipeline.execute(&chaotic([Fault.strategy(weighted)], &clock, 0.1d), counted(&calls))54 let nothing = Fault.Options{..Fault.defaults(), injection: Chaos.always(), generator: Some(Weighted.choose(&[(0, Resilience.Raised("never"))]))}55 let emptyChoice = Pipeline.execute(&chaotic([Fault.strategy(nothing)], &clock, 0.5d), counted(&calls))56 let callsSoFar = Sync.count(&calls)5758 let seenOutcome: Sync.Cell[Int] = Sync.cell(0)59 let outcome = Outcome.Options{..Outcome.injecting(Ok(42), Chaos.always()), onInjected: Some(fn(given: Outcome.Injected[Int, Str]) -> () {60 let _kept = Sync.set(&seenOutcome, match given.outcome { case Ok(value) => value case Err(_) => -1 })61 }) }62 let replaced = Pipeline.execute(&chaotic([Outcome.strategy(outcome)], &clock, 0.5d), counted(&Sync.counter(0)))6364 let latencyClock = Clock.manual(0)65 let latencyCalls = Sync.counter(0)66 let delayed = Pipeline.execute(&chaotic([Latency.strategy(Latency.injecting(250, Chaos.always()))], &latencyClock, 0.5d), counted(&latencyCalls))67 let generatedLatency = Latency.Options{..Latency.injecting(250, Chaos.always()), generator: Some(fn(_given: Chaos.Arguments) -> Int { 0 })}68 let _undelayed = Pipeline.execute(&chaotic([Latency.strategy(generatedLatency)], &latencyClock, 0.5d), counted(&latencyCalls))69 let stopping = Context.create()70 Context.cancel(&stopping, "stop")71 let cancelledLatency = Pipeline.executeWith(&chaotic([Latency.strategy(Latency.injecting(250, Chaos.always()))], &latencyClock, 0.5d), stopping, counted(&latencyCalls))7273 let behaved = Sync.counter(0)74 let behavior = Behavior.injecting(fn(_given: Chaos.Arguments) -> Result[(), Resilience.Failure[Str]] {75 let _counted = Sync.increment(&behaved, 1)76 Ok(())77 }, Chaos.always())78 let withBehavior = Pipeline.execute(&chaotic([Behavior.strategy(behavior)], &clock, 0.5d), counted(&Sync.counter(0)))79 let failingBehavior = Behavior.injecting(fn(_given: Chaos.Arguments) -> Result[(), Resilience.Failure[Str]] { Err(Resilience.Raised("behavior failed")) }, Chaos.always())80 let behaviorCalls = Sync.counter(0)81 let behaviorFailed = Pipeline.execute(&chaotic([Behavior.strategy(failingBehavior)], &clock, 0.5d), counted(&behaviorCalls))8283 let invalid = Pipeline.build([84 Fault.strategy(Fault.Options{..Fault.defaults(), injection: Chaos.atRate(1.5d)}),85 Outcome.strategy(Outcome.defaults()),86 Latency.strategy(Latency.Options{..Latency.defaults(), latency: -1}),87 Behavior.strategy(Behavior.defaults())88 ])89 let problems = match invalid { case Err(found) => found.problems case Ok(_) => [] }90 let checks = Test.suite("Chaos", &[91 Test.equals("a draw below the rate injects the fault", &injected, &Resilience.raise("injected")),92 Test.equals("a draw above the rate runs the callback", &skipped, &Ok(1)),93 Test.equals("a disabled strategy runs the callback", &disabled, &Ok(1)),94 Test.equals("an enabled generator can turn injection on", &keyedOn, &Resilience.raise("injected")),95 Test.equals("an enabled generator can turn injection off", &keyedOff, &Ok(1)),96 Test.equals("a generated rate above one always injects", &clampedRate, &Resilience.raise("injected")),97 Test.equals("a weighted choice follows the weights upward", &heavy, &Err(Resilience.TimedOut(5))),98 Test.equals("a weighted choice follows the weights downward", &light, &Resilience.raise("rare")),99 Test.equals("a choice without positive weights injects nothing", &emptyChoice, &Ok(1)),100 Test.equals("an injected fault does not run the callback", &callsSoFar, &Ok(4)),101 Test.equals("an injected outcome replaces the callback's", &replaced, &Ok(42)),102 Test.equals("onInjected sees the injected outcome", &Sync.get(&seenOutcome), &Ok(42)),103 Test.equals("injected latency runs the callback afterwards", &delayed, &Ok(1)),104 Test.equals("injected latency waits on the clock; a zero delay does not", &Clock.sleeps(&latencyClock), &[250]),105 Test.equals("a fired token ends the delay without running the callback", &cancelledLatency, &Err(Resilience.Cancelled("cancelled: stop"))),106 Test.equals("latency ran the callback only when not cancelled", &Sync.count(&latencyCalls), &Ok(2)),107 Test.equals("injected behavior runs before the callback", &(withBehavior, Sync.count(&behaved)), &(Ok(1), Ok(1))),108 Test.equals("failing behavior answers its failure", &behaviorFailed, &Resilience.raise("behavior failed")),109 Test.equals("failing behavior skips the callback", &Sync.count(&behaviorCalls), &Ok(0)),110 Test.equals("every invalid option is reported", &problems, &["ChaosFault: rate must be from 0 to 1", "ChaosFault: a fault or a generator is required", "ChaosOutcome: an outcome or a generator is required", "ChaosLatency: latency must not be negative", "ChaosBehavior: behavior is required"]),111 Test.that("configured chaos options are valid", Fault.validate(&fault).isEmpty() && Outcome.validate(&outcome).isEmpty() && Latency.validate(&generatedLatency).isEmpty() && Behavior.validate(&behavior).isEmpty()),112 Test.that("the injection bounds are accepted", Chaos.validate(&Chaos.atRate(0d)).isEmpty() && Chaos.validate(&Chaos.atRate(1d)).isEmpty())113 ])114 let ran = Test.run(&checks)115 for failure in Test.failuresOf(&ran) {116 let _reported = Io.writeErrorLine(failure)117 }118 Test.report(&ran)119}120