Skip to main content

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}