diff --git a/build.rs b/build.rs index 8c8801471..ef9d85ca4 100644 --- a/build.rs +++ b/build.rs @@ -68,6 +68,10 @@ mod theme; mod build_l10n_audit; +#[expect( + clippy::disallowed_methods, + reason = "SOURCE_DATE_EPOCH is the reproducible-builds contract: the build system supplies it to the build script's process, so there is no seam to inject it through" +)] fn manual_date() -> String { let Ok(raw) = env::var("SOURCE_DATE_EPOCH") else { return FALLBACK_DATE.into(); @@ -95,6 +99,10 @@ fn manual_date() -> String { }) } +#[expect( + clippy::disallowed_methods, + reason = "TARGET and PROFILE are set by Cargo for the build script alone; nothing else knows the triple and profile being built, so they cannot be passed in" +)] fn out_dir_for_target_profile() -> PathBuf { let target = env::var("TARGET").unwrap_or_else(|_| "unknown-target".into()); let profile = env::var("PROFILE").unwrap_or_else(|_| "unknown-profile".into()); @@ -132,6 +140,10 @@ fn emit_rerun_directives() { println!("cargo:rerun-if-changed=locales/es-ES/messages.ftl"); } +#[expect( + clippy::disallowed_methods, + reason = "CARGO_BIN_NAME, CARGO_PKG_NAME, CARGO_PKG_VERSION and OUT_DIR are Cargo's own build-script inputs; they describe the crate being compiled and Cargo provides them only through the environment" +)] fn generate_man_page(out_dir: &Path) -> Result<(), Box> { let cmd = cli::Cli::command(); let name = cmd diff --git a/clippy.toml b/clippy.toml index effc60402..bbe70becd 100644 --- a/clippy.toml +++ b/clippy.toml @@ -5,3 +5,18 @@ too-many-lines-threshold = 70 # default is 100 excessive-nesting-threshold = 4 # default is off allow-expect-in-tests = true + +# Enforce the AGENTS.md environment mandate. The reason strings surface in the +# diagnostic, so a contributor who trips one is told what to do instead. +# +# Sanctioned sites carry `#[expect(clippy::disallowed_methods, reason = "..")]` +# rather than `allow`, so the expectation goes unfulfilled — and warns — once the +# site is migrated. The backlog removes itself instead of rotting. +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::var_os", reason = "inject an environment reader" }, + { path = "std::env::vars", reason = "inject an environment reader" }, + { path = "std::env::vars_os", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + { path = "std::env::remove_var", reason = "use a stub environment in tests" }, +] diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 39507b852..0c0865ddd 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -1408,6 +1408,53 @@ still safe to use. See the [locale-pinned snapshot tests](snapshot-testing-in-netsuke-using-insta.md#locale-pinned-snapshot-tests) section for the fixture's intended usage. +### Enforcing the environment mandate + +`clippy.toml` disallows the six process-environment entry points, so +`make lint` rejects a new one: + +```toml +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + # ... var_os, vars, vars_os, remove_var +] +``` + +The reason string appears in the diagnostic, so a contributor who trips the +lint is told what to do instead, not merely that they may not. `test_support` +is excluded from the workspace, so it carries its own copy of the list. + +#### Annotating a sanctioned site + +Use `#[expect]`, never `allow`: + +```rust +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the read_env seam" +)] +pub fn resolve(no_emoji: Option) -> OutputPrefs { + resolve_with(no_emoji, |key| env::var(key).ok()) +} +``` + +`expect` becomes *unfulfilled* — and warns — once the site stops tripping the +lint. A migrated file therefore fails the gate until its annotation is removed, +so the backlog cannot rot silently. `allow` would go stale invisibly. + +Three dispositions are in use: + +- **Composition roots** in `src/` keep a permanent site-level expectation naming + the seam they supply. These are the sanctioned ambient boundary. +- **Build scripts and artefact discovery** keep a permanent module-level + expectation: they read what Cargo reports, and there is no seam to inject. +- **Pending migrations** in `tests/` carry a module-level expectation naming the + tracking issue, removed as each file migrates. + +Scope an expectation as tightly as the site allows — a function where one call +is involved, a module only where the whole file is pending migration. + ### `EnvLock` `test_support::env_lock::EnvLock` is a global mutex that serializes all diff --git a/docs/users-guide.md b/docs/users-guide.md index 324907b3a..78d071339 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -11,9 +11,12 @@ change before 1.0. Pin the Netsuke version in automated workflows. ## Install Netsuke Netsuke requires [Ninja](https://ninja-build.org/) on `PATH`. A source build -also requires the dated Rust nightly toolchain pinned in `rust-toolchain.toml`, -because Netsuke builds with the Polonius borrow checker (`-Zpolonius=next`); -`rustup` installs it automatically inside a checkout. +also requires the dated Rust nightly toolchain pinned in `rust-toolchain.toml` +because Netsuke builds with the Polonius borrow checker (`-Zpolonius=next`). + +Inside a checkout both settings are inherited automatically: `rustup` installs +the pinned toolchain, and the repository's `.cargo/config.toml` supplies +`RUSTFLAGS=-Zpolonius=next`. Neither has to be passed on the command line. Netsuke v0.1.0 is available from crates.io. Where [`cargo binstall`](https://github.com/cargo-bins/cargo-binstall) is available, @@ -67,7 +70,9 @@ licence files. Installer packages do not have checksum sidecars in v0.1.0. Windows PowerShell help files are published beside each MSI as sidecar artefacts rather than embedded in the installer. -To install the current source checkout with Cargo: +To install the current source checkout with Cargo. The clone supplies both the +pinned nightly toolchain and `RUSTFLAGS=-Zpolonius=next`, so neither is given +here — unlike the registry install above, which runs outside a checkout: diff --git a/src/cli/discovery.rs b/src/cli/discovery.rs index d2be80145..dd118d1d3 100644 --- a/src/cli/discovery.rs +++ b/src/cli/discovery.rs @@ -44,6 +44,10 @@ pub trait EnvProvider { #[derive(Debug, Default, Clone, Copy)] pub struct StdEnvProvider; +#[expect( + clippy::disallowed_methods, + reason = "composition root: StdEnvProvider is the process-backed adapter behind the EnvProvider seam" +)] impl EnvProvider for StdEnvProvider { fn get(&self, key: &str) -> Option { std::env::var_os(key) diff --git a/src/locale_resolution.rs b/src/locale_resolution.rs index 74083f530..347a3c62a 100644 --- a/src/locale_resolution.rs +++ b/src/locale_resolution.rs @@ -25,6 +25,10 @@ pub trait EnvProvider { #[derive(Debug, Default, Copy, Clone)] pub struct SystemEnv; +#[expect( + clippy::disallowed_methods, + reason = "composition root: SystemEnv is the process-backed adapter behind the EnvProvider seam" +)] impl EnvProvider for SystemEnv { fn var(&self, key: &str) -> Option { std::env::var(key).ok() diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index 4dca09bee..e8ab81981 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -81,6 +81,10 @@ pub enum ManifestLoadStage { /// assert_eq!(env("FOO").unwrap(), "bar"); /// // guard restores prior value on drop /// ``` +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the env() Jinja helper" +)] fn env_var(name: &str) -> std::result::Result { match std::env::var(name) { Ok(val) => Ok(val), diff --git a/src/output_mode.rs b/src/output_mode.rs index 2b22d7362..ff2618f84 100644 --- a/src/output_mode.rs +++ b/src/output_mode.rs @@ -49,6 +49,10 @@ impl OutputMode { /// assert_eq!(resolve(Some(false), None), OutputMode::Standard); /// ``` #[must_use] +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the read_env seam" +)] pub fn resolve(explicit: Option, colour_policy: Option) -> OutputMode { resolve_with(explicit, colour_policy, |key| env::var(key).ok()) } diff --git a/src/output_prefs.rs b/src/output_prefs.rs index dde7e8401..9d9650749 100644 --- a/src/output_prefs.rs +++ b/src/output_prefs.rs @@ -186,6 +186,10 @@ impl OutputPrefs { /// assert!(!prefs.emoji_allowed()); /// ``` #[must_use] +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the read_env seam" +)] pub fn resolve_from_theme(theme: Option, context: ThemeContext) -> OutputPrefs { resolve_from_theme_with(theme, context, |key| env::var(key).ok()) } @@ -227,6 +231,10 @@ where /// assert!(resolve_with(Some(false), |_| None).emoji_allowed()); /// ``` #[must_use] +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the read_env seam" +)] pub fn resolve(no_emoji: Option) -> OutputPrefs { resolve_with(no_emoji, |key| env::var(key).ok()) } diff --git a/src/runner/process/ninja_program.rs b/src/runner/process/ninja_program.rs index 606730ee3..aeb9ca4e0 100644 --- a/src/runner/process/ninja_program.rs +++ b/src/runner/process/ninja_program.rs @@ -64,6 +64,10 @@ where /// Resolve the configured Ninja executable as a UTF-8 path. #[must_use] +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the ninja program resolver seam" +)] pub fn resolve_ninja_program_utf8() -> Utf8PathBuf { resolve_ninja_program_utf8_with(|key| env::var_os(key)) } diff --git a/src/stdlib/path/path_utils.rs b/src/stdlib/path/path_utils.rs index 0e9a0f296..ac90cfbce 100644 --- a/src/stdlib/path/path_utils.rs +++ b/src/stdlib/path/path_utils.rs @@ -152,6 +152,10 @@ fn current_dir_utf8() -> Result { } #[cfg(windows)] +#[expect( + clippy::disallowed_methods, + reason = "composition root: home resolution is the path stdlib's ambient boundary; the injected ladders are tracked in the environment meta issue" +)] fn home_from_env() -> Option { env::var("HOME") .or_else(|_| env::var("USERPROFILE")) @@ -165,6 +169,10 @@ fn home_from_env() -> Option { } #[cfg(not(windows))] +#[expect( + clippy::disallowed_methods, + reason = "composition root: home resolution is the path stdlib's ambient boundary; the injected ladders are tracked in the environment meta issue" +)] fn home_from_env() -> Option { env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok() } diff --git a/src/stdlib/which/env.rs b/src/stdlib/which/env.rs index 2a76b7d65..484c157fe 100644 --- a/src/stdlib/which/env.rs +++ b/src/stdlib/which/env.rs @@ -21,6 +21,10 @@ pub(super) struct EnvSnapshot { } impl EnvSnapshot { + #[expect( + clippy::disallowed_methods, + reason = "composition root: PATH and PATHEXT capture is the which resolver's ambient boundary; injection is tracked in the environment meta issue" + )] pub(super) fn capture( cwd_override: Option<&Utf8Path>, path_override: Option<&OsStr>, diff --git a/src/stdlib/which/lookup/workspace/mod.rs b/src/stdlib/which/lookup/workspace/mod.rs index 055214789..9e3129840 100644 --- a/src/stdlib/which/lookup/workspace/mod.rs +++ b/src/stdlib/which/lookup/workspace/mod.rs @@ -106,6 +106,10 @@ pub(super) fn should_visit_entry(entry: &walkdir::DirEntry, skip_dirs: &Workspac !skip_dirs.contains(&name) } +#[expect( + clippy::disallowed_methods, + reason = "composition root: supplies the process environment to the workspace fallback seam" +)] fn workspace_fallback_enabled() -> bool { match env::var(WORKSPACE_FALLBACK_ENV) { Ok(value) => { diff --git a/test_support/clippy.toml b/test_support/clippy.toml new file mode 100644 index 000000000..cc4374c81 --- /dev/null +++ b/test_support/clippy.toml @@ -0,0 +1,14 @@ +# Enforce the AGENTS.md environment mandate. The reason strings surface in the +# diagnostic, so a contributor who trips one is told what to do instead. +# +# Sanctioned sites carry `#[expect(clippy::disallowed_methods, reason = "..")]` +# rather than `allow`, so the expectation goes unfulfilled — and warns — once the +# site is migrated. The backlog removes itself instead of rotting. +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::var_os", reason = "inject an environment reader" }, + { path = "std::env::vars", reason = "inject an environment reader" }, + { path = "std::env::vars_os", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + { path = "std::env::remove_var", reason = "use a stub environment in tests" }, +] diff --git a/test_support/src/command_helper.rs b/test_support/src/command_helper.rs index 9705c100e..a10288195 100644 --- a/test_support/src/command_helper.rs +++ b/test_support/src/command_helper.rs @@ -120,6 +120,10 @@ pub fn compile_large_output_helper( /// .expect("compile helper"); /// assert!(exe.as_std_path().exists()); /// ``` +#[expect( + clippy::disallowed_methods, + reason = "reads the inherited environment to invoke rustc for a helper binary; the compiler must see the real toolchain" +)] pub fn compile_rust_helper( dir: &Dir, root: &Utf8PathBuf, diff --git a/test_support/src/dev_fast/sandbox.rs b/test_support/src/dev_fast/sandbox.rs index 2b4a0c95b..ad29ae33e 100644 --- a/test_support/src/dev_fast/sandbox.rs +++ b/test_support/src/dev_fast/sandbox.rs @@ -275,6 +275,10 @@ pub fn real_utility(utility: &str) -> Result { /// sourceable `env` shell fragment into `~/.local/bin`, so a plain file probe /// selects a non-executable file and every sandboxed `env` invocation then /// fails with a permission error. +#[expect( + clippy::disallowed_methods, + reason = "resolves a tool through the real PATH, which is what the sandbox under test must observe" +)] fn which(utility: &str) -> Result { let path = std::env::var_os("PATH").context("read PATH")?; for dir in std::env::split_paths(&path) { diff --git a/test_support/src/env.rs b/test_support/src/env.rs index dcc9979e9..a645216f0 100644 --- a/test_support/src/env.rs +++ b/test_support/src/env.rs @@ -43,12 +43,20 @@ pub trait EnvMut: Env { } impl EnvMut for DefaultEnv { + #[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" + )] unsafe fn set_var(&self, key: &str, value: &OsStr) { unsafe { std::env::set_var(key, value) }; } } impl EnvMut for MockEnv { + #[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" + )] unsafe fn set_var(&self, key: &str, value: &OsStr) { unsafe { std::env::set_var(key, value) }; } @@ -59,6 +67,10 @@ 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. +#[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" +)] pub fn mocked_path_env() -> MockEnv { let original = std::env::var("PATH").unwrap_or_default(); let mut env = MockEnv::new(); @@ -72,6 +84,10 @@ pub fn mocked_path_env() -> MockEnv { /// /// The mutation is `unsafe` in Rust 2024 as it alters process state. The /// unsafety is scoped by acquiring [`EnvLock`]. +#[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" +)] pub fn set_var(key: &str, value: &OsStr) -> Option { let _lock = EnvLock::acquire(); let previous = std::env::var_os(key); @@ -81,6 +97,10 @@ pub fn set_var(key: &str, value: &OsStr) -> Option { } /// Set an environment variable while the caller already holds [`EnvLock`]. +#[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" +)] pub fn set_var_locked(lock: &EnvLock, key: &str, value: &OsStr) -> Option { let _ = lock; let previous = std::env::var_os(key); @@ -93,6 +113,10 @@ pub fn set_var_locked(lock: &EnvLock, key: &str, value: &OsStr) -> Option Option { let _lock = EnvLock::acquire(); let previous = std::env::var_os(key); @@ -136,6 +160,10 @@ pub fn restore_many(vars: HashMap>) { /// # Safety /// /// Caller must hold [`EnvLock`] for the duration of this call. +#[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" +)] pub unsafe fn restore_many_locked(vars: HashMap>) { for (key, val) in vars { if let Some(v) = val { @@ -156,6 +184,10 @@ pub struct VarGuard { /// Run `action` with `PATH` temporarily set to `value`, restoring on drop and /// serialising mutations with `EnvLock`. +#[expect( + clippy::disallowed_methods, + reason = "test_support::env is the crate's process-environment boundary: these guards exist to set and restore real variables for subprocess and legacy in-process tests, which AGENTS.md sanctions" +)] pub fn with_isolated_path(value: &OsStr, action: impl FnOnce() -> T) -> T { let _lock = EnvLock::acquire(); let original = std::env::var_os("PATH"); diff --git a/test_support/src/env_guard.rs b/test_support/src/env_guard.rs index 890210c65..845e4d4bc 100644 --- a/test_support/src/env_guard.rs +++ b/test_support/src/env_guard.rs @@ -33,10 +33,18 @@ pub trait Environment { pub struct StdEnv; impl Environment for StdEnv { + #[expect( + clippy::disallowed_methods, + reason = "the guard's whole purpose is to mutate and restore a real process variable; injecting here would leave nothing to guard" + )] unsafe fn set_var(&mut self, key: &str, value: &OsStr) { unsafe { std::env::set_var(key, value) }; } + #[expect( + clippy::disallowed_methods, + reason = "the guard's whole purpose is to mutate and restore a real process variable; injecting here would leave nothing to guard" + )] unsafe fn remove_var(&mut self, key: &str) { unsafe { std::env::remove_var(key) }; } diff --git a/test_support/src/env_var_guard.rs b/test_support/src/env_var_guard.rs index 157d797f1..8a6032f37 100644 --- a/test_support/src/env_var_guard.rs +++ b/test_support/src/env_var_guard.rs @@ -37,6 +37,10 @@ impl EnvVarGuard { /// Mutating process-global state is `unsafe` in Rust 2024. Callers must hold /// an [`EnvLock`](crate::env_lock::EnvLock) to serialise mutations. #[must_use] + #[expect( + clippy::disallowed_methods, + reason = "the guard's whole purpose is to mutate and restore a real process variable; injecting here would leave nothing to guard" + )] pub fn set(name: impl Into>, val: impl AsRef) -> Self { let name = name.into(); let prev = std::env::var_os(&*name); @@ -54,6 +58,10 @@ impl EnvVarGuard { /// Callers must hold an [`EnvLock`](crate::env_lock::EnvLock) to serialise /// mutations of the process environment. #[must_use] + #[expect( + clippy::disallowed_methods, + reason = "the guard's whole purpose is to mutate and restore a real process variable; injecting here would leave nothing to guard" + )] pub fn remove(name: impl Into>) -> Self { let name = name.into(); let prev = std::env::var_os(&*name); diff --git a/test_support/src/http.rs b/test_support/src/http.rs index 27b50b941..b8110af92 100644 --- a/test_support/src/http.rs +++ b/test_support/src/http.rs @@ -276,6 +276,10 @@ fn write_response(stream: &mut TcpStream, body: &str) { let _ = stream.write_all(response.as_bytes()); } +#[expect( + clippy::disallowed_methods, + reason = "reads an ambient tuning variable for the stub HTTP server; the fixture is configured by the environment it runs in, and no seam reaches it" +)] fn duration_from_env(var: &str, default: Duration) -> Duration { match env::var(var) { Ok(value) => { diff --git a/test_support/src/netsuke.rs b/test_support/src/netsuke.rs index 444860822..212b903a1 100644 --- a/test_support/src/netsuke.rs +++ b/test_support/src/netsuke.rs @@ -13,6 +13,10 @@ use std::path::PathBuf; /// /// Prefer `CARGO_BIN_EXE_netsuke` when available, otherwise fall back to a /// `target/(debug|release)`-derived path based on the current test binary. +#[expect( + clippy::disallowed_methods, + reason = "reads the inherited environment to build the subprocess invocation; assert_cmd subprocess isolation is the sanctioned exemption in AGENTS.md" +)] fn netsuke_executable() -> Result { if let Some(path) = std::env::var_os("CARGO_BIN_EXE_netsuke") { return Ok(path.into()); @@ -90,6 +94,10 @@ pub fn run_netsuke_in(current_dir: &Path, args: &[&str]) -> Result { /// /// Returns an error when `netsuke` cannot be located or the process cannot be /// spawned. +#[expect( + clippy::disallowed_methods, + reason = "reads the inherited environment to build the subprocess invocation; assert_cmd subprocess isolation is the sanctioned exemption in AGENTS.md" +)] pub fn run_netsuke_in_with_env( current_dir: &Path, args: &[&str], diff --git a/tests/bdd/helpers/env_mutation.rs b/tests/bdd/helpers/env_mutation.rs index 4645c0f8e..9486a94ca 100644 --- a/tests/bdd/helpers/env_mutation.rs +++ b/tests/bdd/helpers/env_mutation.rs @@ -27,6 +27,10 @@ use std::ffi::OsString; /// /// Returns an error if the environment variable name is empty, contains '=', /// or contains `'\0'`, or if `new_value` contains `'\0'`. +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] pub fn mutate_env_var(world: &TestWorld, key: EnvVarKey, new_value: Option<&str>) -> Result<()> { ensure!( !key.as_str().is_empty(), @@ -80,6 +84,19 @@ mod tests { expect_present: bool, } + /// Read a variable back to confirm the mutation under test took effect. + /// + /// The expectation lives here rather than on the test because `rstest` + /// generates one function per case and the attribute does not survive that + /// expansion; a single narrow helper is tighter than a module-wide waiver. + #[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" + )] + fn read_back(key: &str) -> Result { + std::env::var(key) + } + #[rstest] #[case::empty_key(MutationTestCase { key: "", new_value: None, expect_error: true, expect_present: false })] #[case::key_with_equals(MutationTestCase { key: "KEY=VALUE", new_value: Some("test"), expect_error: true, expect_present: false })] @@ -116,7 +133,7 @@ mod tests { if tc.expect_present { assert_eq!( - std::env::var(tc.key).ok().as_deref(), + read_back(tc.key).ok().as_deref(), tc.new_value, "variable should be set to expected value" ); @@ -125,7 +142,7 @@ mod tests { .expect("cleanup should succeed"); } else if !tc.key.is_empty() { assert!( - std::env::var(tc.key).is_err(), + read_back(tc.key).is_err(), "variable should have been removed" ); } diff --git a/tests/bdd/steps/conditional_manifest.rs b/tests/bdd/steps/conditional_manifest.rs index 26596ba65..ce621796a 100644 --- a/tests/bdd/steps/conditional_manifest.rs +++ b/tests/bdd/steps/conditional_manifest.rs @@ -106,6 +106,10 @@ fn mark_executable(_path: &Path) -> Result<()> { Ok(()) } +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] fn prepend_path_for_child(world: &TestWorld, dir: &Path) -> Result<()> { let mut entries = vec![dir.to_path_buf()]; if let Some(host_path) = std::env::var_os("PATH") { diff --git a/tests/bdd/steps/fs.rs b/tests/bdd/steps/fs.rs index ce4929f46..5e5e5b7c9 100644 --- a/tests/bdd/steps/fs.rs +++ b/tests/bdd/steps/fs.rs @@ -97,6 +97,10 @@ fn create_device_with_fallback(config: DeviceConfig<'_>) -> Result Ok(fallback) } +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] fn setup_environment_variables( world: &TestWorld, root: &Utf8PathBuf, diff --git a/tests/bdd/steps/manifest/mod.rs b/tests/bdd/steps/manifest/mod.rs index 42438ed20..828905382 100644 --- a/tests/bdd/steps/manifest/mod.rs +++ b/tests/bdd/steps/manifest/mod.rs @@ -115,6 +115,10 @@ fn assert_parsed(world: &TestWorld) -> Result<()> { // Environment variable helpers // --------------------------------------------------------------------------- +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] fn parse_env_token(chars: &mut std::iter::Peekable) -> String where I: Iterator, diff --git a/tests/bdd/steps/manifest_command_helpers.rs b/tests/bdd/steps/manifest_command_helpers.rs index 657b365b0..f459e77b8 100644 --- a/tests/bdd/steps/manifest_command_helpers.rs +++ b/tests/bdd/steps/manifest_command_helpers.rs @@ -139,6 +139,10 @@ pub(super) fn netsuke_executable() -> Result { /// - Controlled `PATH` variable /// /// Returns the configured command ready for execution. +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] pub(super) fn build_netsuke_command( world: &TestWorld, args: &[&str], diff --git a/tests/bdd/steps/stdlib/workspace.rs b/tests/bdd/steps/stdlib/workspace.rs index 6352ec268..73bc5293c 100644 --- a/tests/bdd/steps/stdlib/workspace.rs +++ b/tests/bdd/steps/stdlib/workspace.rs @@ -285,6 +285,10 @@ pub(crate) fn stdlib_path_entries(world: &TestWorld, entries: &str) -> Result<() } #[given("HOME points to the stdlib workspace root")] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #492 (rstest-bdd migration)" +)] pub(crate) fn home_points_to_stdlib_root(world: &TestWorld) -> Result<()> { let root = ensure_workspace(world)?; let os_root = OsStr::new(root.as_str()); diff --git a/tests/documentation_examples_e2e_tests.rs b/tests/documentation_examples_e2e_tests.rs index f4f4af4f9..5dd056a09 100644 --- a/tests/documentation_examples_e2e_tests.rs +++ b/tests/documentation_examples_e2e_tests.rs @@ -16,6 +16,10 @@ use test_support::fs as test_fs; use test_support::netsuke::{NetsukeRun, run_netsuke_in_with_env}; use test_support::{ninja::ninja_integration_workspace, write_exec, write_exec_with_content}; +#[expect( + clippy::disallowed_methods, + reason = "locating build artefacts Cargo reports through the environment; there is no seam to inject and no process state to isolate" +)] fn executable_path(stub_directory: &Utf8Path) -> Result { let host_path = std::env::var("PATH").context("read host PATH")?; Ok(format!("{stub_directory}:{host_path}")) diff --git a/tests/env_path_tests.rs b/tests/env_path_tests.rs index 1bacc8f8d..157e4d8f2 100644 --- a/tests/env_path_tests.rs +++ b/tests/env_path_tests.rs @@ -10,6 +10,10 @@ use test_support::env::{VarGuard, mocked_path_env, prepend_dir_to_path, system_e #[rstest] #[serial] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #493 (integration-binary migration)" +)] fn prepend_dir_to_path_sets_and_restores() -> Result<()> { let env = mocked_path_env(); let original = env.raw("PATH").context("mock PATH should be set")?; @@ -37,6 +41,10 @@ fn prepend_dir_to_path_sets_and_restores() -> Result<()> { #[rstest] #[serial] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #493 (integration-binary migration)" +)] fn prepend_dir_to_path_handles_empty_path() -> Result<()> { let _path_guard = VarGuard::set("PATH", OsStr::new("")); let env = system_env(); @@ -61,6 +69,10 @@ fn prepend_dir_to_path_handles_empty_path() -> Result<()> { #[rstest] #[serial] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #493 (integration-binary migration)" +)] fn prepend_dir_to_path_handles_missing_path() -> Result<()> { let _path_guard = VarGuard::unset("PATH"); let env = system_env(); diff --git a/tests/env_restore_tests.rs b/tests/env_restore_tests.rs index 36b515fa5..da76f393e 100644 --- a/tests/env_restore_tests.rs +++ b/tests/env_restore_tests.rs @@ -16,6 +16,10 @@ fn test_var(suffix: &str) -> String { } #[rstest] +#[expect( + clippy::disallowed_methods, + reason = "covers the guards being retired; deleted under #493 (integration-binary migration)" +)] fn restore_many_restores_previously_set_variable() -> Result<()> { let key = test_var("SET"); let _previous = set_var(&key, std::ffi::OsStr::new("original")); @@ -42,6 +46,10 @@ fn restore_many_restores_previously_set_variable() -> Result<()> { } #[rstest] +#[expect( + clippy::disallowed_methods, + reason = "covers the guards being retired; deleted under #493 (integration-binary migration)" +)] fn restore_many_removes_variable_when_prior_value_is_none() -> Result<()> { let key = test_var("NONE"); let _ = set_var(&key, std::ffi::OsStr::new("transient")); @@ -65,6 +73,10 @@ fn restore_many_handles_empty_map() { } #[rstest] +#[expect( + clippy::disallowed_methods, + reason = "covers the guards being retired; deleted under #493 (integration-binary migration)" +)] fn restore_many_restores_multiple_variables() -> Result<()> { let key_a = test_var("MULTI_A"); let key_b = test_var("MULTI_B"); @@ -96,6 +108,10 @@ fn restore_many_restores_multiple_variables() -> Result<()> { } #[rstest] +#[expect( + clippy::disallowed_methods, + reason = "covers the guards being retired; deleted under #493 (integration-binary migration)" +)] fn restore_many_mixed_set_and_remove() -> Result<()> { let key_set = test_var("MIX_SET"); let key_remove = test_var("MIX_REMOVE"); diff --git a/tests/kani_cfg_ui_tests.rs b/tests/kani_cfg_ui_tests.rs index 0f4ada031..69705f062 100644 --- a/tests/kani_cfg_ui_tests.rs +++ b/tests/kani_cfg_ui_tests.rs @@ -98,6 +98,10 @@ fn compile_ui_fixture(source: &str, output_path: &Path) -> io::Result { .output() } +#[expect( + clippy::disallowed_methods, + reason = "locating build artefacts Cargo reports through the environment; there is no seam to inject and no process state to isolate" +)] fn rustc() -> PathBuf { std::env::var_os("RUSTC").map_or_else(|| Path::new("rustc").to_path_buf(), PathBuf::from) } diff --git a/tests/ninja_env_tests.rs b/tests/ninja_env_tests.rs index f2eeab9f2..cb4710df9 100644 --- a/tests/ninja_env_tests.rs +++ b/tests/ninja_env_tests.rs @@ -15,6 +15,10 @@ fn ninja_tmp() -> PathBuf { #[rstest] #[serial] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #493 (integration-binary migration)" +)] fn override_ninja_env_sets_and_restores(ninja_tmp: PathBuf) -> Result<()> { let before = std::env::var_os(NINJA_ENV); let original = before @@ -43,6 +47,10 @@ fn override_ninja_env_sets_and_restores(ninja_tmp: PathBuf) -> Result<()> { #[rstest] #[serial] +#[expect( + clippy::disallowed_methods, + reason = "pending migration under #493 (integration-binary migration)" +)] fn override_ninja_env_unset_removes_variable(ninja_tmp: PathBuf) -> Result<()> { let before = std::env::var_os(NINJA_ENV); let mut env = MockEnv::new(); diff --git a/tests/packaging_smoke_tests.rs b/tests/packaging_smoke_tests.rs index 92f482fd6..e945e106b 100644 --- a/tests/packaging_smoke_tests.rs +++ b/tests/packaging_smoke_tests.rs @@ -18,6 +18,10 @@ const REQUIRED_PACKAGED_FILES: [&str; 5] = [ ]; #[test] +#[expect( + clippy::disallowed_methods, + reason = "locating build artefacts Cargo reports through the environment; there is no seam to inject and no process state to isolate" +)] fn packaged_manifest_retains_build_script_sources() { let cargo_binary = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); let publish_output = Command::new(&cargo_binary) diff --git a/tests/release_help/mod.rs b/tests/release_help/mod.rs index 51acfdaa4..5d33b19cc 100644 --- a/tests/release_help/mod.rs +++ b/tests/release_help/mod.rs @@ -125,6 +125,10 @@ esac "# } +#[expect( + clippy::disallowed_methods, + reason = "locating build artefacts Cargo reports through the environment; there is no seam to inject and no process state to isolate" +)] pub fn path_with_fake_cargo_orthohelp(fixture: &ScriptFixture) -> Result { let existing_path = std::env::var_os("PATH").unwrap_or_default(); let mut entries = vec![fixture.fake_bin_dir.clone()]; diff --git a/tests/std_filter_tests/command_filters/windows_filter_tests.rs b/tests/std_filter_tests/command_filters/windows_filter_tests.rs index 0387c81bb..1808d6fe9 100644 --- a/tests/std_filter_tests/command_filters/windows_filter_tests.rs +++ b/tests/std_filter_tests/command_filters/windows_filter_tests.rs @@ -81,6 +81,10 @@ impl WindowsSetupContext { } } +#[expect( + clippy::disallowed_methods, + reason = "the test must read the inherited Windows PATH so it can prepend its temporary helper directory: the code under test resolves commands through the real process environment, so an injected value would not be consulted; pending migration under #493 (integration-binary migration)" +)] fn windows_command_setup( ctx: WindowsSetupContext, helper_name: &str,