1use libtest_lexarg::OutputFormat;
2
3use crate::{cli, notify, Case, RunError, RunMode, TestContext};
4
5pub trait HarnessState: sealed::_HarnessState_is_Sealed {}
6
7pub struct Harness<State: HarnessState> {
8 state: State,
9}
10
11pub struct StateInitial {
12 start: std::time::Instant,
13}
14impl HarnessState for StateInitial {}
15impl sealed::_HarnessState_is_Sealed for StateInitial {}
16
17impl Harness<StateInitial> {
18 pub fn new() -> Self {
19 Self {
20 state: StateInitial {
21 start: std::time::Instant::now(),
22 },
23 }
24 }
25
26 pub fn with_env(self) -> std::io::Result<Harness<StateArgs>> {
27 let raw = std::env::args_os();
28 self.with_args(raw)
29 }
30
31 pub fn with_args(
32 self,
33 args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
34 ) -> std::io::Result<Harness<StateArgs>> {
35 let raw = expand_args(args)?;
36 Ok(Harness {
37 state: StateArgs {
38 start: self.state.start,
39 raw,
40 },
41 })
42 }
43}
44
45impl Default for Harness<StateInitial> {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51pub struct StateArgs {
52 start: std::time::Instant,
53 raw: Vec<std::ffi::OsString>,
54}
55impl HarnessState for StateArgs {}
56impl sealed::_HarnessState_is_Sealed for StateArgs {}
57
58impl Harness<StateArgs> {
59 pub fn parse(&self) -> Result<Harness<StateParsed>, cli::LexError<'_>> {
60 let mut parser = cli::Parser::new(&self.state.raw);
61 let opts = parse(&mut parser)?;
62
63 #[cfg(feature = "color")]
64 match opts.color {
65 libtest_lexarg::ColorConfig::AutoColor => anstream::ColorChoice::Auto,
66 libtest_lexarg::ColorConfig::AlwaysColor => anstream::ColorChoice::Always,
67 libtest_lexarg::ColorConfig::NeverColor => anstream::ColorChoice::Never,
68 }
69 .write_global();
70
71 let notifier = notifier(&opts);
72
73 Ok(Harness {
74 state: StateParsed {
75 start: self.state.start,
76 opts,
77 notifier,
78 },
79 })
80 }
81}
82
83pub struct StateParsed {
84 start: std::time::Instant,
85 opts: libtest_lexarg::TestOpts,
86 notifier: notify::ArcNotifier,
87}
88impl HarnessState for StateParsed {}
89impl sealed::_HarnessState_is_Sealed for StateParsed {}
90
91impl Harness<StateParsed> {
92 pub fn discover(
93 self,
94 cases: impl IntoIterator<Item = impl Case + 'static>,
95 ) -> std::io::Result<Harness<StateDiscovered>> {
96 self.state.notifier.notify(
97 notify::event::DiscoverStart {
98 elapsed_s: Some(notify::Elapsed(self.state.start.elapsed())),
99 }
100 .into(),
101 )?;
102
103 let mut selected_cases = Vec::new();
104 for case in cases {
105 let selected = case_priority(&case, &self.state.opts).is_some();
106 self.state.notifier.notify(
107 notify::event::DiscoverCase {
108 name: case.name().to_owned(),
109 mode: RunMode::Test,
110 selected,
111 elapsed_s: Some(notify::Elapsed(self.state.start.elapsed())),
112 }
113 .into(),
114 )?;
115 if selected {
116 selected_cases.push(Box::new(case) as Box<dyn Case>);
117 }
118 }
119
120 selected_cases.sort_unstable_by_key(|case| {
121 let priority = case_priority(case.as_ref(), &self.state.opts);
122 let name = case.name().to_owned();
123 (priority, name)
124 });
125
126 self.state.notifier.notify(
127 notify::event::DiscoverComplete {
128 elapsed_s: Some(notify::Elapsed(self.state.start.elapsed())),
129 }
130 .into(),
131 )?;
132
133 Ok(Harness {
134 state: StateDiscovered {
135 start: self.state.start,
136 opts: self.state.opts,
137 notifier: self.state.notifier,
138 cases: selected_cases,
139 },
140 })
141 }
142}
143
144pub struct StateDiscovered {
145 start: std::time::Instant,
146 opts: libtest_lexarg::TestOpts,
147 notifier: notify::ArcNotifier,
148 cases: Vec<Box<dyn Case>>,
149}
150impl HarnessState for StateDiscovered {}
151impl sealed::_HarnessState_is_Sealed for StateDiscovered {}
152
153impl Harness<StateDiscovered> {
154 pub fn run(self) -> std::io::Result<bool> {
155 if self.state.opts.list {
156 Ok(true)
157 } else {
158 run(
159 &self.state.start,
160 &self.state.opts,
161 self.state.cases,
162 self.state.notifier,
163 )
164 }
165 }
166}
167
168mod sealed {
169 #[allow(unnameable_types)]
170 #[allow(non_camel_case_types)]
171 pub trait _HarnessState_is_Sealed {}
172}
173
174pub const ERROR_EXIT_CODE: i32 = 101;
175
176fn expand_args(
177 args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
178) -> std::io::Result<Vec<std::ffi::OsString>> {
179 let mut expanded = Vec::new();
180 for arg in args {
181 let arg = arg.into();
182 if let Some(argfile) = arg.to_str().and_then(|s| s.strip_prefix("@")) {
183 expanded.extend(parse_argfile(std::path::Path::new(argfile))?);
184 } else {
185 expanded.push(arg);
186 }
187 }
188 Ok(expanded)
189}
190
191fn parse_argfile(path: &std::path::Path) -> std::io::Result<Vec<std::ffi::OsString>> {
192 let content = std::fs::read_to_string(path)?;
194 Ok(content.lines().map(|s| s.into()).collect())
195}
196
197fn parse<'p>(parser: &mut cli::Parser<'p>) -> Result<libtest_lexarg::TestOpts, cli::LexError<'p>> {
198 let mut test_opts = libtest_lexarg::TestOptsBuilder::new();
199
200 let bin = parser
201 .next_raw()
202 .expect("first arg, no pending values")
203 .unwrap_or(std::ffi::OsStr::new("test"));
204 let mut prev_arg = cli::Arg::Value(bin);
205 while let Some(arg) = parser.next_arg() {
206 match arg {
207 cli::Arg::Short("h") | cli::Arg::Long("help") => {
208 let bin = bin.to_string_lossy();
209 let options_help = libtest_lexarg::OPTIONS_HELP.trim();
210 let after_help = libtest_lexarg::AFTER_HELP.trim();
211 println!(
212 "Usage: {bin} [OPTIONS] [FILTER]...
213
214{options_help}
215
216{after_help}"
217 );
218 std::process::exit(0);
219 }
220 cli::Arg::Escape(_) => {
222 prev_arg = arg;
223 continue;
224 }
225 cli::Arg::Unexpected(_) => {
226 return Err(cli::LexError::msg("unexpected value")
227 .unexpected(arg)
228 .within(prev_arg));
229 }
230 _ => {}
231 }
232 prev_arg = arg;
233
234 let arg = test_opts.parse_next(parser, arg)?;
235
236 if let Some(arg) = arg {
237 return Err(cli::LexError::msg("unexpected argument").unexpected(arg));
238 }
239 }
240
241 let mut opts = test_opts.finish()?;
242 let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_family = "wasm");
246 opts.test_threads = if cfg!(feature = "threads") && supports_threads {
247 opts.test_threads
248 .or_else(|| std::thread::available_parallelism().ok())
249 } else {
250 None
251 };
252 Ok(opts)
253}
254
255fn notifier(opts: &libtest_lexarg::TestOpts) -> notify::ArcNotifier {
256 #[cfg(feature = "color")]
257 let stdout = anstream::stdout();
258 #[cfg(not(feature = "color"))]
259 let stdout = std::io::stdout();
260 match opts.format {
261 OutputFormat::Json => notify::ArcNotifier::new(notify::JsonNotifier::new(stdout)),
262 _ if opts.list => notify::ArcNotifier::new(notify::TerseListNotifier::new(stdout)),
263 OutputFormat::Pretty => notify::ArcNotifier::new(notify::PrettyRunNotifier::new(stdout)),
264 OutputFormat::Terse => notify::ArcNotifier::new(notify::TerseRunNotifier::new(stdout)),
265 }
266}
267
268fn case_priority(case: &dyn Case, opts: &libtest_lexarg::TestOpts) -> Option<usize> {
269 let filtered_out =
270 !opts.skip.is_empty() && opts.skip.iter().any(|sf| matches_filter(case, sf, opts));
271 if filtered_out {
272 None
273 } else if opts.filters.is_empty() {
274 Some(0)
275 } else {
276 opts.filters
277 .iter()
278 .position(|filter| matches_filter(case, filter, opts))
279 }
280}
281
282fn matches_filter(case: &dyn Case, filter: &str, opts: &libtest_lexarg::TestOpts) -> bool {
283 let test_name = case.name();
284
285 match opts.filter_exact {
286 true => test_name == filter,
287 false => test_name.contains(filter),
288 }
289}
290
291fn run(
292 start: &std::time::Instant,
293 opts: &libtest_lexarg::TestOpts,
294 cases: Vec<Box<dyn Case>>,
295 notifier: notify::ArcNotifier,
296) -> std::io::Result<bool> {
297 notifier.notify(
298 notify::event::RunStart {
299 elapsed_s: Some(notify::Elapsed(start.elapsed())),
300 }
301 .into(),
302 )?;
303
304 if opts.no_capture {
305 return Err(std::io::Error::new(
306 std::io::ErrorKind::Unsupported,
307 "`--no-capture` is not supported at this time",
308 ));
309 }
310 if opts.show_output {
311 return Err(std::io::Error::new(
312 std::io::ErrorKind::Unsupported,
313 "`--show-output` is not supported at this time",
314 ));
315 }
316
317 let threads = opts.test_threads.map(|t| t.get()).unwrap_or(1);
318
319 let run_ignored = match opts.run_ignored {
320 libtest_lexarg::RunIgnored::Yes | libtest_lexarg::RunIgnored::Only => true,
321 libtest_lexarg::RunIgnored::No => false,
322 };
323 let mode = match (opts.run_tests, opts.bench_benchmarks) {
324 (true, true) => {
325 return Err(std::io::Error::other(
326 "`--test` and `-bench` are mutually exclusive",
327 ));
328 }
329 (true, false) => RunMode::Test,
330 (false, true) => RunMode::Bench,
331 (false, false) => unreachable!("libtest-lexarg` should always ensure at least one is set"),
332 };
333 let context = TestContext {
334 start: *start,
335 mode,
336 run_ignored,
337 notifier,
338 test_name: String::new(),
339 };
340
341 let mut success = true;
342
343 let (exclusive_cases, concurrent_cases) = if threads == 1 || cases.len() == 1 {
344 (cases, vec![])
345 } else {
346 cases
347 .into_iter()
348 .partition::<Vec<_>, _>(|c| c.exclusive(&context))
349 };
350 if !concurrent_cases.is_empty() {
351 context.notifier().threaded(true);
352
353 type TestMap = std::collections::HashMap<
355 String,
356 std::thread::JoinHandle<std::io::Result<bool>>,
357 std::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>,
358 >;
359
360 let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success));
361 let mut running: TestMap = Default::default();
362 let (tx, rx) = std::sync::mpsc::channel::<String>();
363 let mut remaining = std::collections::VecDeque::from(concurrent_cases);
364 while !running.is_empty() || !remaining.is_empty() {
365 while running.len() < threads && !remaining.is_empty() {
366 let case = remaining.pop_front().unwrap();
367 let case = std::sync::Arc::new(case);
368 let name = case.name().to_owned();
369
370 let cfg = std::thread::Builder::new().name(name.clone());
371 let thread_tx = tx.clone();
372 let thread_case = case.clone();
373 let mut thread_context = context.clone();
374 thread_context.test_name = name.clone();
375 let thread_sync_success = sync_success.clone();
376 let join_handle = cfg.spawn(move || {
377 let status = run_case(thread_case.as_ref().as_ref(), &thread_context);
378 if !matches!(status, Ok(true)) {
379 thread_sync_success.store(false, std::sync::atomic::Ordering::Relaxed);
380 }
381 let _ = thread_tx.send(thread_case.name().to_owned());
382 status
383 });
384 match join_handle {
385 Ok(join_handle) => {
386 running.insert(name.clone(), join_handle);
387 }
388 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
389 let case_success = run_case(case.as_ref().as_ref(), &context)?;
392 if !case_success {
393 sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed);
394 }
395 }
396 Err(e) => {
397 return Err(e);
398 }
399 }
400 }
401
402 let test_name = rx.recv().unwrap();
403 let running_test = running.remove(&test_name).unwrap();
404 let _ = running_test.join();
405 success &= sync_success.load(std::sync::atomic::Ordering::SeqCst);
406 if !success && opts.fail_fast {
407 break;
408 }
409 }
410 }
411
412 if !exclusive_cases.is_empty() {
413 context.notifier().threaded(false);
414 for case in exclusive_cases {
415 success &= run_case(case.as_ref(), &context)?;
416 if !success && opts.fail_fast {
417 break;
418 }
419 }
420 }
421
422 context.notifier().notify(
423 notify::event::RunComplete {
424 elapsed_s: Some(notify::Elapsed(start.elapsed())),
425 }
426 .into(),
427 )?;
428
429 Ok(success)
430}
431
432fn run_case(case: &dyn Case, context: &TestContext) -> std::io::Result<bool> {
433 context.notifier().notify(
434 notify::event::CaseStart {
435 name: case.name().to_owned(),
436 elapsed_s: Some(context.elapased_s()),
437 }
438 .into(),
439 )?;
440
441 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
442 __rust_begin_short_backtrace(|| case.run(context))
443 }))
444 .unwrap_or_else(|e| {
445 let payload = e
449 .downcast_ref::<String>()
450 .map(|s| s.as_str())
451 .or_else(|| e.downcast_ref::<&str>().copied());
452
453 let msg = match payload {
454 Some(payload) => format!("test panicked: {payload}"),
455 None => "test panicked".to_owned(),
456 };
457 Err(RunError::fail(msg))
458 });
459
460 let mut case_status = None;
461 if let Some(err) = outcome.as_ref().err() {
462 let kind = err.status();
463 case_status = Some(kind);
464 let message = err.cause().map(|c| c.to_string());
465 context.notifier().notify(
466 notify::event::CaseMessage {
467 name: case.name().to_owned(),
468 kind,
469 message,
470 elapsed_s: Some(context.elapased_s()),
471 }
472 .into(),
473 )?;
474 }
475
476 context.notifier().notify(
477 notify::event::CaseComplete {
478 name: case.name().to_owned(),
479 elapsed_s: Some(context.elapased_s()),
480 }
481 .into(),
482 )?;
483
484 Ok(case_status != Some(notify::MessageKind::Error))
485}
486
487#[inline(never)]
489fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
490 let result = f();
491
492 std::hint::black_box(result)
494}