use core::any::Any;
use core::fmt::Debug;
use core::num::ParseIntError;
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 test_runner::failure_persistence::FailurePersistence;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FileFailurePersistence {
Off,
SourceParallel(&'static str),
WithSource(&'static str),
Direct(&'static str),
#[doc(hidden)]
#[allow(missing_docs)]
_NonExhaustive,
}
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, F>(
getcwd: F,
source: &'a Path,
) -> Option<Cow<'a, Path>>
where
F: FnOnce() -> io::Result<PathBuf>,
{
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
}
}
}
}
impl FailurePersistence for FileFailurePersistence {
fn load_persisted_failures(&self, source_file: Option<&'static str>) -> Vec<[u32; 4]> {
let p = self.resolve(
source_file
.and_then(|s| absolutize_source_file(Path::new(s)))
.as_ref()
.map(|cow| &**cow));
let path: Option<&PathBuf> = match p {
Some(ref inner_path) => Some(inner_path),
None => None,
};
let result: io::Result<Vec<[u32; 4]>> = path.map_or_else(
|| Ok(vec![]),
|path| {
let _lock = PERSISTENCE_LOCK.read().ok();
let mut ret = Vec::new();
let input = io::BufReader::new(fs::File::open(path)?);
for (lineno, line) in input.lines().enumerate() {
let mut line = line?;
if let Some(comment_start) = line.find('#') {
line.truncate(comment_start);
}
let parts = line.trim().split(' ').collect::<Vec<_>>();
if 5 == parts.len() && "xs" == parts[0] {
if let Ok(seed) = parse_seed(&*parts) {
ret.push(seed);
} else {
eprintln!(
"proptest: {}:{}: unparsable line, \
ignoring",
path.display(),
lineno + 1
);
}
} else if parts.len() > 1 {
eprintln!(
"proptest: {}:{}: unknown case type `{}` \
(corrupt file or newer proptest version?)",
&path.display(),
lineno + 1,
parts[0]
);
}
}
Ok(ret)
},
);
match result {
Ok(r) => r,
Err(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_failure(
&mut self,
source_file: Option<&'static str>,
seed: [u32; 4],
shrunken_value: &Debug,
) {
let path = self.resolve(source_file.map(|f| Path::new(f)));
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 {
writeln!(
to_write,
"\
# 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."
).expect("writeln! to vec failed");
}
let mut data_line = Vec::<u8>::new();
write!(
data_line,
"xs {} {} {} {} # shrinks to {:?}",
seed[0], seed[1], seed[2], seed[3], shrunken_value
).expect("write! to vec failed");
for byte in &mut data_line {
if b'\n' == *byte || b'\r' == *byte {
*byte = b' ';
}
}
to_write.extend(data_line);
to_write.push(b'\n');
fn do_write(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(())
}
if let Err(e) = do_write(&path, &to_write) {
eprintln!("proptest: failed to append to {}: {}", path.display(), e);
} else if is_new {
eprintln!(
"proptest: Saving this and future failures in {}",
path.display()
);
}
}
}
fn box_clone(&self) -> Box<FailurePersistence> {
Box::new(self.clone())
}
fn eq(&self, other: &FailurePersistence) -> bool {
other.as_any().downcast_ref::<Self>().map_or(false, |x| x == self)
}
fn as_any(&self) -> &Any { self }
}
use self::FileFailurePersistence::*;
impl Default for FileFailurePersistence {
fn default() -> Self {
SourceParallel("proptest-regressions")
}
}
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(());
}
fn parse_seed(parts: &[&str]) -> Result<[u32; 4], ParseIntError> {
let a = parts[1].parse()?;
let b = parts[2].parse()?;
let c = parts[3].parse()?;
let d = parts[4].parse()?;
Ok([a, b, c, d])
}
#[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()
);
}
}