1
0
Fork 0
mirror of https://github.com/imjasonh/testscript-rs synced 2026-07-08 09:05:34 +00:00

Implement support for running specific test files

Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-30 07:21:26 +00:00
parent a8880fa2c1
commit cce8ff1802
3 changed files with 408 additions and 24 deletions

View file

@ -21,28 +21,68 @@ pub use run::run_test;
fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> { fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> {
use walkdir::WalkDir; 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(); let mut test_files = Vec::new();
// Walk the directory and collect matching files // If specific files are provided, use them directly
for entry in WalkDir::new(base_dir).min_depth(1).max_depth(1) { if let Some(ref files) = params.files {
let entry = entry?; // Parse the base directory from the glob pattern
if entry.file_type().is_file() { let base_dir = if let Some(slash_pos) = test_data_glob.rfind('/') {
if let Some(file_name) = entry.file_name().to_str() { &test_data_glob[..slash_pos]
if regex.is_match(file_name) { } else {
test_files.push(entry.path().to_path_buf()); "."
};
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(); test_files.sort();
if test_files.is_empty() { if test_files.is_empty() {
return Err(Error::Generic(format!( if params.files.is_some() {
"No test files found matching pattern: {}", return Err(Error::Generic("No test files specified".to_string()));
test_data_glob } else {
))); return Err(Error::Generic(format!(
"No test files found matching pattern: {}",
test_data_glob
)));
}
} }
// Run each test file // Run each test file
@ -93,6 +137,17 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> {
/// testscript::run("testdata").execute().unwrap(); /// 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 /// ### With Custom Setup and Work Directory Preservation
/// ```no_run /// ```no_run
/// use testscript_rs::testscript; /// use testscript_rs::testscript;
@ -276,6 +331,49 @@ impl Builder {
self 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<String>
/// 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<I, S>(mut self, files: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.params = self.params.files(files);
self
}
/// Execute all test scripts in the configured directory /// Execute all test scripts in the configured directory
/// ///
/// This will discover all `.txt` files in the directory and run them as test scripts. /// 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 /// // Run all tests in testdata directory
/// testscript::run("testdata").execute().unwrap(); /// testscript::run("testdata").execute().unwrap();
///
/// // Run only specific test files
/// testscript::run("testdata")
/// .files(["hello.txt", "exists.txt"])
/// .execute()
/// .unwrap();
/// ``` /// ```
pub mod testscript { pub mod testscript {
use super::*; use super::*;

View file

@ -24,6 +24,8 @@ pub struct RunParams {
pub preserve_work_on_failure: bool, pub preserve_work_on_failure: bool,
/// Optional root directory for test working directories /// Optional root directory for test working directories
pub workdir_root: Option<std::path::PathBuf>, pub workdir_root: Option<std::path::PathBuf>,
/// Specific files to run (if None, discover all .txt files)
pub files: Option<Vec<String>>,
} }
impl RunParams { impl RunParams {
@ -58,6 +60,7 @@ impl RunParams {
update_scripts, update_scripts,
preserve_work_on_failure: false, preserve_work_on_failure: false,
workdir_root: None, workdir_root: None,
files: None,
} }
} }
@ -103,6 +106,19 @@ impl RunParams {
self 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<I, S>(mut self, files: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.files = Some(files.into_iter().map(|s| s.into()).collect());
self
}
/// Check if a program exists in PATH (cross-platform) /// Check if a program exists in PATH (cross-platform)
pub fn program_exists(program: &str) -> bool { pub fn program_exists(program: &str) -> bool {
// TODO: Consider caching results for performance if needed // TODO: Consider caching results for performance if needed

264
tests/specific_files.rs Normal file
View file

@ -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<String>
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<String> 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<String> = 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);
}