cranelift_filetests/
runner.rs

1//! Test runner.
2//!
3//! This module implements the `TestRunner` struct which manages executing tests as well as
4//! scanning directories for tests.
5
6use crate::concurrent::{ConcurrentRunner, Reply};
7use crate::runone;
8use std::error::Error;
9use std::ffi::OsStr;
10use std::fmt::{self, Display};
11use std::path::{Path, PathBuf};
12use std::time;
13
14/// Timeout in seconds when we're not making progress.
15const TIMEOUT_PANIC: usize = 60;
16
17/// Timeout for reporting slow tests without panicking.
18const TIMEOUT_SLOW: usize = 3;
19
20struct QueueEntry {
21    path: PathBuf,
22    state: State,
23}
24
25#[derive(Debug)]
26enum State {
27    New,
28    Queued,
29    Running,
30    Done(anyhow::Result<time::Duration>),
31}
32
33#[derive(PartialEq, Eq, Debug, Clone, Copy)]
34pub enum IsPass {
35    Pass,
36    NotPass,
37}
38
39impl QueueEntry {
40    pub fn path(&self) -> &Path {
41        self.path.as_path()
42    }
43}
44
45impl 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
56pub 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
83impl 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}