1use std::{
5 collections::hash_map::{DefaultHasher, RandomState},
6 env,
7 hash::{BuildHasher, Hash, Hasher},
8 panic::{self, AssertUnwindSafe},
9};
10
11const DEFAULT_ITERATIONS: u64 = 100;
12
13pub struct Chaos {
14 name: String,
15 iterations: u64,
16 base_seed: u64,
17}
18
19pub fn chaos(name: impl Into<String>) -> Chaos {
20 Chaos {
21 name: name.into(),
22 iterations: env_iterations().unwrap_or(DEFAULT_ITERATIONS),
23 base_seed: env_seed().unwrap_or_else(random_base_seed),
24 }
25}
26
27impl Chaos {
28 pub fn iterations(mut self, iterations: u64) -> Self {
29 self.iterations = iterations;
30 self
31 }
32
33 pub fn seed(mut self, base_seed: u64) -> Self {
34 self.base_seed = base_seed;
35 self
36 }
37
38 pub fn run(self, body: impl Fn(u64)) {
39 eprintln!("chaos \"{}\": {} iterations, base seed {}", self.name, self.iterations, self.base_seed);
40 for i in 0..self.iterations {
41 let seed = derive_seed(self.base_seed, i);
42 let result = panic::catch_unwind(AssertUnwindSafe(|| body(seed)));
43 if let Err(payload) = result {
44 eprintln!(
45 "\nchaos \"{}\" FAILED on iteration {} of {}\n base seed: {}\n iteration seed: {}\n reproduce: make test-chaos SEED={} N={}",
46 self.name,
47 i,
48 self.iterations,
49 self.base_seed,
50 seed,
51 self.base_seed,
52 self.iterations
53 );
54 panic::resume_unwind(payload);
55 }
56 }
57 }
58}
59
60#[macro_export]
61macro_rules! chaos_test {
62 ($name:ident, |$seed:ident| $body:block) => {
63 #[test]
64 fn $name() {
65 $crate::chaos::chaos(stringify!($name)).run(|$seed: u64| $body);
66 }
67 };
68}
69
70fn derive_seed(base: u64, salt: u64) -> u64 {
71 let mut h = DefaultHasher::new();
72 base.hash(&mut h);
73 salt.hash(&mut h);
74 h.finish()
75}
76
77fn random_base_seed() -> u64 {
78 RandomState::new().build_hasher().finish()
79}
80
81fn env_iterations() -> Option<u64> {
82 env::var("CHAOS_ITERATIONS").ok().and_then(|s| s.trim().parse::<u64>().ok())
83}
84
85fn env_seed() -> Option<u64> {
86 env::var("CHAOS_SEED").ok().and_then(|s| s.trim().parse::<u64>().ok())
87}
88
89#[cfg(test)]
90mod tests {
91 use std::{
92 panic::{AssertUnwindSafe, catch_unwind},
93 sync::atomic::{AtomicU64, Ordering},
94 };
95
96 use super::{chaos, derive_seed};
97
98 chaos_test!(macro_expands_to_a_runnable_test, |seed| {
104 assert_eq!(seed.wrapping_mul(2), seed.wrapping_add(seed));
105 });
106
107 #[test]
108 fn derive_seed_is_deterministic_and_decorrelated() {
109 assert_eq!(derive_seed(1, 1), derive_seed(1, 1));
113 assert_ne!(derive_seed(1, 1), derive_seed(1, 2));
114 assert_ne!(derive_seed(1, 1), derive_seed(2, 1));
115 }
116
117 #[test]
118 fn derived_iteration_seeds_are_distinct() {
119 let mut seeds: Vec<u64> = (0..1000u64).map(|i| derive_seed(42, i)).collect();
122 let total = seeds.len();
123 seeds.sort_unstable();
124 seeds.dedup();
125 assert_eq!(seeds.len(), total, "iteration seeds collide");
126 }
127
128 #[test]
129 fn passing_body_runs_exactly_iterations_times() {
130 let count = AtomicU64::new(0);
132 chaos("passing").seed(7).iterations(50).run(|_seed| {
133 count.fetch_add(1, Ordering::SeqCst);
134 });
135 assert_eq!(count.load(Ordering::SeqCst), 50);
136 }
137
138 #[test]
139 #[should_panic(expected = "boom")]
140 fn failing_iteration_is_caught_and_reraised_with_original_payload() {
141 let target = derive_seed(123, 3);
145 chaos("failing").seed(123).iterations(100).run(move |seed| {
146 if seed == target {
147 panic!("boom");
148 }
149 });
150 }
151
152 #[test]
153 fn fixed_base_seed_stops_on_the_same_iteration() {
154 let target = derive_seed(999, 17);
159 let first = invocations_until_panic(999, target);
160 let second = invocations_until_panic(999, target);
161 assert_eq!(first, 18, "should panic on iteration index 17 (18th invocation)");
162 assert_eq!(first, second, "same base seed must stop on the same iteration");
163 }
164
165 fn invocations_until_panic(base: u64, target: u64) -> u64 {
166 let count = AtomicU64::new(0);
167 let outcome = catch_unwind(AssertUnwindSafe(|| {
168 chaos("probe").seed(base).iterations(100).run(|seed| {
169 count.fetch_add(1, Ordering::SeqCst);
170 if seed == target {
171 panic!("probe hit");
172 }
173 });
174 }));
175 assert!(outcome.is_err(), "expected the probe to hit its target seed within the run");
176 count.load(Ordering::SeqCst)
177 }
178}