diff options
Diffstat (limited to 'vendor/proptest/src/test_runner/failure_persistence/file.rs')
-rw-r--r-- | vendor/proptest/src/test_runner/failure_persistence/file.rs | 536 |
1 files changed, 536 insertions, 0 deletions
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 <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 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<PersistedSeed> { + 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<Vec<PersistedSeed>> = 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::<u8>::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<dyn FailurePersistence> { + Box::new(*self) + } + + fn eq(&self, other: &dyn FailurePersistence) -> bool { + other + .as_any() + .downcast_ref::<Self>() + .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<Cow<'a, Path>> { + absolutize_source_file_with_cwd(env::current_dir, source) +} + +fn absolutize_source_file_with_cwd<'a>( + getcwd: impl FnOnce() -> io::Result<PathBuf>, + source: &'a Path, +) -> Option<Cow<'a, Path>> { + 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<PersistedSeed> { + // Remove anything after and including '#': + if let Some(comment_start) = line.find('#') { + line.truncate(comment_start); + } + + if line.len() > 0 { + let ret = line.parse::<PersistedSeed>().ok(); + if !ret.is_some() { + eprintln!( + "proptest: {}:{}: unparsable line, ignoring", + path.display(), + lineno + 1 + ); + } + return ret; + } + + None +} + +fn write_seed_line( + buf: &mut Vec<u8>, + 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<u8>) -> 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<PathBuf> { + 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::<PathBuf>(); + + // 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() + ); + } +} |