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
use super::{results, suite};
use crate::errors::RuntError;
use std::{fs, path::PathBuf, time::Duration};
use tokio::{process::Command, time};
/// Configuration of a test to be executed.
pub struct Test {
/// Path of the test to be run.
pub path: PathBuf,
/// Command to be executed for the test.
pub cmd: String,
/// Directory to save/check the expect results for.
/// If set to `None`, defaults to the directory containing `Path`.
pub expect_dir: Option<PathBuf>,
/// Test suite with which this Test is associated.
/// The mapping from the test suite
pub test_suite: suite::Id,
/// Timeout for this test.
pub timeout: Duration,
}
impl Test {
/// Format the output of the test into an expect string.
/// An expect string is of the form:
/// <contents of STDOUT>
/// ---CODE---
/// <exit code>
/// ---STDERR---
/// <contents of STDERR>
pub fn format_expect_string(
status: i32,
stdout: &str,
stderr: &str,
) -> String {
let mut buf = String::new();
if !stdout.is_empty() {
buf.push_str(stdout);
}
if status != 0 {
buf.push_str("---CODE---\n");
buf.push_str(format!("{}", status).as_str());
buf.push('\n');
}
if !stderr.is_empty() {
buf.push_str("---STDERR---\n");
buf.push_str(stderr);
}
buf
}
fn get_base(&self) -> PathBuf {
self.expect_dir
.clone()
.map(|base| base.join(self.path.file_name().unwrap()))
.unwrap_or_else(|| self.path.clone())
.as_path()
.to_path_buf()
}
/// Path of the expect file.
pub fn expect_file(&self) -> PathBuf {
self.get_base().with_extension("expect")
}
/// Path of the skip file
pub fn skip_file(&self) -> PathBuf {
self.get_base().with_extension("skip")
}
/// Construct a command to run by replacing all occurances of `{}` with that
/// matching path.
fn construct_command(&self) -> Command {
let concrete_command =
self.cmd.replace("{}", self.path.to_str().unwrap());
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(concrete_command);
cmd.kill_on_drop(true);
cmd
}
/// Create a task to asynchronously execute this test. We use
/// std library fs::* and command::* so that there is a 1-to-1
/// correspondence between tokio threads and spawned processes.
/// This lets us control the number of parallel running processes.
pub async fn execute_test(self) -> Result<results::Test, RuntError> {
let skip_path = self.skip_file();
if skip_path.exists() {
return Ok(results::Test {
path: self.path,
expect_path: skip_path,
state: results::State::Skip,
saved: false,
test_suite: self.test_suite,
});
}
let expect_path = self.expect_file();
let mut cmd = self.construct_command();
match time::timeout(self.timeout, cmd.output()).await {
Err(_) => Ok(results::Test {
path: self.path,
expect_path,
state: results::State::Timeout,
saved: false,
test_suite: self.test_suite,
}),
Ok(res) => {
let out = res.map_err(|err| {
RuntError(format!(
"{}: {}",
self.path.to_str().unwrap(),
err
))
})?;
let status = out.status.code().unwrap_or(-1);
let stdout = String::from_utf8(out.stdout)?;
let stderr = String::from_utf8(out.stderr)?;
// Generate expected string
let expect_string =
Self::format_expect_string(status, &stdout, &stderr);
// Open expect file for comparison.
let state = fs::read_to_string(expect_path.clone())
.map(|contents| {
if contents == expect_string {
results::State::Correct
} else {
results::State::Mismatch(
expect_string.clone(),
contents,
)
}
})
.unwrap_or(results::State::Missing(expect_string));
Ok(results::Test {
path: self.path,
expect_path,
state,
saved: false,
test_suite: self.test_suite,
})
}
}
}
}