libtest2_harness/
harness.rs

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    // Logic taken from rust-lang/rust's `compiler/rustc_driver_impl/src/args.rs`
193    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            // All values are the same, whether escaped or not, so its a no-op
221            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    // If the platform is single-threaded we're just going to run
243    // the test synchronously, regardless of the concurrency
244    // level.
245    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        // Use a deterministic hasher
354        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                        // `ErrorKind::WouldBlock` means hitting the thread limit on some
390                        // platforms, so run the test synchronously here instead.
391                        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        // The `panic` information is just an `Any` object representing the
446        // value the panic was invoked with. For most panics (which use
447        // `panic!` like `println!`), this is either `&str` or `String`.
448        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/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
488#[inline(never)]
489fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
490    let result = f();
491
492    // prevent this frame from being tail-call optimised away
493    std::hint::black_box(result)
494}