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