diff options
| -rw-r--r-- | dev_tools/Cargo.lock | 154 | ||||
| -rw-r--r-- | dev_tools/Cargo.toml | 2 | ||||
| -rw-r--r-- | dev_tools/src/bin/test-examples.rs | 139 | ||||
| -rw-r--r-- | examples/example-exit-code/src/main.rs | 7 | ||||
| -rw-r--r-- | examples/test-example-async.toml | 54 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 7 |
6 files changed, 357 insertions, 6 deletions
diff --git a/dev_tools/Cargo.lock b/dev_tools/Cargo.lock index 7bcc602..35d3a8f 100644 --- a/dev_tools/Cargo.lock +++ b/dev_tools/Cargo.lock @@ -12,6 +12,28 @@ dependencies = [ ] [[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] name = "just_fmt" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -27,15 +49,138 @@ dependencies = [ ] [[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] name = "tools" version = "0.1.0" dependencies = [ "colored", "just_fmt", "just_template", + "serde", + "toml", ] [[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -49,3 +194,12 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] diff --git a/dev_tools/Cargo.toml b/dev_tools/Cargo.toml index 7abb157..56f89d3 100644 --- a/dev_tools/Cargo.toml +++ b/dev_tools/Cargo.toml @@ -7,3 +7,5 @@ edition = "2024" just_template = "0.1.3" just_fmt = "0.1.2" colored = "3.1.1" +toml = "0.8" +serde = { version = "1", features = ["derive"] } diff --git a/dev_tools/src/bin/test-examples.rs b/dev_tools/src/bin/test-examples.rs new file mode 100644 index 0000000..fa3d8f3 --- /dev/null +++ b/dev_tools/src/bin/test-examples.rs @@ -0,0 +1,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-example-async.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/{}/Cargo.toml", example_name); + 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!("{}.exe", base) + } else { + base.to_string() + } +} diff --git a/examples/example-exit-code/src/main.rs b/examples/example-exit-code/src/main.rs index f62ad22..1fe5424 100644 --- a/examples/example-exit-code/src/main.rs +++ b/examples/example-exit-code/src/main.rs @@ -13,7 +13,7 @@ use mingling::{ macros::{chain, dispatcher, gen_program, pack, r_println, renderer}, - res::update_exit_code, + res::{exit_code, update_exit_code}, setup::ExitCodeSetup, }; @@ -21,7 +21,7 @@ fn main() { let mut program = ThisProgram::new(); program.with_dispatcher(ErrorCommand); program.with_setup(ExitCodeSetup::<ThisProgram>::default()); - program.exec(); + program.exec_and_exit(); } dispatcher!("error", ErrorCommand => ErrorEntry); @@ -35,7 +35,8 @@ fn handle_error_entry(_prev: ErrorEntry) -> NextProcess { #[renderer] fn render_error(_prev: ResultError) { - r_println!("Error!"); + let exit_code = exit_code::<ThisProgram>(); + r_println!("Exit with exit code: {}", exit_code); } gen_program!(); diff --git a/examples/test-example-async.toml b/examples/test-example-async.toml new file mode 100644 index 0000000..a63f566 --- /dev/null +++ b/examples/test-example-async.toml @@ -0,0 +1,54 @@ +[[test.example-async]] +command = "hello World" +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[test.example-basic]] +command = "hello World" +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[test.example-exit-code]] +command = "error" +expect.exit-code = 1 +expect.result = "Exit with exit code: 1" + +[[test.example-general-renderer]] +command = "render Bob 22" +expect.exit-code = 0 +expect.result = "Bob is 22 years old" + +[[test.example-general-renderer]] +command = "render Bob 22 --json" +expect.exit-code = 0 +expect.result = "{\"member_name\":\"Bob\",\"member_age\":22}" + +[[test.example-general-renderer]] +command = "render Bob 22 --yaml" +expect.exit-code = 0 +expect.result = "member_name: Bob\nmember_age: 22" + +[[test.example-picker]] +command = "pick Bob" +expect.exit-code = 0 +expect.result = "Picked: name = Bob, age = 20" + +[[test.example-picker]] +command = "pick Bob --age -15" +expect.exit-code = 0 +expect.result = "Picked: name = Bob, age = 0" + +[[test.example-picker]] +command = "pick --age 99" +expect.exit-code = 0 +expect.result = "No name provided." + +[[test.example-picker]] +command = "pick" +expect.exit-code = 0 +expect.result = "No name provided." + +[[test.example-picker]] +command = "pick --age 150" +expect.exit-code = 0 +expect.result = "No name provided." diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 1669344..ba61f92 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -371,7 +371,7 @@ pub mod example_dispatch_tree {} /// ```ignore /// use mingling::{ /// macros::{chain, dispatcher, gen_program, pack, r_println, renderer}, -/// res::update_exit_code, +/// res::{exit_code, update_exit_code}, /// setup::ExitCodeSetup, /// }; /// @@ -379,7 +379,7 @@ pub mod example_dispatch_tree {} /// let mut program = ThisProgram::new(); /// program.with_dispatcher(ErrorCommand); /// program.with_setup(ExitCodeSetup::<ThisProgram>::default()); -/// program.exec(); +/// program.exec_and_exit(); /// } /// /// dispatcher!("error", ErrorCommand => ErrorEntry); @@ -393,7 +393,8 @@ pub mod example_dispatch_tree {} /// /// #[renderer] /// fn render_error(_prev: ResultError) { -/// r_println!("Error!"); +/// let exit_code = exit_code::<ThisProgram>(); +/// r_println!("Exit with exit code: {}", exit_code); /// } /// /// gen_program!(); |
