From 274adcef19663201c8dcf0dc099d04eae00e9fb1 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 11 Aug 2025 19:39:32 +0100 Subject: [PATCH] Test PATH restoration via injected Env --- tests/cli_world_tests.rs | 27 +++++++++++++ tests/cucumber.rs | 38 ++---------------- tests/steps/process_steps.rs | 11 ++++-- tests/world.rs | 76 ++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 tests/cli_world_tests.rs create mode 100644 tests/world.rs diff --git a/tests/cli_world_tests.rs b/tests/cli_world_tests.rs new file mode 100644 index 00000000..902645e9 --- /dev/null +++ b/tests/cli_world_tests.rs @@ -0,0 +1,27 @@ +//! Tests for environment restoration in `CliWorld`. + +mod world; +use mockable::MockEnv; +use world::CliWorld; + +#[test] +fn drop_restores_path() { + let original = std::env::var("PATH").unwrap_or_default(); + { + let mut env = MockEnv::new(); + let original_clone = original.clone(); + env.expect_raw() + .withf(|key| key == "PATH") + .returning(move |_| Ok(original_clone.clone())); + let mut world = CliWorld::default(); + world.env = Box::new(env); + world.original_path = Some(world.env.raw("PATH").expect("retrieve PATH").into()); + unsafe { + std::env::set_var("PATH", "temp-path"); + } + } + assert_eq!( + std::env::var("PATH").expect("read PATH after drop"), + original + ); +} diff --git a/tests/cucumber.rs b/tests/cucumber.rs index 76e7885d..0082b178 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -1,38 +1,8 @@ -//! Cucumber test runner and world state. +//! Cucumber test runner. -use cucumber::World; - -/// Shared state for Cucumber scenarios. -#[derive(Debug, Default, World)] -pub struct CliWorld { - pub cli: Option, - pub cli_error: Option, - pub manifest: Option, - pub manifest_error: Option, - pub build_graph: Option, - /// Generated Ninja file content. - pub ninja: Option, - /// Status of the last process execution (true for success, false for - /// failure). - pub run_status: Option, - /// Error message from the last failed process execution. - pub run_error: Option, - /// Temporary directory handle for test isolation. - pub temp: Option, - /// Original `PATH` value restored after each scenario. - pub original_path: Option, -} - -impl Drop for CliWorld { - fn drop(&mut self) { - if let Some(path) = self.original_path.take() { - // SAFETY: nightly marks `set_var` as unsafe; restore path for isolation. - unsafe { - std::env::set_var("PATH", path); - } - } - } -} +mod world; +use cucumber::World as _; +pub use world::CliWorld; mod steps; mod support; diff --git a/tests/steps/process_steps.rs b/tests/steps/process_steps.rs index bffcb331..2ebc44e9 100644 --- a/tests/steps/process_steps.rs +++ b/tests/steps/process_steps.rs @@ -3,6 +3,7 @@ use crate::{CliWorld, support}; use cucumber::{given, then, when}; use netsuke::runner::{self, BuildTargets, NINJA_PROGRAM}; +use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; use tempfile::{NamedTempFile, TempDir}; @@ -13,9 +14,13 @@ use tempfile::{NamedTempFile, TempDir}; reason = "helper owns path for simplicity" )] fn install_test_ninja(world: &mut CliWorld, dir: TempDir, ninja_path: PathBuf) { - let original = world - .original_path - .get_or_insert_with(|| std::env::var_os("PATH").unwrap_or_default()); + let original = world.original_path.get_or_insert_with(|| { + world + .env + .raw("PATH") + .map(OsString::from) + .unwrap_or_default() + }); let new_path = format!("{}:{}", dir.path().display(), original.to_string_lossy()); // SAFETY: nightly marks `set_var` as unsafe; override path for test isolation. diff --git a/tests/world.rs b/tests/world.rs new file mode 100644 index 00000000..4db124a5 --- /dev/null +++ b/tests/world.rs @@ -0,0 +1,76 @@ +//! Shared test world for Cucumber scenarios. + +use cucumber::World; +use mockable::{Env, MockEnv}; + +/// Shared state for Cucumber scenarios. +#[derive(World)] +pub struct CliWorld { + pub cli: Option, + pub cli_error: Option, + pub manifest: Option, + pub manifest_error: Option, + pub build_graph: Option, + /// Generated Ninja file content. + pub ninja: Option, + /// Status of the last process execution (true for success, false for + /// failure). + pub run_status: Option, + /// Error message from the last failed process execution. + pub run_error: Option, + /// Temporary directory handle for test isolation. + pub temp: Option, + /// Mockable environment access. + pub env: Box, + /// Original `PATH` value restored after each scenario. + pub original_path: Option, +} + +impl std::fmt::Debug for CliWorld { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CliWorld") + .field("cli", &self.cli) + .field("cli_error", &self.cli_error) + .field("manifest", &self.manifest) + .field("manifest_error", &self.manifest_error) + .field("build_graph", &self.build_graph) + .field("ninja", &self.ninja) + .field("run_status", &self.run_status) + .field("run_error", &self.run_error) + .field("temp", &self.temp) + .field("env", &"") + .field("original_path", &self.original_path) + .finish() + } +} + +impl Default for CliWorld { + fn default() -> Self { + let mut env = MockEnv::new(); + env.expect_raw().returning(|key| std::env::var(key)); + Self { + cli: None, + cli_error: None, + manifest: None, + manifest_error: None, + build_graph: None, + ninja: None, + run_status: None, + run_error: None, + temp: None, + env: Box::new(env), + original_path: None, + } + } +} + +impl Drop for CliWorld { + fn drop(&mut self) { + if let Some(path) = self.original_path.take() { + // SAFETY: Rust 2024 marks `set_var` as unsafe. Dropping `CliWorld` + // reinstates the original `PATH`, ensuring scenarios cannot leak + // environment changes into subsequent tests. + unsafe { std::env::set_var("PATH", path) } + } + } +}