asupersync 0.3.0

Spec-first, cancel-correct, capability-secure async runtime for Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
//! Chaos testing configuration and injection hooks for the Lab runtime.
//!
//! Chaos testing deliberately injects faults into a system to verify that it handles
//! failures correctly. The key insight is that bugs often hide in error handling paths
//! that rarely execute in production—chaos testing exercises those paths.
//!
//! # Why Chaos Testing?
//!
//! Traditional testing often exercises only the "happy path." But production systems
//! face cancellations, timeouts, resource exhaustion, and unexpected delays. Chaos
//! testing verifies your code handles these conditions gracefully.
//!
//! Key benefits:
//! - **Find bugs early**: Discover race conditions and error handling bugs before production
//! - **Build confidence**: Know your code handles adverse conditions correctly
//! - **Reproducible failures**: Same seed = same chaos sequence = reproducible bugs
//!
//! # Chaos Types
//!
//! | Type | What It Tests |
//! |------|---------------|
//! | **Cancellation** | Task cancellation at arbitrary poll points |
//! | **Delay** | Handling of slow operations and timeouts |
//! | **I/O Errors** | Recovery from network/disk failures |
//! | **Wakeup Storms** | Waker correctness under spurious wakes |
//! | **Budget Exhaustion** | Resource quota enforcement |
//!
//! # Determinism
//!
//! All chaos injection uses a deterministic RNG seeded from the configuration.
//! Running the same test with the same seed produces identical chaos sequences,
//! making failures reproducible:
//!
//! ```text
//! FAILED tests/my_test.rs - seed 12345
//! Re-run with `CHAOS_SEED=12345` to reproduce
//! ```
//!
//! # Quick Start
//!
//! ## Using Presets with LabRuntime
//!
//! The easiest way to enable chaos testing:
//!
//! ```ignore
//! use asupersync::lab::{LabConfig, LabRuntime};
//!
//! // Light chaos for CI (low-probability, fast)
//! let config = LabConfig::new(42).with_light_chaos();
//! let mut runtime = LabRuntime::new(config);
//!
//! // Heavy chaos for thorough testing
//! let config = LabConfig::new(42).with_heavy_chaos();
//! let mut runtime = LabRuntime::new(config);
//! ```
//!
//! ## Custom Configuration
//!
//! For fine-grained control:
//!
//! ```ignore
//! use asupersync::lab::{LabConfig, LabRuntime};
//! use asupersync::lab::chaos::ChaosConfig;
//! use std::time::Duration;
//!
//! // Delay-only configuration (no cancellations)
//! let chaos = ChaosConfig::new(42)
//!     .with_cancel_probability(0.0)      // No cancellations
//!     .with_delay_probability(0.3)       // 30% delay
//!     .with_delay_range(Duration::from_micros(1)..Duration::from_micros(100));
//!
//! let config = LabConfig::new(42).with_chaos(chaos);
//! let mut runtime = LabRuntime::new(config);
//! ```
//!
//! ## Checking Injection Statistics
//!
//! Verify chaos is working as expected:
//!
//! ```ignore
//! // After running your test...
//! let stats = runtime.chaos_stats();
//! println!("Decision points: {}", stats.decision_points);
//! println!("Delays injected: {}", stats.delays);
//! println!("Injection rate: {:.1}%", stats.injection_rate() * 100.0);
//! ```
//!
//! # Presets
//!
//! | Preset | Use Case | Cancel | Delay | I/O Error |
//! |--------|----------|--------|-------|-----------|
//! | `ChaosConfig::off()` | Disabled | 0% | 0% | 0% |
//! | `ChaosConfig::light()` | CI pipelines | 1% | 5% | 2% |
//! | `ChaosConfig::heavy()` | Thorough testing | 10% | 20% | 15% |
//!
//! # Best Practices
//!
//! 1. **Start with light chaos in CI** - Catches obvious bugs without excessive flakiness
//! 2. **Use heavy chaos for release testing** - Thorough stress testing before deployment
//! 3. **Log the seed on failure** - Enables exact reproduction of failures
//! 4. **Test cancellation resilience** - Ensure cleanup code runs correctly
//! 5. **Monitor injection rates** - Use `ChaosStats` to verify chaos is working

use std::io;
use std::ops::Range;
use std::time::Duration;

use crate::util::DetRng;

/// Configuration for chaos injection in the Lab runtime.
///
/// ChaosConfig controls what types of chaos are injected and with what probability.
/// All probabilities are in the range \[0.0, 1.0\].
///
/// # Presets
///
/// - [`ChaosConfig::off()`]: No chaos (all probabilities zero)
/// - [`ChaosConfig::light()`]: Light chaos suitable for CI (low probabilities)
/// - [`ChaosConfig::heavy()`]: Heavy chaos for thorough testing (higher probabilities)
#[derive(Debug, Clone)]
pub struct ChaosConfig {
    /// Seed for deterministic chaos (required).
    ///
    /// The same seed produces the same chaos sequence.
    pub seed: u64,

    /// Probability of injecting cancellation at each poll point.
    ///
    /// Range: \[0.0, 1.0\]. Default: 0.0 (no cancellation injection).
    pub cancel_probability: f64,

    /// Probability of adding delay at each poll point.
    ///
    /// Range: \[0.0, 1.0\]. Default: 0.0 (no delay injection).
    pub delay_probability: f64,

    /// Range of delays when delay is injected.
    ///
    /// A random duration in this range is selected uniformly.
    pub delay_range: Range<Duration>,

    /// Probability of I/O operation failing.
    ///
    /// Range: \[0.0, 1.0\]. Default: 0.0 (no I/O error injection).
    pub io_error_probability: f64,

    /// Error kinds to inject for I/O failures.
    ///
    /// When an I/O error is injected, one of these kinds is selected uniformly.
    pub io_error_kinds: Vec<io::ErrorKind>,

    /// Probability of triggering a spurious wakeup storm.
    ///
    /// Range: \[0.0, 1.0\]. Default: 0.0 (no wakeup storms).
    pub wakeup_storm_probability: f64,

    /// Number of spurious wakeups in a storm.
    ///
    /// When a storm is triggered, a random count in this range is selected.
    pub wakeup_storm_count: Range<usize>,

    /// Probability of budget exhaustion.
    ///
    /// Range: \[0.0, 1.0\]. Default: 0.0 (no budget exhaustion).
    pub budget_exhaust_probability: f64,
}

impl Default for ChaosConfig {
    /// Creates a ChaosConfig with all chaos disabled.
    fn default() -> Self {
        Self::off()
    }
}

impl ChaosConfig {
    /// Creates a new ChaosConfig with the given seed and all chaos disabled.
    #[must_use]
    pub const fn new(seed: u64) -> Self {
        Self {
            seed,
            cancel_probability: 0.0,
            delay_probability: 0.0,
            delay_range: Duration::ZERO..Duration::ZERO,
            io_error_probability: 0.0,
            io_error_kinds: Vec::new(),
            wakeup_storm_probability: 0.0,
            wakeup_storm_count: 1..5,
            budget_exhaust_probability: 0.0,
        }
    }

    /// Creates a ChaosConfig with all chaos disabled (same as default).
    ///
    /// Use this as a baseline, then enable specific chaos types.
    #[must_use]
    pub const fn off() -> Self {
        Self::new(0)
    }

    /// Preset for light chaos suitable for CI.
    ///
    /// Low probabilities to catch obvious issues without excessive flakiness.
    ///
    /// - Cancel: 1%
    /// - Delay: 5% (0-10ms)
    /// - I/O Error: 2%
    /// - Wakeup Storm: 1%
    /// - Budget Exhaust: 0.5%
    #[inline]
    #[must_use]
    pub fn light() -> Self {
        Self {
            seed: 0,
            cancel_probability: 0.01,
            delay_probability: 0.05,
            delay_range: Duration::ZERO..Duration::from_millis(10),
            io_error_probability: 0.02,
            io_error_kinds: vec![
                io::ErrorKind::ConnectionReset,
                io::ErrorKind::TimedOut,
                io::ErrorKind::WouldBlock,
            ],
            wakeup_storm_probability: 0.01,
            wakeup_storm_count: 1..5,
            budget_exhaust_probability: 0.005,
        }
    }

    /// Preset for heavy chaos for thorough testing.
    ///
    /// Higher probabilities to stress-test error handling.
    ///
    /// - Cancel: 10%
    /// - Delay: 20% (0-100ms)
    /// - I/O Error: 15%
    /// - Wakeup Storm: 5%
    /// - Budget Exhaust: 5%
    #[inline]
    #[must_use]
    pub fn heavy() -> Self {
        Self {
            seed: 0,
            cancel_probability: 0.10,
            delay_probability: 0.20,
            delay_range: Duration::ZERO..Duration::from_millis(100),
            io_error_probability: 0.15,
            io_error_kinds: vec![
                io::ErrorKind::ConnectionReset,
                io::ErrorKind::ConnectionRefused,
                io::ErrorKind::ConnectionAborted,
                io::ErrorKind::TimedOut,
                io::ErrorKind::WouldBlock,
                io::ErrorKind::BrokenPipe,
                io::ErrorKind::NotConnected,
            ],
            wakeup_storm_probability: 0.05,
            wakeup_storm_count: 1..20,
            budget_exhaust_probability: 0.05,
        }
    }

    // ───────────────────────────────────────────────────────────────────────────
    // Builder methods
    // ───────────────────────────────────────────────────────────────────────────

    /// Sets the seed for deterministic chaos.
    #[inline]
    #[must_use]
    pub const fn with_seed(mut self, seed: u64) -> Self {
        self.seed = seed;
        self
    }

    /// Sets the probability of cancellation injection.
    ///
    /// # Panics
    ///
    /// Panics if `probability` is not in \[0.0, 1.0\].
    #[inline]
    #[must_use]
    pub fn with_cancel_probability(mut self, probability: f64) -> Self {
        assert!(
            (0.0..=1.0).contains(&probability),
            "probability must be in [0.0, 1.0], got {probability}"
        );
        self.cancel_probability = probability;
        self
    }

    /// Sets the probability of delay injection.
    ///
    /// # Panics
    ///
    /// Panics if `probability` is not in \[0.0, 1.0\].
    #[inline]
    #[must_use]
    pub fn with_delay_probability(mut self, probability: f64) -> Self {
        assert!(
            (0.0..=1.0).contains(&probability),
            "probability must be in [0.0, 1.0], got {probability}"
        );
        self.delay_probability = probability;
        self
    }

    /// Sets the range of delays when delay is injected.
    #[inline]
    #[must_use]
    pub fn with_delay_range(mut self, range: Range<Duration>) -> Self {
        self.delay_range = range;
        self
    }

    /// Sets the probability of I/O error injection.
    ///
    /// # Panics
    ///
    /// Panics if `probability` is not in \[0.0, 1.0\].
    #[inline]
    #[must_use]
    pub fn with_io_error_probability(mut self, probability: f64) -> Self {
        assert!(
            (0.0..=1.0).contains(&probability),
            "probability must be in [0.0, 1.0], got {probability}"
        );
        self.io_error_probability = probability;
        self
    }

    /// Sets the error kinds to inject for I/O failures.
    #[inline]
    #[must_use]
    pub fn with_io_error_kinds(mut self, kinds: Vec<io::ErrorKind>) -> Self {
        self.io_error_kinds = kinds;
        self
    }

    /// Sets the probability of wakeup storm injection.
    ///
    /// # Panics
    ///
    /// Panics if `probability` is not in \[0.0, 1.0\].
    #[inline]
    #[must_use]
    pub fn with_wakeup_storm_probability(mut self, probability: f64) -> Self {
        assert!(
            (0.0..=1.0).contains(&probability),
            "probability must be in [0.0, 1.0], got {probability}"
        );
        self.wakeup_storm_probability = probability;
        self
    }

    /// Sets the range of wakeup counts in a storm.
    #[inline]
    #[must_use]
    pub fn with_wakeup_storm_count(mut self, range: Range<usize>) -> Self {
        self.wakeup_storm_count = range;
        self
    }

    /// Sets the probability of budget exhaustion injection.
    ///
    /// # Panics
    ///
    /// Panics if `probability` is not in \[0.0, 1.0\].
    #[inline]
    #[must_use]
    pub fn with_budget_exhaust_probability(mut self, probability: f64) -> Self {
        assert!(
            (0.0..=1.0).contains(&probability),
            "probability must be in [0.0, 1.0], got {probability}"
        );
        self.budget_exhaust_probability = probability;
        self
    }

    // ───────────────────────────────────────────────────────────────────────────
    // Introspection
    // ───────────────────────────────────────────────────────────────────────────

    /// Returns true if any chaos is enabled.
    #[inline]
    #[must_use]
    pub fn is_enabled(&self) -> bool {
        self.cancel_probability > 0.0
            || (self.delay_probability > 0.0 && delay_range_can_emit_nonzero(&self.delay_range))
            || (self.io_error_probability > 0.0 && !self.io_error_kinds.is_empty())
            || (self.wakeup_storm_probability > 0.0
                && wakeup_range_can_emit_positive(&self.wakeup_storm_count))
            || self.budget_exhaust_probability > 0.0
    }

    /// Returns a summary of enabled chaos types.
    #[must_use]
    pub fn summary(&self) -> String {
        let mut parts = Vec::new();
        if self.cancel_probability > 0.0 {
            parts.push(format!("cancel:{:.1}%", self.cancel_probability * 100.0));
        }
        if self.delay_probability > 0.0 && delay_range_can_emit_nonzero(&self.delay_range) {
            parts.push(format!("delay:{:.1}%", self.delay_probability * 100.0));
        }
        if self.io_error_probability > 0.0 && !self.io_error_kinds.is_empty() {
            parts.push(format!("io_err:{:.1}%", self.io_error_probability * 100.0));
        }
        if self.wakeup_storm_probability > 0.0
            && wakeup_range_can_emit_positive(&self.wakeup_storm_count)
        {
            parts.push(format!(
                "wakeup:{:.1}%",
                self.wakeup_storm_probability * 100.0
            ));
        }
        if self.budget_exhaust_probability > 0.0 {
            parts.push(format!(
                "budget:{:.1}%",
                self.budget_exhaust_probability * 100.0
            ));
        }
        if parts.is_empty() {
            "off".to_string()
        } else {
            parts.join(",")
        }
    }

    /// Creates a [`ChaosRng`] from this configuration.
    #[must_use]
    pub fn rng(&self) -> ChaosRng {
        ChaosRng::from_config(self)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Chaos RNG
// ─────────────────────────────────────────────────────────────────────────────

/// Deterministic RNG for chaos injection decisions.
///
/// Uses the existing [`DetRng`] internally but provides chaos-specific methods.
#[derive(Debug, Clone)]
pub struct ChaosRng {
    inner: DetRng,
}

impl ChaosRng {
    /// Creates a new ChaosRng with the given seed.
    #[must_use]
    pub fn new(seed: u64) -> Self {
        Self {
            inner: DetRng::new(seed),
        }
    }

    /// Creates a ChaosRng from a [`ChaosConfig`].
    #[must_use]
    pub fn from_config(config: &ChaosConfig) -> Self {
        Self::new(config.seed)
    }

    /// Returns a random f64 in \[0.0, 1.0).
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    pub fn next_f64(&mut self) -> f64 {
        // Convert u64 to f64 in [0.0, 1.0)
        // We use the upper 53 bits for best precision
        let bits = self.inner.next_u64() >> 11;
        bits as f64 * (1.0 / (1u64 << 53) as f64)
    }

    /// Returns a random u64 from the underlying deterministic RNG.
    #[must_use]
    pub fn next_u64(&mut self) -> u64 {
        self.inner.next_u64()
    }

    /// Returns true with the given probability.
    #[must_use]
    pub fn should_inject(&mut self, probability: f64) -> bool {
        if probability <= 0.0 {
            return false;
        }
        if probability >= 1.0 {
            return true;
        }
        self.next_f64() < probability
    }

    /// Checks if cancellation should be injected based on config.
    #[must_use]
    pub fn should_inject_cancel(&mut self, config: &ChaosConfig) -> bool {
        self.should_inject(config.cancel_probability)
    }

    /// Checks if delay should be injected based on config.
    #[must_use]
    pub fn should_inject_delay(&mut self, config: &ChaosConfig) -> bool {
        if !delay_range_can_emit_nonzero(&config.delay_range) {
            return false;
        }
        self.should_inject(config.delay_probability)
    }

    /// Generates a random delay duration from the config's delay range.
    #[must_use]
    pub fn next_delay(&mut self, config: &ChaosConfig) -> Duration {
        let range = &config.delay_range;
        let start_nanos = range.start.as_nanos();
        let end_nanos = range.end.as_nanos();
        if end_nanos <= start_nanos {
            return Duration::ZERO;
        }
        let min_nanos = if start_nanos == 0 && end_nanos > 1 {
            1
        } else {
            start_nanos
        };
        if end_nanos <= min_nanos {
            return nanos_to_duration_saturating(min_nanos);
        }
        let delta = end_nanos - min_nanos;
        let rand = (u128::from(self.inner.next_u64()) << 64) | u128::from(self.inner.next_u64());
        let offset = rand % delta;
        nanos_to_duration_saturating(min_nanos + offset)
    }

    /// Checks if I/O error should be injected based on config.
    #[must_use]
    pub fn should_inject_io_error(&mut self, config: &ChaosConfig) -> bool {
        if config.io_error_kinds.is_empty() {
            return false;
        }
        self.should_inject(config.io_error_probability)
    }

    /// Generates a random I/O error kind from the config's error kinds.
    ///
    /// Returns `None` if no error kinds are configured.
    #[must_use]
    pub fn next_io_error_kind(&mut self, config: &ChaosConfig) -> Option<io::ErrorKind> {
        if config.io_error_kinds.is_empty() {
            return None;
        }
        let idx = self.inner.next_usize(config.io_error_kinds.len());
        Some(config.io_error_kinds[idx])
    }

    /// Generates a random I/O error based on config.
    ///
    /// Returns `None` if no error kinds are configured.
    #[must_use]
    pub fn next_io_error(&mut self, config: &ChaosConfig) -> Option<io::Error> {
        self.next_io_error_kind(config)
            .map(|kind| io::Error::new(kind, "chaos-injected I/O error"))
    }

    /// Checks if wakeup storm should be triggered based on config.
    #[must_use]
    pub fn should_inject_wakeup_storm(&mut self, config: &ChaosConfig) -> bool {
        if !wakeup_range_can_emit_positive(&config.wakeup_storm_count) {
            return false;
        }
        self.should_inject(config.wakeup_storm_probability)
    }

    /// Generates a random wakeup count from the config's storm range.
    #[must_use]
    pub fn next_wakeup_count(&mut self, config: &ChaosConfig) -> usize {
        let range = &config.wakeup_storm_count;
        if range.end <= range.start {
            return 0;
        }
        let min_count = if range.start == 0 && range.end > 1 {
            1
        } else {
            range.start
        };
        if range.end <= min_count {
            return min_count;
        }
        let delta = range.end - min_count;
        min_count + self.inner.next_usize(delta)
    }

    /// Checks if budget exhaustion should be injected based on config.
    #[must_use]
    pub fn should_inject_budget_exhaust(&mut self, config: &ChaosConfig) -> bool {
        self.should_inject(config.budget_exhaust_probability)
    }

    /// Advances the internal RNG state, useful for synchronization.
    pub fn skip(&mut self, count: usize) {
        for _ in 0..count {
            let _ = self.inner.next_u64();
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Injection Points (for documentation and future integration)
// ─────────────────────────────────────────────────────────────────────────────

/// Identifies where chaos can be injected in the runtime.
///
/// This enum documents the injection points; actual injection is performed
/// by the respective components (Scheduler, Reactor, etc.).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InjectionPoint {
    /// Before polling a task in the scheduler.
    SchedulerPoll,

    /// Before executing a task poll.
    TaskPoll,

    /// In the I/O reactor's poll operation.
    ReactorPoll,

    /// When a waker is invoked.
    WakerInvoke,

    /// When checking budget constraints.
    BudgetCheck,

    /// When a timer fires.
    TimerFire,

    /// Before acquiring a lock/semaphore.
    SyncAcquire,

    /// Before sending on a channel.
    ChannelSend,

    /// Before receiving on a channel.
    ChannelRecv,
}

impl InjectionPoint {
    /// Returns which chaos types are applicable at this injection point.
    #[must_use]
    pub fn applicable_chaos(&self) -> &'static [ChaosType] {
        match self {
            Self::TaskPoll => &[
                ChaosType::Cancel,
                ChaosType::Delay,
                ChaosType::BudgetExhaust,
            ],
            Self::ReactorPoll => &[ChaosType::IoError, ChaosType::Delay],
            Self::WakerInvoke => &[ChaosType::WakeupStorm, ChaosType::Delay],
            Self::BudgetCheck => &[ChaosType::BudgetExhaust],
            Self::TimerFire => &[ChaosType::Delay],
            Self::SchedulerPoll | Self::SyncAcquire | Self::ChannelSend | Self::ChannelRecv => {
                &[ChaosType::Cancel, ChaosType::Delay]
            }
        }
    }
}

/// Types of chaos that can be injected.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChaosType {
    /// Cancellation injection.
    Cancel,
    /// Delay injection.
    Delay,
    /// I/O error injection.
    IoError,
    /// Spurious wakeup storm.
    WakeupStorm,
    /// Budget exhaustion.
    BudgetExhaust,
}

// ─────────────────────────────────────────────────────────────────────────────
// Statistics
// ─────────────────────────────────────────────────────────────────────────────

/// Statistics about chaos injection.
#[derive(Debug, Clone, Default)]
pub struct ChaosStats {
    /// Number of cancellations injected.
    pub cancellations: u64,
    /// Number of delays injected.
    pub delays: u64,
    /// Total delay time injected.
    pub total_delay: Duration,
    /// Number of I/O errors injected.
    pub io_errors: u64,
    /// Number of wakeup storms triggered.
    pub wakeup_storms: u64,
    /// Total spurious wakeups generated.
    pub spurious_wakeups: u64,
    /// Number of budget exhaustions injected.
    pub budget_exhaustions: u64,
    /// Total injection decision points encountered.
    pub decision_points: u64,
}

impl ChaosStats {
    /// Creates a new empty stats tracker.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            cancellations: 0,
            delays: 0,
            total_delay: Duration::ZERO,
            io_errors: 0,
            wakeup_storms: 0,
            spurious_wakeups: 0,
            budget_exhaustions: 0,
            decision_points: 0,
        }
    }

    /// Records a cancellation injection.
    pub fn record_cancel(&mut self) {
        self.cancellations += 1;
        self.decision_points += 1;
    }

    /// Records a delay injection.
    pub fn record_delay(&mut self, delay: Duration) {
        self.delays += 1;
        self.total_delay += delay;
        self.decision_points += 1;
    }

    /// Records an I/O error injection.
    pub fn record_io_error(&mut self) {
        self.io_errors += 1;
        self.decision_points += 1;
    }

    /// Records a wakeup storm.
    pub fn record_wakeup_storm(&mut self, count: u64) {
        self.wakeup_storms += 1;
        self.spurious_wakeups += count;
        self.decision_points += 1;
    }

    /// Records a budget exhaustion.
    pub fn record_budget_exhaust(&mut self) {
        self.budget_exhaustions += 1;
        self.decision_points += 1;
    }

    /// Records the combined outcomes from a single pre-poll chaos decision.
    ///
    /// The pre-poll hook can inject multiple chaos types at once (for example,
    /// cancellation and budget exhaustion on the same task poll boundary). That
    /// still counts as one decision point, so callers should use this helper
    /// instead of chaining [`record_cancel`](Self::record_cancel),
    /// [`record_delay`](Self::record_delay), and
    /// [`record_budget_exhaust`](Self::record_budget_exhaust), which would
    /// overcount `decision_points`.
    pub fn record_pre_poll_outcomes(
        &mut self,
        cancel: bool,
        delay: Option<Duration>,
        budget_exhaust: bool,
    ) {
        if cancel {
            self.cancellations += 1;
        }
        if let Some(delay) = delay {
            self.delays += 1;
            self.total_delay += delay;
        }
        if budget_exhaust {
            self.budget_exhaustions += 1;
        }
        self.decision_points += 1;
    }

    /// Records a decision point where no chaos was injected.
    pub fn record_no_injection(&mut self) {
        self.decision_points += 1;
    }

    /// Merges another stats instance into this one.
    pub fn merge(&mut self, other: &Self) {
        self.cancellations = self.cancellations.saturating_add(other.cancellations);
        self.delays = self.delays.saturating_add(other.delays);
        self.total_delay = self.total_delay.saturating_add(other.total_delay);
        self.io_errors = self.io_errors.saturating_add(other.io_errors);
        self.wakeup_storms = self.wakeup_storms.saturating_add(other.wakeup_storms);
        self.spurious_wakeups = self.spurious_wakeups.saturating_add(other.spurious_wakeups);
        self.budget_exhaustions = self
            .budget_exhaustions
            .saturating_add(other.budget_exhaustions);
        self.decision_points = self.decision_points.saturating_add(other.decision_points);
    }

    /// Returns the injection rate (injections / decision points).
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    pub fn injection_rate(&self) -> f64 {
        if self.decision_points == 0 {
            return 0.0;
        }
        let injections = self
            .cancellations
            .saturating_add(self.delays)
            .saturating_add(self.io_errors)
            .saturating_add(self.wakeup_storms)
            .saturating_add(self.budget_exhaustions);
        if self.decision_points == 0 {
            return 0.0;
        }
        injections as f64 / self.decision_points as f64
    }
}

impl std::fmt::Display for ChaosStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "ChaosStats {{ decisions: {}, cancels: {}, delays: {} ({:?}), io_errors: {}, \
             wakeup_storms: {} ({} wakeups), budget_exhausts: {}, rate: {:.2}% }}",
            self.decision_points,
            self.cancellations,
            self.delays,
            self.total_delay,
            self.io_errors,
            self.wakeup_storms,
            self.spurious_wakeups,
            self.budget_exhaustions,
            self.injection_rate() * 100.0
        )
    }
}

/// Converts nanoseconds into `Duration`, saturating at `Duration::MAX`.
fn nanos_to_duration_saturating(nanos: u128) -> Duration {
    const NANOS_PER_SEC: u128 = 1_000_000_000;
    let secs = nanos / NANOS_PER_SEC;
    let subsec = (nanos % NANOS_PER_SEC) as u32;
    if secs > u128::from(u64::MAX) {
        Duration::MAX
    } else {
        Duration::new(secs as u64, subsec)
    }
}

fn delay_range_can_emit_nonzero(range: &Range<Duration>) -> bool {
    let start_nanos = range.start.as_nanos();
    let end_nanos = range.end.as_nanos();
    end_nanos > start_nanos && (start_nanos > 0 || end_nanos > 1)
}

fn wakeup_range_can_emit_positive(range: &Range<usize>) -> bool {
    range.end > range.start && (range.start > 0 || range.end > 1)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn config_off_has_no_chaos() {
        let config = ChaosConfig::off();
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");
    }

    #[test]
    fn config_light_has_chaos() {
        let config = ChaosConfig::light();
        assert!(config.is_enabled());
        assert!(config.cancel_probability > 0.0);
        assert!(config.delay_probability > 0.0);
    }

    #[test]
    fn config_heavy_has_higher_probabilities() {
        let light = ChaosConfig::light();
        let heavy = ChaosConfig::heavy();
        assert!(heavy.cancel_probability > light.cancel_probability);
        assert!(heavy.delay_probability > light.delay_probability);
        assert!(heavy.io_error_probability > light.io_error_probability);
    }

    #[test]
    fn config_builder_pattern() {
        let config = ChaosConfig::new(42)
            .with_cancel_probability(0.1)
            .with_delay_probability(0.2)
            .with_delay_range(Duration::from_millis(5)..Duration::from_millis(50))
            .with_io_error_probability(0.05)
            .with_io_error_kinds(vec![io::ErrorKind::ConnectionReset])
            .with_wakeup_storm_probability(0.01)
            .with_wakeup_storm_count(1..10)
            .with_budget_exhaust_probability(0.02);

        assert_eq!(config.seed, 42);
        assert!((config.cancel_probability - 0.1).abs() < f64::EPSILON);
        assert!((config.delay_probability - 0.2).abs() < f64::EPSILON);
        assert_eq!(config.delay_range.start, Duration::from_millis(5));
        assert_eq!(config.delay_range.end, Duration::from_millis(50));
        assert!((config.io_error_probability - 0.05).abs() < f64::EPSILON);
        assert_eq!(config.io_error_kinds.len(), 1);
        assert!((config.wakeup_storm_probability - 0.01).abs() < f64::EPSILON);
        assert_eq!(config.wakeup_storm_count, 1..10);
        assert!((config.budget_exhaust_probability - 0.02).abs() < f64::EPSILON);
    }

    #[test]
    #[should_panic(expected = "probability must be in [0.0, 1.0]")]
    fn config_rejects_invalid_probability() {
        let _ = ChaosConfig::new(42).with_cancel_probability(1.5);
    }

    #[test]
    fn config_summary() {
        let config = ChaosConfig::new(42)
            .with_cancel_probability(0.1)
            .with_io_error_probability(0.05)
            .with_io_error_kinds(vec![std::io::ErrorKind::ConnectionReset]);
        let summary = config.summary();
        assert!(summary.contains("cancel:10.0%"));
        assert!(summary.contains("io_err:5.0%"));
    }

    #[test]
    fn rng_deterministic() {
        let config = ChaosConfig::new(42).with_cancel_probability(0.5);

        let mut rng1 = ChaosRng::from_config(&config);
        let mut rng2 = ChaosRng::from_config(&config);

        // Same seed produces same sequence
        for _ in 0..100 {
            assert_eq!(
                rng1.should_inject_cancel(&config),
                rng2.should_inject_cancel(&config)
            );
        }
    }

    #[test]
    fn rng_f64_range() {
        let mut rng = ChaosRng::new(42);
        for _ in 0..1000 {
            let val = rng.next_f64();
            assert!((0.0..1.0).contains(&val), "f64 out of range: {val}");
        }
    }

    #[test]
    fn rng_should_inject_bounds() {
        let mut rng = ChaosRng::new(42);

        // 0% probability never injects
        for _ in 0..100 {
            assert!(!rng.should_inject(0.0));
        }

        // 100% probability always injects
        for _ in 0..100 {
            assert!(rng.should_inject(1.0));
        }
    }

    #[test]
    fn rng_delay_generation() {
        let config = ChaosConfig::new(42)
            .with_delay_range(Duration::from_millis(10)..Duration::from_millis(100));

        let mut rng = config.rng();
        for _ in 0..100 {
            let delay = rng.next_delay(&config);
            assert!(delay >= Duration::from_millis(10));
            assert!(delay < Duration::from_millis(100));
        }
    }

    #[test]
    fn delay_probability_without_nonzero_delay_range_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_delay_probability(1.0)
            .with_delay_range(Duration::ZERO..Duration::ZERO);
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_delay(&config),
                "delay chaos without a nonzero delay range must never inject"
            );
            assert_eq!(rng.next_delay(&config), Duration::ZERO);
        }
    }

    #[test]
    fn delay_probability_with_reversed_range_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_delay_probability(1.0)
            .with_delay_range(Duration::from_millis(5)..Duration::from_millis(1));
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_delay(&config),
                "delay chaos with a reversed range must never inject"
            );
            assert_eq!(rng.next_delay(&config), Duration::ZERO);
        }
    }

    #[test]
    fn delay_probability_with_empty_positive_range_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_delay_probability(1.0)
            .with_delay_range(Duration::from_millis(5)..Duration::from_millis(5));
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_delay(&config),
                "delay chaos with an empty range must never inject"
            );
            assert_eq!(rng.next_delay(&config), Duration::ZERO);
        }
    }

    #[test]
    fn rng_delay_generation_excludes_zero_when_positive_delays_are_possible() {
        let config = ChaosConfig::new(42).with_delay_range(Duration::ZERO..Duration::from_nanos(3));

        let mut rng = config.rng();
        for _ in 0..64 {
            let delay = rng.next_delay(&config);
            assert!(
                delay >= Duration::from_nanos(1),
                "delay {delay:?} should exclude zero when positive delays are possible"
            );
            assert!(
                delay < Duration::from_nanos(3),
                "delay {delay:?} should stay within configured range"
            );
        }
    }

    #[test]
    fn rng_delay_generation_handles_large_duration_ranges() {
        let start = Duration::from_secs(40_000_000_000);
        let end = start + Duration::from_secs(100);
        let config = ChaosConfig::new(42).with_delay_range(start..end);

        let mut rng = config.rng();
        for _ in 0..100 {
            let delay = rng.next_delay(&config);
            assert!(
                delay >= start,
                "delay {delay:?} should be >= start {start:?}"
            );
            assert!(delay < end, "delay {delay:?} should be < end {end:?}");
        }
    }

    #[test]
    fn rng_io_error_kind() {
        let config = ChaosConfig::new(42).with_io_error_kinds(vec![
            io::ErrorKind::ConnectionReset,
            io::ErrorKind::TimedOut,
        ]);

        let mut rng = config.rng();
        for _ in 0..100 {
            let kind = rng.next_io_error_kind(&config).unwrap();
            assert!(
                kind == io::ErrorKind::ConnectionReset || kind == io::ErrorKind::TimedOut,
                "Unexpected error kind: {kind:?}"
            );
        }
    }

    #[test]
    fn io_error_probability_without_kinds_is_effectively_disabled() {
        let config = ChaosConfig::new(42).with_io_error_probability(1.0);
        assert!(
            !config.is_enabled(),
            "io-error chaos without error kinds should not report enabled"
        );
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_io_error(&config),
                "io-error chaos without kinds must never inject"
            );
            assert!(
                rng.next_io_error_kind(&config).is_none(),
                "io-error chaos without kinds must not fabricate an error kind"
            );
        }
    }

    #[test]
    fn rng_wakeup_count() {
        let config = ChaosConfig::new(42).with_wakeup_storm_count(5..15);

        let mut rng = config.rng();
        for _ in 0..100 {
            let count = rng.next_wakeup_count(&config);
            assert!((5..15).contains(&count), "Count out of range: {count}");
        }
    }

    #[test]
    fn wakeup_probability_without_positive_count_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_wakeup_storm_probability(1.0)
            .with_wakeup_storm_count(0..1);
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_wakeup_storm(&config),
                "wakeup storms without positive wake counts must never inject"
            );
            assert_eq!(rng.next_wakeup_count(&config), 0);
        }
    }

    #[test]
    #[allow(clippy::reversed_empty_ranges)]
    fn wakeup_probability_with_reversed_count_range_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_wakeup_storm_probability(1.0)
            .with_wakeup_storm_count(5..1);
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_wakeup_storm(&config),
                "wakeup storms with a reversed count range must never inject"
            );
            assert_eq!(rng.next_wakeup_count(&config), 0);
        }
    }

    #[test]
    fn wakeup_probability_with_empty_positive_count_range_is_effectively_disabled() {
        let config = ChaosConfig::new(42)
            .with_wakeup_storm_probability(1.0)
            .with_wakeup_storm_count(5..5);
        assert!(!config.is_enabled());
        assert_eq!(config.summary(), "off");

        let mut rng = config.rng();
        for _ in 0..32 {
            assert!(
                !rng.should_inject_wakeup_storm(&config),
                "wakeup storms with an empty count range must never inject"
            );
            assert_eq!(rng.next_wakeup_count(&config), 0);
        }
    }

    #[test]
    fn rng_wakeup_count_excludes_zero_when_positive_counts_are_possible() {
        let config = ChaosConfig::new(42).with_wakeup_storm_count(0..3);

        let mut rng = config.rng();
        for _ in 0..64 {
            let count = rng.next_wakeup_count(&config);
            assert!(count > 0, "wakeup storm count should exclude zero");
            assert!(count < 3, "count should stay within configured range");
        }
    }

    #[test]
    fn injection_point_applicable_chaos() {
        // TaskPoll can inject Cancel, Delay, and BudgetExhaust
        let applicable = InjectionPoint::TaskPoll.applicable_chaos();
        assert!(applicable.contains(&ChaosType::Cancel));
        assert!(applicable.contains(&ChaosType::Delay));
        assert!(applicable.contains(&ChaosType::BudgetExhaust));
        assert!(!applicable.contains(&ChaosType::IoError));

        // ReactorPoll can inject IoError and Delay
        let applicable = InjectionPoint::ReactorPoll.applicable_chaos();
        assert!(applicable.contains(&ChaosType::IoError));
        assert!(applicable.contains(&ChaosType::Delay));
        assert!(!applicable.contains(&ChaosType::Cancel));
    }

    #[test]
    fn stats_tracking() {
        let mut stats = ChaosStats::new();
        stats.record_cancel();
        stats.record_delay(Duration::from_millis(10));
        stats.record_io_error();
        stats.record_wakeup_storm(5);
        stats.record_budget_exhaust();
        stats.record_no_injection();
        stats.record_no_injection();

        assert_eq!(stats.cancellations, 1);
        assert_eq!(stats.delays, 1);
        assert_eq!(stats.total_delay, Duration::from_millis(10));
        assert_eq!(stats.io_errors, 1);
        assert_eq!(stats.wakeup_storms, 1);
        assert_eq!(stats.spurious_wakeups, 5);
        assert_eq!(stats.budget_exhaustions, 1);
        assert_eq!(stats.decision_points, 7);

        // 5 injections out of 7 decision points
        let rate = stats.injection_rate();
        assert!((rate - 5.0 / 7.0).abs() < 0.001);
    }

    #[test]
    fn stats_merge() {
        let mut stats1 = ChaosStats::new();
        stats1.record_cancel();
        stats1.record_cancel();

        let mut stats2 = ChaosStats::new();
        stats2.record_io_error();
        stats2.record_delay(Duration::from_millis(5));

        stats1.merge(&stats2);

        assert_eq!(stats1.cancellations, 2);
        assert_eq!(stats1.io_errors, 1);
        assert_eq!(stats1.delays, 1);
        assert_eq!(stats1.decision_points, 4);
    }

    #[test]
    fn pre_poll_outcomes_count_as_one_decision_point() {
        let mut stats = ChaosStats::new();
        stats.record_pre_poll_outcomes(true, Some(Duration::from_millis(2)), true);

        assert_eq!(stats.cancellations, 1);
        assert_eq!(stats.delays, 1);
        assert_eq!(stats.total_delay, Duration::from_millis(2));
        assert_eq!(stats.budget_exhaustions, 1);
        assert_eq!(stats.decision_points, 1);
    }

    #[test]
    fn stats_display() {
        let mut stats = ChaosStats::new();
        stats.record_cancel();
        stats.record_delay(Duration::from_millis(10));

        let display = format!("{stats}");
        assert!(display.contains("cancels: 1"));
        assert!(display.contains("delays: 1"));
    }

    // =========================================================================
    // Wave 48 – pure data-type trait coverage
    // =========================================================================

    #[test]
    fn injection_point_debug_clone_copy_eq_hash() {
        use std::collections::HashSet;
        let all = [
            InjectionPoint::SchedulerPoll,
            InjectionPoint::TaskPoll,
            InjectionPoint::ReactorPoll,
            InjectionPoint::WakerInvoke,
            InjectionPoint::BudgetCheck,
            InjectionPoint::TimerFire,
            InjectionPoint::SyncAcquire,
            InjectionPoint::ChannelSend,
            InjectionPoint::ChannelRecv,
        ];
        let mut set = HashSet::new();
        for ip in &all {
            let copied = *ip;
            let cloned = *ip;
            assert_eq!(copied, cloned);
            assert!(!format!("{ip:?}").is_empty());
            set.insert(*ip);
        }
        assert_eq!(set.len(), 9);
    }

    #[test]
    fn chaos_type_debug_clone_copy_eq_hash() {
        use std::collections::HashSet;
        let all = [
            ChaosType::Cancel,
            ChaosType::Delay,
            ChaosType::IoError,
            ChaosType::WakeupStorm,
            ChaosType::BudgetExhaust,
        ];
        let mut set = HashSet::new();
        for ct in &all {
            let copied = *ct;
            let cloned = *ct;
            assert_eq!(copied, cloned);
            set.insert(*ct);
        }
        assert_eq!(set.len(), 5);
        assert_ne!(ChaosType::Cancel, ChaosType::Delay);
    }

    #[test]
    fn chaos_stats_debug_clone_default() {
        let def = ChaosStats::default();
        assert_eq!(def.cancellations, 0);
        assert_eq!(def.delays, 0);
        assert_eq!(def.io_errors, 0);
        let dbg = format!("{def:?}");
        assert!(dbg.contains("ChaosStats"), "{dbg}");
        let cloned = def;
        assert_eq!(cloned.cancellations, 0);
    }
}