From 26a39e238438d4bb465816b22ff9aaeec1c44c0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Sep 2025 01:56:58 +0000 Subject: [PATCH] Implement network and advanced condition support Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com> --- examples/advanced_conditions.rs | 55 +++++++++++++++ src/lib.rs | 53 +++++++++++++- src/run/execution.rs | 6 ++ src/run/params.rs | 57 ++++++++++++++- testdata/env_conditions.txt | 19 +++++ testdata/network_conditions.txt | 11 +++ testdata/program_conditions.txt | 16 +++++ tests/advanced_conditions.rs | 121 ++++++++++++++++++++++++++++++++ 8 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 examples/advanced_conditions.rs create mode 100644 testdata/env_conditions.txt create mode 100644 testdata/network_conditions.txt create mode 100644 testdata/program_conditions.txt create mode 100644 tests/advanced_conditions.rs diff --git a/examples/advanced_conditions.rs b/examples/advanced_conditions.rs new file mode 100644 index 0000000..c147fbe --- /dev/null +++ b/examples/advanced_conditions.rs @@ -0,0 +1,55 @@ +//! Example demonstrating network and advanced condition support + +use testscript_rs::testscript; + +fn main() -> Result<(), Box> { + println!("Running testscript with advanced condition support..."); + + // Example 1: Basic usage with auto-detection + println!("\n=== Example 1: Auto-detect network and programs ==="); + testscript::run("testdata") + .auto_detect_network() + .auto_detect_programs(&["docker", "git", "npm", "echo"]) + .execute()?; + + // Example 2: Manual condition setting with environment variables + println!("\n=== Example 2: Environment-based conditions ==="); + + // Set a test environment variable + std::env::set_var("CI", "true"); + + testscript::run("testdata") + .condition("net", check_network_available()) + .condition("docker", command_exists("docker")) + .execute()?; + + println!("All tests completed successfully!"); + Ok(()) +} + +/// Check if network is available +fn check_network_available() -> bool { + std::process::Command::new("ping") + .args(if cfg!(windows) { + vec!["-n", "1", "-w", "1000", "1.1.1.1"] + } else { + vec!["-c", "1", "-W", "1", "1.1.1.1"] + }) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} + +/// Check if a command exists +fn command_exists(program: &str) -> bool { + #[cfg(windows)] + let check_cmd = "where"; + #[cfg(not(windows))] + let check_cmd = "which"; + + std::process::Command::new(check_cmd) + .arg(program) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 9173d75..1a3f526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { /// // Simple usage /// testscript::run("testdata").execute().unwrap(); /// -/// // With customization +/// // With customization and advanced conditions /// testscript::run("testdata") /// .setup(|env| { /// // Compile your CLI tool @@ -93,8 +93,28 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { /// // Custom command implementation /// Ok(()) /// }) +/// .condition("net", check_network_available()) +/// .condition("docker", command_exists("docker")) +/// .condition("env:CI", std::env::var("CI").is_ok()) /// .execute() /// .unwrap(); +/// +/// // Auto-detect network and programs +/// testscript::run("testdata") +/// .auto_detect_network() +/// .auto_detect_programs(&["docker", "git", "npm"]) +/// .execute() +/// .unwrap(); +/// +/// fn check_network_available() -> bool { +/// // Your network check implementation +/// true +/// } +/// +/// fn command_exists(cmd: &str) -> bool { +/// // Your command existence check +/// false +/// } /// ``` pub struct Builder { dir: String, @@ -149,6 +169,37 @@ impl Builder { self } + /// Automatically detect network availability and set the 'net' condition + /// + /// This will attempt to ping reliable hosts to determine if network + /// connectivity is available and set the condition accordingly. + pub fn auto_detect_network(mut self) -> Self { + self.params = self.params.auto_detect_network(); + self + } + + /// Automatically detect availability of specified programs + /// + /// This will check if the given programs are available in PATH and + /// set conditions like `exec:program` for each one. + /// + /// # Arguments + /// * `programs` - Slice of program names to check for + /// + /// # Examples + /// ```no_run + /// use testscript_rs::testscript; + /// + /// testscript::run("testdata") + /// .auto_detect_programs(&["docker", "git", "npm"]) + /// .execute() + /// .unwrap(); + /// ``` + pub fn auto_detect_programs(mut self, programs: &[&str]) -> Self { + self.params = self.params.auto_detect_programs(programs); + self + } + /// Execute all test scripts in the configured directory /// /// This will discover all `.txt` files in the directory and run them as test scripts. diff --git a/src/run/execution.rs b/src/run/execution.rs index ad35fee..db1c8ea 100644 --- a/src/run/execution.rs +++ b/src/run/execution.rs @@ -196,10 +196,16 @@ fn execute_command_inner( if let Some(ref condition) = command.condition { let condition_met = if let Some(value) = params.conditions.get(condition) { *value + } else if condition.starts_with("env:") { + // Dynamic environment variable condition + RunParams::check_env_condition(condition) } else if let Some(base_condition) = condition.strip_prefix('!') { // Handle negated conditions if let Some(value) = params.conditions.get(base_condition) { !value + } else if base_condition.starts_with("env:") { + // Dynamic negated environment variable condition + !RunParams::check_env_condition(base_condition) } else { return Err(Error::UnknownCondition { condition: base_condition.to_string(), diff --git a/src/run/params.rs b/src/run/params.rs index 9ca28f2..0c09f39 100644 --- a/src/run/params.rs +++ b/src/run/params.rs @@ -86,14 +86,67 @@ impl RunParams { self } - /// Check if a program exists in PATH + /// Automatically detect network availability and set the 'net' condition + pub fn auto_detect_network(mut self) -> Self { + self.conditions.insert("net".to_string(), Self::check_network_available()); + self + } + + /// Automatically detect availability of specified programs + pub fn auto_detect_programs(mut self, programs: &[&str]) -> Self { + for program in programs { + let condition_name = format!("exec:{}", program); + self.conditions.insert(condition_name, Self::program_exists(program)); + } + self + } + + /// Check if a program exists in PATH (cross-platform) fn program_exists(program: &str) -> bool { - std::process::Command::new("which") + // Use different commands based on platform + #[cfg(windows)] + let check_cmd = "where"; + #[cfg(not(windows))] + let check_cmd = "which"; + + std::process::Command::new(check_cmd) .arg(program) .output() .map(|output| output.status.success()) .unwrap_or(false) } + + /// Check if network is available by attempting to reach a reliable host + fn check_network_available() -> bool { + // Try multiple reliable hosts to increase reliability + let test_hosts = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]; + + for host in &test_hosts { + let output = std::process::Command::new("ping") + .args(if cfg!(windows) { + vec!["-n", "1", "-w", "1000", host] + } else { + vec!["-c", "1", "-W", "1", host] + }) + .output(); + + if let Ok(result) = output { + if result.status.success() { + return true; + } + } + } + false + } + + /// Check environment variable condition + pub fn check_env_condition(condition: &str) -> bool { + if let Some(env_var) = condition.strip_prefix("env:") { + std::env::var(env_var).is_ok() + } else { + false + } + } } impl Default for RunParams { diff --git a/testdata/env_conditions.txt b/testdata/env_conditions.txt new file mode 100644 index 0000000..a71d1e7 --- /dev/null +++ b/testdata/env_conditions.txt @@ -0,0 +1,19 @@ +# Test environment variable conditions + +# Set an env var in the test +env TEST_VAR=hello + +# This should execute since TEST_VAR is now set +[env:TEST_VAR] exec echo "Environment variable TEST_VAR is set" +[env:TEST_VAR] stdout "Environment variable TEST_VAR is set" + +# This should be skipped since MISSING_VAR is not set +[env:MISSING_VAR] exec echo "This should not appear" + +# This should execute since MISSING_VAR is not set (negated condition) +[!env:MISSING_VAR] exec echo "MISSING_VAR is not set (as expected)" +[!env:MISSING_VAR] stdout "MISSING_VAR is not set (as expected)" + +# Test PATH environment variable which should exist on all systems +[env:PATH] exec echo "PATH environment variable exists" +[env:PATH] stdout "PATH environment variable exists" \ No newline at end of file diff --git a/testdata/network_conditions.txt b/testdata/network_conditions.txt new file mode 100644 index 0000000..ae548cf --- /dev/null +++ b/testdata/network_conditions.txt @@ -0,0 +1,11 @@ +# Test network condition support + +# This command will only run if network is available +[net] exec echo "Network is available" + +# This command will only run if network is not available +[!net] exec echo "Network is not available" + +# At least one of the above should execute, so we should see some output +exec echo "Network test completed" +stdout "Network test completed" \ No newline at end of file diff --git a/testdata/program_conditions.txt b/testdata/program_conditions.txt new file mode 100644 index 0000000..33250ee --- /dev/null +++ b/testdata/program_conditions.txt @@ -0,0 +1,16 @@ +# Test program detection conditions + +# Test that echo is available (should work on all platforms) +[exec:echo] exec echo "echo command is available" +[exec:echo] stdout "echo command is available" + +# Test negation for a program that shouldn't exist +[!exec:nonexistent_program_xyz123] exec echo "nonexistent program not found" +[!exec:nonexistent_program_xyz123] stdout "nonexistent program not found" + +# Test built-in program detections that are set by default +[exec:ls] exec echo "ls command detected" +[exec:mkdir] exec echo "mkdir command detected" + +# At least mkdir should work since it's tested +[exec:mkdir] stdout "mkdir command detected" \ No newline at end of file diff --git a/tests/advanced_conditions.rs b/tests/advanced_conditions.rs new file mode 100644 index 0000000..8cba63f --- /dev/null +++ b/tests/advanced_conditions.rs @@ -0,0 +1,121 @@ +//! Tests for network and advanced condition support + +use std::fs; +use tempfile::TempDir; +use testscript_rs::{run::RunParams, testscript}; + +#[test] +fn test_env_conditions() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Set an environment variable for testing + std::env::set_var("TEST_CONDITION", "true"); + + let test_content = r#"[env:TEST_CONDITION] exec echo "env condition works" +stdout "env condition works" + +[!env:NONEXISTENT_VAR] exec echo "negated env condition works" +stdout "negated env condition works" + +[env:NONEXISTENT_VAR] exec echo "should be skipped" +! stdout "should be skipped" +"#; + + fs::write(testdata_dir.join("env_test.txt"), test_content).unwrap(); + + let result = testscript::run(testdata_dir.to_string_lossy()).execute(); + assert!(result.is_ok(), "Environment condition test failed: {:?}", result); + + // Clean up + std::env::remove_var("TEST_CONDITION"); +} + +#[test] +fn test_auto_detect_network() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Create a test that should work whether network is available or not + let test_content = r#"[net] exec echo "network available" +[!net] exec echo "network not available" + +# At least one should execute +exec echo "test completed" +stdout "test completed" +"#; + + fs::write(testdata_dir.join("network_test.txt"), test_content).unwrap(); + + let result = testscript::run(testdata_dir.to_string_lossy()) + .auto_detect_network() + .execute(); + assert!(result.is_ok(), "Network condition test failed: {:?}", result); +} + +#[test] +fn test_auto_detect_programs() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Test with echo which should be available on all platforms + let test_content = r#"[exec:echo] exec echo "echo is available" +stdout "echo is available" + +[!exec:nonexistent_program_xyz] exec echo "nonexistent program not found" +stdout "nonexistent program not found" +"#; + + fs::write(testdata_dir.join("program_test.txt"), test_content).unwrap(); + + let result = testscript::run(testdata_dir.to_string_lossy()) + .auto_detect_programs(&["echo", "nonexistent_program_xyz"]) + .execute(); + assert!(result.is_ok(), "Program detection test failed: {:?}", result); +} + +#[test] +fn test_runparams_condition_helpers() { + // Test the helper functions directly + std::env::set_var("HELPER_TEST", "value"); + + assert!(RunParams::check_env_condition("env:HELPER_TEST")); + assert!(!RunParams::check_env_condition("env:NONEXISTENT")); + assert!(!RunParams::check_env_condition("not_env_condition")); + + std::env::remove_var("HELPER_TEST"); +} + +#[test] +fn test_combined_conditions() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Set environment for testing + std::env::set_var("COMBINED_TEST", "true"); + + let test_content = r#"# Test combining different condition types +# Use separate lines since parser doesn't support multiple conditions per line +[unix] exec echo "unix detected" +[windows] exec echo "windows detected" + +# This should work on any platform with the env var +[env:COMBINED_TEST] exec echo "env var is set" +stdout "env var is set" +"#; + + fs::write(testdata_dir.join("combined_test.txt"), test_content).unwrap(); + + let result = testscript::run(testdata_dir.to_string_lossy()) + .auto_detect_network() + .auto_detect_programs(&["echo"]) + .execute(); + assert!(result.is_ok(), "Combined condition test failed: {:?}", result); + + // Clean up + std::env::remove_var("COMBINED_TEST"); +} \ No newline at end of file