umi_memory/dst/
property.rs

1//! Property-Based Testing for DST
2//!
3//! TigerStyle: Random operation sequences with invariant checking.
4//!
5//! # Philosophy
6//!
7//! Property-based testing generates random operations and verifies that
8//! invariants hold after each operation. Combined with DST, this gives:
9//! - Deterministic reproduction via seed
10//! - Time control via SimClock
11//! - Fault injection via FaultInjector
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use umi_memory::dst::{PropertyTest, PropertyTestable, SimClock, DeterministicRng};
17//!
18//! struct Counter { value: i64, min: i64, max: i64 }
19//!
20//! #[derive(Debug, Clone)]
21//! enum CounterOp { Increment(i64), Decrement(i64), Reset }
22//!
23//! impl PropertyTestable for Counter {
24//!     type Operation = CounterOp;
25//!
26//!     fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation {
27//!         match rng.next_usize(0, 3) {
28//!             0 => CounterOp::Increment(rng.next_usize(1, 10) as i64),
29//!             1 => CounterOp::Decrement(rng.next_usize(1, 10) as i64),
30//!             _ => CounterOp::Reset,
31//!         }
32//!     }
33//!
34//!     fn apply_operation(&mut self, op: &Self::Operation, _clock: &SimClock) {
35//!         match op {
36//!             CounterOp::Increment(n) => self.value = (self.value + n).min(self.max),
37//!             CounterOp::Decrement(n) => self.value = (self.value - n).max(self.min),
38//!             CounterOp::Reset => self.value = 0,
39//!         }
40//!     }
41//!
42//!     fn check_invariants(&self) -> Result<(), String> {
43//!         if self.value < self.min {
44//!             return Err(format!("value {} below min {}", self.value, self.min));
45//!         }
46//!         if self.value > self.max {
47//!             return Err(format!("value {} above max {}", self.value, self.max));
48//!         }
49//!         Ok(())
50//!     }
51//! }
52//!
53//! #[test]
54//! fn test_counter_properties() {
55//!     let counter = Counter { value: 0, min: -100, max: 100 };
56//!     let result = PropertyTest::new(42)
57//!         .with_max_operations(1000)
58//!         .run(counter);
59//!     assert!(result.is_success());
60//! }
61//! ```
62
63use std::fmt::Debug;
64
65use super::clock::SimClock;
66use super::rng::DeterministicRng;
67use crate::constants::DST_SIMULATION_STEPS_MAX;
68
69/// Trait for systems that can be property-tested.
70///
71/// TigerStyle: Explicit operation generation and invariant checking.
72pub trait PropertyTestable {
73    /// The type of operations that can be performed.
74    type Operation: Debug + Clone;
75
76    /// Generate a random operation based on current state.
77    ///
78    /// The operation should be valid for the current state.
79    fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation;
80
81    /// Apply an operation to the state.
82    ///
83    /// May use the clock for time-dependent operations.
84    fn apply_operation(&mut self, op: &Self::Operation, clock: &SimClock);
85
86    /// Check that all invariants hold.
87    ///
88    /// Returns Ok(()) if all invariants pass, Err(message) otherwise.
89    fn check_invariants(&self) -> Result<(), String>;
90
91    /// Optional: Describe the current state for debugging.
92    fn describe_state(&self) -> String {
93        String::from("(state description not implemented)")
94    }
95}
96
97/// Result of a property test run.
98#[derive(Debug)]
99pub struct PropertyTestResult {
100    /// Number of operations successfully executed
101    pub operations_executed: u64,
102    /// Seed used for reproduction
103    pub seed: u64,
104    /// Failure details, if any
105    pub failure: Option<PropertyTestFailure>,
106}
107
108impl PropertyTestResult {
109    /// Check if the test passed.
110    #[must_use]
111    pub fn is_success(&self) -> bool {
112        self.failure.is_none()
113    }
114
115    /// Check if the test failed.
116    #[must_use]
117    pub fn is_failure(&self) -> bool {
118        self.failure.is_some()
119    }
120
121    /// Unwrap the result, panicking with details if failed.
122    ///
123    /// # Panics
124    /// Panics if the test failed, with reproduction info.
125    pub fn unwrap(self) {
126        if let Some(failure) = self.failure {
127            panic!(
128                "Property test failed!\n\
129                 Seed: {} (use this to reproduce)\n\
130                 Operation #{}: {:?}\n\
131                 Invariant violation: {}\n\
132                 State: {}",
133                self.seed,
134                failure.operation_index,
135                failure.operation,
136                failure.message,
137                failure.state_description
138            );
139        }
140    }
141}
142
143/// Details of a property test failure.
144#[derive(Debug)]
145pub struct PropertyTestFailure {
146    /// Index of the failing operation (0-based)
147    pub operation_index: u64,
148    /// The operation that caused the failure
149    pub operation: String,
150    /// The invariant violation message
151    pub message: String,
152    /// Description of the state at failure
153    pub state_description: String,
154}
155
156/// Configuration for time advancement during property tests.
157#[derive(Debug, Clone)]
158pub struct TimeAdvanceConfig {
159    /// Minimum time to advance per operation (ms)
160    pub min_ms: u64,
161    /// Maximum time to advance per operation (ms)
162    pub max_ms: u64,
163    /// Probability of advancing time (0.0 to 1.0)
164    pub probability: f64,
165}
166
167impl Default for TimeAdvanceConfig {
168    fn default() -> Self {
169        Self {
170            min_ms: 0,
171            max_ms: 1000,
172            probability: 0.5,
173        }
174    }
175}
176
177impl TimeAdvanceConfig {
178    /// No time advancement.
179    #[must_use]
180    pub fn none() -> Self {
181        Self {
182            min_ms: 0,
183            max_ms: 0,
184            probability: 0.0,
185        }
186    }
187
188    /// Always advance by fixed amount.
189    #[must_use]
190    pub fn fixed(ms: u64) -> Self {
191        Self {
192            min_ms: ms,
193            max_ms: ms,
194            probability: 1.0,
195        }
196    }
197
198    /// Advance with given range and probability.
199    #[must_use]
200    pub fn random(min_ms: u64, max_ms: u64, probability: f64) -> Self {
201        assert!(probability >= 0.0 && probability <= 1.0);
202        assert!(min_ms <= max_ms);
203        Self {
204            min_ms,
205            max_ms,
206            probability,
207        }
208    }
209}
210
211/// Property-based test runner.
212///
213/// TigerStyle:
214/// - Deterministic via seed
215/// - Explicit operation count limits
216/// - Invariant checking after each operation
217/// - Time advancement control
218#[derive(Debug)]
219pub struct PropertyTest {
220    seed: u64,
221    max_operations: u64,
222    time_config: TimeAdvanceConfig,
223    check_invariants_before: bool,
224}
225
226impl PropertyTest {
227    /// Create a new property test with the given seed.
228    ///
229    /// # Panics
230    /// Panics if max_operations would exceed DST_SIMULATION_STEPS_MAX.
231    #[must_use]
232    pub fn new(seed: u64) -> Self {
233        Self {
234            seed,
235            max_operations: 100, // Sensible default
236            time_config: TimeAdvanceConfig::default(),
237            check_invariants_before: true,
238        }
239    }
240
241    /// Set the maximum number of operations to run.
242    ///
243    /// # Panics
244    /// Panics if max exceeds DST_SIMULATION_STEPS_MAX.
245    #[must_use]
246    pub fn with_max_operations(mut self, max: u64) -> Self {
247        assert!(
248            max <= DST_SIMULATION_STEPS_MAX,
249            "max_operations {} exceeds DST_SIMULATION_STEPS_MAX {}",
250            max,
251            DST_SIMULATION_STEPS_MAX
252        );
253        self.max_operations = max;
254        self
255    }
256
257    /// Configure time advancement between operations.
258    #[must_use]
259    pub fn with_time_advance(mut self, config: TimeAdvanceConfig) -> Self {
260        self.time_config = config;
261        self
262    }
263
264    /// Disable checking invariants before the first operation.
265    #[must_use]
266    pub fn skip_initial_invariant_check(mut self) -> Self {
267        self.check_invariants_before = false;
268        self
269    }
270
271    /// Run the property test.
272    ///
273    /// Generates random operations, applies them, and checks invariants
274    /// after each operation. Returns detailed results.
275    #[must_use]
276    pub fn run<T: PropertyTestable>(self, mut state: T) -> PropertyTestResult {
277        let mut rng = DeterministicRng::new(self.seed);
278        let clock = SimClock::new();
279
280        // Check initial invariants
281        if self.check_invariants_before {
282            if let Err(msg) = state.check_invariants() {
283                return PropertyTestResult {
284                    operations_executed: 0,
285                    seed: self.seed,
286                    failure: Some(PropertyTestFailure {
287                        operation_index: 0,
288                        operation: "(initial state)".to_string(),
289                        message: format!("Initial state violates invariants: {}", msg),
290                        state_description: state.describe_state(),
291                    }),
292                };
293            }
294        }
295
296        for i in 0..self.max_operations {
297            // Maybe advance time
298            if self.time_config.probability > 0.0 && rng.next_bool(self.time_config.probability) {
299                let advance = if self.time_config.min_ms == self.time_config.max_ms {
300                    self.time_config.min_ms
301                } else {
302                    rng.next_usize(
303                        self.time_config.min_ms as usize,
304                        self.time_config.max_ms as usize,
305                    ) as u64
306                };
307                clock.advance_ms(advance);
308            }
309
310            // Generate and apply operation
311            let op = state.generate_operation(&mut rng);
312            let op_debug = format!("{:?}", op);
313            state.apply_operation(&op, &clock);
314
315            // Check invariants
316            if let Err(msg) = state.check_invariants() {
317                return PropertyTestResult {
318                    operations_executed: i + 1,
319                    seed: self.seed,
320                    failure: Some(PropertyTestFailure {
321                        operation_index: i,
322                        operation: op_debug,
323                        message: msg,
324                        state_description: state.describe_state(),
325                    }),
326                };
327            }
328        }
329
330        PropertyTestResult {
331            operations_executed: self.max_operations,
332            seed: self.seed,
333            failure: None,
334        }
335    }
336
337    /// Run the property test, panicking on failure.
338    ///
339    /// Convenience method for use in #[test] functions.
340    ///
341    /// # Panics
342    /// Panics if any invariant is violated.
343    pub fn run_and_assert<T: PropertyTestable>(self, state: T) {
344        self.run(state).unwrap();
345    }
346}
347
348/// Run multiple property tests with different seeds.
349///
350/// TigerStyle: Multi-seed testing for broader coverage.
351///
352/// # Panics
353/// Panics if any test fails.
354pub fn run_property_tests<T, F>(seeds: &[u64], max_operations: u64, state_factory: F)
355where
356    T: PropertyTestable,
357    F: Fn() -> T,
358{
359    for &seed in seeds {
360        let state = state_factory();
361        PropertyTest::new(seed)
362            .with_max_operations(max_operations)
363            .run_and_assert(state);
364    }
365}
366
367/// Generate a set of test seeds including edge cases.
368///
369/// Returns seeds: [0, 1, 42, random, random, ...]
370#[must_use]
371pub fn test_seeds(count: usize) -> Vec<u64> {
372    assert!(count >= 3, "need at least 3 seeds for edge cases");
373
374    let mut seeds = vec![0, 1, 42]; // Edge cases + common test seed
375
376    // Add random seeds
377    let time_seed = std::time::SystemTime::now()
378        .duration_since(std::time::UNIX_EPOCH)
379        .map(|d| d.as_nanos() as u64)
380        .unwrap_or(12345);
381    let mut rng = DeterministicRng::new(time_seed);
382
383    while seeds.len() < count {
384        // Generate u64 from two usize values
385        let high = rng.next_usize(0, u32::MAX as usize) as u64;
386        let low = rng.next_usize(0, u32::MAX as usize) as u64;
387        seeds.push((high << 32) | low);
388    }
389
390    seeds
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    /// Simple counter for testing the property test framework.
398    struct BoundedCounter {
399        value: i64,
400        min: i64,
401        max: i64,
402    }
403
404    #[derive(Debug, Clone)]
405    enum CounterOp {
406        Increment(i64),
407        Decrement(i64),
408        Reset,
409    }
410
411    impl PropertyTestable for BoundedCounter {
412        type Operation = CounterOp;
413
414        fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation {
415            match rng.next_usize(0, 3) {
416                0 => CounterOp::Increment(rng.next_usize(1, 20) as i64),
417                1 => CounterOp::Decrement(rng.next_usize(1, 20) as i64),
418                _ => CounterOp::Reset,
419            }
420        }
421
422        fn apply_operation(&mut self, op: &Self::Operation, _clock: &SimClock) {
423            match op {
424                CounterOp::Increment(n) => {
425                    self.value = (self.value + n).min(self.max);
426                }
427                CounterOp::Decrement(n) => {
428                    self.value = (self.value - n).max(self.min);
429                }
430                CounterOp::Reset => {
431                    self.value = 0;
432                }
433            }
434        }
435
436        fn check_invariants(&self) -> Result<(), String> {
437            if self.value < self.min {
438                return Err(format!("value {} below min {}", self.value, self.min));
439            }
440            if self.value > self.max {
441                return Err(format!("value {} above max {}", self.value, self.max));
442            }
443            Ok(())
444        }
445
446        fn describe_state(&self) -> String {
447            format!(
448                "BoundedCounter {{ value: {}, min: {}, max: {} }}",
449                self.value, self.min, self.max
450            )
451        }
452    }
453
454    #[test]
455    fn test_property_test_success() {
456        let counter = BoundedCounter {
457            value: 0,
458            min: -100,
459            max: 100,
460        };
461
462        let result = PropertyTest::new(42)
463            .with_max_operations(1000)
464            .with_time_advance(TimeAdvanceConfig::none())
465            .run(counter);
466
467        assert!(result.is_success());
468        assert_eq!(result.operations_executed, 1000);
469        assert_eq!(result.seed, 42);
470    }
471
472    #[test]
473    fn test_property_test_determinism() {
474        // Same seed should produce same results
475        let run1 = PropertyTest::new(12345)
476            .with_max_operations(100)
477            .run(BoundedCounter {
478                value: 0,
479                min: -50,
480                max: 50,
481            });
482
483        let run2 = PropertyTest::new(12345)
484            .with_max_operations(100)
485            .run(BoundedCounter {
486                value: 0,
487                min: -50,
488                max: 50,
489            });
490
491        assert_eq!(run1.operations_executed, run2.operations_executed);
492        assert_eq!(run1.is_success(), run2.is_success());
493    }
494
495    /// Buggy counter that doesn't clamp properly - should fail.
496    struct BuggyCounter {
497        value: i64,
498        max: i64,
499    }
500
501    #[derive(Debug, Clone)]
502    enum BuggyOp {
503        Add(i64),
504    }
505
506    impl PropertyTestable for BuggyCounter {
507        type Operation = BuggyOp;
508
509        fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation {
510            BuggyOp::Add(rng.next_usize(1, 50) as i64)
511        }
512
513        fn apply_operation(&mut self, op: &Self::Operation, _clock: &SimClock) {
514            match op {
515                BuggyOp::Add(n) => {
516                    // Bug: doesn't clamp to max!
517                    self.value += n;
518                }
519            }
520        }
521
522        fn check_invariants(&self) -> Result<(), String> {
523            if self.value > self.max {
524                return Err(format!("value {} exceeds max {}", self.value, self.max));
525            }
526            Ok(())
527        }
528
529        fn describe_state(&self) -> String {
530            format!(
531                "BuggyCounter {{ value: {}, max: {} }}",
532                self.value, self.max
533            )
534        }
535    }
536
537    #[test]
538    fn test_property_test_catches_bug() {
539        let counter = BuggyCounter { value: 0, max: 100 };
540
541        let result = PropertyTest::new(42).with_max_operations(1000).run(counter);
542
543        assert!(result.is_failure());
544        let failure = result.failure.unwrap();
545        assert!(failure.message.contains("exceeds max"));
546    }
547
548    #[test]
549    fn test_time_advance_config() {
550        // Test that time actually advances
551        struct TimeTracker {
552            last_time: u64,
553            times_advanced: u64,
554        }
555
556        #[derive(Debug, Clone)]
557        struct NoOp;
558
559        impl PropertyTestable for TimeTracker {
560            type Operation = NoOp;
561
562            fn generate_operation(&self, _rng: &mut DeterministicRng) -> Self::Operation {
563                NoOp
564            }
565
566            fn apply_operation(&mut self, _op: &Self::Operation, clock: &SimClock) {
567                let now = clock.now_ms();
568                if now > self.last_time {
569                    self.times_advanced += 1;
570                    self.last_time = now;
571                }
572            }
573
574            fn check_invariants(&self) -> Result<(), String> {
575                Ok(())
576            }
577
578            fn describe_state(&self) -> String {
579                format!("TimeTracker {{ times_advanced: {} }}", self.times_advanced)
580            }
581        }
582
583        let tracker = TimeTracker {
584            last_time: 0,
585            times_advanced: 0,
586        };
587
588        let _result = PropertyTest::new(42)
589            .with_max_operations(100)
590            .with_time_advance(TimeAdvanceConfig::fixed(10))
591            .run(tracker);
592
593        // Time should have advanced multiple times
594        // (we can't easily check the internal state after run, but no panics = success)
595    }
596
597    #[test]
598    fn test_test_seeds() {
599        let seeds = test_seeds(10);
600        assert_eq!(seeds.len(), 10);
601        assert_eq!(seeds[0], 0); // Edge case
602        assert_eq!(seeds[1], 1); // Edge case
603        assert_eq!(seeds[2], 42); // Common test seed
604    }
605
606    #[test]
607    fn test_run_property_tests_helper() {
608        run_property_tests(&[0, 1, 42], 100, || BoundedCounter {
609            value: 0,
610            min: -100,
611            max: 100,
612        });
613    }
614
615    #[test]
616    fn test_initial_invariant_check() {
617        // State that starts invalid
618        let bad_counter = BoundedCounter {
619            value: 200, // Exceeds max!
620            min: -100,
621            max: 100,
622        };
623
624        let result = PropertyTest::new(42).run(bad_counter);
625
626        assert!(result.is_failure());
627        assert!(result
628            .failure
629            .unwrap()
630            .message
631            .contains("Initial state violates"));
632    }
633
634    #[test]
635    fn test_skip_initial_invariant_check() {
636        // Use BuggyCounter which doesn't clamp - starts invalid and stays invalid
637        let bad_counter = BuggyCounter {
638            value: 200,
639            max: 100,
640        };
641
642        let result = PropertyTest::new(42)
643            .skip_initial_invariant_check()
644            .with_max_operations(1)
645            .run(bad_counter);
646
647        // Should fail when invariant is checked after first op (value still > max)
648        assert!(result.is_failure());
649    }
650
651    #[test]
652    fn test_skip_initial_but_fixes_itself() {
653        // BoundedCounter clamps values, so invalid initial state gets fixed
654        let bad_counter = BoundedCounter {
655            value: 200,
656            min: -100,
657            max: 100,
658        };
659
660        let result = PropertyTest::new(42)
661            .skip_initial_invariant_check()
662            .with_max_operations(1)
663            .run(bad_counter);
664
665        // Should pass because BoundedCounter clamps to valid range on any operation
666        assert!(result.is_success());
667    }
668}