aboutsummaryrefslogtreecommitdiff
path: root/.run/src/bin/test-examples.rs
diff options
context:
space:
mode:
Diffstat (limited to '.run/src/bin/test-examples.rs')
-rw-r--r--.run/src/bin/test-examples.rs158
1 files changed, 158 insertions, 0 deletions
diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs
new file mode 100644
index 0000000..539459e
--- /dev/null
+++ b/.run/src/bin/test-examples.rs
@@ -0,0 +1,158 @@
+use std::collections::HashMap;
+
+use colored::Colorize;
+use indicatif::ProgressBar;
+use serde::Deserialize;
+use tools::{eprintln_cargo_style, println_cargo_style};
+
+#[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();
+
+ // Count total test cases upfront
+ let total: usize = config.test.values().map(|cases| cases.len()).sum();
+ let bar = ProgressBar::new(total as u64);
+ bar.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ " Testing".bold().bright_cyan()
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ bar.set_message("examples");
+
+ let passed = run_all_tests(&config, &bar);
+
+ bar.finish_and_clear();
+
+ 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 number passed
+fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
+ let mut passed = 0;
+
+ for (example_name, test_cases) in &config.test {
+ bar.set_message(example_name.clone());
+
+ if !build_example(example_name) {
+ bar.inc(test_cases.len() as u64);
+ continue;
+ }
+
+ for test_case in test_cases {
+ if run_single_test(example_name, test_case, bar) {
+ passed += 1;
+ }
+ bar.inc(1);
+ }
+ }
+
+ passed
+}
+
+/// Build the example binary, return true on success
+fn build_example(example_name: &str) -> bool {
+ let manifest = format!("examples/{example_name}/Cargo.toml");
+ tools::run_cmd_capture(format!(
+ "cargo build --manifest-path {manifest} --color always",
+ ))
+ .is_ok()
+}
+
+/// Run a single test case, return true on pass
+fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> 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) => {
+ bar.println(format!("'{}' - 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 {
+ true
+ } else {
+ bar.println(format!("failed: '{}'", test_case.command));
+ if !exit_ok {
+ bar.println(format!(
+ " Expected exit code: {}, actual: {}",
+ test_case.expect.exit_code, actual_exit_code
+ ));
+ }
+ if !result_ok {
+ bar.println(format!(" Expected output: {:?}", test_case.expect.result));
+ bar.println(format!(" Actual stdout: {:?}", actual_stdout));
+ if !actual_stderr.is_empty() {
+ bar.println(format!(" 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()
+ }
+}