• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
30 /// Removes shared indentation so that at least one line has no indentation
31 /// (no leading spaces).
clean_expected_log(s: &str) -> String32 pub fn clean_expected_log(s: &str) -> String {
33     let shared_indent = s.lines()
34         .filter(|l| l.contains(|c| c != ' '))
35         .map(|l| l.bytes().take_while(|b| *b == b' ').count())
36         .min()
37         .expect("empty expected");
38 
39     let mut out = String::new();
40     for line in s.lines() {
41         use std::fmt::Write;
42         let cropped = if line.len() <= shared_indent {
43             line
44         } else {
45             &line[shared_indent..]
46         };
47         writeln!(out, "{}", cropped).unwrap();
48     }
49 
50     out
51 }
52 
53 /// Best effort tool to check certain things about a log that might have all
54 /// tests randomly ordered.
assert_reordered_log(actual: &str, num: u64, expected_lines: &[&str], tail: &str)55 pub fn assert_reordered_log(actual: &str, num: u64, expected_lines: &[&str], tail: &str) {
56     let actual = actual.trim();
57     let (first_line, rest) = actual.split_once('\n').expect("log has too few lines");
58     let (middle, last_line) = rest.rsplit_once('\n').expect("log has too few lines");
59 
60 
61     assert_eq!(first_line, &format!("running {} test{}", num, if num == 1 { "" } else { "s" }));
62     assert!(last_line.contains(tail));
63 
64     let mut actual_lines = HashMap::new();
65     for line in middle.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
66         *actual_lines.entry(line).or_insert(0) += 1;
67     }
68 
69     for expected in expected_lines.iter().map(|l| l.trim()).filter(|l| !l.is_empty()) {
70         match actual_lines.get_mut(expected) {
71             None | Some(0) => panic!("expected line \"{expected}\" not in log"),
72             Some(num) => *num -= 1,
73         }
74     }
75 
76     actual_lines.retain(|_, v| *v != 0);
77     if !actual_lines.is_empty() {
78         panic!("Leftover output in log: {actual_lines:#?}");
79     }
80 }
81 
82 /// Like `assert_eq`, but cleans the expected string (removes indendation). Also
83 /// normalizes the "finished in" time if `$expected` ends with "finished in
84 /// 0.00s".
85 #[macro_export]
86 macro_rules! assert_log {
87     ($actual:expr, $expected:expr) => {
88         let mut actual = $actual.trim().to_owned();
89         let expected = crate::common::clean_expected_log($expected);
90         let expected = expected.trim();
91 
92         if expected.ends_with("finished in 0.00s") {
93             // If we don't find that pattern, the assert below will fail anyway.
94             if let Some(pos) = actual.rfind("finished in") {
95                 actual.truncate(pos);
96                 actual.push_str("finished in 0.00s");
97             }
98         }
99 
100         assert_eq!(actual, expected);
101     };
102 }
103 
check( mut args: Arguments, mut tests: impl FnMut() -> Vec<Trial>, num_running_tests: u64, expected_conclusion: Conclusion, expected_output: &str, )104 pub fn check(
105     mut args: Arguments,
106     mut tests: impl FnMut() -> Vec<Trial>,
107     num_running_tests: u64,
108     expected_conclusion: Conclusion,
109     expected_output: &str,
110 ) {
111     // Run in single threaded mode
112     args.test_threads = Some(1);
113     let (c, out) = do_run(args.clone(), tests());
114     let expected = crate::common::clean_expected_log(expected_output);
115     let actual = {
116         let lines = out.trim().lines().skip(1).collect::<Vec<_>>();
117         lines[..lines.len() - 1].join("\n")
118     };
119     assert_eq!(actual.trim(), expected.trim());
120     assert_eq!(c, expected_conclusion);
121 
122     // Run in multithreaded mode.
123     let (c, out) = do_run(args, tests());
124     assert_reordered_log(
125         &out,
126         num_running_tests,
127         &expected_output.lines().collect::<Vec<_>>(),
128         &conclusion_to_output(&c),
129     );
130     assert_eq!(c, expected_conclusion);
131 }
132 
conclusion_to_output(c: &Conclusion) -> String133 fn conclusion_to_output(c: &Conclusion) -> String {
134     let Conclusion { num_filtered_out, num_passed, num_failed, num_ignored, num_measured } = *c;
135     format!(
136         "test result: {}. {} passed; {} failed; {} ignored; {} measured; {} filtered out;",
137         if num_failed > 0 { "FAILED" } else { "ok" },
138         num_passed,
139         num_failed,
140         num_ignored,
141         num_measured,
142         num_filtered_out,
143     )
144 }
145