diff options
Diffstat (limited to 'vendor/proptest/src/test_runner/runner.rs')
-rw-r--r-- | vendor/proptest/src/test_runner/runner.rs | 1538 |
1 files changed, 1538 insertions, 0 deletions
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 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<Reason, u32>; + +/// 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<AtomicUsize>, + + 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", &"<TestRng>") + .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<fs::File>, +} + +#[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<V, F, R>( + _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<Item = TestCaseResult>, +{ + 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<V, F, R>( + 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<Item = TestCaseResult>, +{ + 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::<String>().map(|b| (*b).into())) + .or_else(|what| what.downcast::<Box<str>>().map(|b| (*b).into())) + .unwrap_or_else(|_| "<unknown panic value>".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<S> = Result<(), TestError<<S as Strategy>::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<u8> { + 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<S: Strategy>( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult<S> { + 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<S: Strategy>( + &mut self, + _: &S, + _: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult<S> { + unreachable!() + } + + #[cfg(feature = "fork")] + fn run_in_fork<S: Strategy>( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult<S> { + 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<Option<tempfile::NamedTempFile>> = + 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<tempfile::NamedTempFile>) -> 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<S: Strategy>( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + ) -> TestRunResult<S> { + 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<S: Strategy>( + &mut self, + strategy: &S, + test: impl Fn(S::Value) -> TestCaseResult, + mut replay_from_fork: impl Iterator<Item = TestCaseResult>, + mut fork_output: ForkOutput, + ) -> TestRunResult<S> { + let old_rng = self.rng.clone(); + + let persisted_failure_seeds: Vec<PersistedSeed> = 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<S: Strategy>( + &mut self, + strategy: &S, + f: &impl Fn(S::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator<Item = TestCaseResult>, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> TestRunResult<S> { + 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<V: ValueTree>( + &mut self, + case: V, + test: impl Fn(V::Value) -> TestCaseResult, + ) -> Result<bool, TestError<V::Value>> { + let mut result_cache = self.new_cache(); + self.run_one_with_replay( + case, + test, + &mut iter::empty::<TestCaseResult>().fuse(), + &mut *result_cache, + &mut ForkOutput::empty(), + false, + ) + .map(|ok_type| match ok_type { + TestCaseOk::Reject => false, + _ => true, + }) + } + + fn run_one_with_replay<V: ValueTree>( + &mut self, + mut case: V, + test: impl Fn(V::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator<Item = TestCaseResult>, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> Result<TestCaseOk, TestError<V::Value>> { + 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<V: ValueTree>( + &mut self, + case: &mut V, + test: impl Fn(V::Value) -> TestCaseResult, + replay_from_fork: &mut impl Iterator<Item = TestCaseResult>, + result_cache: &mut dyn ResultCache, + fork_output: &mut ForkOutput, + is_from_persisted_seed: bool, + ) -> Option<Reason> { + #[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<u64> = 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<Reason>, + ) -> 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<T>(&mut self, whence: Reason) -> Result<(), TestError<T>> { + 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<dyn ResultCache> { + (self.config.result_cache)() + } +} + +#[cfg(feature = "fork")] +fn init_replay(rng: &mut TestRng) -> (Vec<TestCaseResult>, 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<TestCaseResult>, ForkOutput) { + (iter::empty(), ForkOutput::empty()) +} + +#[cfg(feature = "fork")] +fn await_child_without_timeout( + child: &mut rusty_fork::ChildWrapper, +) -> (Option<TestCaseError>, Option<u64>) { + 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<TestCaseError>, Option<u64>) { + 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<TestCaseError>, Option<u64>) { + 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); + } + } +} |