1//-
2// Copyright 2017, 2018, 2019 The proptest developers
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
910use core::any::Any;
11use core::fmt::Debug;
12use std::borrow::{Cow, ToOwned};
13use std::boxed::Box;
14use std::env;
15use std::fs;
16use std::io::{self, BufRead, Write};
17use std::path::{Path, PathBuf};
18use std::string::{String, ToString};
19use std::sync::RwLock;
20use std::vec::Vec;
2122use self::FileFailurePersistence::*;
23use crate::test_runner::failure_persistence::{
24 FailurePersistence, PersistedSeed,
25};
2627/// Describes how failing test cases are persisted.
28///
29/// Note that file names in this enum are `&str` rather than `&Path` since
30/// constant functions are not yet in Rust stable as of 2017-12-16.
31///
32/// In all cases, if a derived path references a directory which does not yet
33/// exist, proptest will attempt to create all necessary parent directories.
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub enum FileFailurePersistence {
36/// Completely disables persistence of failing test cases.
37 ///
38 /// This is semantically equivalent to `Direct("/dev/null")` on Unix and
39 /// `Direct("NUL")` on Windows (though it is internally handled by simply
40 /// not doing any I/O).
41Off,
42/// The path of the source file under test is traversed up the directory tree
43 /// until a directory containing a file named `lib.rs` or `main.rs` is found.
44 /// A sibling to that directory with the name given by the string in this
45 /// configuration is created, and a file with the same name and path relative
46 /// to the source directory, but with the extension changed to `.txt`, is used.
47 ///
48 /// For example, given a source path of
49 /// `/home/jsmith/code/project/src/foo/bar.rs` and a configuration of
50 /// `SourceParallel("proptest-regressions")` (the default), assuming the
51 /// `src` directory has a `lib.rs` or `main.rs`, the resulting file would
52 /// be `/home/jsmith/code/project/proptest-regressions/foo/bar.txt`.
53 ///
54 /// If no `lib.rs` or `main.rs` can be found, a warning is printed and this
55 /// behaves like `WithSource`.
56 ///
57 /// If no source file has been configured, a warning is printed and this
58 /// behaves like `Off`.
59SourceParallel(&'static str),
60/// Failures are persisted in a file with the same path as the source file
61 /// under test, but the extension is changed to the string given in this
62 /// configuration.
63 ///
64 /// For example, given a source path of
65 /// `/home/jsmith/code/project/src/foo/bar.rs` and a configuration of
66 /// `WithSource("regressions")`, the resulting path would be
67 /// `/home/jsmith/code/project/src/foo/bar.regressions`.
68WithSource(&'static str),
69/// The string given in this option is directly used as a file path without
70 /// any further processing.
71Direct(&'static str),
72#[doc(hidden)]
73 #[allow(missing_docs)]
74_NonExhaustive,
75}
7677impl Default for FileFailurePersistence {
78fn default() -> Self {
79 SourceParallel("proptest-regressions")
80 }
81}
8283impl FailurePersistence for FileFailurePersistence {
84fn load_persisted_failures2(
85&self,
86 source_file: Option<&'static str>,
87 ) -> Vec<PersistedSeed> {
88let p = self.resolve(
89 source_file
90 .and_then(|s| absolutize_source_file(Path::new(s)))
91 .as_ref()
92 .map(|cow| &**cow),
93 );
9495let path: Option<&PathBuf> = p.as_ref();
96let result: io::Result<Vec<PersistedSeed>> = path.map_or_else(
97 || Ok(vec![]),
98 |path| {
99// .ok() instead of .unwrap() so we don't propagate panics here
100let _lock = PERSISTENCE_LOCK.read().ok();
101 io::BufReader::new(fs::File::open(path)?)
102 .lines()
103 .enumerate()
104 .filter_map(|(lineno, line)| match line {
105Err(err) => Some(Err(err)),
106Ok(line) => parse_seed_line(line, path, lineno).map(Ok),
107 })
108 .collect()
109 },
110 );
111112unwrap_or!(result, err => {
113if io::ErrorKind::NotFound != err.kind() {
114eprintln!(
115"proptest: failed to open {}: {}",
116&path.map(|x| &**x)
117 .unwrap_or_else(|| Path::new("??"))
118 .display(),
119 err
120 );
121 }
122vec![]
123 })
124 }
125126fn save_persisted_failure2(
127&mut self,
128 source_file: Option<&'static str>,
129 seed: PersistedSeed,
130 shrunken_value: &dyn Debug,
131 ) {
132let path = self.resolve(source_file.map(Path::new));
133if let Some(path) = path {
134// .ok() instead of .unwrap() so we don't propagate panics here
135let _lock = PERSISTENCE_LOCK.write().ok();
136let is_new = !path.is_file();
137138let mut to_write = Vec::<u8>::new();
139if is_new {
140 write_header(&mut to_write)
141 .expect("proptest: couldn't write header.");
142 }
143144 write_seed_line(&mut to_write, &seed, shrunken_value)
145 .expect("proptest: couldn't write seed line.");
146147if let Err(e) = write_seed_data_to_file(&path, &to_write) {
148eprintln!(
149"proptest: failed to append to {}: {}",
150 path.display(),
151 e
152 );
153 } else {
154eprintln!(
155"proptest: Saving this and future failures in {}\n\
156 proptest: If this test was run on a CI system, you may \
157 wish to add the following line to your copy of the file.{}\n\
158 {}",
159 path.display(),
160if is_new { " (You may need to create it.)" } else { "" },
161 seed);
162 }
163 }
164 }
165166fn box_clone(&self) -> Box<dyn FailurePersistence> {
167 Box::new(*self)
168 }
169170fn eq(&self, other: &dyn FailurePersistence) -> bool {
171 other
172 .as_any()
173 .downcast_ref::<Self>()
174 .map_or(false, |x| x == self)
175 }
176177fn as_any(&self) -> &dyn Any {
178self
179}
180}
181182/// Ensure that the source file to use for resolving the location of the persisted
183/// failing cases file is absolute.
184///
185/// The source location can only be used if it is absolute. If `source` is
186/// not an absolute path, an attempt will be made to determine the absolute
187/// path based on the current working directory and its parents. If no
188/// absolute path can be determined, a warning will be printed and proptest
189/// will continue as if this function had never been called.
190///
191/// See [`FileFailurePersistence`](enum.FileFailurePersistence.html) for details on
192/// how this value is used once it is made absolute.
193///
194/// This is normally called automatically by the `proptest!` macro, which
195/// passes `file!()`.
196///
197fn absolutize_source_file<'a>(source: &'a Path) -> Option<Cow<'a, Path>> {
198 absolutize_source_file_with_cwd(env::current_dir, source)
199}
200201fn absolutize_source_file_with_cwd<'a>(
202 getcwd: impl FnOnce() -> io::Result<PathBuf>,
203 source: &'a Path,
204) -> Option<Cow<'a, Path>> {
205if source.is_absolute() {
206// On Unix, `file!()` is absolute. In these cases, we can use
207 // that path directly.
208Some(Cow::Borrowed(source))
209 } else {
210// On Windows, `file!()` is relative to the crate root, but the
211 // test is not generally run with the crate root as the working
212 // directory, so the path is not directly usable. However, the
213 // working directory is almost always a subdirectory of the crate
214 // root, so pop directories off until pushing the source onto the
215 // directory results in a path that refers to an existing file.
216 // Once we find such a path, we can use that.
217 //
218 // If we can't figure out an absolute path, print a warning and act
219 // as if no source had been given.
220match getcwd() {
221Ok(mut cwd) => loop {
222let joined = cwd.join(source);
223if joined.is_file() {
224break Some(Cow::Owned(joined));
225 }
226227if !cwd.pop() {
228eprintln!(
229"proptest: Failed to find absolute path of \
230 source file '{:?}'. Ensure the test is \
231 being run from somewhere within the crate \
232 directory hierarchy.",
233 source
234 );
235break None;
236 }
237 },
238239Err(e) => {
240eprintln!(
241"proptest: Failed to determine current \
242 directory, so the relative source path \
243 '{:?}' cannot be resolved: {}",
244 source, e
245 );
246None
247}
248 }
249 }
250}
251252fn parse_seed_line(
253mut line: String,
254 path: &Path,
255 lineno: usize,
256) -> Option<PersistedSeed> {
257// Remove anything after and including '#':
258if let Some(comment_start) = line.find('#') {
259 line.truncate(comment_start);
260 }
261262if line.len() > 0 {
263let ret = line.parse::<PersistedSeed>().ok();
264if !ret.is_some() {
265eprintln!(
266"proptest: {}:{}: unparsable line, ignoring",
267 path.display(),
268 lineno + 1
269);
270 }
271return ret;
272 }
273274None
275}
276277fn write_seed_line(
278 buf: &mut Vec<u8>,
279 seed: &PersistedSeed,
280 shrunken_value: &dyn Debug,
281) -> io::Result<()> {
282// Write the seed itself
283write!(buf, "{}", seed.to_string())?;
284285// Write out comment:
286let debug_start = buf.len();
287write!(buf, " # shrinks to {:?}", shrunken_value)?;
288289// Ensure there are no newlines in the debug output
290for byte in &mut buf[debug_start..] {
291if b'\n' == *byte || b'\r' == *byte {
292*byte = b' ';
293 }
294 }
295296 buf.push(b'\n');
297298Ok(())
299}
300301fn write_header(buf: &mut Vec<u8>) -> io::Result<()> {
302writeln!(
303 buf,
304"\
305# Seeds for failure cases proptest has generated in the past. It is
306# automatically read and these particular cases re-run before any
307# novel cases are generated.
308#
309# It is recommended to check this file in to source control so that
310# everyone who runs the test benefits from these saved cases."
311)
312}
313314fn write_seed_data_to_file(dst: &Path, data: &[u8]) -> io::Result<()> {
315if let Some(parent) = dst.parent() {
316 fs::create_dir_all(parent)?;
317 }
318319let mut options = fs::OpenOptions::new();
320 options.append(true).create(true);
321let mut out = options.open(dst)?;
322 out.write_all(data)?;
323324Ok(())
325}
326327impl FileFailurePersistence {
328/// Given the nominal source path, determine the location of the failure
329 /// persistence file, if any.
330pub(super) fn resolve(&self, source: Option<&Path>) -> Option<PathBuf> {
331let source = source.and_then(absolutize_source_file);
332333match *self {
334 Off => None,
335336 SourceParallel(sibling) => match source {
337Some(source_path) => {
338let mut dir = Cow::into_owned(source_path.clone());
339let mut found = false;
340while dir.pop() {
341if dir.join("lib.rs").is_file()
342 || dir.join("main.rs").is_file()
343 {
344 found = true;
345break;
346 }
347 }
348349if !found {
350eprintln!(
351"proptest: FileFailurePersistence::SourceParallel set, \
352 but failed to find lib.rs or main.rs"
353);
354 WithSource(sibling).resolve(Some(&*source_path))
355 } else {
356let suffix = source_path
357 .strip_prefix(&dir)
358 .expect("parent of source is not a prefix of it?")
359 .to_owned();
360let mut result = dir;
361// If we've somehow reached the root, or someone gave
362 // us a relative path that we've exhausted, just accept
363 // creating a subdirectory instead.
364let _ = result.pop();
365 result.push(sibling);
366 result.push(&suffix);
367 result.set_extension("txt");
368Some(result)
369 }
370 }
371None => {
372eprintln!(
373"proptest: FileFailurePersistence::SourceParallel set, \
374 but no source file known"
375);
376None
377}
378 },
379380 WithSource(extension) => match source {
381Some(source_path) => {
382let mut result = Cow::into_owned(source_path);
383 result.set_extension(extension);
384Some(result)
385 }
386387None => {
388eprintln!(
389"proptest: FileFailurePersistence::WithSource set, \
390 but no source file known"
391);
392None
393}
394 },
395396 Direct(path) => Some(Path::new(path).to_owned()),
397398 _NonExhaustive => {
399panic!("FailurePersistence set to _NonExhaustive")
400 }
401 }
402 }
403}
404405lazy_static! {
406/// Used to guard access to the persistence file(s) so that a single
407 /// process will not step on its own toes.
408 ///
409 /// We don't have much protecting us should two separate process try to
410 /// write to the same file at once (depending on how atomic append mode is
411 /// on the OS), but this should be extremely rare.
412static ref PERSISTENCE_LOCK: RwLock<()> = RwLock::new(());
413}
414415#[cfg(test)]
416mod tests {
417use super::*;
418419struct TestPaths {
420 crate_root: &'static Path,
421 src_file: PathBuf,
422 subdir_file: PathBuf,
423 misplaced_file: PathBuf,
424 }
425426lazy_static! {
427static ref TEST_PATHS: TestPaths = {
428let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
429let lib_root = crate_root.join("src");
430let src_subdir = lib_root.join("strategy");
431let src_file = lib_root.join("foo.rs");
432let subdir_file = src_subdir.join("foo.rs");
433let misplaced_file = crate_root.join("foo.rs");
434 TestPaths {
435 crate_root,
436 src_file,
437 subdir_file,
438 misplaced_file,
439 }
440 };
441 }
442443#[test]
444fn persistence_file_location_resolved_correctly() {
445// If off, there is never a file
446assert_eq!(None, Off.resolve(None));
447assert_eq!(None, Off.resolve(Some(&TEST_PATHS.subdir_file)));
448449// For direct, we don't care about the source file, and instead always
450 // use whatever is in the config.
451assert_eq!(
452Some(Path::new("bar.txt").to_owned()),
453 Direct("bar.txt").resolve(None)
454 );
455assert_eq!(
456Some(Path::new("bar.txt").to_owned()),
457 Direct("bar.txt").resolve(Some(&TEST_PATHS.subdir_file))
458 );
459460// For WithSource, only the extension changes, but we get nothing if no
461 // source file was configured.
462 // Accounting for the way absolute paths work on Windows would be more
463 // complex, so for now don't test that case.
464#[cfg(unix)]
465fn absolute_path_case() {
466assert_eq!(
467Some(Path::new("/foo/bar.ext").to_owned()),
468 WithSource("ext").resolve(Some(Path::new("/foo/bar.rs")))
469 );
470 }
471#[cfg(not(unix))]
472fn absolute_path_case() {}
473 absolute_path_case();
474assert_eq!(None, WithSource("ext").resolve(None));
475476// For SourceParallel, we make a sibling directory tree and change the
477 // extensions to .txt ...
478assert_eq!(
479Some(TEST_PATHS.crate_root.join("sib").join("foo.txt")),
480 SourceParallel("sib").resolve(Some(&TEST_PATHS.src_file))
481 );
482assert_eq!(
483Some(
484 TEST_PATHS
485 .crate_root
486 .join("sib")
487 .join("strategy")
488 .join("foo.txt")
489 ),
490 SourceParallel("sib").resolve(Some(&TEST_PATHS.subdir_file))
491 );
492// ... but if we can't find lib.rs / main.rs, give up and set the
493 // extension instead ...
494assert_eq!(
495Some(TEST_PATHS.crate_root.join("foo.sib")),
496 SourceParallel("sib").resolve(Some(&TEST_PATHS.misplaced_file))
497 );
498// ... and if no source is configured, we do nothing
499assert_eq!(None, SourceParallel("ext").resolve(None));
500 }
501502#[test]
503fn relative_source_files_absolutified() {
504const TEST_RUNNER_PATH: &[&str] = &["src", "test_runner", "mod.rs"];
505lazy_static! {
506static ref TEST_RUNNER_RELATIVE: PathBuf =
507 TEST_RUNNER_PATH.iter().collect();
508 }
509const CARGO_DIR: &str = env!("CARGO_MANIFEST_DIR");
510511let expected = ::std::iter::once(CARGO_DIR)
512 .chain(TEST_RUNNER_PATH.iter().map(|s| *s))
513 .collect::<PathBuf>();
514515// Running from crate root
516assert_eq!(
517&*expected,
518 absolutize_source_file_with_cwd(
519 || Ok(Path::new(CARGO_DIR).to_owned()),
520&TEST_RUNNER_RELATIVE
521 )
522 .unwrap()
523 );
524525// Running from test subdirectory
526assert_eq!(
527&*expected,
528 absolutize_source_file_with_cwd(
529 || Ok(Path::new(CARGO_DIR).join("target")),
530&TEST_RUNNER_RELATIVE
531 )
532 .unwrap()
533 );
534 }
535}