-
Notifications
You must be signed in to change notification settings - Fork 0
Inject environment into Cucumber world for isolated tests #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Decide whether an absent PATH should fail the test Using unwrap_or_default hides environments without PATH and may weaken the assertion. If the repo assumes PATH is always set under CI, prefer: let original = std::env::var("PATH").expect("PATH must be set for this test");🤖 Prompt for AI Agents |
||
| { | ||
| 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"); | ||
| } | ||
|
leynos marked this conversation as resolved.
|
||
| } | ||
| assert_eq!( | ||
| std::env::var("PATH").expect("read PATH after drop"), | ||
| original | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
| }); | ||
|
Comment on lines
16
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid lossy conversions when handling PATH Stop converting OsString to UTF-8 via to_string_lossy when manipulating PATH. The change above to join_paths keeps raw OS encodings intact, which is crucial on non-UTF-8 systems. 🤖 Prompt for AI Agents |
||
|
|
||
| let new_path = format!("{}:{}", dir.path().display(), original.to_string_lossy()); | ||
| // SAFETY: nightly marks `set_var` as unsafe; override path for test isolation. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<netsuke::cli::Cli>, | ||||||||||||||||||||||||||
| pub cli_error: Option<String>, | ||||||||||||||||||||||||||
| pub manifest: Option<netsuke::ast::NetsukeManifest>, | ||||||||||||||||||||||||||
| pub manifest_error: Option<String>, | ||||||||||||||||||||||||||
| pub build_graph: Option<netsuke::ir::BuildGraph>, | ||||||||||||||||||||||||||
| /// Generated Ninja file content. | ||||||||||||||||||||||||||
| pub ninja: Option<String>, | ||||||||||||||||||||||||||
| /// Status of the last process execution (true for success, false for | ||||||||||||||||||||||||||
| /// failure). | ||||||||||||||||||||||||||
| pub run_status: Option<bool>, | ||||||||||||||||||||||||||
| /// Error message from the last failed process execution. | ||||||||||||||||||||||||||
| pub run_error: Option<String>, | ||||||||||||||||||||||||||
| /// Temporary directory handle for test isolation. | ||||||||||||||||||||||||||
| pub temp: Option<tempfile::TempDir>, | ||||||||||||||||||||||||||
| /// Mockable environment access. | ||||||||||||||||||||||||||
| pub env: Box<dyn Env>, | ||||||||||||||||||||||||||
| /// Original `PATH` value restored after each scenario. | ||||||||||||||||||||||||||
| pub original_path: Option<std::ffi::OsString>, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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", &"<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) } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+69
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRemove unnecessary unsafe and correct the misleading comment std::env::set_var is safe on stable; the unsafe block triggers the unused_unsafe lint and violates the “avoid unsafe unless necessary” guideline. Remove the unsafe and fix the comment. Apply this diff: - 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) }
- }
+ if let Some(path) = self.original_path.take() {
+ // Restore the original PATH for this scenario to avoid cross-scenario leakage.
+ std::env::set_var("PATH", path);
+ }Verify the claim about std::env::set_var being unsafe and find any other occurrences of this pattern: 🏁 Script executed: #!/bin/bash
# 1) Confirm whether set_var is ever unsafe on stable/nightly in Rust docs
# 2) Locate all uses of set_var wrapped in `unsafe` to clean them up
# 1) Web-check is requested below.
# 2) Repo-wide search:
rg -n -S $'unsafe\\s*\\{\\s*std::env::set_var' -A 2Length of output: 680 🌐 Web query: 💡 Result: Short answer: Yes—std::env::set_var is marked unsafe in the Rust 2024 edition (including on nightly when using edition 2024), but it remains safe in earlier editions on stable and nightly. On Windows it is always sound to call, but the API is still unsafe in 2024 for cross-platform consistency and because the unsoundness exists on other OSes.[6][2] Key points
What this means for you
Citations:
MCP Context Analysis: 📊 Deepwiki:
🏁 Script executed: #!/bin/bash
# Determine the Rust edition for the main crate
rg -n 'edition' Cargo.tomlLength of output: 45 Retain unsafe and revise the SAFETY comment std::env::set_var is newly marked unsafe in the Rust 2024 edition. Keep the unsafe block and update the comment to justify safety in our single-threaded teardown: Locations:
Example diff for tests/world.rs: - 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) }
- }
+ if let Some(path) = self.original_path.take() {
+ // SAFETY: `set_var` is unsafe in Rust 2024 due to potential data races
+ // on global environment. This runs in single-threaded teardown,
+ // restoring PATH to avoid cross-scenario leakage.
+ unsafe { std::env::set_var("PATH", path) }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+67
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainSerialise environment mutation to avoid cross-test races Mutating process-wide environment variables is inherently racy under parallel test execution. Ensure Cucumber scenarios and any unit/integration tests that set PATH run serially, or guard set_var in Drop with a global test-only mutex to avoid interference. Would you like a small Env lock helper (tests-only) using a static Mutex or once_cell to guard PATH mutations? MCP Context Analysis: 📊 Deepwiki:
Run a quick search to inspect environment mutations and test definitions: 🏁 Script executed: #!/bin/bash
set -eo pipefail
echo "1. Locate the Drop impl for CliWorld:"
rg "impl Drop for CliWorld" -n .
echo "2. Find all PATH modifications via std::env::set_var:"
rg "std::env::set_var" -n .
echo "3. List all test functions in the tests/ directory:"
rg "#\\[test\\]" -n tests
echo "4. Check for Cucumber runner usage in tests:"
rg "cucumber" -n testsLength of output: 2445 Guard all PATH mutations under a global test mutex
Alternatively, annotate all tests that touch PATH with 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Serialise this test to avoid global PATH races
This test mutates the process-wide PATH and can race with others. Serialise it.
Either:
Add to dev-dependencies:
Or:
MCP Context Analysis:
📊 Deepwiki:
🏁 Script executed:
Length of output: 1167
🏁 Script executed:
Length of output: 910
Serialise tests that restore
PATHto prevent inter-test racesThe
drop_restores_pathtest invokesCliWorld’sDropimpl (tests/world.rs:67–75), which usesunsafe { std::env::set_var("PATH", …) }. That mutates the process-widePATHand races with parallel tests. Apply one of the following fixes:• Add
serial_testtodev-dependenciesand annotate:• Or configure CI to run environment-mutating tests single-threaded:
📝 Committable suggestion
🤖 Prompt for AI Agents