proptest/test_runner/config.rs
1//-
2// Copyright 2017, 2018, 2019 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
10use crate::std_facade::Box;
11use core::{fmt, str, u32};
12
13use crate::test_runner::result_cache::{noop_result_cache, ResultCache};
14use crate::test_runner::rng::RngAlgorithm;
15use crate::test_runner::FailurePersistence;
16
17/// Override the config fields from environment variables, if any are set.
18/// Without the `std` feature this function returns config unchanged.
19#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
20pub fn contextualize_config(mut result: Config) -> Config {
21 use std::env;
22 use std::ffi::OsString;
23 use std::fmt;
24 use std::str::FromStr;
25
26 const CASES: &str = "PROPTEST_CASES";
27
28 const MAX_LOCAL_REJECTS: &str = "PROPTEST_MAX_LOCAL_REJECTS";
29 const MAX_GLOBAL_REJECTS: &str = "PROPTEST_MAX_GLOBAL_REJECTS";
30 const MAX_FLAT_MAP_REGENS: &str = "PROPTEST_MAX_FLAT_MAP_REGENS";
31 const MAX_SHRINK_TIME: &str = "PROPTEST_MAX_SHRINK_TIME";
32 const MAX_SHRINK_ITERS: &str = "PROPTEST_MAX_SHRINK_ITERS";
33 const MAX_DEFAULT_SIZE_RANGE: &str = "PROPTEST_MAX_DEFAULT_SIZE_RANGE";
34 #[cfg(feature = "fork")]
35 const FORK: &str = "PROPTEST_FORK";
36 #[cfg(feature = "timeout")]
37 const TIMEOUT: &str = "PROPTEST_TIMEOUT";
38 const VERBOSE: &str = "PROPTEST_VERBOSE";
39 const RNG_ALGORITHM: &str = "PROPTEST_RNG_ALGORITHM";
40 const RNG_SEED: &str = "PROPTEST_RNG_SEED";
41 const DISABLE_FAILURE_PERSISTENCE: &str =
42 "PROPTEST_DISABLE_FAILURE_PERSISTENCE";
43
44 fn parse_or_warn<T: FromStr + fmt::Display>(
45 src: &OsString,
46 dst: &mut T,
47 typ: &str,
48 var: &str,
49 ) {
50 if let Some(src) = src.to_str() {
51 if let Ok(value) = src.parse() {
52 *dst = value;
53 } else {
54 eprintln!(
55 "proptest: The env-var {}={} can't be parsed as {}, \
56 using default of {}.",
57 var, src, typ, *dst
58 );
59 }
60 } else {
61 eprintln!(
62 "proptest: The env-var {} is not valid, using \
63 default of {}.",
64 var, *dst
65 );
66 }
67 }
68
69 for (var, value) in
70 env::vars_os().filter_map(|(k, v)| k.into_string().ok().map(|k| (k, v)))
71 {
72 let var = var.as_str();
73
74 #[cfg(feature = "fork")]
75 if var == FORK {
76 parse_or_warn(&value, &mut result.fork, "bool", FORK);
77 continue;
78 }
79
80 #[cfg(feature = "timeout")]
81 if var == TIMEOUT {
82 parse_or_warn(&value, &mut result.timeout, "timeout", TIMEOUT);
83 continue;
84 }
85
86 if var == CASES {
87 parse_or_warn(&value, &mut result.cases, "u32", CASES);
88 } else if var == MAX_LOCAL_REJECTS {
89 parse_or_warn(
90 &value,
91 &mut result.max_local_rejects,
92 "u32",
93 MAX_LOCAL_REJECTS,
94 );
95 } else if var == MAX_GLOBAL_REJECTS {
96 parse_or_warn(
97 &value,
98 &mut result.max_global_rejects,
99 "u32",
100 MAX_GLOBAL_REJECTS,
101 );
102 } else if var == MAX_FLAT_MAP_REGENS {
103 parse_or_warn(
104 &value,
105 &mut result.max_flat_map_regens,
106 "u32",
107 MAX_FLAT_MAP_REGENS,
108 );
109 } else if var == MAX_SHRINK_TIME {
110 parse_or_warn(
111 &value,
112 &mut result.max_shrink_time,
113 "u32",
114 MAX_SHRINK_TIME,
115 );
116 } else if var == MAX_SHRINK_ITERS {
117 parse_or_warn(
118 &value,
119 &mut result.max_shrink_iters,
120 "u32",
121 MAX_SHRINK_ITERS,
122 );
123 } else if var == MAX_DEFAULT_SIZE_RANGE {
124 parse_or_warn(
125 &value,
126 &mut result.max_default_size_range,
127 "usize",
128 MAX_DEFAULT_SIZE_RANGE,
129 );
130 } else if var == VERBOSE {
131 parse_or_warn(&value, &mut result.verbose, "u32", VERBOSE);
132 } else if var == RNG_ALGORITHM {
133 parse_or_warn(
134 &value,
135 &mut result.rng_algorithm,
136 "RngAlgorithm",
137 RNG_ALGORITHM,
138 );
139 } else if var == RNG_SEED {
140 parse_or_warn(
141 &value,
142 &mut result.rng_seed,
143 "u64",
144 RNG_SEED,
145 );
146 } else if var == DISABLE_FAILURE_PERSISTENCE {
147 result.failure_persistence = None;
148 } else if var.starts_with("PROPTEST_") {
149 eprintln!("proptest: Ignoring unknown env-var {}.", var);
150 }
151 }
152
153 result
154}
155
156/// Without the `std` feature this function returns config unchanged.
157#[cfg(not(all(feature = "std", not(target_arch = "wasm32"))))]
158pub fn contextualize_config(result: Config) -> Config {
159 result
160}
161
162fn default_default_config() -> Config {
163 Config {
164 cases: 256,
165 max_local_rejects: 65_536,
166 max_global_rejects: 1024,
167 max_flat_map_regens: 1_000_000,
168 failure_persistence: None,
169 source_file: None,
170 test_name: None,
171 #[cfg(feature = "fork")]
172 fork: false,
173 #[cfg(feature = "timeout")]
174 timeout: 0,
175 #[cfg(feature = "std")]
176 max_shrink_time: 0,
177 max_shrink_iters: u32::MAX,
178 max_default_size_range: 100,
179 result_cache: noop_result_cache,
180 #[cfg(feature = "std")]
181 verbose: 0,
182 rng_algorithm: RngAlgorithm::default(),
183 rng_seed: RngSeed::Random,
184 _non_exhaustive: (),
185 }
186}
187
188// The default config, computed by combining environment variables and
189// defaults.
190#[cfg(feature = "std")]
191static DEFAULT_CONFIG: std::sync::LazyLock<Config> = std::sync::LazyLock::new(|| {
192 let mut default_config = default_default_config();
193 default_config.failure_persistence = Some(Box::new(crate::test_runner::FileFailurePersistence::default()));
194 contextualize_config(default_config)
195});
196
197/// The seed for the RNG, can either be random or specified as a u64.
198#[derive(Debug, Clone, Copy, PartialEq)]
199pub enum RngSeed {
200 /// Default case, use a random value
201 Random,
202 /// Use a specific value to generate a seed
203 Fixed(u64)
204}
205
206impl str::FromStr for RngSeed {
207 type Err = ();
208 fn from_str(s: &str) -> Result<Self, Self::Err> {
209 s.parse::<u64>().map(RngSeed::Fixed).map_err(|_| ())
210 }
211}
212
213impl fmt::Display for RngSeed {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match self {
216 RngSeed::Random => write!(f, "random"),
217 RngSeed::Fixed(n) => write!(f, "{}", n),
218 }
219 }
220}
221
222/// Configuration for how a proptest test should be run.
223#[derive(Clone, Debug, PartialEq)]
224pub struct Config {
225 /// The number of successful test cases that must execute for the test as a
226 /// whole to pass.
227 ///
228 /// This does not include implicitly-replayed persisted failing cases.
229 ///
230 /// The default is 256, which can be overridden by setting the
231 /// `PROPTEST_CASES` environment variable. (The variable is only considered
232 /// when the `std` feature is enabled, which it is by default.)
233 pub cases: u32,
234
235 /// The maximum number of individual inputs that may be rejected before the
236 /// test as a whole aborts.
237 ///
238 /// The default is 65536, which can be overridden by setting the
239 /// `PROPTEST_MAX_LOCAL_REJECTS` environment variable. (The variable is only
240 /// considered when the `std` feature is enabled, which it is by default.)
241 pub max_local_rejects: u32,
242
243 /// The maximum number of combined inputs that may be rejected before the
244 /// test as a whole aborts.
245 ///
246 /// The default is 1024, which can be overridden by setting the
247 /// `PROPTEST_MAX_GLOBAL_REJECTS` environment variable. (The variable is
248 /// only considered when the `std` feature is enabled, which it is by
249 /// default.)
250 pub max_global_rejects: u32,
251
252 /// The maximum number of times all `Flatten` combinators will attempt to
253 /// regenerate values. This puts a limit on the worst-case exponential
254 /// explosion that can happen with nested `Flatten`s.
255 ///
256 /// The default is 1_000_000, which can be overridden by setting the
257 /// `PROPTEST_MAX_FLAT_MAP_REGENS` environment variable. (The variable is
258 /// only considered when the `std` feature is enabled, which it is by
259 /// default.)
260 pub max_flat_map_regens: u32,
261
262 /// Indicates whether and how to persist failed test results.
263 ///
264 /// When compiling with "std" feature (i.e. the standard library is available), the default
265 /// is `Some(Box::new(FileFailurePersistence::SourceParallel("proptest-regressions")))`.
266 ///
267 /// Without the standard library, the default is `None`, and no persistence occurs.
268 ///
269 /// See the docs of [`FileFailurePersistence`](enum.FileFailurePersistence.html)
270 /// and [`MapFailurePersistence`](struct.MapFailurePersistence.html) for more information.
271 ///
272 /// You can disable failure persistence with the `PROPTEST_DISABLE_FAILURE_PERSISTENCE`
273 /// environment variable but its not currently possible to set the persistence file
274 /// with an environment variable. (The variable is
275 /// only considered when the `std` feature is enabled, which it is by
276 /// default.)
277 pub failure_persistence: Option<Box<dyn FailurePersistence>>,
278
279 /// File location of the current test, relevant for persistence
280 /// and debugging.
281 ///
282 /// Note the use of `&str` rather than `Path` to be compatible with
283 /// `#![no_std]` use cases where `Path` is unavailable.
284 ///
285 /// See the docs of [`FileFailurePersistence`](enum.FileFailurePersistence.html)
286 /// for more information on how it may be used for persistence.
287 pub source_file: Option<&'static str>,
288
289 /// The fully-qualified name of the test being run, as would be passed to
290 /// the test executable to run just that test.
291 ///
292 /// This must be set if `fork` is `true`. Otherwise, it is unused. It is
293 /// automatically set by `proptest!`.
294 ///
295 /// This must include the crate name at the beginning, as produced by
296 /// `module_path!()`.
297 pub test_name: Option<&'static str>,
298
299 /// If true, tests are run in a subprocess.
300 ///
301 /// Forking allows proptest to work with tests which may fail by aborting
302 /// the process, causing a segmentation fault, etc, but can be a lot slower
303 /// in certain environments or when running a very large number of tests.
304 ///
305 /// For forking to work correctly, both the `Strategy` and the content of
306 /// the test case itself must be deterministic.
307 ///
308 /// This requires the "fork" feature, enabled by default.
309 ///
310 /// The default is `false`, which can be overridden by setting the
311 /// `PROPTEST_FORK` environment variable. (The variable is
312 /// only considered when the `std` feature is enabled, which it is by
313 /// default.)
314 #[cfg(feature = "fork")]
315 #[cfg_attr(docsrs, doc(cfg(feature = "fork")))]
316 pub fork: bool,
317
318 /// If non-zero, tests are run in a subprocess and each generated case
319 /// fails if it takes longer than this number of milliseconds.
320 ///
321 /// This implicitly enables forking, even if the `fork` field is `false`.
322 ///
323 /// The type here is plain `u32` (rather than
324 /// `Option<std::time::Duration>`) for the sake of ergonomics.
325 ///
326 /// This requires the "timeout" feature, enabled by default.
327 ///
328 /// Setting a timeout to less than the time it takes the process to start
329 /// up and initialise the first test case will cause the whole test to be
330 /// aborted.
331 ///
332 /// The default is `0` (i.e., no timeout), which can be overridden by
333 /// setting the `PROPTEST_TIMEOUT` environment variable. (The variable is
334 /// only considered when the `std` feature is enabled, which it is by
335 /// default.)
336 #[cfg(feature = "timeout")]
337 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
338 pub timeout: u32,
339
340 /// If non-zero, give up the shrinking process after this many milliseconds
341 /// have elapsed since the start of the shrinking process.
342 ///
343 /// This will not cause currently running test cases to be interrupted.
344 ///
345 /// This configuration is only available when the `std` feature is enabled
346 /// (which it is by default).
347 ///
348 /// The default is `0` (i.e., no limit), which can be overridden by setting
349 /// the `PROPTEST_MAX_SHRINK_TIME` environment variable. (The variable is
350 /// only considered when the `std` feature is enabled, which it is by
351 /// default.)
352 #[cfg(feature = "std")]
353 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
354 pub max_shrink_time: u32,
355
356 /// Give up on shrinking if more than this number of iterations of the test
357 /// code are run.
358 ///
359 /// Setting this to `std::u32::MAX` causes the actual limit to be four
360 /// times the number of test cases.
361 ///
362 /// Setting this value to `0` disables shrinking altogether.
363 ///
364 /// Note that the type of this field will change in a future version of
365 /// proptest to better accommodate its special values.
366 ///
367 /// The default is `std::u32::MAX`, which can be overridden by setting the
368 /// `PROPTEST_MAX_SHRINK_ITERS` environment variable. (The variable is only
369 /// considered when the `std` feature is enabled, which it is by default.)
370 pub max_shrink_iters: u32,
371
372 /// The default maximum size to `proptest::collection::SizeRange`. The default
373 /// strategy for collections (like `Vec`) use collections in the range of
374 /// `0..max_default_size_range`.
375 ///
376 /// The default is `100` which can be overridden by setting the
377 /// `PROPTEST_MAX_DEFAULT_SIZE_RANGE` environment variable. (The variable
378 /// is only considered when the `std` feature is enabled, which it is by
379 /// default.)
380 pub max_default_size_range: usize,
381
382 /// A function to create new result caches.
383 ///
384 /// The default is to do no caching. The easiest way to enable caching is
385 /// to set this field to `basic_result_cache` (though that is currently
386 /// only available with the `std` feature).
387 ///
388 /// This is useful for strategies which have a tendency to produce
389 /// duplicate values, or for tests where shrinking can take a very long
390 /// time due to exploring the same output multiple times.
391 ///
392 /// When caching is enabled, generated values themselves are not stored, so
393 /// this does not pose a risk of memory exhaustion for large test inputs
394 /// unless using extraordinarily large test case counts.
395 ///
396 /// Caching incurs its own overhead, and may very well make your test run
397 /// more slowly.
398 pub result_cache: fn() -> Box<dyn ResultCache>,
399
400 /// Set to non-zero values to cause proptest to emit human-targeted
401 /// messages to stderr as it runs.
402 ///
403 /// Greater values cause greater amounts of logs to be emitted. The exact
404 /// meaning of certain levels other than 0 is subject to change.
405 ///
406 /// - 0: No extra output.
407 /// - 1: Log test failure messages. In state machine tests, this level is
408 /// used to print transitions.
409 /// - 2: Trace low-level details.
410 ///
411 /// This is only available with the `std` feature (enabled by default)
412 /// since on nostd proptest has no way to produce output.
413 ///
414 /// The default is `0`, which can be overridden by setting the
415 /// `PROPTEST_VERBOSE` environment variable. (The variable is only considered
416 /// when the `std` feature is enabled, which it is by default.)
417 #[cfg(feature = "std")]
418 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
419 pub verbose: u32,
420
421 /// The RNG algorithm to use when not using a user-provided RNG.
422 ///
423 /// The default is `RngAlgorithm::default()`, which can be overridden by
424 /// setting the `PROPTEST_RNG_ALGORITHM` environment variable to one of the following:
425 ///
426 /// - `xs` — `RngAlgorithm::XorShift`
427 /// - `cc` — `RngAlgorithm::ChaCha`
428 ///
429 /// (The variable is only considered when the `std` feature is enabled,
430 /// which it is by default.)
431 pub rng_algorithm: RngAlgorithm,
432
433 /// Seed used for the RNG. Set by using the PROPTEST_RNG_SEED environment variable
434 /// If the environment variable is undefined, a random seed is generated (this is the default option).
435 pub rng_seed: RngSeed,
436
437 // Needs to be public so FRU syntax can be used.
438 #[doc(hidden)]
439 pub _non_exhaustive: (),
440}
441
442impl Config {
443 /// Constructs a `Config` only differing from the `default()` in the
444 /// number of test cases required to pass the test successfully.
445 ///
446 /// This is simply a more concise alternative to using field-record update
447 /// syntax:
448 ///
449 /// ```
450 /// # use proptest::test_runner::Config;
451 /// assert_eq!(
452 /// Config::with_cases(42),
453 /// Config { cases: 42, .. Config::default() }
454 /// );
455 /// ```
456 pub fn with_cases(cases: u32) -> Self {
457 Self {
458 cases,
459 ..Config::default()
460 }
461 }
462
463 /// Constructs a `Config` only differing from the `default()` in the
464 /// source_file of the present test.
465 ///
466 /// This is simply a more concise alternative to using field-record update
467 /// syntax:
468 ///
469 /// ```
470 /// # use proptest::test_runner::Config;
471 /// assert_eq!(
472 /// Config::with_source_file("computer/question"),
473 /// Config { source_file: Some("computer/question"), .. Config::default() }
474 /// );
475 /// ```
476 pub fn with_source_file(source_file: &'static str) -> Self {
477 Self {
478 source_file: Some(source_file),
479 ..Config::default()
480 }
481 }
482
483 /// Constructs a `Config` only differing from the provided Config instance, `self`,
484 /// in the source_file of the present test.
485 ///
486 /// This is simply a more concise alternative to using field-record update
487 /// syntax:
488 ///
489 /// ```
490 /// # use proptest::test_runner::Config;
491 /// let a = Config::with_source_file("computer/question");
492 /// let b = a.clone_with_source_file("answer/42");
493 /// assert_eq!(
494 /// a,
495 /// Config { source_file: Some("computer/question"), .. Config::default() }
496 /// );
497 /// assert_eq!(
498 /// b,
499 /// Config { source_file: Some("answer/42"), .. Config::default() }
500 /// );
501 /// ```
502 pub fn clone_with_source_file(&self, source_file: &'static str) -> Self {
503 let mut result = self.clone();
504 result.source_file = Some(source_file);
505 result
506 }
507
508 /// Constructs a `Config` only differing from the `default()` in the
509 /// failure_persistence member.
510 ///
511 /// This is simply a more concise alternative to using field-record update
512 /// syntax:
513 ///
514 /// ```
515 /// # use proptest::test_runner::{Config, FileFailurePersistence};
516 /// assert_eq!(
517 /// Config::with_failure_persistence(FileFailurePersistence::WithSource("regressions")),
518 /// Config {
519 /// failure_persistence: Some(Box::new(FileFailurePersistence::WithSource("regressions"))),
520 /// .. Config::default()
521 /// }
522 /// );
523 /// ```
524 pub fn with_failure_persistence<T>(failure_persistence: T) -> Self
525 where
526 T: FailurePersistence + 'static,
527 {
528 Self {
529 failure_persistence: Some(Box::new(failure_persistence)),
530 ..Default::default()
531 }
532 }
533
534 /// Return whether this configuration implies forking.
535 ///
536 /// This method exists even if the "fork" feature is disabled, in which
537 /// case it simply returns false.
538 pub fn fork(&self) -> bool {
539 self._fork() || self.timeout() > 0
540 }
541
542 #[cfg(feature = "fork")]
543 fn _fork(&self) -> bool {
544 self.fork
545 }
546
547 #[cfg(not(feature = "fork"))]
548 fn _fork(&self) -> bool {
549 false
550 }
551
552 /// Returns the configured timeout.
553 ///
554 /// This method exists even if the "timeout" feature is disabled, in which
555 /// case it simply returns 0.
556 #[cfg(feature = "timeout")]
557 pub fn timeout(&self) -> u32 {
558 self.timeout
559 }
560
561 /// Returns the configured timeout.
562 ///
563 /// This method exists even if the "timeout" feature is disabled, in which
564 /// case it simply returns 0.
565 #[cfg(not(feature = "timeout"))]
566 pub fn timeout(&self) -> u32 {
567 0
568 }
569
570 /// Returns the configured limit on shrinking iterations.
571 ///
572 /// This takes into account the special "automatic" behaviour.
573 pub fn max_shrink_iters(&self) -> u32 {
574 if u32::MAX == self.max_shrink_iters {
575 self.cases.saturating_mul(4)
576 } else {
577 self.max_shrink_iters
578 }
579 }
580
581 // Used by macros to force the config to be owned without depending on
582 // certain traits being `use`d.
583 #[allow(missing_docs)]
584 #[doc(hidden)]
585 pub fn __sugar_to_owned(&self) -> Self {
586 self.clone()
587 }
588}
589
590#[cfg(feature = "std")]
591impl Default for Config {
592 fn default() -> Self {
593 DEFAULT_CONFIG.clone()
594 }
595}
596
597#[cfg(not(feature = "std"))]
598impl Default for Config {
599 fn default() -> Self {
600 default_default_config()
601 }
602}