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
9 changes: 9 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 @@ -69,6 +69,7 @@ assert_cmd = "2.0.17"
mockable = { version = "0.3", features = ["mock"] }
serial_test = "3"
mockall = "0.11"
test_support = { path = "test_support" }

[[test]]
name = "cucumber"
Expand Down
13 changes: 13 additions & 0 deletions test_support/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "test_support"
version = "0.1.0"
edition = "2024"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
rust-version = "1.85.0"
publish = false

[dependencies]
tempfile = "3.8.0"
mockable = { version = "0.3", features = ["mock"] }

[dev-dependencies]
rstest = "0.18.0"
File renamed without changes.
8 changes: 1 addition & 7 deletions tests/support/env.rs → test_support/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@
//! manifests.

use mockable::{DefaultEnv, Env, MockEnv};
use rstest::fixture;
use std::ffi::{OsStr, OsString};
use std::io::{self, Write};
use std::path::Path;

use crate::support::env_lock::EnvLock;
use crate::support::path_guard::PathGuard;
use crate::{env_lock::EnvLock, path_guard::PathGuard};

/// Alias for the real process environment.
#[allow(dead_code, reason = "re-exported for tests")]
pub type SystemEnv = DefaultEnv;

/// Environment trait with mutation capabilities.
Expand Down Expand Up @@ -44,7 +41,6 @@ impl EnvMut for MockEnv {
/// Returns a `MockEnv` that yields the current `PATH` when queried. Tests can
/// modify the real environment while the mock continues to expose the initial
/// value.
#[fixture]
pub fn mocked_path_env() -> MockEnv {
let original = std::env::var("PATH").unwrap_or_default();
let mut env = MockEnv::new();
Expand All @@ -57,7 +53,6 @@ pub fn mocked_path_env() -> MockEnv {
/// Write a minimal manifest to `file`.
///
/// The manifest declares a single `hello` target that prints a greeting.
#[allow(dead_code, reason = "used in Cucumber tests")]
pub fn write_manifest(file: &mut impl Write) -> io::Result<()> {
writeln!(
file,
Expand All @@ -77,7 +72,6 @@ pub fn write_manifest(file: &mut impl Write) -> io::Result<()> {
/// Mutating `PATH` is `unsafe` in Rust 2024 because it alters process globals.
/// `EnvLock` serialises access and `PathGuard` rolls back the change, keeping
/// the unsafety scoped to a single test.
#[allow(dead_code, reason = "used in runner tests")]
pub fn prepend_dir_to_path(env: &impl EnvMut, dir: &Path) -> PathGuard {
let original = env.raw("PATH").ok();
let original_os = original.clone().map(OsString::from);
Expand Down
File renamed without changes.
100 changes: 100 additions & 0 deletions test_support/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Test-support crate for Netsuke.
//!
//! This crate provides test-only utilities for:
//! - creating fake executables for process-related tests
//! - manipulating PATH safely (PathGuard)
//! - serialising environment mutation across tests (EnvLock)
//!
//! All items are intended for use in tests within this workspace; avoid using
//! them in production code.
//!
//! Platform notes: fake executables are implemented for Unix and Windows.

pub mod check_ninja;
pub mod env;
pub mod env_lock;
pub mod path_guard;
/// Re-export of [`PathGuard`] for crate-level ergonomics in tests.
pub use path_guard::PathGuard;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use tempfile::TempDir;

/// Create a fake Ninja executable that exits with `exit_code`.
///
/// Returns the temporary directory and the path to the executable.
///
/// The returned [`TempDir`] must be kept alive for the executable to remain on
/// disk.
///
/// # Example
///
/// ```rust,ignore
/// use test_support::fake_ninja;
///
/// // Create a fake `ninja` that exits with code 1
/// let (dir, ninja_path) = fake_ninja(1u8);
///
/// // Prepend `dir.path()` to PATH via your env helper, then spawn `ninja`.
/// // When `dir` is dropped, the fake executable is removed.
/// ```
pub fn fake_ninja(exit_code: u8) -> (TempDir, PathBuf) {
let dir = TempDir::new()
.unwrap_or_else(|e| panic!("fake_ninja: failed to create temporary directory: {e}"));

#[cfg(unix)]
let path = dir.path().join("ninja");
#[cfg(windows)]
let path = dir.path().join("ninja.cmd");

#[cfg(unix)]
{
let mut file = File::create(&path).unwrap_or_else(|e| {
panic!(
"fake_ninja: failed to create script {}: {e}",
path.display()
)
});
writeln!(file, "#!/bin/sh\nexit {}", exit_code).unwrap_or_else(|e| {
panic!("fake_ninja: failed to write script {}: {e}", path.display())
});
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path)
.unwrap_or_else(|e| {
panic!(
"fake_ninja: failed to read metadata {}: {e}",
path.display()
)
})
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap_or_else(|e| {
panic!(
"fake_ninja: failed to set permissions {}: {e}",
path.display()
)
});
}

#[cfg(windows)]
{
let mut file = File::create(&path).unwrap_or_else(|e| {
panic!(
"fake_ninja: failed to create batch file {}: {e}",
path.display()
)
});
writeln!(file, "@echo off\r\nexit /B {}", exit_code).unwrap_or_else(|e| {
panic!(
"fake_ninja: failed to write batch file {}: {e}",
path.display()
)
});
}

(dir, path)
}

// Additional helpers can be added here as the test suite evolves.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use std::ffi::{OsStr, OsString};

use super::env_lock::EnvLock;
use crate::env_lock::EnvLock;

/// Environment abstraction for setting variables.
pub trait Env {
Expand All @@ -28,7 +28,6 @@ impl Env for StdEnv {
}

/// Original `PATH` state captured by `PathGuard`.
#[allow(dead_code, reason = "only some tests mutate PATH")]
#[derive(Debug)]
enum OriginalPath {
Unset,
Expand All @@ -38,7 +37,6 @@ enum OriginalPath {
/// Guard that restores `PATH` to its original value when dropped.
///
/// This uses RAII to ensure the environment is reset even if a test panics.
#[allow(dead_code, reason = "only some tests mutate PATH")]
#[derive(Debug)]
pub struct PathGuard<E: Env = StdEnv> {
original: Option<OriginalPath>,
Expand All @@ -49,7 +47,6 @@ impl PathGuard {
/// Create a guard capturing the current `PATH` using the real environment.
///
/// Returns a guard that restores the variable when dropped.
#[allow(dead_code, reason = "only some tests mutate PATH")]
pub fn new(original: Option<OsString>) -> Self {
let state = original.map_or(OriginalPath::Unset, OriginalPath::Set);
Self {
Expand All @@ -61,7 +58,6 @@ impl PathGuard {

impl<E: Env> PathGuard<E> {
/// Create a guard that uses `env` to restore `PATH`.
#[allow(dead_code, reason = "only some tests mutate PATH")]
pub fn with_env(original: OsString, env: E) -> Self {
Self {
original: Some(OriginalPath::Set(original)),
Expand All @@ -70,7 +66,6 @@ impl<E: Env> PathGuard<E> {
}

/// Access the underlying environment.
#[allow(dead_code, reason = "only some tests mutate PATH")]
pub fn env_mut(&mut self) -> &mut E {
&mut self.env
}
Expand Down
5 changes: 2 additions & 3 deletions tests/assert_cmd_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
use assert_cmd::Command;
use std::fs;
use tempfile::tempdir;

mod support;
use test_support::fake_ninja;

#[test]
fn manifest_subcommand_writes_file() {
Expand All @@ -27,7 +26,7 @@ fn manifest_subcommand_writes_file() {

#[test]
fn build_with_emit_writes_file() {
let (ninja_dir, _ninja_path) = support::fake_ninja(0);
let (ninja_dir, _ninja_path) = fake_ninja(0u8);
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");
Expand Down
8 changes: 2 additions & 6 deletions tests/cucumber.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Cucumber test runner and world state.

use cucumber::World;
use test_support::PathGuard;

/// Shared state for Cucumber scenarios.
#[derive(Debug, Default, World)]
Expand All @@ -20,15 +21,10 @@ pub struct CliWorld {
/// Temporary directory handle for test isolation.
pub temp: Option<tempfile::TempDir>,
/// Guard that restores `PATH` after each scenario.
pub path_guard: Option<support::path_guard::PathGuard>,
pub path_guard: Option<PathGuard>,
}

#[path = "support/check_ninja.rs"]
mod check_ninja;
#[path = "support/env.rs"]
mod env;
mod steps;
mod support;

#[tokio::main]
async fn main() {
Expand Down
10 changes: 4 additions & 6 deletions tests/env_path_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
use mockable::Env;
use rstest::rstest;
use serial_test::serial;

#[path = "support/env.rs"]
mod env;
mod support;
use env::{SystemEnv, mocked_path_env, prepend_dir_to_path};
use support::env_lock::EnvLock;
use test_support::{
env::{SystemEnv, mocked_path_env, prepend_dir_to_path},
env_lock::EnvLock,
};

#[rstest]
#[serial]
Expand Down
7 changes: 1 addition & 6 deletions tests/path_guard_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
//! Verifies that `PathGuard` restores `PATH` without mutating the real
//! process environment.

#[path = "support/env_lock.rs"]
mod env_lock;
#[path = "support/path_guard.rs"]
mod path_guard;

use mockall::{Sequence, mock};
use path_guard::{Env, PathGuard};
use std::ffi::OsStr;
use test_support::{PathGuard, path_guard::Env};

mock! {
pub Env {}
Expand Down
24 changes: 13 additions & 11 deletions tests/runner_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ use netsuke::runner::{BuildTargets, NINJA_ENV, run, run_ninja};
use rstest::{fixture, rstest};
use serial_test::serial;
use std::path::{Path, PathBuf};

#[path = "support/check_ninja.rs"]
mod check_ninja;
#[path = "support/env.rs"]
mod env;
mod support;
use env::{SystemEnv, prepend_dir_to_path};
use support::path_guard::PathGuard;
use test_support::{
check_ninja,
env::{SystemEnv, prepend_dir_to_path},
env_lock::EnvLock,
fake_ninja,
path_guard::PathGuard,
};

/// Fixture: Put a fake `ninja` (that checks for a build file) on `PATH`.
///
Expand All @@ -35,8 +34,8 @@ fn ninja_in_path() -> (tempfile::TempDir, PathBuf, PathGuard) {
///
/// Returns: (tempdir holding ninja, `ninja_path`, PATH guard)
#[fixture]
fn ninja_with_exit_code(#[default(0)] exit_code: i32) -> (tempfile::TempDir, PathBuf, PathGuard) {
let (ninja_dir, ninja_path) = support::fake_ninja(exit_code);
fn ninja_with_exit_code(#[default(0u8)] exit_code: u8) -> (tempfile::TempDir, PathBuf, PathGuard) {
let (ninja_dir, ninja_path) = fake_ninja(exit_code);
let env = SystemEnv::new();
let guard = prepend_dir_to_path(&env, ninja_dir.path());
(ninja_dir, ninja_path, guard)
Expand Down Expand Up @@ -209,8 +208,10 @@ fn run_manifest_subcommand_writes_file() {
#[test]
#[serial]
fn run_respects_env_override_for_ninja() {
let (temp_dir, ninja_path) = support::fake_ninja(0);
let (temp_dir, ninja_path) = fake_ninja(0u8);
let original = std::env::var_os(NINJA_ENV);
let _lock = EnvLock::acquire();
// SAFETY: `EnvLock` serialises access to process-global state.
unsafe {
std::env::set_var(NINJA_ENV, &ninja_path);
}
Expand All @@ -232,6 +233,7 @@ fn run_respects_env_override_for_ninja() {
let result = run(&cli);
assert!(result.is_ok());

// SAFETY: `EnvLock` ensures exclusive access while the variable is reset.
unsafe {
if let Some(val) = original {
std::env::set_var(NINJA_ENV, val);
Expand Down
15 changes: 8 additions & 7 deletions tests/steps/process_steps.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
//! Step definitions for Ninja process execution.

use crate::{
CliWorld, check_ninja,
env::{self, EnvMut},
support,
};
use crate::CliWorld;
use cucumber::{given, then, when};
use netsuke::runner::{self, BuildTargets, NINJA_PROGRAM};
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::{NamedTempFile, TempDir};
use test_support::{
check_ninja,
env::{self, EnvMut},
fake_ninja,
};

/// Installs a test-specific ninja binary and updates the `PATH`.
#[expect(
Expand All @@ -25,8 +26,8 @@ fn install_test_ninja(env: &impl EnvMut, world: &mut CliWorld, dir: TempDir, nin

/// Creates a fake ninja executable that exits with the given status code.
#[given(expr = "a fake ninja executable that exits with {int}")]
fn fake_ninja(world: &mut CliWorld, code: i32) {
let (dir, path) = support::fake_ninja(code);
fn install_fake_ninja(world: &mut CliWorld, code: i32) {
let (dir, path) = fake_ninja(u8::try_from(code).expect("exit code must be between 0 and 255"));
let env = env::mocked_path_env();
install_test_ninja(&env, world, dir, path);
}
Expand Down
Loading
Loading