Skip to content
Closed
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
1 change: 1 addition & 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 @@ -68,6 +68,7 @@ insta = { version = "1", features = ["yaml"] }
assert_cmd = "2.0.17"
mockable = { version = "0.3", features = ["mock"] }
serial_test = "3"
mockall = "0.11"
Comment thread
leynos marked this conversation as resolved.

[[test]]
name = "cucumber"
Expand Down
42 changes: 42 additions & 0 deletions tests/path_guard_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Tests for PATH restoration behaviour using mock environments.
//!
//! 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;

mock! {
pub Env {}
impl Env for Env {
unsafe fn set_var(&mut self, key: &str, val: &OsStr);
}
}
Comment on lines +15 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer rstest fixtures for reusable mocks per repository testing guidance

Adopt rstest fixtures for the mock environment to align with the repo’s testing standards and reduce duplication as tests grow.

According to the team’s guidance (DeepWiki), tests should use rstest fixtures for injectable environments with mockall. I can sketch a fixture like:

use rstest::fixture;

#[fixture]
fn mock_env() -> MockEnv {
    let mut env = MockEnv::new();
    // set common expectations here if needed
    env
}

Would you like me to refactor the tests to use rstest and extract a reusable mock_env fixture?

🤖 Prompt for AI Agents
In tests/path_guard_tests.rs around lines 12 to 17, the mocked Env is declared
inline with mockall; replace that pattern with an rstest fixture so tests can
reuse the mock per the repo guideline. Create a #[fixture] fn mock_env() ->
MockEnv that constructs MockEnv::new(), configures any common expectations, and
returns it; update tests to accept mock_env: MockEnv as a parameter instead of
creating local mocks; add the rstest::fixture import and any necessary mockall
imports to the test module and ensure visibility (pub if needed) so existing
tests compile.


#[test]
fn restores_path_without_touching_real_env() {
let mut env = MockEnv::new();
let mut seq = Sequence::new();
env.expect_set_var()
.withf(|k, v| k == "PATH" && v == OsStr::new("/tmp"))
.times(1)
.in_sequence(&mut seq)
.return_const(());
env.expect_set_var()
.withf(|k, v| k == "PATH" && v == OsStr::new("/orig"))
.times(1)
.in_sequence(&mut seq)
.return_const(());
{
let mut guard = PathGuard::with_env("/orig".into(), env);
unsafe {
guard.env_mut().set_var("PATH", OsStr::new("/tmp"));
}
}
}
3 changes: 3 additions & 0 deletions tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! This module provides helpers for creating fake executables along with
//! logging utilities used in behavioural tests.

#![allow(unexpected_cfgs, reason = "test utilities use custom cfg")]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: violates custom rule forbidding #[allow] - should use #[expect(unexpected_cfgs, reason = "...")] instead

Suggested change
#![allow(unexpected_cfgs, reason = "test utilities use custom cfg")]
#![expect(unexpected_cfgs, reason = "test utilities use custom cfg")]

Context Used: Rule - #[allow] is forbidden. (link)


pub mod env_lock;
pub mod path_guard;

Expand All @@ -17,6 +19,7 @@ use tempfile::TempDir;
/// Create a fake Ninja executable that exits with `exit_code`.
///
/// Returns the temporary directory and the path to the executable.
#[cfg(test)]
pub fn fake_ninja(exit_code: i32) -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("temp dir");
let path = dir.path().join("ninja");
Expand Down
74 changes: 64 additions & 10 deletions tests/support/path_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,89 @@
//! Provides a guard that resets the environment variable on drop so tests do
//! not pollute global state.

use std::ffi::OsString;
#![expect(unexpected_cfgs, reason = "test utilities use custom cfg")]

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

use super::env_lock::EnvLock;

/// Environment interface allowing `PATH` mutation.
#[cfg(test)]
pub trait Env {
/// Set an environment variable.
///
/// # Safety
///
/// Mutating process-wide state is `unsafe` in Rust 2024.
unsafe fn set_var(&mut self, key: &str, val: &OsStr);
}

/// Real environment implementation.
#[cfg(test)]
#[derive(Debug, Default)]
pub struct RealEnv;

impl Env for RealEnv {
#[expect(unsafe_op_in_unsafe_fn, reason = "delegates to std::env")]
unsafe fn set_var(&mut self, key: &str, val: &OsStr) {
std::env::set_var(key, val);
}
}

/// 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")]
#[cfg_attr(expect, expect(dead_code, reason = "only some tests mutate PATH"))]
#[cfg_attr(not(expect), allow(dead_code, reason = "only some tests mutate PATH"))]
Comment thread
leynos marked this conversation as resolved.
#[derive(Debug)]
pub struct PathGuard {
pub struct PathGuard<E: Env = RealEnv> {
env: E,
original_path: Option<OsString>,
}

impl PathGuard {
#[allow(dead_code, reason = "only some tests mutate PATH")]
/// Create a guard capturing the current `PATH`.
/// Create a guard capturing the current `PATH` using the real environment.
#[cfg_attr(expect, expect(dead_code, reason = "only some tests mutate PATH"))]
#[cfg_attr(not(expect), allow(dead_code, reason = "only some tests mutate PATH"))]
pub fn new(original: OsString) -> Self {
Self::with_env(original, RealEnv)
}
}
Comment thread
leynos marked this conversation as resolved.

impl<E: Env> PathGuard<E> {
/// Create a guard for `PATH` using a provided environment.
#[cfg_attr(expect, expect(dead_code, reason = "only some tests mutate PATH"))]
#[cfg_attr(not(expect), allow(dead_code, reason = "only some tests mutate PATH"))]
pub fn with_env(original: OsString, env: E) -> Self {
Self {
env,
original_path: Some(original),
}
}
Comment thread
leynos marked this conversation as resolved.
}

impl Drop for PathGuard {
fn drop(&mut self) {
/// Access the underlying environment for mutation during a test.
#[cfg_attr(expect, expect(dead_code, reason = "used in env injection tests"))]
#[cfg_attr(not(expect), allow(dead_code, reason = "used in env injection tests"))]
pub fn env_mut(&mut self) -> &mut E {
&mut self.env
}

Comment thread
leynos marked this conversation as resolved.
#[cfg_attr(expect, expect(dead_code, reason = "only used via Drop impl"))]
#[cfg_attr(not(expect), allow(dead_code, reason = "only used via Drop impl"))]
fn restore(&mut self) {
let _lock = EnvLock::acquire();
if let Some(path) = self.original_path.take() {
// Nightly marks `set_var` unsafe; restoring `PATH` cleans up global state.
unsafe { std::env::set_var("PATH", path) };
// SAFETY: `std::env::set_var` is `unsafe` in Rust 2024 because it
// mutates process-wide state. `EnvLock` serialises mutations and
// this guard's RAII drop restores the prior `PATH`, mitigating the
// unsafety.
unsafe { self.env.set_var("PATH", &path) };
}
}
Comment thread
leynos marked this conversation as resolved.
}

impl<E: Env> Drop for PathGuard<E> {
fn drop(&mut self) {
self.restore();
}
}
Loading