proptest/test_runner/
replay.rs

1//-
2// Copyright 2018 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.
9
10#![allow(dead_code)]
11
12use std::fs;
13use std::io::{self, BufRead, Read, Seek, Write};
14use std::path::Path;
15use std::string::String;
16use std::vec::Vec;
17
18use crate::test_runner::{Seed, TestCaseError, TestCaseResult};
19
20const SENTINEL: &'static str = "proptest-forkfile";
21
22/// A "replay" of a `TestRunner` invocation.
23///
24/// The replay mechanism is used to support forking. When a child process
25/// exits, the parent can read the replay to reproduce the state the child had;
26/// similarly, if a child crashes, a new one can be started and given a replay
27/// which steps it one complication past the input that caused the crash.
28///
29/// The replay system is tightly coupled to the `TestRunner` itself. It does
30/// not carry enough information to be used in different builds of the same
31/// application, or even two different runs of the test process since changes
32/// to the persistence file will perturb the replay.
33///
34/// `Replay` has a special string format for being stored in files. It starts
35/// with a line just containing the text in `SENTINEL`, then 16 lines
36/// containing the values of `seed`, then an unterminated line consisting of
37/// `+`, `-`, and `!` characters to indicate test case passes/failures/rejects,
38/// `.` to indicate termination of the test run, or ` ` as a dummy "I'm alive"
39/// signal. This format makes it easy for the child process to blindly append
40/// to the file without having to worry about the possibility of appends being
41/// non-atomic.
42#[derive(Clone, Debug)]
43pub(crate) struct Replay {
44    /// The seed of the RNG used to start running the test cases.
45    pub(crate) seed: Seed,
46    /// A log of whether certain test cases passed or failed. The runner will
47    /// assume the same results occur without actually running the test cases.
48    pub(crate) steps: Vec<TestCaseResult>,
49}
50
51impl Replay {
52    /// If `other` is longer than `self`, add the extra elements to `self`.
53    pub fn merge(&mut self, other: &Replay) {
54        if other.steps.len() > self.steps.len() {
55            let sl = self.steps.len();
56            self.steps.extend_from_slice(&other.steps[sl..]);
57        }
58    }
59}
60
61/// Result of loading a replay file.
62#[derive(Clone, Debug)]
63pub(crate) enum ReplayFileStatus {
64    /// The file is valid and represents a currently-in-progress test.
65    InProgress(Replay),
66    /// The file is valid, but indicates that all testing has completed.
67    Terminated(Replay),
68    /// The file is not parsable.
69    Corrupt,
70}
71
72/// Open the file in the usual read+append+create mode.
73pub(crate) fn open_file(path: impl AsRef<Path>) -> io::Result<fs::File> {
74    fs::OpenOptions::new()
75        .read(true)
76        .append(true)
77        .create(true)
78        .truncate(false)
79        .open(path)
80}
81
82fn step_to_char(step: &TestCaseResult) -> char {
83    match *step {
84        Ok(_) => '+',
85        Err(TestCaseError::Reject(_)) => '!',
86        Err(TestCaseError::Fail(_)) => '-',
87    }
88}
89
90/// Append the given step to the given output.
91pub(crate) fn append(
92    mut file: impl Write,
93    step: &TestCaseResult,
94) -> io::Result<()> {
95    write!(file, "{}", step_to_char(step))
96}
97
98/// Append a no-op step to the given output.
99pub(crate) fn ping(mut file: impl Write) -> io::Result<()> {
100    write!(file, " ")
101}
102
103/// Append a termination mark to the given output.
104pub(crate) fn terminate(mut file: impl Write) -> io::Result<()> {
105    write!(file, ".")
106}
107
108impl Replay {
109    /// Write the full state of this `Replay` to the given output.
110    pub fn init_file(&self, mut file: impl Write) -> io::Result<()> {
111        writeln!(file, "{}", SENTINEL)?;
112        writeln!(file, "{}", self.seed.to_persistence())?;
113
114        let mut step_data = Vec::<u8>::new();
115        for step in &self.steps {
116            step_data.push(step_to_char(step) as u8);
117        }
118
119        file.write_all(&step_data)?;
120
121        Ok(())
122    }
123
124    /// Mark the replay as complete in the file.
125    pub fn complete(mut file: impl Write) -> io::Result<()> {
126        write!(file, ".")
127    }
128
129    /// Parse a `Replay` out of the given file.
130    ///
131    /// The reader is implicitly seeked to the beginning before reading.
132    pub fn parse_from(
133        mut file: impl Read + Seek,
134    ) -> io::Result<ReplayFileStatus> {
135        file.seek(io::SeekFrom::Start(0))?;
136
137        let mut reader = io::BufReader::new(&mut file);
138        let mut line = String::new();
139
140        // Ensure it starts with the sentinel. We do this since we rely on a
141        // named temporary file which could be in a location where another
142        // actor could replace it with, eg, a symlink to a location they don't
143        // control but we do. By rejecting a read from a file missing the
144        // sentinel, and not doing any writes if we can't read the file, we
145        // won't risk overwriting another file since the prospective attacker
146        // would need to be able to change the file to start with the sentinel
147        // themselves.
148        //
149        // There are still some possible symlink attacks that can work by
150        // tricking us into reading, but those are non-destructive things like
151        // interfering with a FIFO or Unix socket.
152        reader.read_line(&mut line)?;
153        if SENTINEL != line.trim() {
154            return Ok(ReplayFileStatus::Corrupt);
155        }
156
157        line.clear();
158        reader.read_line(&mut line)?;
159        let seed = match Seed::from_persistence(&line) {
160            Some(seed) => seed,
161            None => return Ok(ReplayFileStatus::Corrupt),
162        };
163
164        line.clear();
165        reader.read_line(&mut line)?;
166
167        let mut steps = Vec::new();
168        for ch in line.chars() {
169            match ch {
170                '+' => steps.push(Ok(())),
171                '-' => steps
172                    .push(Err(TestCaseError::fail("failed in other process"))),
173                '!' => steps.push(Err(TestCaseError::reject(
174                    "rejected in other process",
175                ))),
176                '.' => {
177                    return Ok(ReplayFileStatus::Terminated(Replay {
178                        seed,
179                        steps,
180                    }))
181                }
182                ' ' => (),
183                _ => return Ok(ReplayFileStatus::Corrupt),
184            }
185        }
186
187        Ok(ReplayFileStatus::InProgress(Replay { seed, steps }))
188    }
189}