proptest 0.6.0

Hypothesis-like property-based testing and shrinking.
Documentation
//-
// Copyright 2017, 2018 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 std::num::ParseIntError;
use std::fmt;
use std::fs;
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
use std::sync::RwLock;

/// 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 FailurePersistence {
    /// 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,
}

use self::FailurePersistence::*;

impl Default for FailurePersistence {
    fn default() -> Self {
        SourceParallel("proptest-regressions")
    }
}

impl FailurePersistence {
    /// 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> {
        match *self {
            Off => None,

            SourceParallel(sibling) => match source {
                Some(source_path) => {
                    let mut dir = source_path.to_owned();
                    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: FailurePersistence::SourceParallel set, \
                             but failed to find lib.rs or main.rs");
                        WithSource(sibling).resolve(source)
                    } 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: FailurePersistence::SourceParallel set, \
                         but no source file known");
                    None
                },
            },

            WithSource(extension) => match source {
                Some(source_path) => {
                    let mut result = source_path.to_owned();
                    result.set_extension(extension);
                    Some(result)
                },

                None => {
                    eprintln!("proptest: FailurePersistence::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(());
}

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])
}

pub(super) fn load_persisted_failures(path: Option<&PathBuf>) -> Vec<[u32; 4]> {
    let result: io::Result<Vec<[u32; 4]>> =
        path.map_or_else(|| Ok(vec![]), |path| {
            // .ok() instead of .unwrap() so we don't propagate panics here
            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![]
        },
    }
}

pub(super) fn save_persisted_failure
    (path: Option<&PathBuf>, seed: [u32;4], value: &fmt::Debug)
{
    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 {
            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],
               value).expect("write! to vec failed");
        // Ensure there are no newlines in the debug output
        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());
        }
    }
}

#[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));
    }
}