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