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