Skip to main content

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::{RefUnwindSafe, UnwindSafe, catch_unwind};
8
9use crate::hints::Hints;
10use crate::runner::Error;
11use crate::runner::{self, LimitSeries};
12use crate::stats::Stats;
13use crate::{Fate, Limit, Prng, hints};
14
15/// An additional regression test that will be run before the random test runs.
16#[derive(Debug, Clone)]
17pub struct Regression {
18    /// The initial state of the number generator the regression test will use for generating
19    /// test data.
20    pub prng: Prng,
21    /// The limit for dynamic data structures the regression test will use for generating
22    /// test data.
23    pub limit: Limit,
24}
25
26/// The configuration for repeated test runs.
27///
28/// It contains parameters for both regression tests and random tests.
29#[derive(Debug, Clone)]
30pub struct Config {
31    /// Additional regression tests that will be run before the random test runs.
32    pub regressions: Vec<Regression>,
33    /// The initial upper limit for the length of generated dynamic data structures
34    ///
35    /// It's used for the first test run. The following test runs use an
36    /// interpolated limit between [`start_limit`] and [`end_limit`].
37    ///
38    /// [`start_limit`]: Config::start_limit
39    /// [`end_limit`]: Config::end_limit
40    pub start_limit: Limit,
41    /// The final upper limit for the length of generated dynamic data structures.
42    ///
43    /// It's used for the last test run. The previous test runs use an interpolated limit
44    /// between [`start_limit`] and [`end_limit`].
45    ///
46    /// [`start_limit`]: Config::start_limit
47    /// [`end_limit`]: Config::end_limit
48    pub end_limit: Limit,
49    /// Defines how many times the test needs to be run without failing.
50    ///
51    /// The runner aborts early if a counterexample has been found.
52    pub passes: u64,
53    /// Defines whether the counterexample will be rerun with enabled hints. The hints will be
54    /// added to the report.
55    ///
56    /// This parameter works only if the feature `hints` is present.
57    pub hints_enabled: bool,
58    /// Defines whether the stats will be enabled during the test runs. The stats will be added
59    /// to the report.
60    ///
61    /// This parameter works only if the feature `stats` is present.
62    pub stats_enabled: bool,
63}
64
65/// Contains details about a failed test run.
66#[derive(Debug)]
67pub struct Counterexample {
68    /// The initial state of the number generator the counterexample has used for generating
69    /// test data.
70    pub prng: Prng,
71    /// The limit for dynamic data structures the counterexample has used for generating
72    /// test data.
73    pub limit: Limit,
74    /// The hints collected during the counterexample run.
75    ///
76    /// If hints are enabled, the runner tries to rerun the counterexample to collect hints.
77    /// Rerunning the counterexample can fail if the test is not deterministic.
78    pub hints: Option<Hints>,
79    /// The error occurred during the counterexample run.
80    pub error: Error,
81}
82
83/// The result of repeated test runs.
84#[derive(Debug)]
85pub struct Report {
86    /// The number of test runs that did not fail.
87    ///
88    /// Both regression tests and random tests are included in this number.
89    pub passes: u64,
90    /// The stats collected during all test runs. It's defined if and only if stats are enabled.
91    pub stats: Option<Stats>,
92    /// If defined it contains the failed test run. Otherwise all test runs were successful.
93    pub counterexample: Option<Counterexample>,
94}
95
96/// Runs the test repeatedly with the given configuration and different seeds.
97///
98/// The test will be run until the configured number of passes has been reached or a test run
99/// has failed.
100pub fn run<T>(prng: Prng, config: &Config, test: T) -> Report
101where
102    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
103{
104    let limit_series = LimitSeries::new(config.start_limit, config.end_limit, config.passes);
105
106    let test_runs = || search_counterexample(&config.regressions, prng, limit_series, &test);
107
108    let ((passes, counterexample_without_hints), stats) =
109        runner::util::collect_stats(config.stats_enabled, test_runs);
110
111    let counterexample = if config.hints_enabled {
112        counterexample_without_hints
113            .map(|counterexample| rerun_counterexample(counterexample, &test))
114    } else {
115        counterexample_without_hints
116    };
117
118    Report {
119        passes,
120        stats,
121        counterexample,
122    }
123}
124
125fn search_counterexample<T>(
126    regressions: &[Regression],
127    mut prng: Prng,
128    limit_series: LimitSeries,
129    test: &T,
130) -> (u64, Option<Counterexample>)
131where
132    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
133{
134    let mut passes = 0;
135
136    for regression in regressions {
137        let test_result = catch_unwind(|| {
138            let mut prng = regression.prng.clone();
139            let fate = Fate::new(&mut prng, regression.limit);
140            test(fate);
141        });
142
143        if let Err(err) = test_result {
144            let counterexample = Counterexample {
145                prng: regression.prng.clone(),
146                limit: regression.limit,
147                hints: None,
148                error: Error(err),
149            };
150            return (passes, Some(counterexample));
151        }
152
153        passes += 1;
154    }
155
156    let mut limits = limit_series.into_iter();
157
158    loop {
159        let limit = match limits.next() {
160            None => return (passes, None),
161            Some(limit) => limit,
162        };
163
164        let prng_before_run = prng.clone();
165
166        let test_result = catch_unwind(|| {
167            let fate = Fate::new(&mut prng, limit);
168            test(fate);
169            prng
170        });
171
172        prng = match test_result {
173            Err(err) => {
174                let counterexample = Counterexample {
175                    prng: prng_before_run,
176                    limit,
177                    hints: None,
178                    error: Error(err),
179                };
180                return (passes, Some(counterexample));
181            }
182            Ok(prng_after_run) => prng_after_run,
183        };
184
185        passes += 1;
186    }
187}
188
189fn rerun_counterexample<T>(counterexample: Counterexample, test: &T) -> Counterexample
190where
191    T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
192{
193    let (test_result, hints) = {
194        let mut prng = counterexample.prng.clone();
195        let limit = counterexample.limit;
196        hints::collect(|| {
197            catch_unwind(move || {
198                let fate = Fate::new(&mut prng, limit);
199                test(fate)
200            })
201        })
202    };
203
204    match test_result {
205        Ok(()) => counterexample,
206        Err(err) => Counterexample {
207            hints: Some(hints),
208            error: Error(err),
209            ..counterexample
210        },
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use core::panic;
217    use std::sync::atomic::{AtomicU64, Ordering};
218
219    use crate::runner::repeatedly::{Config, run};
220    use crate::{Prng, Seed, hints};
221
222    use super::Regression;
223
224    fn default_prng() -> Prng {
225        Prng::from_seed(Seed::from(42))
226    }
227
228    fn default_config() -> Config {
229        Config {
230            regressions: Vec::new(),
231            start_limit: 0.into(),
232            end_limit: 100.into(),
233            passes: 100,
234            hints_enabled: true,
235            stats_enabled: false,
236        }
237    }
238
239    fn regression(seed: u64) -> Regression {
240        let seed = Seed(seed);
241        let prng = Prng::from_seed(seed);
242        Regression {
243            prng,
244            limit: 42.into(),
245        }
246    }
247
248    #[test]
249    fn zero_passes_if_test_fails() {
250        let config = default_config();
251        let report = run(default_prng(), &config, |_| panic!());
252        assert_eq!(report.passes, 0);
253    }
254
255    #[test]
256    fn zero_passes_if_test_fails_with_regressions() {
257        let mut config = default_config();
258        config.regressions = vec![regression(123), regression(321)];
259        let report = run(default_prng(), &config, |_| panic!());
260        assert_eq!(report.passes, 0);
261    }
262
263    #[test]
264    fn mixed_passes_if_test_fails_later() {
265        let counter = AtomicU64::new(1);
266        let config = default_config();
267        let report = run(default_prng(), &config, |_| {
268            let run = counter.fetch_add(1, Ordering::Relaxed);
269            if run == 10 {
270                panic!()
271            }
272        });
273        assert_eq!(report.passes, 9);
274    }
275
276    #[test]
277    fn mixed_passes_if_test_fails_later_with_regressions() {
278        let counter = AtomicU64::new(1);
279        let mut config = default_config();
280        config.regressions = vec![regression(123), regression(321)];
281        let report = run(default_prng(), &config, |_| {
282            let run = counter.fetch_add(1, Ordering::Relaxed);
283            if run == 10 {
284                panic!()
285            }
286        });
287        assert_eq!(report.passes, 9);
288    }
289
290    #[test]
291    fn mixed_passes_if_regression_fails() {
292        let counter = AtomicU64::new(1);
293        let mut config = default_config();
294        config.regressions = vec![regression(123), regression(321)];
295        let report = run(default_prng(), &config, |_| {
296            let run = counter.fetch_add(1, Ordering::Relaxed);
297            if run == 2 {
298                panic!()
299            }
300        });
301        assert_eq!(report.passes, 1);
302    }
303
304    #[test]
305    fn full_passes_if_test_succeeds() {
306        let config = default_config();
307        let report = run(default_prng(), &config, |_| ());
308        assert_eq!(report.passes, config.passes);
309    }
310
311    #[test]
312    fn full_passes_if_test_succeeds_with_regressions() {
313        let mut config = default_config();
314        config.regressions = vec![regression(123), regression(321)];
315        let report = run(default_prng(), &config, |_| ());
316        assert_eq!(report.passes, config.passes + 2);
317    }
318
319    #[test]
320    fn has_counterproof_if_test_fails() {
321        let config = default_config();
322        let report = run(default_prng(), &config, |_| panic!());
323        assert!(report.counterexample.is_some());
324    }
325
326    #[test]
327    fn has_counterproof_if_test_fails_with_regressions() {
328        let mut config = default_config();
329        config.regressions = vec![regression(123), regression(321)];
330        let report = run(default_prng(), &config, |_| panic!());
331        assert!(report.counterexample.is_some());
332    }
333
334    #[test]
335    fn no_counterproof_if_test_succeeds() {
336        let config = default_config();
337        let report = run(default_prng(), &config, |_| ());
338        assert!(report.counterexample.is_none());
339    }
340
341    #[test]
342    fn no_hints_if_disabled() {
343        let config = Config {
344            hints_enabled: false,
345            ..default_config()
346        };
347        let report = run(default_prng(), &config, |_| panic!());
348        let counterexample = report.counterexample.unwrap();
349        assert!(counterexample.hints.is_none());
350    }
351
352    #[test]
353    fn no_hints_if_enabled_but_failure_not_reproducible() {
354        if cfg!(feature = "hints") {
355            let config = Config {
356                hints_enabled: true,
357                passes: 1,
358                ..default_config()
359            };
360
361            for _ in 0..10 {
362                let (report, has_failed) = hints::collect(|| {
363                    run(default_prng(), &config, |_| {
364                        let should_fail = Seed::random().0.is_multiple_of(2);
365
366                        hints::add(|| format!("{}", should_fail));
367
368                        if should_fail {
369                            panic!();
370                        }
371                    })
372                });
373
374                let failure_was_not_reproducible =
375                    &has_failed.0[0].text == "true" && &has_failed.0[1].text == "false";
376
377                if failure_was_not_reproducible {
378                    let counterexample = report.counterexample.unwrap();
379                    assert!(counterexample.hints.is_none());
380                }
381            }
382        }
383    }
384
385    #[test]
386    fn has_hints_if_enabled_and_test_deterministic() {
387        let config = Config {
388            hints_enabled: true,
389            ..default_config()
390        };
391        let report = run(default_prng(), &config, |_| panic!());
392        let counterexample = report.counterexample.unwrap();
393        assert!(counterexample.hints.is_some());
394    }
395
396    #[test]
397    fn no_stats_if_disabled_and_test_succeeds() {
398        let config = Config {
399            stats_enabled: false,
400            ..default_config()
401        };
402        let report = run(default_prng(), &config, |_| ());
403        let stats = report.stats;
404        assert!(stats.is_none());
405    }
406
407    #[test]
408    fn no_stats_if_disabled_and_test_fails() {
409        let config = Config {
410            stats_enabled: false,
411            ..default_config()
412        };
413        let report = run(default_prng(), &config, |_| panic!());
414        let stats = report.stats;
415        assert!(stats.is_none());
416    }
417
418    #[test]
419    fn has_stats_if_enabled_test_succeeds() {
420        let config = Config {
421            stats_enabled: true,
422            ..default_config()
423        };
424        let report = run(default_prng(), &config, |_| ());
425        let stats = report.stats;
426        assert!(stats.is_some());
427    }
428
429    #[test]
430    fn has_stats_if_enabled_and_test_fails() {
431        let config = Config {
432            stats_enabled: true,
433            ..default_config()
434        };
435        let report = run(default_prng(), &config, |_| panic!());
436        let stats = report.stats;
437        assert!(stats.is_some());
438    }
439}