diff options
36 files changed, 953 insertions, 496 deletions
diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs index 1b2342e..ff2c3fc 100644 --- a/.run/src/bin/cov-test.rs +++ b/.run/src/bin/cov-test.rs @@ -18,7 +18,6 @@ //! cargo install --git https://github.com/Weicao-CatilGrass/cargo-llvm-cov cargo-llvm-cov //! ``` -use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -34,15 +33,16 @@ const OUTPUT_DIR: &str = "docs/cov-test"; /// in one place so the final `report` can merge everything. const COV_TARGET_DIR: &str = ".temp/cov-llvm"; -/// Parsed `examples/test-examples.toml` (`test.<example> = [ { command, expect } ]`). +/// An example's `test.toml` (`[[runs]]` entries). #[derive(Deserialize)] struct TestConfig { - test: HashMap<String, Vec<TestCase>>, + runs: Vec<TestCase>, } +/// One `[[runs]]` entry of an example's `test.toml`. #[derive(Deserialize)] struct TestCase { - command: String, + input: Vec<String>, } fn main() { @@ -108,7 +108,7 @@ fn main() { } // 3. Examples: build each example with explicit RUSTFLAGS, then execute - // every command from test-examples.toml directly. + // every command declared in the example's test.toml directly. // // NOTE: `cargo llvm-cov run` cannot be used here. Its rustc wrapper // only instruments the crates of the *current* cargo project (with @@ -127,7 +127,7 @@ fn main() { } let examples = load_example_commands(&repo_root); let mut built = std::collections::HashSet::new(); - for (example, command) in &examples { + for (example, input) in &examples { if built.insert(example.clone()) { println_cargo_style!("Building: {}", example); run_cmd!(format!( @@ -150,7 +150,7 @@ fn main() { example ); match std::process::Command::new(&binary) - .args(command.split_whitespace()) + .args(input) .env("LLVM_PROFILE_FILE", &profraw) .status() { @@ -279,22 +279,43 @@ fn find_test_crate_manifests(repo_root: &Path) -> Vec<PathBuf> { manifests } -/// Parse `examples/test-examples.toml` into `(example_name, command)` pairs. -fn load_example_commands(repo_root: &Path) -> Vec<(String, String)> { - let content = - fs::read_to_string(repo_root.join("examples/test-examples.toml")).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to read examples/test-examples.toml: {}", e); +/// Parse every `examples/<name>/test.toml` into `(example_name, input)` pairs. +fn load_example_commands(repo_root: &Path) -> Vec<(String, Vec<String>)> { + let examples_dir = repo_root.join("examples"); + let mut entries: Vec<_> = std::fs::read_dir(&examples_dir) + .unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", examples_dir.display(), e); std::process::exit(1); - }); - let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to parse examples/test-examples.toml: {}", e); - std::process::exit(1); - }); + }) + .flatten() + .collect(); + entries.sort_by_key(|e| e.file_name()); let mut pairs = Vec::new(); - for (example, cases) in &config.test { - for case in cases { - pairs.push((example.clone(), case.command.clone())); + for entry in entries { + let path = entry.path(); + if !path.is_dir() { + continue; + } + let test_toml = path.join("test.toml"); + if !test_toml.is_file() { + continue; + } + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let content = fs::read_to_string(&test_toml).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + for case in config.runs { + pairs.push((name.clone(), case.input)); } } pairs diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs index 539459e..617a745 100644 --- a/.run/src/bin/test-examples.rs +++ b/.run/src/bin/test-examples.rs @@ -1,18 +1,20 @@ -use std::collections::HashMap; +use std::path::Path; use colored::Colorize; use indicatif::ProgressBar; use serde::Deserialize; -use tools::{eprintln_cargo_style, println_cargo_style}; +use tools::{eprintln_cargo_style, println_cargo_style, run_parallel}; +/// An example's `test.toml` (`[[runs]]` entries). #[derive(Deserialize)] struct TestConfig { - test: HashMap<String, Vec<TestCase>>, + runs: Vec<TestCase>, } +/// A single `[[runs]]` entry of an example's `test.toml`. #[derive(Deserialize)] struct TestCase { - command: String, + input: Vec<String>, expect: Expect, } @@ -27,10 +29,16 @@ fn main() { #[cfg(windows)] let _ = colored::control::set_virtual_terminal(true); - let config = load_config(); + let configs = load_all_test_configs(); - // Count total test cases upfront - let total: usize = config.test.values().map(|cases| cases.len()).sum(); + // Phase 1: build all examples in parallel. + if let Err(code) = build_all_examples(&configs) { + // `run_parallel` already printed every failed build above. + std::process::exit(code); + } + + // Phase 2: run the tests serially against the pre-built binaries. + let total: usize = configs.iter().map(|(_, cases)| cases.len()).sum(); let bar = ProgressBar::new(total as u64); bar.set_style( indicatif::ProgressStyle::default_bar() @@ -43,7 +51,7 @@ fn main() { ); bar.set_message("examples"); - let passed = run_all_tests(&config, &bar); + let passed = run_all_tests(&configs, &bar); bar.finish_and_clear(); @@ -55,31 +63,71 @@ fn main() { } } -/// 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); +/// Load `examples/<name>/test.toml` for every example that has one, in +/// alphabetical order of the example directory name. +fn load_all_test_configs() -> Vec<(String, Vec<TestCase>)> { + let examples_dir = Path::new("examples"); + let mut configs = Vec::new(); + + let entries = std::fs::read_dir(examples_dir).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read examples dir: {}", 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); - }) + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + let test_toml = path.join("test.toml"); + if !test_toml.is_file() { + continue; + } + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let content = std::fs::read_to_string(&test_toml).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + configs.push((name, config.runs)); + } + + configs.sort_by(|a, b| a.0.cmp(&b.0)); + configs } -/// Run all example test groups, return number passed -fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize { +/// Phase 1: build every example that has a `test.toml` in parallel. +/// +/// Build tasks are spawned in parallel (like `ci.rs`'s `build_all`); on any +/// build failure the whole run aborts with the first failure's exit code. +fn build_all_examples(configs: &[(String, Vec<TestCase>)]) -> Result<(), i32> { + let tasks: Vec<(String, String, String)> = configs + .iter() + .map(|(name, _)| { + ( + format!("Build: {name}"), + name.clone(), + format!("cargo build --manifest-path examples/{name}/Cargo.toml --color always"), + ) + }) + .collect(); + run_parallel("Building", tasks) +} + +/// Phase 2: run all example test groups serially, return number passed +fn run_all_tests(configs: &[(String, Vec<TestCase>)], bar: &ProgressBar) -> usize { let mut passed = 0; - for (example_name, test_cases) in &config.test { + for (example_name, test_cases) in configs { 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; @@ -91,27 +139,18 @@ fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize { 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 command = test_case.input.join(" "); let output = match std::process::Command::new(&binary_path) - .args(&args) + .args(&test_case.input) .output() { Ok(o) => o, Err(e) => { - bar.println(format!("'{}' - failed to run: {}", test_case.command, e)); + bar.println(format!("'{command}' - failed to run: {e}")); return false; } }; @@ -127,7 +166,7 @@ fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) if exit_ok && result_ok { true } else { - bar.println(format!("failed: '{}'", test_case.command)); + bar.println(format!("failed: '{command}'")); if !exit_ok { bar.println(format!( " Expected exit code: {}, actual: {}", diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ba50c..713e7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,35 @@ None The suggestion collection was also reworked: it now uses a `BTreeSet<SuggestItem>` for natural ordering and deduplication (replacing the previous `Vec<String>` + manual `sort()`/`dedup()`), carries both the suggested token and the fully-qualified owner node path used for the description lookup, and returns `Suggest::Suggest(suggestions)` directly. `entry_description` returns `None` for intermediate trie segments that have no entry of their own or entries without a registered `Description`, in which case suggestions fall back to plain `SuggestItem::new(token)`. The final empty-suggestions fallback to `file_suggest()` is unchanged. +12. **[`core:render`]** Reworked the `RenderResult` immediate-output mechanism from a single boolean flag into a general **print-hook** system, enabling multiple user-defined hooks to be invoked with the content and output mode of every write. + + **`RenderResultPrint` struct** — Added a new public struct bundling the emitted text content and its output mode: + + - **`content: String`** — The raw text written. For `println`/`eprintln` it includes the trailing newline; for `print`/`eprint` it is exactly the given text. + - **`mode: RenderResultMode`** — The output mode (`Stdout` or `Stderr`) the content was written with, telling the hook where the content belongs. + + Derives `Debug`, `Clone`, `PartialEq`, `Eq`. + + **`print_hook` field** — Replaced `immediate_output: bool` with `print_hook: PrintHook` (a `Vec<Box<dyn FnMut(RenderResultPrint)>>` inside an `Option`). The default is `None`, meaning content is only buffered and output uniformly at the end (e.g. via `std_print`). + + **`bind_print_hook()` method** — New method that pushes a user-provided hook onto the hook list; multiple hooks can be bound and are invoked in binding order. Returns `&mut Self` for chaining. + + **`immediate_output()` behavior change** — Now calls `bind_print_hook()` with a hook that flushes content to stdout/stderr in real time (functionally identical to the old boolean behavior, but implemented via the hook mechanism). No longer `const`. + + **`emit()` private helper** — Iterates bound hooks and invokes each with a `RenderResultPrint { content, mode }` value. + + **Write methods updated** — `print`, `println`, `eprint`, and `eprintln` now call `self.emit(&text, Stdout/Stderr)` (after formatting the trailing newline for `println`/`eprintln`) instead of checking the `immediate_output` flag. + + **`append_other()` semantics** — Now checks whether _self has hooks_ and _other has none_; when true, other's buffered content is emitted through self's hooks while being appended. The other's hooks and `exit_code` are **not** transferred — only its buffered content is merged. + + **Manual trait impls** — Since hooks are opaque closures that cannot be cloned or meaningfully compared, hand-written impls replaced the derives: + + - **`Clone`** — Clones the buffered content and exit code but drops the print hooks (creates a result with no hooks). + - **`PartialEq` / `Eq`** — Compares only the render buffer and exit code; hooks are ignored. + - **`Debug`** — Prints the buffered content, exit code, and the _number_ of bound hooks (`print_hooks: Vec::len`), avoiding attempting to format opaque closures. + + `Default` is still derived (all fields default to empty/`None`). + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e729295..779bc3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ Before contributing, we recommend reading [README](README.md) to get an overview ## 1. Project Structure 📦 -| Category | Path/Name | Description | -| --------------------------- | -------------------- | ------------------------------------------------------------------ | -| **Entry crate** | `mingling/` | Project entry point | -| **Core library** | `mingling_core/` | Imported as an external dependency | -| **Macro library** | `mingling_macros/` | Imported as an external dependency | -| **Mingling Pathfinder** | `mingling_pathf/` | Build-time module path resolution for types | -| **Mingling Picker2** | `arg_picker/` | Mingling Arguments Parser | -| **Mingling Picker2 Macros** | `arg_picker_macros/` | Mingling Arguments Parser Macros | -| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | -| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | -| **Documents** | `docs/` | All documents | -| **Dev Documents** | `docs/dev/` | Internal documents | -| **Resources** | `docs/res/` | All resources | -| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | -| **CI** | `.run/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | -| **Temporary files** | `.temp/` | Ignored by `.gitignore` | +| Category | Path/Name | Description | +| --------------------------- | -------------------- | -------------------------------------------------------------------- | +| **Entry crate** | `mingling/` | Project entry point | +| **Core library** | `mingling_core/` | Imported as an external dependency | +| **Macro library** | `mingling_macros/` | Imported as an external dependency | +| **Mingling Pathfinder** | `mingling_pathf/` | Build-time module path resolution for types | +| **Mingling Picker2** | `arg_picker/` | Mingling Arguments Parser | +| **Mingling Picker2 Macros** | `arg_picker_macros/` | Mingling Arguments Parser Macros | +| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | +| **Examples** | `examples/` | To add expected output tests, add a `test.toml` in the example's dir | +| **Documents** | `docs/` | All documents | +| **Dev Documents** | `docs/dev/` | Internal documents | +| **Resources** | `docs/res/` | All resources | +| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | +| **CI** | `.run/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | +| **Temporary files** | `.temp/` | Ignored by `.gitignore` | ## 2. How to Contribute @@ -51,7 +51,20 @@ tags = ["tag1", "tag2"] # Tags (optional) files = ["Cargo.toml", "src/main.rs"] ``` -If you change expected behavior, update the test assertions in `examples/test-examples.toml`. +Optionally, each example may contain a `test.toml` file declaring expected output tests, which are executed by CI (`./run.sh test-examples`): + +```toml +[[runs]] +input = ["greet", "Alice"] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" +``` + +- `input` is the list of CLI arguments passed to the example binary +- `expect.exit-code` / `expect.result` assert the expected process exit code and stdout output + +If you change expected behavior, update the assertions in the example's `test.toml`. After editing examples, run these scripts to keep things in sync: diff --git a/docs/dev/pages/abouts/ci.md b/docs/dev/pages/abouts/ci.md index a9be500..9f638d7 100644 --- a/docs/dev/pages/abouts/ci.md +++ b/docs/dev/pages/abouts/ci.md @@ -26,16 +26,16 @@ cargo ci Every CI step is an independent switch (`--check-*`). Running `cargo ci` with no options executes **all** steps in the order below; pass one or more `--check-*` flags to run only the selected steps. -| Step | Flag | What it does | -| --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| Build | `--check-build` | Recursively finds all `Cargo.toml` files and runs `cargo build` for each crate in parallel (workspace members build with all documented features). | -| Clippy | `--check-clippy` | Runs `cargo clippy ... -- -D warnings` for every crate in parallel; any warning fails the check. | -| Test | `--check-test` | Runs `cargo test` for every crate in parallel (workspace tests run with all documented features; `arg-picker` is excluded). | -| Arg picker | `--check-arg-picker` | Runs `cargo test -p arg-picker` with its default features. | -| Markdown code | `--check-markdown-code` | Runs the `test-all-markdown-code` tool to verify code blocks in all `*.md` files compile. See [ABOUT_CODE_VERIFY](docs/_ABOUT_CODE_VERIFY.md). | -| Examples | `--check-examples` | Runs the `test-examples` tool to verify all examples behave as expected. | -| Docs up to date | `--check-docs-refresh` | Runs the documentation refresh tools and `cargo fmt`, then fails if the working tree is no longer clean (i.e. the docs were stale). | -| API docs | `--check-api-docs` | Builds API docs with the `[package.metadata.docs.rs]` features and fails if `docs/api-docs/` is out of date. | +| Step | Flag | What it does | +| --------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Build | `--check-build` | Recursively finds all `Cargo.toml` files and runs `cargo build` for each crate in parallel (workspace members build with all documented features). | +| Clippy | `--check-clippy` | Runs `cargo clippy ... -- -D warnings` for every crate in parallel; any warning fails the check. | +| Test | `--check-test` | Runs `cargo test` for every crate in parallel (workspace tests run with all documented features; `arg-picker` is excluded). | +| Arg picker | `--check-arg-picker` | Runs `cargo test -p arg-picker` with its default features. | +| Markdown code | `--check-markdown-code` | Runs the `test-all-markdown-code` tool to verify code blocks in all `*.md` files compile. See [ABOUT_CODE_VERIFY](docs/_ABOUT_CODE_VERIFY.md). | +| Examples | `--check-examples` | Runs the `test-examples` tool to verify all examples behave as expected. Each example declares its expected output tests in `examples/<example>/test.toml`. | +| Docs up to date | `--check-docs-refresh` | Runs the documentation refresh tools and `cargo fmt`, then fails if the working tree is no longer clean (i.e. the docs were stale). | +| API docs | `--check-api-docs` | Builds API docs with the `[package.metadata.docs.rs]` features and fails if `docs/api-docs/` is out of date. | ### Docs up to date in detail @@ -49,6 +49,15 @@ Every CI step is an independent switch (`--check-*`). Running `cargo ci` with no Finally, it runs `cargo fmt` to unify code formatting. Because the refresh tools regenerate derived files, running this check against stale documentation modifies the working tree — and `ci.rs` fails the run in that case. (Using `--dirty` skips the cleanliness check, which makes this flag behave like a plain "refresh docs" command.) +### Examples in detail + +`--check-examples` runs the `test-examples` tool in two phases: + +1. **Build** — every example that has a `test.toml` is built in parallel (one `cargo build` task per example, reusing the shared `.temp/target` cache). +2. **Test** — each `[[runs]]` entry in `examples/*/test.toml` is executed serially against the pre-built binary, asserting the CLI arguments (`input`) and the expected `exit-code` / `result`. + +An example that changes its behavior only needs its own `test.toml` updated. + ### Combining steps When several `--check-*` flags are combined, the steps run in the order listed above. In "run all" mode (no flags given), the documentation steps all execute even if one of them fails, so every problem is reported in a single run. diff --git a/docs/dev/pages/issues/0.5.0-roadmap.md b/docs/dev/pages/issues/0.5.0-roadmap.md index 0312193..5238cae 100644 --- a/docs/dev/pages/issues/0.5.0-roadmap.md +++ b/docs/dev/pages/issues/0.5.0-roadmap.md @@ -108,6 +108,31 @@ version = "0.4.0" features = [ "build", "pathf" ] # No `dispatch_tree` feature; `pathf` no longer needs to consider its branches ``` +8. **Feature:** A new macro designed for `pathf`: `#[pathf_export(type::TypePath)]` + +`pathf` has been around since 0.2.0 and has worked well for a long time, with many edge cases resolved. However, it still lacks an escape hatch — "when certain indirect expansions cannot be recognized by `pathf`, how can we assist its inference?" + +I plan to introduce a new attribute macro to supplement `pathf`'s path inference. + +```rust +#[macro_export] +macro_rules! repack { + ($name:ident) => { + // Ignored! This section cannot be parsed by pathf. + #[mingling::macros::pathf_ignore] + #[derive(mingling::Grouped)] + pub struct $name; + }; +} + +// The expansion contains macros that need to be parsed by pathf +#[pathf_export(MyType)] // Explicitly specified to assist pathf's inference +repack!(MyType); +``` + +> [!Note] +> Haha, hopefully we'll never have to use it. + <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass </p> diff --git a/examples/example-argument-parse/test.toml b/examples/example-argument-parse/test.toml new file mode 100644 index 0000000..a405bd9 --- /dev/null +++ b/examples/example-argument-parse/test.toml @@ -0,0 +1,29 @@ +[[runs]] +input = [ "transfer" ] + +expect.exit-code = 0 +expect.result = "file: (1048576)" + +[[runs]] +input = [ "transfer", "--dir", "src" ] + +expect.exit-code = 0 +expect.result = "dir: src (1048576)" + +[[runs]] +input = [ "transfer", "--size", "500", "myfile.txt" ] + +expect.exit-code = 0 +expect.result = "file: myfile.txt (500)" + +[[runs]] +input = [ "strict-transfer", "README.md" ] + +expect.exit-code = 0 +expect.result = "file: README.md (1048576)" + +[[runs]] +input = [ "strict-transfer" ] + +expect.exit-code = 0 +expect.result = "Error: name is not provided" diff --git a/examples/example-argument-picker/test.toml b/examples/example-argument-picker/test.toml new file mode 100644 index 0000000..a67ffbe --- /dev/null +++ b/examples/example-argument-picker/test.toml @@ -0,0 +1,41 @@ +[[runs]] +input = [ "calc", "1", "+", "1" ] + +expect.exit-code = 0 +expect.result = "Result: 2" + +[[runs]] +input = [ "calc", "7", "*", "7" ] + +expect.exit-code = 0 +expect.result = "Result: 49" + +[[runs]] +input = [ "calc" ] + +expect.exit-code = 0 +expect.result = "Error: First number (number_a) was not provided." + +[[runs]] +input = [ "calc", "1" ] + +expect.exit-code = 0 +expect.result = "Error: Operator was not provided." + +[[runs]] +input = [ "calc", "1", "+" ] + +expect.exit-code = 0 +expect.result = "Error: Second number (number_b) was not provided." + +[[runs]] +input = [ "calc", "4", "/", "3" ] + +expect.exit-code = 0 +expect.result = "Result: 1.3333334" + +[[runs]] +input = [ "calc", "4", "/", "3", "--round" ] + +expect.exit-code = 0 +expect.result = "Result: 1" diff --git a/examples/example-async-support/test.toml b/examples/example-async-support/test.toml new file mode 100644 index 0000000..735e115 --- /dev/null +++ b/examples/example-async-support/test.toml @@ -0,0 +1,5 @@ +[[runs]] +input = [ "download", "readme.txt" ] + +expect.exit-code = 0 +expect.result = "\"readme.txt\" downloaded." diff --git a/examples/example-basic/test.toml b/examples/example-basic/test.toml new file mode 100644 index 0000000..fb4f1de --- /dev/null +++ b/examples/example-basic/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "greet", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" diff --git a/examples/example-clap-binding/test.toml b/examples/example-clap-binding/test.toml new file mode 100644 index 0000000..a12daa0 --- /dev/null +++ b/examples/example-clap-binding/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "greet", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" + +[[runs]] +input = [ "greet", "Alice", "-r", "5" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice, Alice, Alice, Alice, Alice!" diff --git a/examples/example-combine-pathf-dispatch-tree/test.toml b/examples/example-combine-pathf-dispatch-tree/test.toml new file mode 100644 index 0000000..1fd14d9 --- /dev/null +++ b/examples/example-combine-pathf-dispatch-tree/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "hello", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" + +[[runs]] +input = [ "hello" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" diff --git a/examples/example-combine-pathf-metadata/test.toml b/examples/example-combine-pathf-metadata/test.toml new file mode 100644 index 0000000..2803cee --- /dev/null +++ b/examples/example-combine-pathf-metadata/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "hello" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "hello", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" + +[[runs]] +input = [ "desc" ] + +expect.exit-code = 0 +expect.result = "EntryHello desc = okay" diff --git a/examples/example-command-macro/test.toml b/examples/example-command-macro/test.toml new file mode 100644 index 0000000..215cbb3 --- /dev/null +++ b/examples/example-command-macro/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "hello", "world" ] + +expect.exit-code = 0 +expect.result = "Hello, World" + +[[runs]] +input = [ "greet-someone", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice" + +[[runs]] +input = [ "goodbye" ] + +expect.exit-code = 0 +expect.result = "Goodbye!" diff --git a/examples/example-completion/test.toml b/examples/example-completion/test.toml new file mode 100644 index 0000000..ccb0e46 --- /dev/null +++ b/examples/example-completion/test.toml @@ -0,0 +1,5 @@ +[[runs]] +input = [ "greet", "World", "--repeat", "1" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" diff --git a/examples/example-custom-pickable/test.toml b/examples/example-custom-pickable/test.toml new file mode 100644 index 0000000..8fc20eb --- /dev/null +++ b/examples/example-custom-pickable/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "connect", "192.168.1.1:8080" ] + +expect.exit-code = 0 +expect.result = "Connected to \"192.168.1.1:8080\"" + +[[runs]] +input = [ "connect" ] + +expect.exit-code = 0 +expect.result = "Failed to parse address" + +[[runs]] +input = [ "connect", "invalid" ] + +expect.exit-code = 0 +expect.result = "Failed to parse address" diff --git a/examples/example-dispatch-tree/test.toml b/examples/example-dispatch-tree/test.toml new file mode 100644 index 0000000..d455c48 --- /dev/null +++ b/examples/example-dispatch-tree/test.toml @@ -0,0 +1,5 @@ +[[runs]] +input = [ "cmd5" ] + +expect.exit-code = 0 +expect.result = "It's works!" diff --git a/examples/example-enum-tag/test.toml b/examples/example-enum-tag/test.toml new file mode 100644 index 0000000..ca83b7f --- /dev/null +++ b/examples/example-enum-tag/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "lang-select" ] + +expect.exit-code = 0 +expect.result = "Selected: Rust" + +[[runs]] +input = [ "lang-select", "Python" ] + +expect.exit-code = 0 +expect.result = "Selected: Python" + +[[runs]] +input = [ "lang-select", "OCaml" ] + +expect.exit-code = 0 +expect.result = "Selected: OCaml" diff --git a/examples/example-error-handling/test.toml b/examples/example-error-handling/test.toml new file mode 100644 index 0000000..9df5f6f --- /dev/null +++ b/examples/example-error-handling/test.toml @@ -0,0 +1,29 @@ +[[runs]] +input = [ "hello" ] + +expect.exit-code = 0 +expect.result = "No name provided" + +[[runs]] +input = [ "hello", "MyBestFriendAlice" ] + +expect.exit-code = 0 +expect.result = "Name too long: 17 > 10" + +[[runs]] +input = [ "hello", "Alice" ] + +expect.exit-code = 0 +expect.result = "Name not available" + +[[runs]] +input = [ "hello", "Peter" ] + +expect.exit-code = 0 +expect.result = "Hello, Peter" + +[[runs]] +input = [ "hallo" ] + +expect.exit-code = 0 +expect.result = "Command not found: \"hallo\"" diff --git a/examples/example-exitcode/test.toml b/examples/example-exitcode/test.toml new file mode 100644 index 0000000..4a4989d --- /dev/null +++ b/examples/example-exitcode/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "hello", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice" + +[[runs]] +input = [ "hello" ] + +expect.exit-code = 1 +expect.result = "No name provided (with exit code 1)" + +[[runs]] +input = [ "hello", "--help" ] + +expect.exit-code = 2 +expect.result = "Usage: hello <NAME>" diff --git a/examples/example-help/test.toml b/examples/example-help/test.toml new file mode 100644 index 0000000..d1ec550 --- /dev/null +++ b/examples/example-help/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "" + +[[runs]] +input = [ "greet", "--help" ] + +expect.exit-code = 0 +expect.result = "Usage: greet <NAME>" diff --git a/examples/example-hook/test.toml b/examples/example-hook/test.toml new file mode 100644 index 0000000..fb4f1de --- /dev/null +++ b/examples/example-hook/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "greet", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" diff --git a/examples/example-lazy-resources/test.toml b/examples/example-lazy-resources/test.toml new file mode 100644 index 0000000..c8729a7 --- /dev/null +++ b/examples/example-lazy-resources/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "none" ] + +expect.exit-code = 0 +expect.result = "None" + +[[runs]] +input = [ "show" ] + +expect.exit-code = 0 +expect.result = "Initialized\nbaz: qux\nfoo: bar\nhello: world\nkey: value\nrust: lang" diff --git a/examples/example-metadata/test.toml b/examples/example-metadata/test.toml new file mode 100644 index 0000000..0cd0ce1 --- /dev/null +++ b/examples/example-metadata/test.toml @@ -0,0 +1,23 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "greet", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" + +[[runs]] +input = [ "desc" ] + +expect.exit-code = 0 +expect.result = "EntryGreet desc = ok" + +[[runs]] +input = [ "nodoc" ] + +expect.exit-code = 0 +expect.result = "EntryDescription has no description" diff --git a/examples/example-outside-type/test.toml b/examples/example-outside-type/test.toml new file mode 100644 index 0000000..50eaa90 --- /dev/null +++ b/examples/example-outside-type/test.toml @@ -0,0 +1,17 @@ +[[runs]] +input = [ "parse", "42" ] + +expect.exit-code = 0 +expect.result = "Parsed number: 42" + +[[runs]] +input = [ "parse", "hello" ] + +expect.exit-code = 0 +expect.result = "Parse error: invalid digit found in string" + +[[runs]] +input = [ "error" ] + +expect.exit-code = 0 +expect.result = "IO_ERROR: Error" diff --git a/examples/example-pack-err/test.toml b/examples/example-pack-err/test.toml new file mode 100644 index 0000000..c4509cb --- /dev/null +++ b/examples/example-pack-err/test.toml @@ -0,0 +1,35 @@ +[[runs]] +input = [ "find" ] + +expect.exit-code = 0 +expect.result = "Search path not provided" + +[[runs]] +input = [ "find", "Cargo.toml" ] + +expect.exit-code = 0 +expect.result = "Not a directory: Cargo.toml" + +[[runs]] +input = [ "find", "examples" ] + +expect.exit-code = 0 +expect.result = "Found directory: examples" + +[[runs]] +input = [ "find-structural", "--json" ] + +expect.exit-code = 0 +expect.result = "{\"name\":\"error_not_found_structural\"}" + +[[runs]] +input = [ "find-structural", "Cargo.toml", "--json" ] + +expect.exit-code = 0 +expect.result = "{\"name\":\"error_not_dir_structural\",\"info\":\"Cargo.toml\"}" + +[[runs]] +input = [ "find-structural", "examples", "--json" ] + +expect.exit-code = 0 +expect.result = "{\"inner\":\"examples\"}" diff --git a/examples/example-panic-unwind/test.toml b/examples/example-panic-unwind/test.toml new file mode 100644 index 0000000..2a29071 --- /dev/null +++ b/examples/example-panic-unwind/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "panic" ] + +expect.exit-code = 0 +expect.result = "Program not panic" + +[[runs]] +input = [ "panic", "something_went_wrong" ] + +expect.exit-code = 0 +expect.result = "Program panic: something_went_wrong" diff --git a/examples/example-pathfinder/test.toml b/examples/example-pathfinder/test.toml new file mode 100644 index 0000000..fb4f1de --- /dev/null +++ b/examples/example-pathfinder/test.toml @@ -0,0 +1,11 @@ +[[runs]] +input = [ "greet" ] + +expect.exit-code = 0 +expect.result = "Hello, World!" + +[[runs]] +input = [ "greet", "Alice" ] + +expect.exit-code = 0 +expect.result = "Hello, Alice!" diff --git a/examples/example-resources/test.toml b/examples/example-resources/test.toml new file mode 100644 index 0000000..3dfa8fe --- /dev/null +++ b/examples/example-resources/test.toml @@ -0,0 +1,5 @@ +[[runs]] +input = [ "current" ] + +expect.exit-code = 0 +expect.result = "Current directory:" diff --git a/examples/example-setup/test.toml b/examples/example-setup/test.toml new file mode 100644 index 0000000..811a108 --- /dev/null +++ b/examples/example-setup/test.toml @@ -0,0 +1,29 @@ +[[runs]] +input = [ "1" ] + +expect.exit-code = 0 +expect.result = "" + +[[runs]] +input = [ "2" ] + +expect.exit-code = 0 +expect.result = "" + +[[runs]] +input = [ "3" ] + +expect.exit-code = 0 +expect.result = "" + +[[runs]] +input = [ "4" ] + +expect.exit-code = 0 +expect.result = "" + +[[runs]] +input = [ "5" ] + +expect.exit-code = 0 +expect.result = "" diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock index 453945b..c3f24c7 100644 --- a/examples/example-structural-renderer/Cargo.lock +++ b/examples/example-structural-renderer/Cargo.lock @@ -80,6 +80,7 @@ dependencies = [ "might_be_async", "serde", "serde_json", + "serde_yaml", ] [[package]] @@ -111,6 +112,12 @@ dependencies = [ ] [[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -163,6 +170,19 @@ dependencies = [ ] [[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] name = "size" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -227,6 +247,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/examples/example-structural-renderer/Cargo.toml b/examples/example-structural-renderer/Cargo.toml index 2090166..e29e9c3 100644 --- a/examples/example-structural-renderer/Cargo.toml +++ b/examples/example-structural-renderer/Cargo.toml @@ -10,6 +10,7 @@ serde = { version = "1.0.228", features = ["derive"] } path = "../../mingling" features = [ "structural_renderer", + "yaml_serde_fmt", "parser", ] diff --git a/examples/example-structural-renderer/test.toml b/examples/example-structural-renderer/test.toml new file mode 100644 index 0000000..3e87151 --- /dev/null +++ b/examples/example-structural-renderer/test.toml @@ -0,0 +1,18 @@ +[[runs]] +input = [ "render", "Bob", "22" ] + +expect.exit-code = 0 +expect.result = "Bob is 22 years old" + +[[runs]] +input = [ "render", "Bob", "22", "--json" ] + +expect.exit-code = 0 +expect.result = "{\"member_name\":\"Bob\",\"member_age\":22}" + +[[runs]] +input = [ "render", "Bob", "22", "--yaml" ] + +expect.exit-code = 0 +expect.result = """member_name: Bob +member_age: 22""" diff --git a/examples/test-examples.toml b/examples/test-examples.toml deleted file mode 100644 index 03df5e1..0000000 --- a/examples/test-examples.toml +++ /dev/null @@ -1,364 +0,0 @@ -[[test.example-outside-type]] -command = "parse 42" -expect.exit-code = 0 -expect.result = "Parsed number: 42" - -[[test.example-outside-type]] -command = "parse hello" -expect.exit-code = 0 -expect.result = "Parse error: invalid digit found in string" - -[[test.example-outside-type]] -command = "error" -expect.exit-code = 0 -expect.result = "IO_ERROR: Error" - -[[test.example-lazy-resources]] -command = "none" -expect.exit-code = 0 -expect.result = "None" - -[[test.example-lazy-resources]] -command = "show" -expect.exit-code = 0 -expect.result = "Initialized\nbaz: qux\nfoo: bar\nhello: world\nkey: value\nrust: lang" - -[[test.example-basic]] -command = "greet" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-basic]] -command = "greet Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-exitcode]] -command = "hello Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice" - -[[test.example-exitcode]] -command = "hello" -expect.exit-code = 1 -expect.result = "No name provided (with exit code 1)" - -[[test.example-exitcode]] -command = "hello --help" -expect.exit-code = 2 -expect.result = "Usage: hello <NAME>" - -[[test.example-error-handling]] -command = "hello" -expect.exit-code = 0 -expect.result = "No name provided" - -[[test.example-error-handling]] -command = "hello MyBestFriendAlice" -expect.exit-code = 0 -expect.result = "Name too long: 17 > 10" - -[[test.example-error-handling]] -command = "hello Alice" -expect.exit-code = 0 -expect.result = "Name not available" - -[[test.example-error-handling]] -command = "hello Peter" -expect.exit-code = 0 -expect.result = "Hello, Peter" - -[[test.example-error-handling]] -command = "hallo" -expect.exit-code = 0 -expect.result = "Command not found: \"hallo\"" - -[[test.example-argument-parse]] -command = "transfer" -expect.exit-code = 0 -expect.result = "file: (1048576)" - -[[test.example-argument-parse]] -command = "transfer --dir src" -expect.exit-code = 0 -expect.result = "dir: src (1048576)" - -[[test.example-argument-parse]] -command = "transfer --size 500 myfile.txt" -expect.exit-code = 0 -expect.result = "file: myfile.txt (500)" - -[[test.example-argument-parse]] -command = "strict-transfer README.md" -expect.exit-code = 0 -expect.result = "file: README.md (1048576)" - -[[test.example-argument-parse]] -command = "strict-transfer" -expect.exit-code = 0 -expect.result = "Error: name is not provided" - -[[test.example-async-support]] -command = "download readme.txt" -expect.exit-code = 0 -expect.result = "\"readme.txt\" downloaded." - -[[test.example-custom-pickable]] -command = "connect 192.168.1.1:8080" -expect.exit-code = 0 -expect.result = "Connected to \"192.168.1.1:8080\"" - -[[test.example-custom-pickable]] -command = "connect" -expect.exit-code = 0 -expect.result = "Failed to parse address" - -[[test.example-custom-pickable]] -command = "connect invalid" -expect.exit-code = 0 -expect.result = "Failed to parse address" - -[[test.example-dispatch-tree]] -command = "cmd5" -expect.exit-code = 0 -expect.result = "It's works!" - -[[test.example-enum-tag]] -command = "lang-select" -expect.exit-code = 0 -expect.result = "Selected: Rust" - -[[test.example-enum-tag]] -command = "lang-select Python" -expect.exit-code = 0 -expect.result = "Selected: Python" - -[[test.example-enum-tag]] -command = "lang-select OCaml" -expect.exit-code = 0 -expect.result = "Selected: OCaml" - -[[test.example-structural-renderer]] -command = "render Bob 22" -expect.exit-code = 0 -expect.result = "Bob is 22 years old" - -[[test.example-structural-renderer]] -command = "render Bob 22 --json" -expect.exit-code = 0 -expect.result = "{\"member_name\":\"Bob\",\"member_age\":22}" - -[[test.example-help]] -command = "greet" -expect.exit-code = 0 -expect.result = "" - -[[test.example-help]] -command = "greet --help" -expect.exit-code = 0 -expect.result = "Usage: greet <NAME>" - -[[test.example-hook]] -command = "greet" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-hook]] -command = "greet Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-panic-unwind]] -command = "panic" -expect.exit-code = 0 -expect.result = "Program not panic" - -[[test.example-panic-unwind]] -command = "panic something_went_wrong" -expect.exit-code = 0 -expect.result = "Program panic: something_went_wrong" - -[[test.example-resources]] -command = "current" -expect.exit-code = 0 -expect.result = "Current directory:" - -[[test.example-setup]] -command = "1" -expect.exit-code = 0 -expect.result = "" - -[[test.example-setup]] -command = "2" -expect.exit-code = 0 -expect.result = "" - -[[test.example-setup]] -command = "3" -expect.exit-code = 0 -expect.result = "" - -[[test.example-setup]] -command = "4" -expect.exit-code = 0 -expect.result = "" - -[[test.example-setup]] -command = "5" -expect.exit-code = 0 -expect.result = "" - -[[test.example-completion]] -command = "greet World --repeat 1" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-clap-binding]] -command = "greet" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-clap-binding]] -command = "greet Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-clap-binding]] -command = "greet Alice -r 5" -expect.exit-code = 0 -expect.result = "Hello, Alice, Alice, Alice, Alice, Alice!" - -[[test.example-pack-err]] -command = "find" -expect.exit-code = 0 -expect.result = "Search path not provided" - -[[test.example-pack-err]] -command = "find Cargo.toml" -expect.exit-code = 0 -expect.result = "Not a directory: Cargo.toml" - -[[test.example-pack-err]] -command = "find examples" -expect.exit-code = 0 -expect.result = "Found directory: examples" - -[[test.example-pack-err]] -command = "find-structural --json" -expect.exit-code = 0 -expect.result = '{"name":"error_not_found_structural"}' - -[[test.example-pack-err]] -command = "find-structural Cargo.toml --json" -expect.exit-code = 0 -expect.result = '{"name":"error_not_dir_structural","info":"Cargo.toml"}' - -[[test.example-pack-err]] -command = "find-structural examples --json" -expect.exit-code = 0 -expect.result = '{"inner":"examples"}' - -[[test.example-pathfinder]] -command = "greet" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-pathfinder]] -command = "greet Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-combine-pathf-dispatch-tree]] -command = "hello Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-combine-pathf-dispatch-tree]] -command = "hello" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-argument-picker]] -command = "calc 1 + 1" -expect.exit-code = 0 -expect.result = "Result: 2" - -[[test.example-argument-picker]] -command = "calc 7 * 7" -expect.exit-code = 0 -expect.result = "Result: 49" - -[[test.example-argument-picker]] -command = "calc" -expect.exit-code = 0 -expect.result = "Error: First number (number_a) was not provided." - -[[test.example-argument-picker]] -command = "calc 1" -expect.exit-code = 0 -expect.result = "Error: Operator was not provided." - -[[test.example-argument-picker]] -command = "calc 1 +" -expect.exit-code = 0 -expect.result = "Error: Second number (number_b) was not provided." - -[[test.example-argument-picker]] -command = "calc 4 / 3" -expect.exit-code = 0 -expect.result = "Result: 1.3333334" - -[[test.example-argument-picker]] -command = "calc 4 / 3 --round" -expect.exit-code = 0 -expect.result = "Result: 1" - -[[test.example-command-macro]] -command = "hello world" -expect.exit-code = 0 -expect.result = "Hello, World" - -[[test.example-command-macro]] -command = "greet-someone Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice" - -[[test.example-command-macro]] -command = "goodbye" -expect.exit-code = 0 -expect.result = "Goodbye!" - -[[test.example-metadata]] -command = "greet" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-metadata]] -command = "greet Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-metadata]] -command = "desc" -expect.exit-code = 0 -expect.result = "EntryGreet desc = ok" - -[[test.example-metadata]] -command = "nodoc" -expect.exit-code = 0 -expect.result = "EntryDescription has no description" - -[[test.example-combine-pathf-metadata]] -command = "hello" -expect.exit-code = 0 -expect.result = "Hello, World!" - -[[test.example-combine-pathf-metadata]] -command = "hello Alice" -expect.exit-code = 0 -expect.result = "Hello, Alice!" - -[[test.example-combine-pathf-metadata]] -command = "desc" -expect.exit-code = 0 -expect.result = "EntryHello desc = okay" diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 55aabdf..c292598 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -2970,6 +2970,7 @@ pub mod example_setup {} /// path = "../../mingling" /// features = [ /// "structural_renderer", +/// "yaml_serde_fmt", /// "parser", /// ] /// diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index 0351925..22d787c 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -1,11 +1,99 @@ use std::{ - fmt::{Display, Formatter}, + fmt::{self, Display, Formatter}, io::Write, process::{ExitCode, exit}, }; use crate::RenderResultMode::{Stderr, Stdout}; +/// A single emitted output item handed to a print hook. +/// +/// `RenderResultPrint` bundles the text content and the output mode together +/// into one value, so a print hook can route the content to stdout/stderr — or +/// any custom sink — as a unit instead of juggling two separate arguments. +/// +/// Values of this type are produced whenever a [`RenderResult`] with bound +/// print hooks (see [`RenderResult::bind_print_hook`] and +/// [`RenderResult::immediate_output`]) writes content through +/// `print`/`println`/`eprint`/`eprintln`, and are handed to every hook in +/// binding order. They are also used to flush another result's buffered content +/// via [`RenderResult::append_other`] when the destination has hooks bound. +/// +/// # Fields +/// +/// * `content` — The raw text that was written, including any trailing newline +/// added by `println`/`eprintln`. +/// * `mode` — The output mode (`Stdout` or `Stderr`) the content was written +/// with, which tells the hook where the content belongs. +/// +/// # Examples +/// +/// ``` +/// use mingling_core::{RenderResult, RenderResultMode, RenderResultPrint}; +/// +/// // Build an output item manually +/// let print = RenderResultPrint { +/// content: "Hello, world!".to_string(), +/// mode: RenderResultMode::Stdout, +/// }; +/// assert_eq!(print.content, "Hello, world!"); +/// assert_eq!(print.mode, RenderResultMode::Stdout); +/// +/// // Use it inside a print hook +/// let mut result = RenderResult::default(); +/// result.bind_print_hook(|print| match print.mode { +/// RenderResultMode::Stdout => print!("{}", print.content), +/// RenderResultMode::Stderr => eprint!("{}", print.content), +/// }); +/// result.eprintln("something went wrong"); // goes to stderr via the hook +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RenderResultPrint { + /// The emitted text content. + /// + /// This is the raw text that was written to the render buffer when the + /// hook fired. For `println`/`eprintln` it includes the trailing newline; + /// for `print`/`eprint` it is exactly the given text. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResultMode, RenderResultPrint}; + /// + /// let print = RenderResultPrint { + /// content: "Hello".to_string(), + /// mode: RenderResultMode::Stdout, + /// }; + /// assert_eq!(print.content, "Hello"); + /// ``` + pub content: String, + + /// The output mode the content was written with. + /// + /// Indicates whether the content was originally directed to stdout + /// (`Stdout`) or stderr (`Stderr`), allowing a hook to route the content + /// to the matching stream. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResultMode, RenderResultPrint}; + /// + /// let print = RenderResultPrint { + /// content: "error".to_string(), + /// mode: RenderResultMode::Stderr, + /// }; + /// assert_eq!(print.mode, RenderResultMode::Stderr); + /// ``` + pub mode: RenderResultMode, +} + +/// Optional list of print hooks bound to a `RenderResult`. +/// +/// Each hook receives the emitted [`RenderResultPrint`]. See +/// [`RenderResult::bind_print_hook`] and [`RenderResult::immediate_output`]. +type PrintHook = Option<Vec<Box<dyn FnMut(RenderResultPrint)>>>; + /// Render result, containing the rendered text content. /// /// `RenderResult` is the core data structure used throughout the rendering pipeline @@ -20,8 +108,9 @@ use crate::RenderResultMode::{Stderr, Stdout}; /// - **Buffered output**: All rendered content is first collected into the buffer /// and can be output uniformly at a convenient time. /// - **Immediate output**: Can be enabled via [`immediate_output`](RenderResult::immediate_output), -/// causing content to be flushed to stdout/stderr in real time while also being -/// added to the buffer. +/// which binds a print hook that flushes content to stdout/stderr in real time +/// while also being added to the buffer. Custom hooks can be bound with +/// [`bind_print_hook`](RenderResult::bind_print_hook). /// - **Dual-channel output**: The `Stdout` and `Stderr` modes distinguish between /// normal output and error output. /// - **Exit code management**: Supports carrying an exit code to exit the process @@ -59,19 +148,19 @@ use crate::RenderResultMode::{Stderr, Stdout}; /// let result: RenderResult = (|| RenderResult::from("closure result")).into(); /// assert_eq!(result.to_string(), "closure result"); /// ``` -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default)] pub struct RenderResult { - /// Whether immediate output is enabled. + /// Print hooks invoked with the buffered content and its output mode. /// - /// When set to `true`, rendered content is flushed to stdout/stderr in real time - /// while also being written to the buffer, enabling live output. This is useful - /// in scenarios where results should be displayed incrementally, such as in - /// long-running rendering tasks where the user wants to see partial output - /// without waiting for the entire rendering process to complete. + /// When hooks are bound (via [`immediate_output`](RenderResult::immediate_output) + /// or [`bind_print_hook`](RenderResult::bind_print_hook)), every + /// `print`/`println`/`eprint`/`eprintln` call additionally emits the content + /// through each hook in binding order — typically flushing it to stdout/stderr + /// in real time — while the content is still appended to the buffer. /// - /// The default value is `false`, meaning all content is first written to the - /// buffer and output uniformly at the end. - immediate_output: bool, + /// The default value is `None`, meaning content is only buffered and output + /// uniformly at the end (e.g. via [`std_print`](RenderResult::std_print)). + print_hook: PrintHook, /// Render buffer, stored as a list of (text, output mode) pairs. /// @@ -111,6 +200,36 @@ pub struct RenderResult { pub exit_code: i32, } +impl fmt::Debug for RenderResult { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("RenderResult") + .field("render_buffer", &self.render_buffer) + .field("exit_code", &self.exit_code) + .field("print_hooks", &self.print_hook.as_ref().map(Vec::len)) + .finish() + } +} + +impl Clone for RenderResult { + /// The bound print hooks are opaque closures and cannot be cloned, so the + /// cloned result is created without any hooks. + fn clone(&self) -> Self { + Self { + print_hook: None, + render_buffer: self.render_buffer.clone(), + exit_code: self.exit_code, + } + } +} + +impl PartialEq for RenderResult { + fn eq(&self, other: &Self) -> bool { + self.render_buffer == other.render_buffer && self.exit_code == other.exit_code + } +} + +impl Eq for RenderResult {} + /// Enum representing the output mode for render results. /// /// This determines whether the rendered content should be directed to standard @@ -244,12 +363,13 @@ impl RenderResult { Self::default() } - /// Marks the render result for immediate output, bypassing any buffering or - /// deferred rendering. + /// Enables immediate output by binding a print hook that flushes content to + /// stdout/stderr in real time. /// - /// When set, the rendered content will be both collected in the result and - /// immediately flushed to stdout/stderr in real time, rather than being - /// deferred for later display. + /// After this is called, every `print`/`println`/`eprint`/`eprintln` call + /// writes its content to the corresponding output stream immediately, while + /// also keeping it in the buffer for later use (e.g. [`std_print`](RenderResult::std_print) + /// or `to_string()`). /// /// # Examples /// @@ -258,9 +378,47 @@ impl RenderResult { /// /// let mut result = RenderResult::default(); /// result.immediate_output(); + /// result.print("Hello, "); + /// result.print("world!"); // flushed to stdout right away + /// assert_eq!(result.to_string(), "Hello, world!"); /// ``` - pub const fn immediate_output(&mut self) -> &mut Self { - self.immediate_output = true; + pub fn immediate_output(&mut self) -> &mut Self { + self.bind_print_hook(|RenderResultPrint { content, mode }| match mode { + Stdout => print!("{content}"), + Stderr => eprint!("{content}"), + }) + } + + /// Binds a custom print hook invoked with the content and output mode of + /// every `print`/`println`/`eprint`/`eprintln` call. + /// + /// Multiple hooks can be bound; they are invoked in binding order. This is + /// the building block behind [`immediate_output`](RenderResult::immediate_output) + /// and can be used to route output to a custom sink (e.g. for testing). + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResult, RenderResultMode}; + /// + /// let mut result = RenderResult::default(); + /// result.bind_print_hook(|print| { + /// println!( + /// "[{}] {}", + /// if print.mode == RenderResultMode::Stdout { + /// "out" + /// } else { + /// "err" + /// }, + /// print.content + /// ); + /// }); + /// result.print("Hello"); + /// ``` + pub fn bind_print_hook(&mut self, hook: impl FnMut(RenderResultPrint) + 'static) -> &mut Self { + self.print_hook + .get_or_insert_with(Vec::new) + .push(Box::new(hook)); self } @@ -317,12 +475,12 @@ impl RenderResult { /// Appends the contents of another `RenderResult` to this one. /// - /// If this `RenderResult` has `immediate_output` enabled but the other does not, - /// the other's content will be immediately flushed to the appropriate output stream - /// (stdout/stderr) while also being appended to the render buffer. + /// If this `RenderResult` has print hooks bound but the other does not, the + /// other's content is emitted through this result's hooks (e.g. flushed to + /// stdout/stderr) while also being appended to the render buffer. /// - /// The `exit_code` of the other result is **not** transferred — only the buffered - /// content and the `immediate_output` flag of the other result are merged. + /// The `exit_code` and the print hooks of the other result are **not** + /// transferred — only its buffered content is merged. /// /// # Arguments /// @@ -345,17 +503,15 @@ impl RenderResult { pub fn append_other(&mut self, other: impl Into<Self>) { let other = other.into(); - // If self has immediate output enabled, but the input does not, the input needs immediate output. - let immediate_output = !other.immediate_output && self.immediate_output; + // If self has hooks but the other does not, the other's buffered content + // was never emitted — flush it through self's hooks while appending. + let should_emit = self.print_hook.is_some() && other.print_hook.is_none(); - for i in other.render_buffer { - if immediate_output { - match &i.1 { - Stdout => print!("{}", i.0), - Stderr => eprint!("{}", i.0), - } + for (content, mode) in other.render_buffer { + if should_emit { + self.emit(&content, mode); } - self.render_buffer.push(i); + self.render_buffer.push((content, mode)); } } @@ -373,9 +529,7 @@ impl RenderResult { /// ``` pub fn print(&mut self, text: impl Into<String>) { let text = text.into(); - if self.immediate_output { - print!("{text}"); - } + self.emit(&text, Stdout); self.append_to_buffer(text, Stdout); } @@ -393,9 +547,7 @@ impl RenderResult { /// ``` pub fn println(&mut self, text: impl Into<String>) { let text = text.into(); - if self.immediate_output { - println!("{text}"); - } + self.emit(&format!("{text}\n"), Stdout); self.append_line_to_buffer(text, Stdout); } @@ -413,9 +565,7 @@ impl RenderResult { /// ``` pub fn eprint(&mut self, text: impl Into<String>) { let text = text.into(); - if self.immediate_output { - eprint!("{text}"); - } + self.emit(&text, Stderr); self.append_to_buffer(text, Stderr); } @@ -433,9 +583,7 @@ impl RenderResult { /// ``` pub fn eprintln(&mut self, text: impl Into<String>) { let text = text.into(); - if self.immediate_output { - eprintln!("{text}"); - } + self.emit(&format!("{text}\n"), Stderr); self.append_line_to_buffer(text, Stderr); } @@ -538,7 +686,7 @@ impl RenderResult { /// /// # Returns /// - /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with + /// A new `RenderResult` with the same print hooks and `exit_code`, but with /// trimmed text content. /// /// # Examples @@ -579,11 +727,23 @@ impl RenderResult { Self { render_buffer: buffer, - immediate_output: self.immediate_output, + print_hook: self.print_hook, exit_code: self.exit_code, } } + /// Emits `content` to every bound print hook, if any. + fn emit(&mut self, content: &str, mode: RenderResultMode) { + if let Some(hooks) = &mut self.print_hook { + for hook in hooks { + hook(RenderResultPrint { + content: content.to_string(), + mode, + }); + } + } + } + /// Exits the process with the exit code stored in this `RenderResult`. /// /// This method calls `std::process::exit()` with the `exit_code` value, @@ -623,7 +783,9 @@ fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) -> #[cfg(test)] mod tests { use super::*; + use std::cell::RefCell; use std::io::Write as IoWrite; + use std::rc::Rc; #[test] fn default_creates_empty_text_with_exit_code_zero() { @@ -749,4 +911,79 @@ mod tests { assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr); assert_eq!(trimmed.to_string(), "error"); } + + #[test] + fn print_hooks_receive_content_and_mode() { + let mut result = RenderResult::default(); + let captured: Rc<RefCell<Vec<RenderResultPrint>>> = Rc::default(); + let hook_captured = Rc::clone(&captured); + result.bind_print_hook(move |print| hook_captured.borrow_mut().push(print)); + + result.print("Hello"); + result.eprintln("World"); + + assert_eq!( + captured.borrow()[0], + RenderResultPrint { + content: "Hello".to_string(), + mode: RenderResultMode::Stdout + } + ); + assert_eq!( + captured.borrow()[1], + RenderResultPrint { + content: "World\n".to_string(), + mode: RenderResultMode::Stderr + } + ); + assert_eq!(result.to_string(), "HelloWorld"); + } + + #[test] + fn immediate_output_binds_stdout_hook() { + let mut result = RenderResult::default(); + assert!(result.print_hook.is_none()); + result.immediate_output(); + assert!(result.print_hook.is_some()); + } + + #[test] + fn append_other_emits_through_hooks_when_self_has_them() { + let mut dest = RenderResult::default(); + let emitted: Rc<RefCell<Vec<String>>> = Rc::default(); + let hook_emitted = Rc::clone(&emitted); + dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content)); + + let mut src = RenderResult::default(); + src.append_to_buffer("Hello", RenderResultMode::Stdout); + dest.append_other(src); + + assert_eq!(emitted.borrow().as_slice(), ["Hello"]); + assert_eq!(dest.to_string(), "Hello"); + } + + #[test] + fn append_other_does_not_reemit_when_other_has_hooks() { + let mut dest = RenderResult::default(); + let emitted: Rc<RefCell<Vec<String>>> = Rc::default(); + let hook_emitted = Rc::clone(&emitted); + dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content)); + + let mut src = RenderResult::default(); + src.bind_print_hook(|_| {}); + src.append_to_buffer("Hello", RenderResultMode::Stdout); + dest.append_other(src); + + assert!(emitted.borrow().is_empty()); + assert_eq!(dest.to_string(), "Hello"); + } + + #[test] + fn write_does_not_emit_through_hooks() { + let mut result = RenderResult::default(); + result.bind_print_hook(|_| panic!("append_to_buffer must not emit")); + + IoWrite::write(&mut result, b"Hello").unwrap(); + assert_eq!(result.to_string(), "Hello"); + } } |
