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}