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
4 changes: 2 additions & 2 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2250,8 +2250,8 @@ mutate_env_var(world, EnvVarKey::from("NETSUKE_EMOJI"), None)?;

Production-facing unit and integration tests follow the same rule. Use the
appropriate injected seam, such as `run_with_ninja_program`,
`from_path_with_policy_and_env`, `StdlibConfig::with_path_override`,
`StdlibConfig::with_home_override`, or
`from_path_with_policy_and_env`, `manifest::from_str_with_env_and_config`,
`StdlibConfig::with_path_override`, `StdlibConfig::with_home_override`, or
`StdlibConfig::with_command_path_override`. End-to-end tests may call
`env_clear()` and then apply values with `Command::env`, because the mutation
is confined to the child.
Expand Down
2 changes: 2 additions & 0 deletions src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod expand;
mod glob;
mod hints;
mod jinja_macros;
mod parse_with_config;
mod render;

/// JSON representation of a manifest node after YAML and Jinja evaluation.
Expand All @@ -58,6 +59,7 @@ pub use env_reader::{EnvReadError, EnvReader, process_env_reader};
pub use glob::glob_paths;

pub(crate) use expand::expand_foreach;
pub use parse_with_config::from_str_with_env_and_config;
pub use render::render_manifest;

use self::{env_reader::env_var_with, jinja_macros::register_manifest_macros};
Expand Down
73 changes: 73 additions & 0 deletions src/manifest/parse_with_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Manifest string parsing with explicit stdlib configuration.
//!
//! The `from_str`-family entrypoints in the parent module parse with the
//! default stdlib registration. This module hosts the variant that also
//! injects a caller-owned [`StdlibConfig`], so tests can pin behaviour such
//! as `command_available` resolution without mutating the process
//! environment.

use super::{EnvReader, ManifestName, ManifestParse, from_str_named};
use crate::{ast::NetsukeManifest, stdlib::StdlibConfig};
use anyhow::Result;

/// Parse a manifest string with an explicit environment reader and stdlib
/// configuration.
///
/// Combines the [`EnvReader`] of
/// [`crate::manifest::from_str_with_env`] with a full
/// [`StdlibConfig`], so a caller — in practice a test — can pin stdlib
/// behaviour such as `command_available` resolution without touching the
/// process environment or `PATH`. For instance,
/// [`StdlibConfig::with_path_override`] substitutes the host `PATH` the
/// helper searches.
///
/// # Errors
///
/// Returns an error if YAML parsing or Jinja evaluation fails.
///
/// # Examples
///
/// ```
/// use netsuke::{
/// ast::Recipe,
/// manifest::{EnvReadError, EnvReader, from_str_with_env_and_config},
/// stdlib::StdlibConfig,
/// };
/// use std::sync::Arc;
///
/// let reader: EnvReader = Arc::new(|name| match name {
/// "PROFILE" => Ok("release".to_owned()),
/// _ => Err(EnvReadError::NotPresent),
/// });
/// let config = StdlibConfig::from_current_dir()
/// .expect("construct stdlib config")
/// .with_path_override("");
/// let yaml = concat!(
/// "netsuke_version: 1.0.0\n",
/// "targets:\n",
/// " - name: build\n",
/// " command: echo {{ env('PROFILE') }}\n",
/// );
/// let manifest =
/// from_str_with_env_and_config(yaml, &reader, config).expect("parse manifest");
///
/// assert!(matches!(
/// &manifest.targets[0].recipe,
/// Recipe::Command { command } if command.as_single() == Some("echo release")
/// ));
/// ```
pub fn from_str_with_env_and_config(
yaml: &str,
env_reader: &EnvReader,
stdlib_config: StdlibConfig,
) -> Result<NetsukeManifest> {
from_str_named(
yaml,
ManifestParse {
name: &ManifestName::new("Netsukefile"),
stdlib_config: Some(stdlib_config),
env_reader,
},
&mut None,
)
}
58 changes: 56 additions & 2 deletions tests/ninja_snapshot_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
//! output using `insta`, and validate it with the real `ninja`
//! executable. The manifest uses a simple TOUCH rule so the build is
//! fast and deterministic.

use anyhow::{Context, Result, ensure};
use cap_std::{ambient_authority, fs_utf8::Dir};
use insta::{Settings, assert_snapshot};
use netsuke::{ir::BuildGraph, manifest, ninja_gen};
use netsuke::{ir::BuildGraph, manifest, ninja_gen, stdlib::StdlibConfig};
use std::{fs, process::Command};
use tempfile::tempdir;
use test_support::ensure_binaries_available;
Expand Down Expand Up @@ -131,6 +130,61 @@ fn conditional_manifest_ninja_snapshot() -> Result<()> {
Ok(())
}

#[test]
fn command_available_manifest_ninja_snapshot() -> Result<()> {
// Pin the `command_available` resolver to an empty PATH through the
// stdlib configuration seam, so a host or CI image with a binary named
// like the fixture cannot flip the guard. `cwd_mode="never"` additionally
// excludes the workspace root from the search; the absent command name
// alone is not sufficient for determinism.
let manifest_yaml = r#"
netsuke_version: "1.0.0"
actions:
- name: preferred-action
command: echo preferred
when: command_available("netsuke-command-that-should-not-exist", cwd_mode="never")
- name: fallback-action
command: echo fallback
when: not command_available("netsuke-command-that-should-not-exist", cwd_mode="never")
rules:
- name: touch
command: "touch $out"
targets:
- name: out/result
sources: in/source
rule: touch
"#;

let config = StdlibConfig::from_current_dir()?.with_path_override("");
let manifest = manifest::from_str_with_env_and_config(
manifest_yaml,
&manifest::process_env_reader(),
config,
)?;
let ir = BuildGraph::from_manifest(&manifest)?;
let ninja_content = ninja_gen::generate(&ir)?;

ensure!(
ninja_content.contains("fallback"),
"expected fallback action in Ninja output:\n{ninja_content}"
);
ensure!(
!ninja_content.contains("preferred"),
"preferred action guarded by an absent command should not appear:\n{ninja_content}"
);

let mut settings = Settings::new();
settings.set_snapshot_path(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/snapshots/ninja"
));
settings.bind(|| {
assert_snapshot!("command_available_manifest_ninja", ninja_content);
});

Ok(())
}

#[test]
fn multi_command_manifest_ninja_snapshot() -> Result<()> {
let fixture_dir = Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: tests/ninja_snapshot_tests.rs
assertion_line: 174
expression: ninja_content
---
rule 001cc863ffad2333bb5b4a22fde35146827965e79233821681254065519a1ce2
command = touch out/result

rule e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5
command = echo fallback

build fallback-action: e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5

build out/result: 001cc863ffad2333bb5b4a22fde35146827965e79233821681254065519a1ce2 in/source
Loading