Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ cucumber = "0.20.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"], default-features = false }
insta = { version = "1", features = ["yaml"] }
serial_test = "3"
assert_cmd = "2.0.17"

[[test]]
name = "cucumber"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ markdownlint: ## Lint Markdown files
find . -type f -name '*.md' -not -path './target/*' -print0 | xargs -0 -- $(MDLINT)

nixie: ## Validate Mermaid diagrams
find . -type f -name '*.md' -not -path './target/*' -print0 | xargs -0 -- $(NIXIE)
find . -type f -name '*.md' -not -path './target/*' -print0 | xargs -0 -n 1 -- $(NIXIE)

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,14 @@ command: "echo {{ dangerous_value | raw }}" # Unsafe (your problem now)
netsuke [build] [target1 target2 ...]
netsuke clean
netsuke graph
netsuke manifest FILE
```

- `netsuke` alone builds the `defaults:` targets from your manifest
- `netsuke graph` emits a Graphviz `.dot` of the build DAG
- `netsuke clean` runs `ninja -t clean`
- `netsuke manifest FILE` writes the Ninja manifest to `FILE` without invoking
Ninja

You can also pass:

Expand Down
6 changes: 3 additions & 3 deletions docs/netsuke-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1387,8 +1387,8 @@ and retain it.
/// Display the build dependency graph in DOT format for visualisation.
Graph {},

/// Emit the Ninja manifest to `FILE` without invoking Ninja. Emit { ///
Output path for the generated Ninja file.
/// Write the Ninja manifest to `FILE` without invoking Ninja. Manifest {
/// Output path for the generated Ninja file.
#[arg(value_name = "FILE")]
file: PathBuf, }, }
```
Expand Down Expand Up @@ -1422,7 +1422,7 @@ The behaviour of each subcommand is clearly defined:
Dagre.js viewer. Visualizing the graph is invaluable for understanding and
debugging complex projects.

- `Netsuke emit FILE`: This command performs the pipeline up to Ninja
- `Netsuke manifest FILE`: This command performs the pipeline up to Ninja
synthesis and writes the resulting Ninja file to `FILE` without invoking
Ninja.

Expand Down
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ pub enum Commands {
/// Display the build dependency graph in DOT format for visualization.
Graph,

/// Emit the Ninja manifest to the specified file without running Ninja.
Emit {
/// Write the Ninja manifest to the specified file without invoking Ninja.
Manifest {
Comment thread
leynos marked this conversation as resolved.
/// Output path for the generated Ninja file.
#[arg(value_name = "FILE")]
file: PathBuf,
Expand Down
2 changes: 1 addition & 1 deletion src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub fn run(cli: &Cli) -> Result<()> {
}
Ok(())
}
Commands::Emit { file } => {
Commands::Manifest { file } => {
let ninja = generate_ninja(cli)?;
write_and_log(&file, &ninja)?;
Ok(())
Expand Down
48 changes: 48 additions & 0 deletions tests/assert_cmd_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Integration tests for CLI execution using `assert_cmd`.
//!
//! These tests exercise end-to-end command handling by invoking the compiled
//! binary and verifying file outputs for the `manifest` subcommand and the
//! `--emit` build option.

use assert_cmd::Command;
use std::fs;
use tempfile::tempdir;

mod support;

#[test]
fn manifest_subcommand_writes_file() {
let temp = tempdir().expect("temp dir");
fs::copy("tests/data/minimal.yml", temp.path().join("Netsukefile")).expect("copy manifest");
let output = temp.path().join("standalone.ninja");
let mut cmd = Command::cargo_bin("netsuke").expect("binary");
cmd.current_dir(temp.path())
.env("PATH", "")
.arg("manifest")
.arg(&output)
.assert()
.success();
assert!(output.exists());
}

#[test]
fn build_with_emit_writes_file() {
let (ninja_dir, _ninja_path) = support::fake_ninja(0);
let temp = tempdir().expect("temp dir");
fs::copy("tests/data/minimal.yml", temp.path().join("Netsukefile")).expect("copy manifest");
let output = temp.path().join("emitted.ninja");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let original = std::env::var_os("PATH").unwrap_or_default();
let path = std::env::join_paths(
std::iter::once(ninja_dir.path().to_path_buf()).chain(std::env::split_paths(&original)),
)
.expect("join path");
let mut cmd = Command::cargo_bin("netsuke").expect("binary");
cmd.current_dir(temp.path())
.env("PATH", path)
.arg("build")
.arg("--emit")
.arg(&output)
.assert()
.success();
assert!(output.exists());
}
5 changes: 3 additions & 2 deletions tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ use std::path::PathBuf;
Commands::Build { emit: Some(PathBuf::from("out.ninja")), targets: vec!["a".into()] },
)]
#[case(
vec!["netsuke", "emit", "out.ninja"],
vec!["netsuke", "manifest", "out.ninja"],
PathBuf::from("Netsukefile"),
None,
None,
false,
Commands::Emit { file: PathBuf::from("out.ninja") },
Commands::Manifest { file: PathBuf::from("out.ninja") },
)]
Comment thread
leynos marked this conversation as resolved.
fn parse_cli(
#[case] argv: Vec<&str>,
Expand All @@ -56,6 +56,7 @@ fn parse_cli(
#[case(vec!["netsuke", "--file"], ErrorKind::InvalidValue)]
#[case(vec!["netsuke", "-j", "notanumber"], ErrorKind::ValueValidation)]
#[case(vec!["netsuke", "--file", "alt.yml", "-C"], ErrorKind::InvalidValue)]
#[case(vec!["netsuke", "manifest"], ErrorKind::MissingRequiredArgument)]
fn parse_cli_errors(#[case] argv: Vec<&str>, #[case] expected_error: ErrorKind) {
let err = Cli::try_parse_from(argv).expect_err("unexpected success");
assert_eq!(err.kind(), expected_error);
Expand Down
13 changes: 9 additions & 4 deletions tests/features/cli.feature
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ Feature: CLI parsing
And the emit path is "out.ninja"
And the first target is "target"

Scenario: Emit subcommand writes Ninja file
When the CLI is parsed with "emit out.ninja"
Scenario: Manifest subcommand writes Ninja file
When the CLI is parsed with "manifest out.ninja"
Then parsing succeeds
And the command is emit
And the emit command path is "out.ninja"
And the command is manifest
And the manifest command path is "out.ninja"

Scenario: Manifest subcommand requires a path
When the CLI is parsed with invalid arguments "manifest"
Then an error should be returned
And the error message should contain "<FILE>"

Scenario: Unknown command fails
When the CLI is parsed with invalid arguments "unknown"
Expand Down
10 changes: 5 additions & 5 deletions tests/runner_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fn run_build_with_emit_keeps_file() {

#[test]
#[serial]
fn run_emit_subcommand_writes_file() {
fn run_manifest_subcommand_writes_file() {
let original_path = std::env::var_os("PATH").unwrap_or_default();
unsafe {
std::env::set_var("PATH", "");
Expand All @@ -143,20 +143,20 @@ fn run_emit_subcommand_writes_file() {
let temp = tempfile::tempdir().expect("temp dir");
let manifest_path = temp.path().join("Netsukefile");
std::fs::copy("tests/data/minimal.yml", &manifest_path).expect("copy manifest");
let emit_path = temp.path().join("standalone.ninja");
let output_path = temp.path().join("standalone.ninja");
let cli = Cli {
file: manifest_path.clone(),
directory: Some(temp.path().to_path_buf()),
jobs: None,
verbose: false,
command: Some(Commands::Emit {
file: emit_path.clone(),
command: Some(Commands::Manifest {
file: output_path.clone(),
}),
};

let result = run(&cli);
assert!(result.is_ok());
assert!(emit_path.exists());
assert!(output_path.exists());
assert!(!temp.path().join("build.ninja").exists());

unsafe {
Expand Down
14 changes: 7 additions & 7 deletions tests/steps/cli_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ fn command_is_graph(world: &mut CliWorld) {
));
}

#[then("the command is emit")]
fn command_is_emit(world: &mut CliWorld) {
#[then("the command is manifest")]
fn command_is_manifest(world: &mut CliWorld) {
let cli = world.cli.as_ref().expect("cli");
assert!(matches!(
cli.command.as_ref().expect("command"),
Commands::Emit { .. }
Commands::Manifest { .. }
));
}

Expand Down Expand Up @@ -145,12 +145,12 @@ fn emit_path(world: &mut CliWorld, path: String) {
clippy::needless_pass_by_value,
reason = "Cucumber requires owned String arguments"
)]
#[then(expr = "the emit command path is {string}")]
fn emit_command_path(world: &mut CliWorld, path: String) {
#[then(expr = "the manifest command path is {string}")]
fn manifest_command_path(world: &mut CliWorld, path: String) {
let cli = world.cli.as_ref().expect("cli");
match cli.command.as_ref().expect("command") {
Commands::Emit { file } => assert_eq!(file, &PathBuf::from(&path)),
_ => panic!("command should be emit"),
Commands::Manifest { file } => assert_eq!(file, &PathBuf::from(&path)),
_ => panic!("command should be manifest"),
}
}

Expand Down
Loading