This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-sim / tests / reproducible.rs
7.8 kB 238 lines
1use std::collections::BTreeSet; 2 3use futures::future::join_all; 4use futures::stream::StreamExt; 5use knot_sim::{Outcome, Step, Trace}; 6 7fn status_of(step: &Step) -> Option<u16> { 8 match step.outcome { 9 Outcome::Answered { status, .. } => Some(status.get()), 10 Outcome::Killed => None, 11 } 12} 13 14fn union_has(traces: &[Trace], predicate: impl Fn(&Step) -> bool) -> bool { 15 traces 16 .iter() 17 .any(|trace| trace.steps.iter().any(&predicate)) 18} 19 20const READ_OPS: [&str; 11] = [ 21 "version", 22 "owner", 23 "listMembers", 24 "didJson", 25 "infoRefs", 26 "branches", 27 "log", 28 "describeRepo", 29 "tree", 30 "blob", 31 "languages", 32]; 33 34fn every_step_with<'a>( 35 traces: &'a [Trace], 36 fault: &'a str, 37 holds: impl Fn(&Step) -> bool + 'a, 38) -> (bool, usize) { 39 let matching: Vec<&Step> = traces 40 .iter() 41 .flat_map(|trace| trace.steps.iter()) 42 .filter(|step| step.fault == fault) 43 .collect(); 44 (matching.iter().all(|step| holds(step)), matching.len()) 45} 46 47#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 48async fn the_same_seed_produces_an_identical_whole_system_trace() { 49 futures::stream::iter([1u64, 7, 42, 100, 2026]) 50 .for_each(|seed| async move { 51 let first = knot_sim::run(seed, 16).await; 52 let second = knot_sim::run(seed, 16).await; 53 assert_eq!( 54 first, second, 55 "seed {seed} must replay to an identical trace" 56 ); 57 assert_eq!( 58 first.digest(), 59 second.digest(), 60 "seed {seed} digest is stable" 61 ); 62 }) 63 .await; 64} 65 66#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 67async fn concurrent_execution_stays_stable_across_fifty_repeats() { 68 let baseline = knot_sim::run(7, 18).await.digest(); 69 let digests: Vec<u64> = futures::stream::iter(0..50) 70 .then(|_| knot_sim::run(7, 18)) 71 .map(|trace| trace.digest()) 72 .collect() 73 .await; 74 assert!( 75 digests.iter().all(|digest| *digest == baseline), 76 "4-thread schedule leaked into trace: {} of 50 replays diverged", 77 digests.iter().filter(|digest| **digest != baseline).count() 78 ); 79} 80 81#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 82async fn distinct_seeds_produce_distinct_traces() { 83 let digests: Vec<u64> = join_all((0u64..8).map(|seed| knot_sim::run(seed, 16))) 84 .await 85 .iter() 86 .map(Trace::digest) 87 .collect(); 88 let unique: BTreeSet<u64> = digests.iter().copied().collect(); 89 assert_eq!( 90 unique.len(), 91 digests.len(), 92 "every seed must drive different whole-system trace: {digests:?}" 93 ); 94} 95 96#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 97async fn the_round_count_extends_the_same_prefix() { 98 let short = knot_sim::run(7, 10).await; 99 let long = knot_sim::run(7, 20).await; 100 assert_ne!(short.digest(), long.digest()); 101 assert_eq!( 102 short.steps, 103 long.steps[..short.steps.len()], 104 "longer run extends the same prefix the shorter run produced" 105 ); 106} 107 108#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 109async fn the_injected_failures_are_correlated_with_their_observable_outcomes() { 110 let traces = join_all([1u64, 7, 13, 42, 99, 2026].map(|seed| knot_sim::run(seed, 16))).await; 111 112 let (skew_all_401, skew_count) = 113 every_step_with(&traces, "clock_skew", |step| status_of(step) == Some(401)); 114 assert!(skew_count > 0, "clock-skew fault must actually fire"); 115 assert!( 116 skew_all_401, 117 "every clock-skewed token must produce 401 expiry rejection, not just one example" 118 ); 119 120 let (drop_all_503, drop_count) = every_step_with(&traces, "drop_identity", |step| { 121 status_of(step) == Some(503) 122 }); 123 assert!(drop_count > 0, "drop-identity fault must actually fire"); 124 assert!( 125 drop_all_503, 126 "every dropped identity resolution must surface as 503 upstream-unavailable failure" 127 ); 128 129 let (killed_all_killed, killed_count) = every_step_with(&traces, "killed", |step| { 130 matches!(step.outcome, Outcome::Killed) && READ_OPS.contains(&step.op) 131 }); 132 assert!(killed_count > 0, "kill fault must actually fire"); 133 assert!( 134 killed_all_killed, 135 "killed connection must record a Killed outcome and is only injected on read paths" 136 ); 137 138 assert!( 139 union_has(&traces, |step| step.op == "probe" 140 && step.fault == "none" 141 && status_of(step) == Some(403)), 142 "resolved stranger with no fault must be denied 403 by access-control layer" 143 ); 144 145 [ 146 "addMember", 147 "addCollaborator", 148 "createRepo", 149 "maintain", 150 "describeRepo", 151 "listMembers", 152 "infoRefs", 153 "didJson", 154 "branches", 155 "log", 156 "tree", 157 "blob", 158 "languages", 159 ] 160 .iter() 161 .for_each(|op| { 162 assert!( 163 union_has(&traces, |step| step.op == *op 164 && status_of(step) == Some(200)), 165 "{op} path must succeed against the doubles in at least one seed" 166 ); 167 }); 168} 169 170#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 171async fn the_final_projection_matches_an_independent_model() { 172 let seeds = [1u64, 7, 13, 42, 99, 2026]; 173 let predicted: Vec<knot_sim::Projection> = seeds 174 .iter() 175 .map(|seed| knot_sim::predict(*seed, 18)) 176 .collect(); 177 let runs = join_all(seeds.map(|seed| knot_sim::run(seed, 18))).await; 178 179 seeds 180 .iter() 181 .zip(predicted.iter()) 182 .zip(runs.iter()) 183 .for_each(|((seed, model), trace)| { 184 let last = trace.snapshots.last().expect("at least one round"); 185 assert_eq!( 186 last.members, model.members, 187 "seed {seed}: executed member projection must equal the independent model" 188 ); 189 assert_eq!( 190 last.blocked, model.blocked, 191 "seed {seed}: executed blocklist projection must equal the independent model" 192 ); 193 }); 194 195 assert!( 196 predicted.iter().any(|model| !model.members.is_empty()), 197 "oracle is vacuous unless at least one seed predicts a non-empty member set" 198 ); 199 assert!( 200 predicted.iter().any(|model| !model.blocked.is_empty()), 201 "oracle is vacuous unless at least one seed predicts a non-empty blocklist" 202 ); 203} 204 205#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 206async fn maintenance_never_reports_a_failure() { 207 let traces = join_all([1u64, 7, 13, 42, 99, 2026].map(|seed| knot_sim::run(seed, 16))).await; 208 let failures = traces 209 .iter() 210 .flat_map(|trace| trace.steps.iter()) 211 .filter(|step| step.op == "maintain" && status_of(step) == Some(500)) 212 .count(); 213 assert_eq!( 214 failures, 0, 215 "maintenance must succeed on every repo simulation runs it against" 216 ); 217 assert!( 218 union_has(&traces, |step| step.op == "maintain" 219 && status_of(step) == Some(200)), 220 "maintenance path must actually run against a populated repo in at least one seed" 221 ); 222} 223 224#[tokio::test(flavor = "multi_thread", worker_threads = 4)] 225async fn the_final_projection_state_is_seed_stable() { 226 let first = knot_sim::run(2026, 16).await; 227 let second = knot_sim::run(2026, 16).await; 228 assert_eq!( 229 first.snapshots, second.snapshots, 230 "order-independent COB projections must converge to the same logical state" 231 ); 232 let last = first.snapshots.last().expect("at least one round"); 233 assert!( 234 last.repos.len() > 1, 235 "simulation must have minted repos beyond the seed repo: {:?}", 236 last.repos 237 ); 238}