Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/cranelift/filetests/src/runner.rs
1691 views
1
//! Test runner.
2
//!
3
//! This module implements the `TestRunner` struct which manages executing tests as well as
4
//! scanning directories for tests.
5
6
use crate::concurrent::{ConcurrentRunner, Reply};
7
use crate::runone;
8
use std::error::Error;
9
use std::ffi::OsStr;
10
use std::fmt::{self, Display};
11
use std::path::{Path, PathBuf};
12
use std::time;
13
14
/// Timeout in seconds when we're not making progress.
15
const TIMEOUT_PANIC: usize = 60;
16
17
/// Timeout for reporting slow tests without panicking.
18
const TIMEOUT_SLOW: usize = 3;
19
20
struct QueueEntry {
21
path: PathBuf,
22
state: State,
23
}
24
25
#[derive(Debug)]
26
enum State {
27
New,
28
Queued,
29
Running,
30
Done(anyhow::Result<time::Duration>),
31
}
32
33
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
34
pub enum IsPass {
35
Pass,
36
NotPass,
37
}
38
39
impl QueueEntry {
40
pub fn path(&self) -> &Path {
41
self.path.as_path()
42
}
43
}
44
45
impl Display for QueueEntry {
46
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47
let p = self.path.to_string_lossy();
48
match self.state {
49
State::Done(Ok(dur)) => write!(f, "{}.{:03} {}", dur.as_secs(), dur.subsec_millis(), p),
50
State::Done(Err(ref e)) => write!(f, "FAIL {p}: {e:?}"),
51
_ => write!(f, "{p}"),
52
}
53
}
54
}
55
56
pub struct TestRunner {
57
verbose: bool,
58
59
// Should we print the timings out?
60
report_times: bool,
61
62
// Directories that have not yet been scanned.
63
dir_stack: Vec<PathBuf>,
64
65
// Filenames of tests to run.
66
tests: Vec<QueueEntry>,
67
68
// Pointer into `tests` where the `New` entries begin.
69
new_tests: usize,
70
71
// Number of contiguous reported tests at the front of `tests`.
72
reported_tests: usize,
73
74
// Number of errors seen so far.
75
errors: usize,
76
77
// Number of ticks received since we saw any progress.
78
ticks_since_progress: usize,
79
80
threads: Option<ConcurrentRunner>,
81
}
82
83
impl TestRunner {
84
/// Create a new blank TestRunner.
85
pub fn new(verbose: bool, report_times: bool) -> Self {
86
Self {
87
verbose,
88
report_times,
89
dir_stack: Vec::new(),
90
tests: Vec::new(),
91
new_tests: 0,
92
reported_tests: 0,
93
errors: 0,
94
ticks_since_progress: 0,
95
threads: None,
96
}
97
}
98
99
/// Add a directory path to be scanned later.
100
///
101
/// If `dir` turns out to be a regular file, it is silently ignored.
102
/// Otherwise, any problems reading the directory are reported.
103
pub fn push_dir<P: Into<PathBuf>>(&mut self, dir: P) {
104
self.dir_stack.push(dir.into());
105
}
106
107
/// Add a test to be executed later.
108
///
109
/// Any problems reading `file` as a test case file will be reported as a test failure.
110
pub fn push_test<P: Into<PathBuf>>(&mut self, file: P) {
111
self.tests.push(QueueEntry {
112
path: file.into(),
113
state: State::New,
114
});
115
}
116
117
/// Begin running tests concurrently.
118
pub fn start_threads(&mut self) {
119
assert!(self.threads.is_none());
120
self.threads = Some(ConcurrentRunner::new());
121
}
122
123
/// Scan any directories pushed so far.
124
/// Push any potential test cases found.
125
pub fn scan_dirs(&mut self, pass_status: IsPass) {
126
// This recursive search tries to minimize statting in a directory hierarchy containing
127
// mostly test cases.
128
//
129
// - Directory entries with a "clif" or "wat" extension are presumed to be test case files.
130
// - Directory entries with no extension are presumed to be subdirectories.
131
// - Anything else is ignored.
132
//
133
while let Some(dir) = self.dir_stack.pop() {
134
match dir.read_dir() {
135
Err(err) => {
136
// Fail silently if `dir` was actually a regular file.
137
// This lets us skip spurious extensionless files without statting everything
138
// needlessly.
139
if !dir.is_file() {
140
self.path_error(&dir, &err);
141
}
142
}
143
Ok(entries) => {
144
// Read all directory entries. Avoid statting.
145
for entry_result in entries {
146
match entry_result {
147
Err(err) => {
148
// Not sure why this would happen. `read_dir` succeeds, but there's
149
// a problem with an entry. I/O error during a getdirentries
150
// syscall seems to be the reason. The implementation in
151
// libstd/sys/unix/fs.rs seems to suggest that breaking now would
152
// be a good idea, or the iterator could keep returning the same
153
// error forever.
154
self.path_error(&dir, &err);
155
break;
156
}
157
Ok(entry) => {
158
let path = entry.path();
159
// Recognize directories and tests by extension.
160
// Yes, this means we ignore directories with '.' in their name.
161
match path.extension().and_then(OsStr::to_str) {
162
Some("clif" | "wat") => self.push_test(path),
163
Some(_) => {}
164
None => self.push_dir(path),
165
}
166
}
167
}
168
}
169
}
170
}
171
if pass_status == IsPass::Pass {
172
continue;
173
} else {
174
// Get the new jobs running before moving on to the next directory.
175
self.schedule_jobs();
176
}
177
}
178
}
179
180
/// Report an error related to a path.
181
fn path_error<E: Error>(&mut self, path: &PathBuf, err: &E) {
182
self.errors += 1;
183
println!("{}: {}", path.to_string_lossy(), err);
184
}
185
186
/// Report on the next in-order job, if it's done.
187
fn report_job(&self) -> bool {
188
let jobid = self.reported_tests;
189
if let Some(&QueueEntry {
190
state: State::Done(ref result),
191
..
192
}) = self.tests.get(jobid)
193
{
194
if self.verbose || result.is_err() {
195
println!("{}", self.tests[jobid]);
196
}
197
true
198
} else {
199
false
200
}
201
}
202
203
/// Schedule any new jobs to run.
204
fn schedule_jobs(&mut self) {
205
for jobid in self.new_tests..self.tests.len() {
206
assert!(matches!(self.tests[jobid].state, State::New));
207
if let Some(ref mut conc) = self.threads {
208
// Queue test for concurrent execution.
209
self.tests[jobid].state = State::Queued;
210
conc.put(jobid, self.tests[jobid].path());
211
} else {
212
// Run test synchronously.
213
self.tests[jobid].state = State::Running;
214
let result = runone::run(self.tests[jobid].path(), None, None);
215
self.finish_job(jobid, result);
216
}
217
self.new_tests = jobid + 1;
218
}
219
220
// Check for any asynchronous replies without blocking.
221
while let Some(reply) = self.threads.as_mut().and_then(ConcurrentRunner::try_get) {
222
self.handle_reply(reply);
223
}
224
}
225
226
/// Schedule any new job to run for the pass command.
227
fn schedule_pass_job(&mut self, passes: &[String], target: &str) {
228
self.tests[0].state = State::Running;
229
let result: anyhow::Result<time::Duration>;
230
231
let specified_target = match target {
232
"" => None,
233
targ => Some(targ),
234
};
235
236
result = runone::run(self.tests[0].path(), Some(passes), specified_target);
237
self.finish_job(0, result);
238
}
239
240
/// Report the end of a job.
241
fn finish_job(&mut self, jobid: usize, result: anyhow::Result<time::Duration>) {
242
assert!(matches!(self.tests[jobid].state, State::Running));
243
if result.is_err() {
244
self.errors += 1;
245
}
246
self.tests[jobid].state = State::Done(result);
247
248
// Reports jobs in order.
249
while self.report_job() {
250
self.reported_tests += 1;
251
}
252
}
253
254
/// Handle a reply from the async threads.
255
fn handle_reply(&mut self, reply: Reply) {
256
match reply {
257
Reply::Starting { jobid, .. } => {
258
assert!(matches!(self.tests[jobid].state, State::Queued));
259
self.tests[jobid].state = State::Running;
260
}
261
Reply::Done { jobid, result } => {
262
self.ticks_since_progress = 0;
263
self.finish_job(jobid, result)
264
}
265
Reply::Tick => {
266
self.ticks_since_progress += 1;
267
if self.ticks_since_progress == TIMEOUT_SLOW {
268
println!(
269
"STALLED for {} seconds with {}/{} tests finished",
270
self.ticks_since_progress,
271
self.reported_tests,
272
self.tests.len()
273
);
274
for jobid in self.reported_tests..self.tests.len() {
275
if let State::Running = self.tests[jobid].state {
276
println!("slow: {}", self.tests[jobid]);
277
}
278
}
279
}
280
if self.ticks_since_progress >= TIMEOUT_PANIC {
281
panic!(
282
"worker threads stalled for {} seconds.",
283
self.ticks_since_progress
284
);
285
}
286
}
287
}
288
}
289
290
/// Drain the async jobs and shut down the threads.
291
fn drain_threads(&mut self) {
292
if let Some(mut conc) = self.threads.take() {
293
conc.shutdown();
294
while self.reported_tests < self.tests.len() {
295
match conc.get() {
296
Some(reply) => self.handle_reply(reply),
297
None => break,
298
}
299
}
300
let pass_times = conc.join();
301
if self.report_times {
302
println!("{pass_times}");
303
}
304
}
305
}
306
307
/// Print out a report of slow tests.
308
fn report_slow_tests(&self) {
309
// Collect runtimes of succeeded tests.
310
let mut times = self
311
.tests
312
.iter()
313
.filter_map(|entry| match *entry {
314
QueueEntry {
315
state: State::Done(Ok(dur)),
316
..
317
} => Some(dur),
318
_ => None,
319
})
320
.collect::<Vec<_>>();
321
322
// Get me some real data, kid.
323
let len = times.len();
324
if len < 4 {
325
return;
326
}
327
328
// Compute quartiles.
329
times.sort();
330
let qlen = len / 4;
331
let q1 = times[qlen];
332
let q3 = times[len - 1 - qlen];
333
// Inter-quartile range.
334
let iqr = q3 - q1;
335
336
// Cut-off for what we consider a 'slow' test: 3 IQR from the 75% quartile.
337
//
338
// Q3 + 1.5 IQR are the data points that would be plotted as outliers outside a box plot,
339
// but we have a wider distribution of test times, so double it to 3 IQR.
340
let cut = q3 + iqr * 3;
341
if cut > *times.last().unwrap() {
342
return;
343
}
344
345
for t in self.tests.iter().filter(|entry| match **entry {
346
QueueEntry {
347
state: State::Done(Ok(dur)),
348
..
349
} => dur > cut,
350
_ => false,
351
}) {
352
println!("slow: {t}")
353
}
354
}
355
356
/// Scan pushed directories for tests and run them.
357
pub fn run(&mut self) -> anyhow::Result<()> {
358
self.scan_dirs(IsPass::NotPass);
359
self.schedule_jobs();
360
self.report_slow_tests();
361
self.drain_threads();
362
363
println!("{} tests", self.tests.len());
364
match self.errors {
365
0 => Ok(()),
366
1 => anyhow::bail!("1 failure"),
367
n => anyhow::bail!("{} failures", n),
368
}
369
}
370
371
/// Scan pushed directories for tests and run specified passes from commandline on them.
372
pub fn run_passes(&mut self, passes: &[String], target: &str) -> anyhow::Result<()> {
373
self.scan_dirs(IsPass::Pass);
374
self.schedule_pass_job(passes, target);
375
self.report_slow_tests();
376
self.drain_threads();
377
378
println!("{} tests", self.tests.len());
379
match self.errors {
380
0 => Ok(()),
381
1 => anyhow::bail!("1 failure"),
382
n => anyhow::bail!("{} failures", n),
383
}
384
}
385
}
386
387