1use 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
14const TIMEOUT_PANIC: usize = 60;
16
17const 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 report_times: bool,
61
62 dir_stack: Vec<PathBuf>,
64
65 tests: Vec<QueueEntry>,
67
68 new_tests: usize,
70
71 reported_tests: usize,
73
74 errors: usize,
76
77 ticks_since_progress: usize,
79
80 threads: Option<ConcurrentRunner>,
81}
82
83impl TestRunner {
84 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 pub fn push_dir<P: Into<PathBuf>>(&mut self, dir: P) {
104 self.dir_stack.push(dir.into());
105 }
106
107 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 pub fn start_threads(&mut self) {
119 assert!(self.threads.is_none());
120 self.threads = Some(ConcurrentRunner::new());
121 }
122
123 pub fn scan_dirs(&mut self, pass_status: IsPass) {
126 while let Some(dir) = self.dir_stack.pop() {
134 match dir.read_dir() {
135 Err(err) => {
136 if !dir.is_file() {
140 self.path_error(&dir, &err);
141 }
142 }
143 Ok(entries) => {
144 for entry_result in entries {
146 match entry_result {
147 Err(err) => {
148 self.path_error(&dir, &err);
155 break;
156 }
157 Ok(entry) => {
158 let path = entry.path();
159 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 self.schedule_jobs();
176 }
177 }
178 }
179
180 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 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 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 self.tests[jobid].state = State::Queued;
210 conc.put(jobid, self.tests[jobid].path());
211 } else {
212 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 while let Some(reply) = self.threads.as_mut().and_then(ConcurrentRunner::try_get) {
222 self.handle_reply(reply);
223 }
224 }
225
226 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 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 while self.report_job() {
250 self.reported_tests += 1;
251 }
252 }
253
254 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 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 fn report_slow_tests(&self) {
309 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 let len = times.len();
324 if len < 4 {
325 return;
326 }
327
328 times.sort();
330 let qlen = len / 4;
331 let q1 = times[qlen];
332 let q3 = times[len - 1 - qlen];
333 let iqr = q3 - q1;
335
336 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 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 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}