1use 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#[derive(Debug, Clone)]
17pub struct Regression {
18 pub prng: Prng,
21 pub limit: Limit,
24}
25
26#[derive(Debug, Clone)]
30pub struct Config {
31 pub regressions: Vec<Regression>,
33 pub start_limit: Limit,
41 pub end_limit: Limit,
49 pub passes: u64,
53 pub hints_enabled: bool,
58 pub stats_enabled: bool,
63}
64
65#[derive(Debug)]
67pub struct Counterexample {
68 pub prng: Prng,
71 pub limit: Limit,
74 pub hints: Option<Hints>,
79 pub error: Error,
81}
82
83#[derive(Debug)]
85pub struct Report {
86 pub passes: u64,
90 pub stats: Option<Stats>,
92 pub counterexample: Option<Counterexample>,
94}
95
96pub 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}