proptest_state_machine/strategy.rs
1//-
2// Copyright 2023 The proptest developers
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10//! Strategies used for abstract state machine testing.
11
12use std::sync::atomic::{self, AtomicUsize};
13use std::sync::Arc;
14
15use proptest::bits::{BitSetLike, VarBitSet};
16use proptest::collection::SizeRange;
17use proptest::num::sample_uniform_incl;
18use proptest::std_facade::fmt::{Debug, Formatter, Result};
19use proptest::std_facade::Vec;
20use proptest::strategy::BoxedStrategy;
21use proptest::strategy::{NewTree, Strategy, ValueTree};
22use proptest::test_runner::TestRunner;
23
24/// This trait is used to model system under test as an abstract state machine.
25///
26/// The key to how this works is that the set of next valid transitions depends
27/// on its current state (it's not the same as generating a random sequence of
28/// transitions) and just like other prop strategies, the state machine strategy
29/// attempts to shrink the transitions to find the minimal reproducible example
30/// when it encounters a case that breaks any of the defined properties.
31///
32/// This is achieved with the [`ReferenceStateMachine::transitions`] that takes
33/// the current state as an argument and can be used to decide which transitions
34/// are valid from this state, together with the
35/// [`ReferenceStateMachine::preconditions`], which are checked during generation
36/// of transitions and during shrinking.
37///
38/// Hence, the `preconditions` only needs to contain checks for invariants that
39/// depend on the current state and may be broken by shrinking and it doesn't
40/// need to cover invariants that do not depend on the current state.
41///
42/// The reference state machine generation runs before the generated transitions
43/// are attempted to be executed against the SUT (the concrete state machine)
44/// as defined by [`crate::StateMachineTest`].
45pub trait ReferenceStateMachine: 'static {
46 /// The reference state machine's state type. This should contain the minimum
47 /// required information needed to implement the state machine. It is used
48 /// to drive the generations of transitions to decide which transitions are
49 /// valid for the current state.
50 type State: Clone + Debug;
51
52 /// The reference state machine's transition type. This is typically an enum
53 /// with its variants containing the parameters required to apply the
54 /// transition, if any.
55 type Transition: Clone + Debug;
56
57 // TODO Instead of the boxed strategies, this could use
58 // <https://github.com/rust-lang/rust/issues/63063> once stabilized:
59 // type StateStrategy = impl Strategy<Value = Self::State>;
60 // type TransitionStrategy = impl Strategy<Value = Self::Transition>;
61
62 /// The initial state may be generated by any strategy. For a constant
63 /// initial state, use [`proptest::strategy::Just`].
64 fn init_state() -> BoxedStrategy<Self::State>;
65
66 /// Generate the initial transitions.
67 fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition>;
68
69 /// Apply a transition in the reference state.
70 fn apply(state: Self::State, transition: &Self::Transition) -> Self::State;
71
72 /// Pre-conditions may be specified to control which transitions are valid
73 /// from the current state. If not overridden, this allows any transition.
74 /// The pre-conditions are checked in the generated transitions and during
75 /// shrinking.
76 ///
77 /// The pre-conditions checking relies on proptest global rejection
78 /// filtering, which comes with some [disadvantages](https://altsysrq.github.io/proptest-book/proptest/tutorial/filtering.html).
79 /// This means that pre-conditions that are hard to satisfy might slow down
80 /// the test or even fail by exceeding the maximum rejection count.
81 fn preconditions(
82 state: &Self::State,
83 transition: &Self::Transition,
84 ) -> bool {
85 // This is to avoid `unused_variables` warning
86 let _ = (state, transition);
87
88 true
89 }
90
91 /// A sequential strategy runs the state machine transitions generated from
92 /// the reference model sequentially in a test over a concrete state, which
93 /// can be implemented with the help of
94 /// [`crate::StateMachineTest`] trait.
95 ///
96 /// You typically never need to override this method.
97 fn sequential_strategy(
98 size: impl Into<SizeRange>,
99 ) -> Sequential<
100 Self::State,
101 Self::Transition,
102 BoxedStrategy<Self::State>,
103 BoxedStrategy<Self::Transition>,
104 > {
105 Sequential::new(
106 size.into(),
107 Self::init_state,
108 Self::preconditions,
109 Self::transitions,
110 Self::apply,
111 )
112 }
113}
114
115/// In a sequential state machine strategy, we first generate an acceptable
116/// sequence of transitions. That is a sequence that satisfies the given
117/// pre-conditions. The acceptability of each transition in the sequence depends
118/// on the current state of the state machine, which is updated by the
119/// transitions with the `next` function.
120///
121/// The shrinking strategy is to iteratively apply `Shrink::InitialState`,
122/// `Shrink::DeleteTransition` and `Shrink::Transition`.
123///
124/// 1. We start by trying to delete transitions from the back of the list that
125/// were never seen by the test, if any. Note that because proptest expects
126/// deterministic results in for reproducible issues, unlike the following
127/// steps this step will not be undone on `complicate`. If there were any
128/// unseen transitions, then the next step will start at trying to delete
129/// the transition before the last one seen as we know that the last
130/// transition cannot be deleted as it's the one that has failed.
131/// 2. Then, we keep trying to delete transitions from the back of the list, until
132/// we can do so no further (reached the beginning of the list)..
133/// 3. Then, we again iteratively attempt to shrink the individual transitions,
134/// but this time starting from the front of the list - i.e. from the first
135/// transition to be applied.
136/// 4. Finally, we try to shrink the initial state until it's not possible to
137/// shrink it any further.
138///
139/// For `complicate`, we attempt to undo the last shrink operation, if there was
140/// any.
141pub struct Sequential<State, Transition, StateStrategy, TransitionStrategy> {
142 size: SizeRange,
143 init_state: Arc<dyn Fn() -> StateStrategy + Send + Sync>,
144 preconditions: Arc<dyn Fn(&State, &Transition) -> bool + Send + Sync>,
145 transitions: Arc<dyn Fn(&State) -> TransitionStrategy + Send + Sync>,
146 next: Arc<dyn Fn(State, &Transition) -> State + Send + Sync>,
147}
148
149impl<State, Transition, StateStrategy, TransitionStrategy>
150 Sequential<State, Transition, StateStrategy, TransitionStrategy>
151where
152 State: 'static,
153 Transition: 'static,
154 StateStrategy: 'static,
155 TransitionStrategy: 'static,
156{
157 pub fn new(
158 size: SizeRange,
159 init_state: impl Fn() -> StateStrategy + 'static + Send + Sync,
160 preconditions: impl Fn(&State, &Transition) -> bool + 'static + Send + Sync,
161 transitions: impl Fn(&State) -> TransitionStrategy + 'static + Send + Sync,
162 next: impl Fn(State, &Transition) -> State + 'static + Send + Sync,
163 ) -> Self {
164 Self {
165 size,
166 init_state: Arc::new(init_state),
167 preconditions: Arc::new(preconditions),
168 transitions: Arc::new(transitions),
169 next: Arc::new(next),
170 }
171 }
172}
173
174impl<State, Transition, StateStrategy, TransitionStrategy> Debug
175 for Sequential<State, Transition, StateStrategy, TransitionStrategy>
176{
177 fn fmt(&self, f: &mut Formatter) -> Result {
178 f.debug_struct("Sequential")
179 .field("size", &self.size)
180 .finish()
181 }
182}
183
184impl<
185 State: Clone + Debug,
186 Transition: Clone + Debug,
187 StateStrategy: Strategy<Value = State>,
188 TransitionStrategy: Strategy<Value = Transition>,
189 > Strategy
190 for Sequential<State, Transition, StateStrategy, TransitionStrategy>
191{
192 type Tree = SequentialValueTree<
193 State,
194 Transition,
195 StateStrategy::Tree,
196 TransitionStrategy::Tree,
197 >;
198 type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
199
200 fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
201 // Generate the initial state value tree
202 let initial_state = (self.init_state)().new_tree(runner)?;
203 let last_valid_initial_state = initial_state.current();
204
205 let (min_size, end) = self.size.start_end_incl();
206 // Sample the maximum number of the transitions from the size range
207 let max_size = sample_uniform_incl(runner, min_size, end);
208 let mut transitions = Vec::with_capacity(max_size);
209 let mut acceptable_transitions = Vec::with_capacity(max_size);
210 let included_transitions = VarBitSet::saturated(max_size);
211 let shrinkable_transitions = VarBitSet::saturated(max_size);
212
213 // Sample the transitions until we reach the `max_size`
214 let mut state = initial_state.current();
215 while transitions.len() < max_size {
216 // Apply the current state to find the current transition
217 let transition_tree =
218 (self.transitions)(&state).new_tree(runner)?;
219 let transition = transition_tree.current();
220
221 // If the pre-conditions are satisfied, use the transition
222 if (self.preconditions)(&state, &transition) {
223 transitions.push(transition_tree);
224 state = (self.next)(state, &transition);
225 acceptable_transitions
226 .push((TransitionState::Accepted, transition));
227 } else {
228 runner.reject_local("Pre-conditions were not satisfied")?;
229 }
230 }
231
232 // The maximum index into the vectors and bit sets
233 let max_ix = max_size - 1;
234
235 Ok(SequentialValueTree {
236 initial_state,
237 is_initial_state_shrinkable: true,
238 last_valid_initial_state,
239 preconditions: self.preconditions.clone(),
240 next: self.next.clone(),
241 transitions,
242 acceptable_transitions,
243 included_transitions,
244 shrinkable_transitions,
245 max_ix,
246 // On a failure, we start by shrinking transitions from the back
247 // which is less likely to invalidate pre-conditions
248 shrink: Shrink::DeleteTransition(max_ix),
249 last_shrink: None,
250 seen_transitions_counter: Some(Default::default()),
251 })
252 }
253}
254
255/// A shrinking operation
256#[derive(Clone, Copy, Debug)]
257enum Shrink {
258 /// Shrink the initial state
259 InitialState,
260 /// Delete a transition at given index
261 DeleteTransition(usize),
262 /// Shrink a transition at given index
263 Transition(usize),
264}
265use Shrink::*;
266
267/// The state of a transition in the model
268#[derive(Clone, Copy, Debug)]
269enum TransitionState {
270 /// The transition that is equal to the result of `ValueTree::current()`
271 /// and satisfies the pre-conditions
272 Accepted,
273 /// The transition has been simplified, but rejected by pre-conditions
274 SimplifyRejected,
275 /// The transition has been complicated, but rejected by pre-conditions
276 ComplicateRejected,
277}
278use TransitionState::*;
279
280/// The generated value tree for a sequential state machine.
281pub struct SequentialValueTree<
282 State,
283 Transition,
284 StateValueTree,
285 TransitionValueTree,
286> {
287 /// The initial state value tree
288 initial_state: StateValueTree,
289 /// Can the `initial_state` be shrunk any further?
290 is_initial_state_shrinkable: bool,
291 /// The last initial state that has been accepted by the pre-conditions.
292 /// We have to store this every time before attempt to shrink to be able
293 /// to back to it in case the shrinking is rejected.
294 last_valid_initial_state: State,
295 /// The pre-conditions predicate
296 preconditions: Arc<dyn Fn(&State, &Transition) -> bool>,
297 /// The function from current state and a transition to an updated state
298 next: Arc<dyn Fn(State, &Transition) -> State>,
299 /// The list of transitions' value trees
300 transitions: Vec<TransitionValueTree>,
301 /// The sequence of included transitions with their shrinking state
302 acceptable_transitions: Vec<(TransitionState, Transition)>,
303 /// The bit-set of transitions that have not been deleted by shrinking
304 included_transitions: VarBitSet,
305 /// The bit-set of transitions that can be shrunk further
306 shrinkable_transitions: VarBitSet,
307 /// The maximum index in the `transitions` vector (its size - 1)
308 max_ix: usize,
309 /// The next shrink operation to apply
310 shrink: Shrink,
311 /// The last applied shrink operation, if any
312 last_shrink: Option<Shrink>,
313 /// The number of transitions that were seen by the test runner.
314 /// On a test run this is shared with `StateMachineTest::test_sequential`
315 /// which increments the inner counter value on every transition. If the
316 /// test fails, the counter is used to remove any unseen transitions before
317 /// shrinking and this field is set to `None` as it's no longer needed for
318 /// shrinking.
319 seen_transitions_counter: Option<Arc<AtomicUsize>>,
320}
321
322impl<
323 State: Clone + Debug,
324 Transition: Clone + Debug,
325 StateValueTree: ValueTree<Value = State>,
326 TransitionValueTree: ValueTree<Value = Transition>,
327 >
328 SequentialValueTree<State, Transition, StateValueTree, TransitionValueTree>
329{
330 /// Try to apply the next `self.shrink`. Returns `true` if a shrink has been
331 /// applied.
332 fn try_simplify(&mut self) -> bool {
333 if let Some(seen_transitions_counter) =
334 self.seen_transitions_counter.as_ref()
335 {
336 let seen_count =
337 seen_transitions_counter.load(atomic::Ordering::SeqCst);
338
339 let included_count = self.included_transitions.count();
340
341 if seen_count < included_count {
342 // the test runner did not see all the transitions so we can
343 // delete the transitions that were not seen because they were
344 // not executed
345
346 let mut kept_count = 0;
347 for ix in 0..self.transitions.len() {
348 if self.included_transitions.test(ix) {
349 // transition at ix was part of test
350
351 if kept_count < seen_count {
352 // transition at xi was seen by the test or we are
353 // still below minimum size for the test
354 kept_count += 1;
355 } else {
356 // transition at ix was never seen
357 self.included_transitions.clear(ix);
358 self.shrinkable_transitions.clear(ix);
359 }
360 }
361 }
362 // Set the next shrink based on how many transitions were seen:
363 // - If 0 seen: go directly to shrinking the initial state.
364 // - If 1 seen: can't delete any more, so shrink individual transitions.
365 // - If >1 seen: delete the transition before the last seen transition.
366 // (subtract 2 from `kept_count` because the last seen transition
367 // caused the failure).
368 if kept_count == 0 {
369 self.shrink = InitialState;
370 } else if kept_count == 1 {
371 self.shrink = Transition(0);
372 } else {
373 self.shrink = DeleteTransition(
374 kept_count.checked_sub(2).unwrap_or_default(),
375 );
376 }
377 }
378
379 // Remove the seen transitions counter for shrinking runs
380 self.seen_transitions_counter = None;
381 }
382
383 if let DeleteTransition(ix) = self.shrink {
384 // Delete the index from the included transitions
385 self.included_transitions.clear(ix);
386
387 self.last_shrink = Some(self.shrink);
388 self.shrink = if ix == 0 {
389 // Reached the beginning of the list, move on to shrinking
390 Transition(0)
391 } else {
392 // Try to delete the previous transition next
393 DeleteTransition(ix - 1)
394 };
395 // If this delete is not acceptable, undo it and try again
396 if !self
397 .check_acceptable(None, self.last_valid_initial_state.clone())
398 {
399 self.included_transitions.set(ix);
400 self.last_shrink = None;
401 return self.try_simplify();
402 }
403 // If the delete was accepted, remove this index from shrinkable
404 // transitions
405 self.shrinkable_transitions.clear(ix);
406 return true;
407 }
408
409 while let Transition(ix) = self.shrink {
410 if self.shrinkable_transitions.count() == 0 {
411 // Move on to shrinking the initial state
412 self.shrink = Shrink::InitialState;
413 break;
414 }
415
416 if !self.included_transitions.test(ix) {
417 // No use shrinking something we're not including
418 self.shrink = self.next_shrink_transition(ix);
419 continue;
420 }
421
422 if let Some((SimplifyRejected, _trans)) =
423 self.acceptable_transitions.get(ix)
424 {
425 // This transition is already simplified and rejected
426 self.shrink = self.next_shrink_transition(ix);
427 } else if self.transitions[ix].simplify() {
428 self.last_shrink = Some(self.shrink);
429 if self.check_acceptable(
430 Some(ix),
431 self.last_valid_initial_state.clone(),
432 ) {
433 self.acceptable_transitions[ix] =
434 (Accepted, self.transitions[ix].current());
435 return true;
436 } else {
437 let (state, _trans) =
438 self.acceptable_transitions.get_mut(ix).unwrap();
439 *state = SimplifyRejected;
440 self.shrinkable_transitions.clear(ix);
441 self.shrink = self.next_shrink_transition(ix);
442 return self.simplify();
443 }
444 } else {
445 self.shrinkable_transitions.clear(ix);
446 self.shrink = self.next_shrink_transition(ix);
447 }
448 }
449
450 if let InitialState = self.shrink {
451 if self.initial_state.simplify() {
452 if self.check_acceptable(None, self.initial_state.current()) {
453 self.last_valid_initial_state =
454 self.initial_state.current();
455 self.last_shrink = Some(self.shrink);
456 return true;
457 } else {
458 // If the shrink is not acceptable, clear it out
459 self.last_shrink = None;
460
461 // `initial_state` is "dirty" here but we won't ever use it again because it is unshrinkable from here.
462 }
463 }
464 self.is_initial_state_shrinkable = false;
465 // Nothing left to do
466 return false;
467 }
468
469 // This statement should never be reached
470 panic!("Unexpected shrink state");
471 }
472
473 /// Find if there's any acceptable included transition that is not current,
474 /// starting from the given index. Expects that all the included transitions
475 /// are currently being rejected (when `can_simplify` returns `false`).
476 fn try_to_find_acceptable_transition(&mut self, ix: usize) -> bool {
477 let mut ix_to_check = ix;
478 loop {
479 if self.included_transitions.test(ix_to_check)
480 && self.check_acceptable(
481 Some(ix_to_check),
482 self.last_valid_initial_state.clone(),
483 )
484 {
485 self.acceptable_transitions[ix_to_check] =
486 (Accepted, self.transitions[ix_to_check].current());
487 return true;
488 }
489 // Move on to the next transition
490 if ix_to_check == self.max_ix {
491 ix_to_check = 0;
492 } else {
493 ix_to_check += 1;
494 }
495 // We're back to where we started, there nothing left to do
496 if ix_to_check == ix {
497 return false;
498 }
499 }
500 }
501
502 /// Check if the sequence of included transitions is acceptable by the
503 /// pre-conditions. When `ix` is not `None`, the transition at the given
504 /// index is taken from its current value.
505 fn check_acceptable(&self, ix: Option<usize>, mut state: State) -> bool {
506 let transitions = self.get_included_acceptable_transitions(ix);
507 for transition in transitions.iter() {
508 let is_acceptable = (self.preconditions)(&state, transition);
509 if is_acceptable {
510 state = (self.next)(state, transition);
511 } else {
512 return false;
513 }
514 }
515 true
516 }
517
518 /// The currently included and acceptable transitions. When `ix` is not
519 /// `None`, the transition at this index is taken from its current value
520 /// which may not be acceptable by the pre-conditions, instead of its
521 /// acceptable value.
522 fn get_included_acceptable_transitions(
523 &self,
524 ix: Option<usize>,
525 ) -> Vec<Transition> {
526 self.acceptable_transitions
527 .iter()
528 .enumerate()
529 // Filter out deleted transitions
530 .filter(|&(this_ix, _)| self.included_transitions.test(this_ix))
531 // Map the indices to the values
532 .map(|(this_ix, (_, transition))| match ix {
533 Some(ix) if this_ix == ix => self.transitions[ix].current(),
534 _ => transition.clone(),
535 })
536 .collect()
537 }
538
539 /// Find if the initial state is still shrinkable or if any of the
540 /// simplifications and complications of the included transitions have not
541 /// yet been rejected.
542 fn can_simplify(&self) -> bool {
543 self.is_initial_state_shrinkable ||
544 // If there are some transitions whose shrinking has not yet been
545 // rejected, we can try to shrink them further
546 !self
547 .acceptable_transitions
548 .iter()
549 .enumerate()
550 // Filter out deleted transitions
551 .filter(|&(ix, _)| self.included_transitions.test(ix))
552 .all(|(_, (state, _transition))| {
553 matches!(state, SimplifyRejected | ComplicateRejected)
554 })
555 }
556
557 /// Find the next shrink transition. Loops back to the front of the list
558 /// when the end is reached, because sometimes a transition might become
559 /// acceptable only after a transition that comes before it in the sequence
560 /// gets shrunk.
561 fn next_shrink_transition(&self, current_ix: usize) -> Shrink {
562 if current_ix == self.max_ix {
563 // Either loop back to the start of the list...
564 Transition(0)
565 } else {
566 // ...or move on to the next transition
567 Transition(current_ix + 1)
568 }
569 }
570}
571
572impl<
573 State: Clone + Debug,
574 Transition: Clone + Debug,
575 StateValueTree: ValueTree<Value = State>,
576 TransitionValueTree: ValueTree<Value = Transition>,
577 > ValueTree
578 for SequentialValueTree<
579 State,
580 Transition,
581 StateValueTree,
582 TransitionValueTree,
583 >
584{
585 type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
586
587 fn current(&self) -> Self::Value {
588 if let Some(seen_transitions_counter) = &self.seen_transitions_counter {
589 if seen_transitions_counter.load(atomic::Ordering::SeqCst) > 0 {
590 panic!("Unexpected non-zero `seen_transitions_counter`");
591 }
592 }
593
594 (
595 self.last_valid_initial_state.clone(),
596 // The current included acceptable transitions
597 self.get_included_acceptable_transitions(None),
598 self.seen_transitions_counter.clone(),
599 )
600 }
601
602 fn simplify(&mut self) -> bool {
603 let was_simplified = if self.can_simplify() {
604 self.try_simplify()
605 } else if let Some(Transition(ix)) = self.last_shrink {
606 self.try_to_find_acceptable_transition(ix)
607 } else {
608 false
609 };
610
611 // reset seen transactions counter for next run
612 self.seen_transitions_counter = Default::default();
613
614 was_simplified
615 }
616
617 fn complicate(&mut self) -> bool {
618 // reset seen transactions counter for next run
619 self.seen_transitions_counter = Default::default();
620
621 match &self.last_shrink {
622 None => false,
623 Some(DeleteTransition(ix)) => {
624 // Undo the last item we deleted. Can't complicate any further,
625 // so unset prev_shrink.
626 self.included_transitions.set(*ix);
627 self.shrinkable_transitions.set(*ix);
628 self.last_shrink = None;
629 true
630 }
631 Some(Transition(ix)) => {
632 let ix = *ix;
633 if self.transitions[ix].complicate() {
634 if self.check_acceptable(
635 Some(ix),
636 self.last_valid_initial_state.clone(),
637 ) {
638 self.acceptable_transitions[ix] =
639 (Accepted, self.transitions[ix].current());
640 // Don't unset prev_shrink; we may be able to complicate
641 // it again
642 return true;
643 } else {
644 let (state, _trans) =
645 self.acceptable_transitions.get_mut(ix).unwrap();
646 *state = ComplicateRejected;
647 }
648 }
649 // Can't complicate the last element any further
650 self.last_shrink = None;
651 false
652 }
653 Some(InitialState) => {
654 if self.initial_state.complicate()
655 && self.check_acceptable(None, self.initial_state.current())
656 {
657 self.last_valid_initial_state =
658 self.initial_state.current();
659 // Don't unset prev_shrink; we may be able to complicate
660 // it again
661 return true;
662 }
663 // Can't complicate the initial state any further
664 self.last_shrink = None;
665 false
666 }
667 }
668 }
669}
670
671#[cfg(test)]
672mod test {
673 use super::*;
674
675 use proptest::collection::hash_set;
676 use proptest::prelude::*;
677
678 use heap_state_machine::*;
679 use std::collections::HashSet;
680
681 /// A number of simplifications that can be applied in the `ValueTree`
682 /// produced by [`deterministic_sequential_value_tree`]. It depends on the
683 /// [`TRANSITIONS`] given to its `sequential_strategy`.
684 ///
685 /// This constant can be determined from the test
686 /// `number_of_sequential_value_tree_simplifications`.
687 const SIMPLIFICATIONS: usize = 32;
688 /// Number of transitions in the [`deterministic_sequential_value_tree`].
689 const TRANSITIONS: usize = 32;
690
691 #[test]
692 fn number_of_sequential_value_tree_simplifications() {
693 let mut value_tree = deterministic_sequential_value_tree();
694 value_tree
695 .seen_transitions_counter
696 .as_mut()
697 .unwrap()
698 .store(TRANSITIONS, atomic::Ordering::SeqCst);
699
700 let mut i = 0;
701 loop {
702 let simplified = value_tree.simplify();
703 if simplified {
704 i += 1;
705 } else {
706 break;
707 }
708 }
709 assert_eq!(i, SIMPLIFICATIONS);
710 }
711
712 proptest! {
713 /// Test the simplifications and complication of the
714 /// `SequentialValueTree` produced by
715 /// `deterministic_sequential_value_tree`.
716 ///
717 /// The indices of simplification on which we'll attempt to complicate
718 /// after simplification are selected from the randomly generated
719 /// `complicate_ixs`.
720 ///
721 /// Every simplification and complication must satisfy pre-conditions of
722 /// the state-machine.
723 #[test]
724 fn test_state_machine_sequential_value_tree(
725 complicate_ixs in hash_set(0..SIMPLIFICATIONS, 0..SIMPLIFICATIONS)
726 ) {
727 test_state_machine_sequential_value_tree_aux(complicate_ixs)
728 }
729 }
730
731 fn test_state_machine_sequential_value_tree_aux(
732 complicate_ixs: HashSet<usize>,
733 ) {
734 println!("Complicate indices: {complicate_ixs:?}");
735
736 let mut value_tree = deterministic_sequential_value_tree();
737
738 let check_preconditions = |value_tree: &TestValueTree| {
739 let (mut state, transitions, _seen_counter) = value_tree.current();
740 let len = transitions.len();
741 println!("Transitions {}", len);
742 for (ix, transition) in transitions.into_iter().enumerate() {
743 println!("Transition {}/{len} {transition:?}", ix + 1);
744 // Every transition must satisfy the pre-conditions
745 assert!(
746 <HeapStateMachine as ReferenceStateMachine>::preconditions(
747 &state,
748 &transition
749 )
750 );
751
752 // Apply the transition to update the state for the next transition
753 state = <HeapStateMachine as ReferenceStateMachine>::apply(
754 state,
755 &transition,
756 );
757 }
758 };
759
760 let mut ix = 0_usize;
761 loop {
762 let simplified = value_tree.simplify();
763
764 check_preconditions(&value_tree);
765
766 if !simplified {
767 break;
768 }
769 ix += 1;
770
771 if complicate_ixs.contains(&ix) {
772 loop {
773 let complicated = value_tree.complicate();
774
775 check_preconditions(&value_tree);
776
777 if !complicated {
778 break;
779 }
780 }
781 }
782 }
783 }
784
785 proptest! {
786 /// Test the initial simplifications of the `SequentialValueTree` produced
787 /// by `deterministic_sequential_value_tree`.
788 ///
789 /// We want to make sure that we initially remove the transitions that
790 /// where not seen.
791 #[test]
792 fn test_value_tree_initial_simplification(
793 len in 10usize..100,
794 ) {
795 test_value_tree_initial_simplification_aux(len)
796 }
797 }
798
799 fn test_value_tree_initial_simplification_aux(len: usize) {
800 let sequential =
801 <HeapStateMachine as ReferenceStateMachine>::sequential_strategy(
802 ..len,
803 );
804
805 let mut runner = TestRunner::deterministic();
806 let mut value_tree = sequential.new_tree(&mut runner).unwrap();
807
808 let (_, transitions, mut seen_counter) = value_tree.current();
809
810 let num_seen = transitions.len() / 2;
811 let seen_counter = seen_counter.as_mut().unwrap();
812 seen_counter.store(num_seen, atomic::Ordering::SeqCst);
813
814 let mut seen_before_complication =
815 transitions.into_iter().take(num_seen).collect::<Vec<_>>();
816
817 assert!(value_tree.simplify());
818
819 let (_, transitions, _seen_counter) = value_tree.current();
820
821 let seen_after_first_complication =
822 transitions.into_iter().collect::<Vec<_>>();
823
824 // After the unseen transitions are removed, the shrink behavior depends
825 // on how many transitions were seen:
826 // - If > 1 seen: delete the transition before the last seen one
827 // - If = 1 seen: can't delete any more, may start individual transition shrinking
828 if seen_before_complication.len() > 1 {
829 let last = seen_before_complication.pop().unwrap();
830 seen_before_complication.pop();
831 seen_before_complication.push(last);
832 assert_eq!(
833 seen_before_complication, seen_after_first_complication,
834 "only seen transitions should be present after first simplification"
835 );
836 } else {
837 // When there's only 1 seen transition, we expect it to be preserved.
838 assert!(
839 !seen_after_first_complication.is_empty(),
840 "When only 1 transition was seen, at least 1 should remain after simplification"
841 );
842 assert!(
843 matches!(value_tree.shrink, Transition(0)),
844 "When only 1 transition was seen, shrink should be set to Transition(0)"
845 );
846 }
847 }
848
849 #[test]
850 fn test_call_to_current_with_non_zero_seen_counter() {
851 let result = std::panic::catch_unwind(|| {
852 let value_tree = deterministic_sequential_value_tree();
853
854 let (_, _transitions1, mut seen_counter) = value_tree.current();
855 {
856 let seen_counter = seen_counter.as_mut().unwrap();
857 seen_counter.store(1, atomic::Ordering::SeqCst);
858 }
859 drop(seen_counter);
860
861 let _transitions2 = value_tree.current();
862 })
863 .expect_err("should panic");
864
865 let s = "Unexpected non-zero `seen_transitions_counter`";
866 assert_eq!(result.downcast_ref::<&str>(), Some(&s));
867 }
868
869 /// The following is a definition of an reference state machine used for the
870 /// tests.
871 mod heap_state_machine {
872 use std::vec::Vec;
873
874 use crate::{ReferenceStateMachine, SequentialValueTree};
875 use proptest::prelude::*;
876 use proptest::test_runner::TestRunner;
877
878 use super::TRANSITIONS;
879
880 pub struct HeapStateMachine;
881
882 pub type TestValueTree = SequentialValueTree<
883 TestState,
884 TestTransition,
885 <BoxedStrategy<TestState> as Strategy>::Tree,
886 <BoxedStrategy<TestTransition> as Strategy>::Tree,
887 >;
888
889 pub type TestState = Vec<i32>;
890
891 #[derive(Clone, Debug, PartialEq)]
892 pub enum TestTransition {
893 PopNonEmpty,
894 PopEmpty,
895 Push(i32),
896 }
897
898 pub fn deterministic_sequential_value_tree() -> TestValueTree {
899 let sequential =
900 <HeapStateMachine as ReferenceStateMachine>::sequential_strategy(
901 TRANSITIONS,
902 );
903 let mut runner = TestRunner::deterministic();
904 sequential.new_tree(&mut runner).unwrap()
905 }
906
907 impl ReferenceStateMachine for HeapStateMachine {
908 type State = TestState;
909 type Transition = TestTransition;
910
911 fn init_state() -> BoxedStrategy<Self::State> {
912 Just(vec![]).boxed()
913 }
914
915 fn transitions(
916 state: &Self::State,
917 ) -> BoxedStrategy<Self::Transition> {
918 if state.is_empty() {
919 prop_oneof![
920 1 => Just(TestTransition::PopEmpty),
921 2 => (any::<i32>()).prop_map(TestTransition::Push),
922 ]
923 .boxed()
924 } else {
925 prop_oneof![
926 1 => Just(TestTransition::PopNonEmpty),
927 2 => (any::<i32>()).prop_map(TestTransition::Push),
928 ]
929 .boxed()
930 }
931 }
932
933 fn apply(
934 mut state: Self::State,
935 transition: &Self::Transition,
936 ) -> Self::State {
937 match transition {
938 TestTransition::PopEmpty => {
939 state.pop();
940 }
941 TestTransition::PopNonEmpty => {
942 state.pop();
943 }
944 TestTransition::Push(value) => state.push(*value),
945 }
946 state
947 }
948
949 fn preconditions(
950 state: &Self::State,
951 transition: &Self::Transition,
952 ) -> bool {
953 match transition {
954 TestTransition::PopEmpty => state.is_empty(),
955 TestTransition::PopNonEmpty => !state.is_empty(),
956 TestTransition::Push(_) => true,
957 }
958 }
959 }
960 }
961
962 /// A tests that verifies that the strategy finds a simplest failing case, and
963 /// that this simplest failing case is ultimately reported by the test runner,
964 /// as opposed to reporting input that actually passes the test.
965 ///
966 /// This module defines a state machine test that is designed to fail.
967 /// The reference state machine consists of a lower bound the acceptable value
968 /// of a transition. And the test fails if an unacceptably low transition
969 /// value is observed, given the reference state's limit.
970 ///
971 /// This intentionally-failing state machine test is then run inside a proptest
972 /// to verify that it reports a simplest failing input when it fails.
973 mod find_simplest_failure {
974 use proptest::prelude::*;
975 use proptest::strategy::BoxedStrategy;
976 use proptest::test_runner::TestRng;
977 use proptest::{
978 collection,
979 strategy::Strategy,
980 test_runner::{Config, TestError, TestRunner},
981 };
982
983 use crate::{ReferenceStateMachine, StateMachineTest};
984
985 const MIN_TRANSITION: u32 = 10;
986 const MAX_TRANSITION: u32 = 20;
987
988 const MIN_LIMIT: u32 = 2;
989 const MAX_LIMIT: u32 = 50;
990
991 #[derive(Debug, Default, Clone)]
992 struct FailIfLessThan(u32);
993 impl ReferenceStateMachine for FailIfLessThan {
994 type State = Self;
995 type Transition = u32;
996
997 fn init_state() -> BoxedStrategy<Self> {
998 (MIN_LIMIT..MAX_LIMIT).prop_map(FailIfLessThan).boxed()
999 }
1000
1001 fn transitions(_: &Self::State) -> BoxedStrategy<u32> {
1002 (MIN_TRANSITION..MAX_TRANSITION).boxed()
1003 }
1004
1005 fn apply(state: Self::State, _: &Self::Transition) -> Self::State {
1006 state
1007 }
1008 }
1009
1010 /// Defines a test that is intended to fail, so that we can inspect the
1011 /// failing input.
1012 struct FailIfLessThanTest;
1013 impl StateMachineTest for FailIfLessThanTest {
1014 type SystemUnderTest = ();
1015 type Reference = FailIfLessThan;
1016
1017 fn init_test(ref_state: &FailIfLessThan) {
1018 println!();
1019 println!("starting {ref_state:?}");
1020 }
1021
1022 fn apply(
1023 (): Self::SystemUnderTest,
1024 ref_state: &FailIfLessThan,
1025 transition: u32,
1026 ) -> Self::SystemUnderTest {
1027 // Fail on any transition that is less than the ref state's limit.
1028 let FailIfLessThan(limit) = ref_state;
1029 println!("{transition} < {}?", limit);
1030 if transition < ref_state.0 {
1031 panic!("{transition} < {}", limit);
1032 }
1033 }
1034 }
1035
1036 proptest! {
1037 #[test]
1038 fn test_returns_simplest_failure(
1039 seed in collection::vec(any::<u8>(), 32).no_shrink()) {
1040
1041 // We need to explicitly run create a runner so that we can
1042 // inspect the output, and determine if it does return an input that
1043 // should fail, and is minimal.
1044 let mut runner = TestRunner::new_with_rng(
1045 Config::default(), TestRng::from_seed(Default::default(), &seed));
1046 let result = runner.run(
1047 &FailIfLessThan::sequential_strategy(10..50_usize),
1048 |(ref_state, transitions, seen_counter)| {
1049 Ok(FailIfLessThanTest::test_sequential(
1050 Default::default(),
1051 ref_state,
1052 transitions,
1053 seen_counter,
1054 ))
1055 },
1056 );
1057 if let Err(TestError::Fail(
1058 _,
1059 (FailIfLessThan(limit), transitions, _seen_counter),
1060 )) = result
1061 {
1062 assert_eq!(transitions.len(), 1, "The minimal failing case should be ");
1063 assert_eq!(limit, MIN_TRANSITION + 1);
1064 assert!(transitions.into_iter().next().unwrap() < limit);
1065 } else {
1066 prop_assume!(false,
1067 "If the state machine doesn't fail as intended, we need a case that fails.");
1068 }
1069 }
1070 }
1071 }
1072
1073 #[test]
1074 fn test_zero_seen_transitions_optimization() {
1075 // Test that when 0 transitions are seen, we go directly to InitialState shrinking
1076 let mut value_tree = deterministic_sequential_value_tree();
1077
1078 // Simulate that no transitions were seen (kept_count = 0)
1079 value_tree
1080 .seen_transitions_counter
1081 .as_mut()
1082 .unwrap()
1083 .store(0, atomic::Ordering::SeqCst);
1084
1085 // Call simplify - this should trigger the optimization
1086 let simplified = value_tree.simplify();
1087
1088 assert_eq!(value_tree.included_transitions.count(), 0,
1089 "All transitions should be removed when none were seen");
1090 assert!(matches!(value_tree.shrink, InitialState),
1091 "Shrink should be set to InitialState when kept_count == 0");
1092
1093 // The HeapStateMachine uses Just(vec![]) for initial state, which is not shrinkable
1094 // So simplify() should return false, but the optimization still works correctly
1095 assert!(!simplified,
1096 "Simplification should return false since initial state (Just(vec![])) is not shrinkable");
1097
1098 let (_, transitions, _) = value_tree.current();
1099 assert!(transitions.is_empty(),
1100 "No transitions should remain when none were seen");
1101 }
1102}