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 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 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 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 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 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 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 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#[inline(never)]
443fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
444 let result = f();
445
446 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 let _ = self.tx.send(event);
459 Ok(())
460 }
461}