Skip to main content

proptest_state_machine/
test_runner.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//! Test declaration helpers and runners for abstract state machine testing.
11
12use std::sync::atomic::{self, AtomicUsize};
13use std::sync::Arc;
14
15use crate::strategy::ReferenceStateMachine;
16use proptest::test_runner::Config;
17
18/// State machine test that relies on a reference state machine model
19pub trait StateMachineTest {
20    /// The concrete state, that is the system under test (SUT).
21    type SystemUnderTest;
22
23    /// The abstract state machine that implements [`ReferenceStateMachine`]
24    /// drives the generation of the state machine's transitions.
25    type Reference: ReferenceStateMachine;
26
27    /// Initialize the state of SUT.
28    ///
29    /// If the reference state machine is generated from a non-constant
30    /// strategy, ensure to use it to initialize the SUT to a corresponding
31    /// state.
32    fn init_test(
33        ref_state: &<Self::Reference as ReferenceStateMachine>::State,
34    ) -> Self::SystemUnderTest;
35
36    /// Apply a transition in the SUT state and check post-conditions.
37    /// The post-conditions are properties of your state machine that you want
38    /// to assert.
39    ///
40    /// Note that the `ref_state` is the state *after* this `transition` is
41    /// applied. You can use it to compare it with your SUT after you apply
42    /// the transition.
43    fn apply(
44        state: Self::SystemUnderTest,
45        ref_state: &<Self::Reference as ReferenceStateMachine>::State,
46        transition: <Self::Reference as ReferenceStateMachine>::Transition,
47    ) -> Self::SystemUnderTest;
48
49    /// Check some invariant on the SUT state after every transition.
50    ///
51    /// Note that just like in [`StateMachineTest::apply`] you can use
52    /// the `ref_state` to compare it with your SUT.
53    fn check_invariants(
54        state: &Self::SystemUnderTest,
55        ref_state: &<Self::Reference as ReferenceStateMachine>::State,
56    ) {
57        // This is to avoid `unused_variables` warning
58        let _ = (state, ref_state);
59    }
60
61    /// Override this function to add some teardown logic on the SUT state
62    /// at the end of each test case. The default implementation simply drops
63    /// the state.
64    fn teardown(
65        state: Self::SystemUnderTest,
66        ref_state: <Self::Reference as ReferenceStateMachine>::State,
67    ) {
68        // This is to avoid `unused_variables` warning
69        let _ = state;
70        let _ = ref_state;
71    }
72
73    /// Run the test sequentially. You typically don't need to override this
74    /// method.
75    fn test_sequential(
76        config: Config,
77        mut ref_state: <Self::Reference as ReferenceStateMachine>::State,
78        transitions: Vec<
79            <Self::Reference as ReferenceStateMachine>::Transition,
80        >,
81        mut seen_counter: Option<Arc<AtomicUsize>>,
82    ) {
83        #[cfg(feature = "std")]
84        use proptest::test_runner::INFO_LOG;
85
86        let trans_len = transitions.len();
87        #[cfg(feature = "std")]
88        if config.verbose >= INFO_LOG {
89            eprintln!();
90            eprintln!("Running a test case with {} transitions.", trans_len);
91        }
92        #[cfg(not(feature = "std"))]
93        let _ = (config, trans_len);
94
95        let mut concrete_state = Self::init_test(&ref_state);
96
97        // Check the invariants on the initial state
98        Self::check_invariants(&concrete_state, &ref_state);
99
100        for (ix, transition) in transitions.into_iter().enumerate() {
101            // The counter is `Some` only before shrinking. When it's `Some` it
102            // must be incremented before every transition that's being applied
103            // to inform the strategy that the transition has been applied for
104            // the first step of its shrinking process which removes any unseen
105            // transitions.
106            if let Some(seen_counter) = seen_counter.as_mut() {
107                seen_counter.fetch_add(1, atomic::Ordering::SeqCst);
108            }
109
110            #[cfg(feature = "std")]
111            if config.verbose >= INFO_LOG {
112                eprintln!();
113                eprintln!(
114                    "Applying transition {}/{}: {:?}",
115                    ix + 1,
116                    trans_len,
117                    transition
118                );
119            }
120            #[cfg(not(feature = "std"))]
121            let _ = ix;
122
123            // Apply the transition on the states
124            ref_state = <Self::Reference as ReferenceStateMachine>::apply(
125                ref_state,
126                &transition,
127            );
128            concrete_state =
129                Self::apply(concrete_state, &ref_state, transition);
130
131            // Check the invariants after the transition is applied
132            Self::check_invariants(&concrete_state, &ref_state);
133        }
134
135        Self::teardown(concrete_state, ref_state)
136    }
137}
138
139/// This macro helps to turn a state machine test implementation into a runnable
140/// test. The macro expects a function header whose arguments follow a special
141/// syntax rules: First, we declare if we want to apply the state machine
142/// transitions sequentially or concurrently (currently, only the `sequential`
143/// is supported). Next, we give a range of how many transitions to generate,
144/// followed by `=>` and finally, an identifier that must implement
145/// `StateMachineTest`.
146///
147/// ## Example
148///
149/// ```rust,ignore
150/// struct MyTest;
151///
152/// impl StateMachineTest for MyTest {}
153///
154/// prop_state_machine! {
155///     #[test]
156///     fn run_with_macro(sequential 1..20 => MyTest);
157/// }
158/// ```
159///
160/// This example will expand to:
161///
162/// ```rust,ignore
163/// struct MyTest;
164///
165/// impl StateMachineTest for MyTest {}
166///
167/// proptest! {
168///     #[test]
169///     fn run_with_macro(
170///         (initial_state, transitions) in MyTest::sequential_strategy(1..20)
171///     ) {
172///        MyTest::test_sequential(initial_state, transitions)
173///     }
174/// }
175/// ```
176#[macro_export]
177macro_rules! prop_state_machine {
178    // With proptest config annotation
179    (#![proptest_config($config:expr)]
180    $(
181        $(#[$meta:meta])*
182        fn $test_name:ident(sequential $size:expr => $test:ident $(< $( $ty_param:tt ),+ >)?);
183    )*) => {
184        $(
185            ::proptest::proptest! {
186                #![proptest_config($config)]
187                $(#[$meta])*
188                fn $test_name(
189                    (initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
190                ) {
191
192                    let config = $config.__sugar_to_owned();
193                    <$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(config, initial_state, transitions, seen_counter)
194                }
195            }
196        )*
197    };
198
199    // Without proptest config annotation
200    ($(
201        $(#[$meta:meta])*
202        fn $test_name:ident(sequential $size:expr => $test:ident $(< $( $ty_param:tt ),+ >)?);
203    )*) => {
204        $(
205            ::proptest::proptest! {
206                $(#[$meta])*
207                fn $test_name(
208                    (initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
209                ) {
210                    <$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(
211                        ::proptest::test_runner::Config::default(), initial_state, transitions, seen_counter)
212                }
213            }
214        )*
215    };
216}
217
218#[cfg(test)]
219mod tests {
220
221    mod macro_test {
222        //! tests to verify that invocations of all forms of the
223        //! `prop_state_machine!` macro compile cleanly, and hygenically,
224        //!  as intended.
225
226        /// Note: no imports here, so as to guarantee hygienic macros
227
228        /// A no-op test. Exists strictly as something to reference
229        /// in the macro invocation.
230        struct Test;
231        impl crate::ReferenceStateMachine for Test {
232            type State = ();
233            type Transition = ();
234
235            fn init_state() -> proptest::strategy::BoxedStrategy<Self::State> {
236                use proptest::prelude::*;
237                Just(()).boxed()
238            }
239
240            fn transitions(
241                _: &Self::State,
242            ) -> proptest::strategy::BoxedStrategy<Self::Transition>
243            {
244                use proptest::prelude::*;
245                Just(()).boxed()
246            }
247
248            fn apply(_: Self::State, _: &Self::Transition) -> Self::State {
249                ()
250            }
251        }
252
253        impl crate::StateMachineTest for Test {
254            type SystemUnderTest = ();
255
256            type Reference = Self;
257
258            fn init_test(
259                _: &<Self::Reference as crate::ReferenceStateMachine>::State,
260            ) -> Self::SystemUnderTest {
261            }
262
263            fn apply(
264                _: Self::SystemUnderTest,
265                _: &<Self::Reference as crate::ReferenceStateMachine>::State,
266                _: <Self::Reference as crate::ReferenceStateMachine>::Transition,
267            ) -> Self::SystemUnderTest {
268            }
269        }
270
271        // Invocation of the `prop_state_machine` macro without
272        // a `![proptest_config]` annotation
273        prop_state_machine! {
274            #[test]
275            fn no_config_annotation(sequential 1..2 => Test);
276        }
277
278        // Invocation of the `prop_state_machine` macro with a
279        // `![proptest_config]` annotation
280        prop_state_machine! {
281            #![proptest_config(::proptest::test_runner::Config::default())]
282
283            #[test]
284            fn with_config_annotation(sequential 1..2 => Test);
285        }
286    }
287}