From 9835e2ae736235810b4ea1c162ca5e65c547e770 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 18 May 2024 04:49:50 +0200 Subject: Merging upstream version 1.71.1+dfsg1. Signed-off-by: Daniel Baumann --- vendor/proptest/src/test_runner/config.rs | 516 +++++++ vendor/proptest/src/test_runner/errors.rs | 135 ++ .../src/test_runner/failure_persistence/file.rs | 536 +++++++ .../src/test_runner/failure_persistence/map.rs | 99 ++ .../src/test_runner/failure_persistence/mod.rs | 156 ++ .../src/test_runner/failure_persistence/noop.rs | 76 + vendor/proptest/src/test_runner/mod.rs | 31 + vendor/proptest/src/test_runner/reason.rs | 54 + vendor/proptest/src/test_runner/replay.rs | 189 +++ vendor/proptest/src/test_runner/result_cache.rs | 120 ++ vendor/proptest/src/test_runner/rng.rs | 724 +++++++++ vendor/proptest/src/test_runner/runner.rs | 1538 ++++++++++++++++++++ 12 files changed, 4174 insertions(+) create mode 100644 vendor/proptest/src/test_runner/config.rs create mode 100644 vendor/proptest/src/test_runner/errors.rs create mode 100644 vendor/proptest/src/test_runner/failure_persistence/file.rs create mode 100644 vendor/proptest/src/test_runner/failure_persistence/map.rs create mode 100644 vendor/proptest/src/test_runner/failure_persistence/mod.rs create mode 100644 vendor/proptest/src/test_runner/failure_persistence/noop.rs create mode 100644 vendor/proptest/src/test_runner/mod.rs create mode 100644 vendor/proptest/src/test_runner/reason.rs create mode 100644 vendor/proptest/src/test_runner/replay.rs create mode 100644 vendor/proptest/src/test_runner/result_cache.rs create mode 100644 vendor/proptest/src/test_runner/rng.rs create mode 100644 vendor/proptest/src/test_runner/runner.rs (limited to 'vendor/proptest/src/test_runner') diff --git a/vendor/proptest/src/test_runner/config.rs b/vendor/proptest/src/test_runner/config.rs new file mode 100644 index 000000000..a3d30846a --- /dev/null +++ b/vendor/proptest/src/test_runner/config.rs @@ -0,0 +1,516 @@ +//- +// Copyright 2017, 2018, 2019 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::Box; +use core::u32; + +#[cfg(feature = "std")] +use std::env; +#[cfg(feature = "std")] +use std::ffi::OsString; +#[cfg(feature = "std")] +use std::fmt; +#[cfg(feature = "std")] +use std::str::FromStr; + +use crate::test_runner::result_cache::{noop_result_cache, ResultCache}; +use crate::test_runner::rng::RngAlgorithm; +use crate::test_runner::FailurePersistence; +#[cfg(feature = "std")] +use crate::test_runner::FileFailurePersistence; + +#[cfg(feature = "std")] +const CASES: &str = "PROPTEST_CASES"; +#[cfg(feature = "std")] +const MAX_LOCAL_REJECTS: &str = "PROPTEST_MAX_LOCAL_REJECTS"; +#[cfg(feature = "std")] +const MAX_GLOBAL_REJECTS: &str = "PROPTEST_MAX_GLOBAL_REJECTS"; +#[cfg(feature = "std")] +const MAX_FLAT_MAP_REGENS: &str = "PROPTEST_MAX_FLAT_MAP_REGENS"; +#[cfg(feature = "std")] +const MAX_SHRINK_TIME: &str = "PROPTEST_MAX_SHRINK_TIME"; +#[cfg(feature = "std")] +const MAX_SHRINK_ITERS: &str = "PROPTEST_MAX_SHRINK_ITERS"; +#[cfg(feature = "fork")] +const FORK: &str = "PROPTEST_FORK"; +#[cfg(feature = "timeout")] +const TIMEOUT: &str = "PROPTEST_TIMEOUT"; +#[cfg(feature = "std")] +const VERBOSE: &str = "PROPTEST_VERBOSE"; +#[cfg(feature = "std")] +const RNG_ALGORITHM: &str = "PROPTEST_RNG_ALGORITHM"; +#[cfg(feature = "std")] +const DISABLE_FAILURE_PERSISTENCE: &str = + "PROPTEST_DISABLE_FAILURE_PERSISTENCE"; + +#[cfg(feature = "std")] +fn contextualize_config(mut result: Config) -> Config { + fn parse_or_warn( + src: &OsString, + dst: &mut T, + typ: &str, + var: &str, + ) { + if let Some(src) = src.to_str() { + if let Ok(value) = src.parse() { + *dst = value; + } else { + eprintln!( + "proptest: The env-var {}={} can't be parsed as {}, \ + using default of {}.", + var, src, typ, *dst + ); + } + } else { + eprintln!( + "proptest: The env-var {} is not valid, using \ + default of {}.", + var, *dst + ); + } + } + + result.failure_persistence = + Some(Box::new(FileFailurePersistence::default())); + for (var, value) in + env::vars_os().filter_map(|(k, v)| k.into_string().ok().map(|k| (k, v))) + { + match var.as_str() { + CASES => parse_or_warn(&value, &mut result.cases, "u32", CASES), + MAX_LOCAL_REJECTS => parse_or_warn( + &value, + &mut result.max_local_rejects, + "u32", + MAX_LOCAL_REJECTS, + ), + MAX_GLOBAL_REJECTS => parse_or_warn( + &value, + &mut result.max_global_rejects, + "u32", + MAX_GLOBAL_REJECTS, + ), + MAX_FLAT_MAP_REGENS => parse_or_warn( + &value, + &mut result.max_flat_map_regens, + "u32", + MAX_FLAT_MAP_REGENS, + ), + #[cfg(feature = "fork")] + FORK => parse_or_warn(&value, &mut result.fork, "bool", FORK), + #[cfg(feature = "timeout")] + TIMEOUT => { + parse_or_warn(&value, &mut result.timeout, "timeout", TIMEOUT) + } + MAX_SHRINK_TIME => parse_or_warn( + &value, + &mut result.max_shrink_time, + "u32", + MAX_SHRINK_TIME, + ), + MAX_SHRINK_ITERS => parse_or_warn( + &value, + &mut result.max_shrink_iters, + "u32", + MAX_SHRINK_ITERS, + ), + VERBOSE => { + parse_or_warn(&value, &mut result.verbose, "u32", VERBOSE) + } + RNG_ALGORITHM => parse_or_warn( + &value, + &mut result.rng_algorithm, + "RngAlgorithm", + RNG_ALGORITHM, + ), + DISABLE_FAILURE_PERSISTENCE => result.failure_persistence = None, + + _ => { + if var.starts_with("PROPTEST_") { + eprintln!("proptest: Ignoring unknown env-var {}.", var); + } + } + } + } + + result +} + +#[cfg(not(feature = "std"))] +fn contextualize_config(result: Config) -> Config { + result +} + +fn default_default_config() -> Config { + Config { + cases: 256, + max_local_rejects: 65_536, + max_global_rejects: 1024, + max_flat_map_regens: 1_000_000, + failure_persistence: None, + source_file: None, + test_name: None, + #[cfg(feature = "fork")] + fork: false, + #[cfg(feature = "timeout")] + timeout: 0, + #[cfg(feature = "std")] + max_shrink_time: 0, + max_shrink_iters: u32::MAX, + result_cache: noop_result_cache, + #[cfg(feature = "std")] + verbose: 0, + rng_algorithm: RngAlgorithm::default(), + _non_exhaustive: (), + } +} + +// The default config, computed by combining environment variables and +// defaults. +#[cfg(feature = "std")] +lazy_static! { + static ref DEFAULT_CONFIG: Config = + contextualize_config(default_default_config()); +} + +/// Configuration for how a proptest test should be run. +#[derive(Clone, Debug, PartialEq)] +pub struct Config { + /// The number of successful test cases that must execute for the test as a + /// whole to pass. + /// + /// This does not include implicitly-replayed persisted failing cases. + /// + /// The default is 256, which can be overridden by setting the + /// `PROPTEST_CASES` environment variable. (The variable is only considered + /// when the `std` feature is enabled, which it is by default.) + pub cases: u32, + + /// The maximum number of individual inputs that may be rejected before the + /// test as a whole aborts. + /// + /// The default is 65536, which can be overridden by setting the + /// `PROPTEST_MAX_LOCAL_REJECTS` environment variable. (The variable is only + /// considered when the `std` feature is enabled, which it is by default.) + pub max_local_rejects: u32, + + /// The maximum number of combined inputs that may be rejected before the + /// test as a whole aborts. + /// + /// The default is 1024, which can be overridden by setting the + /// `PROPTEST_MAX_GLOBAL_REJECTS` environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + pub max_global_rejects: u32, + + /// The maximum number of times all `Flatten` combinators will attempt to + /// regenerate values. This puts a limit on the worst-case exponential + /// explosion that can happen with nested `Flatten`s. + /// + /// The default is 1_000_000, which can be overridden by setting the + /// `PROPTEST_MAX_FLAT_MAP_REGENS` environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + pub max_flat_map_regens: u32, + + /// Indicates whether and how to persist failed test results. + /// + /// When compiling with "std" feature (i.e. the standard library is available), the default + /// is `Some(Box::new(FileFailurePersistence::SourceParallel("proptest-regressions")))`. + /// + /// Without the standard library, the default is `None`, and no persistence occurs. + /// + /// See the docs of [`FileFailurePersistence`](enum.FileFailurePersistence.html) + /// and [`MapFailurePersistence`](struct.MapFailurePersistence.html) for more information. + /// + /// You can disable failure persistence with the `PROPTEST_DISABLE_FAILURE_PERSISTENCE` + /// environment variable but its not currently possible to set the persistence file + /// with an environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + pub failure_persistence: Option>, + + /// File location of the current test, relevant for persistence + /// and debugging. + /// + /// Note the use of `&str` rather than `Path` to be compatible with + /// `#![no_std]` use cases where `Path` is unavailable. + /// + /// See the docs of [`FileFailurePersistence`](enum.FileFailurePersistence.html) + /// for more information on how it may be used for persistence. + pub source_file: Option<&'static str>, + + /// The fully-qualified name of the test being run, as would be passed to + /// the test executable to run just that test. + /// + /// This must be set if `fork` is `true`. Otherwise, it is unused. It is + /// automatically set by `proptest!`. + /// + /// This must include the crate name at the beginning, as produced by + /// `module_path!()`. + pub test_name: Option<&'static str>, + + /// If true, tests are run in a subprocess. + /// + /// Forking allows proptest to work with tests which may fail by aborting + /// the process, causing a segmentation fault, etc, but can be a lot slower + /// in certain environments or when running a very large number of tests. + /// + /// For forking to work correctly, both the `Strategy` and the content of + /// the test case itself must be deterministic. + /// + /// This requires the "fork" feature, enabled by default. + /// + /// The default is `false`, which can be overridden by setting the + /// `PROPTEST_FORK` environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + #[cfg(feature = "fork")] + pub fork: bool, + + /// If non-zero, tests are run in a subprocess and each generated case + /// fails if it takes longer than this number of milliseconds. + /// + /// This implicitly enables forking, even if the `fork` field is `false`. + /// + /// The type here is plain `u32` (rather than + /// `Option`) for the sake of ergonomics. + /// + /// This requires the "timeout" feature, enabled by default. + /// + /// Setting a timeout to less than the time it takes the process to start + /// up and initialise the first test case will cause the whole test to be + /// aborted. + /// + /// The default is `0` (i.e., no timeout), which can be overridden by + /// setting the `PROPTEST_TIMEOUT` environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + #[cfg(feature = "timeout")] + pub timeout: u32, + + /// If non-zero, give up the shrinking process after this many milliseconds + /// have elapsed since the start of the shrinking process. + /// + /// This will not cause currently running test cases to be interrupted. + /// + /// This configuration is only available when the `std` feature is enabled + /// (which it is by default). + /// + /// The default is `0` (i.e., no limit), which can be overridden by setting + /// the `PROPTEST_MAX_SHRINK_TIME` environment variable. (The variable is + /// only considered when the `std` feature is enabled, which it is by + /// default.) + #[cfg(feature = "std")] + pub max_shrink_time: u32, + + /// Give up on shrinking if more than this number of iterations of the test + /// code are run. + /// + /// Setting this to `std::u32::MAX` causes the actual limit to be four + /// times the number of test cases. + /// + /// Setting this value to `0` disables shrinking altogether. + /// + /// Note that the type of this field will change in a future version of + /// proptest to better accommodate its special values. + /// + /// The default is `std::u32::MAX`, which can be overridden by setting the + /// `PROPTEST_MAX_SHRINK_ITERS` environment variable. (The variable is only + /// considered when the `std` feature is enabled, which it is by default.) + pub max_shrink_iters: u32, + + /// A function to create new result caches. + /// + /// The default is to do no caching. The easiest way to enable caching is + /// to set this field to `basic_result_cache` (though that is currently + /// only available with the `std` feature). + /// + /// This is useful for strategies which have a tendency to produce + /// duplicate values, or for tests where shrinking can take a very long + /// time due to exploring the same output multiple times. + /// + /// When caching is enabled, generated values themselves are not stored, so + /// this does not pose a risk of memory exhaustion for large test inputs + /// unless using extraordinarily large test case counts. + /// + /// Caching incurs its own overhead, and may very well make your test run + /// more slowly. + pub result_cache: fn() -> Box, + + /// Set to non-zero values to cause proptest to emit human-targeted + /// messages to stderr as it runs. + /// + /// Greater values cause greater amounts of logs to be emitted. The exact + /// meaning of certain levels other than 0 is subject to change. + /// + /// - 0: No extra output. + /// - 1: Log test failure messages. + /// - 2: Trace low-level details. + /// + /// This is only available with the `std` feature (enabled by default) + /// since on nostd proptest has no way to produce output. + /// + /// The default is `0`, which can be overridden by setting the + /// `PROPTEST_VERBOSE` environment variable. (The variable is only considered + /// when the `std` feature is enabled, which it is by default.) + #[cfg(feature = "std")] + pub verbose: u32, + + /// The RNG algorithm to use when not using a user-provided RNG. + /// + /// The default is `RngAlgorithm::default()`, which can be overridden by + /// setting the `PROPTEST_RNG_ALGORITHM` environment variable to one of the following: + /// + /// - `xs` — `RngAlgorithm::XorShift` + /// - `cc` — `RngAlgorithm::ChaCha` + /// + /// (The variable is only considered when the `std` feature is enabled, + /// which it is by default.) + pub rng_algorithm: RngAlgorithm, + + // Needs to be public so FRU syntax can be used. + #[doc(hidden)] + pub _non_exhaustive: (), +} + +impl Config { + /// Constructs a `Config` only differing from the `default()` in the + /// number of test cases required to pass the test successfully. + /// + /// This is simply a more concise alternative to using field-record update + /// syntax: + /// + /// ``` + /// # use proptest::test_runner::Config; + /// assert_eq!( + /// Config::with_cases(42), + /// Config { cases: 42, .. Config::default() } + /// ); + /// ``` + pub fn with_cases(cases: u32) -> Self { + Self { + cases, + ..Config::default() + } + } + + /// Constructs a `Config` only differing from the `default()` in the + /// source_file of the present test. + /// + /// This is simply a more concise alternative to using field-record update + /// syntax: + /// + /// ``` + /// # use proptest::test_runner::Config; + /// assert_eq!( + /// Config::with_source_file("computer/question"), + /// Config { source_file: Some("computer/question"), .. Config::default() } + /// ); + /// ``` + pub fn with_source_file(source_file: &'static str) -> Self { + Self { + source_file: Some(source_file), + ..Config::default() + } + } + + /// Constructs a `Config` only differing from the provided Config instance, `self`, + /// in the source_file of the present test. + /// + /// This is simply a more concise alternative to using field-record update + /// syntax: + /// + /// ``` + /// # use proptest::test_runner::Config; + /// let a = Config::with_source_file("computer/question"); + /// let b = a.clone_with_source_file("answer/42"); + /// assert_eq!( + /// a, + /// Config { source_file: Some("computer/question"), .. Config::default() } + /// ); + /// assert_eq!( + /// b, + /// Config { source_file: Some("answer/42"), .. Config::default() } + /// ); + /// ``` + pub fn clone_with_source_file(&self, source_file: &'static str) -> Self { + let mut result = self.clone(); + result.source_file = Some(source_file); + result + } + + /// Return whether this configuration implies forking. + /// + /// This method exists even if the "fork" feature is disabled, in which + /// case it simply returns false. + pub fn fork(&self) -> bool { + self._fork() || self.timeout() > 0 + } + + #[cfg(feature = "fork")] + fn _fork(&self) -> bool { + self.fork + } + + #[cfg(not(feature = "fork"))] + fn _fork(&self) -> bool { + false + } + + /// Returns the configured timeout. + /// + /// This method exists even if the "timeout" feature is disabled, in which + /// case it simply returns 0. + #[cfg(feature = "timeout")] + pub fn timeout(&self) -> u32 { + self.timeout + } + + /// Returns the configured timeout. + /// + /// This method exists even if the "timeout" feature is disabled, in which + /// case it simply returns 0. + #[cfg(not(feature = "timeout"))] + pub fn timeout(&self) -> u32 { + 0 + } + + /// Returns the configured limit on shrinking iterations. + /// + /// This takes into account the special "automatic" behaviour. + pub fn max_shrink_iters(&self) -> u32 { + if u32::MAX == self.max_shrink_iters { + self.cases.saturating_mul(4) + } else { + self.max_shrink_iters + } + } + + // Used by macros to force the config to be owned without depending on + // certain traits being `use`d. + #[allow(missing_docs)] + #[doc(hidden)] + pub fn __sugar_to_owned(&self) -> Self { + self.clone() + } +} + +#[cfg(feature = "std")] +impl Default for Config { + fn default() -> Self { + DEFAULT_CONFIG.clone() + } +} + +#[cfg(not(feature = "std"))] +impl Default for Config { + fn default() -> Self { + default_default_config() + } +} diff --git a/vendor/proptest/src/test_runner/errors.rs b/vendor/proptest/src/test_runner/errors.rs new file mode 100644 index 000000000..dfa4a4852 --- /dev/null +++ b/vendor/proptest/src/test_runner/errors.rs @@ -0,0 +1,135 @@ +//- +// Copyright 2017, 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::fmt; + +#[cfg(feature = "std")] +use std::string::ToString; + +use crate::test_runner::Reason; + +/// Errors which can be returned from test cases to indicate non-successful +/// completion. +/// +/// Note that in spite of the name, `TestCaseError` is currently *not* an +/// instance of `Error`, since otherwise `impl From` could not be +/// provided. +/// +/// Any `Error` can be converted to a `TestCaseError`, which places +/// `Error::display()` into the `Fail` case. +#[derive(Debug, Clone)] +pub enum TestCaseError { + /// The input was not valid for the test case. This does not count as a + /// test failure (nor a success); rather, it simply signals to generate + /// a new input and try again. + Reject(Reason), + /// The code under test failed the test. + Fail(Reason), +} + +/// Indicates the type of test that ran successfully. +/// +/// This is used for managing whether or not a success is counted against +/// configured `PROPTEST_CASES`; only `NewCases` shall be counted. +/// +/// TODO-v2: Ideally `TestCaseResult = Result` +/// however this breaks source compability in version 1.x.x because +/// `TestCaseResult` is public. +#[derive(Debug, Clone)] +pub(crate) enum TestCaseOk { + NewCaseSuccess, + PersistedCaseSuccess, + ReplayFromForkSuccess, + CacheHitSuccess, + Reject, +} + +/// Convenience for the type returned by test cases. +pub type TestCaseResult = Result<(), TestCaseError>; + +/// Intended to replace `TestCaseResult` in v2. +/// +/// TODO-v2: Ideally `TestCaseResult = Result` +/// however this breaks source compability in version 1.x.x because +/// `TestCaseResult` is public. +pub(crate) type TestCaseResultV2 = Result; + +impl TestCaseError { + /// Rejects the generated test input as invalid for this test case. This + /// does not count as a test failure (nor a success); rather, it simply + /// signals to generate a new input and try again. + /// + /// The string gives the location and context of the rejection, and + /// should be suitable for formatting like `Foo did X at {whence}`. + pub fn reject(reason: impl Into) -> Self { + TestCaseError::Reject(reason.into()) + } + + /// The code under test failed the test. + /// + /// The string should indicate the location of the failure, but may + /// generally be any string. + pub fn fail(reason: impl Into) -> Self { + TestCaseError::Fail(reason.into()) + } +} + +impl fmt::Display for TestCaseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TestCaseError::Reject(ref whence) => { + write!(f, "Input rejected at {}", whence) + } + TestCaseError::Fail(ref why) => write!(f, "Case failed: {}", why), + } + } +} + +#[cfg(feature = "std")] +impl From for TestCaseError { + fn from(cause: E) -> Self { + TestCaseError::fail(cause.to_string()) + } +} + +/// A failure state from running test cases for a single test. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TestError { + /// The test was aborted for the given reason, for example, due to too many + /// inputs having been rejected. + Abort(Reason), + /// A failing test case was found. The string indicates where and/or why + /// the test failed. The `T` is the minimal input found to reproduce the + /// failure. + Fail(Reason, T), +} + +impl fmt::Display for TestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TestError::Abort(ref why) => write!(f, "Test aborted: {}", why), + TestError::Fail(ref why, ref what) => write!( + f, + "Test failed: {}; minimal failing input: {:?}", + why, what + ), + } + } +} + +#[cfg(feature = "std")] +#[allow(deprecated)] // description() +impl ::std::error::Error for TestError { + fn description(&self) -> &str { + match *self { + TestError::Abort(..) => "Abort", + TestError::Fail(..) => "Fail", + } + } +} diff --git a/vendor/proptest/src/test_runner/failure_persistence/file.rs b/vendor/proptest/src/test_runner/failure_persistence/file.rs new file mode 100644 index 000000000..61d7dcf6a --- /dev/null +++ b/vendor/proptest/src/test_runner/failure_persistence/file.rs @@ -0,0 +1,536 @@ +//- +// Copyright 2017, 2018, 2019 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::any::Any; +use core::fmt::Debug; +use std::borrow::{Cow, ToOwned}; +use std::boxed::Box; +use std::env; +use std::fs; +use std::io::{self, BufRead, Write}; +use std::path::{Path, PathBuf}; +use std::string::{String, ToString}; +use std::sync::RwLock; +use std::vec::Vec; + +use self::FileFailurePersistence::*; +use crate::test_runner::failure_persistence::{ + FailurePersistence, PersistedSeed, +}; + +/// Describes how failing test cases are persisted. +/// +/// Note that file names in this enum are `&str` rather than `&Path` since +/// constant functions are not yet in Rust stable as of 2017-12-16. +/// +/// In all cases, if a derived path references a directory which does not yet +/// exist, proptest will attempt to create all necessary parent directories. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FileFailurePersistence { + /// Completely disables persistence of failing test cases. + /// + /// This is semantically equivalent to `Direct("/dev/null")` on Unix and + /// `Direct("NUL")` on Windows (though it is internally handled by simply + /// not doing any I/O). + Off, + /// The path given to `TestRunner::set_source_file()` is parsed. The path + /// is traversed up the directory tree until a directory containing a file + /// named `lib.rs` or `main.rs` is found. A sibling to that directory with + /// the name given by the string in this configuration is created, and a + /// file with the same name and path relative to the source directory, but + /// with the extension changed to `.txt`, is used. + /// + /// For example, given a source path of + /// `/home/jsmith/code/project/src/foo/bar.rs` and a configuration of + /// `SourceParallel("proptest-regressions")` (the default), assuming the + /// `src` directory has a `lib.rs` or `main.rs`, the resulting file would + /// be `/home/jsmith/code/project/proptest-regressions/foo/bar.txt`. + /// + /// If no `lib.rs` or `main.rs` can be found, a warning is printed and this + /// behaves like `WithSource`. + /// + /// If no source file has been configured, a warning is printed and this + /// behaves like `Off`. + SourceParallel(&'static str), + /// The path given to `TestRunner::set_source_file()` is parsed. The + /// extension of the path is changed to the string given in this + /// configuration, and that filename is used. + /// + /// For example, given a source path of + /// `/home/jsmith/code/project/src/foo/bar.rs` and a configuration of + /// `WithSource("regressions")`, the resulting path would be + /// `/home/jsmith/code/project/src/foo/bar.regressions`. + WithSource(&'static str), + /// The string given in this option is directly used as a file path without + /// any further processing. + Direct(&'static str), + #[doc(hidden)] + #[allow(missing_docs)] + _NonExhaustive, +} + +impl Default for FileFailurePersistence { + fn default() -> Self { + SourceParallel("proptest-regressions") + } +} + +impl FailurePersistence for FileFailurePersistence { + fn load_persisted_failures2( + &self, + source_file: Option<&'static str>, + ) -> Vec { + let p = self.resolve( + source_file + .and_then(|s| absolutize_source_file(Path::new(s))) + .as_ref() + .map(|cow| &**cow), + ); + + let path: Option<&PathBuf> = p.as_ref(); + let result: io::Result> = path.map_or_else( + || Ok(vec![]), + |path| { + // .ok() instead of .unwrap() so we don't propagate panics here + let _lock = PERSISTENCE_LOCK.read().ok(); + io::BufReader::new(fs::File::open(path)?) + .lines() + .enumerate() + .filter_map(|(lineno, line)| match line { + Err(err) => Some(Err(err)), + Ok(line) => parse_seed_line(line, path, lineno).map(Ok), + }) + .collect() + }, + ); + + unwrap_or!(result, err => { + if io::ErrorKind::NotFound != err.kind() { + eprintln!( + "proptest: failed to open {}: {}", + &path.map(|x| &**x) + .unwrap_or_else(|| Path::new("??")) + .display(), + err + ); + } + vec![] + }) + } + + fn save_persisted_failure2( + &mut self, + source_file: Option<&'static str>, + seed: PersistedSeed, + shrunken_value: &dyn Debug, + ) { + let path = self.resolve(source_file.map(Path::new)); + if let Some(path) = path { + // .ok() instead of .unwrap() so we don't propagate panics here + let _lock = PERSISTENCE_LOCK.write().ok(); + let is_new = !path.is_file(); + + let mut to_write = Vec::::new(); + if is_new { + write_header(&mut to_write) + .expect("proptest: couldn't write header."); + } + + write_seed_line(&mut to_write, &seed, shrunken_value) + .expect("proptest: couldn't write seed line."); + + if let Err(e) = write_seed_data_to_file(&path, &to_write) { + eprintln!( + "proptest: failed to append to {}: {}", + path.display(), + e + ); + } else if is_new { + eprintln!( + "proptest: Saving this and future failures in {}\n\ + proptest: If this test was run on a CI system, you may \ + wish to add the following line to your copy of the file.{}\n\ + {}", + path.display(), + if is_new { " (You may need to create it.)" } else { "" }, + seed); + } + } + } + + fn box_clone(&self) -> Box { + Box::new(*self) + } + + fn eq(&self, other: &dyn FailurePersistence) -> bool { + other + .as_any() + .downcast_ref::() + .map_or(false, |x| x == self) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Ensure that the source file to use for resolving the location of the persisted +/// failing cases file is absolute. +/// +/// The source location can only be used if it is absolute. If `source` is +/// not an absolute path, an attempt will be made to determine the absolute +/// path based on the current working directory and its parents. If no +/// absolute path can be determined, a warning will be printed and proptest +/// will continue as if this function had never been called. +/// +/// See [`FileFailurePersistence`](enum.FileFailurePersistence.html) for details on +/// how this value is used once it is made absolute. +/// +/// This is normally called automatically by the `proptest!` macro, which +/// passes `file!()`. +/// +fn absolutize_source_file<'a>(source: &'a Path) -> Option> { + absolutize_source_file_with_cwd(env::current_dir, source) +} + +fn absolutize_source_file_with_cwd<'a>( + getcwd: impl FnOnce() -> io::Result, + source: &'a Path, +) -> Option> { + if source.is_absolute() { + // On Unix, `file!()` is absolute. In these cases, we can use + // that path directly. + Some(Cow::Borrowed(source)) + } else { + // On Windows, `file!()` is relative to the crate root, but the + // test is not generally run with the crate root as the working + // directory, so the path is not directly usable. However, the + // working directory is almost always a subdirectory of the crate + // root, so pop directories off until pushing the source onto the + // directory results in a path that refers to an existing file. + // Once we find such a path, we can use that. + // + // If we can't figure out an absolute path, print a warning and act + // as if no source had been given. + match getcwd() { + Ok(mut cwd) => loop { + let joined = cwd.join(source); + if joined.is_file() { + break Some(Cow::Owned(joined)); + } + + if !cwd.pop() { + eprintln!( + "proptest: Failed to find absolute path of \ + source file '{:?}'. Ensure the test is \ + being run from somewhere within the crate \ + directory hierarchy.", + source + ); + break None; + } + }, + + Err(e) => { + eprintln!( + "proptest: Failed to determine current \ + directory, so the relative source path \ + '{:?}' cannot be resolved: {}", + source, e + ); + None + } + } + } +} + +fn parse_seed_line( + mut line: String, + path: &Path, + lineno: usize, +) -> Option { + // Remove anything after and including '#': + if let Some(comment_start) = line.find('#') { + line.truncate(comment_start); + } + + if line.len() > 0 { + let ret = line.parse::().ok(); + if !ret.is_some() { + eprintln!( + "proptest: {}:{}: unparsable line, ignoring", + path.display(), + lineno + 1 + ); + } + return ret; + } + + None +} + +fn write_seed_line( + buf: &mut Vec, + seed: &PersistedSeed, + shrunken_value: &dyn Debug, +) -> io::Result<()> { + // Write the seed itself + write!(buf, "{}", seed.to_string())?; + + // Write out comment: + let debug_start = buf.len(); + write!(buf, " # shrinks to {:?}", shrunken_value)?; + + // Ensure there are no newlines in the debug output + for byte in &mut buf[debug_start..] { + if b'\n' == *byte || b'\r' == *byte { + *byte = b' '; + } + } + + buf.push(b'\n'); + + Ok(()) +} + +fn write_header(buf: &mut Vec) -> io::Result<()> { + writeln!( + buf, + "\ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases." + ) +} + +fn write_seed_data_to_file(dst: &Path, data: &[u8]) -> io::Result<()> { + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent)?; + } + + let mut options = fs::OpenOptions::new(); + options.append(true).create(true); + let mut out = options.open(dst)?; + out.write_all(data)?; + + Ok(()) +} + +impl FileFailurePersistence { + /// Given the nominal source path, determine the location of the failure + /// persistence file, if any. + pub(super) fn resolve(&self, source: Option<&Path>) -> Option { + let source = source.and_then(absolutize_source_file); + + match *self { + Off => None, + + SourceParallel(sibling) => match source { + Some(source_path) => { + let mut dir = Cow::into_owned(source_path.clone()); + let mut found = false; + while dir.pop() { + if dir.join("lib.rs").is_file() + || dir.join("main.rs").is_file() + { + found = true; + break; + } + } + + if !found { + eprintln!( + "proptest: FileFailurePersistence::SourceParallel set, \ + but failed to find lib.rs or main.rs" + ); + WithSource(sibling).resolve(Some(&*source_path)) + } else { + let suffix = source_path + .strip_prefix(&dir) + .expect("parent of source is not a prefix of it?") + .to_owned(); + let mut result = dir; + // If we've somehow reached the root, or someone gave + // us a relative path that we've exhausted, just accept + // creating a subdirectory instead. + let _ = result.pop(); + result.push(sibling); + result.push(&suffix); + result.set_extension("txt"); + Some(result) + } + } + None => { + eprintln!( + "proptest: FileFailurePersistence::SourceParallel set, \ + but no source file known" + ); + None + } + }, + + WithSource(extension) => match source { + Some(source_path) => { + let mut result = Cow::into_owned(source_path); + result.set_extension(extension); + Some(result) + } + + None => { + eprintln!( + "proptest: FileFailurePersistence::WithSource set, \ + but no source file known" + ); + None + } + }, + + Direct(path) => Some(Path::new(path).to_owned()), + + _NonExhaustive => { + panic!("FailurePersistence set to _NonExhaustive") + } + } + } +} + +lazy_static! { + /// Used to guard access to the persistence file(s) so that a single + /// process will not step on its own toes. + /// + /// We don't have much protecting us should two separate process try to + /// write to the same file at once (depending on how atomic append mode is + /// on the OS), but this should be extremely rare. + static ref PERSISTENCE_LOCK: RwLock<()> = RwLock::new(()); +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestPaths { + crate_root: &'static Path, + src_file: PathBuf, + subdir_file: PathBuf, + misplaced_file: PathBuf, + } + + lazy_static! { + static ref TEST_PATHS: TestPaths = { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let lib_root = crate_root.join("src"); + let src_subdir = lib_root.join("strategy"); + let src_file = lib_root.join("foo.rs"); + let subdir_file = src_subdir.join("foo.rs"); + let misplaced_file = crate_root.join("foo.rs"); + TestPaths { + crate_root, + src_file, + subdir_file, + misplaced_file, + } + }; + } + + #[test] + fn persistence_file_location_resolved_correctly() { + // If off, there is never a file + assert_eq!(None, Off.resolve(None)); + assert_eq!(None, Off.resolve(Some(&TEST_PATHS.subdir_file))); + + // For direct, we don't care about the source file, and instead always + // use whatever is in the config. + assert_eq!( + Some(Path::new("bar.txt").to_owned()), + Direct("bar.txt").resolve(None) + ); + assert_eq!( + Some(Path::new("bar.txt").to_owned()), + Direct("bar.txt").resolve(Some(&TEST_PATHS.subdir_file)) + ); + + // For WithSource, only the extension changes, but we get nothing if no + // source file was configured. + // Accounting for the way absolute paths work on Windows would be more + // complex, so for now don't test that case. + #[cfg(unix)] + fn absolute_path_case() { + assert_eq!( + Some(Path::new("/foo/bar.ext").to_owned()), + WithSource("ext").resolve(Some(Path::new("/foo/bar.rs"))) + ); + } + #[cfg(not(unix))] + fn absolute_path_case() {} + absolute_path_case(); + assert_eq!(None, WithSource("ext").resolve(None)); + + // For SourceParallel, we make a sibling directory tree and change the + // extensions to .txt ... + assert_eq!( + Some(TEST_PATHS.crate_root.join("sib").join("foo.txt")), + SourceParallel("sib").resolve(Some(&TEST_PATHS.src_file)) + ); + assert_eq!( + Some( + TEST_PATHS + .crate_root + .join("sib") + .join("strategy") + .join("foo.txt") + ), + SourceParallel("sib").resolve(Some(&TEST_PATHS.subdir_file)) + ); + // ... but if we can't find lib.rs / main.rs, give up and set the + // extension instead ... + assert_eq!( + Some(TEST_PATHS.crate_root.join("foo.sib")), + SourceParallel("sib").resolve(Some(&TEST_PATHS.misplaced_file)) + ); + // ... and if no source is configured, we do nothing + assert_eq!(None, SourceParallel("ext").resolve(None)); + } + + #[test] + fn relative_source_files_absolutified() { + const TEST_RUNNER_PATH: &[&str] = &["src", "test_runner", "mod.rs"]; + lazy_static! { + static ref TEST_RUNNER_RELATIVE: PathBuf = + TEST_RUNNER_PATH.iter().collect(); + } + const CARGO_DIR: &str = env!("CARGO_MANIFEST_DIR"); + + let expected = ::std::iter::once(CARGO_DIR) + .chain(TEST_RUNNER_PATH.iter().map(|s| *s)) + .collect::(); + + // Running from crate root + assert_eq!( + &*expected, + absolutize_source_file_with_cwd( + || Ok(Path::new(CARGO_DIR).to_owned()), + &TEST_RUNNER_RELATIVE + ) + .unwrap() + ); + + // Running from test subdirectory + assert_eq!( + &*expected, + absolutize_source_file_with_cwd( + || Ok(Path::new(CARGO_DIR).join("target")), + &TEST_RUNNER_RELATIVE + ) + .unwrap() + ); + } +} diff --git a/vendor/proptest/src/test_runner/failure_persistence/map.rs b/vendor/proptest/src/test_runner/failure_persistence/map.rs new file mode 100644 index 000000000..322e554c6 --- /dev/null +++ b/vendor/proptest/src/test_runner/failure_persistence/map.rs @@ -0,0 +1,99 @@ +//- +// Copyright 2017, 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{fmt, BTreeMap, BTreeSet, Box, Vec}; +use core::any::Any; + +use crate::test_runner::failure_persistence::FailurePersistence; +use crate::test_runner::failure_persistence::PersistedSeed; + +/// Failure persistence option that loads and saves seeds in memory +/// on the heap. This may be useful when accumulating test failures +/// across multiple `TestRunner` instances for external reporting +/// or batched persistence. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MapFailurePersistence { + /// Backing map, keyed by source_file. + pub map: BTreeMap<&'static str, BTreeSet>, +} + +impl FailurePersistence for MapFailurePersistence { + fn load_persisted_failures2( + &self, + source_file: Option<&'static str>, + ) -> Vec { + source_file + .and_then(|source| self.map.get(source)) + .map(|seeds| seeds.iter().cloned().collect::>()) + .unwrap_or_default() + } + + fn save_persisted_failure2( + &mut self, + source_file: Option<&'static str>, + seed: PersistedSeed, + _shrunken_value: &dyn fmt::Debug, + ) { + let s = match source_file { + Some(sf) => sf, + None => return, + }; + let set = self.map.entry(s).or_insert_with(BTreeSet::new); + set.insert(seed); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn eq(&self, other: &dyn FailurePersistence) -> bool { + other + .as_any() + .downcast_ref::() + .map_or(false, |x| x == self) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_runner::failure_persistence::tests::*; + + #[test] + fn initial_map_is_empty() { + assert!(MapFailurePersistence::default() + .load_persisted_failures2(HI_PATH) + .is_empty()) + } + + #[test] + fn seeds_recoverable() { + let mut p = MapFailurePersistence::default(); + p.save_persisted_failure2(HI_PATH, INC_SEED, &""); + let restored = p.load_persisted_failures2(HI_PATH); + assert_eq!(1, restored.len()); + assert_eq!(INC_SEED, *restored.first().unwrap()); + + assert!(p.load_persisted_failures2(None).is_empty()); + assert!(p.load_persisted_failures2(UNREL_PATH).is_empty()); + } + + #[test] + fn seeds_deduplicated() { + let mut p = MapFailurePersistence::default(); + p.save_persisted_failure2(HI_PATH, INC_SEED, &""); + p.save_persisted_failure2(HI_PATH, INC_SEED, &""); + let restored = p.load_persisted_failures2(HI_PATH); + assert_eq!(1, restored.len()); + } +} diff --git a/vendor/proptest/src/test_runner/failure_persistence/mod.rs b/vendor/proptest/src/test_runner/failure_persistence/mod.rs new file mode 100644 index 000000000..6d21c4002 --- /dev/null +++ b/vendor/proptest/src/test_runner/failure_persistence/mod.rs @@ -0,0 +1,156 @@ +//- +// Copyright 2017, 2018, 2019 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{fmt, Box, Vec}; +use core::any::Any; +use core::fmt::Display; +use core::result::Result; +use core::str::FromStr; + +#[cfg(feature = "std")] +mod file; +mod map; +mod noop; + +#[cfg(feature = "std")] +pub use self::file::*; +pub use self::map::*; +pub use self::noop::*; + +use crate::test_runner::Seed; + +/// Opaque struct representing a seed which can be persisted. +/// +/// The `Display` and `FromStr` implementations go to and from the format +/// Proptest uses for its persistence file. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct PersistedSeed(pub(crate) Seed); + +impl Display for PersistedSeed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_persistence()) + } +} + +impl FromStr for PersistedSeed { + type Err = (); + + fn from_str(s: &str) -> Result { + Seed::from_persistence(s).map(PersistedSeed).ok_or(()) + } +} + +/// Provides external persistence for historical test failures by storing seeds. +/// +/// **Note**: Implementing `load_persisted_failures` and +/// `save_persisted_failures` is **deprecated** and these methods will be +/// removed in proptest 0.10.0. Instead, implement `load_persisted_failures2` +/// and `save_persisted_failures2`. +pub trait FailurePersistence: Send + Sync + fmt::Debug { + /// Supply seeds associated with the given `source_file` that may be used + /// by a `TestRunner`'s random number generator in order to consistently + /// recreate a previously-failing `Strategy`-provided value. + /// + /// The default implementation is **for backwards compatibility**. It + /// delegates to `load_persisted_failures` and converts the results into + /// XorShift seeds. + #[allow(deprecated)] + fn load_persisted_failures2( + &self, + source_file: Option<&'static str>, + ) -> Vec { + self.load_persisted_failures(source_file) + .into_iter() + .map(|seed| PersistedSeed(Seed::XorShift(seed))) + .collect() + } + + /// Use `load_persisted_failures2` instead. + /// + /// This function inadvertently exposes the implementation of seeds prior + /// to Proptest 0.9.1 and only works with XorShift seeds. + #[deprecated] + #[allow(unused_variables)] + fn load_persisted_failures( + &self, + source_file: Option<&'static str>, + ) -> Vec<[u8; 16]> { + panic!("load_persisted_failures2 not implemented"); + } + + /// Store a new failure-generating seed associated with the given `source_file`. + /// + /// The default implementation is **for backwards compatibility**. It + /// delegates to `save_persisted_failure` if `seed` is a XorShift seed. + #[allow(deprecated)] + fn save_persisted_failure2( + &mut self, + source_file: Option<&'static str>, + seed: PersistedSeed, + shrunken_value: &dyn fmt::Debug, + ) { + match seed.0 { + Seed::XorShift(seed) => { + self.save_persisted_failure(source_file, seed, shrunken_value) + } + _ => (), + } + } + + /// Use `save_persisted_failures2` instead. + /// + /// This function inadvertently exposes the implementation of seeds prior + /// to Proptest 0.9.1 and only works with XorShift seeds. + #[deprecated] + #[allow(unused_variables)] + fn save_persisted_failure( + &mut self, + source_file: Option<&'static str>, + seed: [u8; 16], + shrunken_value: &dyn fmt::Debug, + ) { + panic!("save_persisted_failure2 not implemented"); + } + + /// Delegate method for producing a trait object usable with `Clone` + fn box_clone(&self) -> Box; + + /// Equality testing delegate required due to constraints of trait objects. + fn eq(&self, other: &dyn FailurePersistence) -> bool; + + /// Assistant method for trait object comparison. + fn as_any(&self) -> &dyn Any; +} + +impl<'a, 'b> PartialEq + for dyn FailurePersistence + 'a +{ + fn eq(&self, other: &(dyn FailurePersistence + 'b)) -> bool { + FailurePersistence::eq(self, other) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.box_clone() + } +} + +#[cfg(test)] +mod tests { + use super::PersistedSeed; + use crate::test_runner::rng::Seed; + + pub const INC_SEED: PersistedSeed = PersistedSeed(Seed::XorShift([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ])); + + pub const HI_PATH: Option<&str> = Some("hi"); + pub const UNREL_PATH: Option<&str> = Some("unrelated"); +} diff --git a/vendor/proptest/src/test_runner/failure_persistence/noop.rs b/vendor/proptest/src/test_runner/failure_persistence/noop.rs new file mode 100644 index 000000000..59538392a --- /dev/null +++ b/vendor/proptest/src/test_runner/failure_persistence/noop.rs @@ -0,0 +1,76 @@ +//- +// Copyright 2017, 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{fmt, Box, Vec}; +use core::any::Any; + +use crate::test_runner::failure_persistence::{ + FailurePersistence, PersistedSeed, +}; + +/// Failure persistence option that loads and saves nothing at all. +#[derive(Debug, Default, PartialEq)] +struct NoopFailurePersistence; + +impl FailurePersistence for NoopFailurePersistence { + fn load_persisted_failures2( + &self, + _source_file: Option<&'static str>, + ) -> Vec { + Vec::new() + } + + fn save_persisted_failure2( + &mut self, + _source_file: Option<&'static str>, + _seed: PersistedSeed, + _shrunken_value: &dyn fmt::Debug, + ) { + } + + fn box_clone(&self) -> Box { + Box::new(NoopFailurePersistence) + } + + fn eq(&self, other: &dyn FailurePersistence) -> bool { + other + .as_any() + .downcast_ref::() + .map_or(false, |x| x == self) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_runner::failure_persistence::tests::*; + + #[test] + fn default_load_is_empty() { + assert!(NoopFailurePersistence::default() + .load_persisted_failures2(None) + .is_empty()); + assert!(NoopFailurePersistence::default() + .load_persisted_failures2(HI_PATH) + .is_empty()); + } + + #[test] + fn seeds_not_recoverable() { + let mut p = NoopFailurePersistence::default(); + p.save_persisted_failure2(HI_PATH, INC_SEED, &""); + assert!(p.load_persisted_failures2(HI_PATH).is_empty()); + assert!(p.load_persisted_failures2(None).is_empty()); + assert!(p.load_persisted_failures2(UNREL_PATH).is_empty()); + } +} diff --git a/vendor/proptest/src/test_runner/mod.rs b/vendor/proptest/src/test_runner/mod.rs new file mode 100644 index 000000000..d7516ab96 --- /dev/null +++ b/vendor/proptest/src/test_runner/mod.rs @@ -0,0 +1,31 @@ +//- +// Copyright 2017, 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! State and functions for running proptest tests. +//! +//! You do not normally need to access things in this module directly except +//! when implementing new low-level strategies. + +mod config; +mod errors; +mod failure_persistence; +mod reason; +#[cfg(feature = "fork")] +mod replay; +mod result_cache; +mod rng; +mod runner; + +pub use self::config::*; +pub use self::errors::*; +pub use self::failure_persistence::*; +pub use self::reason::*; +pub use self::result_cache::*; +pub use self::rng::*; +pub use self::runner::*; diff --git a/vendor/proptest/src/test_runner/reason.rs b/vendor/proptest/src/test_runner/reason.rs new file mode 100644 index 000000000..38cc7e322 --- /dev/null +++ b/vendor/proptest/src/test_runner/reason.rs @@ -0,0 +1,54 @@ +//- +// Copyright 2017, 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{fmt, Box, Cow, String}; + +/// The reason for why something, such as a generated value, was rejected. +/// +/// Currently this is merely a wrapper around a message, but more properties +/// may be added in the future. +/// +/// This is constructed via `.into()` on a `String`, `&'static str`, or +/// `Box`. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Reason(Cow<'static, str>); + +impl Reason { + /// Return the message for this `Reason`. + /// + /// The message is intended for human consumption, and is not guaranteed to + /// have any format in particular. + pub fn message(&self) -> &str { + &*self.0 + } +} + +impl From<&'static str> for Reason { + fn from(s: &'static str) -> Self { + Reason(s.into()) + } +} + +impl From for Reason { + fn from(s: String) -> Self { + Reason(s.into()) + } +} + +impl From> for Reason { + fn from(s: Box) -> Self { + Reason(String::from(s).into()) + } +} + +impl fmt::Display for Reason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self.message(), f) + } +} diff --git a/vendor/proptest/src/test_runner/replay.rs b/vendor/proptest/src/test_runner/replay.rs new file mode 100644 index 000000000..4365d5538 --- /dev/null +++ b/vendor/proptest/src/test_runner/replay.rs @@ -0,0 +1,189 @@ +//- +// Copyright 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(dead_code)] + +use std::fs; +use std::io::{self, BufRead, Read, Seek, Write}; +use std::path::Path; +use std::string::String; +use std::vec::Vec; + +use crate::test_runner::{Seed, TestCaseError, TestCaseResult}; + +const SENTINEL: &'static str = "proptest-forkfile"; + +/// A "replay" of a `TestRunner` invocation. +/// +/// The replay mechanism is used to support forking. When a child process +/// exits, the parent can read the replay to reproduce the state the child had; +/// similarly, if a child crashes, a new one can be started and given a replay +/// which steps it one complication past the input that caused the crash. +/// +/// The replay system is tightly coupled to the `TestRunner` itself. It does +/// not carry enough information to be used in different builds of the same +/// application, or even two different runs of the test process since changes +/// to the persistence file will perturb the replay. +/// +/// `Replay` has a special string format for being stored in files. It starts +/// with a line just containing the text in `SENTINEL`, then 16 lines +/// containing the values of `seed`, then an unterminated line consisting of +/// `+`, `-`, and `!` characters to indicate test case passes/failures/rejects, +/// `.` to indicate termination of the test run, or ` ` as a dummy "I'm alive" +/// signal. This format makes it easy for the child process to blindly append +/// to the file without having to worry about the possibility of appends being +/// non-atomic. +#[derive(Clone, Debug)] +pub(crate) struct Replay { + /// The seed of the RNG used to start running the test cases. + pub(crate) seed: Seed, + /// A log of whether certain test cases passed or failed. The runner will + /// assume the same results occur without actually running the test cases. + pub(crate) steps: Vec, +} + +impl Replay { + /// If `other` is longer than `self`, add the extra elements to `self`. + pub fn merge(&mut self, other: &Replay) { + if other.steps.len() > self.steps.len() { + let sl = self.steps.len(); + self.steps.extend_from_slice(&other.steps[sl..]); + } + } +} + +/// Result of loading a replay file. +#[derive(Clone, Debug)] +pub(crate) enum ReplayFileStatus { + /// The file is valid and represents a currently-in-progress test. + InProgress(Replay), + /// The file is valid, but indicates that all testing has completed. + Terminated(Replay), + /// The file is not parsable. + Corrupt, +} + +/// Open the file in the usual read+append+create mode. +pub(crate) fn open_file(path: impl AsRef) -> io::Result { + fs::OpenOptions::new() + .read(true) + .append(true) + .create(true) + .truncate(false) + .open(path) +} + +fn step_to_char(step: &TestCaseResult) -> char { + match *step { + Ok(_) => '+', + Err(TestCaseError::Reject(_)) => '!', + Err(TestCaseError::Fail(_)) => '-', + } +} + +/// Append the given step to the given output. +pub(crate) fn append( + mut file: impl Write, + step: &TestCaseResult, +) -> io::Result<()> { + write!(file, "{}", step_to_char(step)) +} + +/// Append a no-op step to the given output. +pub(crate) fn ping(mut file: impl Write) -> io::Result<()> { + write!(file, " ") +} + +/// Append a termination mark to the given output. +pub(crate) fn terminate(mut file: impl Write) -> io::Result<()> { + write!(file, ".") +} + +impl Replay { + /// Write the full state of this `Replay` to the given output. + pub fn init_file(&self, mut file: impl Write) -> io::Result<()> { + writeln!(file, "{}", SENTINEL)?; + writeln!(file, "{}", self.seed.to_persistence())?; + + let mut step_data = Vec::::new(); + for step in &self.steps { + step_data.push(step_to_char(step) as u8); + } + + file.write_all(&step_data)?; + + Ok(()) + } + + /// Mark the replay as complete in the file. + pub fn complete(mut file: impl Write) -> io::Result<()> { + write!(file, ".") + } + + /// Parse a `Replay` out of the given file. + /// + /// The reader is implicitly seeked to the beginning before reading. + pub fn parse_from( + mut file: impl Read + Seek, + ) -> io::Result { + file.seek(io::SeekFrom::Start(0))?; + + let mut reader = io::BufReader::new(&mut file); + let mut line = String::new(); + + // Ensure it starts with the sentinel. We do this since we rely on a + // named temporary file which could be in a location where another + // actor could replace it with, eg, a symlink to a location they don't + // control but we do. By rejecting a read from a file missing the + // sentinel, and not doing any writes if we can't read the file, we + // won't risk overwriting another file since the prospective attacker + // would need to be able to change the file to start with the sentinel + // themselves. + // + // There are still some possible symlink attacks that can work by + // tricking us into reading, but those are non-destructive things like + // interfering with a FIFO or Unix socket. + reader.read_line(&mut line)?; + if SENTINEL != line.trim() { + return Ok(ReplayFileStatus::Corrupt); + } + + line.clear(); + reader.read_line(&mut line)?; + let seed = match Seed::from_persistence(&line) { + Some(seed) => seed, + None => return Ok(ReplayFileStatus::Corrupt), + }; + + line.clear(); + reader.read_line(&mut line)?; + + let mut steps = Vec::new(); + for ch in line.chars() { + match ch { + '+' => steps.push(Ok(())), + '-' => steps + .push(Err(TestCaseError::fail("failed in other process"))), + '!' => steps.push(Err(TestCaseError::reject( + "rejected in other process", + ))), + '.' => { + return Ok(ReplayFileStatus::Terminated(Replay { + seed, + steps, + })) + } + ' ' => (), + _ => return Ok(ReplayFileStatus::Corrupt), + } + } + + Ok(ReplayFileStatus::InProgress(Replay { seed, steps })) + } +} diff --git a/vendor/proptest/src/test_runner/result_cache.rs b/vendor/proptest/src/test_runner/result_cache.rs new file mode 100644 index 000000000..c5bc68c3a --- /dev/null +++ b/vendor/proptest/src/test_runner/result_cache.rs @@ -0,0 +1,120 @@ +//- +// Copyright 2018 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::fmt; +use crate::std_facade::Box; +#[cfg(feature = "std")] +use std::collections::HashMap; + +use crate::test_runner::errors::TestCaseResult; + +/// A key used for the result cache. +/// +/// The capabilities of this structure are currently quite limited; all one can +/// do with safe code is get the `&dyn Debug` of the test input value. This may +/// improve in the future, particularly at such a time that specialisation +/// becomes stable. +#[derive(Debug)] +pub struct ResultCacheKey<'a> { + value: &'a dyn fmt::Debug, +} + +impl<'a> ResultCacheKey<'a> { + pub(crate) fn new(value: &'a dyn fmt::Debug) -> Self { + Self { value } + } + + /// Return the test input value as an `&dyn Debug`. + pub fn value_debug(&self) -> &dyn fmt::Debug { + self.value + } +} + +/// An object which can cache the outcomes of tests. +pub trait ResultCache { + /// Convert the given cache key into a `u64` representing that value. The + /// u64 is used as the key below. + /// + /// This is a separate step so that ownership of the key value can be + /// handed off to user code without needing to be able to clone it. + fn key(&self, key: &ResultCacheKey) -> u64; + /// Save `result` as the outcome associated with the test input in `key`. + /// + /// `result` is passed as a reference so that the decision to clone depends + /// on whether the cache actually plans on storing it. + fn put(&mut self, key: u64, result: &TestCaseResult); + /// If `put()` has been called with a semantically equivalent `key`, return + /// the saved result. Otherwise, return `None`. + fn get(&self, key: u64) -> Option<&TestCaseResult>; +} + +#[cfg(feature = "std")] +#[derive(Debug, Default, Clone)] +struct BasicResultCache { + entries: HashMap, +} + +#[cfg(feature = "std")] +impl ResultCache for BasicResultCache { + fn key(&self, val: &ResultCacheKey) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::Hasher; + use std::io::{self, Write}; + + struct HashWriter(DefaultHasher); + impl io::Write for HashWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let mut hash = HashWriter(DefaultHasher::default()); + write!(hash, "{:?}", val).expect("Debug format returned Err"); + hash.0.finish() + } + + fn put(&mut self, key: u64, result: &TestCaseResult) { + self.entries.insert(key, result.clone()); + } + + fn get(&self, key: u64) -> Option<&TestCaseResult> { + self.entries.get(&key) + } +} + +/// A basic result cache. +/// +/// Values are identified by their `Debug` string representation. +#[cfg(feature = "std")] +pub fn basic_result_cache() -> Box { + Box::new(BasicResultCache::default()) +} + +pub(crate) struct NoOpResultCache; +impl ResultCache for NoOpResultCache { + fn key(&self, _: &ResultCacheKey) -> u64 { + 0 + } + fn put(&mut self, _: u64, _: &TestCaseResult) {} + fn get(&self, _: u64) -> Option<&TestCaseResult> { + None + } +} + +/// A result cache that does nothing. +/// +/// This is the default value of `ProptestConfig.result_cache`. +pub fn noop_result_cache() -> Box { + Box::new(NoOpResultCache) +} diff --git a/vendor/proptest/src/test_runner/rng.rs b/vendor/proptest/src/test_runner/rng.rs new file mode 100644 index 000000000..78c8d7a75 --- /dev/null +++ b/vendor/proptest/src/test_runner/rng.rs @@ -0,0 +1,724 @@ +//- +// Copyright 2017, 2018, 2019, 2020 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{Arc, String, ToOwned, Vec}; +use core::result::Result; +use core::{fmt, str, u8}; + +use byteorder::{ByteOrder, LittleEndian}; +use rand::{self, Rng, RngCore, SeedableRng}; +use rand_chacha::ChaChaRng; +use rand_xorshift::XorShiftRng; + +/// Identifies a particular RNG algorithm supported by proptest. +/// +/// Proptest supports dynamic configuration of algorithms to allow it to +/// continue operating with persisted regression files and to allow the +/// configuration to be expressed in the `Config` struct. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RngAlgorithm { + /// The [XorShift](https://rust-random.github.io/rand/rand_xorshift/struct.XorShiftRng.html) + /// algorithm. This was the default up through and including Proptest 0.9.0. + /// + /// It is faster than ChaCha but produces lower quality randomness and has + /// some pathological cases where it may fail to produce outputs that are + /// random even to casual observation. + /// + /// The seed must be exactly 16 bytes. + XorShift, + /// The [ChaCha](https://rust-random.github.io/rand/rand_chacha/struct.ChaChaRng.html) + /// algorithm. This became the default with Proptest 0.9.1. + /// + /// The seed must be exactly 32 bytes. + ChaCha, + /// This is not an actual RNG algorithm, but instead returns data directly + /// from its "seed". + /// + /// This is useful when Proptest is being driven from some other entropy + /// source, such as a fuzzer. + /// + /// If the seed is depleted, the RNG will return 0s forever. + /// + /// Note that in cases where a new RNG is to be derived from an existing + /// one, *the data is split evenly between them*, regardless of how much + /// entropy is actually needed. This means that combinators like + /// `prop_perturb` and `prop_flat_map` can require extremely large inputs. + PassThrough, + /// This is equivalent to the `ChaCha` RNG, with the addition that it + /// records the bytes used to create a value. + /// + /// This is useful when Proptest is used for fuzzing, and a corpus of + /// initial inputs need to be created. Note that in these cases, you need + /// to use the `TestRunner` API directly yourself instead of using the + /// `proptest!` macro, as otherwise there is no way to obtain the bytes + /// this captures. + Recorder, + #[allow(missing_docs)] + #[doc(hidden)] + _NonExhaustive, +} + +impl Default for RngAlgorithm { + fn default() -> Self { + RngAlgorithm::ChaCha + } +} + +impl RngAlgorithm { + pub(crate) fn persistence_key(self) -> &'static str { + match self { + RngAlgorithm::XorShift => "xs", + RngAlgorithm::ChaCha => "cc", + RngAlgorithm::PassThrough => "pt", + RngAlgorithm::Recorder => "rc", + RngAlgorithm::_NonExhaustive => unreachable!(), + } + } + + pub(crate) fn from_persistence_key(k: &str) -> Option { + match k { + "xs" => Some(RngAlgorithm::XorShift), + "cc" => Some(RngAlgorithm::ChaCha), + "pt" => Some(RngAlgorithm::PassThrough), + "rc" => Some(RngAlgorithm::Recorder), + _ => None, + } + } +} + +// These two are only used for parsing the environment variable +// PROPTEST_RNG_ALGORITHM. +impl str::FromStr for RngAlgorithm { + type Err = (); + fn from_str(s: &str) -> Result { + RngAlgorithm::from_persistence_key(s).ok_or(()) + } +} +impl fmt::Display for RngAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.persistence_key()) + } +} + +/// Proptest's random number generator. +#[derive(Clone, Debug)] +pub struct TestRng { + rng: TestRngImpl, +} + +#[derive(Clone, Debug)] +enum TestRngImpl { + XorShift(XorShiftRng), + ChaCha(ChaChaRng), + PassThrough { + off: usize, + end: usize, + data: Arc<[u8]>, + }, + Recorder { + rng: ChaChaRng, + record: Vec, + }, +} + +impl RngCore for TestRng { + fn next_u32(&mut self) -> u32 { + match &mut self.rng { + &mut TestRngImpl::XorShift(ref mut rng) => rng.next_u32(), + + &mut TestRngImpl::ChaCha(ref mut rng) => rng.next_u32(), + + &mut TestRngImpl::PassThrough { .. } => { + let mut buf = [0; 4]; + self.fill_bytes(&mut buf[..]); + LittleEndian::read_u32(&buf[..]) + } + + &mut TestRngImpl::Recorder { + ref mut rng, + ref mut record, + } => { + let read = rng.next_u32(); + record.extend_from_slice(&read.to_le_bytes()); + read + } + } + } + + fn next_u64(&mut self) -> u64 { + match &mut self.rng { + &mut TestRngImpl::XorShift(ref mut rng) => rng.next_u64(), + + &mut TestRngImpl::ChaCha(ref mut rng) => rng.next_u64(), + + &mut TestRngImpl::PassThrough { .. } => { + let mut buf = [0; 8]; + self.fill_bytes(&mut buf[..]); + LittleEndian::read_u64(&buf[..]) + } + + &mut TestRngImpl::Recorder { + ref mut rng, + ref mut record, + } => { + let read = rng.next_u64(); + record.extend_from_slice(&read.to_le_bytes()); + read + } + } + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + match &mut self.rng { + &mut TestRngImpl::XorShift(ref mut rng) => rng.fill_bytes(dest), + + &mut TestRngImpl::ChaCha(ref mut rng) => rng.fill_bytes(dest), + + &mut TestRngImpl::PassThrough { + ref mut off, + end, + ref data, + } => { + let bytes_to_copy = dest.len().min(end - *off); + dest[..bytes_to_copy] + .copy_from_slice(&data[*off..*off + bytes_to_copy]); + *off += bytes_to_copy; + for i in bytes_to_copy..dest.len() { + dest[i] = 0; + } + } + + &mut TestRngImpl::Recorder { + ref mut rng, + ref mut record, + } => { + let res = rng.fill_bytes(dest); + record.extend_from_slice(&dest); + res + } + } + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + match self.rng { + TestRngImpl::XorShift(ref mut rng) => rng.try_fill_bytes(dest), + + TestRngImpl::ChaCha(ref mut rng) => rng.try_fill_bytes(dest), + + TestRngImpl::PassThrough { .. } => { + self.fill_bytes(dest); + Ok(()) + } + + TestRngImpl::Recorder { + ref mut rng, + ref mut record, + } => { + let res = rng.try_fill_bytes(dest); + if res.is_ok() { + record.extend_from_slice(&dest); + } + res + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Seed { + XorShift([u8; 16]), + ChaCha([u8; 32]), + PassThrough(Option<(usize, usize)>, Arc<[u8]>), + Recorder([u8; 32]), +} + +impl Seed { + pub(crate) fn from_bytes(algorithm: RngAlgorithm, seed: &[u8]) -> Self { + match algorithm { + RngAlgorithm::XorShift => { + assert_eq!(16, seed.len(), "XorShift requires a 16-byte seed"); + let mut buf = [0; 16]; + buf.copy_from_slice(seed); + Seed::XorShift(buf) + } + + RngAlgorithm::ChaCha => { + assert_eq!(32, seed.len(), "ChaCha requires a 32-byte seed"); + let mut buf = [0; 32]; + buf.copy_from_slice(seed); + Seed::ChaCha(buf) + } + + RngAlgorithm::PassThrough => Seed::PassThrough(None, seed.into()), + + RngAlgorithm::Recorder => { + assert_eq!(32, seed.len(), "Recorder requires a 32-byte seed"); + let mut buf = [0; 32]; + buf.copy_from_slice(seed); + Seed::Recorder(buf) + } + + RngAlgorithm::_NonExhaustive => unreachable!(), + } + } + + pub(crate) fn from_persistence(string: &str) -> Option { + fn from_base16(dst: &mut [u8], src: &str) -> Option<()> { + if dst.len() * 2 != src.len() { + return None; + } + + for (dst_byte, src_pair) in + dst.into_iter().zip(src.as_bytes().chunks(2)) + { + *dst_byte = + u8::from_str_radix(str::from_utf8(src_pair).ok()?, 16) + .ok()?; + } + + Some(()) + } + + let parts = + string.trim().split(char::is_whitespace).collect::>(); + RngAlgorithm::from_persistence_key(&parts[0]).and_then( + |alg| match alg { + RngAlgorithm::XorShift => { + if 5 != parts.len() { + return None; + } + + let mut dwords = [0u32; 4]; + for (dword, part) in + (&mut dwords[..]).into_iter().zip(&parts[1..]) + { + *dword = part.parse().ok()?; + } + + let mut seed = [0u8; 16]; + LittleEndian::write_u32_into(&dwords[..], &mut seed[..]); + Some(Seed::XorShift(seed)) + } + + RngAlgorithm::ChaCha => { + if 2 != parts.len() { + return None; + } + + let mut seed = [0u8; 32]; + from_base16(&mut seed, &parts[1])?; + Some(Seed::ChaCha(seed)) + } + + RngAlgorithm::PassThrough => { + if 1 == parts.len() { + return Some(Seed::PassThrough(None, vec![].into())); + } + + if 2 != parts.len() { + return None; + } + + let mut seed = vec![0u8; parts[1].len() / 2]; + from_base16(&mut seed, &parts[1])?; + Some(Seed::PassThrough(None, seed.into())) + } + + RngAlgorithm::Recorder => { + if 2 != parts.len() { + return None; + } + + let mut seed = [0u8; 32]; + from_base16(&mut seed, &parts[1])?; + Some(Seed::Recorder(seed)) + } + + RngAlgorithm::_NonExhaustive => unreachable!(), + }, + ) + } + + pub(crate) fn to_persistence(&self) -> String { + fn to_base16(dst: &mut String, src: &[u8]) { + for byte in src { + dst.push_str(&format!("{:02x}", byte)); + } + } + + match *self { + Seed::XorShift(ref seed) => { + let mut dwords = [0u32; 4]; + LittleEndian::read_u32_into(seed, &mut dwords[..]); + format!( + "{} {} {} {} {}", + RngAlgorithm::XorShift.persistence_key(), + dwords[0], + dwords[1], + dwords[2], + dwords[3] + ) + } + + Seed::ChaCha(ref seed) => { + let mut string = + RngAlgorithm::ChaCha.persistence_key().to_owned(); + string.push(' '); + to_base16(&mut string, seed); + string + } + + Seed::PassThrough(bounds, ref data) => { + let data = + bounds.map_or(&data[..], |(start, end)| &data[start..end]); + let mut string = + RngAlgorithm::PassThrough.persistence_key().to_owned(); + string.push(' '); + to_base16(&mut string, data); + string + } + + Seed::Recorder(ref seed) => { + let mut string = + RngAlgorithm::Recorder.persistence_key().to_owned(); + string.push(' '); + to_base16(&mut string, seed); + string + } + } + } +} + +impl TestRng { + /// Create a new RNG with the given algorithm and seed. + /// + /// Any RNG created with the same algorithm-seed pair will produce the same + /// sequence of values on all systems and all supporting versions of + /// proptest. + /// + /// ## Panics + /// + /// Panics if `seed` is not an appropriate length for `algorithm`. + pub fn from_seed(algorithm: RngAlgorithm, seed: &[u8]) -> Self { + TestRng::from_seed_internal(Seed::from_bytes(algorithm, seed)) + } + + /// Dumps the bytes obtained from the RNG so far (only works if the RNG is + /// set to `Recorder`). + /// + /// ## Panics + /// + /// Panics if this RNG does not capture generated data. + pub fn bytes_used(&self) -> Vec { + match self.rng { + TestRngImpl::Recorder { ref record, .. } => record.clone(), + _ => panic!("bytes_used() called on non-Recorder RNG"), + } + } + + /// Construct a default TestRng from entropy. + pub(crate) fn default_rng(algorithm: RngAlgorithm) -> Self { + #[cfg(feature = "std")] + { + Self { + rng: match algorithm { + RngAlgorithm::XorShift => { + TestRngImpl::XorShift(XorShiftRng::from_entropy()) + } + RngAlgorithm::ChaCha => { + TestRngImpl::ChaCha(ChaChaRng::from_entropy()) + } + RngAlgorithm::PassThrough => { + panic!("cannot create default instance of PassThrough") + } + RngAlgorithm::Recorder => TestRngImpl::Recorder { + rng: ChaChaRng::from_entropy(), + record: Vec::new(), + }, + RngAlgorithm::_NonExhaustive => unreachable!(), + }, + } + } + #[cfg(all( + not(feature = "std"), + any(target_arch = "x86", target_arch = "x86_64"), + feature = "hardware-rng" + ))] + { + return Self::hardware_rng(algorithm); + } + #[cfg(not(feature = "std"))] + { + return Self::deterministic_rng(algorithm); + } + } + + const SEED_FOR_XOR_SHIFT: [u8; 16] = [ + 0xf4, 0x16, 0x16, 0x48, 0xc3, 0xac, 0x77, 0xac, 0x72, 0x20, 0x0b, 0xea, + 0x99, 0x67, 0x2d, 0x6d, + ]; + + const SEED_FOR_CHA_CHA: [u8; 32] = [ + 0xf4, 0x16, 0x16, 0x48, 0xc3, 0xac, 0x77, 0xac, 0x72, 0x20, 0x0b, 0xea, + 0x99, 0x67, 0x2d, 0x6d, 0xca, 0x9f, 0x76, 0xaf, 0x1b, 0x09, 0x73, 0xa0, + 0x59, 0x22, 0x6d, 0xc5, 0x46, 0x39, 0x1c, 0x4a, + ]; + + /// Returns a `TestRng` with a seed generated with the + /// RdRand instruction on x86 machines. + /// + /// This is useful in `no_std` scenarios on x86 where we don't + /// have a random number infrastructure but the `rdrand` instruction is + /// available. + #[cfg(all( + not(feature = "std"), + any(target_arch = "x86", target_arch = "x86_64"), + feature = "hardware-rng" + ))] + pub fn hardware_rng(algorithm: RngAlgorithm) -> Self { + use x86::random::{rdrand_slice, RdRand}; + + Self::from_seed_internal(match algorithm { + RngAlgorithm::XorShift => { + // Initialize to a sane seed just in case + let mut seed: [u8; 16] = TestRng::SEED_FOR_XOR_SHIFT; + unsafe { + let r = rdrand_slice(&mut seed); + debug_assert!(r, "hardware_rng should only be called on machines with support for rdrand"); + } + Seed::XorShift(seed) + } + RngAlgorithm::ChaCha => { + // Initialize to a sane seed just in case + let mut seed: [u8; 32] = TestRng::SEED_FOR_CHA_CHA; + unsafe { + let r = rdrand_slice(&mut seed); + debug_assert!(r, "hardware_rng should only be called on machines with support for rdrand"); + } + Seed::ChaCha(seed) + } + RngAlgorithm::PassThrough => { + panic!("deterministic RNG not available for PassThrough") + } + RngAlgorithm::Recorder => { + // Initialize to a sane seed just in case + let mut seed: [u8; 32] = TestRng::SEED_FOR_CHA_CHA; + unsafe { + let r = rdrand_slice(&mut seed); + debug_assert!(r, "hardware_rng should only be called on machines with support for rdrand"); + } + Seed::Recorder(seed) + } + RngAlgorithm::_NonExhaustive => unreachable!(), + }) + } + + /// Returns a `TestRng` with a particular hard-coded seed. + /// + /// The seed value will always be the same for a particular version of + /// Proptest and algorithm, but may change across releases. + /// + /// This is useful for testing things like strategy implementations without + /// risking getting "unlucky" RNGs which deviate from average behaviour + /// enough to cause spurious failures. For example, a strategy for `bool` + /// which is supposed to produce `true` 50% of the time might have a test + /// which checks that the distribution is "close enough" to 50%. If every + /// test run starts with a different RNG, occasionally there will be + /// spurious test failures when the RNG happens to produce a very skewed + /// distribution. Using this or `TestRunner::deterministic()` avoids such + /// issues. + pub fn deterministic_rng(algorithm: RngAlgorithm) -> Self { + Self::from_seed_internal(match algorithm { + RngAlgorithm::XorShift => { + Seed::XorShift(TestRng::SEED_FOR_XOR_SHIFT) + } + RngAlgorithm::ChaCha => Seed::ChaCha(TestRng::SEED_FOR_CHA_CHA), + RngAlgorithm::PassThrough => { + panic!("deterministic RNG not available for PassThrough") + } + RngAlgorithm::Recorder => Seed::Recorder(TestRng::SEED_FOR_CHA_CHA), + RngAlgorithm::_NonExhaustive => unreachable!(), + }) + } + + /// Construct a TestRng by the perturbed randomized seed + /// from an existing TestRng. + pub(crate) fn gen_rng(&mut self) -> Self { + Self::from_seed_internal(self.new_rng_seed()) + } + + /// Overwrite the given TestRng with the provided seed. + pub(crate) fn set_seed(&mut self, seed: Seed) { + *self = Self::from_seed_internal(seed); + } + + /// Generate a new randomized seed, set it to this TestRng, + /// and return the seed. + pub(crate) fn gen_get_seed(&mut self) -> Seed { + let seed = self.new_rng_seed(); + self.set_seed(seed.clone()); + seed + } + + /// Randomize a perturbed randomized seed from the given TestRng. + pub(crate) fn new_rng_seed(&mut self) -> Seed { + match self.rng { + TestRngImpl::XorShift(ref mut rng) => { + let mut seed = rng.gen::<[u8; 16]>(); + + // Directly using XorShiftRng::from_seed() at this point would + // result in rng and the returned value being exactly the same. + // Perturb the seed with some arbitrary values to prevent this. + for word in seed.chunks_mut(4) { + word[3] ^= 0xde; + word[2] ^= 0xad; + word[1] ^= 0xbe; + word[0] ^= 0xef; + } + + Seed::XorShift(seed) + } + + TestRngImpl::ChaCha(ref mut rng) => Seed::ChaCha(rng.gen()), + + TestRngImpl::PassThrough { + ref mut off, + ref mut end, + ref data, + } => { + let len = *end - *off; + let child_start = *off + len / 2; + let child_end = *off + len; + *end = child_start; + Seed::PassThrough( + Some((child_start, child_end)), + Arc::clone(data), + ) + } + + TestRngImpl::Recorder { ref mut rng, .. } => { + Seed::Recorder(rng.gen()) + } + } + } + + /// Construct a TestRng from a given seed. + fn from_seed_internal(seed: Seed) -> Self { + Self { + rng: match seed { + Seed::XorShift(seed) => { + TestRngImpl::XorShift(XorShiftRng::from_seed(seed)) + } + + Seed::ChaCha(seed) => { + TestRngImpl::ChaCha(ChaChaRng::from_seed(seed)) + } + + Seed::PassThrough(bounds, data) => { + let (start, end) = bounds.unwrap_or((0, data.len())); + TestRngImpl::PassThrough { + off: start, + end, + data, + } + } + + Seed::Recorder(seed) => TestRngImpl::Recorder { + rng: ChaChaRng::from_seed(seed), + record: Vec::new(), + }, + }, + } + } +} + +#[cfg(test)] +mod test { + use crate::std_facade::Vec; + + use rand::{Rng, RngCore}; + + use super::{RngAlgorithm, Seed, TestRng}; + use crate::arbitrary::any; + use crate::strategy::*; + + proptest! { + #[test] + fn gen_parse_seeds( + seed in prop_oneof![ + any::<[u8;16]>().prop_map(Seed::XorShift), + any::<[u8;32]>().prop_map(Seed::ChaCha), + any::>().prop_map(|data| Seed::PassThrough(None, data.into())), + any::<[u8;32]>().prop_map(Seed::Recorder), + ]) + { + assert_eq!(seed, Seed::from_persistence(&seed.to_persistence()).unwrap()); + } + + #[test] + fn rngs_dont_clone_self_on_genrng( + seed in prop_oneof![ + any::<[u8;16]>().prop_map(Seed::XorShift), + any::<[u8;32]>().prop_map(Seed::ChaCha), + Just(()).prop_perturb(|_, mut rng| { + let mut buf = vec![0u8; 2048]; + rng.fill_bytes(&mut buf); + Seed::PassThrough(None, buf.into()) + }), + any::<[u8;32]>().prop_map(Seed::Recorder), + ]) + { + type Value = [u8;32]; + let orig = TestRng::from_seed_internal(seed); + + { + let mut rng1 = orig.clone(); + let mut rng2 = rng1.gen_rng(); + assert_ne!(rng1.gen::(), rng2.gen::()); + } + + { + let mut rng1 = orig.clone(); + let mut rng2 = rng1.gen_rng(); + let mut rng3 = rng1.gen_rng(); + let mut rng4 = rng2.gen_rng(); + let a = rng1.gen::(); + let b = rng2.gen::(); + let c = rng3.gen::(); + let d = rng4.gen::(); + assert_ne!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + assert_ne!(b, c); + assert_ne!(b, d); + assert_ne!(c, d); + } + } + } + + #[test] + fn passthrough_rng_behaves_properly() { + let mut rng = TestRng::from_seed( + RngAlgorithm::PassThrough, + &[ + 0xDE, 0xC0, 0x12, 0x34, 0x56, 0x78, 0xFE, 0xCA, 0xEF, 0xBE, + 0xAD, 0xDE, 0x01, 0x02, 0x03, + ], + ); + + assert_eq!(0x3412C0DE, rng.next_u32()); + assert_eq!(0xDEADBEEFCAFE7856, rng.next_u64()); + + let mut buf = [0u8; 4]; + rng.try_fill_bytes(&mut buf[0..4]).unwrap(); + assert_eq!([1, 2, 3, 0], buf); + rng.try_fill_bytes(&mut buf[0..4]).unwrap(); + assert_eq!([0, 0, 0, 0], buf); + } +} diff --git a/vendor/proptest/src/test_runner/runner.rs b/vendor/proptest/src/test_runner/runner.rs new file mode 100644 index 000000000..ce540499e --- /dev/null +++ b/vendor/proptest/src/test_runner/runner.rs @@ -0,0 +1,1538 @@ +//- +// Copyright 2017, 2018, 2019 The proptest developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::std_facade::{Arc, BTreeMap, Box, String, Vec}; +use core::sync::atomic::AtomicUsize; +use core::sync::atomic::Ordering::SeqCst; +use core::{fmt, iter}; +#[cfg(feature = "std")] +use std::panic::{self, AssertUnwindSafe}; + +#[cfg(feature = "fork")] +use rusty_fork; +#[cfg(feature = "fork")] +use std::cell::{Cell, RefCell}; +#[cfg(feature = "fork")] +use std::env; +#[cfg(feature = "fork")] +use std::fs; +#[cfg(feature = "fork")] +use tempfile; + +use crate::strategy::*; +use crate::test_runner::config::*; +use crate::test_runner::errors::*; +use crate::test_runner::failure_persistence::PersistedSeed; +use crate::test_runner::reason::*; +#[cfg(feature = "fork")] +use crate::test_runner::replay; +use crate::test_runner::result_cache::*; +use crate::test_runner::rng::TestRng; + +#[cfg(feature = "fork")] +const ENV_FORK_FILE: &'static str = "_PROPTEST_FORKFILE"; + +const ALWAYS: u32 = 0; +const SHOW_FALURES: u32 = 1; +const TRACE: u32 = 2; + +#[cfg(feature = "std")] +macro_rules! verbose_message { + ($runner:expr, $level:expr, $fmt:tt $($arg:tt)*) => { { + #[allow(unused_comparisons)] + { + if $runner.config.verbose >= $level { + eprintln!(concat!("proptest: ", $fmt) $($arg)*); + } + }; + () + } } +} + +#[cfg(not(feature = "std"))] +macro_rules! verbose_message { + ($runner:expr, $level:expr, $fmt:tt $($arg:tt)*) => { + let _ = $level; + }; +} + +type RejectionDetail = BTreeMap; + +/// State used when running a proptest test. +#[derive(Clone)] +pub struct TestRunner { + config: Config, + successes: u32, + local_rejects: u32, + global_rejects: u32, + rng: TestRng, + flat_map_regens: Arc, + + local_reject_detail: RejectionDetail, + global_reject_detail: RejectionDetail, +} + +impl fmt::Debug for TestRunner { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("TestRunner") + .field("config", &self.config) + .field("successes", &self.successes) + .field("local_rejects", &self.local_rejects) + .field("global_rejects", &self.global_rejects) + .field("rng", &"") + .field("flat_map_regens", &self.flat_map_regens) + .field("local_reject_detail", &self.local_reject_detail) + .field("global_reject_detail", &self.global_reject_detail) + .finish() + } +} + +impl fmt::Display for TestRunner { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "\tsuccesses: {}\n\ + \tlocal rejects: {}\n", + self.successes, self.local_rejects + )?; + for (whence, count) in &self.local_reject_detail { + writeln!(f, "\t\t{} times at {}", count, whence)?; + } + writeln!(f, "\tglobal rejects: {}", self.global_rejects)?; + for (whence, count) in &self.global_reject_detail { + writeln!(f, "\t\t{} times at {}", count, whence)?; + } + + Ok(()) + } +} + +/// Equivalent to: `TestRunner::new(Config::default())`. +impl Default for TestRunner { + fn default() -> Self { + Self::new(Config::default()) + } +} + +#[cfg(feature = "fork")] +#[derive(Debug)] +struct ForkOutput { + file: Option, +} + +#[cfg(feature = "fork")] +impl ForkOutput { + fn append(&mut self, result: &TestCaseResult) { + if let Some(ref mut file) = self.file { + replay::append(file, result) + .expect("Failed to append to replay file"); + } + } + + fn ping(&mut self) { + if let Some(ref mut file) = self.file { + replay::ping(file).expect("Failed to append to replay file"); + } + } + + fn terminate(&mut self) { + if let Some(ref mut file) = self.file { + replay::terminate(file).expect("Failed to append to replay file"); + } + } + + fn empty() -> Self { + ForkOutput { file: None } + } + + fn is_in_fork(&self) -> bool { + self.file.is_some() + } +} + +#[cfg(not(feature = "fork"))] +#[derive(Debug)] +struct ForkOutput; + +#[cfg(not(feature = "fork"))] +impl ForkOutput { + fn append(&mut self, _result: &TestCaseResult) {} + fn ping(&mut self) {} + fn terminate(&mut self) {} + fn empty() -> Self { + ForkOutput + } + fn is_in_fork(&self) -> bool { + false + } +} + +#[cfg(not(feature = "std"))] +fn call_test( + _runner: &mut TestRunner, + case: V, + test: &F, + replay_from_fork: &mut R, + result_cache: &mut dyn ResultCache, + _: &mut ForkOutput, + is_from_persisted_seed: bool, +) -> TestCaseResultV2 +where + V: fmt::Debug, + F: Fn(V) -> TestCaseResult, + R: Iterator, +{ + if let Some(result) = replay_from_fork.next() { + return result.map(|_| TestCaseOk::ReplayFromForkSuccess); + } + + let cache_key = result_cache.key(&ResultCacheKey::new(&case)); + if let Some(result) = result_cache.get(cache_key) { + return result.clone().map(|_| TestCaseOk::CacheHitSuccess); + } + + let result = test(case); + result_cache.put(cache_key, &result); + result.map(|_| { + if is_from_persisted_seed { + TestCaseOk::PersistedCaseSuccess + } else { + TestCaseOk::NewCaseSuccess + } + }) +} + +#[cfg(feature = "std")] +fn call_test( + runner: &mut TestRunner, + case: V, + test: &F, + replay_from_fork: &mut R, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, +) -> TestCaseResultV2 +where + V: fmt::Debug, + F: Fn(V) -> TestCaseResult, + R: Iterator, +{ + use std::time; + + let timeout = runner.config.timeout(); + + if let Some(result) = replay_from_fork.next() { + return result.map(|_| TestCaseOk::ReplayFromForkSuccess); + } + + // Now that we're about to start a new test (as far as the replay system is + // concerned), ping the replay file so the parent process can determine + // that we made it this far. + fork_output.ping(); + + verbose_message!(runner, TRACE, "Next test input: {:?}", case); + + let cache_key = result_cache.key(&ResultCacheKey::new(&case)); + if let Some(result) = result_cache.get(cache_key) { + verbose_message!( + runner, + TRACE, + "Test input hit cache, skipping execution" + ); + return result.clone().map(|_| TestCaseOk::CacheHitSuccess); + } + + let time_start = time::Instant::now(); + + let mut result = unwrap_or!( + panic::catch_unwind(AssertUnwindSafe(|| test(case))), + what => Err(TestCaseError::Fail( + what.downcast::<&'static str>().map(|s| (*s).into()) + .or_else(|what| what.downcast::().map(|b| (*b).into())) + .or_else(|what| what.downcast::>().map(|b| (*b).into())) + .unwrap_or_else(|_| "".into())))); + + // If there is a timeout and we exceeded it, fail the test here so we get + // consistent behaviour. (The parent process cannot precisely time the test + // cases itself.) + if timeout > 0 && result.is_ok() { + let elapsed = time_start.elapsed(); + let elapsed_millis = elapsed.as_secs() as u32 * 1000 + + elapsed.subsec_nanos() / 1_000_000; + + if elapsed_millis > timeout { + result = Err(TestCaseError::fail(format!( + "Timeout of {} ms exceeded: test took {} ms", + timeout, elapsed_millis + ))); + } + } + + result_cache.put(cache_key, &result); + fork_output.append(&result); + + match result { + Ok(()) => verbose_message!(runner, TRACE, "Test case passed"), + Err(TestCaseError::Reject(ref reason)) => verbose_message!( + runner, + SHOW_FALURES, + "Test case rejected: {}", + reason + ), + Err(TestCaseError::Fail(ref reason)) => verbose_message!( + runner, + SHOW_FALURES, + "Test case failed: {}", + reason + ), + } + + result.map(|_| { + if is_from_persisted_seed { + TestCaseOk::PersistedCaseSuccess + } else { + TestCaseOk::NewCaseSuccess + } + }) +} + +type TestRunResult = Result<(), TestError<::Value>>; + +impl TestRunner { + /// Create a fresh `TestRunner` with the given configuration. + /// + /// The runner will use an RNG with a generated seed and the default + /// algorithm. + /// + /// In `no_std` environments, every `TestRunner` will use the same + /// hard-coded seed. This seed is not contractually guaranteed and may be + /// changed between releases without notice. + pub fn new(config: Config) -> Self { + let algorithm = config.rng_algorithm; + TestRunner::new_with_rng(config, TestRng::default_rng(algorithm)) + } + + /// Create a fresh `TestRunner` with the standard deterministic RNG. + /// + /// This is sugar for the following: + /// + /// ```rust + /// # use proptest::test_runner::*; + /// let config = Config::default(); + /// let algorithm = config.rng_algorithm; + /// TestRunner::new_with_rng( + /// config, + /// TestRng::deterministic_rng(algorithm)); + /// ``` + /// + /// Refer to `TestRng::deterministic_rng()` for more information on the + /// properties of the RNG used here. + pub fn deterministic() -> Self { + let config = Config::default(); + let algorithm = config.rng_algorithm; + TestRunner::new_with_rng(config, TestRng::deterministic_rng(algorithm)) + } + + /// Create a fresh `TestRunner` with the given configuration and RNG. + pub fn new_with_rng(config: Config, rng: TestRng) -> Self { + TestRunner { + config: config, + successes: 0, + local_rejects: 0, + global_rejects: 0, + rng: rng, + flat_map_regens: Arc::new(AtomicUsize::new(0)), + local_reject_detail: BTreeMap::new(), + global_reject_detail: BTreeMap::new(), + } + } + + /// Create a fresh `TestRunner` with the same config and global counters as + /// this one, but with local state reset and an independent `Rng` (but + /// deterministic). + pub(crate) fn partial_clone(&mut self) -> Self { + TestRunner { + config: self.config.clone(), + successes: 0, + local_rejects: 0, + global_rejects: 0, + rng: self.new_rng(), + flat_map_regens: Arc::clone(&self.flat_map_regens), + local_reject_detail: BTreeMap::new(), + global_reject_detail: BTreeMap::new(), + } + } + + /// Returns the RNG for this test run. + pub fn rng(&mut self) -> &mut TestRng { + &mut self.rng + } + + /// Create a new, independent but deterministic RNG from the RNG in this + /// runner. + pub fn new_rng(&mut self) -> TestRng { + self.rng.gen_rng() + } + + /// Returns the configuration of this runner. + pub fn config(&self) -> &Config { + &self.config + } + + /// Dumps the bytes obtained from the RNG so far (only works if the RNG is + /// set to `Recorder`). + /// + /// ## Panics + /// + /// Panics if the RNG does not capture generated data. + pub fn bytes_used(&self) -> Vec { + self.rng.bytes_used() + } + + /// Run test cases against `f`, choosing inputs via `strategy`. + /// + /// If any failure cases occur, try to find a minimal failure case and + /// report that. If invoking `f` panics, the panic is turned into a + /// `TestCaseError::Fail`. + /// + /// If failure persistence is enabled, all persisted failing cases are + /// tested first. If a later non-persisted case fails, its seed is + /// persisted before returning failure. + /// + /// Returns success or failure indicating why the test as a whole failed. + pub fn run( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult { + if self.config.fork() { + self.run_in_fork(strategy, test) + } else { + self.run_in_process(strategy, test) + } + } + + #[cfg(not(feature = "fork"))] + fn run_in_fork( + &mut self, + _: &S, + _: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult { + unreachable!() + } + + #[cfg(feature = "fork")] + fn run_in_fork( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult { + let mut test = Some(test); + + let test_name = rusty_fork::fork_test::fix_module_path( + self.config + .test_name + .expect("Must supply test_name when forking enabled"), + ); + let forkfile: RefCell> = + RefCell::new(None); + let init_forkfile_size = Cell::new(0u64); + let seed = self.rng.new_rng_seed(); + let mut replay = replay::Replay { + seed, + steps: vec![], + }; + let mut child_count = 0; + let timeout = self.config.timeout(); + + fn forkfile_size(forkfile: &Option) -> u64 { + forkfile.as_ref().map_or(0, |ff| { + ff.as_file().metadata().map(|md| md.len()).unwrap_or(0) + }) + } + + loop { + let (child_error, last_fork_file_len) = rusty_fork::fork( + test_name, + rusty_fork_id!(), + |cmd| { + let mut forkfile = forkfile.borrow_mut(); + if forkfile.is_none() { + *forkfile = + Some(tempfile::NamedTempFile::new().expect( + "Failed to create temporary file for fork", + )); + replay.init_file(forkfile.as_mut().unwrap()).expect( + "Failed to initialise temporary file for fork", + ); + } + + init_forkfile_size.set(forkfile_size(&forkfile)); + + cmd.env(ENV_FORK_FILE, forkfile.as_ref().unwrap().path()); + }, + |child, _| { + await_child( + child, + &mut forkfile.borrow_mut().as_mut().unwrap(), + timeout, + ) + }, + || match self.run_in_process(strategy, test.take().unwrap()) { + Ok(_) => (), + Err(e) => panic!( + "Test failed normally in child process.\n{}\n{}", + e, self + ), + }, + ) + .expect("Fork failed"); + + let parsed = replay::Replay::parse_from( + &mut forkfile.borrow_mut().as_mut().unwrap(), + ) + .expect("Failed to re-read fork file"); + match parsed { + replay::ReplayFileStatus::InProgress(new_replay) => { + replay = new_replay + } + replay::ReplayFileStatus::Terminated(new_replay) => { + replay = new_replay; + break; + } + replay::ReplayFileStatus::Corrupt => { + panic!("Child process corrupted replay file") + } + } + + let curr_forkfile_size = forkfile_size(&forkfile.borrow()); + + // If the child failed to append *anything* to the forkfile, it + // crashed or timed out before starting even one test case, so + // bail. + if curr_forkfile_size == init_forkfile_size.get() { + return Err(TestError::Abort( + "Child process crashed or timed out before the first test \ + started running; giving up." + .into(), + )); + } + + // The child only terminates early if it outright crashes or we + // kill it due to timeout, so add a synthetic failure to the + // output. But only do this if the length of the fork file is the + // same as when we last saw it, or if the child was not killed due + // to timeout. (This is because the child could have appended + // something to the file after we gave up waiting for it but before + // we were able to kill it). + if last_fork_file_len.map_or(true, |last_fork_file_len| { + last_fork_file_len == curr_forkfile_size + }) { + let error = Err(child_error.unwrap_or(TestCaseError::fail( + "Child process was terminated abruptly \ + but with successful status", + ))); + replay::append(forkfile.borrow_mut().as_mut().unwrap(), &error) + .expect("Failed to append to replay file"); + replay.steps.push(error); + } + + // Bail if we've gone through too many processes in case the + // shrinking process itself is crashing. + child_count += 1; + if child_count >= 10000 { + return Err(TestError::Abort( + "Giving up after 10000 child processes crashed".into(), + )); + } + } + + // Run through the steps in-process (without ever running the actual + // tests) to produce the shrunken value and update the persistence + // file. + self.rng.set_seed(replay.seed); + self.run_in_process_with_replay( + strategy, + |_| panic!("Ran past the end of the replay"), + replay.steps.into_iter(), + ForkOutput::empty(), + ) + } + + fn run_in_process( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult { + let (replay_steps, fork_output) = init_replay(&mut self.rng); + self.run_in_process_with_replay( + strategy, + test, + replay_steps.into_iter(), + fork_output, + ) + } + + fn run_in_process_with_replay( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + mut replay_from_fork: impl Iterator, + mut fork_output: ForkOutput, + ) -> TestRunResult { + let old_rng = self.rng.clone(); + + let persisted_failure_seeds: Vec = self + .config + .failure_persistence + .as_ref() + .map(|f| f.load_persisted_failures2(self.config.source_file)) + .unwrap_or_default(); + + let mut result_cache = self.new_cache(); + + for PersistedSeed(persisted_seed) in persisted_failure_seeds { + self.rng.set_seed(persisted_seed); + self.gen_and_run_case( + strategy, + &test, + &mut replay_from_fork, + &mut *result_cache, + &mut fork_output, + true, + )?; + } + self.rng = old_rng; + + while self.successes < self.config.cases { + // Generate a new seed and make an RNG from that so that we know + // what seed to persist if this case fails. + let seed = self.rng.gen_get_seed(); + let result = self.gen_and_run_case( + strategy, + &test, + &mut replay_from_fork, + &mut *result_cache, + &mut fork_output, + false, + ); + if let Err(TestError::Fail(_, ref value)) = result { + if let Some(ref mut failure_persistence) = + self.config.failure_persistence + { + let source_file = &self.config.source_file; + + // Don't update the persistence file if we're a child + // process. The parent relies on it remaining consistent + // and will take care of updating it itself. + if !fork_output.is_in_fork() { + failure_persistence.save_persisted_failure2( + *source_file, + PersistedSeed(seed), + value, + ); + } + } + } + + if let Err(e) = result { + fork_output.terminate(); + return Err(e.into()); + } + } + + fork_output.terminate(); + Ok(()) + } + + fn gen_and_run_case( + &mut self, + strategy: &S, + f: &impl Fn(S::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> TestRunResult { + let case = unwrap_or!(strategy.new_tree(self), msg => + return Err(TestError::Abort(msg))); + + // We only count new cases to our set of successful runs against + // `PROPTEST_CASES` config. + let ok_type = self.run_one_with_replay( + case, + f, + replay_from_fork, + result_cache, + fork_output, + is_from_persisted_seed, + )?; + match ok_type { + TestCaseOk::NewCaseSuccess | TestCaseOk::ReplayFromForkSuccess => { + self.successes += 1 + } + TestCaseOk::PersistedCaseSuccess + | TestCaseOk::CacheHitSuccess + | TestCaseOk::Reject => (), + } + + Ok(()) + } + + /// Run one specific test case against this runner. + /// + /// If the test fails, finds the minimal failing test case. If the test + /// does not fail, returns whether it succeeded or was filtered out. + /// + /// This does not honour the `fork` config, and will not be able to + /// terminate the run if it runs for longer than `timeout`. However, if the + /// test function returns but took longer than `timeout`, the test case + /// will fail. + pub fn run_one( + &mut self, + case: V, + test: impl Fn(V::Value) -> TestCaseResult, + ) -> Result> { + let mut result_cache = self.new_cache(); + self.run_one_with_replay( + case, + test, + &mut iter::empty::().fuse(), + &mut *result_cache, + &mut ForkOutput::empty(), + false, + ) + .map(|ok_type| match ok_type { + TestCaseOk::Reject => false, + _ => true, + }) + } + + fn run_one_with_replay( + &mut self, + mut case: V, + test: impl Fn(V::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> Result> { + let result = call_test( + self, + case.current(), + &test, + replay_from_fork, + result_cache, + fork_output, + is_from_persisted_seed, + ); + + match result { + Ok(success_type) => Ok(success_type), + Err(TestCaseError::Fail(why)) => { + let why = self + .shrink( + &mut case, + test, + replay_from_fork, + result_cache, + fork_output, + is_from_persisted_seed, + ) + .unwrap_or(why); + Err(TestError::Fail(why, case.current())) + } + Err(TestCaseError::Reject(whence)) => { + self.reject_global(whence)?; + Ok(TestCaseOk::Reject) + } + } + } + + fn shrink( + &mut self, + case: &mut V, + test: impl Fn(V::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> Option { + #[cfg(feature = "std")] + use std::time; + + let mut last_failure = None; + let mut iterations = 0; + #[cfg(feature = "std")] + let start_time = time::Instant::now(); + + if case.simplify() { + loop { + #[cfg(feature = "std")] + let timed_out = if self.config.max_shrink_time > 0 { + let elapsed = start_time.elapsed(); + let elapsed_ms = elapsed + .as_secs() + .saturating_mul(1000) + .saturating_add(elapsed.subsec_millis().into()); + if elapsed_ms > self.config.max_shrink_time as u64 { + Some(elapsed_ms) + } else { + None + } + } else { + None + }; + #[cfg(not(feature = "std"))] + let timed_out: Option = None; + + let bail = if iterations >= self.config.max_shrink_iters() { + #[cfg(feature = "std")] + const CONTROLLER: &str = + "the PROPTEST_MAX_SHRINK_ITERS environment \ + variable or ProptestConfig.max_shrink_iters"; + #[cfg(not(feature = "std"))] + const CONTROLLER: &str = "ProptestConfig.max_shrink_iters"; + verbose_message!( + self, + ALWAYS, + "Aborting shrinking after {} iterations (set {} \ + to a large(r) value to shrink more; current \ + configuration: {} iterations)", + CONTROLLER, + self.config.max_shrink_iters(), + iterations + ); + true + } else if let Some(ms) = timed_out { + #[cfg(feature = "std")] + const CONTROLLER: &str = + "the PROPTEST_MAX_SHRINK_TIME environment \ + variable or ProptestConfig.max_shrink_time"; + #[cfg(feature = "std")] + let current = self.config.max_shrink_time; + #[cfg(not(feature = "std"))] + const CONTROLLER: &str = "(not configurable in no_std)"; + #[cfg(not(feature = "std"))] + let current = 0; + verbose_message!( + self, + ALWAYS, + "Aborting shrinking after taking too long: {} ms \ + (set {} to a large(r) value to shrink more; current \ + configuration: {} ms)", + ms, + CONTROLLER, + current + ); + true + } else { + false + }; + + if bail { + // Move back to the most recent failing case + while case.complicate() { + fork_output.append(&Ok(())); + } + break; + } + + iterations += 1; + + let result = call_test( + self, + case.current(), + &test, + replay_from_fork, + result_cache, + fork_output, + is_from_persisted_seed, + ); + + match result { + // Rejections are effectively a pass here, + // since they indicate that any behaviour of + // the function under test is acceptable. + Ok(_) | Err(TestCaseError::Reject(..)) => { + if !case.complicate() { + break; + } + } + Err(TestCaseError::Fail(why)) => { + last_failure = Some(why); + if !case.simplify() { + break; + } + } + } + } + } + + last_failure + } + + /// Update the state to account for a local rejection from `whence`, and + /// return `Ok` if the caller should keep going or `Err` to abort. + pub fn reject_local( + &mut self, + whence: impl Into, + ) -> Result<(), Reason> { + if self.local_rejects >= self.config.max_local_rejects { + Err("Too many local rejects".into()) + } else { + self.local_rejects += 1; + Self::insert_or_increment( + &mut self.local_reject_detail, + whence.into(), + ); + Ok(()) + } + } + + /// Update the state to account for a global rejection from `whence`, and + /// return `Ok` if the caller should keep going or `Err` to abort. + fn reject_global(&mut self, whence: Reason) -> Result<(), TestError> { + if self.global_rejects >= self.config.max_global_rejects { + Err(TestError::Abort("Too many global rejects".into())) + } else { + self.global_rejects += 1; + Self::insert_or_increment(&mut self.global_reject_detail, whence); + Ok(()) + } + } + + /// Insert 1 or increment the rejection detail at key for whence. + fn insert_or_increment(into: &mut RejectionDetail, whence: Reason) { + into.entry(whence) + .and_modify(|count| *count += 1) + .or_insert(1); + } + + /// Increment the counter of flat map regenerations and return whether it + /// is still under the configured limit. + pub fn flat_map_regen(&self) -> bool { + self.flat_map_regens.fetch_add(1, SeqCst) + < self.config.max_flat_map_regens as usize + } + + fn new_cache(&self) -> Box { + (self.config.result_cache)() + } +} + +#[cfg(feature = "fork")] +fn init_replay(rng: &mut TestRng) -> (Vec, ForkOutput) { + use crate::test_runner::replay::{open_file, Replay, ReplayFileStatus::*}; + + if let Some(path) = env::var_os(ENV_FORK_FILE) { + let mut file = open_file(&path).expect("Failed to open replay file"); + let loaded = + Replay::parse_from(&mut file).expect("Failed to read replay file"); + match loaded { + InProgress(replay) => { + rng.set_seed(replay.seed); + (replay.steps, ForkOutput { file: Some(file) }) + } + + Terminated(_) => { + panic!("Replay file for child process is terminated?") + } + + Corrupt => panic!("Replay file for child process is corrupt"), + } + } else { + (vec![], ForkOutput::empty()) + } +} + +#[cfg(not(feature = "fork"))] +fn init_replay( + _rng: &mut TestRng, +) -> (iter::Empty, ForkOutput) { + (iter::empty(), ForkOutput::empty()) +} + +#[cfg(feature = "fork")] +fn await_child_without_timeout( + child: &mut rusty_fork::ChildWrapper, +) -> (Option, Option) { + let status = child.wait().expect("Failed to wait for child process"); + + if status.success() { + (None, None) + } else { + ( + Some(TestCaseError::fail(format!( + "Child process exited with {}", + status + ))), + None, + ) + } +} + +#[cfg(all(feature = "fork", not(feature = "timeout")))] +fn await_child( + child: &mut rusty_fork::ChildWrapper, + _: &mut tempfile::NamedTempFile, + _timeout: u32, +) -> (Option, Option) { + await_child_without_timeout(child) +} + +#[cfg(all(feature = "fork", feature = "timeout"))] +fn await_child( + child: &mut rusty_fork::ChildWrapper, + forkfile: &mut tempfile::NamedTempFile, + timeout: u32, +) -> (Option, Option) { + use std::time::Duration; + + if 0 == timeout { + return await_child_without_timeout(child); + } + + // The child can run for longer than the timeout since it may run + // multiple tests. Each time the timeout expires, we check whether the + // file has grown larger. If it has, we allow the child to keep running + // until the next timeout. + let mut last_forkfile_len = forkfile + .as_file() + .metadata() + .map(|md| md.len()) + .unwrap_or(0); + + loop { + if let Some(status) = child + .wait_timeout(Duration::from_millis(timeout.into())) + .expect("Failed to wait for child process") + { + if status.success() { + return (None, None); + } else { + return ( + Some(TestCaseError::fail(format!( + "Child process exited with {}", + status + ))), + None, + ); + } + } + + let current_len = forkfile + .as_file() + .metadata() + .map(|md| md.len()) + .unwrap_or(0); + // If we've gone a full timeout period without the file growing, + // fail the test and kill the child. + if current_len <= last_forkfile_len { + return ( + Some(TestCaseError::fail(format!( + "Timed out waiting for child process" + ))), + Some(current_len), + ); + } else { + last_forkfile_len = current_len; + } + } +} + +#[cfg(test)] +mod test { + use std::cell::Cell; + use std::fs; + + use super::*; + use crate::strategy::Strategy; + use crate::test_runner::{FileFailurePersistence, RngAlgorithm, TestRng}; + + #[test] + fn gives_up_after_too_many_rejections() { + let config = Config::default(); + let mut runner = TestRunner::new(config.clone()); + let runs = Cell::new(0); + let result = runner.run(&(0u32..), |_| { + runs.set(runs.get() + 1); + Err(TestCaseError::reject("reject")) + }); + match result { + Err(TestError::Abort(_)) => (), + e => panic!("Unexpected result: {:?}", e), + } + assert_eq!(config.max_global_rejects + 1, runs.get()); + } + + #[test] + fn test_pass() { + let mut runner = TestRunner::default(); + let result = runner.run(&(1u32..), |v| { + assert!(v > 0); + Ok(()) + }); + assert_eq!(Ok(()), result); + } + + #[test] + fn test_fail_via_result() { + let mut runner = TestRunner::new(Config { + failure_persistence: None, + ..Config::default() + }); + let result = runner.run(&(0u32..10u32), |v| { + if v < 5 { + Ok(()) + } else { + Err(TestCaseError::fail("not less than 5")) + } + }); + + assert_eq!(Err(TestError::Fail("not less than 5".into(), 5)), result); + } + + #[test] + fn test_fail_via_panic() { + let mut runner = TestRunner::new(Config { + failure_persistence: None, + ..Config::default() + }); + let result = runner.run(&(0u32..10u32), |v| { + assert!(v < 5, "not less than 5"); + Ok(()) + }); + assert_eq!(Err(TestError::Fail("not less than 5".into(), 5)), result); + } + + #[test] + fn persisted_cases_do_not_count_towards_total_cases() { + const FILE: &'static str = "persistence-test.txt"; + let _ = fs::remove_file(FILE); + + let config = Config { + failure_persistence: Some(Box::new( + FileFailurePersistence::Direct(FILE), + )), + cases: 1, + ..Config::default() + }; + + let max = 10_000_000i32; + { + TestRunner::new(config.clone()) + .run(&(0i32..max), |_v| { + Err(TestCaseError::Fail("persist a failure".into())) + }) + .expect_err("didn't fail?"); + } + + let run_count = RefCell::new(0); + TestRunner::new(config.clone()) + .run(&(0i32..max), |_v| { + *run_count.borrow_mut() += 1; + Ok(()) + }) + .expect("should succeed"); + + // Persisted ran, and a new case ran, and only new case counts + // against `cases: 1`. + assert_eq!(run_count.into_inner(), 2); + } + + #[derive(Clone, Copy, PartialEq)] + struct PoorlyBehavedDebug(i32); + impl fmt::Debug for PoorlyBehavedDebug { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\r\n{:?}\r\n", self.0) + } + } + + #[test] + fn failing_cases_persisted_and_reloaded() { + const FILE: &'static str = "persistence-test.txt"; + let _ = fs::remove_file(FILE); + + let max = 10_000_000i32; + let input = (0i32..max).prop_map(PoorlyBehavedDebug); + let config = Config { + failure_persistence: Some(Box::new( + FileFailurePersistence::Direct(FILE), + )), + ..Config::default() + }; + + // First test with cases that fail above half max, and then below half + // max, to ensure we can correctly parse both lines of the persistence + // file. + let first_sub_failure = { + TestRunner::new(config.clone()) + .run(&input, |v| { + if v.0 < max / 2 { + Ok(()) + } else { + Err(TestCaseError::Fail("too big".into())) + } + }) + .expect_err("didn't fail?") + }; + let first_super_failure = { + TestRunner::new(config.clone()) + .run(&input, |v| { + if v.0 >= max / 2 { + Ok(()) + } else { + Err(TestCaseError::Fail("too small".into())) + } + }) + .expect_err("didn't fail?") + }; + let second_sub_failure = { + TestRunner::new(config.clone()) + .run(&input, |v| { + if v.0 < max / 2 { + Ok(()) + } else { + Err(TestCaseError::Fail("too big".into())) + } + }) + .expect_err("didn't fail?") + }; + let second_super_failure = { + TestRunner::new(config.clone()) + .run(&input, |v| { + if v.0 >= max / 2 { + Ok(()) + } else { + Err(TestCaseError::Fail("too small".into())) + } + }) + .expect_err("didn't fail?") + }; + + assert_eq!(first_sub_failure, second_sub_failure); + assert_eq!(first_super_failure, second_super_failure); + } + + #[test] + fn new_rng_makes_separate_rng() { + use rand::Rng; + let mut runner = TestRunner::default(); + let from_1 = runner.new_rng().gen::<[u8; 16]>(); + let from_2 = runner.rng().gen::<[u8; 16]>(); + assert_ne!(from_1, from_2); + } + + #[test] + fn record_rng_use() { + use rand::Rng; + + // create value with recorder rng + let default_config = Config::default(); + let recorder_rng = TestRng::default_rng(RngAlgorithm::Recorder); + let mut runner = + TestRunner::new_with_rng(default_config.clone(), recorder_rng); + let random_byte_array1 = runner.rng().gen::<[u8; 16]>(); + let bytes_used = runner.bytes_used(); + assert!(bytes_used.len() >= 16); // could use more bytes for some reason + + // re-create value with pass-through rng + let passthrough_rng = + TestRng::from_seed(RngAlgorithm::PassThrough, &bytes_used); + let mut runner = + TestRunner::new_with_rng(default_config, passthrough_rng); + let random_byte_array2 = runner.rng().gen::<[u8; 16]>(); + + // make sure the same value was created + assert_eq!(random_byte_array1, random_byte_array2); + } + + #[cfg(feature = "fork")] + #[test] + fn run_successful_test_in_fork() { + let mut runner = TestRunner::new(Config { + fork: true, + test_name: Some(concat!( + module_path!(), + "::run_successful_test_in_fork" + )), + ..Config::default() + }); + + assert!(runner.run(&(0u32..1000), |_| Ok(())).is_ok()); + } + + #[cfg(feature = "fork")] + #[test] + fn normal_failure_in_fork_results_in_correct_failure() { + let mut runner = TestRunner::new(Config { + fork: true, + test_name: Some(concat!( + module_path!(), + "::normal_failure_in_fork_results_in_correct_failure" + )), + ..Config::default() + }); + + let failure = runner + .run(&(0u32..1000), |v| { + prop_assert!(v < 500); + Ok(()) + }) + .err() + .unwrap(); + + match failure { + TestError::Fail(_, value) => assert_eq!(500, value), + failure => panic!("Unexpected failure: {:?}", failure), + } + } + + #[cfg(feature = "fork")] + #[test] + fn nonsuccessful_exit_finds_correct_failure() { + let mut runner = TestRunner::new(Config { + fork: true, + test_name: Some(concat!( + module_path!(), + "::nonsuccessful_exit_finds_correct_failure" + )), + ..Config::default() + }); + + let failure = runner + .run(&(0u32..1000), |v| { + if v >= 500 { + ::std::process::exit(1); + } + Ok(()) + }) + .err() + .unwrap(); + + match failure { + TestError::Fail(_, value) => assert_eq!(500, value), + failure => panic!("Unexpected failure: {:?}", failure), + } + } + + #[cfg(feature = "fork")] + #[test] + fn spurious_exit_finds_correct_failure() { + let mut runner = TestRunner::new(Config { + fork: true, + test_name: Some(concat!( + module_path!(), + "::spurious_exit_finds_correct_failure" + )), + ..Config::default() + }); + + let failure = runner + .run(&(0u32..1000), |v| { + if v >= 500 { + ::std::process::exit(0); + } + Ok(()) + }) + .err() + .unwrap(); + + match failure { + TestError::Fail(_, value) => assert_eq!(500, value), + failure => panic!("Unexpected failure: {:?}", failure), + } + } + + #[cfg(feature = "timeout")] + #[test] + fn long_sleep_timeout_finds_correct_failure() { + let mut runner = TestRunner::new(Config { + fork: true, + timeout: 500, + test_name: Some(concat!( + module_path!(), + "::long_sleep_timeout_finds_correct_failure" + )), + ..Config::default() + }); + + let failure = runner + .run(&(0u32..1000), |v| { + if v >= 500 { + ::std::thread::sleep(::std::time::Duration::from_millis( + 10_000, + )); + } + Ok(()) + }) + .err() + .unwrap(); + + match failure { + TestError::Fail(_, value) => assert_eq!(500, value), + failure => panic!("Unexpected failure: {:?}", failure), + } + } + + #[cfg(feature = "timeout")] + #[test] + fn mid_sleep_timeout_finds_correct_failure() { + let mut runner = TestRunner::new(Config { + fork: true, + timeout: 500, + test_name: Some(concat!( + module_path!(), + "::mid_sleep_timeout_finds_correct_failure" + )), + ..Config::default() + }); + + let failure = runner + .run(&(0u32..1000), |v| { + if v >= 500 { + // Sleep a little longer than the timeout. This means that + // sometimes the test case itself will return before the parent + // process has noticed the child is timing out, so it's up to + // the child to mark it as a failure. + ::std::thread::sleep(::std::time::Duration::from_millis( + 600, + )); + } else { + // Sleep a bit so that the parent and child timing don't stay + // in sync. + ::std::thread::sleep(::std::time::Duration::from_millis( + 100, + )) + } + Ok(()) + }) + .err() + .unwrap(); + + match failure { + TestError::Fail(_, value) => assert_eq!(500, value), + failure => panic!("Unexpected failure: {:?}", failure), + } + } + + #[cfg(feature = "std")] + #[test] + fn duplicate_tests_not_run_with_basic_result_cache() { + use std::cell::{Cell, RefCell}; + use std::collections::HashSet; + use std::rc::Rc; + + for _ in 0..256 { + let mut runner = TestRunner::new(Config { + failure_persistence: None, + result_cache: + crate::test_runner::result_cache::basic_result_cache, + ..Config::default() + }); + let pass = Rc::new(Cell::new(true)); + let seen = Rc::new(RefCell::new(HashSet::new())); + let result = + runner.run(&(0u32..65536u32).prop_map(|v| v % 10), |val| { + if !seen.borrow_mut().insert(val) { + println!("Value {} seen more than once", val); + pass.set(false); + } + + prop_assert!(val <= 5); + Ok(()) + }); + + assert!(pass.get()); + if let Err(TestError::Fail(_, val)) = result { + assert_eq!(6, val); + } else { + panic!("Incorrect result: {:?}", result); + } + } + } +} + +#[cfg(all(feature = "fork", feature = "timeout", test))] +mod timeout_tests { + use core::u32; + use std::thread; + use std::time::Duration; + + use super::*; + + rusty_fork_test! { + #![rusty_fork(timeout_ms = 4_000)] + + #[test] + fn max_shrink_iters_works() { + test_shrink_bail(Config { + max_shrink_iters: 5, + .. Config::default() + }); + } + + #[test] + fn max_shrink_time_works() { + test_shrink_bail(Config { + max_shrink_time: 1000, + .. Config::default() + }); + } + + #[test] + fn max_shrink_iters_works_with_forking() { + test_shrink_bail(Config { + fork: true, + test_name: Some( + concat!(module_path!(), + "::max_shrink_iters_works_with_forking")), + max_shrink_time: 1000, + .. Config::default() + }); + } + + #[test] + fn detects_child_failure_to_start() { + let mut runner = TestRunner::new(Config { + timeout: 100, + test_name: Some( + concat!(module_path!(), + "::detects_child_failure_to_start")), + .. Config::default() + }); + let result = runner.run(&Just(()).prop_map(|()| { + thread::sleep(Duration::from_millis(200)) + }), Ok); + + if let Err(TestError::Abort(_)) = result { + // OK + } else { + panic!("Unexpected result: {:?}", result); + } + } + } + + fn test_shrink_bail(config: Config) { + let mut runner = TestRunner::new(config); + let result = runner.run(&crate::num::u64::ANY, |v| { + thread::sleep(Duration::from_millis(250)); + prop_assert!(v <= u32::MAX as u64); + Ok(()) + }); + + if let Err(TestError::Fail(_, value)) = result { + // Ensure the final value was in fact a failing case. + assert!(value > u32::MAX as u64); + } else { + panic!("Unexpected result: {:?}", result); + } + } +} -- cgit v1.2.3