libtest2_harness/
harness.rs

1use libtest_lexarg::OutputFormat;
2
3use crate::{cli, notify, Case, RunError, RunMode, State};
4
5pub struct Harness {
6    raw: std::io::Result<Vec<std::ffi::OsString>>,
7    cases: Vec<Box<dyn Case>>,
8}
9
10impl Harness {
11    pub fn with_args(args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> Self {
12        let raw = expand_args(args);
13        Self { raw, cases: vec![] }
14    }
15
16    pub fn with_env() -> Self {
17        let raw = std::env::args_os();
18        let raw = expand_args(raw);
19        Self { raw, cases: vec![] }
20    }
21
22    pub fn case(mut self, case: impl Case + 'static) -> Self {
23        self.cases.push(Box::new(case));
24        self
25    }
26
27    pub fn cases(mut self, cases: impl IntoIterator<Item = impl Case + 'static>) -> Self {
28        for case in cases {
29            self.cases.push(Box::new(case));
30        }
31        self
32    }
33
34    pub fn main(mut self) -> ! {
35        let raw = match self.raw {
36            Ok(raw) => raw,
37            Err(err) => {
38                eprintln!("{err}");
39                std::process::exit(1)
40            }
41        };
42        let mut parser = cli::Parser::new(&raw);
43        let opts = parse(&mut parser).unwrap_or_else(|err| {
44            eprintln!("{err}");
45            std::process::exit(1)
46        });
47
48        #[cfg(feature = "color")]
49        match opts.color {
50            libtest_lexarg::ColorConfig::AutoColor => anstream::ColorChoice::Auto,
51            libtest_lexarg::ColorConfig::AlwaysColor => anstream::ColorChoice::Always,
52            libtest_lexarg::ColorConfig::NeverColor => anstream::ColorChoice::Never,
53        }
54        .write_global();
55
56        let mut notifier = notifier(&opts).unwrap_or_else(|err| {
57            eprintln!("{err}");
58            std::process::exit(1)
59        });
60        discover(&opts, &mut self.cases, notifier.as_mut()).unwrap_or_else(|err| {
61            eprintln!("{err}");
62            std::process::exit(1)
63        });
64
65        if !opts.list {
66            match run(&opts, self.cases, notifier.as_mut()) {
67                Ok(true) => {}
68                Ok(false) => std::process::exit(ERROR_EXIT_CODE),
69                Err(e) => {
70                    eprintln!("error: io error when listing tests: {e:?}");
71                    std::process::exit(ERROR_EXIT_CODE)
72                }
73            }
74        }
75
76        std::process::exit(0)
77    }
78}
79
80const ERROR_EXIT_CODE: i32 = 101;
81
82fn parse<'p>(
83    parser: &mut cli::Parser<'p>,
84) -> Result<libtest_lexarg::TestOpts, cli::ErrorContext<'p>> {
85    let mut test_opts = libtest_lexarg::TestOptsBuilder::new();
86
87    let bin = parser
88        .next_raw()
89        .expect("first arg, no pending values")
90        .unwrap_or(std::ffi::OsStr::new("test"));
91    let mut prev_arg = cli::Arg::Value(bin);
92    while let Some(arg) = parser.next_arg() {
93        match arg {
94            cli::Arg::Short("h") | cli::Arg::Long("help") => {
95                let bin = bin.to_string_lossy();
96                let options_help = libtest_lexarg::OPTIONS_HELP.trim();
97                let after_help = libtest_lexarg::AFTER_HELP.trim();
98                println!(
99                    "Usage: {bin} [OPTIONS] [FILTER]...
100
101{options_help}
102
103{after_help}"
104                );
105                std::process::exit(0);
106            }
107            // All values are the same, whether escaped or not, so its a no-op
108            cli::Arg::Escape(_) => {
109                prev_arg = arg;
110                continue;
111            }
112            cli::Arg::Unexpected(_) => {
113                return Err(cli::ErrorContext::msg("unexpected value")
114                    .unexpected(arg)
115                    .within(prev_arg));
116            }
117            _ => {}
118        }
119        prev_arg = arg;
120
121        let arg = test_opts.parse_next(parser, arg)?;
122
123        if let Some(arg) = arg {
124            return Err(cli::ErrorContext::msg("unexpected argument").unexpected(arg));
125        }
126    }
127
128    let mut opts = test_opts.finish()?;
129    // If the platform is single-threaded we're just going to run
130    // the test synchronously, regardless of the concurrency
131    // level.
132    let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_family = "wasm");
133    opts.test_threads = if cfg!(feature = "threads") && supports_threads {
134        opts.test_threads
135            .or_else(|| std::thread::available_parallelism().ok())
136    } else {
137        None
138    };
139    Ok(opts)
140}
141
142fn expand_args(
143    args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
144) -> std::io::Result<Vec<std::ffi::OsString>> {
145    let mut expanded = Vec::new();
146    for arg in args {
147        let arg = arg.into();
148        if let Some(argfile) = arg.to_str().and_then(|s| s.strip_prefix("@")) {
149            expanded.extend(parse_argfile(std::path::Path::new(argfile))?);
150        } else {
151            expanded.push(arg);
152        }
153    }
154    Ok(expanded)
155}
156
157fn parse_argfile(path: &std::path::Path) -> std::io::Result<Vec<std::ffi::OsString>> {
158    // Logic taken from rust-lang/rust's `compiler/rustc_driver_impl/src/args.rs`
159    let content = std::fs::read_to_string(path)?;
160    Ok(content.lines().map(|s| s.into()).collect())
161}
162
163fn notifier(opts: &libtest_lexarg::TestOpts) -> std::io::Result<Box<dyn notify::Notifier>> {
164    #[cfg(feature = "color")]
165    let stdout = anstream::stdout();
166    #[cfg(not(feature = "color"))]
167    let stdout = std::io::stdout();
168    let notifier: Box<dyn notify::Notifier> = match opts.format {
169        #[cfg(feature = "json")]
170        OutputFormat::Json => Box::new(notify::JsonNotifier::new(stdout)),
171        #[cfg(not(feature = "json"))]
172        OutputFormat::Json => {
173            return Err(std::io::Error::new(
174                std::io::ErrorKind::Other,
175                "`--format=json` is not supported",
176            ));
177        }
178        _ if opts.list => Box::new(notify::TerseListNotifier::new(stdout)),
179        OutputFormat::Pretty => Box::new(notify::PrettyRunNotifier::new(stdout)),
180        OutputFormat::Terse => Box::new(notify::TerseRunNotifier::new(stdout)),
181    };
182    Ok(notifier)
183}
184
185fn discover(
186    opts: &libtest_lexarg::TestOpts,
187    cases: &mut Vec<Box<dyn Case>>,
188    notifier: &mut dyn notify::Notifier,
189) -> std::io::Result<()> {
190    notifier.notify(notify::Event::DiscoverStart)?;
191    let timer = std::time::Instant::now();
192
193    let matches_filter = |case: &dyn Case, filter: &str| {
194        let test_name = case.name();
195
196        match opts.filter_exact {
197            true => test_name == filter,
198            false => test_name.contains(filter),
199        }
200    };
201
202    // Do this first so it applies to both discover and running
203    cases.sort_unstable_by_key(|case| {
204        let priority = if opts.filters.is_empty() {
205            Some(0)
206        } else {
207            opts.filters
208                .iter()
209                .position(|filter| matches_filter(case.as_ref(), filter))
210        };
211        let name = case.name().to_owned();
212        (priority, name)
213    });
214
215    let mut retain_cases = Vec::with_capacity(cases.len());
216    for case in cases.iter() {
217        let filtered_in = opts.filters.is_empty()
218            || opts
219                .filters
220                .iter()
221                .any(|filter| matches_filter(case.as_ref(), filter));
222        let filtered_out =
223            !opts.skip.is_empty() && opts.skip.iter().any(|sf| matches_filter(case.as_ref(), sf));
224        let retain_case = filtered_in && !filtered_out;
225        retain_cases.push(retain_case);
226        notifier.notify(notify::Event::DiscoverCase {
227            name: case.name().to_owned(),
228            mode: RunMode::Test,
229            run: retain_case,
230        })?;
231    }
232    let mut retain_cases = retain_cases.into_iter();
233    cases.retain(|_| retain_cases.next().unwrap());
234
235    notifier.notify(notify::Event::DiscoverComplete {
236        elapsed_s: notify::Elapsed(timer.elapsed()),
237    })?;
238
239    Ok(())
240}
241
242fn run(
243    opts: &libtest_lexarg::TestOpts,
244    cases: Vec<Box<dyn Case>>,
245    notifier: &mut dyn notify::Notifier,
246) -> std::io::Result<bool> {
247    notifier.notify(notify::Event::SuiteStart)?;
248    let timer = std::time::Instant::now();
249
250    if opts.nocapture {
251        todo!("`--nocapture` is not yet supported");
252    }
253    if opts.options.display_output {
254        todo!("`--show-output` is not yet supported");
255    }
256    if opts.options.panic_abort {
257        todo!("panic-abort is not yet supported");
258    }
259
260    let threads = opts.test_threads.map(|t| t.get()).unwrap_or(1);
261
262    let mut state = State::new();
263    let run_ignored = match opts.run_ignored {
264        libtest_lexarg::RunIgnored::Yes | libtest_lexarg::RunIgnored::Only => true,
265        libtest_lexarg::RunIgnored::No => false,
266    };
267    let mode = match (opts.run_tests, opts.bench_benchmarks) {
268        (true, true) => {
269            return Err(std::io::Error::other(
270                "`--test` and `-bench` are mutually exclusive",
271            ));
272        }
273        (true, false) => RunMode::Test,
274        (false, true) => RunMode::Bench,
275        (false, false) => unreachable!("libtest-lexarg` should always ensure at least one is set"),
276    };
277    state.set_mode(mode);
278    state.set_run_ignored(run_ignored);
279    let state = std::sync::Arc::new(state);
280
281    let mut success = true;
282
283    let (exclusive_cases, concurrent_cases) = if threads == 1 || cases.len() == 1 {
284        (cases, vec![])
285    } else {
286        cases
287            .into_iter()
288            .partition::<Vec<_>, _>(|c| c.exclusive(&state))
289    };
290    if !concurrent_cases.is_empty() {
291        notifier.threaded(true);
292        struct RunningTest {
293            join_handle: std::thread::JoinHandle<()>,
294        }
295
296        impl RunningTest {
297            fn join(self, event: &mut notify::Event) {
298                if self.join_handle.join().is_err() {
299                    if let notify::Event::CaseComplete {
300                        status, message, ..
301                    } = event
302                    {
303                        if status.is_none() {
304                            *status = Some(notify::RunStatus::Failed);
305                            *message = Some("panicked after reporting success".to_owned());
306                        }
307                    }
308                }
309            }
310        }
311
312        // Use a deterministic hasher
313        type TestMap = std::collections::HashMap<
314            String,
315            RunningTest,
316            std::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>,
317        >;
318
319        let sync_success = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(success));
320        let mut running_tests: TestMap = Default::default();
321        let mut pending = 0;
322        let (tx, rx) = std::sync::mpsc::channel::<notify::Event>();
323        let mut remaining = std::collections::VecDeque::from(concurrent_cases);
324        while pending > 0 || !remaining.is_empty() {
325            while pending < threads && !remaining.is_empty() {
326                let case = remaining.pop_front().unwrap();
327                let name = case.name().to_owned();
328
329                let cfg = std::thread::Builder::new().name(name.clone());
330                let tx = tx.clone();
331                let case = std::sync::Arc::new(case);
332                let case_fallback = case.clone();
333                let state = state.clone();
334                let state_fallback = state.clone();
335                let sync_success = sync_success.clone();
336                let sync_success_fallback = sync_success.clone();
337                let join_handle = cfg.spawn(move || {
338                    let mut notifier = SenderNotifier { tx: tx.clone() };
339                    let case_success = run_case(case.as_ref().as_ref(), &state, &mut notifier)
340                        .expect("`SenderNotifier` is infallible");
341                    if !case_success {
342                        sync_success.store(case_success, std::sync::atomic::Ordering::Relaxed);
343                    }
344                });
345                match join_handle {
346                    Ok(join_handle) => {
347                        running_tests.insert(name.clone(), RunningTest { join_handle });
348                        pending += 1;
349                    }
350                    Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
351                        // `ErrorKind::WouldBlock` means hitting the thread limit on some
352                        // platforms, so run the test synchronously here instead.
353                        let case_success =
354                            run_case(case_fallback.as_ref().as_ref(), &state_fallback, notifier)
355                                .expect("`SenderNotifier` is infallible");
356                        if !case_success {
357                            sync_success_fallback
358                                .store(case_success, std::sync::atomic::Ordering::Relaxed);
359                        }
360                    }
361                    Err(e) => {
362                        return Err(e);
363                    }
364                }
365            }
366
367            let mut event = rx.recv().unwrap();
368            if let notify::Event::CaseComplete { name, .. } = &event {
369                let running_test = running_tests.remove(name).unwrap();
370                running_test.join(&mut event);
371                pending -= 1;
372            }
373            notifier.notify(event)?;
374            success &= sync_success.load(std::sync::atomic::Ordering::SeqCst);
375            if !success && opts.fail_fast {
376                break;
377            }
378        }
379    }
380
381    if !exclusive_cases.is_empty() {
382        notifier.threaded(false);
383        for case in exclusive_cases {
384            success &= run_case(case.as_ref(), &state, notifier)?;
385            if !success && opts.fail_fast {
386                break;
387            }
388        }
389    }
390
391    notifier.notify(notify::Event::SuiteComplete {
392        elapsed_s: notify::Elapsed(timer.elapsed()),
393    })?;
394
395    Ok(success)
396}
397
398fn run_case(
399    case: &dyn Case,
400    state: &State,
401    notifier: &mut dyn notify::Notifier,
402) -> std::io::Result<bool> {
403    notifier.notify(notify::Event::CaseStart {
404        name: case.name().to_owned(),
405    })?;
406    let timer = std::time::Instant::now();
407
408    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
409        __rust_begin_short_backtrace(|| case.run(state))
410    }))
411    .unwrap_or_else(|e| {
412        // The `panic` information is just an `Any` object representing the
413        // value the panic was invoked with. For most panics (which use
414        // `panic!` like `println!`), this is either `&str` or `String`.
415        let payload = e
416            .downcast_ref::<String>()
417            .map(|s| s.as_str())
418            .or_else(|| e.downcast_ref::<&str>().copied());
419
420        let msg = match payload {
421            Some(payload) => format!("test panicked: {payload}"),
422            None => "test panicked".to_owned(),
423        };
424        Err(RunError::fail(msg))
425    });
426
427    let err = outcome.as_ref().err();
428    let status = err.map(|e| e.status());
429    let message = err.and_then(|e| e.cause().map(|c| c.to_string()));
430    notifier.notify(notify::Event::CaseComplete {
431        name: case.name().to_owned(),
432        mode: RunMode::Test,
433        status,
434        message,
435        elapsed_s: Some(notify::Elapsed(timer.elapsed())),
436    })?;
437
438    Ok(status != Some(notify::RunStatus::Failed))
439}
440
441/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
442#[inline(never)]
443fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
444    let result = f();
445
446    // prevent this frame from being tail-call optimised away
447    std::hint::black_box(result)
448}
449
450#[derive(Clone, Debug)]
451struct SenderNotifier {
452    tx: std::sync::mpsc::Sender<notify::Event>,
453}
454
455impl notify::Notifier for SenderNotifier {
456    fn notify(&mut self, event: notify::Event) -> std::io::Result<()> {
457        // If the sender doesn't care, neither do we
458        let _ = self.tx.send(event);
459        Ok(())
460    }
461}