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}