diff --git a/CLAUDE.md b/CLAUDE.md index 07236c5..6681c8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,12 @@ cargo test parser::tests:: # Run single test cargo test test_basic_api +# Format code (always run before committing) +cargo fmt + +# Check code formatting +cargo fmt --check + # Check code quality cargo clippy --all-targets -- -D warnings cargo check diff --git a/src/lib.rs b/src/lib.rs index 34887ac..123510e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,18 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { /// true /// } /// ``` +/// +/// ### With Custom Work Directory Root +/// ```no_run +/// use testscript_rs::testscript; +/// +/// // Use a custom location for test working directories +/// testscript::run("testdata") +/// .workdir_root("/tmp/my-app-tests") +/// .preserve_work_on_failure(true) // Combine features +/// .execute() +/// .unwrap(); +/// ``` pub struct Builder { dir: String, params: RunParams, @@ -229,6 +241,41 @@ impl Builder { self } + /// Set the root directory for test working directories + /// + /// When specified, test directories will be created inside this root directory + /// instead of the system default temporary directory. This is useful for: + /// - **Debugging**: Use a known location for test directories + /// - **Performance**: Use faster storage (e.g., tmpfs on Linux) + /// - **CI environments**: Use specific temp locations + /// - **Security**: Isolate test directories to specific paths + /// + /// # Arguments + /// * `root` - The directory path where test working directories should be created + /// + /// # Examples + /// ```no_run + /// use testscript_rs::testscript; + /// + /// // Use custom location for test directories + /// testscript::run("testdata") + /// .workdir_root("/tmp/my-app-tests") + /// .preserve_work_on_failure(true) // Combine with other features + /// .execute() + /// .unwrap(); + /// ``` + /// + /// This creates test directories like `/tmp/my-app-tests/testscript-abc123/`. + /// + /// # Notes + /// - The root directory must exist and be writable + /// - If not specified, uses the system default temporary directory + /// - Each test still gets its own isolated subdirectory + pub fn workdir_root>(mut self, root: P) -> Self { + self.params = self.params.workdir_root(root); + 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/environment.rs b/src/run/environment.rs index aa3141c..f8352f7 100644 --- a/src/run/environment.rs +++ b/src/run/environment.rs @@ -34,7 +34,37 @@ pub struct TestEnvironment { impl TestEnvironment { /// Create a new test environment with a temporary directory pub fn new() -> Result { - let temp_dir = tempfile::tempdir()?; + Self::new_with_root(None) + } + + /// Create a new test environment with a temporary directory in the specified root + pub fn new_with_root(workdir_root: Option<&std::path::Path>) -> Result { + let temp_dir = match workdir_root { + Some(root) => { + // Validate that the root directory exists and is writable + if !root.exists() { + return Err(Error::Generic(format!( + "Workdir root directory does not exist: {}", + root.display() + ))); + } + if !root.is_dir() { + return Err(Error::Generic(format!( + "Workdir root path is not a directory: {}", + root.display() + ))); + } + // Test if the directory is writable by creating a temp directory + tempfile::TempDir::new_in(root).map_err(|e| { + Error::Generic(format!( + "Cannot create temporary directory in workdir root {}: {}", + root.display(), + e + )) + })? + } + None => tempfile::tempdir()?, + }; let work_dir = temp_dir.path().to_path_buf(); Ok(TestEnvironment { diff --git a/src/run/execution.rs b/src/run/execution.rs index ef9864f..5ceac06 100644 --- a/src/run/execution.rs +++ b/src/run/execution.rs @@ -38,7 +38,7 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> { let script_file = script_path.to_string_lossy().to_string(); // Create test environment - let mut env = TestEnvironment::new()?; + let mut env = TestEnvironment::new_with_root(params.workdir_root.as_deref())?; // Set up files from the script env.setup_files(&script.files)?; diff --git a/src/run/params.rs b/src/run/params.rs index 0fa867c..06b2b9f 100644 --- a/src/run/params.rs +++ b/src/run/params.rs @@ -22,6 +22,8 @@ pub struct RunParams { pub update_scripts: bool, /// Whether to preserve working directories when tests fail pub preserve_work_on_failure: bool, + /// Optional root directory for test working directories + pub workdir_root: Option, } impl RunParams { @@ -55,6 +57,7 @@ impl RunParams { conditions, update_scripts, preserve_work_on_failure: false, + workdir_root: None, } } @@ -91,6 +94,15 @@ impl RunParams { self } + /// Set the root directory for test working directories + /// + /// When specified, test directories will be created inside this root directory + /// instead of the system default temporary directory. + pub fn workdir_root>(mut self, root: P) -> Self { + self.workdir_root = Some(root.into()); + self + } + /// Check if a program exists in PATH (cross-platform) pub fn program_exists(program: &str) -> bool { // TODO: Consider caching results for performance if needed diff --git a/tests/workdir_root.rs b/tests/workdir_root.rs new file mode 100644 index 0000000..164d958 --- /dev/null +++ b/tests/workdir_root.rs @@ -0,0 +1,160 @@ +//! Tests for workdir root configuration functionality + +use std::fs; +use tempfile::TempDir; +use testscript_rs::testscript; + +#[test] +fn test_workdir_root_basic_functionality() { + // Create a temporary directory to use as our custom root + let custom_root = TempDir::new().unwrap(); + let custom_root_path = custom_root.path(); + + // Create a test directory structure + let testdata_dir = TempDir::new().unwrap(); + let testdata_path = testdata_dir.path().join("testdata"); + fs::create_dir(&testdata_path).unwrap(); + + // Create a test that will fail due to mismatched stdout expectation + let test_content = r#"exec echo "hello" +stdout "goodbye" + +-- test_file.txt -- +Test content in custom root +"#; + + fs::write(testdata_path.join("custom_root_test.txt"), test_content).unwrap(); + + // Run test with custom workdir root and preserve work on failure + let result = testscript::run(testdata_path.to_string_lossy()) + .workdir_root(custom_root_path) + .preserve_work_on_failure(true) + .execute(); + + // The test should fail due to the mismatched stdout expectation + assert!( + result.is_err(), + "Test should fail due to mismatched stdout expectation. Got: {:?}", + result + ); + + // Verify that the workdir was created in our custom root and contains expected files + let entries: Vec<_> = fs::read_dir(custom_root_path) + .expect("Custom root should be readable") + .collect::, _>>() + .expect("Should be able to read directory entries"); + + assert!( + !entries.is_empty(), + "Custom root should contain test working directories" + ); + + // Find the test working directory (should start with a temp directory name) + let work_dir = entries + .iter() + .find(|entry| entry.file_type().unwrap().is_dir()) + .expect("Should find at least one working directory"); + + let work_dir_path = work_dir.path(); + + // Verify that the test file was created in the working directory + let test_file_path = work_dir_path.join("test_file.txt"); + assert!( + test_file_path.exists(), + "test_file.txt should exist in the working directory" + ); + + let test_file_content = + fs::read_to_string(&test_file_path).expect("Should be able to read test_file.txt"); + assert_eq!( + test_file_content.trim(), + "Test content in custom root", + "Test file should contain expected content" + ); +} + +#[test] +fn test_workdir_root_nonexistent_directory() { + let testdata_dir = TempDir::new().unwrap(); + let testdata_path = testdata_dir.path().join("testdata"); + fs::create_dir(&testdata_path).unwrap(); + + let test_content = r#"exec echo "test" +stdout "test" +"#; + + fs::write(testdata_path.join("simple_test.txt"), test_content).unwrap(); + + // Try to use a non-existent directory as workdir root + let nonexistent_path = "/this/path/does/not/exist"; + let result = testscript::run(testdata_path.to_string_lossy()) + .workdir_root(nonexistent_path) + .execute(); + + assert!( + result.is_err(), + "Expected error for non-existent workdir root" + ); + let error_msg = format!("{:?}", result.unwrap_err()); + assert!( + error_msg.contains("does not exist"), + "Error should mention directory doesn't exist" + ); +} + +#[test] +fn test_workdir_root_not_a_directory() { + let temp_dir = TempDir::new().unwrap(); + + // Create a file (not directory) to test the validation + let file_path = temp_dir.path().join("not_a_directory.txt"); + fs::write(&file_path, "this is a file").unwrap(); + + let testdata_dir = TempDir::new().unwrap(); + let testdata_path = testdata_dir.path().join("testdata"); + fs::create_dir(&testdata_path).unwrap(); + + let test_content = r#"exec echo "test" +stdout "test" +"#; + + fs::write(testdata_path.join("simple_test.txt"), test_content).unwrap(); + + // Try to use a file as workdir root + let result = testscript::run(testdata_path.to_string_lossy()) + .workdir_root(&file_path) + .execute(); + + assert!( + result.is_err(), + "Expected error for file used as workdir root" + ); + let error_msg = format!("{:?}", result.unwrap_err()); + assert!( + error_msg.contains("not a directory"), + "Error should mention path is not a directory" + ); +} + +#[test] +fn test_workdir_root_api_chaining() { + // Test that the API method exists and can be chained + let custom_root = TempDir::new().unwrap(); + let testdata_dir = TempDir::new().unwrap(); + let testdata_path = testdata_dir.path().join("testdata"); + fs::create_dir(&testdata_path).unwrap(); + + let test_content = r#"exec echo "chaining test" +stdout "chaining test" +"#; + + fs::write(testdata_path.join("chain_test.txt"), test_content).unwrap(); + + let result = testscript::run(testdata_path.to_string_lossy()) + .workdir_root(custom_root.path()) + .workdir_root(custom_root.path()) // Can be called multiple times + .preserve_work_on_failure(false) + .execute(); + + assert!(result.is_ok(), "API chaining test failed: {:?}", result); +}