Skip to main content

asupersync/lab/
config.rs

1//! Configuration for the lab runtime.
2//!
3//! The lab configuration controls deterministic execution:
4//! - Random seed for scheduling decisions
5//! - Entropy seed for capability-based randomness
6//! - Whether to panic on obligation leaks
7//! - Trace buffer size
8//! - Futurelock detection settings
9//! - Chaos injection settings
10//!
11//! # Basic Usage
12//!
13//! ```ignore
14//! use asupersync::lab::{LabConfig, LabRuntime};
15//!
16//! // Default configuration (seed=42)
17//! let config = LabConfig::default();
18//!
19//! // Explicit seed for reproducibility
20//! let config = LabConfig::new(12345);
21//!
22//! // Time-based seed for variety (useful in CI)
23//! let config = LabConfig::from_time();
24//! ```
25//!
26//! # Chaos Testing
27//!
28//! Enable chaos injection to stress-test error handling paths:
29//!
30//! ```ignore
31//! use asupersync::lab::{LabConfig, LabRuntime};
32//! use asupersync::lab::chaos::ChaosConfig;
33//!
34//! // Quick: use presets
35//! let config = LabConfig::new(42).with_light_chaos();  // CI-friendly
36//! let config = LabConfig::new(42).with_heavy_chaos();  // Thorough
37//!
38//! // Custom: fine-grained control
39//! let chaos = ChaosConfig::new(42)
40//!     .with_delay_probability(0.3)
41//!     .with_cancel_probability(0.05);
42//! let config = LabConfig::new(42).with_chaos(chaos);
43//!
44//! // Check if chaos is enabled
45//! assert!(config.has_chaos());
46//! ```
47//!
48//! # Futurelock Detection
49//!
50//! Detect tasks that hold obligations but stop being polled:
51//!
52//! ```ignore
53//! let config = LabConfig::new(42)
54//!     .futurelock_max_idle_steps(5000)  // Trigger after 5000 idle steps
55//!     .panic_on_futurelock(true);       // Panic when detected
56//! ```
57//!
58//! # Builder Style
59//!
60//! `LabConfig` uses a fluent, move-based builder style. Each method consumes
61//! `self` and returns an updated configuration so you can chain options safely.
62//!
63//! # Configuration Examples
64//!
65//! ## Deterministic Multi-Worker Simulation
66//!
67//! ```ignore
68//! use asupersync::lab::{LabConfig, LabRuntime};
69//!
70//! let config = LabConfig::new(7)
71//!     .worker_count(4)
72//!     .trace_capacity(16_384);
73//! let mut lab = LabRuntime::new(config);
74//! lab.run_until_quiescent();
75//! ```
76//!
77//! ## Replay Capture for Debugging
78//!
79//! ```ignore
80//! use asupersync::lab::{LabConfig, LabRuntime};
81//!
82//! let config = LabConfig::new(42).with_default_replay_recording();
83//! let mut lab = LabRuntime::new(config);
84//! lab.run_until_quiescent();
85//! ```
86//!
87//! ## Entropy Decoupling
88//!
89//! ```ignore
90//! use asupersync::lab::LabConfig;
91//!
92//! // Keep scheduling deterministic but vary entropy-derived behavior.
93//! let config = LabConfig::new(42).entropy_seed(7);
94//! ```
95//!
96//! # Migration Guide (Struct Updates → Builder Style)
97//!
98//! ```ignore
99//! use asupersync::lab::LabConfig;
100//!
101//! // Old style: struct update
102//! let config = LabConfig {
103//!     seed: 42,
104//!     worker_count: 4,
105//!     ..LabConfig::new(42)
106//! };
107//!
108//! // New style: builder methods
109//! let config = LabConfig::new(42).worker_count(4);
110//! ```
111
112use crate::lab::chaos::ChaosConfig;
113use crate::trace::RecorderConfig;
114use crate::util::DetRng;
115
116/// Configuration for the lab runtime.
117#[derive(Debug, Clone)]
118#[allow(clippy::struct_excessive_bools)]
119pub struct LabConfig {
120    /// Random seed for deterministic scheduling.
121    pub seed: u64,
122    /// Seed for deterministic entropy sources.
123    ///
124    /// By default this matches `seed`, but can be overridden to decouple
125    /// scheduler decisions from entropy generation.
126    pub entropy_seed: u64,
127    /// Number of virtual workers to model in the lab scheduler.
128    ///
129    /// This does not spawn threads; it controls deterministic multi-worker simulation.
130    /// Values less than 1 are clamped to 1.
131    pub worker_count: usize,
132    /// Whether to panic on obligation leaks.
133    pub panic_on_obligation_leak: bool,
134    /// Trace buffer capacity.
135    pub trace_capacity: usize,
136    /// Max lab steps a task may go unpolled while holding obligations.
137    ///
138    /// `0` disables the futurelock detector.
139    pub futurelock_max_idle_steps: u64,
140    /// Whether to panic when a futurelock is detected.
141    pub panic_on_futurelock: bool,
142    /// Maximum number of steps before forced termination.
143    pub max_steps: Option<u64>,
144    /// Chaos injection configuration.
145    ///
146    /// When enabled, the runtime will inject faults at various points
147    /// to stress-test the system's resilience.
148    pub chaos: Option<ChaosConfig>,
149    /// Replay recording configuration.
150    ///
151    /// When enabled, the runtime will record all non-determinism sources
152    /// for later replay.
153    pub replay_recording: Option<RecorderConfig>,
154    /// When true, the runtime auto-advances virtual time to the next timer
155    /// deadline whenever all tasks are idle (no runnable tasks in scheduler).
156    ///
157    /// This enables "instant timeout testing" — a 24-hour wall-clock scenario
158    /// completes in <1 second of real time because sleep/timeout deadlines
159    /// are jumped to instantly rather than waited for.
160    pub auto_advance_time: bool,
161    /// Whether to enable real-time cancellation protocol oracle verification.
162    ///
163    /// When enabled, the runtime will continuously verify that the cancellation
164    /// protocol is followed correctly during execution.
165    pub enable_cancellation_oracle: bool,
166    /// Whether to panic when cancellation protocol violations are detected.
167    ///
168    /// When false, violations are logged as warnings instead of panicking.
169    pub panic_on_cancellation_violation: bool,
170}
171
172impl LabConfig {
173    /// Creates a new lab configuration with the given seed.
174    #[must_use]
175    pub const fn new(seed: u64) -> Self {
176        Self {
177            seed,
178            entropy_seed: seed,
179            worker_count: 1,
180            panic_on_obligation_leak: true,
181            trace_capacity: 4096,
182            futurelock_max_idle_steps: 10_000,
183            panic_on_futurelock: true,
184            max_steps: Some(100_000),
185            chaos: None,
186            replay_recording: None,
187            auto_advance_time: false,
188            enable_cancellation_oracle: true,
189            panic_on_cancellation_violation: true,
190        }
191    }
192
193    /// Creates a lab configuration from the current time (for quick testing).
194    #[must_use]
195    pub fn from_time() -> Self {
196        use std::time::{SystemTime, UNIX_EPOCH};
197        let seed = SystemTime::now()
198            .duration_since(UNIX_EPOCH)
199            .map_or(42, |d| d.as_nanos().min(u128::from(u64::MAX)) as u64);
200        Self::new(seed)
201    }
202
203    /// Sets whether to panic on obligation leaks.
204    #[must_use]
205    pub const fn panic_on_leak(mut self, value: bool) -> Self {
206        self.panic_on_obligation_leak = value;
207        self
208    }
209
210    /// Sets the trace buffer capacity.
211    #[must_use]
212    pub const fn trace_capacity(mut self, capacity: usize) -> Self {
213        self.trace_capacity = capacity;
214        self
215    }
216
217    /// Sets the number of virtual workers to model.
218    ///
219    /// Values less than 1 are clamped to 1.
220    #[must_use]
221    pub const fn worker_count(mut self, count: usize) -> Self {
222        self.worker_count = if count == 0 { 1 } else { count };
223        self
224    }
225
226    /// Sets the entropy seed used for capability-based randomness.
227    #[must_use]
228    pub const fn entropy_seed(mut self, seed: u64) -> Self {
229        self.entropy_seed = seed;
230        self
231    }
232
233    /// Sets the maximum idle steps before the futurelock detector triggers.
234    #[must_use]
235    pub const fn futurelock_max_idle_steps(mut self, steps: u64) -> Self {
236        self.futurelock_max_idle_steps = steps;
237        self
238    }
239
240    /// Sets whether to panic when a futurelock is detected.
241    #[must_use]
242    pub const fn panic_on_futurelock(mut self, value: bool) -> Self {
243        self.panic_on_futurelock = value;
244        self
245    }
246
247    /// Sets the maximum number of steps.
248    #[must_use]
249    pub const fn max_steps(mut self, steps: u64) -> Self {
250        self.max_steps = Some(steps);
251        self
252    }
253
254    /// Disables the step limit.
255    #[must_use]
256    pub const fn no_step_limit(mut self) -> Self {
257        self.max_steps = None;
258        self
259    }
260
261    /// Enables chaos injection with the given configuration.
262    ///
263    /// The chaos seed will be derived from the main seed for determinism.
264    #[must_use]
265    pub fn with_chaos(mut self, config: ChaosConfig) -> Self {
266        // Derive chaos seed from main seed for determinism
267        let chaos_seed = self.seed.wrapping_add(0xCAFE_BABE);
268        self.chaos = Some(config.with_seed(chaos_seed));
269        self
270    }
271
272    /// Enables light chaos (suitable for CI).
273    #[must_use]
274    pub fn with_light_chaos(self) -> Self {
275        self.with_chaos(ChaosConfig::light())
276    }
277
278    /// Enables heavy chaos (thorough testing).
279    #[must_use]
280    pub fn with_heavy_chaos(self) -> Self {
281        self.with_chaos(ChaosConfig::heavy())
282    }
283
284    /// Returns true if chaos injection is enabled.
285    #[must_use]
286    pub fn has_chaos(&self) -> bool {
287        self.chaos.as_ref().is_some_and(ChaosConfig::is_enabled)
288    }
289
290    /// Enables replay recording with the given configuration.
291    #[must_use]
292    pub fn with_replay_recording(mut self, config: RecorderConfig) -> Self {
293        self.replay_recording = Some(config);
294        self
295    }
296
297    /// Enables replay recording with default configuration.
298    #[must_use]
299    pub fn with_default_replay_recording(self) -> Self {
300        self.with_replay_recording(RecorderConfig::enabled())
301    }
302
303    /// Enables automatic time advancement when all tasks are idle.
304    ///
305    /// When enabled, `run_with_auto_advance()` will jump virtual time to the
306    /// next timer deadline whenever the scheduler has no runnable tasks,
307    /// enabling instant timeout testing.
308    #[must_use]
309    pub const fn with_auto_advance(mut self) -> Self {
310        self.auto_advance_time = true;
311        self
312    }
313
314    /// Returns true if replay recording is enabled.
315    #[must_use]
316    pub fn has_replay_recording(&self) -> bool {
317        self.replay_recording.as_ref().is_some_and(|c| c.enabled)
318    }
319
320    /// Enables or disables real-time cancellation protocol oracle verification.
321    #[must_use]
322    pub const fn with_cancellation_oracle(mut self, enable: bool) -> Self {
323        self.enable_cancellation_oracle = enable;
324        self
325    }
326
327    /// Sets whether to panic on cancellation protocol violations.
328    ///
329    /// When false, violations are logged as warnings instead of panicking.
330    #[must_use]
331    pub const fn panic_on_cancellation_violation(mut self, value: bool) -> Self {
332        self.panic_on_cancellation_violation = value;
333        self
334    }
335
336    /// Enables cancellation oracle in warning mode (logs violations but doesn't panic).
337    #[must_use]
338    pub const fn with_cancellation_oracle_warnings(mut self) -> Self {
339        self.enable_cancellation_oracle = true;
340        self.panic_on_cancellation_violation = false;
341        self
342    }
343
344    /// Returns true if real-time cancellation protocol oracle verification is enabled.
345    #[must_use]
346    pub const fn has_cancellation_oracle(&self) -> bool {
347        self.enable_cancellation_oracle
348    }
349
350    /// Creates a deterministic RNG from this configuration.
351    #[must_use]
352    pub fn rng(&self) -> DetRng {
353        DetRng::new(self.seed)
354    }
355}
356
357impl Default for LabConfig {
358    fn default() -> Self {
359        Self::new(42)
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    fn init_test(name: &str) {
368        crate::test_utils::init_test_logging();
369        crate::test_phase!(name);
370    }
371
372    #[test]
373    fn default_config() {
374        init_test("default_config");
375        let config = LabConfig::default();
376        let ok = config.seed == 42;
377        crate::assert_with_log!(ok, "seed", 42, config.seed);
378        crate::assert_with_log!(
379            config.entropy_seed == 42,
380            "entropy_seed",
381            42,
382            config.entropy_seed
383        );
384        crate::assert_with_log!(
385            config.worker_count == 1,
386            "worker_count",
387            1,
388            config.worker_count
389        );
390        crate::assert_with_log!(
391            config.panic_on_obligation_leak,
392            "panic_on_obligation_leak",
393            true,
394            config.panic_on_obligation_leak
395        );
396        crate::assert_with_log!(
397            config.panic_on_futurelock,
398            "panic_on_futurelock",
399            true,
400            config.panic_on_futurelock
401        );
402        crate::test_complete!("default_config");
403    }
404
405    #[test]
406    fn rng_is_deterministic() {
407        init_test("rng_is_deterministic");
408        let config = LabConfig::new(12345);
409        let mut rng1 = config.rng();
410        let mut rng2 = config.rng();
411
412        let a = rng1.next_u64();
413        let b = rng2.next_u64();
414        crate::assert_with_log!(a == b, "rng equal", b, a);
415        crate::test_complete!("rng_is_deterministic");
416    }
417
418    #[test]
419    fn worker_count_clamps_to_one() {
420        init_test("worker_count_clamps_to_one");
421        let config = LabConfig::new(7).worker_count(0);
422        crate::assert_with_log!(
423            config.worker_count == 1,
424            "worker_count",
425            1,
426            config.worker_count
427        );
428        crate::test_complete!("worker_count_clamps_to_one");
429    }
430
431    #[test]
432    fn lab_config_debug() {
433        init_test("lab_config_debug");
434        let cfg = LabConfig::new(42);
435        let dbg = format!("{cfg:?}");
436        assert!(dbg.contains("LabConfig"));
437        crate::test_complete!("lab_config_debug");
438    }
439
440    #[test]
441    fn lab_config_clone() {
442        init_test("lab_config_clone");
443        let cfg = LabConfig::new(99).worker_count(3);
444        let cfg2 = cfg;
445        assert_eq!(cfg2.seed, 99);
446        assert_eq!(cfg2.worker_count, 3);
447        crate::test_complete!("lab_config_clone");
448    }
449
450    #[test]
451    fn new_sets_fields() {
452        init_test("new_sets_fields");
453        let cfg = LabConfig::new(123);
454        assert_eq!(cfg.seed, 123);
455        assert_eq!(cfg.entropy_seed, 123);
456        assert_eq!(cfg.worker_count, 1);
457        assert!(cfg.panic_on_obligation_leak);
458        assert_eq!(cfg.trace_capacity, 4096);
459        assert_eq!(cfg.futurelock_max_idle_steps, 10_000);
460        assert!(cfg.panic_on_futurelock);
461        assert_eq!(cfg.max_steps, Some(100_000));
462        assert!(cfg.chaos.is_none());
463        assert!(cfg.replay_recording.is_none());
464        assert!(!cfg.auto_advance_time);
465        crate::test_complete!("new_sets_fields");
466    }
467
468    #[test]
469    fn from_time_creates_valid_config() {
470        init_test("from_time_creates_valid_config");
471        let cfg = LabConfig::from_time();
472        // seed should be set from system time (non-deterministic but valid)
473        assert_eq!(cfg.entropy_seed, cfg.seed);
474        assert_eq!(cfg.worker_count, 1);
475        crate::test_complete!("from_time_creates_valid_config");
476    }
477
478    #[test]
479    fn panic_on_leak_builder() {
480        init_test("panic_on_leak_builder");
481        let cfg = LabConfig::new(1).panic_on_leak(false);
482        assert!(!cfg.panic_on_obligation_leak);
483        let cfg = cfg.panic_on_leak(true);
484        assert!(cfg.panic_on_obligation_leak);
485        crate::test_complete!("panic_on_leak_builder");
486    }
487
488    #[test]
489    fn trace_capacity_builder() {
490        init_test("trace_capacity_builder");
491        let cfg = LabConfig::new(1).trace_capacity(8192);
492        assert_eq!(cfg.trace_capacity, 8192);
493        crate::test_complete!("trace_capacity_builder");
494    }
495
496    #[test]
497    fn entropy_seed_builder() {
498        init_test("entropy_seed_builder");
499        let cfg = LabConfig::new(42).entropy_seed(7);
500        assert_eq!(cfg.seed, 42);
501        assert_eq!(cfg.entropy_seed, 7);
502        crate::test_complete!("entropy_seed_builder");
503    }
504
505    #[test]
506    fn futurelock_max_idle_steps_builder() {
507        init_test("futurelock_max_idle_steps_builder");
508        let cfg = LabConfig::new(1).futurelock_max_idle_steps(5000);
509        assert_eq!(cfg.futurelock_max_idle_steps, 5000);
510        crate::test_complete!("futurelock_max_idle_steps_builder");
511    }
512
513    #[test]
514    fn panic_on_futurelock_builder() {
515        init_test("panic_on_futurelock_builder");
516        let cfg = LabConfig::new(1).panic_on_futurelock(false);
517        assert!(!cfg.panic_on_futurelock);
518        crate::test_complete!("panic_on_futurelock_builder");
519    }
520
521    #[test]
522    fn max_steps_builder() {
523        init_test("max_steps_builder");
524        let cfg = LabConfig::new(1).max_steps(500);
525        assert_eq!(cfg.max_steps, Some(500));
526        crate::test_complete!("max_steps_builder");
527    }
528
529    #[test]
530    fn no_step_limit_builder() {
531        init_test("no_step_limit_builder");
532        let cfg = LabConfig::new(1).no_step_limit();
533        assert_eq!(cfg.max_steps, None);
534        crate::test_complete!("no_step_limit_builder");
535    }
536
537    #[test]
538    fn with_auto_advance_builder() {
539        init_test("with_auto_advance_builder");
540        let cfg = LabConfig::new(1);
541        assert!(!cfg.auto_advance_time);
542        let cfg = cfg.with_auto_advance();
543        assert!(cfg.auto_advance_time);
544        crate::test_complete!("with_auto_advance_builder");
545    }
546
547    #[test]
548    fn has_chaos_false_by_default() {
549        init_test("has_chaos_false_by_default");
550        let cfg = LabConfig::new(1);
551        assert!(!cfg.has_chaos());
552        crate::test_complete!("has_chaos_false_by_default");
553    }
554
555    #[test]
556    fn with_light_chaos_enables() {
557        init_test("with_light_chaos_enables");
558        let cfg = LabConfig::new(1).with_light_chaos();
559        assert!(cfg.has_chaos());
560        assert!(cfg.chaos.is_some());
561        crate::test_complete!("with_light_chaos_enables");
562    }
563
564    #[test]
565    fn with_heavy_chaos_enables() {
566        init_test("with_heavy_chaos_enables");
567        let cfg = LabConfig::new(1).with_heavy_chaos();
568        assert!(cfg.has_chaos());
569        crate::test_complete!("with_heavy_chaos_enables");
570    }
571
572    #[test]
573    fn has_replay_recording_false_by_default() {
574        init_test("has_replay_recording_false_by_default");
575        let cfg = LabConfig::new(1);
576        assert!(!cfg.has_replay_recording());
577        crate::test_complete!("has_replay_recording_false_by_default");
578    }
579
580    #[test]
581    fn with_default_replay_recording_enables() {
582        init_test("with_default_replay_recording_enables");
583        let cfg = LabConfig::new(1).with_default_replay_recording();
584        assert!(cfg.has_replay_recording());
585        assert!(cfg.replay_recording.is_some());
586        crate::test_complete!("with_default_replay_recording_enables");
587    }
588
589    #[test]
590    fn builder_chaining() {
591        init_test("builder_chaining");
592        let cfg = LabConfig::new(99)
593            .worker_count(4)
594            .entropy_seed(7)
595            .trace_capacity(2048)
596            .panic_on_leak(false)
597            .futurelock_max_idle_steps(3000)
598            .panic_on_futurelock(false)
599            .max_steps(5000)
600            .with_auto_advance();
601        assert_eq!(cfg.seed, 99);
602        assert_eq!(cfg.worker_count, 4);
603        assert_eq!(cfg.entropy_seed, 7);
604        assert_eq!(cfg.trace_capacity, 2048);
605        assert!(!cfg.panic_on_obligation_leak);
606        assert_eq!(cfg.futurelock_max_idle_steps, 3000);
607        assert!(!cfg.panic_on_futurelock);
608        assert_eq!(cfg.max_steps, Some(5000));
609        assert!(cfg.auto_advance_time);
610        crate::test_complete!("builder_chaining");
611    }
612
613    #[test]
614    fn worker_count_positive_value() {
615        init_test("worker_count_positive_value");
616        let cfg = LabConfig::new(1).worker_count(8);
617        assert_eq!(cfg.worker_count, 8);
618        crate::test_complete!("worker_count_positive_value");
619    }
620
621    #[test]
622    fn with_chaos_derives_seed() {
623        init_test("with_chaos_derives_seed");
624        let cfg = LabConfig::new(42).with_chaos(ChaosConfig::light());
625        let chaos = cfg.chaos.as_ref().unwrap();
626        // Chaos seed derived from main seed + 0xCAFE_BABE
627        let dbg = format!("{chaos:?}");
628        assert!(!dbg.is_empty());
629        crate::test_complete!("with_chaos_derives_seed");
630    }
631}