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,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FileFailurePersistence {
Off,
SourceParallel(&'static str),
WithSource(&'static str),
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| {
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 {
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
}
}
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() {
Some(Cow::Borrowed(source))
} else {
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> {
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!(buf, "{}", seed.to_string())?;
let debug_start = buf.len();
write!(buf, " # shrinks to {:?}", shrunken_value)?;
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 {
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;
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! {
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() {
assert_eq!(None, Off.resolve(None));
assert_eq!(None, Off.resolve(Some(&TEST_PATHS.subdir_file)));
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))
);
#[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));
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))
);
assert_eq!(
Some(TEST_PATHS.crate_root.join("foo.sib")),
SourceParallel("sib").resolve(Some(&TEST_PATHS.misplaced_file))
);
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>();
assert_eq!(
&*expected,
absolutize_source_file_with_cwd(
|| Ok(Path::new(CARGO_DIR).to_owned()),
&TEST_RUNNER_RELATIVE
)
.unwrap()
);
assert_eq!(
&*expected,
absolutize_source_file_with_cwd(
|| Ok(Path::new(CARGO_DIR).join("target")),
&TEST_RUNNER_RELATIVE
)
.unwrap()
);
}
}