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}