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