1use std::fmt::Debug;
64
65use super::clock::SimClock;
66use super::rng::DeterministicRng;
67use crate::constants::DST_SIMULATION_STEPS_MAX;
68
69pub trait PropertyTestable {
73 type Operation: Debug + Clone;
75
76 fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation;
80
81 fn apply_operation(&mut self, op: &Self::Operation, clock: &SimClock);
85
86 fn check_invariants(&self) -> Result<(), String>;
90
91 fn describe_state(&self) -> String {
93 String::from("(state description not implemented)")
94 }
95}
96
97#[derive(Debug)]
99pub struct PropertyTestResult {
100 pub operations_executed: u64,
102 pub seed: u64,
104 pub failure: Option<PropertyTestFailure>,
106}
107
108impl PropertyTestResult {
109 #[must_use]
111 pub fn is_success(&self) -> bool {
112 self.failure.is_none()
113 }
114
115 #[must_use]
117 pub fn is_failure(&self) -> bool {
118 self.failure.is_some()
119 }
120
121 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#[derive(Debug)]
145pub struct PropertyTestFailure {
146 pub operation_index: u64,
148 pub operation: String,
150 pub message: String,
152 pub state_description: String,
154}
155
156#[derive(Debug, Clone)]
158pub struct TimeAdvanceConfig {
159 pub min_ms: u64,
161 pub max_ms: u64,
163 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 #[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 #[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 #[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#[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 #[must_use]
232 pub fn new(seed: u64) -> Self {
233 Self {
234 seed,
235 max_operations: 100, time_config: TimeAdvanceConfig::default(),
237 check_invariants_before: true,
238 }
239 }
240
241 #[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 #[must_use]
259 pub fn with_time_advance(mut self, config: TimeAdvanceConfig) -> Self {
260 self.time_config = config;
261 self
262 }
263
264 #[must_use]
266 pub fn skip_initial_invariant_check(mut self) -> Self {
267 self.check_invariants_before = false;
268 self
269 }
270
271 #[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 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 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 let op = state.generate_operation(&mut rng);
312 let op_debug = format!("{:?}", op);
313 state.apply_operation(&op, &clock);
314
315 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 pub fn run_and_assert<T: PropertyTestable>(self, state: T) {
344 self.run(state).unwrap();
345 }
346}
347
348pub 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#[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]; 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 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 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 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 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 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 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 }
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); assert_eq!(seeds[1], 1); assert_eq!(seeds[2], 42); }
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 let bad_counter = BoundedCounter {
619 value: 200, 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 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 assert!(result.is_failure());
649 }
650
651 #[test]
652 fn test_skip_initial_but_fixes_itself() {
653 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 assert!(result.is_success());
667 }
668}