| //- |
| // 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::{TestCaseError, TestCaseResult, Seed}; |
| |
| 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 })) |
| } |
| } |