1
0
Fork 0
mirror of https://github.com/imjasonh/testscript-rs synced 2026-07-18 14:46:25 +00:00

Allow setting environment variables in setup hook (#44)

## Plan: Support passing env vars into test environment in `setup`

- [x] Update `SetupFn` type alias in `params.rs` to accept `&mut
TestEnvironment` instead of `&TestEnvironment`
- [x] Update `setup` method signature in `params.rs` to accept `Fn(&mut
TestEnvironment)` instead of `Fn(&TestEnvironment)`
- [x] Update `setup` method signature in `lib.rs` Builder to accept
`Fn(&mut TestEnvironment)` instead of `Fn(&TestEnvironment)`
- [x] Update execution in `execution.rs` to pass `&mut env` to setup
function
- [x] Update existing test in `setup_hook.rs` to test the new
functionality (make it pass)
- [x] Add a new focused test to validate setting environment variables
in setup hook
- [x] Run tests to validate changes - all tests pass
- [x] Update documentation examples to show the new capability
- [x] Address code review feedback - fix trailing newline inconsistency
- [x] Add comprehensive test for environment variable presence/absence
to ensure proper isolation
- [x] Enhance tests to demonstrate env vars work without sh -c wrapper
(using printenv/set)
- [x] Format assert statement to single line per style guidelines

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>support passing env vars into the test environment in
`setup`</issue_title>
> <issue_description>```
> .setup(|env| => {
>   env.set("FOO", "bar")
> }
> ```
> 
> (or similar)
> 
> this should make `FOO` have the value of `bar` in the test
environment</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes imjasonh/testscript-rs#43

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/imjasonh/testscript-rs/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
Copilot 2025-12-29 21:29:03 -05:00 committed by GitHub
parent 57965e8c91
commit a69a295cbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 164 additions and 27 deletions

View file

@ -44,6 +44,9 @@ use testscript_rs::testscript;
fn test_my_cli() { fn test_my_cli() {
testscript::run("testdata") testscript::run("testdata")
.setup(|env| { .setup(|env| {
// Set environment variables for tests
env.set_env_var("MY_APP_CONFIG", "/path/to/config");
// Compile your CLI tool // Compile your CLI tool
std::process::Command::new("cargo") std::process::Command::new("cargo")
.args(["build", "--bin", "my-cli"]) .args(["build", "--bin", "my-cli"])

View file

@ -154,6 +154,10 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> {
/// ///
/// testscript::run("testdata") /// testscript::run("testdata")
/// .setup(|env| { /// .setup(|env| {
/// // Set environment variables for your CLI
/// env.set_env_var("MY_APP_MODE", "testing");
/// env.set_env_var("LOG_LEVEL", "debug");
///
/// // Compile your CLI tool /// // Compile your CLI tool
/// std::process::Command::new("cargo") /// std::process::Command::new("cargo")
/// .args(["build", "--bin", "my-cli"]) /// .args(["build", "--bin", "my-cli"])
@ -203,11 +207,30 @@ impl Builder {
/// Add a setup function that runs before each test script /// Add a setup function that runs before each test script
/// ///
/// The setup function receives a reference to the test environment and can /// The setup function receives a mutable reference to the test environment and can
/// perform actions like compiling binaries or setting up test data. /// perform actions like compiling binaries, setting up test data, or setting environment variables.
///
/// # Examples
/// ```no_run
/// use testscript_rs::testscript;
///
/// testscript::run("testdata")
/// .setup(|env| {
/// // Set an environment variable
/// env.set_env_var("FOO", "bar");
///
/// // Compile a binary
/// std::process::Command::new("cargo")
/// .args(["build", "--bin", "my-cli"])
/// .status()?;
/// Ok(())
/// })
/// .execute()
/// .unwrap();
/// ```
pub fn setup<F>(mut self, func: F) -> Self pub fn setup<F>(mut self, func: F) -> Self
where where
F: Fn(&TestEnvironment) -> Result<()> + 'static, F: Fn(&mut TestEnvironment) -> Result<()> + 'static,
{ {
self.params = self.params.setup(func); self.params = self.params.setup(func);
self self

View file

@ -49,7 +49,7 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
// Run setup hook if provided // Run setup hook if provided
if let Some(setup) = &params.setup { if let Some(setup) = &params.setup {
setup(&env)?; setup(&mut env)?;
} }
// Track script updates if we're in update mode // Track script updates if we're in update mode

View file

@ -8,7 +8,7 @@ use std::collections::HashMap;
pub type CommandFn = fn(&mut TestEnvironment, &[String]) -> Result<()>; pub type CommandFn = fn(&mut TestEnvironment, &[String]) -> Result<()>;
/// Type alias for a setup function /// Type alias for a setup function
pub type SetupFn = Box<dyn Fn(&TestEnvironment) -> Result<()>>; pub type SetupFn = Box<dyn Fn(&mut TestEnvironment) -> Result<()>>;
/// Configuration parameters for running tests /// Configuration parameters for running tests
pub struct RunParams { pub struct RunParams {
@ -73,7 +73,7 @@ impl RunParams {
/// Set a setup function to run before each script /// Set a setup function to run before each script
pub fn setup<F>(mut self, func: F) -> Self pub fn setup<F>(mut self, func: F) -> Self
where where
F: Fn(&TestEnvironment) -> Result<()> + 'static, F: Fn(&mut TestEnvironment) -> Result<()> + 'static,
{ {
self.setup = Some(Box::new(func)); self.setup = Some(Box::new(func));
self self

View file

@ -87,37 +87,66 @@ fn test_setup_hook_environment() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let script_path = temp_dir.path().join("env_test.txt"); let script_path = temp_dir.path().join("env_test.txt");
let script_content = r#"# Test setup hook can modify environment // Use sh -c to allow shell variable expansion
env TEST_FROM_SETUP=should_be_set let script_content = if cfg!(windows) {
exec echo "Value: $TEST_FROM_SETUP" r#"# Test setup hook can modify environment (Windows)
stdout "Value: hello_from_setup""#; exec cmd /c "echo Value: %TEST_FROM_SETUP%"
stdout "Value: hello_from_setup""#
} else {
r#"# Test setup hook can modify environment (Unix)
exec sh -c "echo Value: $TEST_FROM_SETUP"
stdout "Value: hello_from_setup"#
};
fs::write(&script_path, script_content).unwrap(); fs::write(&script_path, script_content).unwrap();
// Create RunParams with setup hook that sets an environment variable // Create RunParams with setup hook that sets an environment variable
let params = RunParams::new().setup(|env| { let params = RunParams::new().setup(|env| {
// The setup hook has access to the TestEnvironment but can't modify it // Now we can modify the environment directly in the setup hook
// This demonstrates the current API limitation - we can access but not modify env.set_env_var("TEST_FROM_SETUP", "hello_from_setup");
println!("Setup running in: {}", env.work_dir.display());
// We could write files that contain environment setup
let env_script = env.work_dir.join("setup_env.sh");
std::fs::write(&env_script, "export TEST_FROM_SETUP=hello_from_setup")?;
Ok(()) Ok(())
}); });
let mut params = params; let result = run_script(&script_path, &params);
// Manually set the environment variable for this test assert!(
params = params.condition("TEST_FROM_SETUP", true); result.is_ok(),
"Environment setup test should pass: {:?}",
result
);
}
#[test]
fn test_setup_hook_multiple_env_vars() {
let temp_dir = TempDir::new().unwrap();
let script_path = temp_dir.path().join("multi_env_test.txt");
// Use sh -c to allow shell variable expansion
let script_content = if cfg!(windows) {
r#"# Test setup hook can set multiple environment variables (Windows)
exec cmd /c "echo FOO=%FOO% BAR=%BAR% BAZ=%BAZ%"
stdout "FOO=value1 BAR=value2 BAZ=value3""#
} else {
r#"# Test setup hook can set multiple environment variables (Unix)
exec sh -c "echo FOO=$FOO BAR=$BAR BAZ=$BAZ"
stdout "FOO=value1 BAR=value2 BAZ=value3"#
};
fs::write(&script_path, script_content).unwrap();
// Create RunParams with setup hook that sets multiple environment variables
let params = RunParams::new().setup(|env| {
env.set_env_var("FOO", "value1");
env.set_env_var("BAR", "value2");
env.set_env_var("BAZ", "value3");
Ok(())
});
let result = run_script(&script_path, &params); let result = run_script(&script_path, &params);
// This test demonstrates the current limitation - setup can't modify the running environment assert!(
if result.is_err() { result.is_ok(),
println!( "Multiple environment variables test should pass: {:?}",
"Environment setup test failed (expected with current API): {:?}", result
result );
);
}
} }
#[test] #[test]
@ -144,3 +173,85 @@ exec echo "Should not execute""#;
.to_string() .to_string()
.contains("Setup deliberately failed")); .contains("Setup deliberately failed"));
} }
#[test]
fn test_setup_env_vars_presence_and_absence() {
let temp_dir = TempDir::new().unwrap();
let script_path = temp_dir.path().join("env_presence_test.txt");
// Test that verifies:
// 1. Variables set in setup ARE available in the test
// 2. Variables NOT set in setup are NOT available in the test
let script_content = if cfg!(windows) {
r#"# Test environment variable presence and absence (Windows)
# Check that SET_VAR is available using printenv (no shell needed)
exec cmd /c set SET_VAR
stdout "SET_VAR=value_from_setup"
# Also verify with shell expansion
exec cmd /c "echo SET_VAR=%SET_VAR%"
stdout "SET_VAR=value_from_setup"
# Check that UNSET_VAR is not available
! exec cmd /c set UNSET_VAR"#
} else {
r#"# Test environment variable presence and absence (Unix)
# Check that SET_VAR is available using printenv (no shell needed)
exec printenv SET_VAR
stdout "value_from_setup"
# Also verify with shell expansion
exec sh -c "echo SET_VAR=$SET_VAR"
stdout "SET_VAR=value_from_setup"
# Check that UNSET_VAR is not available
! exec printenv UNSET_VAR"#
};
fs::write(&script_path, script_content).unwrap();
// Create RunParams with setup hook that only sets SET_VAR, not UNSET_VAR
let params = RunParams::new().setup(|env| {
env.set_env_var("SET_VAR", "value_from_setup");
// Deliberately NOT setting UNSET_VAR to verify it's not available
Ok(())
});
let result = run_script(&script_path, &params);
assert!(
result.is_ok(),
"Environment presence/absence test should pass: {:?}",
result
);
}
#[test]
fn test_env_command_prints_all_vars() {
let temp_dir = TempDir::new().unwrap();
let script_path = temp_dir.path().join("env_print_test.txt");
// Test that 'env' without args prints all environment variables
let script_content = r#"# Test env command prints all variables
env VAR1=value1
env VAR2=value2
# The env command should be callable and set variables
# (printing is done to stdout which we can't easily test here,
# but we verify the vars are set by using them)
exec sh -c "echo $VAR1"
stdout "value1"
exec sh -c "echo $VAR2"
stdout "value2"
"#;
fs::write(&script_path, script_content).unwrap();
let params = RunParams::new().setup(|env| {
env.set_env_var("SETUP_VAR", "from_setup");
Ok(())
});
let result = run_script(&script_path, &params);
assert!(result.is_ok(), "Env command test should pass: {:?}", result);
}