diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 475e5a13..b2dd6ad3 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1997,13 +1997,12 @@ the targets listed in the `defaults` section of the manifest are built. directory. It will invoke the Ninja backend with the appropriate flags, such 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. Visualizing the graph is invaluable for understanding and - debugging complex projects. +- `Netsuke graph`: This command is an introspection and debugging tool. It + runs the Netsuke pipeline through Ninja synthesis (Stage 6) to produce a + temporary `build.ninja`, then invokes Ninja with the graph tool, + `ninja -t graph`, which outputs the complete build dependency graph in the + DOT language. The result can be piped through Graphviz tools such as + `dot -Tsvg`. An optional `--html` renderer is planned for a later milestone. - `Netsuke manifest FILE`: This command performs the pipeline up to Ninja synthesis and writes the resulting Ninja file to `FILE` without invoking diff --git a/docs/roadmap.md b/docs/roadmap.md index eac2d53c..d0ba6185 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -181,8 +181,8 @@ library, and CLI ergonomics. - [x] Implement the `clean` subcommand by invoking `ninja -t clean`. - - [ ] Implement the graph subcommand by invoking ninja -t graph to output - a DOT representation of the dependency graph. + - [x] Implement the graph subcommand by invoking `ninja -t graph` to output a + DOT representation of the dependency graph. - [ ] Refine all CLI output for clarity, ensuring help messages are descriptive and command feedback is intuitive. diff --git a/docs/users-guide.md b/docs/users-guide.md index 0da3388e..b53c27fb 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -65,6 +65,10 @@ guiding you. The `Netsukefile` is a YAML file describing your build process. +Netsuke targets YAML 1.2 and forbids duplicate keys in manifests. If the same +mapping key appears more than once (even if a YAML parser would normally accept +it with “last key wins” behaviour), Netsuke treats this as an error. + ### Top-Level Structure ```yaml @@ -497,9 +501,9 @@ netsuke [OPTIONS] [COMMAND] [TARGETS...] rules/targets to be properly configured for cleaning in Ninja (often via `phony` targets). -- `graph`: Generates the build dependency graph and outputs it in DOT format - (suitable for Graphviz). Future versions may support other formats like - `--html`. +- `graph`: Generates the build dependency graph by running `ninja -t graph` on + the generated `build.ninja`, outputting DOT to stdout (suitable for + Graphviz). Future versions may support other formats like `--html`. ### Exit Codes diff --git a/src/runner/mod.rs b/src/runner/mod.rs index f084a9c9..73b90e99 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -95,10 +95,7 @@ pub fn run(cli: &Cli) -> Result<()> { Ok(()) } Commands::Clean => handle_clean(cli), - Commands::Graph => { - info!(target: "netsuke::subcommand", subcommand = "graph", "Subcommand requested"); - Ok(()) - } + Commands::Graph => handle_graph(cli), } } @@ -156,32 +153,58 @@ fn handle_build(cli: &Cli, args: &BuildArgs) -> Result<()> { Ok(()) } -/// Remove build artefacts by invoking `ninja -t clean`. +/// Execute a Ninja tool (e.g., `ninja -t clean`) using a temporary build file. /// -/// Generates the Ninja manifest to a temporary file, then invokes Ninja's clean -/// tool to remove all outputs defined by the build graph. +/// Generates the Ninja manifest to a temporary file, then invokes Ninja with +/// `-t ` while preserving the CLI settings (working directory and job +/// count). /// /// # Errors /// /// Returns an error if manifest generation or Ninja execution fails. -fn handle_clean(cli: &Cli) -> Result<()> { - info!(target: "netsuke::subcommand", subcommand = "clean", "Subcommand requested"); +fn handle_ninja_tool(cli: &Cli, tool: &str) -> Result<()> { + info!(target: "netsuke::subcommand", subcommand = tool, "Subcommand requested"); let ninja = generate_ninja(cli)?; let tmp = process::create_temp_ninja_file(&ninja)?; let build_path = tmp.path(); let program = process::resolve_ninja_program(); - run_ninja_tool(program.as_path(), cli, build_path, "clean").with_context(|| { + run_ninja_tool(program.as_path(), cli, build_path, tool).with_context(|| { format!( - "running {} -t clean with build file {}", + "running {} -t {} with build file {}", program.display(), + tool, build_path.display() ) })?; Ok(()) } +/// Remove build artefacts by invoking `ninja -t clean`. +/// +/// Generates the Ninja manifest to a temporary file, then invokes Ninja's clean +/// tool to remove all outputs defined by the build graph. +/// +/// # Errors +/// +/// Returns an error if manifest generation or Ninja execution fails. +fn handle_clean(cli: &Cli) -> Result<()> { + handle_ninja_tool(cli, "clean") +} + +/// Display build dependency graph by invoking `ninja -t graph`. +/// +/// Generates the Ninja manifest to a temporary file, then invokes Ninja's graph +/// tool to emit a DOT representation to stdout. +/// +/// # Errors +/// +/// Returns an error if manifest generation or Ninja execution fails. +fn handle_graph(cli: &Cli) -> Result<()> { + handle_ninja_tool(cli, "graph") +} + /// Generate the Ninja manifest string from the Netsuke manifest referenced by `cli`. /// /// # Errors diff --git a/test_support/src/env.rs b/test_support/src/env.rs index 7d6cae14..1168a031 100644 --- a/test_support/src/env.rs +++ b/test_support/src/env.rs @@ -223,6 +223,12 @@ pub struct NinjaEnvGuard { _lock: EnvLock, } +impl std::fmt::Debug for NinjaEnvGuard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NinjaEnvGuard").finish_non_exhaustive() + } +} + /// Override the `NINJA_ENV` variable with `path`, returning a guard that resets it. /// /// In Rust 2024 `std::env::set_var` is `unsafe` because it mutates process-global diff --git a/test_support/src/env_lock.rs b/test_support/src/env_lock.rs index 3dcadae6..93c73780 100644 --- a/test_support/src/env_lock.rs +++ b/test_support/src/env_lock.rs @@ -4,6 +4,7 @@ //! synchronised, preventing interference between concurrently running tests. use std::sync::{Mutex, MutexGuard}; +use std::{fmt, fmt::Formatter}; static ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -12,6 +13,12 @@ pub struct EnvLock { _guard: MutexGuard<'static, ()>, } +impl fmt::Debug for EnvLock { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("EnvLock").finish_non_exhaustive() + } +} + impl EnvLock { /// Acquire the global lock serialising environment mutations. pub fn acquire() -> Self { diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..a6d70c18 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,35 @@ +//! Shared helpers for integration tests. +//! +//! Integration tests under `tests/` compile as independent crates. This module +//! is included via `mod common;` in individual test files to share fixtures and +//! helpers while keeping test modules small and avoiding duplication. + +use anyhow::{Context, Result}; +use rstest::fixture; +use std::path::PathBuf; +use test_support::{ + env::{NinjaEnvGuard, SystemEnv, override_ninja_env}, + fake_ninja, +}; + +/// Create a temporary project with a Netsukefile from `minimal.yml`. +pub fn create_test_manifest() -> Result<(tempfile::TempDir, PathBuf)> { + let temp = tempfile::tempdir().context("create temp dir for test manifest")?; + let manifest_path = temp.path().join("Netsukefile"); + std::fs::copy("tests/data/minimal.yml", &manifest_path) + .with_context(|| format!("copy minimal.yml to {}", manifest_path.display()))?; + Ok((temp, manifest_path)) +} + +/// Fixture: point `NINJA_ENV` at a fake `ninja` with a configurable exit code. +/// +/// Returns: (tempdir holding ninja, `NINJA_ENV` guard) +#[fixture] +pub fn ninja_with_exit_code( + #[default(0u8)] exit_code: u8, +) -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { + let (ninja_dir, ninja_path) = fake_ninja(exit_code)?; + let env = SystemEnv::new(); + let guard = override_ninja_env(&env, ninja_path.as_path()); + Ok((ninja_dir, ninja_path, guard)) +} diff --git a/tests/cucumber.rs b/tests/cucumber.rs index 994bfa77..56f3f6cf 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -7,7 +7,11 @@ use netsuke::stdlib::{NetworkPolicy, StdlibState}; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use std::{collections::HashMap, ffi::OsString, net::TcpListener}; -use test_support::{PathGuard, env::restore_many, http}; +use test_support::{ + PathGuard, + env::{NinjaEnvGuard, restore_many}, + http, +}; /// Shared state for Cucumber scenarios. #[derive(Debug, Default, World)] @@ -37,6 +41,8 @@ pub struct CliWorld { pub temp: Option, /// Guard that restores `PATH` after each scenario. pub path_guard: Option, + /// Guard that overrides `NINJA_ENV` for deterministic Ninja resolution. + pub ninja_env_guard: Option, /// Root directory for stdlib scenarios. pub stdlib_root: Option, /// Captured output from the last stdlib render. @@ -181,6 +187,7 @@ fn block_device_exists() -> bool { impl Drop for CliWorld { fn drop(&mut self) { self.shutdown_http_server(); + self.ninja_env_guard.take(); self.restore_environment(); self.stdlib_text = None; } diff --git a/tests/features/clean.feature b/tests/features_unix/clean.feature similarity index 99% rename from tests/features/clean.feature rename to tests/features_unix/clean.feature index 77f79521..79a448dd 100644 --- a/tests/features/clean.feature +++ b/tests/features_unix/clean.feature @@ -1,3 +1,4 @@ +@unix Feature: Clean subcommand execution Scenario: Clean invokes ninja with tool flag diff --git a/tests/features_unix/graph.feature b/tests/features_unix/graph.feature new file mode 100644 index 00000000..735d582a --- /dev/null +++ b/tests/features_unix/graph.feature @@ -0,0 +1,29 @@ +@unix +Feature: Graph subcommand execution + + Scenario: Graph invokes ninja with tool flag + Given a fake ninja executable that expects the graph tool + And the CLI is parsed with "graph" + And the CLI uses the temporary directory + When the graph process is run + Then the command should succeed + + Scenario: Graph fails when ninja fails + Given a fake ninja executable that exits with 1 + And the CLI is parsed with "graph" + And the CLI uses the temporary directory + When the graph process is run + Then the command should fail with error "ninja exited" + + Scenario: Graph respects jobs flag + Given a fake ninja executable that expects graph with 4 jobs + And the CLI is parsed with "-j 4 graph" + And the CLI uses the temporary directory + When the graph process is run + Then the command should succeed + + Scenario: Graph fails when ninja is missing + Given no ninja executable is available + And the CLI is parsed with "graph" + When the graph process is run + Then the command should fail with error "No such file or directory" diff --git a/tests/features/ninja_process.feature b/tests/features_unix/ninja_process.feature similarity index 99% rename from tests/features/ninja_process.feature rename to tests/features_unix/ninja_process.feature index 4d4785f6..da591f90 100644 --- a/tests/features/ninja_process.feature +++ b/tests/features_unix/ninja_process.feature @@ -1,3 +1,4 @@ +@unix Feature: Ninja process execution Scenario: Ninja succeeds diff --git a/tests/runner_tests.rs b/tests/runner_tests.rs index 39b04a02..f01ecd5b 100644 --- a/tests/runner_tests.rs +++ b/tests/runner_tests.rs @@ -6,11 +6,27 @@ use netsuke::runner::{BuildTargets, run, run_ninja, run_ninja_tool}; use rstest::{fixture, rstest}; use std::path::{Path, PathBuf}; use test_support::{ - check_ninja::{self, ToolName}, + check_ninja, env::{NinjaEnvGuard, SystemEnv, override_ninja_env, prepend_dir_to_path}, fake_ninja, }; +mod common; +use common::create_test_manifest; + +/// Fixture: provide a fake `ninja` binary with a configurable exit code. +/// +/// This is a re-export of `common::ninja_with_exit_code` so `rstest` can +/// discover it in this integration test crate. +/// +/// Returns: (`tempfile::TempDir`, path to the ninja binary, `NinjaEnvGuard`) +#[fixture] +fn ninja_with_exit_code( + #[default(0u8)] exit_code: u8, +) -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { + common::ninja_with_exit_code(exit_code) +} + /// Fixture: point `NINJA_ENV` at a fake `ninja` that validates `-f` files. /// /// Using `NINJA_ENV` avoids mutating `PATH`, letting tests run in parallel @@ -25,19 +41,6 @@ fn ninja_in_env() -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { Ok((ninja_dir, ninja_path, guard)) } -/// Fixture: point `NINJA_ENV` at a fake `ninja` with a configurable exit code. -/// -/// Returns: (tempdir holding ninja, `NINJA_ENV` guard) -#[fixture] -fn ninja_with_exit_code( - #[default(0u8)] exit_code: u8, -) -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { - let (ninja_dir, ninja_path) = fake_ninja(exit_code)?; - let env = SystemEnv::new(); - let guard = override_ninja_env(&env, ninja_path.as_path()); - Ok((ninja_dir, ninja_path, guard)) -} - /// Shared setup for tests that rely on `NINJA_ENV`. /// /// Returns the fake ninja directory, temp project directory, constructed CLI, @@ -59,15 +62,6 @@ fn setup_ninja_env_test() -> Result<( Ok((ninja_dir, ninja_path, temp, cli, guard)) } -/// Create a temporary project with a Netsukefile from `minimal.yml`. -fn create_test_manifest() -> Result<(tempfile::TempDir, PathBuf)> { - let temp = tempfile::tempdir().context("create temp dir for test manifest")?; - let manifest_path = temp.path().join("Netsukefile"); - std::fs::copy("tests/data/minimal.yml", &manifest_path) - .with_context(|| format!("copy minimal.yml to {}", manifest_path.display()))?; - Ok((temp, manifest_path)) -} - #[test] fn run_exits_with_manifest_error_on_invalid_version() -> Result<()> { let temp = tempfile::tempdir().context("create temp dir for invalid manifest test")?; @@ -132,14 +126,24 @@ where Ok(()) } -#[rstest] -fn run_ninja_not_found() -> Result<()> { +/// Helper: assert that a runner function fails with `NotFound` when ninja binary is missing. +fn assert_runner_not_found(runner_call: F) -> Result<()> +where + F: FnOnce(&Cli) -> std::io::Result<()>, +{ assert_binary_not_found(|| { let cli = Cli::default(); + runner_call(&cli) + }) +} + +#[rstest] +fn run_ninja_not_found() -> Result<()> { + assert_runner_not_found(|cli| { let targets = BuildTargets::default(); run_ninja( Path::new("does-not-exist"), - &cli, + cli, Path::new("build.ninja"), &targets, ) @@ -302,78 +306,14 @@ fn run_fails_with_failing_ninja_env() -> Result<()> { assert_ninja_failure_propagates(None) } -// --- Clean subcommand tests --- - -/// Fixture: point `NINJA_ENV` at a fake `ninja` that expects `-t clean`. -/// -/// Returns: (tempdir holding ninja, path to ninja, `NINJA_ENV` guard) -#[fixture] -fn ninja_expecting_clean() -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { - let (ninja_dir, ninja_path) = check_ninja::fake_ninja_expect_tool(ToolName::new("clean"))?; - let env = SystemEnv::new(); - let guard = override_ninja_env(&env, ninja_path.as_path()); - Ok((ninja_dir, ninja_path, guard)) -} - -#[cfg(unix)] -#[rstest] -fn run_clean_subcommand_succeeds() -> Result<()> { - let (_ninja_dir, _ninja_path, _guard) = ninja_expecting_clean()?; - let (temp, manifest_path) = create_test_manifest()?; - let cli = Cli { - file: manifest_path.clone(), - directory: Some(temp.path().to_path_buf()), - command: Some(Commands::Clean), - ..Cli::default() - }; - - run(&cli).context("expected clean subcommand to succeed")?; - - ensure!( - !temp.path().join("build.ninja").exists(), - "clean subcommand should not leave build.ninja in project directory" - ); - Ok(()) -} - -#[cfg(unix)] -#[rstest] -fn run_clean_fails_with_failing_ninja() -> Result<()> { - assert_ninja_failure_propagates(Some(Commands::Clean)) -} - #[rstest] fn run_ninja_tool_not_found() -> Result<()> { - assert_binary_not_found(|| { - let cli = Cli::default(); + assert_runner_not_found(|cli| { run_ninja_tool( Path::new("does-not-exist"), - &cli, + cli, Path::new("build.ninja"), "clean", ) }) } - -#[cfg(unix)] -#[rstest] -fn run_clean_fails_with_invalid_manifest() -> Result<()> { - let temp = tempfile::tempdir().context("create temp dir for invalid manifest test")?; - let manifest_path = temp.path().join("Netsukefile"); - std::fs::copy("tests/data/invalid_version.yml", &manifest_path) - .with_context(|| format!("copy invalid manifest to {}", manifest_path.display()))?; - let cli = Cli { - file: manifest_path.clone(), - command: Some(Commands::Clean), - ..Cli::default() - }; - - let Err(err) = run(&cli) else { - bail!("expected clean to fail for invalid manifest"); - }; - ensure!( - err.to_string().contains("loading manifest at"), - "error should mention manifest loading, got: {err}" - ); - Ok(()) -} diff --git a/tests/runner_tool_subcommands_tests.rs b/tests/runner_tool_subcommands_tests.rs new file mode 100644 index 00000000..f462b1db --- /dev/null +++ b/tests/runner_tool_subcommands_tests.rs @@ -0,0 +1,180 @@ +//! Integration tests for Netsuke tool subcommands. +//! +//! Covers the `clean` and `graph` subcommands which invoke Ninja tools via +//! `ninja -t `. + +use anyhow::{Context, Result, bail, ensure}; +use netsuke::cli::{Cli, Commands}; +use netsuke::runner::run; +use rstest::{fixture, rstest}; +use std::path::PathBuf; +use test_support::{ + check_ninja::{self, ToolName}, + env::{NinjaEnvGuard, SystemEnv, override_ninja_env}, +}; + +mod common; +use common::create_test_manifest; + +/// Fixture: provide a fake `ninja` binary with a configurable exit code. +/// +/// This is a re-export of `common::ninja_with_exit_code` so `rstest` can +/// discover it in this integration test crate. +/// +/// Returns: (`tempfile::TempDir`, path to the ninja binary, `NinjaEnvGuard`) +#[fixture] +fn ninja_with_exit_code( + #[default(0u8)] exit_code: u8, +) -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { + common::ninja_with_exit_code(exit_code) +} + +/// Helper: test that a command fails when ninja exits with non-zero status. +fn assert_ninja_failure_propagates(command: Commands) -> Result<()> { + let (_ninja_dir, _ninja_path, _guard) = ninja_with_exit_code(7)?; + let (temp, manifest_path) = create_test_manifest()?; + let expected_tool = match &command { + Commands::Clean => "clean", + Commands::Graph => "graph", + other => bail!("unsupported command for this helper: {other:?}"), + }; + let cli = Cli { + file: manifest_path.clone(), + directory: Some(temp.path().to_path_buf()), + command: Some(command), + ..Cli::default() + }; + + let Err(err) = run(&cli) else { + bail!("expected run to fail when ninja exits non-zero"); + }; + let messages: Vec = err.chain().map(ToString::to_string).collect(); + ensure!( + messages.iter().any(|m| m.contains("ninja exited")), + "error should report ninja exit status, got: {messages:?}" + ); + ensure!( + messages + .iter() + .any(|m| m.contains(&format!("-t {expected_tool}"))), + "error should mention running ninja tool {expected_tool}, got: {messages:?}" + ); + ensure!( + messages + .iter() + .any(|m| m.contains("with build file") && m.contains(".ninja")), + "error should include build file context, got: {messages:?}" + ); + Ok(()) +} + +fn assert_subcommand_succeeds_without_persisting_file( + fixture: Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)>, + command: Commands, + name: &'static str, +) -> Result<()> { + let (_ninja_dir, _ninja_path, _guard) = fixture?; + let (temp, manifest_path) = create_test_manifest()?; + let cli = Cli { + file: manifest_path.clone(), + directory: Some(temp.path().to_path_buf()), + command: Some(command), + ..Cli::default() + }; + + run(&cli).with_context(|| format!("expected {name} subcommand to succeed"))?; + + ensure!( + !temp.path().join("build.ninja").exists(), + "{name} subcommand should not leave build.ninja in project directory" + ); + Ok(()) +} + +fn assert_subcommand_fails_with_invalid_manifest( + command: Commands, + name: &'static str, +) -> Result<()> { + let temp = tempfile::tempdir().context("create temp dir for invalid manifest test")?; + let manifest_path = temp.path().join("Netsukefile"); + std::fs::copy("tests/data/invalid_version.yml", &manifest_path) + .with_context(|| format!("copy invalid manifest to {}", manifest_path.display()))?; + let cli = Cli { + file: manifest_path.clone(), + command: Some(command), + ..Cli::default() + }; + + let Err(err) = run(&cli) else { + bail!("expected {name} to fail for invalid manifest"); + }; + let messages: Vec = err.chain().map(ToString::to_string).collect(); + ensure!( + messages.iter().any(|m| m.contains("loading manifest at")), + "error should mention manifest loading, got: {messages:?}" + ); + Ok(()) +} + +type NinjaToolFixture = fn() -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)>; + +/// Fixture: point `NINJA_ENV` at a fake `ninja` that expects `-t clean`. +/// +/// Returns: (tempdir holding ninja, path to ninja, `NINJA_ENV` guard) +#[fixture] +fn ninja_expecting_clean() -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { + let (ninja_dir, ninja_path) = check_ninja::fake_ninja_expect_tool(ToolName::new("clean"))?; + let env = SystemEnv::new(); + let guard = override_ninja_env(&env, ninja_path.as_path()); + Ok((ninja_dir, ninja_path, guard)) +} + +/// Fixture: point `NINJA_ENV` at a fake `ninja` that expects `-t graph`. +/// +/// Returns: (tempdir holding ninja, path to ninja, `NINJA_ENV` guard) +#[fixture] +fn ninja_expecting_graph() -> Result<(tempfile::TempDir, PathBuf, NinjaEnvGuard)> { + let (ninja_dir, ninja_path) = check_ninja::fake_ninja_expect_tool(ToolName::new("graph"))?; + let env = SystemEnv::new(); + let guard = override_ninja_env(&env, ninja_path.as_path()); + Ok((ninja_dir, ninja_path, guard)) +} + +#[cfg(unix)] +#[rstest] +fn run_clean_fails_with_failing_ninja() -> Result<()> { + assert_ninja_failure_propagates(Commands::Clean) +} + +#[cfg(unix)] +#[rstest] +fn run_graph_fails_with_failing_ninja() -> Result<()> { + assert_ninja_failure_propagates(Commands::Graph) +} + +#[cfg(unix)] +#[rstest] +#[case( + Some(ninja_expecting_clean as NinjaToolFixture), + Commands::Clean, + "clean" +)] +#[case(None, Commands::Clean, "clean")] +#[case( + Some(ninja_expecting_graph as NinjaToolFixture), + Commands::Graph, + "graph" +)] +#[case(None, Commands::Graph, "graph")] +fn run_tool_subcommand_table_cases( + #[case] fixture: Option, + #[case] command: Commands, + #[case] name: &'static str, +) -> Result<()> { + match fixture { + Some(factory) => { + assert_subcommand_succeeds_without_persisting_file(factory(), command, name) + } + None => assert_subcommand_fails_with_invalid_manifest(command, name), + } +} diff --git a/tests/steps/process_steps.rs b/tests/steps/process_steps.rs index 5a1e584c..45f14f38 100644 --- a/tests/steps/process_steps.rs +++ b/tests/steps/process_steps.rs @@ -40,6 +40,9 @@ fn install_test_ninja( let guard = env::prepend_dir_to_path(env, dir.path())?; world.path_guard = Some(guard); world.ninja = Some(ninja_path.to_string_lossy().into_owned()); + world.ninja_env_guard.take(); + let system_env = env::SystemEnv::new(); + world.ninja_env_guard = Some(env::override_ninja_env(&system_env, &ninja_path)); world.temp = Some(dir); Ok(()) } @@ -81,6 +84,25 @@ fn fake_ninja_expects_clean_with_jobs(world: &mut CliWorld, jobs: u32) -> Result install_test_ninja(&env, world, dir, path) } +/// Creates a fake ninja executable that expects the `-t graph` tool invocation. +#[cfg(unix)] +#[given("a fake ninja executable that expects the graph tool")] +fn fake_ninja_expects_graph(world: &mut CliWorld) -> Result<()> { + let (dir, path) = check_ninja::fake_ninja_expect_tool(ToolName::new("graph"))?; + let env = env::mocked_path_env(); + install_test_ninja(&env, world, dir, path) +} + +/// Creates a fake ninja executable that expects `-t graph` and `-j `. +#[cfg(unix)] +#[given(expr = "a fake ninja executable that expects graph with {int} jobs")] +fn fake_ninja_expects_graph_with_jobs(world: &mut CliWorld, jobs: u32) -> Result<()> { + let (dir, path) = + check_ninja::fake_ninja_expect_tool_with_jobs(ToolName::new("graph"), Some(jobs), None)?; + let env = env::mocked_path_env(); + install_test_ninja(&env, world, dir, path) +} + /// Sets up a scenario where no ninja executable is available. /// /// This step creates a temporary directory and records the path to a @@ -200,14 +222,8 @@ fn run(world: &mut CliWorld) -> Result<()> { Ok(()) } -/// Executes the clean subcommand and captures the result in the test world. -/// -/// This step runs the full `runner::run` function with the Clean command, -/// ensuring the manifest exists first and updating the world's `run_status` -/// and `run_error` fields based on the execution outcome. #[cfg(unix)] -#[when("the clean process is run")] -fn run_clean(world: &mut CliWorld) -> Result<()> { +fn run_subcommand(world: &mut CliWorld) -> Result<()> { prepare_cli_with_absolute_file(world)?; let cli = world .cli @@ -219,6 +235,28 @@ fn run_clean(world: &mut CliWorld) -> Result<()> { Ok(()) } +/// Executes the clean subcommand and captures the result in the test world. +/// +/// This step runs the full `runner::run` function with the Clean command, +/// ensuring the manifest exists first and updating the world's `run_status` +/// and `run_error` fields based on the execution outcome. +#[cfg(unix)] +#[when("the clean process is run")] +fn run_clean(world: &mut CliWorld) -> Result<()> { + run_subcommand(world) +} + +/// Executes the graph subcommand and captures the result in the test world. +/// +/// This step runs the full `runner::run` function with the Graph command, +/// ensuring the manifest exists first and updating the world's `run_status` +/// and `run_error` fields based on the execution outcome. +#[cfg(unix)] +#[when("the graph process is run")] +fn run_graph(world: &mut CliWorld) -> Result<()> { + run_subcommand(world) +} + /// Asserts that the command succeeds. #[then("the command should succeed")] fn command_should_succeed(world: &mut CliWorld) -> Result<()> {