1 use std::{path::Path, iter::repeat_with, collections::HashMap};
2 use pretty_assertions::assert_eq;
3
4 use libtest_mimic::{run, Arguments, Conclusion, Trial};
5
6
7 const TEMPDIR: &str = env!("CARGO_TARGET_TMPDIR");
8
args<const N: usize>(args: [&str; N]) -> Arguments9 pub fn args<const N: usize>(args: [&str; N]) -> Arguments {
10 let mut v = vec!["<dummy-executable>"];
11 v.extend(args);
12 Arguments::from_iter(v)
13 }
14
do_run(mut args: Arguments, tests: Vec<Trial>) -> (Conclusion, String)15 pub fn do_run(mut args: Arguments, tests: Vec<Trial>) -> (Conclusion, String) {
16 // Create path to temporary file.
17 let suffix = repeat_with(fastrand::alphanumeric).take(10).collect::<String>();
18 let path = Path::new(&TEMPDIR).join(format!("libtest_mimic_output_{suffix}.txt"));
19
20 args.logfile = Some(path.display().to_string());
21
22 let c = run(&args, tests);
23 let output = std::fs::read_to_string(&path)
24 .expect("Can't read temporary logfile");
25 std::fs::remove_file(&path)
26 .expect("Can't remove temporary logfile");
27 (c, output)
28 }
29
clean_expected_log(s: &str) -> String30 pub fn clean_expected_log(s: &str) -> String {
31 let shared_indent = s.lines()
32 .filter(|l| l.contains(|c| c != ' '))
33 .map(|l| l.bytes().take_while(|b| *b == b' ').count())
34 .min()
35 .expect("empty expected");
36
37 let mut out = String::new();
38 for line in s.lines() {
39 use std::fmt::Write;
40 let cropped = if line.len() <= shared_indent {
41 line
42 } else {
43 &line[shared_indent..]
44 };
45 writeln!(out, "{}", cropped).unwrap();
46 }
47
48 out
49 }
50
51 /// Best effort tool to check certain things about a log that might have all
52 /// tests randomly ordered.
assert_reordered_log(actual: &str, num: u64, expected_lines: &[&str], tail: &str)53 pub fn assert_reordered_log(actual: &str, num: u64, expected_lines: &[&str], tail: &str) {
54 let actual = actual.trim();
55 let (first_line, rest) = actual.split_once('\n').expect("log has too few lines");
56 let (middle, last_line) = rest.rsplit_once('\n').expect("log has too few lines");
57
58
59 assert_eq!(first_line, &format!("running {} test{}", num, if num == 1 { "" } else { "s" }));
60 assert!(last_line.contains(tail));
61
62 let mut actual_lines = HashMap::new();
63 for line in middle.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
64 *actual_lines.entry(line).or_insert(0) += 1;
65 }
66
67 for expected in expected_lines.iter().map(|l| l.trim()).filter(|l| !l.is_empty()) {
68 match actual_lines.get_mut(expected) {
69 None | Some(0) => panic!("expected line \"{expected}\" not in log"),
70 Some(num) => *num -= 1,
71 }
72 }
73
74 actual_lines.retain(|_, v| *v != 0);
75 if !actual_lines.is_empty() {
76 panic!("Leftover output in log: {actual_lines:#?}");
77 }
78 }
79
80 /// Like `assert_eq`, but cleans the expected string (removes indendation).
81 #[macro_export]
82 macro_rules! assert_log {
83 ($actual:expr, $expected:expr) => {
84 let actual = $actual;
85 let expected = crate::common::clean_expected_log($expected);
86
87 assert_eq!(actual.trim(), expected.trim());
88 };
89 }
90
check( mut args: Arguments, mut tests: impl FnMut() -> Vec<Trial>, num_running_tests: u64, expected_conclusion: Conclusion, expected_output: &str, )91 pub fn check(
92 mut args: Arguments,
93 mut tests: impl FnMut() -> Vec<Trial>,
94 num_running_tests: u64,
95 expected_conclusion: Conclusion,
96 expected_output: &str,
97 ) {
98 // Run in single threaded mode
99 args.test_threads = Some(1);
100 let (c, out) = do_run(args.clone(), tests());
101 let expected = crate::common::clean_expected_log(expected_output);
102 let actual = {
103 let lines = out.trim().lines().skip(1).collect::<Vec<_>>();
104 lines[..lines.len() - 1].join("\n")
105 };
106 assert_eq!(actual.trim(), expected.trim());
107 assert_eq!(c, expected_conclusion);
108
109 // Run in multithreaded mode.
110 let (c, out) = do_run(args, tests());
111 assert_reordered_log(
112 &out,
113 num_running_tests,
114 &expected_output.lines().collect::<Vec<_>>(),
115 &conclusion_to_output(&c),
116 );
117 assert_eq!(c, expected_conclusion);
118 }
119
conclusion_to_output(c: &Conclusion) -> String120 fn conclusion_to_output(c: &Conclusion) -> String {
121 let Conclusion { num_filtered_out, num_passed, num_failed, num_ignored, num_measured } = *c;
122 format!(
123 "test result: {}. {} passed; {} failed; {} ignored; {} measured; {} filtered out;",
124 if num_failed > 0 { "FAILED" } else { "ok" },
125 num_passed,
126 num_failed,
127 num_ignored,
128 num_measured,
129 num_filtered_out,
130 )
131 }
132