From e2665f8ae75ac34469748df6d71719f93e2b4a8a Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 22:33:30 +0100 Subject: [PATCH 1/7] Format markdown documentation --- AGENTS.md | 8 +- docs/netsuke-design.md | 418 +++++++++--------- docs/roadmap.md | 6 +- docs/rust-testing-with-rstest-fixtures.md | 374 ++++++++-------- ...snapshot-testing-in-netsuke-using-insta.md | 104 ++--- 5 files changed, 465 insertions(+), 445 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d76c2434..b0cd6305 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,8 @@ - **Clarity over cleverness.** Be concise, but favour explicit over terse or obscure idioms. Prefer code that's easy to follow. - **Use functions and composition.** Avoid repetition by extracting reusable - logic. Prefer generators or comprehensions, and declarative code to imperative - repetition when readable. + logic. Prefer generators or comprehensions, and declarative code to + imperative repetition when readable. - **Small, meaningful functions.** Functions must be small, clear in purpose, single responsibility, and obey command/query segregation. - **Clear commit messages.** Commit messages should be descriptive, explaining @@ -95,8 +95,8 @@ ## Rust Specific Guidance This repository is written in Rust and uses Cargo for building and dependency -management. Contributors should follow these best practices when working on -the project: +management. Contributors should follow these best practices when working on the +project: - Run `make fmt`, `make lint`, and `make test` before committing. These targets wrap `cargo fmt`, `cargo clippy`, and `cargo test` with the appropriate flags. diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 170af0a3..6b0920f6 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -3,8 +3,8 @@ ## Section 1: Core Architecture and Data Flow This document presents a mid-level engineering design for Netsuke, a modern -build automation tool implemented in Rust. Netsuke is designed to provide -the power and dependency resolution capabilities of traditional `make` while +build automation tool implemented in Rust. Netsuke is designed to provide the +power and dependency resolution capabilities of traditional `make` while offering a significantly more intuitive, readable, and secure user experience. This is achieved by leveraging a user-friendly YAML-based manifest, a powerful Jinja templating engine for dynamic configuration, and the high-performance @@ -16,14 +16,14 @@ At its core, Netsuke should not be conceptualized as a direct, imperative replacement for `make`. Instead, it is architected as a high-level **build system compiler**. This architectural paradigm is central to its design. Ninja, the chosen execution backend, describes itself as a low-level "assembler" for -build systems.[^1] It is intentionally constrained, lacking features like string -manipulation or conditional logic, to ensure its primary goal: running builds as -fast as possible.[^2] +build systems.[^1] It is intentionally constrained, lacking features like +string manipulation or conditional logic, to ensure its primary goal: running +builds as fast as possible.[^2] This design choice by Ninja's authors necessitates the existence of a higher- level generator tool. Netsuke fulfills this role. It provides a rich, user- -friendly language (YAML with Jinja) for describing the *what* and *why* of -a build—the project's structure, its logical rules, and its configurable +friendly language (YAML with Jinja) for describing the *what* and *why* of a +build—the project's structure, its logical rules, and its configurable parameters. Netsuke's primary responsibility is to compile this high-level description into a low-level, highly optimized execution plan that Ninja can understand and execute. This separation of concerns—Netsuke managing build @@ -52,10 +52,10 @@ before execution, a critical requirement for compatibility with Ninja. 3. Stage 3: YAML Parsing & Deserialization - The static YAML string generated in the previous stage is passed to a - YAML parser. This parser validates the YAML syntax and deserializes the - content into a set of strongly typed Rust data structures. This collection - of structs, which directly mirrors the YAML schema, can be considered an + The static YAML string generated in the previous stage is passed to a YAML + parser. This parser validates the YAML syntax and deserializes the content + into a set of strongly typed Rust data structures. This collection of + structs, which directly mirrors the YAML schema, can be considered an "unprocessed" Abstract Syntax Tree (AST) of the build plan. 4. Stage 4: IR Generation & Validation @@ -64,8 +64,8 @@ before execution, a critical requirement for compatibility with Ninja. Representation (IR) of the build. This IR represents the build as a static dependency graph with all file paths, commands, and dependencies explicitly defined. During this transformation, Netsuke performs critical validation - checks. It verifies the existence of referenced rules, ensures each rule - has exactly one of `command` or `script`, and ensures every target specifies + checks. It verifies the existence of referenced rules, ensures each rule has + exactly one of `command` or `script`, and ensures every target specifies exactly one of `rule`, `command`, or `script`. Circular dependencies and missing inputs are also detected at this stage. @@ -99,27 +99,28 @@ sequenceDiagram ### 1.3 The Static Graph Mandate -The architecture's multi-stage pipeline is a direct consequence of a fundamental -design constraint imposed by the choice of Ninja as the backend. Ninja's -remarkable speed in incremental builds stems from its simplicity; it operates -on a pre-computed, static dependency graph and avoids costly runtime operations -like filesystem queries (e.g., glob expansion) or string manipulation.[^2] +The architecture's multi-stage pipeline is a direct consequence of a +fundamental design constraint imposed by the choice of Ninja as the backend. +Ninja's remarkable speed in incremental builds stems from its simplicity; it +operates on a pre-computed, static dependency graph and avoids costly runtime +operations like filesystem queries (e.g., glob expansion) or string +manipulation.[^2] At the same time, a "friendlier" build system must offer dynamic capabilities. -Users will expect to define builds that can adapt to their environment, such -as using different compiler flags on Linux versus Windows, or automatically +Users will expect to define builds that can adapt to their environment, such as +using different compiler flags on Linux versus Windows, or automatically discovering source files in a directory. These features are provided in Netsuke by the Jinja templating engine. This creates a necessary architectural division. All the dynamic logic, templating, and configuration must be fully evaluated by Netsuke *before* Ninja -is ever invoked. The point of this transition is the Intermediate Representation -(IR) generated in Stage 4. The IR serves as a static snapshot of the build -plan after all Jinja logic has been resolved. It is the "object code" that -the Netsuke "compiler" produces, which can then be handed off to the Ninja -"assembler" for execution. This mandate for a pre-computed static graph dictates -the entire five-stage pipeline and establishes a clean boundary between the -user-facing logic layer and the machine-facing execution layer. +is ever invoked. The point of this transition is the Intermediate +Representation (IR) generated in Stage 4. The IR serves as a static snapshot of +the build plan after all Jinja logic has been resolved. It is the "object code" +that the Netsuke "compiler" produces, which can then be handed off to the Ninja +"assembler" for execution. This mandate for a pre-computed static graph +dictates the entire five-stage pipeline and establishes a clean boundary +between the user-facing logic layer and the machine-facing execution layer. ## Section 2: The Netsuke Manifest: A User-Centric YAML Schema @@ -164,8 +165,8 @@ level keys. exposed to the Jinja templating context. - `macros`: An optional list of Jinja macro definitions. Each item provides a - `signature` string using standard Jinja syntax and a `body` declared with - the YAML `|` block style. Netsuke registers these macros in the template + `signature` string using standard Jinja syntax and a `body` declared with the + YAML `|` block style. Netsuke registers these macros in the template environment before rendering other sections. - `rules`: A list of rule definitions. Each rule is a reusable template for a @@ -248,12 +249,13 @@ Each entry in the `rules` list is a mapping that defines a reusable action. - `name`: A unique string identifier for the rule. - `command`: A single command string to be executed. It may include the - placeholders `{{ ins }}` and `{{ outs }}` to represent input and output files. - Netsuke expands these placeholders to space-separated, shell-escaped lists - of file paths before hashing the action. When generating the Ninja rule, the - lists are replaced with Ninja's `$in` and `$out` macros. After interpolation - the command must be parsable by [shlex](https://docs.rs/shlex/latest/shlex/). - Any interpolation other than `ins` or `outs` is automatically shell-escaped. + placeholders `{{ ins }}` and `{{ outs }}` to represent input and output + files. Netsuke expands these placeholders to space-separated, shell-escaped + lists of file paths before hashing the action. When generating the Ninja + rule, the lists are replaced with Ninja's `$in` and `$out` macros. After + interpolation the command must be parsable by + [shlex](https://docs.rs/shlex/latest/shlex/). Any interpolation other than + `ins` or `outs` is automatically shell-escaped. - `script`: A multi-line script declared with the YAML `|` block style. The entire block is passed to an interpreter. If the first line begins with `#!` @@ -262,10 +264,10 @@ Each entry in the `rules` list is a mapping that defines a reusable action. field (defaulting to `/bin/sh -e`). For `/bin/sh` scripts, each interpolation is automatically passed through the `shell_escape` filter unless a `| raw` filter is applied. Future versions will allow configurable script languages - with their own escaping rules. On Windows, scripts default to `powershell - -Command` unless the manifest's `interpreter` field overrides the setting. - Exactly one of `command` or `script` must be provided. The manifest parser - enforces this rule to prevent invalid states. + with their own escaping rules. On Windows, scripts default to + `powershell -Command` unless the manifest's `interpreter` field overrides the + setting. Exactly one of `command` or `script` must be provided. The manifest + parser enforces this rule to prevent invalid states. Internally, these options deserialize into a shared `Recipe` enum tagged with a `kind` field. Serde aliases ensure manifests that omit the tag continue to @@ -276,9 +278,9 @@ Each entry in the `rules` list is a mapping that defines a reusable action. and improves the user's visibility into the build process.[^2] - `deps`: An optional field to configure support for C/C++-style header - dependency generation. Its value specifies the format (e.g., `gcc` or `msvc`), - which instructs Netsuke to generate the appropriate `depfile` or `deps` - attribute in the corresponding Ninja rule.[^3] + dependency generation. Its value specifies the format (e.g., `gcc` or + `msvc`), which instructs Netsuke to generate the appropriate `depfile` or + `deps` attribute in the corresponding Ninja rule.[^3] ### 2.4 Defining `targets` @@ -307,14 +309,14 @@ validates this exclusivity during deserialization. When multiple fields are present, Netsuke emits a `RecipeConflict` error with the message "rule, command and script are mutually exclusive". - This union deserializes into the same `Recipe` enum used for rules. The parser - enforces that only one variant is present, maintaining backward compatibility - through serde aliases when `kind` is omitted. + This union deserializes into the same `Recipe` enum used for rules. The + parser enforces that only one variant is present, maintaining backward + compatibility through serde aliases when `kind` is omitted. - `sources`: The input files required by the command. This can be a single - string or a list of strings. If any source entry matches the `name` of another - target, that target is built first, before the current target's explicit - `deps`. + string or a list of strings. If any source entry matches the `name` of + another target, that target is built first, before the current target's + explicit `deps`. - `deps`: An optional list of other target names. These targets are explicit dependencies and must be successfully built before this target can be. A @@ -332,8 +334,8 @@ and script are mutually exclusive". variables.[^3] - `macros`: An optional list of Jinja macro definitions. Each item provides a - `signature` string using standard Jinja syntax and a `body` declared with - the YAML `|` block style. Netsuke registers these macros in the template + `signature` string using standard Jinja syntax and a `body` declared with the + YAML `|` block style. Netsuke registers these macros in the template environment before rendering other sections. - `phony`: When set to `true`, the target runs when explicitly requested even if @@ -345,9 +347,9 @@ and script are mutually exclusive". ### 2.5 Generated Targets with `foreach` Large sets of similar outputs can clutter a manifest when written individually. -Netsuke supports a `foreach` entry within `targets` to generate multiple outputs -succinctly. The expression assigned to `foreach` is evaluated during the Jinja -render phase, and each value becomes `item` in the target context. +Netsuke supports a `foreach` entry within `targets` to generate multiple +outputs succinctly. The expression assigned to `foreach` is evaluated during +the Jinja render phase, and each value becomes `item` in the target context. ```yaml - foreach: "{{ glob('assets/svg/*.svg') }}" @@ -387,28 +389,28 @@ data structures are crucial for the robustness and maintainability of Netsuke. For YAML parsing and deserialization, the recommended crate is `serde_yaml`. This choice is based on its deep and direct integration with the `serde` framework, the de-facto standard for serialization and deserialization in the -Rust ecosystem. Using `serde_yaml` allows `serde`'s powerful derive macros -to automatically generate the deserialization logic for Rust structs. This +Rust ecosystem. Using `serde_yaml` allows `serde`'s powerful derive macros to +automatically generate the deserialization logic for Rust structs. This approach is idiomatic, highly efficient, and significantly reduces the amount of boilerplate code that needs to be written and maintained. A simple `#` annotation on a struct is sufficient to make it a deserialization target. While other promising YAML libraries like `saphyr` exist, their `serde` -integration (`saphyr-serde`) is currently described as "soon-to-be" or is at -a highly experimental stage (version 0.0.0)[^11] Building a core component -of Netsuke on a nascent or unreleased library would introduce significant and +integration (`saphyr-serde`) is currently described as "soon-to-be" or is at a +highly experimental stage (version 0.0.0)[^11] Building a core component of +Netsuke on a nascent or unreleased library would introduce significant and unnecessary project risk. -`serde_yaml` is mature, widely adopted, and battle-tested, making it the prudent -choice for production-quality software. +`serde_yaml` is mature, widely adopted, and battle-tested, making it the +prudent choice for production-quality software. ### 3.2 Core Data Structures (`ast.rs`) The Rust structs that `serde_yaml` will deserialize into form the Abstract -Syntax Tree (AST) of the build manifest. These structs must precisely mirror the -YAML schema defined in Section 2. They will be defined in a dedicated module, -`src/ast.rs`, and annotated with `#` to enable automatic deserialization and -easy debugging. +Syntax Tree (AST) of the build manifest. These structs must precisely mirror +the YAML schema defined in Section 2. They will be defined in a dedicated +module, `src/ast.rs`, and annotated with `#` to enable automatic +deserialization and easy debugging. Rust @@ -498,8 +500,8 @@ pub enum StringOrList { ```rust *Note: The* `StringOrList` *enum with* `#[serde(untagged)]` *provides the -flexibility for users to specify single sources, dependencies, and rule names as -a simple string and multiple as a list, enhancing user-friendliness.* +flexibility for users to specify single sources, dependencies, and rule names +as a simple string and multiple as a list, enhancing user-friendliness.* ### 3.3 The Two-Pass Parsing Requirement @@ -518,9 +520,9 @@ targets: rule: compile ```` -The value of `sources`, `{{ glob('src/*.c') }}`, is not a valid YAML string from -the perspective of a strict parser. Attempting to deserialize this directly with -`serde_yaml` would result in a parsing error. +The value of `sources`, `{{ glob('src/*.c') }}`, is not a valid YAML string +from the perspective of a strict parser. Attempting to deserialize this +directly with `serde_yaml` would result in a parsing error. Therefore, the process must be sequential: @@ -560,15 +562,15 @@ The recommended templating engine is `minijinja`. This crate is the ideal choice for several reasons. It is explicitly designed as a Rust implementation of the Jinja2 template engine, aiming for close -compatibility with its syntax and behaviour.[^15] This is advantageous as Jinja2 -is a mature, well-documented, and widely understood language, reducing the -learning curve for new Netsuke users. Furthermore, +compatibility with its syntax and behaviour.[^15] This is advantageous as +Jinja2 is a mature, well-documented, and widely understood language, reducing +the learning curve for new Netsuke users. Furthermore, `minijinja` is designed with minimal dependencies, which is beneficial for keeping Netsuke's compile times and binary size reasonable.[^17] Its API is -well-documented and provides first-class support for adding custom functions and -filters, which is essential for extending its capabilities to suit the needs of -a build system.[^16] +well-documented and provides first-class support for adding custom functions +and filters, which is essential for extending its capabilities to suit the +needs of a build system.[^16] Alternative template engines like Askama are less suitable for this use case. Askama is a type-safe engine that compiles templates into Rust code at build @@ -584,8 +586,8 @@ Netsuke will construct a single `minijinja::Environment` instance at startup. This environment will be configured with a set of custom functions and filters that provide build-specific functionality. -When rendering a user's `Netsukefile` file, the initial context provided to -the template will be constructed from the `vars` section of the manifest. This +When rendering a user's `Netsukefile` file, the initial context provided to the +template will be constructed from the `vars` section of the manifest. This allows users to define variables in their YAML and immediately reference them within Jinja expressions. For example: @@ -607,8 +609,8 @@ targets: Netsuke allows users to declare reusable Jinja macros directly in the manifest. These are provided in a top-level `macros` list where each entry defines a `signature` and a `body` string. The body must use YAML's `|` block syntax so -multi-line macro definitions remain readable. All macros are registered with the -template environment before any other section is rendered. +multi-line macro definitions remain readable. All macros are registered with +the template environment before any other section is rendered. YAML @@ -646,14 +648,15 @@ providing a secure bridge to the underlying system. - `glob(pattern: &str) -> Result, Error>`: A function that performs file path globbing. This is a critical feature for any modern build tool, allowing users to easily specify sets of source files (e.g., `src/**/*.c`). - The results are returned sorted lexicographically and symlinks are followed to - keep builds deterministic. This function bridges a key feature gap, as Ninja - itself does not support globbing.[^3] + The results are returned sorted lexicographically and symlinks are followed + to keep builds deterministic. This function bridges a key feature gap, as + Ninja itself does not support globbing.[^3] - `python_version(requirement: &str) -> Result`: An example of a domain-specific helper function that demonstrates the extensibility of this - architecture. This function would execute `python --version` or `python3 - --version` using `std::process::Command` 19, parse the output using the + architecture. This function would execute `python --version` or + `python3 --version` using `std::process::Command` 19, parse the output using + the `semver` crate 4, and compare it against a user-provided SemVer requirement string (e.g., @@ -668,8 +671,8 @@ for transforming data within templates. - `| shell_escape`: A filter that takes a string or list and escapes it for safe inclusion as a single argument in a shell command. This is a non- - negotiable security feature to prevent command injection vulnerabilities. - The implementation will use the `shell-quote` crate for robust, shell-aware + negotiable security feature to prevent command injection vulnerabilities. The + implementation will use the `shell-quote` crate for robust, shell-aware quoting.[^22] - `| to_path`: A filter that converts a string into a platform-native path @@ -681,27 +684,27 @@ for transforming data within templates. ### 4.6 Jinja as the "Logic Layer" The integration of Jinja is more than a simple convenience for string -substitution. It effectively serves as the **logic layer** for the entire -build system. Traditional `make` provides powerful but often opaque functions -like `$(shell...)` and `$(wildcard...)`. Netsuke achieves and surpasses this +substitution. It effectively serves as the **logic layer** for the entire build +system. Traditional `make` provides powerful but often opaque functions like +`$(shell...)` and `$(wildcard...)`. Netsuke achieves and surpasses this functionality in a much friendlier and safer way. -By implementing complex or potentially unsafe operations (like filesystem access -or command execution) as custom functions in Rust and exposing them as simple, -declarative primitives in the Jinja environment, Netsuke provides a powerful -yet controlled scripting environment. The user can write a clean, readable -template like `sources: {{ glob("src/*.c") }}`, and the complex, error-prone -logic of traversing the filesystem is handled by secure, well-tested Rust code. -This design pattern is the key to providing both power and safety, fulfilling -the core requirement of a system that is friendlier and more robust than its -predecessors. +By implementing complex or potentially unsafe operations (like filesystem +access or command execution) as custom functions in Rust and exposing them as +simple, declarative primitives in the Jinja environment, Netsuke provides a +powerful yet controlled scripting environment. The user can write a clean, +readable template like `sources: {{ glob("src/*.c") }}`, and the complex, +error-prone logic of traversing the filesystem is handled by secure, +well-tested Rust code. This design pattern is the key to providing both power +and safety, fulfilling the core requirement of a system that is friendlier and +more robust than its predecessors. ### 4.7 Template Standard Library Netsuke bundles a small "standard library" of Jinja helpers. These tests, -filters and functions are available to every template and give concise access to -common filesystem queries, path manipulations, collection utilities and network -operations. +filters and functions are available to every template and give concise access +to common filesystem queries, path manipulations, collection utilities and +network operations. #### File-system tests @@ -745,8 +748,8 @@ operations. | `snake_case` / `camel_case` / `kebab-case` | Rename helpers | All built-in filters use `snake_case`. The `camel_case` helper is provided in -place of `camelCase` so naming remains consistent with `snake_case` and `kebab- -case`. +place of `camelCase` so naming remains consistent with `snake_case` and +`kebab- case`. #### Generic collection filters @@ -796,44 +799,46 @@ be marked `pure` if safe for caching or `impure` otherwise. ## Section 5: The Bridge to Ninja: Intermediate Representation and Code Generation After the user's manifest has been fully rendered by Jinja and deserialized -into the AST, the next phase is to transform this high-level representation into -a format suitable for the Ninja backend. This is accomplished via a two-step -process: converting the AST into a canonical Intermediate Representation (IR), -and then synthesizing the final `build.ninja` file from that IR. +into the AST, the next phase is to transform this high-level representation +into a format suitable for the Ninja backend. This is accomplished via a +two-step process: converting the AST into a canonical Intermediate +Representation (IR), and then synthesizing the final `build.ninja` file from +that IR. ### 5.1 The Role of the Intermediate Representation (IR) The Intermediate Representation is a critical architectural component that -serves as the static, fully resolved, and validated representation of the entire -build graph. It is the bridge between the user-facing front-end (the YAML schema -and its corresponding AST) and the machine-facing back-end (the Ninja file -format). +serves as the static, fully resolved, and validated representation of the +entire build graph. It is the bridge between the user-facing front-end (the +YAML schema and its corresponding AST) and the machine-facing back-end (the +Ninja file format). The primary purpose of the IR is to create a decoupling layer. This abstraction barrier allows the front-end and back-end to evolve independently. For example, the YAML schema could be significantly redesigned in a future version of Netsuke, but as long as the transformation logic is updated to produce the same stable IR, the Ninja generation back-end would require no changes. Conversely, -if the decision were made to support an alternative execution back-end (e.g., -a distributed build system), only a new generator module (`IR -> NewBackend`) +if the decision were made to support an alternative execution back-end (e.g., a +distributed build system), only a new generator module (`IR -> NewBackend`) would need to be written, leaving the entire front-end parsing and validation logic untouched. -Importantly, the IR contains **no Ninja-isms**. Placeholders such as `$in` -and `$out` are resolved to plain lists of file paths, and command strings are +Importantly, the IR contains **no Ninja-isms**. Placeholders such as `$in` and +`$out` are resolved to plain lists of file paths, and command strings are expanded before hashing. This deliberate absence of Ninja-specific syntax makes the IR a stable contract that future back-ends—distributed builders, remote executors, or otherwise—can consume without modification. Furthermore, the IR is the ideal stage at which to perform graph-level analysis -and optimizations, such as detecting circular dependencies, pruning unused build -targets, or identifying duplicate build actions. +and optimizations, such as detecting circular dependencies, pruning unused +build targets, or identifying duplicate build actions. ### 5.2 IR Data Structures (`ir.rs`) The IR data structures are designed to closely mirror the conceptual model of -the Ninja build system, which consists of "Action" nodes (commands) and "Target" -nodes (files).[^7] This close mapping simplifies the final code generation step. +the Ninja build system, which consists of "Action" nodes (commands) and +"Target" nodes (files).[^7] This close mapping simplifies the final code +generation step. Rust @@ -902,10 +907,10 @@ consumes a `NetsukeManifest` (the AST) and produces a `BuildGraph` (the IR). This transformation involves several steps: 1. **Action Consolidation:** Iterate through the `manifest.rules` from the AST. - For each rule, create a corresponding `ir::Action` struct. These actions - are stored in the `BuildGraph`'s `actions` map, keyed by a hash of their - fully resolved command text, interpreter, local variables, and depfile - options. This ensures deduplication only occurs when two actions are truly + For each rule, create a corresponding `ir::Action` struct. These actions are + stored in the `BuildGraph`'s `actions` map, keyed by a hash of their fully + resolved command text, interpreter, local variables, and depfile options. + This ensures deduplication only occurs when two actions are truly interchangeable. 2. **Target Expansion:** Iterate through the `manifest.targets` and the optional @@ -919,9 +924,9 @@ This transformation involves several steps: output vectors. 4. **Graph Validation:** As the graph is constructed, perform validation checks. - This includes ensuring that every rule referenced by a target exists in - the `actions` map and running a cycle detection algorithm (e.g., a depth- - first search maintaining a visitation state) on the dependency graph to fail + This includes ensuring that every rule referenced by a target exists in the + `actions` map and running a cycle detection algorithm (e.g., a depth- first + search maintaining a visitation state) on the dependency graph to fail compilation if a circular dependency is found. ### 5.4 Ninja File Synthesis (`ninja_gen.rs`) @@ -953,10 +958,10 @@ structures to the Ninja file syntax. ```` 3. **Write Build Edges:** Iterate through the `graph.targets` map. For each - `ir::BuildEdge`, write a corresponding Ninja `build` statement. This involves - formatting the lists of explicit outputs, implicit outputs, inputs, and - order-only dependencies using the correct Ninja syntax (`:`, `|`, and `|| - `).[^7] Use Ninja's built-in `phony` rule when `phony` is `true`. For an + `ir::BuildEdge`, write a corresponding Ninja `build` statement. This + involves formatting the lists of explicit outputs, implicit outputs, inputs, + and order-only dependencies using the correct Ninja syntax (`:`, `|`, and + `||`).[^7] Use Ninja's built-in `phony` rule when `phony` is `true`. For an `always` edge, either generate a `phony` build with no outputs or emit a dummy output marked `restat = 1` and depend on a permanently dirty target so the command runs on each invocation. @@ -981,9 +986,10 @@ default my_app ## Section 6: Process Management and Secure Execution The final stage of a Netsuke build involves executing commands. While Netsuke -delegates the core task scheduling and execution to the Ninja binary, it remains -responsible for invoking Ninja correctly and, most importantly, for ensuring -that the commands it generates for Ninja to run are constructed securely. +delegates the core task scheduling and execution to the Ninja binary, it +remains responsible for invoking Ninja correctly and, most importantly, for +ensuring that the commands it generates for Ninja to run are constructed +securely. ### 6.1 Invoking Ninja @@ -1013,15 +1019,15 @@ The command construction will follow this pattern: ### 6.2 The Criticality of Shell Escaping A primary security responsibility for Netsuke is the prevention of command -injection attacks. The `command` strings defined in a user's `Netsukefile` -are templates. When Netsuke substitutes variables like file paths into these +injection attacks. The `command` strings defined in a user's `Netsukefile` are +templates. When Netsuke substitutes variables like file paths into these templates, it is imperative that these substituted values are treated as single, literal arguments by the shell that Ninja ultimately uses to execute the command. -Without proper escaping, a malicious or even accidental filename like `"my file; -rm -rf /;.c"` could be interpreted as multiple commands, leading to catastrophic -consequences. +Without proper escaping, a malicious or even accidental filename like +`"my file; rm -rf /;.c"` could be interpreted as multiple commands, leading to +catastrophic consequences. For this critical task, the recommended crate is `shell-quote`. @@ -1032,39 +1038,39 @@ platform build tool. It also correctly handles a wide variety of input types, including byte strings and OS-native strings, which is essential for dealing with non-UTF8 file paths. The -`QuoteExt` trait provided by the crate offers an ergonomic and safe method -for building command strings by pushing quoted components into a buffer: +`QuoteExt` trait provided by the crate offers an ergonomic and safe method for +building command strings by pushing quoted components into a buffer: `script.push_quoted(Bash, "foo bar")`. ### 6.3 Implementation Strategy The command generation logic within the `ninja_gen.rs` module must not use -simple string formatting (like `format!`) to construct the final command strings -Instead, parse the Netsuke command template (e.g., `{{ cc }} -c {{ ins }} -o` -`{{ outs }}`) and build the final command string step by step. The placeholders -`{{ ins }}` and `{{ outs }}` are expanded to space-separated lists of file paths -within Netsuke itself, each path being shell-escaped using the `shell- quote` -API. When the command is written to `build.ninja`, these lists replace Ninja's -`$in` and `$out` macros. After substitution, the command is validated with -[`shlex`] () to ensure it parses correctly. -This approach guarantees that every dynamic part of the command is securely -quoted. +simple string formatting (like `format!`) to construct the final command +strings Instead, parse the Netsuke command template (e.g., +`{{ cc }} -c {{ ins }} -o` `{{ outs }}`) and build the final command string +step by step. The placeholders `{{ ins }}` and `{{ outs }}` are expanded to +space-separated lists of file paths within Netsuke itself, each path being +shell-escaped using the `shell- quote` API. When the command is written to +`build.ninja`, these lists replace Ninja's `$in` and `$out` macros. After +substitution, the command is validated with [`shlex`] +() to ensure it parses correctly. This +approach guarantees that every dynamic part of the command is securely quoted. ### 6.4 Automatic Security as a "Friendliness" Feature -The concept of being "friendlier" than `make` extends beyond syntactic sugar -to encompass safety and reliability. A tool that is easy to use but exposes the +The concept of being "friendlier" than `make` extends beyond syntactic sugar to +encompass safety and reliability. A tool that is easy to use but exposes the user to trivial security vulnerabilities is fundamentally unfriendly. In many build systems, the burden of correct shell quoting falls on the user, an error- prone task that requires specialized knowledge. Netsuke's design elevates security to a core feature by making it automatic and transparent. The user writes a simple, unquoted command template, and Netsuke -performs the complex and critical task of making it secure behind the scenes. By -integrating `shell-quote` directly into the Ninja file synthesis stage, Netsuke -protects users from a common and dangerous class of errors by default. This -approach embodies a deeper form of user-friendliness: one that anticipates and -mitigates risks on the user's behalf. +performs the complex and critical task of making it secure behind the scenes. +By integrating `shell-quote` directly into the Ninja file synthesis stage, +Netsuke protects users from a common and dangerous class of errors by default. +This approach embodies a deeper form of user-friendliness: one that anticipates +and mitigates risks on the user's behalf. ## Section 7: A Framework for Friendly and Actionable Error Reporting @@ -1083,13 +1089,13 @@ three fundamental questions: failed," "Build configuration is invalid"). 2. **Where** did it go wrong? Precise location information, including the file, - line number, and column where applicable (e.g., "in `Netsukefile` at line 15, - column 3"). + line number, and column where applicable (e.g., "in `Netsukefile` at line + 15, column 3"). 3. **Why** did it go wrong, and what can be done about it? The underlying cause of the error and a concrete suggestion for how to fix it (e.g., "Cause: - Found a tab character, which is not allowed. Hint: Use spaces for indentation - instead."). + Found a tab character, which is not allowed. Hint: Use spaces for + indentation instead."). ### 7.2 Crate Selection and Strategy: `anyhow` and `thiserror` @@ -1143,10 +1149,11 @@ enrichment: preserving the original error as its source. 3. A higher-level function in the call stack, which called the failing function, - receives this `Err` value. It uses the `.with_context()` method to - wrap the error with more application-level context. For example: - `ir::from_manifest(ast)` `.with_context(|| "Failed to build the internal - build graph from the manifest")?`. + receives this `Err` value. It uses the `.with_context()` method to wrap the + error with more application-level context. For example: + `ir::from_manifest(ast)` + `.with_context(|| "Failed to build the internal build graph from the manifest")?` + . 4. This process of propagation and contextualization repeats as the error bubbles up towards `main`. @@ -1154,8 +1161,8 @@ enrichment: 5. Finally, the `main` function receives the `Err` result. It prints the entire error chain provided by `anyhow`, which displays the highest-level context first, followed by a list of underlying "Caused by:" messages. This provides - the user with a rich, layered explanation of the failure, from the general to - the specific. + the user with a rich, layered explanation of the failure, from the general + to the specific. For automation use cases, Netsuke will support a `--diag-json` flag. When enabled, the entire error chain is serialized to JSON, allowing editors and CI @@ -1175,9 +1182,10 @@ actionable output that the implementation should produce. ## Section 8: Command-Line Interface (CLI) Design -The command-line interface is the user's entry point to Netsuke. A well-designed -CLI is essential for a good user experience. It should be intuitive, self- -documenting, and consistent with the conventions of modern command-line tools. +The command-line interface is the user's entry point to Netsuke. A +well-designed CLI is essential for a good user experience. It should be +intuitive, self- documenting, and consistent with the conventions of modern +command-line tools. ### 8.1 Crate Selection: `clap` @@ -1255,23 +1263,23 @@ The behaviour of each subcommand is clearly defined: as `ninja -t clean`, to remove the outputs of the build rules. - `Netsuke graph`: This command is an introspection and debugging tool. It will - run the Netsuke pipeline up to Stage 4 (IR Generation) and then invoke - Ninja with the graph tool, `ninja -t graph`. This outputs the complete build - dependency graph in the DOT language. The result can be piped through `dot - -Tsvg` or displayed via `netsuke graph --html` using an embedded Dagre.js - viewer. Visualising the graph is invaluable for understanding and debugging - complex projects. + run the Netsuke pipeline up to Stage 4 (IR Generation) and then invoke Ninja + with the graph tool, `ninja -t graph`. This outputs the complete build + dependency graph in the DOT language. The result can be piped through + `dot -Tsvg` or displayed via `netsuke graph --html` using an embedded + Dagre.js viewer. Visualising the graph is invaluable for understanding and + debugging complex projects. ### 8.4 Design Decisions The CLI is implemented using clap's derive API in `src/cli.rs`. Clap's -`default_value_t` attribute marks `Build` as the default subcommand, so invoking -`netsuke` with no explicit command still triggers a build. CLI execution and -dispatch live in `src/runner.rs`, keeping `main.rs` focused on parsing. The -working directory flag uses `-C` to mirror Ninja's convention, ensuring command -line arguments map directly onto the underlying build tool. Error scenarios are -validated using clap's `ErrorKind` enumeration in unit tests and via Cucumber -steps for behavioural coverage. +`default_value_t` attribute marks `Build` as the default subcommand, so +invoking `netsuke` with no explicit command still triggers a build. CLI +execution and dispatch live in `src/runner.rs`, keeping `main.rs` focused on +parsing. The working directory flag uses `-C` to mirror Ninja's convention, +ensuring command line arguments map directly onto the underlying build tool. +Error scenarios are validated using clap's `ErrorKind` enumeration in unit +tests and via Cucumber steps for behavioural coverage. ## Section 9: Implementation Roadmap and Strategic Recommendations @@ -1295,20 +1303,20 @@ goal. 1. Implement the initial `clap` CLI structure for the `build` command. - 2. Implement the YAML parser using `serde_yaml` and the AST data structures + 1. Implement the YAML parser using `serde_yaml` and the AST data structures (`ast.rs`). - 3. Implement the AST-to-IR transformation logic, including basic validation + 1. Implement the AST-to-IR transformation logic, including basic validation like checking for rule existence. - 4. Implement the IR-to-Ninja file generator (`ninja_gen.rs`). + 1. Implement the IR-to-Ninja file generator (`ninja_gen.rs`). - 5. Implement the `std::process::Command` logic to invoke `ninja`. + 1. Implement the `std::process::Command` logic to invoke `ninja`. - **Success Criterion:** Netsuke can successfully take a `Netsukefile` file *without any Jinja syntax* and compile it to a `build.ninja` file, then - execute it to produce the correct artifacts. This phase validates the entire - static compilation pipeline. + execute it to produce the correct artifacts. This phase validates the + entire static compilation pipeline. - **Phase 2: The Dynamic Engine** @@ -1319,18 +1327,18 @@ goal. 1. Integrate the `minijinja` crate into the build pipeline. - 2. Implement the two-pass parsing mechanism: first render the manifest with + 1. Implement the two-pass parsing mechanism: first render the manifest with `minijinja`, then parse the result with `serde_yaml`. - 3. Populate the initial Jinja context with the global `vars` from the + 1. Populate the initial Jinja context with the global `vars` from the manifest. - 4. Implement basic Jinja control flow (`{% if... %}`, `{% for... %}`) and + 1. Implement basic Jinja control flow (`{% if... %}`, `{% for... %}`) and variable substitution. - **Success Criterion:** Netsuke can successfully build a manifest that uses - variables and conditional logic (e.g., different compiler flags based on - a variable). + variables and conditional logic (e.g., different compiler flags based on a + variable). - **Phase 3: The "Friendly" Polish** @@ -1342,15 +1350,15 @@ goal. 1. Implement the full suite of custom Jinja functions (`glob`, `env`, etc.) and filters (`shell_escape`). - 2. Mandate the use of `shell-quote` for all command variable substitutions. + 1. Mandate the use of `shell-quote` for all command variable substitutions. - 3. Refactor the error handling to fully adopt the `anyhow`/`thiserror` + 1. Refactor the error handling to fully adopt the `anyhow`/`thiserror` strategy, ensuring all user-facing errors are contextual and actionable as specified in Section 7. - 4. Implement the `clean` and `graph` subcommands. + 1. Implement the `clean` and `graph` subcommands. - 5. Refine the CLI output for clarity and readability. + 1. Refine the CLI output for clarity and readability. - **Success Criterion:** Netsuke is a feature-complete, secure, and user-friendly build tool that meets all the initial design goals. @@ -1376,9 +1384,9 @@ powerful build tool. The use of a decoupled IR, in particular, opens up many possibilities for future enhancements beyond the initial scope. - **Advanced Caching:** While Ninja provides excellent file-based incremental - build caching, Netsuke could implement a higher-level artifact caching - layer. This could involve caching build outputs in a shared network location - (e.g., S3) or a local content-addressed store, allowing for cache hits across + build caching, Netsuke could implement a higher-level artifact caching layer. + This could involve caching build outputs in a shared network location (e.g., + S3) or a local content-addressed store, allowing for cache hits across different machines or clean checkouts. - **Plugin Architecture:** A system could be designed to allow users to load @@ -1387,8 +1395,8 @@ possibilities for future enhancements beyond the initial scope. changes to the core application. - **Language-Specific Toolchains:** Netsuke could offer pre-packaged "toolchain" - modules. For example, a `Netsuke-rust-toolchain` could provide a standard - set of rules and variables for compiling Rust projects, abstracting away the + modules. For example, a `Netsuke-rust-toolchain` could provide a standard set + of rules and variables for compiling Rust projects, abstracting away the details of invoking `cargo`. - **Distributed Builds:** The IR is backend-agnostic. A future version of @@ -1398,8 +1406,8 @@ possibilities for future enhancements beyond the initial scope. ## Section 10: Example Manifests -The repository includes several complete Netsuke manifests in the `examples/ -` directory. They demonstrate how the YAML schema can be applied to real-world +The repository includes several complete Netsuke manifests in the `examples/` +directory. They demonstrate how the YAML schema can be applied to real-world projects. - [`basic_c.yml`](../examples/basic_c.yml): a minimal C project compiling two diff --git a/docs/roadmap.md b/docs/roadmap.md index 78f230c0..25280772 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,8 +1,8 @@ # Netsuke Implementation Roadmap -This roadmap translates the [netsuke-design.md](netsuke-design.md) document into -a phased, actionable implementation plan. Each phase has a clear objective and a -checklist of tasks that must be completed to meet the success criteria. +This roadmap translates the [netsuke-design.md](netsuke-design.md) document +into a phased, actionable implementation plan. Each phase has a clear objective +and a checklist of tasks that must be completed to meet the success criteria. ## Phase 1: The Static Core 🏗️ diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index abdfedbb..62c1f0ca 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -1,15 +1,15 @@ # Mastering Test Fixtures in Rust with `rstest` Testing is an indispensable part of modern software development, ensuring code -reliability, maintainability, and correctness. In the Rust ecosystem, while -the built-in testing framework provides a solid foundation, managing test +reliability, maintainability, and correctness. In the Rust ecosystem, while the +built-in testing framework provides a solid foundation, managing test dependencies and creating parameterized tests can become verbose. The `rstest` crate () emerges as a powerful solution, offering a sophisticated fixture-based and parameterized testing framework that significantly simplifies these tasks through the use of procedural macros. This document provides a comprehensive exploration of `rstest`, from fundamental -concepts to advanced techniques, enabling Rust developers to write cleaner, more -expressive, and robust tests. +concepts to advanced techniques, enabling Rust developers to write cleaner, +more expressive, and robust tests. ## I. Introduction to `rstest` and Test Fixtures in Rust @@ -35,31 +35,32 @@ Fundamentally, the use of fixtures promotes a crucial separation of concerns: the *preparation* of the test environment is decoupled from the *execution* of the test logic. Traditional testing approaches often intermingle setup, action, and assertion logic within a single test function. This can result in lengthy -and convoluted tests that are difficult to comprehend at a glance. By extracting -the setup logic into reusable components (fixtures), the actual test functions -become shorter, more focused, and thus more readable and maintainable. +and convoluted tests that are difficult to comprehend at a glance. By +extracting the setup logic into reusable components (fixtures), the actual test +functions become shorter, more focused, and thus more readable and maintainable. ### B. Introducing `rstest`: Simplifying Fixture-Based Testing in Rust `rstest` is a Rust crate specifically designed to simplify and enhance testing by leveraging the concept of fixtures and providing powerful parameterization -capabilities. It is available on `crates.io` and its source code is hosted -at , distinguishing it from other software -projects that may share the same name but operate in different ecosystems (e.g., -a JavaScript/TypeScript framework mentioned). +capabilities. It is available on `crates.io` and its source code is hosted at +, distinguishing it from other software +projects that may share the same name but operate in different ecosystems +(e.g., a JavaScript/TypeScript framework mentioned). The `rstest` crate utilizes Rust's procedural macros, such as `#[rstest]` and `#[fixture]`, to achieve its declarative and expressive syntax. These macros allow developers to define fixtures and inject them into test functions simply -by listing them as arguments. This compile-time mechanism analyzes test function -signatures and fixture definitions to wire up dependencies automatically. - -This reliance on procedural macros is a key architectural decision. It -enables `rstest` to offer a remarkably clean and intuitive syntax at the test- -writing level. Developers declare the dependencies their tests need, and the -macros handle the resolution and injection. While this significantly improves -the developer experience for writing tests, the underlying macro expansion -involves compile-time code generation. This complexity, though hidden, can have +by listing them as arguments. This compile-time mechanism analyzes test +function signatures and fixture definitions to wire up dependencies +automatically. + +This reliance on procedural macros is a key architectural decision. It enables +`rstest` to offer a remarkably clean and intuitive syntax at the test- writing +level. Developers declare the dependencies their tests need, and the macros +handle the resolution and injection. While this significantly improves the +developer experience for writing tests, the underlying macro expansion involves +compile-time code generation. This complexity, though hidden, can have implications for build times, particularly in large test suites. Furthermore, understanding the macro expansion can sometimes be necessary for debugging complex test scenarios or unexpected behaviour. @@ -72,11 +73,11 @@ quality and developer productivity: - **Readability:** By injecting dependencies as function arguments, `rstest` makes the requirements of a test explicit and easy to understand. The test function's signature clearly documents what it needs to run. This allows - developers to "focus on the important stuff in your tests" by abstracting away - the setup details. + developers to "focus on the important stuff in your tests" by abstracting + away the setup details. - **Reusability:** Fixtures defined with `rstest` are reusable components. A - single fixture, such as one setting up a database connection or creating - a complex data structure, can be used across multiple tests, eliminating + single fixture, such as one setting up a database connection or creating a + complex data structure, can be used across multiple tests, eliminating redundant setup code. - **Reduced Boilerplate:** `rstest` significantly cuts down on repetitive setup and teardown code. Parameterization features, like `#[case]` and `#[values]`, @@ -84,12 +85,13 @@ quality and developer productivity: variations from a single function. The declarative nature of `rstest` is central to these benefits. Instead of -imperatively writing setup code within each test (the *how*), developers declare -the fixtures they need (the *what*) in the test function's signature. This -shifts the cognitive load from managing setup details in every test to designing -a system of well-defined, reusable fixtures. Over time, particularly in larger -projects, this can lead to a more robust, maintainable, and understandable test -suite as common setup patterns are centralized and managed effectively. +imperatively writing setup code within each test (the *how*), developers +declare the fixtures they need (the *what*) in the test function's signature. +This shifts the cognitive load from managing setup details in every test to +designing a system of well-defined, reusable fixtures. Over time, particularly +in larger projects, this can lead to a more robust, maintainable, and +understandable test suite as common setup patterns are centralized and managed +effectively. ## II. Getting Started with `rstest` @@ -119,8 +121,8 @@ libraries. This convention prevents testing utilities from being included in production binaries, which helps keep them small and reduces compile times for non-test builds. -When leveraging Tokio's test utilities—for example `tokio::time::pause` or -the I/O helpers in `tokio-test`—enable the `test-util` feature via a dev-only +When leveraging Tokio's test utilities—for example `tokio::time::pause` or the +I/O helpers in `tokio-test`—enable the `test-util` feature via a dev-only dependency: ```toml @@ -131,9 +133,9 @@ rstest = "0.18" ### B. Your First Fixture: Defining with `#[fixture]` -A fixture in `rstest` is essentially a Rust function that provides some data -or performs some setup action, with its result being injectable into tests. -To designate a function as a fixture, it is annotated with the `#[fixture]` +A fixture in `rstest` is essentially a Rust function that provides some data or +performs some setup action, with its result being injectable into tests. To +designate a function as a fixture, it is annotated with the `#[fixture]` attribute. Consider a simple fixture that provides a numeric value: @@ -147,21 +149,23 @@ pub fn answer_to_life() -> u32 { } ``` -In this example, `answer_to_life` is a public function marked with `#[fixture]`. -It takes no arguments and returns a `u32` value of 42. The `#[fixture]` macro -effectively registers this function with the `rstest` system, transforming it -into a component that `rstest` can discover and utilize. The return type of -the fixture function (here, `u32`) defines the type of the data that will be -injected into tests requesting this fixture. Fixtures can return any valid Rust -type, from simple primitives to complex structs or trait objects. Fixtures can -also depend on other fixtures, allowing for compositional setup. +In this example, `answer_to_life` is a public function marked with +`#[fixture]`. It takes no arguments and returns a `u32` value of 42. The +`#[fixture]` macro effectively registers this function with the `rstest` +system, transforming it into a component that `rstest` can discover and +utilize. The return type of the fixture function (here, `u32`) defines the type +of the data that will be injected into tests requesting this fixture. Fixtures +can return any valid Rust type, from simple primitives to complex structs or +trait objects. Fixtures can also depend on other fixtures, allowing for +compositional setup. ### C. Injecting Fixtures into Tests with `#[rstest]` Once a fixture is defined, it can be used in a test function. Test functions that utilize `rstest` features, including fixture injection, must be annotated -with the `#[rstest]` attribute. The fixture is then injected by simply declaring -an argument in the test function with the same name as the fixture function. +with the `#[rstest]` attribute. The fixture is then injected by simply +declaring an argument in the test function with the same name as the fixture +function. Here’s how to use the `answer_to_life` fixture in a test: @@ -180,16 +184,16 @@ fn test_with_fixture(answer_to_life: u32) { ``` In `test_with_fixture`, the argument `answer_to_life: u32` signals to `rstest` -that the `answer_to_life` fixture should be injected. `rstest` resolves this -by name: it looks for a fixture function named `answer_to_life`, calls it, and +that the `answer_to_life` fixture should be injected. `rstest` resolves this by +name: it looks for a fixture function named `answer_to_life`, calls it, and passes its return value as the argument to the test function. The argument name in the test function serves as the primary key for fixture resolution. This convention makes usage intuitive but necessitates careful -naming of fixtures to avoid ambiguity, especially if multiple fixtures with -the same name exist in different modules but are brought into the same scope. -`rstest` generally follows Rust's standard name resolution rules, meaning -an identically named fixture can be used in different contexts depending on +naming of fixtures to avoid ambiguity, especially if multiple fixtures with the +same name exist in different modules but are brought into the same scope. +`rstest` generally follows Rust's standard name resolution rules, meaning an +identically named fixture can be used in different contexts depending on visibility and `use` declarations. ## III. Mastering Fixture Injection and Basic Usage @@ -202,8 +206,8 @@ leveraging `rstest` effectively. The flexibility of `rstest` fixtures allows them to provide a wide array of data types and perform various setup tasks. Fixtures are not limited by the kind of data they can return; any valid Rust type is permissible. This enables -fixtures to encapsulate diverse setup logic, providing ready-to-use dependencies -for tests. +fixtures to encapsulate diverse setup logic, providing ready-to-use +dependencies for tests. Here are a few examples illustrating different kinds of fixtures: @@ -298,13 +302,13 @@ implementation. ### B. Understanding fixture scope and lifetime (default behaviour) By default, `rstest` calls a fixture function anew for each test that uses it. -This means if five different tests inject the same fixture, the fixture function -will be executed five times, and each test will receive a fresh, independent -instance of the fixture's result. This behaviour is crucial for test isolation. -The `rstest` macro effectively desugars a test like `fn the_test(injected: i32)` -into something conceptually similar to `#[test] fn the_test() { let injected = -injected_fixture_func(); /* … */ }` within the test body, implying a new call -each time. +This means if five different tests inject the same fixture, the fixture +function will be executed five times, and each test will receive a fresh, +independent instance of the fixture's result. This behaviour is crucial for +test isolation. The `rstest` macro effectively desugars a test like +`fn the_test(injected: i32)` into something conceptually similar to +`#[test] fn the_test() { let injected = injected_fixture_func(); /* … */ }` +within the test body, implying a new call each time. Test isolation prevents the state from one test from inadvertently affecting another. If fixtures were shared by default, a mutation to a fixture's state in @@ -319,9 +323,9 @@ concern or when the cost of fixture creation is prohibitive. ## IV. Parameterized Tests with `rstest` -`rstest` excels at creating parameterized tests, allowing a single test logic to -be executed with multiple sets of input data. This is achieved primarily through -the `#[case]` and `#[values]` attributes. +`rstest` excels at creating parameterized tests, allowing a single test logic +to be executed with multiple sets of input data. This is achieved primarily +through the `#[case]` and `#[values]` attributes. ### A. Table-Driven Tests with `#[case]`: Defining Specific Scenarios @@ -357,19 +361,20 @@ fn test_fibonacci(#[case] input: u32, #[case] expected: u32) { For each `#[case(input_val, expected_val)]` line, `rstest` generates a separate, independent test. If one case fails, the others are still executed -and reported individually by the test runner. These generated tests are -often named by appending `::case_N` to the original test function name (e.g., +and reported individually by the test runner. These generated tests are often +named by appending `::case_N` to the original test function name (e.g., `test_fibonacci::case_1`, `test_fibonacci::case_2`, etc.), which aids in -identifying specific failing cases. This individual reporting mechanism provides -clearer feedback than a loop within a single test, where the first failure might -obscure subsequent ones. +identifying specific failing cases. This individual reporting mechanism +provides clearer feedback than a loop within a single test, where the first +failure might obscure subsequent ones. ### B. Combinatorial Testing with `#[values]`: Generating Test Matrices The `#[values(…)]` attribute is used on test function arguments to generate tests for every possible combination of the provided values (the Cartesian -product). This is particularly useful for testing interactions between different -parameters or ensuring comprehensive coverage across various input states. +product). This is particularly useful for testing interactions between +different parameters or ensuring comprehensive coverage across various input +states. Consider testing a state machine's transition logic based on current state and an incoming event: @@ -413,24 +418,25 @@ all combinations of `initial_state` and `event` specified in the `#[values]` attributes. It is important to be mindful that the number of generated tests can grow very -rapidly with `#[values]`. If a test function has three arguments, each with -ten values specified via `#[values]`, 10×10×10=1000 tests will be generated. -This combinatorial explosion can significantly impact test execution time -and even compile times. Developers must balance the desire for exhaustive -combinatorial coverage against these practical constraints, perhaps by selecting +rapidly with `#[values]`. If a test function has three arguments, each with ten +values specified via `#[values]`, 10×10×10=1000 tests will be generated. This +combinatorial explosion can significantly impact test execution time and even +compile times. Developers must balance the desire for exhaustive combinatorial +coverage against these practical constraints, perhaps by selecting representative values or using `#[case]` for more targeted scenarios. ### C. Using Fixtures within Parameterized Tests -Fixtures can be seamlessly combined with parameterized arguments (`#[case]` -or `#[values]`) in the same test function. This powerful combination allows -for testing different aspects of a component (varied by parameters) within a -consistent environment or context (provided by fixtures). The "Complete Example" -in the `rstest` documentation hints at this synergy, stating that all features -can be used together, mixing fixture variables, fixed cases, and value lists. +Fixtures can be seamlessly combined with parameterized arguments (`#[case]` or +`#[values]`) in the same test function. This powerful combination allows for +testing different aspects of a component (varied by parameters) within a +consistent environment or context (provided by fixtures). The "Complete +Example" in the `rstest` documentation hints at this synergy, stating that all +features can be used together, mixing fixture variables, fixed cases, and value +lists. -For example, a test might use a fixture to obtain a database connection and then -use `#[case]` arguments to test operations with different user IDs: +For example, a test might use a fixture to obtain a database connection and +then use `#[case]` arguments to test operations with different user IDs: ```rust use rstest::*; @@ -493,12 +499,12 @@ fn test_composed_fixture_with_override(#[with("special_")] configured_item: Stri } ``` -In this example, `derived_value` depends on `base_value`, and -`configured_item` depends on `derived_value`. When `test_composed_fixture` -requests `configured_item`, `rstest` first calls `base_value()`, then +In this example, `derived_value` depends on `base_value`, and `configured_item` +depends on `derived_value`. When `test_composed_fixture` requests +`configured_item`, `rstest` first calls `base_value()`, then `derived_value(10)`, and finally `configured_item(20, "item_".to_string())`. -This hierarchical dependency resolution mirrors good software design principles, -promoting modularity and maintainability in test setups. +This hierarchical dependency resolution mirrors good software design +principles, promoting modularity and maintainability in test setups. ### B. Controlling Fixture Initialization: `#[once]` for Shared State @@ -537,8 +543,8 @@ When using `#[once]`, there are critical warnings: 1. **Resource Lifetime:** The value returned by an `#[once]` fixture is effectively promoted to a `static` lifetime and is **never dropped**. This means any resources it holds (e.g., file handles, network connections) that - require explicit cleanup via `Drop` will not be cleaned up automatically - at the end of the test suite. This makes `#[once]` fixtures best suited for + require explicit cleanup via `Drop` will not be cleaned up automatically at + the end of the test suite. This makes `#[once]` fixtures best suited for truly passive data or resources whose cleanup is managed by the operating system upon process exit. 2. **Functional Limitations:** `#[once]` fixtures cannot be `async` functions @@ -548,18 +554,19 @@ When using `#[once]`, there are critical warnings: attributes. If you rely on lint expectations, use `#[allow]` instead to silence false positives. -The "never dropped" behaviour arises because `rstest` typically creates -a `static` variable to hold the result of the `#[once]` fixture. `static` +The "never dropped" behaviour arises because `rstest` typically creates a +`static` variable to hold the result of the `#[once]` fixture. `static` variables in Rust live for the entire duration of the program, and their `Drop` implementations are not usually called at program exit. This is a crucial consideration for resource management. ### C. Renaming Fixtures for Clarity: The `#[from]` Attribute -Sometimes a fixture's function name might be long and descriptive, but a shorter -or different name is preferred for the argument in a test or another fixture. -The `#[from(original_fixture_name)]` attribute on an argument allows renaming. -This is particularly useful when destructuring the result of a fixture. +Sometimes a fixture's function name might be long and descriptive, but a +shorter or different name is preferred for the argument in a test or another +fixture. The `#[from(original_fixture_name)]` attribute on an argument allows +renaming. This is particularly useful when destructuring the result of a +fixture. ```rust use rstest::*; @@ -581,10 +588,10 @@ fn test_with_destructured_fixture(#[from(complex_user_data_fixture)] (name, _, _ ``` The `#[from]` attribute decouples the fixture's actual function name from the -variable name used within the consuming function. As shown, if a fixture returns -a tuple or struct and the test only cares about some parts or wants to use more -idiomatic names for destructured elements, `#[from]` is essential to link the -argument pattern to the correct source fixture. +variable name used within the consuming function. As shown, if a fixture +returns a tuple or struct and the test only cares about some parts or wants to +use more idiomatic names for destructured elements, `#[from]` is essential to +link the argument pattern to the correct source fixture. ### D. Partial Fixture Injection & Default Arguments @@ -670,12 +677,12 @@ fn check_socket_port(#[case] addr: SocketAddr, #[case] expected_port: u16) { } ``` -In this test, `rstest` sees the argument `addr: SocketAddr` -and the string literal `"127.0.0.1:8080"`. It implicitly calls +In this test, `rstest` sees the argument `addr: SocketAddr` and the string +literal `"127.0.0.1:8080"`. It implicitly calls `SocketAddr::from_str("127.0.0.1:8080")` to create the `SocketAddr` instance. -This "magic" conversion makes test definitions more concise and readable -by allowing the direct use of string representations for types that support -it. However, if the `FromStr` conversion fails (e.g., because of a malformed +This "magic" conversion makes test definitions more concise and readable by +allowing the direct use of string representations for types that support it. +However, if the `FromStr` conversion fails (e.g., because of a malformed string), the error will typically occur at test runtime, potentially leading to a panic. For types with complex parsing logic or many failure modes, it might be clearer to perform the conversion explicitly within a fixture or at the @@ -684,8 +691,9 @@ diagnostic messages. ## VI. Asynchronous Testing with `rstest` -`rstest` provides robust support for testing asynchronous Rust code, integrating -with common async runtimes and offering syntactic sugar for managing futures. +`rstest` provides robust support for testing asynchronous Rust code, +integrating with common async runtimes and offering syntactic sugar for +managing futures. ### A. Defining Asynchronous Fixtures (`async fn`) @@ -712,12 +720,12 @@ default async runtime support, but the fixture logic can be any async code. ### B. Writing Asynchronous Tests (`async fn` with `#[rstest]`) -Test functions themselves can also be `async fn`. `rstest` will manage -the execution of these async tests. By default, `rstest` often uses +Test functions themselves can also be `async fn`. `rstest` will manage the +execution of these async tests. By default, `rstest` often uses `#[async_std::test]` to annotate the generated async test functions. However, -it is designed to be largely runtime-agnostic and can be integrated with -other popular async runtimes like Tokio or Actix. This is typically done -by adding the runtime's specific test attribute (e.g., `#[tokio::test]` or +it is designed to be largely runtime-agnostic and can be integrated with other +popular async runtimes like Tokio or Actix. This is typically done by adding +the runtime's specific test attribute (e.g., `#[tokio::test]` or `#[actix_rt::test]`) alongside `#[rstest]`. ```rust @@ -739,14 +747,14 @@ async fn my_async_test(async_fixture_value: u32) { } ``` -The order of procedural macro attributes can sometimes matter. While -`rstest` documentation and examples show flexibility (e.g., `#[rstest]` then +The order of procedural macro attributes can sometimes matter. While `rstest` +documentation and examples show flexibility (e.g., `#[rstest]` then `#[tokio::test]`, or vice versa), users should ensure their chosen async runtime's test macro is correctly placed to provide the necessary execution -context for the async test body and any async fixtures. `rstest` itself does not -bundle a runtime; it integrates with existing ones. The "Inject Test Attribute" -feature mentioned in `rstest` documentation may offer more explicit control over -which test runner attribute is applied. +context for the async test body and any async fixtures. `rstest` itself does +not bundle a runtime; it integrates with existing ones. The "Inject Test +Attribute" feature mentioned in `rstest` documentation may offer more explicit +control over which test runner attribute is applied. ### C. Managing Futures: `#[future]` and `#[awt]` Attributes @@ -812,8 +820,8 @@ away some of the explicit `async`/`.await` mechanics. Long-running or stalled asynchronous operations can cause tests to hang indefinitely. `rstest` provides a `#[timeout(…)]` attribute to set a maximum -execution time for async tests. This feature typically relies on the `async- -timeout` feature of `rstest`, which is enabled by default. +execution time for async tests. This feature typically relies on the +`async- timeout` feature of `rstest`, which is enabled by default. ```rust use rstest::*; @@ -900,16 +908,16 @@ fn test_read_from_temp_file(temp_file_with_content: PathBuf) { ``` By encapsulating temporary resource management within fixtures, tests become -cleaner and less prone to errors related to resource setup or cleanup. The -RAII (Resource Acquisition Is Initialization) pattern, common in Rust and -exemplified by `tempfile::TempDir` (which cleans up the directory when dropped), -works effectively with `rstest`'s fixture model. When a regular (non-`#[once]`) -fixture returns a `TempDir` object, or an object that owns it, the resource is -typically cleaned up after the test finishes, as the fixture's return value goes -out of scope. This localizes resource management logic to the fixture, keeping -the test focused on its assertions. For temporary resources, regular (per- -test) fixtures are generally preferred over `#[once]` fixtures to ensure proper -cleanup, as `#[once]` fixtures are never dropped. +cleaner and less prone to errors related to resource setup or cleanup. The RAII +(Resource Acquisition Is Initialization) pattern, common in Rust and +exemplified by `tempfile::TempDir` (which cleans up the directory when +dropped), works effectively with `rstest`'s fixture model. When a regular +(non-`#[once]`) fixture returns a `TempDir` object, or an object that owns it, +the resource is typically cleaned up after the test finishes, as the fixture's +return value goes out of scope. This localizes resource management logic to the +fixture, keeping the test focused on its assertions. For temporary resources, +regular (per- test) fixtures are generally preferred over `#[once]` fixtures to +ensure proper cleanup, as `#[once]` fixtures are never dropped. ### B. Mocking External Services (e.g., Database Connections, HTTP APIs) @@ -918,10 +926,11 @@ or HTTP APIs, mocking is a crucial technique. Mocks allow tests to run in isolation, without relying on real external systems, making them faster and more reliable. `rstest` fixtures are an ideal place to encapsulate the setup and configuration of mock objects. Crates like `mockall` can be used to create -mocks, or they can be hand-rolled. The fixture would then provide the configured -mock instance to the test. General testing advice also strongly recommends -mocking external dependencies. The `rstest` documentation itself shows examples -with fakes or mocks like `empty_repository` and `string_processor`. +mocks, or they can be hand-rolled. The fixture would then provide the +configured mock instance to the test. General testing advice also strongly +recommends mocking external dependencies. The `rstest` documentation itself +shows examples with fakes or mocks like `empty_repository` and +`string_processor`. A conceptual example using a hypothetical mocking library: @@ -1002,14 +1011,14 @@ readable and maintainable. ### C. Using `#[files(…)]` for Test Input from Filesystem Paths -For tests that need to process data from multiple input files, `rstest` provides -the `#[files("glob_pattern")]` attribute. This attribute can be used on a test -function argument to inject file paths that match a given glob pattern. The -argument type is typically `PathBuf`. It can also inject file contents directly -as `&str` or `&[u8]` by specifying a mode, e.g., `#[files("glob_pattern", mode -= "str")]`, and additional attributes such as `#[base_dir = "…"]` can specify -a base directory for the glob, and `#[exclude("regex")]` can filter out paths -matching a regular expression. +For tests that need to process data from multiple input files, `rstest` +provides the `#[files("glob_pattern")]` attribute. This attribute can be used +on a test function argument to inject file paths that match a given glob +pattern. The argument type is typically `PathBuf`. It can also inject file +contents directly as `&str` or `&[u8]` by specifying a mode, e.g., +`#[files("glob_pattern", mode = "str")]`, and additional attributes such as +`#[base_dir = "…"]` can specify a base directory for the glob, and +`#[exclude("regex")]` can filter out paths matching a regular expression. ```rust use rstest::*; @@ -1046,15 +1055,15 @@ significantly increase binary size if used with large data files. ## VIII. Reusability and Organization As test suites grow, maintaining reusability and clear organization becomes -paramount. `rstest` and its ecosystem provide tools and encourage practices that -support these goals. +paramount. `rstest` and its ecosystem provide tools and encourage practices +that support these goals. ### A. Leveraging `rstest_reuse` for Test Templates While `rstest`'s `#[case]` attribute is excellent for parameterization, repeating the same set of `#[case]` attributes across multiple test functions -can lead to duplication. The `rstest_reuse` crate addresses this by allowing the -definition of reusable test templates. +can lead to duplication. The `rstest_reuse` crate addresses this by allowing +the definition of reusable test templates. `rstest_reuse` introduces two main attributes: @@ -1121,21 +1130,21 @@ for maintainability and scalability. `src/lib.rs` or `src/fixtures.rs` under `#[cfg(test)]`) and `use` them in integration tests. - **Naming Conventions:** Use clear, descriptive names for fixtures that - indicate what they provide or set up. Test function names should clearly state - what behaviour they are verifying. + indicate what they provide or set up. Test function names should clearly + state what behaviour they are verifying. - **Fixture Responsibility:** Aim for fixtures with a single, well-defined responsibility. Complex setups can be achieved by composing smaller, focused fixtures. - **Scope Management (**`#[once]` **vs. Regular):** Make conscious decisions about fixture lifetimes. Use `#[once]` sparingly, only for genuinely - expensive, read-only, and safely static resources, being mindful of its "never - dropped" nature. Prefer regular (per-test) fixtures for test isolation and - proper resource management. + expensive, read-only, and safely static resources, being mindful of its + "never dropped" nature. Prefer regular (per-test) fixtures for test isolation + and proper resource management. - **Modularity:** Group related fixtures and tests into modules. This improves navigation and understanding of the test suite. - **Readability:** Utilize features like `#[from]` for renaming and - `#[default]` / `#[with]` for configurable fixtures to enhance the clarity of - both fixture definitions and their usage in tests. + `#[default]` / `#[with]` for configurable fixtures to enhance the clarity + of both fixture definitions and their usage in tests. General testing advice, such as keeping tests small and focused and mocking external dependencies, also applies and is well-supported by `rstest`'s design. @@ -1147,21 +1156,21 @@ potential trade-offs helps in deciding when and how to best utilize it. ### A. `rstest` vs. Standard Rust `#[test]` and Manual Setup -Standard Rust testing using just the `#[test]` attribute is functional but -can become verbose for scenarios involving shared setup or parameterization. +Standard Rust testing using just the `#[test]` attribute is functional but can +become verbose for scenarios involving shared setup or parameterization. `rstest` offers significant improvements in these areas: - **Fixture Management:** With standard `#[test]`, shared setup typically involves calling helper functions manually at the beginning of each test. `rstest` automates this via declarative fixture injection. - **Parameterization:** Achieving table-driven tests with standard `#[test]` - often requires writing loops inside a single test function (which has - poor failure reporting for individual cases) or creating multiple distinct + often requires writing loops inside a single test function (which has poor + failure reporting for individual cases) or creating multiple distinct `#[test]` functions with slight variations. `rstest`'s `#[case]` and `#[values]` attributes provide a much cleaner and more powerful solution. - **Readability and Boilerplate:** `rstest` generally leads to less boilerplate - code and more readable tests because dependencies are explicit in the function - signature, and parameterization is handled declaratively. + code and more readable tests because dependencies are explicit in the + function signature, and parameterization is handled declaratively. The following table summarizes key differences: @@ -1211,11 +1220,12 @@ mind: macros expand. - **Debugging Parameterized Tests:** `rstest` generates individual test functions for parameterized cases, often named like - `test_function_name::case_N`. Understanding this naming convention is - helpful for identifying and running specific failing cases with `cargo test - test_function_name::case_N`. Some IDEs or debuggers might require specific - configurations or might not fully support stepping through the macro-generated - code as seamlessly as handwritten code, though support is improving. + `test_function_name::case_N`. Understanding this naming convention is helpful + for identifying and running specific failing cases with + `cargo test test_function_name::case_N`. Some IDEs or debuggers might require + specific configurations or might not fully support stepping through the + macro-generated code as seamlessly as handwritten code, though support is + improving. - **Static Nature of Test Cases:** Test cases (e.g., from `#[case]` or `#[files]`) are defined and discovered at compile time. This means the structure of the tests is validated by the Rust compiler, which can catch @@ -1227,8 +1237,8 @@ mind: known at compile time) is not directly supported by `rstest`'s core model. - `no_std` **Support:** `rstest` generally relies on the standard library (`std`) being available, as test runners and many common testing utilities - depend on `std`. Therefore, it is typically not suitable for testing `#! - [no_std]` libraries in a truly `no_std` test environment where the test + depend on `std`. Therefore, it is typically not suitable for testing + `#! [no_std]` libraries in a truly `no_std` test environment where the test harness itself cannot link `std`. - **Learning Curve:** While designed for simplicity in basic use cases, the full range of attributes and advanced features (e.g., fixture composition, partial @@ -1245,8 +1255,8 @@ For developers who rely on logging frameworks like `log` or `tracing` for debugging tests, the `rstest-log` crate can simplify integration. Test runners often capture standard output and error streams, and logging frameworks require proper initialization. `rstest-log` likely provides attributes or wrappers to -ensure that logging is correctly set up before each `rstest`-generated test case -runs, making it easier to get consistent log output from tests. +ensure that logging is correctly set up before each `rstest`-generated test +case runs, making it easier to get consistent log output from tests. ### B. `logtest`: Verifying Log Output @@ -1276,15 +1286,15 @@ are logged under specific conditions. The `test-with` crate allows for conditional execution of tests based on various runtime conditions, such as the presence of environment variables, the -existence of specific files or folders, or the availability of network services. -It can be used with `rstest`. For example, an `rstest` test could be further -annotated with `test-with` attributes to ensure it only runs if a particular -database configuration file exists or if a dependent web service is reachable. -The order of macros is important: `rstest` should typically generate the test -cases first, and then `test-with` can apply its conditional execution logic to -these generated tests. This allows `rstest` to focus on test structure and data -provision, while `test-with` provides an orthogonal layer of control over test -execution conditions. +existence of specific files or folders, or the availability of network +services. It can be used with `rstest`. For example, an `rstest` test could be +further annotated with `test-with` attributes to ensure it only runs if a +particular database configuration file exists or if a dependent web service is +reachable. The order of macros is important: `rstest` should typically generate +the test cases first, and then `test-with` can apply its conditional execution +logic to these generated tests. This allows `rstest` to focus on test structure +and data provision, while `test-with` provides an orthogonal layer of control +over test execution conditions. ## XI. Conclusion and Further Resources @@ -1299,8 +1309,8 @@ suites. While considerations such as compile-time impact and the learning curve for advanced features exist, the benefits in terms of cleaner, more robust, and -more expressive tests often outweigh these for projects with non-trivial testing -requirements. +more expressive tests often outweigh these for projects with non-trivial +testing requirements. ### A. Recap of `rstest`'s Power for Fixture-Based Testing @@ -1349,6 +1359,6 @@ provided by `rstest`: | #[timeout(…)] | Sets a timeout for an asynchronous test. | | #[files("glob_pattern",…)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. | -By mastering `rstest`, Rust developers can significantly elevate the quality and -efficiency of their testing practices, leading to more reliable and maintainable -software. +By mastering `rstest`, Rust developers can significantly elevate the quality +and efficiency of their testing practices, leading to more reliable and +maintainable software. diff --git a/docs/snapshot-testing-in-netsuke-using-insta.md b/docs/snapshot-testing-in-netsuke-using-insta.md index e42f8e65..5d465463 100644 --- a/docs/snapshot-testing-in-netsuke-using-insta.md +++ b/docs/snapshot-testing-in-netsuke-using-insta.md @@ -1,8 +1,8 @@ # Snapshot Testing IR and Ninja Outputs in Netsuke Snapshot testing with the `insta` crate provides a powerful way to ensure -Netsuke’s intermediate representations and generated Ninja build files -remain correct over time. According to the Netsuke design, the Intermediate +Netsuke’s intermediate representations and generated Ninja build files remain +correct over time. According to the Netsuke design, the Intermediate Representation (IR) is a backend-agnostic build graph, and the Ninja file generation is a separate stage built on that IR. Leverage this separation by writing **separate snapshot tests** for IR and for Ninja output. This guide @@ -26,8 +26,8 @@ tool `cargo-insta` for reviewing or updating snapshots (useful in CI and local development). **Project Structure:** Organize the tests in the `tests/` directory, using one -module for IR snapshots and another for Ninja snapshots. Each module has its own -snapshot output directory for clarity. A possible layout: +module for IR snapshots and another for Ninja snapshots. Each module has its +own snapshot output directory for clarity. A possible layout: ```text netsuke/ @@ -46,14 +46,14 @@ netsuke/ ``` By default, `insta` creates a `tests/snapshots` directory and stores snapshot -data in files named after the test modules. This configuration separates IR -and Ninja snapshots into subfolders, keeping the expected outputs organized and +data in files named after the test modules. This configuration separates IR and +Ninja snapshots into subfolders, keeping the expected outputs organized and aligning with Netsuke’s design separation of IR from code generation. ## Writing Snapshot Tests for IR Outputs -A dedicated test module (e.g. **tests/ir_snapshot_tests.rs**) contains -IR snapshot tests. Each test feeds a Netsuke manifest (the input build +A dedicated test module (e.g. **tests/ir_snapshot_tests.rs**) contains IR +snapshot tests. Each test feeds a Netsuke manifest (the input build specification) into the compiler’s IR generation stage and captures the resulting IR in a stable, human-readable form. According to the design, the IR (BuildGraph) is intended to be independent of any particular backend, so it is @@ -134,8 +134,8 @@ This test involves: - **No timestamps or environment-specific data:** The IR should not include timestamps, random values, or absolute file system paths. If such data is - unavoidable, use `insta` redactions or post-process the output to replace them - with placeholders (e.g., ``). + unavoidable, use `insta` redactions or post-process the output to replace + them with placeholders (e.g., ``). By making the IR snapshot output stable, the snapshot tests will reliably catch regressions. If the IR generation logic changes intentionally (e.g., new fields @@ -144,11 +144,11 @@ added), the snapshot will change predictably, prompting a review. ## Writing Snapshot Tests for Ninja File Output Next, create **tests/ninja_snapshot_tests.rs** to verify Ninja build file -generation separately. This stage takes the IR (BuildGraph) and produces a Ninja -build script (usually the contents of a `build.ninja` file). Because Netsuke’s -design cleanly separates IR building from code generation, it is possible to -use the same manifest (or multiple manifest scenarios) to test the Ninja output -specifically. +generation separately. This stage takes the IR (BuildGraph) and produces a +Ninja build script (usually the contents of a `build.ninja` file). Because +Netsuke’s design cleanly separates IR building from code generation, it is +possible to use the same manifest (or multiple manifest scenarios) to test the +Ninja output specifically. **Example Ninja Snapshot Test:** @@ -197,8 +197,8 @@ Key points for Ninja snapshot tests: realistic coverage. - Call the Ninja generation function (e.g. `ninja_gen::generate_ninja`), which - produces the Ninja file contents as a `String`. This function traverses the IR - and outputs rules and build statements in Ninja syntax. + produces the Ninja file contents as a `String`. This function traverses the + IR and outputs rules and build statements in Ninja syntax. - As with IR, **determinism is crucial**. The Ninja output should list rules, targets, and dependencies in a consistent order. For example, if the IR does @@ -209,15 +209,15 @@ Key points for Ninja snapshot tests: stabilized. - Use `Settings::set_snapshot_path` to store these snapshots in a separate - `tests/snapshots/ninja` directory. The snapshot name `"simple_manifest_ninja"` - identifies this particular scenario. + `tests/snapshots/ninja` directory. The snapshot name + `"simple_manifest_ninja"` identifies this particular scenario. With this setup, IR tests and Ninja tests have distinct snapshot files. For example, after the first test run (see next section), expected snapshot files -include `tests/snapshots/ir/simple_manifest_ir.snap` and `tests/snapshots/ -ninja/simple_manifest_ninja.snap` (or combined snapshot files per test module). -These snapshot files contain the expected IR debug output and Ninja file text -respectively. +include `tests/snapshots/ir/simple_manifest_ir.snap` and +`tests/snapshots/ ninja/simple_manifest_ninja.snap` (or combined snapshot files +per test module). These snapshot files contain the expected IR debug output and +Ninja file text respectively. ## Running and Updating Snapshot Tests @@ -245,9 +245,10 @@ snapshot assertion for `simple_manifest_ninja` failed in "tests/ninja_snapshot_t … ``` -On the first run, `insta` (with default `INSTA_UPDATE=auto`) writes new snapshot -files and marks the tests as failed for review. `.snap` files (or `.snap.new` if -not auto-approved) appear in the `tests/snapshots/` subdirectories. +On the first run, `insta` (with default `INSTA_UPDATE=auto`) writes new +snapshot files and marks the tests as failed for review. `.snap` files (or +`.snap.new` if not auto-approved) appear in the `tests/snapshots/` +subdirectories. **Reviewing and Accepting Snapshots:** Use the `cargo-insta` CLI to review and accept these new snapshots: @@ -257,8 +258,8 @@ accept these new snapshots: first run, it only shows the new content. - Accept the new snapshots using the review interface. `cargo-insta` then moves - the `.snap.new` files to replace the old snapshots or create the `.snap` files - if they did not exist. + the `.snap.new` files to replace the old snapshots or create the `.snap` + files if they did not exist. - As an alternative, when confident in the outputs, run `cargo insta accept --all` to accept all changes in one go. @@ -268,9 +269,9 @@ snapshots now match the output. Commit the new/updated `.snap` files to version control. **Always include the snapshot files** so that CI can validate against them. -**Deterministic Failures:** If a snapshot test fails unexpectedly in the future, -it means the IR or Ninja output changed. This could reveal a regression or a -legitimate update: +**Deterministic Failures:** If a snapshot test fails unexpectedly in the +future, it means the IR or Ninja output changed. This could reveal a regression +or a legitimate update: - For an intended change (e.g., the IR structure or Ninja output format was updated as part of a feature), review and accept the new snapshots, then @@ -282,13 +283,14 @@ legitimate update: ## Integrating Snapshot Tests into GitHub Actions CI -Automating snapshot tests in CI ensures that changes to Netsuke do not introduce -regressions without notice. Use GitHub Actions to run `cargo test` (which -includes the snapshot tests) on every push or pull request. Here’s how to set -it up: +Automating snapshot tests in CI ensures that changes to Netsuke do not +introduce regressions without notice. Use GitHub Actions to run `cargo test` +(which includes the snapshot tests) on every push or pull request. Here’s how +to set it up: -**1. CI Workflow Setup:** In the repository (e.g., `.github/workflows/ -test.yml`), use a Rust toolchain action and run tests. For example: +**1. CI Workflow Setup:** In the repository (e.g., +`.github/workflows/ test.yml`), use a Rust toolchain action and run tests. For +example: ```yaml name: Rust CI @@ -330,9 +332,9 @@ jobs: **Notes:** - Setting `INSTA_UPDATE: no` in CI disables automatic snapshot creation or - updating. If a snapshot is missing or differs, the tests **fail**. The default - `auto` mode already treats CI specially (it will not auto-accept in CI), but - setting `no` is an explicit safeguard. + updating. If a snapshot is missing or differs, the tests **fail**. The + default `auto` mode already treats CI specially (it will not auto-accept in + CI), but setting `no` is an explicit safeguard. - Install `cargo-insta` mainly for completeness – running `cargo test` does not strictly require the CLI tool, but its presence enables `cargo insta` @@ -342,8 +344,8 @@ jobs: - The caches for Cargo help speed up CI. Ensure you include the snapshot files in the repository so that tests can find the expected outputs. -**2. Handling Snapshot Changes in CI:** In a typical workflow, CI will run tests -and either pass or fail: +**2. Handling Snapshot Changes in CI:** In a typical workflow, CI will run +tests and either pass or fail: - If all snapshots match, CI passes. No action needed. @@ -375,16 +377,16 @@ Ninja snapshot tests then verify that the IR-to-Ninja translation is correct. Both sets of tests use deterministic outputs to ensure consistent, meaningful snapshots. -With the `insta` crate, adding new test cases is straightforward – simply create -a manifest (or multiple variants) and assert that the IR or Ninja output matches -the snapshot. The snapshot files serve as living documentation of the expected -build graph and build script for given scenarios. Integrated into GitHub -Actions, this testing framework will catch regressions early: any change in -Netsuke’s IR logic or code generation will surface as a snapshot diff, prompting -careful review. +With the `insta` crate, adding new test cases is straightforward – simply +create a manifest (or multiple variants) and assert that the IR or Ninja output +matches the snapshot. The snapshot files serve as living documentation of the +expected build graph and build script for given scenarios. Integrated into +GitHub Actions, this testing framework will catch regressions early: any change +in Netsuke’s IR logic or code generation will surface as a snapshot diff, +prompting careful review. -This structured snapshot testing approach enables confident evolution of -the Netsuke project while preserving the correctness of its core compilation +This structured snapshot testing approach enables confident evolution of the +Netsuke project while preserving the correctness of its core compilation pipeline. **Sources:** From 4969017c9d4e602f757f9df699a78ddd7b19dd50 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 23:00:17 +0100 Subject: [PATCH 2/7] Fix doc typos --- docs/netsuke-design.md | 15 ++++++++------- docs/rust-testing-with-rstest-fixtures.md | 4 ++-- docs/snapshot-testing-in-netsuke-using-insta.md | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 6b0920f6..1d9647aa 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -21,9 +21,10 @@ string manipulation or conditional logic, to ensure its primary goal: running builds as fast as possible.[^2] This design choice by Ninja's authors necessitates the existence of a higher- -level generator tool. Netsuke fulfills this role. It provides a rich, user- -friendly language (YAML with Jinja) for describing the *what* and *why* of a -build—the project's structure, its logical rules, and its configurable + +level generator tool. Netsuke fulfills this role. It provides a rich, +user-friendly language (YAML with Jinja) for describing the *what* and *why* +of a build—the project's structure, its logical rules, and its configurable parameters. Netsuke's primary responsibility is to compile this high-level description into a low-level, highly optimized execution plan that Ninja can understand and execute. This separation of concerns—Netsuke managing build @@ -749,7 +750,7 @@ network operations. All built-in filters use `snake_case`. The `camel_case` helper is provided in place of `camelCase` so naming remains consistent with `snake_case` and -`kebab- case`. +`kebab-case`. #### Generic collection filters @@ -1050,7 +1051,7 @@ strings Instead, parse the Netsuke command template (e.g., `{{ cc }} -c {{ ins }} -o` `{{ outs }}`) and build the final command string step by step. The placeholders `{{ ins }}` and `{{ outs }}` are expanded to space-separated lists of file paths within Netsuke itself, each path being -shell-escaped using the `shell- quote` API. When the command is written to +shell-escaped using the `shell-quote` API. When the command is written to `build.ninja`, these lists replace Ninja's `$in` and `$out` macros. After substitution, the command is validated with [`shlex`] () to ensure it parses correctly. This @@ -1061,8 +1062,8 @@ approach guarantees that every dynamic part of the command is securely quoted. The concept of being "friendlier" than `make` extends beyond syntactic sugar to encompass safety and reliability. A tool that is easy to use but exposes the user to trivial security vulnerabilities is fundamentally unfriendly. In many -build systems, the burden of correct shell quoting falls on the user, an error- -prone task that requires specialized knowledge. +build systems, the burden of correct shell quoting falls on the user, an error-prone +task that requires specialized knowledge. Netsuke's design elevates security to a core feature by making it automatic and transparent. The user writes a simple, unquoted command template, and Netsuke diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 62c1f0ca..79755680 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -56,7 +56,7 @@ function signatures and fixture definitions to wire up dependencies automatically. This reliance on procedural macros is a key architectural decision. It enables -`rstest` to offer a remarkably clean and intuitive syntax at the test- writing +`rstest` to offer a remarkably clean and intuitive syntax at the test-writing level. Developers declare the dependencies their tests need, and the macros handle the resolution and injection. While this significantly improves the developer experience for writing tests, the underlying macro expansion involves @@ -821,7 +821,7 @@ away some of the explicit `async`/`.await` mechanics. Long-running or stalled asynchronous operations can cause tests to hang indefinitely. `rstest` provides a `#[timeout(…)]` attribute to set a maximum execution time for async tests. This feature typically relies on the -`async- timeout` feature of `rstest`, which is enabled by default. +`async-timeout` feature of `rstest`, which is enabled by default. ```rust use rstest::*; diff --git a/docs/snapshot-testing-in-netsuke-using-insta.md b/docs/snapshot-testing-in-netsuke-using-insta.md index 5d465463..c13ae619 100644 --- a/docs/snapshot-testing-in-netsuke-using-insta.md +++ b/docs/snapshot-testing-in-netsuke-using-insta.md @@ -215,7 +215,7 @@ Key points for Ninja snapshot tests: With this setup, IR tests and Ninja tests have distinct snapshot files. For example, after the first test run (see next section), expected snapshot files include `tests/snapshots/ir/simple_manifest_ir.snap` and -`tests/snapshots/ ninja/simple_manifest_ninja.snap` (or combined snapshot files +`tests/snapshots/ninja/simple_manifest_ninja.snap` (or combined snapshot files per test module). These snapshot files contain the expected IR debug output and Ninja file text respectively. @@ -289,7 +289,7 @@ introduce regressions without notice. Use GitHub Actions to run `cargo test` to set it up: **1. CI Workflow Setup:** In the repository (e.g., -`.github/workflows/ test.yml`), use a Rust toolchain action and run tests. For +`.github/workflows/test.yml`), use a Rust toolchain action and run tests. For example: ```yaml From 7d3bb066a95c5fd721bdaff205efac0c19df9a64 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 23:29:09 +0100 Subject: [PATCH 3/7] Apply feedback to docs --- ...havioural-testing-in-rust-with-cucumber.md | 4 +- docs/netsuke-design.md | 42 ++++++++----------- docs/rust-testing-with-rstest-fixtures.md | 10 ++--- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/docs/behavioural-testing-in-rust-with-cucumber.md b/docs/behavioural-testing-in-rust-with-cucumber.md index cf6c6b4d..8eab8399 100644 --- a/docs/behavioural-testing-in-rust-with-cucumber.md +++ b/docs/behavioural-testing-in-rust-with-cucumber.md @@ -935,10 +935,10 @@ single Gherkin step matches the patterns of two or more Rust functions.[^21] **Solution:** -5. **Be More Specific:** Make the Gherkin step text or the matching pattern more +1. **Be More Specific:** Make the Gherkin step text or the matching pattern more precise to eliminate the ambiguity. -6. **Anchor Regex:** When using regular expressions, always anchor them with `^` +2. **Anchor Regex:** When using regular expressions, always anchor them with `^` at the start and `$` at the end (e.g., `regex = r"^the user is logged in$"`). This prevents a step like `"the admin user is logged in"` from accidentally matching a less specific diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 1d9647aa..865bc51e 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -23,8 +23,8 @@ builds as fast as possible.[^2] This design choice by Ninja's authors necessitates the existence of a higher- level generator tool. Netsuke fulfills this role. It provides a rich, -user-friendly language (YAML with Jinja) for describing the *what* and *why* -of a build—the project's structure, its logical rules, and its configurable +user-friendly language (YAML with Jinja) for describing the *what* and *why* of +a build—the project's structure, its logical rules, and its configurable parameters. Netsuke's primary responsibility is to compile this high-level description into a low-level, highly optimized execution plan that Ninja can understand and execute. This separation of concerns—Netsuke managing build @@ -254,7 +254,7 @@ Each entry in the `rules` list is a mapping that defines a reusable action. files. Netsuke expands these placeholders to space-separated, shell-escaped lists of file paths before hashing the action. When generating the Ninja rule, the lists are replaced with Ninja's `$in` and `$out` macros. After - interpolation the command must be parsable by + interpolation, the command must be parsable by [shlex](https://docs.rs/shlex/latest/shlex/). Any interpolation other than `ins` or `outs` is automatically shell-escaped. @@ -398,7 +398,7 @@ annotation on a struct is sufficient to make it a deserialization target. While other promising YAML libraries like `saphyr` exist, their `serde` integration (`saphyr-serde`) is currently described as "soon-to-be" or is at a -highly experimental stage (version 0.0.0)[^11] Building a core component of +highly experimental stage (version 0.0.0)[^11]. Building a core component of Netsuke on a nascent or unreleased library would introduce significant and unnecessary project risk. @@ -703,8 +703,8 @@ more robust than its predecessors. ### 4.7 Template Standard Library Netsuke bundles a small "standard library" of Jinja helpers. These tests, -filters and functions are available to every template and give concise access -to common filesystem queries, path manipulations, collection utilities and +filters, and functions are available to every template and give concise access +to common filesystem queries, path manipulations, collection utilities, and network operations. #### File-system tests @@ -1047,7 +1047,7 @@ building command strings by pushing quoted components into a buffer: The command generation logic within the `ninja_gen.rs` module must not use simple string formatting (like `format!`) to construct the final command -strings Instead, parse the Netsuke command template (e.g., +strings. Instead, parse the Netsuke command template (e.g., `{{ cc }} -c {{ ins }} -o` `{{ outs }}`) and build the final command string step by step. The placeholders `{{ ins }}` and `{{ outs }}` are expanded to space-separated lists of file paths within Netsuke itself, each path being @@ -1208,40 +1208,32 @@ Rust ```rust // In src/main.rs -use clap::{Parser, Subcommand}; -use std::path::PathBuf; +use clap::{Parser, Subcommand}; use std::path::PathBuf; /// A modern, friendly build system that uses YAML and Jinja, powered by Ninja. #[command(author, version, about, long_about = None)] -struct Cli { - /// Path to the Netsuke manifest file to use. +struct Cli { /// Path to the Netsuke manifest file to use. #[arg(short, long, value_name = "FILE", default_value = "Netsukefile")] file: PathBuf, - /// Change to this directory before doing anything. - directory: Option, + /// Change to this directory before doing anything. directory: + Option, /// Set the number of parallel build jobs. #[arg(short, long, value_name = "N")] jobs: Option, #[command(subcommand)] - command: Option, -} + command: Option, } -enum Commands { - /// Build specified targets (or default targets if none are given) [default]. - Build { - /// A list of specific targets to build. - targets: Vec, - }, +enum Commands { /// Build specified targets (or default targets if none are +given) [default]. Build { /// A list of specific targets to build. targets: +Vec, }, - /// Remove build artifacts and intermediate files. - Clean {}, + /// Remove build artifacts and intermediate files. Clean {}, /// Display the build dependency graph in DOT format for visualization. - Graph {}, -} + Graph {}, } ``` *Note: The* `Build` *command is wrapped in an* `Option` *and will be diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 79755680..18f501b0 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -73,8 +73,8 @@ quality and developer productivity: - **Readability:** By injecting dependencies as function arguments, `rstest` makes the requirements of a test explicit and easy to understand. The test function's signature clearly documents what it needs to run. This allows - developers to "focus on the important stuff in your tests" by abstracting - away the setup details. + developers to focus on the important parts of tests by abstracting away the + setup details. - **Reusability:** Fixtures defined with `rstest` are reusable components. A single fixture, such as one setting up a database connection or creating a complex data structure, can be used across multiple tests, eliminating @@ -402,7 +402,7 @@ impl State { #[rstest] fn test_state_transitions( - # initial_state: State, + initial_state: State, #[values(Event::Process, Event::Error, Event::Fatal)] event: Event ) { // In a real test, you'd have more specific assertions based on expected_next_state @@ -617,7 +617,7 @@ impl User { #[fixture] fn user_fixture( - # name: &str, + name: &str, #[default(30)] age: u8, #[default("Viewer")] role: &str, ) -> User { @@ -643,7 +643,7 @@ fn test_admin_user(#[with("AdminUser", 42, "Admin")] user_fixture: User) { // For named overrides, one might need to define intermediate fixtures or check specific rstest version capabilities. // Assuming positional override for the first argument (name): #[rstest] -fn test_custom_name_user(# user_fixture: User) { +fn test_custom_name_user(user_fixture: User) { assert_eq!(user_fixture.name, "SpecificName"); assert_eq!(user_fixture.age, 30); // Age uses default assert_eq!(user_fixture.role, "Viewer"); // Role uses default From b30a468c2e07022883d23fbdec00a2d709fd84f7 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 18 Jul 2025 23:43:14 +0100 Subject: [PATCH 4/7] Address PR comments --- docs/behavioural-testing-in-rust-with-cucumber.md | 10 +++++----- docs/netsuke-design.md | 2 +- docs/rust-testing-with-rstest-fixtures.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/behavioural-testing-in-rust-with-cucumber.md b/docs/behavioural-testing-in-rust-with-cucumber.md index 8eab8399..04ecc453 100644 --- a/docs/behavioural-testing-in-rust-with-cucumber.md +++ b/docs/behavioural-testing-in-rust-with-cucumber.md @@ -933,13 +933,13 @@ variant is far more valuable for debugging than a stack trace from a panic.[^20] **Pitfall:** The test run fails with an "ambiguous step" error. This means a single Gherkin step matches the patterns of two or more Rust functions.[^21] -**Solution:** +**Solution:** -1. **Be More Specific:** Make the Gherkin step text or the matching pattern more - precise to eliminate the ambiguity. +1. **Be More Specific:** Make the Gherkin step text or the matching pattern + more precise to eliminate the ambiguity. -2. **Anchor Regex:** When using regular expressions, always anchor them with `^` - at the start and `$` at the end (e.g., +2. **Anchor Regex:** When using regular expressions, always anchor them with + `^` at the start and `$` at the end (e.g., `regex = r"^the user is logged in$"`). This prevents a step like `"the admin user is logged in"` from accidentally matching a less specific pattern like `regex = r"user is logged in"`.[^18] diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 865bc51e..7a71494c 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1185,7 +1185,7 @@ actionable output that the implementation should produce. The command-line interface is the user's entry point to Netsuke. A well-designed CLI is essential for a good user experience. It should be -intuitive, self- documenting, and consistent with the conventions of modern +intuitive, self-documenting, and consistent with the conventions of modern command-line tools. ### 8.1 Crate Selection: `clap` diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 18f501b0..7101b1a4 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -916,7 +916,7 @@ dropped), works effectively with `rstest`'s fixture model. When a regular the resource is typically cleaned up after the test finishes, as the fixture's return value goes out of scope. This localizes resource management logic to the fixture, keeping the test focused on its assertions. For temporary resources, -regular (per- test) fixtures are generally preferred over `#[once]` fixtures to +regular (per-test) fixtures are generally preferred over `#[once]` fixtures to ensure proper cleanup, as `#[once]` fixtures are never dropped. ### B. Mocking External Services (e.g., Database Connections, HTTP APIs) From cb5f1233eb51ba7e45877363d88e3e239bd4cced Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 01:42:58 +0100 Subject: [PATCH 5/7] Fix style issues --- docs/netsuke-design.md | 113 +++++++++++----------- docs/rust-testing-with-rstest-fixtures.md | 2 +- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 7a71494c..d4f499ac 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -310,9 +310,9 @@ validates this exclusivity during deserialization. When multiple fields are present, Netsuke emits a `RecipeConflict` error with the message "rule, command and script are mutually exclusive". - This union deserializes into the same `Recipe` enum used for rules. The - parser enforces that only one variant is present, maintaining backward - compatibility through serde aliases when `kind` is omitted. +This union deserializes into the same `Recipe` enum used for rules. The parser +enforces that only one variant is present, maintaining backward compatibility +through serde aliases when `kind` is omitted. - `sources`: The input files required by the command. This can be a single string or a list of strings. If any source entry matches the `name` of @@ -375,7 +375,7 @@ explicit, structured, and self-documenting nature. | Variables | CC=gcc | { vars: { cc: gcc } } | | Macros | define greet\\t@echo Hello $$1endef | { macros: { signature: "greet(name)", body: "Hello {{ name }}" } } | | Rule Definition | %.o: %.c\\n\\t$(CC) -c $< -o $@ | { rules: { name: compile, command: "{{ cc }} -c {{ ins }} -o {{ outs }}", description: "Compiling {{ outs }}" } } | -| Target Build | my_program: main.o utils.o\\t$(CC) $^ -o $@ | { targets: { name: my_program, rule: link, sources: \[main.o, utils.o\] } | +| Target Build | my_program: main.o utils.o\\t$(CC) $^ -o $@ | { targets: { name: my_program, rule: link, sources: [main.o, utils.o] } | | Readability | Relies on cryptic automatic variables ($@, $\<, $^) and implicit pattern matching. | Uses explicit, descriptive keys (name, rule, sources) and standard YAML list/map syntax. | ## Section 3: Parsing and Deserialization Strategy @@ -773,8 +773,8 @@ place of `camelCase` so naming remains consistent with `snake_case` and | `grep`, `sed`, `awk`, `cut`, `wc`, `tr` | filters | Canonical wrappers implemented via `shell()` for convenience | Using `shell()` marks the template as *impure* and disables caching of the -rendered YAML between Stage 2 and Stage 3. This avoids accidental -reuse of results that depend on external commands. +rendered YAML between Stage 2 and Stage 3. This avoids accidental reuse of +results that depend on external commands. Custom external commands can be registered as additional filters. Those should be marked `pure` if safe for caching or `impure` otherwise. @@ -938,6 +938,7 @@ structures to the Ninja file syntax. 1. **Write Variables:** Any global variables that need to be passed to Ninja can be written at the top of the file (e.g., `msvc_deps_prefix` for Windows + 2. **Write Rules:** Iterate through the `graph.actions` map. For each `ir::Action`, write a corresponding Ninja `rule` statement. The input and output lists stored in the action replace the `ins` and `outs` placeholders. @@ -1003,15 +1004,15 @@ The command construction will follow this pattern: 1. A new `Command` is created via `Command::new("ninja")`. Netsuke will assume `ninja` is available in the system's `PATH`. -2. Arguments passed to Netsuke's own CLI will be translated and forwarded to +1. Arguments passed to Netsuke's own CLI will be translated and forwarded to Ninja. For example, a `Netsuke build -C build/ my_target` command would result in `Command::new("ninja").arg("-C").arg("build/").arg("my_target")`. Flags like `-j` for parallelism will also be passed through.[^8] -3. The working directory for the Ninja process will be set using +1. The working directory for the Ninja process will be set using `.current_dir()` if the user provides a `-C` flag. -4. Standard I/O streams (`stdin`, `stdout`, `stderr`) will be configured using +1. Standard I/O streams (`stdin`, `stdout`, `stderr`) will be configured using `.stdout(Stdio::piped())` and `.stderr(Stdio::piped())`.[^24] This allows Netsuke to capture the real-time output from Ninja, which can then be streamed to the user's console, potentially with additional formatting or @@ -1053,7 +1054,7 @@ step by step. The placeholders `{{ ins }}` and `{{ outs }}` are expanded to space-separated lists of file paths within Netsuke itself, each path being shell-escaped using the `shell-quote` API. When the command is written to `build.ninja`, these lists replace Ninja's `$in` and `$out` macros. After -substitution, the command is validated with [`shlex`] +substitution, the command is validated with \[`shlex`\] () to ensure it parses correctly. This approach guarantees that every dynamic part of the command is securely quoted. @@ -1089,11 +1090,11 @@ three fundamental questions: 1. **What** went wrong? A concise summary of the failure (e.g., "YAML parsing failed," "Build configuration is invalid"). -2. **Where** did it go wrong? Precise location information, including the file, +1. **Where** did it go wrong? Precise location information, including the file, line number, and column where applicable (e.g., "in `Netsukefile` at line 15, column 3"). -3. **Why** did it go wrong, and what can be done about it? The underlying cause +1. **Why** did it go wrong, and what can be done about it? The underlying cause of the error and a concrete suggestion for how to fix it (e.g., "Cause: Found a tab character, which is not allowed. Hint: Use spaces for indentation instead."). @@ -1144,22 +1145,22 @@ enrichment: 1. A specific, low-level error occurs within a module. For instance, the IR generator detects a missing rule and creates an `IrGenError::RuleNotFound`. -2. The function where the error occurred returns +1. The function where the error occurred returns `Err(IrGenError::RuleNotFound {... }.into())`. The `.into()` call converts the specific `thiserror` enum variant into a generic `anyhow::Error` object, preserving the original error as its source. -3. A higher-level function in the call stack, which called the failing function, +1. A higher-level function in the call stack, which called the failing function, receives this `Err` value. It uses the `.with_context()` method to wrap the error with more application-level context. For example: `ir::from_manifest(ast)` `.with_context(|| "Failed to build the internal build graph from the manifest")?` . -4. This process of propagation and contextualization repeats as the error +1. This process of propagation and contextualization repeats as the error bubbles up towards `main`. -5. Finally, the `main` function receives the `Err` result. It prints the entire +1. Finally, the `main` function receives the `Err` result. It prints the entire error chain provided by `anyhow`, which displays the highest-level context first, followed by a list of underlying "Caused by:" messages. This provides the user with a rich, layered explanation of the failure, from the general @@ -1175,11 +1176,11 @@ This table provides a specification for the desired output of Netsuke's error reporting system, contrasting raw, unhelpful messages with the friendly, actionable output that the implementation should produce. -| Error Type | Poor Message (Default) | Netsuke's Friendly Message (Goal) | +| Error Type | Poor Message (Default) | Netsuke's Friendly Message (Goal) | | ---------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| YAML Parse | (line 15, column 3): Found a tab character where indentation is expected | Error: Failed to parse 'Netsukefile'. Caused by: Found a tab character. Hint: Use spaces for indentation instead of tabs. | -| Validation | thread 'main' panicked at 'Rule not found' | Error: Build configuration is invalid. Caused by: Target 'my_program' uses a rule named 'link-program' which is not defined in the 'rules' section. | -| Execution | ninja: error: 'main.o', needed by 'my_program', missing and no known rule to make it | Error: Build failed during execution. Caused by: Ninja could not build target 'my_program' because its dependency 'main.o' is missing. Hint: Ensure a target produces 'main.o'. | +| YAML Parse | (line 15, column 3): Found a tab character where indentation is expected | Error: Failed to parse 'Netsukefile'. Caused by: Found a tab character. Hint: Use spaces for indentation instead of tabs. | +| Validation | thread 'main' panicked at 'Rule not found' | Error: Build configuration is invalid. Caused by: Target 'my_program' uses a rule named 'link-program' which is not defined in the 'rules' section. | +| Execution | ninja: error: 'main.o', needed by 'my_program', missing and no known rule to make it | Error: Build failed during execution. Caused by: Ninja could not build target 'my_program' because its dependency 'main.o' is missing. Hint: Ensure a target produces 'main.o'. | ## Section 8: Command-Line Interface (CLI) Design @@ -1208,7 +1209,8 @@ Rust ```rust // In src/main.rs -use clap::{Parser, Subcommand}; use std::path::PathBuf; +use clap::{Parser, Subcommand}; +use std::path::PathBuf; /// A modern, friendly build system that uses YAML and Jinja, powered by Ninja. #[command(author, version, about, long_about = None)] @@ -1260,7 +1262,7 @@ The behaviour of each subcommand is clearly defined: with the graph tool, `ninja -t graph`. This outputs the complete build dependency graph in the DOT language. The result can be piped through `dot -Tsvg` or displayed via `netsuke graph --html` using an embedded - Dagre.js viewer. Visualising the graph is invaluable for understanding and + Dagre.js viewer. Visualizing the graph is invaluable for understanding and debugging complex projects. ### 8.4 Design Decisions @@ -1361,14 +1363,14 @@ goal. This table serves as a quick-reference guide to the core third-party crates selected for this project and the rationale for their inclusion. -| Component | Recommended Crate | Rationale | +| Component | Recommended Crate | Rationale | | -------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | -| CLI Parsing | clap | The Rust standard for powerful, derive-based CLI development. | -| YAML Parsing | serde_yaml | Mature, stable, and provides seamless integration with the serde framework. | -| Templating | minijinja | High compatibility with Jinja2, minimal dependencies, and supports runtime template loading. | -| Shell Quoting | shell-quote | A critical security component; provides robust, shell-specific escaping for command arguments. | -| Error Handling | anyhow + thiserror | An idiomatic and powerful combination for creating rich, contextual, and user-friendly error reports. | -| Versioning | semver | The standard library for parsing and evaluating Semantic Versioning strings, essential for the `netsuke_version` field. | +| CLI Parsing | clap | The Rust standard for powerful, derive-based CLI development. | +| YAML Parsing | serde_yaml | Mature, stable, and provides seamless integration with the serde framework. | +| Templating | minijinja | High compatibility with Jinja2, minimal dependencies, and supports runtime template loading. | +| Shell Quoting | shell-quote | A critical security component; provides robust, shell-specific escaping for command arguments. | +| Error Handling | anyhow + thiserror | An idiomatic and powerful combination for creating rich, contextual, and user-friendly error reports. | +| Versioning | semver | The standard library for parsing and evaluating Semantic Versioning strings, essential for the `netsuke_version` field. | ### 9.3 Future Enhancements @@ -1416,33 +1418,34 @@ projects. ### **Works cited** -[^1]: Ninja, a small build system with a focus on speed, accessed on July 12, +\[^1\]: Ninja, a small build system with a focus on speed, accessed on July 12, 2025, -[^2]: Ninja (build system) - Wikipedia, accessed on July 12, 2025, +\[^2\]: Ninja (build system) - Wikipedia, accessed on July 12, 2025, \ -[^3]: A Complete Guide To The Ninja Build System - Spectra - Mathpix, accessed -on July 12, 2025, << +\[^3\]: A Complete Guide To The Ninja Build System - Spectra - Mathpix, accessed +on July 12, 2025, \<< complete-> guide-to-the-ninja-build-system> -[^4]: semver - Rust, accessed on July 12, 2025, < +\[^4\]: semver - Rust, accessed on July 12, 2025, \< hard-way.github.io/Agents/semver/index.html> -Versioning - GitHub, accessed on July 12, 2025, << +Versioning - GitHub, accessed on July 12, 2025, \<< + > semver> -[^7]: How Ninja works - Fuchsia, accessed on July 12, 2025, -[^8]: The Ninja build system, accessed on July 12, 2025, < +\[^8\]: The Ninja build system, accessed on July 12, 2025, \< build.org/manual.html> -[^11]: Saphyr libraries - [crates.io](http://crates.io): Rust Package Registry, +\[^11\]: Saphyr libraries - [crates.io](http://crates.io): Rust Package Registry, accessed on July 12, 2025, accessed on July 12, 2025, @@ -1451,16 +1454,16 @@ accessed on July 12, 2025, -[^15]: minijinja - [crates.io](http://crates.io): Rust Package Registry, +\[^15\]: minijinja - [crates.io](http://crates.io): Rust Package Registry, accessed on July 12, 2025, -[^16]: minijinja - Rust - [Docs.rs](http://Docs.rs), accessed on July 12, 2025, +\[^16\]: minijinja - Rust - [Docs.rs](http://Docs.rs), accessed on July 12, 2025, -[^17]: minijinja - Rust, accessed on July 12, 2025, < wasmer-pack/api-docs/minijinja/index.html> -[^18]: Template engine — list of Rust libraries/crates // [Lib.rs](http:// +\[^18\]: Template engine — list of Rust libraries/crates // \[Lib.rs\](http:// Lib.rs), accessed on July 12, 2025, web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/std/ @@ -1470,38 +1473,38 @@ process/struct.Command.html> -[^22]: shell_quote - Rust - [Docs.rs](http://Docs.rs), accessed on July 12, +\[^22\]: shell_quote - Rust - [Docs.rs](http://Docs.rs), accessed on July 12, 2025, web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/std/ process/index.html> -[^24]: std::process - Rust, accessed on July 12, 2025, < +\[^24\]: std::process - Rust, accessed on July 12, 2025, \< lang.org/std/process/index.html> doc.rust-lang.org/std/process/struct.Command.html> -[^27]: Rust Error Handling Compared: anyhow vs thiserror vs snafu, accessed on -July 12, 2025, << +\[^27\]: Rust Error Handling Compared: anyhow vs thiserror vs snafu, accessed on +July 12, 2025, \<< vs-> thiserror-vs-snafu-2003> -and Advanced Techniques - Technorely, accessed on July 12, 2025, -[^29]: Practical guide to Error Handling in Rust - Dev State, accessed on July +\[^29\]: Practical guide to Error Handling in Rust - Dev State, accessed on July 12, 2025, -[^30]: thiserror and anyhow - Comprehensive Rust, accessed on July 12, 2025, +\[^30\]: thiserror and anyhow - Comprehensive Rust, accessed on July 12, 2025, -[^31]: Simple error handling for precondition/argument checking in Rust +\[^31\]: Simple error handling for precondition/argument checking in Rust -- Stack Overflow, accessed on July 12, 2025, < -questions/78217448/simple-error-handling-for-precondition-argument-checking- -in-rust> +- Stack Overflow, accessed on July 12, 2025, \< + questions/78217448/simple-error-handling-for-precondition-argument-checking- + in-rust> -[^32]: Nicer error reporting - Command Line Applications in Rust, accessed on +\[^32\]: Nicer error reporting - Command Line Applications in Rust, accessed on July 12, 2025, diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 7101b1a4..68425ef7 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -589,7 +589,7 @@ fn test_with_destructured_fixture(#[from(complex_user_data_fixture)] (name, _, _ The `#[from]` attribute decouples the fixture's actual function name from the variable name used within the consuming function. As shown, if a fixture -returns a tuple or struct and the test only cares about some parts or wants to +returns a tuple or struct and the test only cares about some parts or needs to use more idiomatic names for destructured elements, `#[from]` is essential to link the argument pattern to the correct source fixture. From c870782a4ea5c28236899dca99beaa69e37b6055 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 02:00:39 +0100 Subject: [PATCH 6/7] Split CLI imports onto separate lines --- docs/netsuke-design.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index d4f499ac..4f81e6a3 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1209,7 +1209,11 @@ Rust ```rust // In src/main.rs -use clap::{Parser, Subcommand}; +#[rustfmt::skip] +use clap::Parser; +#[rustfmt::skip] +use clap::Subcommand; +#[rustfmt::skip] use std::path::PathBuf; /// A modern, friendly build system that uses YAML and Jinja, powered by Ninja. From 0a9e60e3ea96a857b0f3317b2ff0ede228610be0 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 03:02:59 +0100 Subject: [PATCH 7/7] Fix CLI snippet and hyphenation --- docs/netsuke-design.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 4f81e6a3..1ed690ae 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -671,9 +671,9 @@ In addition to functions, custom filters provide a concise, pipe-based syntax for transforming data within templates. - `| shell_escape`: A filter that takes a string or list and escapes it for - safe inclusion as a single argument in a shell command. This is a non- - negotiable security feature to prevent command injection vulnerabilities. The - implementation will use the `shell-quote` crate for robust, shell-aware + safe inclusion as a single argument in a shell command. This is a + non-negotiable security feature to prevent command injection vulnerabilities. + The implementation will use the `shell-quote` crate for robust, shell-aware quoting.[^22] - `| to_path`: A filter that converts a string into a platform-native path @@ -1216,14 +1216,15 @@ use clap::Subcommand; #[rustfmt::skip] use std::path::PathBuf; -/// A modern, friendly build system that uses YAML and Jinja, powered by Ninja. +#[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// Path to the Netsuke manifest file to use. #[arg(short, long, value_name = "FILE", default_value = "Netsukefile")] file: PathBuf, - /// Change to this directory before doing anything. directory: - Option, + /// Change to this directory before doing anything. + #[arg(short = 'C', long, value_name = "DIR")] + directory: Option, /// Set the number of parallel build jobs. #[arg(short, long, value_name = "N")] @@ -1232,6 +1233,7 @@ struct Cli { /// Path to the Netsuke manifest file to use. #[command(subcommand)] command: Option, } +#[derive(Subcommand)] enum Commands { /// Build specified targets (or default targets if none are given) [default]. Build { /// A list of specific targets to build. targets: Vec, }, @@ -1239,7 +1241,7 @@ Vec, }, /// Remove build artifacts and intermediate files. Clean {}, /// Display the build dependency graph in DOT format for visualization. - Graph {}, } + Graph {}, ``` *Note: The* `Build` *command is wrapped in an* `Option` *and will be