summaryrefslogtreecommitdiffstats
path: root/vendor/proptest/src/test_runner/replay.rs
blob: 4365d5538d0e384010f9250cd910bb7bf234939b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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 }))
    }
}