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 {
path(&self) -> &Path40     pub fn path(&self) -> &Path {
41         self.path.as_path()
42     }
43 }
44 
45 impl Display for QueueEntry {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result46     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.
new(verbose: bool, report_times: bool) -> Self85     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.
push_dir<P: Into<PathBuf>>(&mut self, dir: P)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.
push_test<P: Into<PathBuf>>(&mut self, file: P)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.
start_threads(&mut self)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.
scan_dirs(&mut self, pass_status: IsPass)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.
path_error<E: Error>(&mut self, path: &PathBuf, err: &E)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.
report_job(&self) -> bool187     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.
schedule_jobs(&mut self)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.
schedule_pass_job(&mut self, passes: &[String], target: &str)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.
finish_job(&mut self, jobid: usize, result: anyhow::Result<time::Duration>)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.
handle_reply(&mut self, reply: Reply)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.
drain_threads(&mut self)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.
report_slow_tests(&self)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.
run(&mut self) -> anyhow::Result<()>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!("{n} failures"),
368         }
369     }
370 
371     /// Scan pushed directories for tests and run specified passes from commandline on them.
run_passes(&mut self, passes: &[String], target: &str) -> anyhow::Result<()>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!("{n} failures"),
383         }
384     }
385 }
386