| use std::{path::Path, iter::repeat_with, collections::HashMap}; |
| use pretty_assertions::assert_eq; |
| |
| use libtest_mimic::{run, Arguments, Conclusion, Trial}; |
| |
| |
| const TEMPDIR: &str = env!("CARGO_TARGET_TMPDIR"); |
| |
| pub fn args<const N: usize>(args: [&str; N]) -> Arguments { |
| let mut v = vec!["<dummy-executable>"]; |
| v.extend(args); |
| Arguments::from_iter(v) |
| } |
| |
| pub fn do_run(mut args: Arguments, tests: Vec<Trial>) -> (Conclusion, String) { |
| // Create path to temporary file. |
| let suffix = repeat_with(fastrand::alphanumeric).take(10).collect::<String>(); |
| let path = Path::new(&TEMPDIR).join(format!("libtest_mimic_output_{suffix}.txt")); |
| |
| args.logfile = Some(path.display().to_string()); |
| |
| let c = run(&args, tests); |
| let output = std::fs::read_to_string(&path) |
| .expect("Can't read temporary logfile"); |
| std::fs::remove_file(&path) |
| .expect("Can't remove temporary logfile"); |
| (c, output) |
| } |
| |
| pub fn clean_expected_log(s: &str) -> String { |
| let shared_indent = s.lines() |
| .filter(|l| l.contains(|c| c != ' ')) |
| .map(|l| l.bytes().take_while(|b| *b == b' ').count()) |
| .min() |
| .expect("empty expected"); |
| |
| let mut out = String::new(); |
| for line in s.lines() { |
| use std::fmt::Write; |
| let cropped = if line.len() <= shared_indent { |
| line |
| } else { |
| &line[shared_indent..] |
| }; |
| writeln!(out, "{}", cropped).unwrap(); |
| } |
| |
| out |
| } |
| |
| /// Best effort tool to check certain things about a log that might have all |
| /// tests randomly ordered. |
| pub fn assert_reordered_log(actual: &str, num: u64, expected_lines: &[&str], tail: &str) { |
| let actual = actual.trim(); |
| let (first_line, rest) = actual.split_once('\n').expect("log has too few lines"); |
| let (middle, last_line) = rest.rsplit_once('\n').expect("log has too few lines"); |
| |
| |
| assert_eq!(first_line, &format!("running {} test{}", num, if num == 1 { "" } else { "s" })); |
| assert!(last_line.contains(tail)); |
| |
| let mut actual_lines = HashMap::new(); |
| for line in middle.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { |
| *actual_lines.entry(line).or_insert(0) += 1; |
| } |
| |
| for expected in expected_lines.iter().map(|l| l.trim()).filter(|l| !l.is_empty()) { |
| match actual_lines.get_mut(expected) { |
| None | Some(0) => panic!("expected line \"{expected}\" not in log"), |
| Some(num) => *num -= 1, |
| } |
| } |
| |
| actual_lines.retain(|_, v| *v != 0); |
| if !actual_lines.is_empty() { |
| panic!("Leftover output in log: {actual_lines:#?}"); |
| } |
| } |
| |
| /// Like `assert_eq`, but cleans the expected string (removes indendation). |
| #[macro_export] |
| macro_rules! assert_log { |
| ($actual:expr, $expected:expr) => { |
| let actual = $actual; |
| let expected = crate::common::clean_expected_log($expected); |
| |
| assert_eq!(actual.trim(), expected.trim()); |
| }; |
| } |
| |
| pub fn check( |
| mut args: Arguments, |
| mut tests: impl FnMut() -> Vec<Trial>, |
| num_running_tests: u64, |
| expected_conclusion: Conclusion, |
| expected_output: &str, |
| ) { |
| // Run in single threaded mode |
| args.test_threads = Some(1); |
| let (c, out) = do_run(args.clone(), tests()); |
| let expected = crate::common::clean_expected_log(expected_output); |
| let actual = { |
| let lines = out.trim().lines().skip(1).collect::<Vec<_>>(); |
| lines[..lines.len() - 1].join("\n") |
| }; |
| assert_eq!(actual.trim(), expected.trim()); |
| assert_eq!(c, expected_conclusion); |
| |
| // Run in multithreaded mode. |
| let (c, out) = do_run(args, tests()); |
| assert_reordered_log( |
| &out, |
| num_running_tests, |
| &expected_output.lines().collect::<Vec<_>>(), |
| &conclusion_to_output(&c), |
| ); |
| assert_eq!(c, expected_conclusion); |
| } |
| |
| fn conclusion_to_output(c: &Conclusion) -> String { |
| let Conclusion { num_filtered_out, num_passed, num_failed, num_ignored, num_measured } = *c; |
| format!( |
| "test result: {}. {} passed; {} failed; {} ignored; {} measured; {} filtered out;", |
| if num_failed > 0 { "FAILED" } else { "ok" }, |
| num_passed, |
| num_failed, |
| num_ignored, |
| num_measured, |
| num_filtered_out, |
| ) |
| } |