diff --git a/src/lib.rs b/src/lib.rs index 123510e..62c0a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,28 +21,68 @@ pub use run::run_test; fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { use walkdir::WalkDir; - // Simple glob pattern matching - for now just handle basic patterns like "testdata/*.txt" - let (base_dir, pattern) = if let Some(slash_pos) = test_data_glob.rfind('/') { - let base_dir = &test_data_glob[..slash_pos]; - let pattern = &test_data_glob[slash_pos + 1..]; - (base_dir, pattern) - } else { - (".", test_data_glob) - }; - - // Convert glob pattern to a simple matcher - let pattern_regex = pattern.replace("*", ".*"); - let regex = regex::Regex::new(&format!("^{}$", pattern_regex))?; - let mut test_files = Vec::new(); - // Walk the directory and collect matching files - for entry in WalkDir::new(base_dir).min_depth(1).max_depth(1) { - let entry = entry?; - if entry.file_type().is_file() { - if let Some(file_name) = entry.file_name().to_str() { - if regex.is_match(file_name) { - test_files.push(entry.path().to_path_buf()); + // If specific files are provided, use them directly + if let Some(ref files) = params.files { + // Parse the base directory from the glob pattern + let base_dir = if let Some(slash_pos) = test_data_glob.rfind('/') { + &test_data_glob[..slash_pos] + } else { + "." + }; + + for file in files { + let file_path = if file.starts_with('/') { + // Absolute path - use as-is + std::path::PathBuf::from(file) + } else if file.contains('/') { + // Relative path - resolve relative to base directory + std::path::PathBuf::from(base_dir).join(file) + } else { + // Just a filename - look in the base directory + std::path::PathBuf::from(base_dir).join(file) + }; + + // Validate that the file exists + if !file_path.exists() { + return Err(Error::Generic(format!( + "Test file not found: {}", + file_path.display() + ))); + } + + if !file_path.is_file() { + return Err(Error::Generic(format!( + "Path is not a file: {}", + file_path.display() + ))); + } + + test_files.push(file_path); + } + } else { + // Use the original glob-based discovery + let (base_dir, pattern) = if let Some(slash_pos) = test_data_glob.rfind('/') { + let base_dir = &test_data_glob[..slash_pos]; + let pattern = &test_data_glob[slash_pos + 1..]; + (base_dir, pattern) + } else { + (".", test_data_glob) + }; + + // Convert glob pattern to a simple matcher + let pattern_regex = pattern.replace("*", ".*"); + let regex = regex::Regex::new(&format!("^{}$", pattern_regex))?; + + // Walk the directory and collect matching files + for entry in WalkDir::new(base_dir).min_depth(1).max_depth(1) { + let entry = entry?; + if entry.file_type().is_file() { + if let Some(file_name) = entry.file_name().to_str() { + if regex.is_match(file_name) { + test_files.push(entry.path().to_path_buf()); + } } } } @@ -52,10 +92,14 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { test_files.sort(); if test_files.is_empty() { - return Err(Error::Generic(format!( - "No test files found matching pattern: {}", - test_data_glob - ))); + if params.files.is_some() { + return Err(Error::Generic("No test files specified".to_string())); + } else { + return Err(Error::Generic(format!( + "No test files found matching pattern: {}", + test_data_glob + ))); + } } // Run each test file @@ -93,6 +137,17 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { /// testscript::run("testdata").execute().unwrap(); /// ``` /// +/// ### Running Specific Test Files +/// ```no_run +/// use testscript_rs::testscript; +/// +/// // Run only specific test files instead of all .txt files +/// testscript::run("testdata") +/// .files(["hello.txt", "exists.txt"]) +/// .execute() +/// .unwrap(); +/// ``` +/// /// ### With Custom Setup and Work Directory Preservation /// ```no_run /// use testscript_rs::testscript; @@ -276,6 +331,49 @@ impl Builder { self } + /// Run only specific test files instead of discovering all .txt files + /// + /// When specified, only these files will be executed instead of discovering + /// all .txt files in the directory. Files can be specified as: + /// - Relative paths (relative to the test directory): `"hello.txt"` + /// - Absolute paths: `"/path/to/test.txt"` + /// - Just filenames: `"test.txt"` (looked up in the test directory) + /// + /// # Arguments + /// * `files` - An iterator of file paths to run + /// + /// # Examples + /// ```no_run + /// use testscript_rs::testscript; + /// + /// // Run specific test files + /// testscript::run("testdata") + /// .files(["hello.txt", "exists.txt"]) + /// .execute() + /// .unwrap(); + /// + /// // Using Vec + /// let test_files = vec!["hello.txt".to_string(), "exists.txt".to_string()]; + /// testscript::run("testdata") + /// .files(test_files) + /// .execute() + /// .unwrap(); + /// ``` + /// + /// # Benefits + /// - **Selective testing**: Run only specific test scenarios + /// - **Faster development**: Skip unrelated tests during development + /// - **CI optimization**: Run subsets of tests in different jobs + /// - **Go compatibility**: Match Go testscript functionality + pub fn files(mut self, files: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.params = self.params.files(files); + self + } + /// Execute all test scripts in the configured directory /// /// This will discover all `.txt` files in the directory and run them as test scripts. @@ -300,6 +398,12 @@ impl Builder { /// /// // Run all tests in testdata directory /// testscript::run("testdata").execute().unwrap(); +/// +/// // Run only specific test files +/// testscript::run("testdata") +/// .files(["hello.txt", "exists.txt"]) +/// .execute() +/// .unwrap(); /// ``` pub mod testscript { use super::*; diff --git a/src/run/params.rs b/src/run/params.rs index 06b2b9f..2c3bdf0 100644 --- a/src/run/params.rs +++ b/src/run/params.rs @@ -24,6 +24,8 @@ pub struct RunParams { pub preserve_work_on_failure: bool, /// Optional root directory for test working directories pub workdir_root: Option, + /// Specific files to run (if None, discover all .txt files) + pub files: Option>, } impl RunParams { @@ -58,6 +60,7 @@ impl RunParams { update_scripts, preserve_work_on_failure: false, workdir_root: None, + files: None, } } @@ -103,6 +106,19 @@ impl RunParams { self } + /// Set specific files to run instead of discovering all .txt files + /// + /// When specified, only these files will be executed instead of discovering + /// all .txt files in the directory. + pub fn files(mut self, files: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.files = Some(files.into_iter().map(|s| s.into()).collect()); + 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/specific_files.rs b/tests/specific_files.rs new file mode 100644 index 0000000..ac52489 --- /dev/null +++ b/tests/specific_files.rs @@ -0,0 +1,264 @@ +//! Tests for running specific test files functionality + +use std::fs; +use tempfile::TempDir; +use testscript_rs::testscript; + +#[test] +fn test_files_basic_functionality() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Create multiple test files + let hello_content = r#"exec echo "Hello" +stdout "Hello" +"#; + + let exists_content = r#"exists test_file.txt + +-- test_file.txt -- +content"#; + + let skip_content = r#"skip 'This test should be skipped' +exec echo "This should not run" +"#; + + fs::write(testdata_dir.join("hello.txt"), hello_content).unwrap(); + fs::write(testdata_dir.join("exists.txt"), exists_content).unwrap(); + fs::write(testdata_dir.join("skip.txt"), skip_content).unwrap(); + + // Test running specific files (should only run hello.txt and exists.txt) + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["hello.txt", "exists.txt"]) + .execute(); + + assert!(result.is_ok(), "Specific files test failed: {:?}", result); +} + +#[test] +fn test_files_with_vec_string() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + let test_content = r#"exec echo "Test" +stdout "Test" +"#; + + fs::write(testdata_dir.join("test1.txt"), test_content).unwrap(); + fs::write(testdata_dir.join("test2.txt"), test_content).unwrap(); + + // Test with Vec + let test_files = vec!["test1.txt".to_string(), "test2.txt".to_string()]; + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(test_files) + .execute(); + + assert!( + result.is_ok(), + "Vec files test failed: {:?}", + result + ); +} + +#[test] +fn test_files_with_relative_paths() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + let subdir = testdata_dir.join("subdir"); + fs::create_dir_all(&subdir).unwrap(); + + let test_content = r#"exec echo "Relative test" +stdout "Relative test" +"#; + + fs::write(testdata_dir.join("test.txt"), test_content).unwrap(); + fs::write(subdir.join("nested.txt"), test_content).unwrap(); + + // Test with relative paths including subdirectories + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["test.txt", "subdir/nested.txt"]) + .execute(); + + assert!(result.is_ok(), "Relative paths test failed: {:?}", result); +} + +#[test] +fn test_files_with_absolute_paths() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + let test_content = r#"exec echo "Absolute test" +stdout "Absolute test" +"#; + + let test_file = testdata_dir.join("absolute_test.txt"); + fs::write(&test_file, test_content).unwrap(); + + // Test with absolute path + let result = testscript::run(testdata_dir.to_string_lossy()) + .files([test_file.to_string_lossy().to_string()]) + .execute(); + + assert!(result.is_ok(), "Absolute paths test failed: {:?}", result); +} + +#[test] +fn test_files_nonexistent_file_error() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Test with nonexistent file + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["nonexistent.txt"]) + .execute(); + + assert!(result.is_err(), "Expected error for nonexistent file"); + let error_msg = format!("{:?}", result.unwrap_err()); + assert!( + error_msg.contains("Test file not found"), + "Error message should mention file not found: {}", + error_msg + ); +} + +#[test] +fn test_files_empty_list_error() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Test with empty file list + let empty_files: Vec = vec![]; + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(empty_files) + .execute(); + + assert!(result.is_err(), "Expected error for empty file list"); + let error_msg = format!("{:?}", result.unwrap_err()); + assert!( + error_msg.contains("No test files specified"), + "Error message should mention no files specified: {}", + error_msg + ); +} + +#[test] +fn test_files_directory_instead_of_file_error() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + let subdir = testdata_dir.join("subdir"); + fs::create_dir_all(&subdir).unwrap(); + + // Test with directory instead of file + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["subdir"]) + .execute(); + + assert!( + result.is_err(), + "Expected error for directory instead of file" + ); + let error_msg = format!("{:?}", result.unwrap_err()); + assert!( + error_msg.contains("Path is not a file"), + "Error message should mention path not a file: {}", + error_msg + ); +} + +#[test] +fn test_files_combined_with_other_features() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + let test_content = r#"exec echo "combined_test" +stdout "combined_test" +"#; + + fs::write(testdata_dir.join("combined.txt"), test_content).unwrap(); + + // Test files() combined with other builder methods + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["combined.txt"]) + .condition("custom", true) + .setup(|_env| Ok(())) + .execute(); + + assert!( + result.is_ok(), + "Combined features test failed: {:?}", + result + ); +} + +#[test] +fn test_files_vs_normal_discovery() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + let good_content = r#"exec echo "good" +stdout "good" +"#; + + let bad_content = r#"exec echo "bad" +stdout "different_output" +"#; + + fs::write(testdata_dir.join("good.txt"), good_content).unwrap(); + fs::write(testdata_dir.join("bad.txt"), bad_content).unwrap(); + + // Normal discovery should fail because bad.txt fails + let result_all = testscript::run(testdata_dir.to_string_lossy()).execute(); + assert!( + result_all.is_err(), + "Expected failure when running all files" + ); + + // But specific file selection should succeed when only running good.txt + let result_specific = testscript::run(testdata_dir.to_string_lossy()) + .files(["good.txt"]) + .execute(); + assert!( + result_specific.is_ok(), + "Specific good file should succeed: {:?}", + result_specific + ); +} + +#[test] +fn test_files_execution_order() { + let temp_dir = TempDir::new().unwrap(); + let testdata_dir = temp_dir.path().join("testdata"); + fs::create_dir(&testdata_dir).unwrap(); + + // Create test files with simple operations to verify they all run + let test1_content = r#"exec echo "test1" +stdout "test1" +"#; + + let test2_content = r#"exec echo "test2" +stdout "test2" +"#; + + let test3_content = r#"exec echo "test3" +stdout "test3" +"#; + + fs::write(testdata_dir.join("z_test1.txt"), test1_content).unwrap(); + fs::write(testdata_dir.join("a_test2.txt"), test2_content).unwrap(); + fs::write(testdata_dir.join("m_test3.txt"), test3_content).unwrap(); + + // Files should be executed in the order they're specified, not alphabetical + // This test verifies that files execute successfully when specified in a custom order + let result = testscript::run(testdata_dir.to_string_lossy()) + .files(["z_test1.txt", "a_test2.txt", "m_test3.txt"]) + .execute(); + + assert!(result.is_ok(), "Execution order test failed: {:?}", result); +}