dicetest/runner/
repeatedly.rs

1//! Provides a runner function that runs a test repeatedly.
2//!
3//! This runner function can be used for property testing because it tries to falsify the
4//! test assertions by running the test with different seeds. If the test panics, a counterexample
5//! has been found.
6
7use std::panic::{catch_unwind, RefUnwindSafe, UnwindSafe};
8
9use crate::hints::Hints;
10use crate::runner::Error;
11use crate::runner::{self, LimitSeries};
12use crate::stats::Stats;
13use crate::{hints, Fate, Limit, Prng};
14
15/// The configuration for repeated test runs.
16#[derive(Debug, Clone)]
17pub struct Config {
18    /// The initial upper limit for the length of generated dynamic data structures
19    ///
20    /// It's used for the first test run. The following test runs use an interpolated limit
21    /// between [`start_limit`] and [`end_limit`].
22    ///
23    /// [`start_limit`]: Config::start_limit
24    /// [`end_limit`]: Config::end_limit
25    pub start_limit: Limit,
26    /// The final upper limit for the length of generated dynamic data structures.
27    ///
28    /// It's used for the last test run. The previous test runs use an interpolated limit
29    /// between [`start_limit`] and [`end_limit`].
30    ///
31    /// [`start_limit`]: Config::start_limit
32    /// [`end_limit`]: Config::end_limit
33    pub end_limit: Limit,
34    /// Defines how many times the test needs to be run without failing.
35    ///
36    /// The runner aborts early if a counterexample has been found.
37    pub passes: u64,
38    /// Defines whether the counterexample will be rerun with enabled hints. The hints will be
39    /// added to the report.
40    ///
41    /// This parameter works only if the feature `hints` is present.
42    pub hints_enabled: bool,
43    /// Defines whether the stats will be enabled during the test runs. The stats will be added
44    /// to the report.
45    ///
46    /// This parameter works only if the feature `stats` is present.
47    pub stats_enabled: bool,
48}
49
50/// Contains details about a failed test run.
51#[derive(Debug)]
52pub struct Counterexample {
53    /// The initial state of the number generator the counterexample has used for generating
54    /// test data.
55    pub prng: Prng,
56    /// The limit for dynamic data structures the counterexample has used for generating
57    /// test data.
58    pub limit: Limit,
59    /// The hints collected during the counterexample run.
60    ///
61    /// If hints are enabled, the runner tries to rerun the counterexample to collect hints.
62    /// Rerunning the counterexample can fail if the test is not deterministic.
63    pub hints: Option<Hints>,
64    /// The error occurred during the counterexample run.
65    pub error: Error,
66}
67
68/// The result of repeated test runs.
69#[derive(Debug)]
70pub struct Report {
71    /// The number of test runs that did not fail.
72    pub passes: u64,
73    /// The stats collected during all test runs. It's defined if and only if stats are enabled.
74    pub stats: Option<Stats>,
75    /// If defined it contains the failed test run. Otherwise all test runs were successful.
76    pub counterexample: Option<Counterexample>,
77}
78
79/// Runs the test repeatedly with the given configuration and different seeds.
80///
81/// The test will be run until the configured number of passes has been reached or a test run
82/// has failed.
83pub fn run<T>(prng: Prng, config: &Config, test: T) -> Report
84where
85    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
86{
87    let limit_series = LimitSeries::new(config.start_limit, config.end_limit, config.passes);
88
89    let test_runs = || search_counterexample(prng, limit_series, &test);
90
91    let ((passes, counterexample_without_hints), stats) =
92        runner::util::collect_stats(config.stats_enabled, test_runs);
93
94    let counterexample = if config.hints_enabled {
95        counterexample_without_hints
96            .map(|counterexample| rerun_counterexample(counterexample, &test))
97    } else {
98        counterexample_without_hints
99    };
100
101    Report {
102        passes,
103        stats,
104        counterexample,
105    }
106}
107
108fn search_counterexample<T>(
109    mut prng: Prng,
110    limit_series: LimitSeries,
111    test: &T,
112) -> (u64, Option<Counterexample>)
113where
114    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
115{
116    let mut passes = 0;
117    let mut limits = limit_series.into_iter();
118
119    let counterexample = loop {
120        let limit = match limits.next() {
121            None => break None,
122            Some(limit) => limit,
123        };
124
125        let prng_before_run = prng.clone();
126
127        let test_result = catch_unwind(|| {
128            let fate = Fate::new(&mut prng, limit);
129            test(fate);
130            prng
131        });
132
133        prng = match test_result {
134            Err(err) => {
135                let counterexample = Counterexample {
136                    prng: prng_before_run,
137                    limit,
138                    hints: None,
139                    error: Error(err),
140                };
141                break Some(counterexample);
142            }
143            Ok(prng_after_run) => prng_after_run,
144        };
145
146        passes += 1;
147    };
148
149    (passes, counterexample)
150}
151
152fn rerun_counterexample<T>(counterexample: Counterexample, test: &T) -> Counterexample
153where
154    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
155{
156    let (test_result, hints) = {
157        let mut prng = counterexample.prng.clone();
158        let limit = counterexample.limit;
159        hints::collect(|| {
160            catch_unwind(move || {
161                let fate = Fate::new(&mut prng, limit);
162                test(fate)
163            })
164        })
165    };
166
167    match test_result {
168        Ok(()) => counterexample,
169        Err(err) => Counterexample {
170            hints: Some(hints),
171            error: Error(err),
172            ..counterexample
173        },
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use crate::runner::repeatedly::{run, Config};
180    use crate::{hints, Prng, Seed};
181
182    fn default_prng() -> Prng {
183        Prng::from_seed(Seed::from(42))
184    }
185
186    fn default_config() -> Config {
187        Config {
188            start_limit: 0.into(),
189            end_limit: 100.into(),
190            passes: 100,
191            hints_enabled: true,
192            stats_enabled: false,
193        }
194    }
195
196    #[test]
197    fn zero_passes_if_test_fails() {
198        let config = default_config();
199        let report = run(default_prng(), &config, |_| panic!());
200        assert_eq!(report.passes, 0);
201    }
202
203    #[test]
204    fn full_passes_if_test_succeeds() {
205        let config = default_config();
206        let report = run(default_prng(), &config, |_| ());
207        assert_eq!(report.passes, config.passes);
208    }
209
210    #[test]
211    fn has_counterproof_if_test_fails() {
212        let config = default_config();
213        let report = run(default_prng(), &config, |_| panic!());
214        assert!(report.counterexample.is_some());
215    }
216
217    #[test]
218    fn no_counterproof_if_test_succeeds() {
219        let config = default_config();
220        let report = run(default_prng(), &config, |_| ());
221        assert!(report.counterexample.is_none());
222    }
223
224    #[test]
225    fn no_hints_if_disabled() {
226        let config = Config {
227            hints_enabled: false,
228            ..default_config()
229        };
230        let report = run(default_prng(), &config, |_| panic!());
231        let counterexample = report.counterexample.unwrap();
232        assert!(counterexample.hints.is_none());
233    }
234
235    #[test]
236    fn no_hints_if_enabled_but_failure_not_reproducible() {
237        if cfg!(feature = "hints") {
238            let config = Config {
239                hints_enabled: true,
240                passes: 1,
241                ..default_config()
242            };
243
244            for _ in 0..10 {
245                let (report, has_failed) = hints::collect(|| {
246                    run(default_prng(), &config, |_| {
247                        let should_fail = Seed::random().0 % 2 == 0;
248
249                        hints::add(|| format!("{}", should_fail));
250
251                        if should_fail {
252                            panic!();
253                        }
254                    })
255                });
256
257                let failure_was_not_reproducible =
258                    &has_failed.0[0].text == "true" && &has_failed.0[1].text == "false";
259
260                if failure_was_not_reproducible {
261                    let counterexample = report.counterexample.unwrap();
262                    assert!(counterexample.hints.is_none());
263                }
264            }
265        }
266    }
267
268    #[test]
269    fn has_hints_if_enabled_and_test_deterministic() {
270        let config = Config {
271            hints_enabled: true,
272            ..default_config()
273        };
274        let report = run(default_prng(), &config, |_| panic!());
275        let counterexample = report.counterexample.unwrap();
276        assert!(counterexample.hints.is_some());
277    }
278
279    #[test]
280    fn no_stats_if_disabled_and_test_succeeds() {
281        let config = Config {
282            stats_enabled: false,
283            ..default_config()
284        };
285        let report = run(default_prng(), &config, |_| ());
286        let stats = report.stats;
287        assert!(stats.is_none());
288    }
289
290    #[test]
291    fn no_stats_if_disabled_and_test_fails() {
292        let config = Config {
293            stats_enabled: false,
294            ..default_config()
295        };
296        let report = run(default_prng(), &config, |_| panic!());
297        let stats = report.stats;
298        assert!(stats.is_none());
299    }
300
301    #[test]
302    fn has_stats_if_enabled_test_succeeds() {
303        let config = Config {
304            stats_enabled: true,
305            ..default_config()
306        };
307        let report = run(default_prng(), &config, |_| ());
308        let stats = report.stats;
309        assert!(stats.is_some());
310    }
311
312    #[test]
313    fn has_stats_if_enabled_and_test_fails() {
314        let config = Config {
315            stats_enabled: true,
316            ..default_config()
317        };
318        let report = run(default_prng(), &config, |_| panic!());
319        let stats = report.stats;
320        assert!(stats.is_some());
321    }
322}