blob: 647f47843d67d789e88e420583c026784d90a0ea [file] [log] [blame]
//-
// 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()
);
}
}