You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rust 2024 edition marks std::env::set_var and std::env::remove_var as unsafe because they can cause undefined behavior in multi-threaded environments. During the 2024 edition migration (commit 73b78571), these calls were left in an inconsistent state:
177 env::set_var/env::remove_var calls across 17 test-bearing files
The test build currently fails with 119 E0133 errors on cargo check --lib --tests
7 files already have #[serial] applied to some (but not all) of their env-touching tests
serial_test = "3.4" already exists in [dev-dependencies]
This issue captures the plan to finish the migration via a safe EnvGuard RAII wrapper plus #[serial], removing all unsafe {} blocks from test code and retiring the custom once_cell::Mutex pattern in tests/backendai_env_test.rs.
Current State (verified 2026-04-14)
Build Status
$ cargo check --lib --tests
error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block
...
error: could not compile `bssh` (lib test) due to 123 previous errors
119 of those 123 errors are E0133 against std::env::set_var / remove_var; the remainder are cascading type errors in the same modules.
"partial" means the file imports serial_test::serial and applies #[serial] to some — but not all — env-touching test functions.
tests/fail_fast_test.rs already imports serial_test and uses #[serial], but contains zeroenv::set_var/remove_var calls and is not in scope for this issue.
Counts come from grep -rn 'env::set_var\|env::remove_var' and the error locations in cargo check --lib --tests output.
Solution: EnvGuard RAII + #[serial]
Encapsulate every unsafe environment variable mutation inside a single RAII type that restores the original value on drop, and serialize every test that touches environment variables with #[serial] from the serial_test crate.
Benefits
Zero unsafe in test bodies — all unsafety lives in one audited module with a documented SAFETY: comment.
Guaranteed cleanup — Drop restores state even on panic or early return.
Thread-safety via #[serial] — eliminates cross-test interference without hand-rolled mutexes.
Removes ENV_MUTEX in tests/backendai_env_test.rs (obsolete once #[serial] is applied everywhere).
Minimal, measurable perf cost — expected +10–15% test duration; no impact on release build or runtime.
Design
EnvGuard (src/test_helpers/env_guard.rs)
// Copyright 2025 Lablup Inc. and Jeongkyu Shin// SPDX-License-Identifier: Apache-2.0//! RAII wrapper for safe environment variable mutation in tests.//!//! All `unsafe` calls to `std::env::set_var` / `std::env::remove_var` are//! encapsulated here. Combined with `#[serial_test::serial]`, this gives tests//! a safe, cleanup-guaranteed way to manipulate process-wide environment//! variables without scattering `unsafe {}` blocks across the codebase.#![cfg(test)]use std::ffi::{OsStr,OsString};/// RAII guard that sets or removes an environment variable on construction/// and restores the previous value (or unset state) on drop.////// Tests that use `EnvGuard` **must** also be annotated with/// `#[serial_test::serial]` to prevent races with other tests that read or/// mutate the same variable.#[must_use = "EnvGuard must be bound to a local; dropping it immediately restores the variable"]pubstructEnvGuard{key:OsString,original:Option<OsString>,}implEnvGuard{/// Set an environment variable, saving its prior value for restoration.pubfnset(key:implInto<OsString>,value:implAsRef<OsStr>) -> Self{let key = key.into();let original = std::env::var_os(&key);// SAFETY: tests that construct `EnvGuard` are `#[serial]`, so no// other thread reads or writes the environment concurrently.unsafe{ std::env::set_var(&key, value);}Self{ key, original }}/// Remove an environment variable, saving its prior value for restoration.pubfnremove(key:implInto<OsString>) -> Self{let key = key.into();let original = std::env::var_os(&key);// SAFETY: same rationale as `set`.unsafe{ std::env::remove_var(&key);}Self{ key, original }}}implDropforEnvGuard{fndrop(&mutself){// SAFETY: same rationale as `set` / `remove`.unsafe{matchself.original.take(){Some(v) => std::env::set_var(&self.key, v),None => std::env::remove_var(&self.key),}}}}
Design choices
OsString / OsStr instead of String preserves non-UTF-8 values (e.g. PATH on Windows) correctly. Tests using HOME pass &str transparently via AsRef<OsStr>.
#![cfg(test)] at the module level guarantees the code never ships in a release binary.
#[must_use] flags the common mistake of let _ = EnvGuard::set(...), which would drop the guard immediately and restore the variable before the test body ran.
LIFO drop order when a test holds multiple guards matches the user's mental model:
let _home = EnvGuard::set("HOME","/tmp/test");let _user = EnvGuard::set("USER","testuser");// Drops in reverse: USER first, then HOME.
v1 deliberately has no set_many / remove_many helpers — multiple guards compose cleanly.
For integration tests under tests/ (which can't see pub(crate) items), use a thin shared module:
tests/common/mod.rs:
// Re-export via a `#[path]` include of the library's `env_guard.rs` so the// integration tests use the *same* implementation as unit tests.#[path = "../../src/test_helpers/env_guard.rs"]mod env_guard_impl;pubuse env_guard_impl::EnvGuard;
Each integration test file then adds mod common; and use common::EnvGuard;. The #[path] approach avoids publishing EnvGuard as a public API of the bssh crate. (Alternative: mark the module pub and re-export; we'll pick one during Phase 1 depending on which compiles more cleanly.)
Implementation Phases
Phase 1 — Foundation (blocks everything else)
Create src/test_helpers/env_guard.rs with EnvGuard + Drop.
Create src/test_helpers/mod.rs.
Add #[cfg(test)] pub(crate) mod test_helpers; to src/lib.rs.
Create tests/common/mod.rs exposing EnvGuard to integration tests (or finalize an alternative visibility path).
Verify serial_test = "3.4" is in [dev-dependencies] (already present ✅).
Exit criteria:cargo check --lib --tests 2>&1 | grep "test_helpers" reports no errors referencing the new module.
Phase 2 — Convert unit tests in src/(12 files, 119 − 30 = 89 compile errors cleared)
Fix src/ test modules in ascending order of complexity to catch pattern issues early:
tests/interactive_test.rs (3) — smallest, validates the tests/common/ wiring
tests/pdsh_compat_test.rs (5)
tests/jump_host_config_test.rs (8)
tests/exit_code_integration_test.rs (8)
tests/backendai_env_test.rs (23) — also delete the ENV_MUTEX / once_cell::Lazy<Mutex<()>> boilerplate at the top of the file and every let _guard = ENV_MUTEX.lock().await; call site
use serial_test::serial;usecrate::test_helpers::EnvGuard;#[test]#[serial]fntest_expand_tilde(){let _home = EnvGuard::set("HOME","/home/user");let expanded = expand_tilde(Path::new("~/.ssh/config"));assert_eq!(expanded,PathBuf::from("/home/user/.ssh/config"));// `_home` dropped here → HOME automatically restored.}
Multi-variable test (replaces the ENV_MUTEX pattern)
Not a concern for our test values (all ASCII); documented in the module header for future readers.
Existing ENV_MUTEX.lock().await in backendai_env_test.rs is async, #[serial] is sync
serial_test provides #[serial] that works with #[tokio::test] — no change needed. Verified on crate docs.
Performance Impact
Test execution time: +10–15% expected (driven by #[serial] enforcement, not by EnvGuard itself). Actual delta measured in the PR.
Release build: zero impact (#![cfg(test)] on the module).
Runtime performance: zero impact (never linked into the production binary).
Future Work
Once this lands, a follow-up issue should:
Eliminate env var dependence from unit tests by injecting configuration through function parameters or a Config::builder() pattern, so env-touching is reserved for tests that validate env var parsing itself.
Audit src/ssh/client/connection.rs — can the SSH agent detection tests inject a socket path parameter instead of mutating SSH_AUTH_SOCK?
Audit src/executor/rank_detector.rs — 17 env var calls suggest the rank detector could accept an injected env-like map, removing the need for process-global mutation in tests.
Each of these would reduce the #[serial] surface and let more tests run in parallel.
Overview
Rust 2024 edition marks
std::env::set_varandstd::env::remove_varasunsafebecause they can cause undefined behavior in multi-threaded environments. During the 2024 edition migration (commit73b78571), these calls were left in an inconsistent state:env::set_var/env::remove_varcalls across 17 test-bearing filesE0133errors oncargo check --lib --testsunsafe {}blocks (partial manual fix)#[serial]applied to some (but not all) of their env-touching testsserial_test = "3.4"already exists in[dev-dependencies]This issue captures the plan to finish the migration via a safe
EnvGuardRAII wrapper plus#[serial], removing allunsafe {}blocks from test code and retiring the customonce_cell::Mutexpattern intests/backendai_env_test.rs.Current State (verified 2026-04-14)
Build Status
119 of those 123 errors are
E0133againststd::env::set_var/remove_var; the remainder are cascading type errors in the same modules.File-by-file Inventory
unsafe {}blocks#[serial]already?E0133errorssrc/jump/chain/auth.rs(test module)src/executor/rank_detector.rs(test module)src/config/tests.rssrc/server/config/loader.rs(test module)src/ssh/client/connection.rs(test module)src/cli/mode_detection_tests.rssrc/jump/parser/tests.rstests/backendai_env_test.rsENV_MUTEX)tests/jump_host_config_test.rstests/exit_code_integration_test.rssrc/ssh/auth.rs(test module)src/security/sudo.rs(test module)tests/pdsh_compat_test.rstests/interactive_test.rssrc/commands/interactive_unit_test.rssrc/ssh/ssh_config/integration_tests/env_cache_integration_test.rssrc/cli/pdsh.rs(test module)Notes:
serial_test::serialand applies#[serial]to some — but not all — env-touching test functions.tests/fail_fast_test.rsalready importsserial_testand uses#[serial], but contains zeroenv::set_var/remove_varcalls and is not in scope for this issue.grep -rn 'env::set_var\|env::remove_var'and the error locations incargo check --lib --testsoutput.Solution:
EnvGuardRAII +#[serial]Encapsulate every
unsafeenvironment variable mutation inside a single RAII type that restores the original value on drop, and serialize every test that touches environment variables with#[serial]from theserial_testcrate.Benefits
unsafein test bodies — all unsafety lives in one audited module with a documentedSAFETY:comment.Droprestores state even on panic or earlyreturn.#[serial]— eliminates cross-test interference without hand-rolled mutexes.ENV_MUTEXintests/backendai_env_test.rs(obsolete once#[serial]is applied everywhere).Design
EnvGuard(src/test_helpers/env_guard.rs)Design choices
OsString/OsStrinstead ofStringpreserves non-UTF-8 values (e.g.PATHon Windows) correctly. Tests usingHOMEpass&strtransparently viaAsRef<OsStr>.#![cfg(test)]at the module level guarantees the code never ships in a release binary.#[must_use]flags the common mistake oflet _ = EnvGuard::set(...), which would drop the guard immediately and restore the variable before the test body ran.set_many/remove_manyhelpers — multiple guards compose cleanly.Module wiring
src/test_helpers/mod.rs:src/lib.rs(test-only):For integration tests under
tests/(which can't seepub(crate)items), use a thin shared module:tests/common/mod.rs:Each integration test file then adds
mod common;anduse common::EnvGuard;. The#[path]approach avoids publishingEnvGuardas a public API of thebsshcrate. (Alternative: mark the modulepuband re-export; we'll pick one during Phase 1 depending on which compiles more cleanly.)Implementation Phases
Phase 1 — Foundation (blocks everything else)
src/test_helpers/env_guard.rswithEnvGuard+Drop.src/test_helpers/mod.rs.#[cfg(test)] pub(crate) mod test_helpers;tosrc/lib.rs.tests/common/mod.rsexposingEnvGuardto integration tests (or finalize an alternative visibility path).serial_test = "3.4"is in[dev-dependencies](already present ✅).Exit criteria:
cargo check --lib --tests 2>&1 | grep "test_helpers"reports no errors referencing the new module.Phase 2 — Convert unit tests in
src/(12 files, 119 − 30 = 89 compile errors cleared)Fix
src/test modules in ascending order of complexity to catch pattern issues early:src/cli/pdsh.rs(1 call)src/commands/interactive_unit_test.rs(3)src/ssh/ssh_config/integration_tests/env_cache_integration_test.rs(2)src/security/sudo.rs(7)src/ssh/auth.rs(7)src/cli/mode_detection_tests.rs(13)src/jump/parser/tests.rs(13)src/config/tests.rs(17)src/server/config/loader.rs(15)src/ssh/client/connection.rs(15)src/executor/rank_detector.rs(17)src/jump/chain/auth.rs(20)After each file:
cargo check --lib --testsshould have one fewer batch of errors. Commit per file (or per logical pair) for reviewability.Exit criteria:
cargo check --lib --testsreports 0 errors fromsrc/.Phase 3 — Convert integration tests in
tests/(5 files, ~30 remaining errors cleared)tests/interactive_test.rs(3) — smallest, validates thetests/common/wiringtests/pdsh_compat_test.rs(5)tests/jump_host_config_test.rs(8)tests/exit_code_integration_test.rs(8)tests/backendai_env_test.rs(23) — also delete theENV_MUTEX/once_cell::Lazy<Mutex<()>>boilerplate at the top of the file and everylet _guard = ENV_MUTEX.lock().await;call siteExit criteria:
cargo test --lib --bins --tests --no-runcompiles; nounsafe { env::set_var }/unsafe { env::remove_var }remain outsidesrc/test_helpers/env_guard.rs.Phase 4 — Validation
cargo build --all-targets— clean.cargo clippy --all-targets --all-features -- -D warnings— clean.cargo test --lib --bins --tests— all green.grep -rn "unsafe.*env::set_var\|unsafe.*env::remove_var" src/ tests/→ 0 matches outsidesrc/test_helpers/env_guard.rs.grep -rn "ENV_MUTEX\|once_cell::sync::Lazy" tests/→ 0 matches in converted files.Conversion Pattern Reference
Before (current broken state —
src/config/tests.rs)After
Multi-variable test (replaces the
ENV_MUTEXpattern)No more
ENV_MUTEX.lock(), no more manualorig_hostssave/restore, no moreunsafe {}.Completion Checklist
Phase 1 — Foundation
src/test_helpers/env_guard.rscreated withEnvGuard+Dropsrc/test_helpers/mod.rscreated, exportsEnvGuardsrc/lib.rsgains#[cfg(test)] pub(crate) mod test_helpers;tests/common/mod.rscreated (or alternative visibility path chosen)Phase 2 —
src/test modules (119 of 119 E0133 errors must be resolved across both phases)src/cli/pdsh.rs— converted,#[serial]appliedsrc/commands/interactive_unit_test.rs— converted,#[serial]appliedsrc/ssh/ssh_config/integration_tests/env_cache_integration_test.rs— converted,#[serial]appliedsrc/security/sudo.rs— converted, existing#[serial]preservedsrc/ssh/auth.rs— converted,#[serial]appliedsrc/cli/mode_detection_tests.rs— converted, existing#[serial]preserved + extendedsrc/jump/parser/tests.rs— converted,#[serial]appliedsrc/config/tests.rs— converted,#[serial]appliedsrc/server/config/loader.rs— converted,#[serial]appliedsrc/ssh/client/connection.rs— converted, existing#[serial]preserved + extendedsrc/executor/rank_detector.rs— converted, existing#[serial]preserved + extendedsrc/jump/chain/auth.rs— converted,#[serial]appliedPhase 3 —
tests/integration teststests/interactive_test.rs— converted,#[serial]appliedtests/pdsh_compat_test.rs— converted, existing#[serial]preservedtests/jump_host_config_test.rs— converted,#[serial]appliedtests/exit_code_integration_test.rs— converted, existing#[serial]preservedtests/backendai_env_test.rs— converted;ENV_MUTEX+once_cell::sync::LazydeletedPhase 4 — Validation
cargo build --all-targets— cleancargo clippy --all-targets --all-features -- -D warnings— cleancargo test --lib --bins --tests— all greengrep -rn "unsafe.*env::set_var\|unsafe.*env::remove_var" src/ tests/= 0 matches outsideenv_guard.rsgrep -rn "ENV_MUTEX" tests/= 0 matchesRisks & Mitigations
#[serial]serializes all env-touching tests → CI slowdown#[serial(key)]by variable family. Measure in PR.EnvGuardbut forgets#[serial]→ race with other testsEnvGuardmust also importserial_test::serial.test_helpers#[path]include intests/common/mod.rs, so unit and integration tests share one implementation file without makingEnvGuardpublic API.var_os+set_varcycle loses OS-level encoding fidelityENV_MUTEX.lock().awaitinbackendai_env_test.rsis async,#[serial]is syncserial_testprovides#[serial]that works with#[tokio::test]— no change needed. Verified on crate docs.Performance Impact
#[serial]enforcement, not byEnvGuarditself). Actual delta measured in the PR.#![cfg(test)]on the module).Future Work
Once this lands, a follow-up issue should:
Config::builder()pattern, so env-touching is reserved for tests that validate env var parsing itself.src/ssh/client/connection.rs— can the SSH agent detection tests inject a socket path parameter instead of mutatingSSH_AUTH_SOCK?src/executor/rank_detector.rs— 17 env var calls suggest the rank detector could accept an injected env-like map, removing the need for process-global mutation in tests.Each of these would reduce the
#[serial]surface and let more tests run in parallel.References
std::env::set_varunsafe soundness issue: Consider deprecating and/or modifying behavior of std::env::set_var rust-lang/rust#90308serial_testcrate: https://crates.io/crates/serial_test73b78571(2024 edition migration),65fca4d4(clippy guard-clause cleanup)