1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
use std::collections::HashMap;
use serde::Deserialize;
use tools::{eprintln_cargo_style, println_cargo_style, run_cmd};
#[derive(Deserialize)]
struct TestConfig {
test: HashMap<String, Vec<TestCase>>,
}
#[derive(Deserialize)]
struct TestCase {
command: String,
expect: Expect,
}
#[derive(Deserialize)]
struct Expect {
#[serde(rename = "exit-code")]
exit_code: i32,
result: String,
}
fn main() {
#[cfg(windows)]
let _ = colored::control::set_virtual_terminal(true);
let config = load_config();
let (passed, total) = run_all_tests(&config);
println_cargo_style!("Result: {}/{} tests passed", passed, total);
if passed != total {
eprintln_cargo_style!("{} test(s) failed", total - passed);
std::process::exit(1);
}
}
/// Parse test config from TOML file
fn load_config() -> TestConfig {
let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| {
eprintln_cargo_style!("Failed to read TOML config file: {}", e);
std::process::exit(1);
});
toml::from_str(&content).unwrap_or_else(|e| {
eprintln_cargo_style!("Failed to parse TOML config: {}", e);
std::process::exit(1);
})
}
/// Run all example test groups, return (passed, total)
fn run_all_tests(config: &TestConfig) -> (usize, usize) {
let mut total = 0;
let mut passed = 0;
for (example_name, test_cases) in &config.test {
println_cargo_style!("Test: {}", example_name);
if !build_example(example_name) {
total += test_cases.len();
continue;
}
for test_case in test_cases {
total += 1;
if run_single_test(example_name, test_case) {
passed += 1;
}
}
}
(passed, total)
}
/// Build the example binary, return true on success
fn build_example(example_name: &str) -> bool {
let manifest = format!("examples/{example_name}/Cargo.toml");
run_cmd!("cargo build --manifest-path {}", manifest).is_ok()
}
/// Run a single test case, return true on pass
fn run_single_test(example_name: &str, test_case: &TestCase) -> bool {
let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name));
let args: Vec<&str> = test_case.command.split_whitespace().collect();
let output = match std::process::Command::new(&binary_path)
.args(&args)
.output()
{
Ok(o) => o,
Err(e) => {
eprintln_cargo_style!("'{}' - failed to run: {}", test_case.command, e);
return false;
}
};
let actual_exit_code = output.status.code().unwrap_or(-1);
let actual_stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let actual_stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let exit_ok = actual_exit_code == test_case.expect.exit_code;
let result_ok = actual_stdout == test_case.expect.result
|| actual_stdout.contains(&test_case.expect.result);
if exit_ok && result_ok {
println_cargo_style!("Passed: '{}'", test_case.command);
true
} else {
eprintln_cargo_style!("'{}'", test_case.command);
if !exit_ok {
eprintln_cargo_style!(
"Expected exit code: {}, actual: {}",
test_case.expect.exit_code,
actual_exit_code
);
}
if !result_ok {
eprintln_cargo_style!("Expected output: {:?}", test_case.expect.result);
eprintln_cargo_style!("Actual stdout: {:?}", actual_stdout);
if !actual_stderr.is_empty() {
eprintln_cargo_style!("Actual stderr: {:?}", actual_stderr);
}
}
false
}
}
/// Resolve binary filename for the given example
///
/// The binary name matches the package name. On Windows, the `.exe` suffix is required.
fn get_binary_name(example_name: &str) -> String {
let base = example_name;
if cfg!(target_os = "windows") {
format!("{base}.exe")
} else {
base.to_string()
}
}
|