summaryrefslogtreecommitdiffstats
path: root/vendor/proptest/src/test_runner/replay.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/proptest/src/test_runner/replay.rs')
-rw-r--r--vendor/proptest/src/test_runner/replay.rs189
1 files changed, 189 insertions, 0 deletions
diff --git a/vendor/proptest/src/test_runner/replay.rs b/vendor/proptest/src/test_runner/replay.rs
new file mode 100644
index 000000000..4365d5538
--- /dev/null
+++ b/vendor/proptest/src/test_runner/replay.rs
@@ -0,0 +1,189 @@
+//-
+// Copyright 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.
+
+#![allow(dead_code)]
+
+use std::fs;
+use std::io::{self, BufRead, Read, Seek, Write};
+use std::path::Path;
+use std::string::String;
+use std::vec::Vec;
+
+use crate::test_runner::{Seed, TestCaseError, TestCaseResult};
+
+const SENTINEL: &'static str = "proptest-forkfile";
+
+/// A "replay" of a `TestRunner` invocation.
+///
+/// The replay mechanism is used to support forking. When a child process
+/// exits, the parent can read the replay to reproduce the state the child had;
+/// similarly, if a child crashes, a new one can be started and given a replay
+/// which steps it one complication past the input that caused the crash.
+///
+/// The replay system is tightly coupled to the `TestRunner` itself. It does
+/// not carry enough information to be used in different builds of the same
+/// application, or even two different runs of the test process since changes
+/// to the persistence file will perturb the replay.
+///
+/// `Replay` has a special string format for being stored in files. It starts
+/// with a line just containing the text in `SENTINEL`, then 16 lines
+/// containing the values of `seed`, then an unterminated line consisting of
+/// `+`, `-`, and `!` characters to indicate test case passes/failures/rejects,
+/// `.` to indicate termination of the test run, or ` ` as a dummy "I'm alive"
+/// signal. This format makes it easy for the child process to blindly append
+/// to the file without having to worry about the possibility of appends being
+/// non-atomic.
+#[derive(Clone, Debug)]
+pub(crate) struct Replay {
+ /// The seed of the RNG used to start running the test cases.
+ pub(crate) seed: Seed,
+ /// A log of whether certain test cases passed or failed. The runner will
+ /// assume the same results occur without actually running the test cases.
+ pub(crate) steps: Vec<TestCaseResult>,
+}
+
+impl Replay {
+ /// If `other` is longer than `self`, add the extra elements to `self`.
+ pub fn merge(&mut self, other: &Replay) {
+ if other.steps.len() > self.steps.len() {
+ let sl = self.steps.len();
+ self.steps.extend_from_slice(&other.steps[sl..]);
+ }
+ }
+}
+
+/// Result of loading a replay file.
+#[derive(Clone, Debug)]
+pub(crate) enum ReplayFileStatus {
+ /// The file is valid and represents a currently-in-progress test.
+ InProgress(Replay),
+ /// The file is valid, but indicates that all testing has completed.
+ Terminated(Replay),
+ /// The file is not parsable.
+ Corrupt,
+}
+
+/// Open the file in the usual read+append+create mode.
+pub(crate) fn open_file(path: impl AsRef<Path>) -> io::Result<fs::File> {
+ fs::OpenOptions::new()
+ .read(true)
+ .append(true)
+ .create(true)
+ .truncate(false)
+ .open(path)
+}
+
+fn step_to_char(step: &TestCaseResult) -> char {
+ match *step {
+ Ok(_) => '+',
+ Err(TestCaseError::Reject(_)) => '!',
+ Err(TestCaseError::Fail(_)) => '-',
+ }
+}
+
+/// Append the given step to the given output.
+pub(crate) fn append(
+ mut file: impl Write,
+ step: &TestCaseResult,
+) -> io::Result<()> {
+ write!(file, "{}", step_to_char(step))
+}
+
+/// Append a no-op step to the given output.
+pub(crate) fn ping(mut file: impl Write) -> io::Result<()> {
+ write!(file, " ")
+}
+
+/// Append a termination mark to the given output.
+pub(crate) fn terminate(mut file: impl Write) -> io::Result<()> {
+ write!(file, ".")
+}
+
+impl Replay {
+ /// Write the full state of this `Replay` to the given output.
+ pub fn init_file(&self, mut file: impl Write) -> io::Result<()> {
+ writeln!(file, "{}", SENTINEL)?;
+ writeln!(file, "{}", self.seed.to_persistence())?;
+
+ let mut step_data = Vec::<u8>::new();
+ for step in &self.steps {
+ step_data.push(step_to_char(step) as u8);
+ }
+
+ file.write_all(&step_data)?;
+
+ Ok(())
+ }
+
+ /// Mark the replay as complete in the file.
+ pub fn complete(mut file: impl Write) -> io::Result<()> {
+ write!(file, ".")
+ }
+
+ /// Parse a `Replay` out of the given file.
+ ///
+ /// The reader is implicitly seeked to the beginning before reading.
+ pub fn parse_from(
+ mut file: impl Read + Seek,
+ ) -> io::Result<ReplayFileStatus> {
+ file.seek(io::SeekFrom::Start(0))?;
+
+ let mut reader = io::BufReader::new(&mut file);
+ let mut line = String::new();
+
+ // Ensure it starts with the sentinel. We do this since we rely on a
+ // named temporary file which could be in a location where another
+ // actor could replace it with, eg, a symlink to a location they don't
+ // control but we do. By rejecting a read from a file missing the
+ // sentinel, and not doing any writes if we can't read the file, we
+ // won't risk overwriting another file since the prospective attacker
+ // would need to be able to change the file to start with the sentinel
+ // themselves.
+ //
+ // There are still some possible symlink attacks that can work by
+ // tricking us into reading, but those are non-destructive things like
+ // interfering with a FIFO or Unix socket.
+ reader.read_line(&mut line)?;
+ if SENTINEL != line.trim() {
+ return Ok(ReplayFileStatus::Corrupt);
+ }
+
+ line.clear();
+ reader.read_line(&mut line)?;
+ let seed = match Seed::from_persistence(&line) {
+ Some(seed) => seed,
+ None => return Ok(ReplayFileStatus::Corrupt),
+ };
+
+ line.clear();
+ reader.read_line(&mut line)?;
+
+ let mut steps = Vec::new();
+ for ch in line.chars() {
+ match ch {
+ '+' => steps.push(Ok(())),
+ '-' => steps
+ .push(Err(TestCaseError::fail("failed in other process"))),
+ '!' => steps.push(Err(TestCaseError::reject(
+ "rejected in other process",
+ ))),
+ '.' => {
+ return Ok(ReplayFileStatus::Terminated(Replay {
+ seed,
+ steps,
+ }))
+ }
+ ' ' => (),
+ _ => return Ok(ReplayFileStatus::Corrupt),
+ }
+ }
+
+ Ok(ReplayFileStatus::InProgress(Replay { seed, steps }))
+ }
+}