| //- |
| // 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::sync::RwLock; |
| use std::vec::Vec; |
| use std::string::{String, ToString}; |
| |
| use crate::test_runner::failure_persistence::{PersistedSeed, FailurePersistence}; |
| use self::FileFailurePersistence::*; |
| |
| /// 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() |
| ); |
| } |
| } |