mirror of
https://github.com/imjasonh/testscript-rs
synced 2026-07-09 01:26:40 +00:00
Implement TestWork functionality for preserving working directories on failure
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
parent
6cb1109c05
commit
fe5da3cebe
7 changed files with 282 additions and 5 deletions
30
README.md
30
README.md
|
|
@ -102,6 +102,7 @@ testscript::run("testdata")
|
|||
Ok(())
|
||||
})
|
||||
.condition("feature-enabled", true)
|
||||
.preserve_work_on_failure(true) // Debug failed tests
|
||||
.execute()
|
||||
.unwrap();
|
||||
```
|
||||
|
|
@ -200,3 +201,32 @@ stdout "my-tool 2.1.0"
|
|||
```
|
||||
|
||||
This feature only updates `stdout` and `stderr` expectations while preserving file structure and comments.
|
||||
|
||||
## TestWork (Debug Failed Tests)
|
||||
|
||||
When tests fail, you can preserve the working directory to inspect the test environment and debug issues:
|
||||
|
||||
```rust
|
||||
testscript::run("testdata")
|
||||
.preserve_work_on_failure(true)
|
||||
.execute()
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
When a test fails with this option enabled, you'll see output like:
|
||||
|
||||
```
|
||||
Test failed. Work directory preserved at: /tmp/testscript-work-abc123
|
||||
You can inspect the test environment:
|
||||
cd /tmp/testscript-work-abc123
|
||||
ls -la
|
||||
```
|
||||
|
||||
This feature matches Go's testscript `TestWork` functionality and makes debugging much easier by allowing you to:
|
||||
|
||||
- Inspect files created during test execution
|
||||
- Manually run commands that failed
|
||||
- Understand the exact state when the test failed
|
||||
- Debug complex test scenarios step-by-step
|
||||
|
||||
The working directory contains all files from the test script's file blocks, plus any files created during test execution.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,22 @@ fn main() {
|
|||
std::env::set_current_dir(manifest_dir).expect("Failed to change directory");
|
||||
}
|
||||
|
||||
// Run all tests in testdata directory
|
||||
match testscript::run("testdata").execute() {
|
||||
// Check for test directory from environment for test purposes
|
||||
let test_dir = std::env::var("TESTSCRIPT_TEST_DIR").unwrap_or_else(|_| "testdata".to_string());
|
||||
|
||||
// Check if preserve work should be enabled
|
||||
let preserve_work = std::env::var("TESTSCRIPT_PRESERVE_WORK")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
// Run all tests in the specified directory
|
||||
let mut builder = testscript::run(test_dir);
|
||||
|
||||
if preserve_work {
|
||||
builder = builder.preserve_work_on_failure(true);
|
||||
}
|
||||
|
||||
match builder.execute() {
|
||||
Ok(()) => println!("All tests passed!"),
|
||||
Err(e) => {
|
||||
eprintln!("Test failed: {}", e);
|
||||
|
|
|
|||
31
src/lib.rs
31
src/lib.rs
|
|
@ -93,7 +93,7 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> {
|
|||
/// testscript::run("testdata").execute().unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// ### With Custom Setup
|
||||
/// ### With Custom Setup and Work Directory Preservation
|
||||
/// ```no_run
|
||||
/// use testscript_rs::testscript;
|
||||
///
|
||||
|
|
@ -111,6 +111,7 @@ fn run(params: &mut RunParams, test_data_glob: &str) -> Result<()> {
|
|||
/// Ok(())
|
||||
/// })
|
||||
/// .condition("custom", my_custom_check())
|
||||
/// .preserve_work_on_failure(true) // Preserve work directory on test failure
|
||||
/// .execute()
|
||||
/// .unwrap();
|
||||
///
|
||||
|
|
@ -200,6 +201,34 @@ impl Builder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Enable or disable preserving working directories when tests fail
|
||||
///
|
||||
/// When enabled, if a test fails, the working directory will be preserved
|
||||
/// and its path will be printed to stderr for debugging purposes.
|
||||
/// This matches the behavior of Go's testscript TestWork functionality.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// use testscript_rs::testscript;
|
||||
///
|
||||
/// testscript::run("testdata")
|
||||
/// .preserve_work_on_failure(true)
|
||||
/// .execute()
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// When a test fails, you'll see output like:
|
||||
/// ```text
|
||||
/// Test failed. Work directory preserved at: /tmp/testscript-work-abc123
|
||||
/// You can inspect the test environment:
|
||||
/// cd /tmp/testscript-work-abc123
|
||||
/// ls -la
|
||||
/// ```
|
||||
pub fn preserve_work_on_failure(mut self, preserve: bool) -> Self {
|
||||
self.params = self.params.preserve_work_on_failure(preserve);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute all test scripts in the configured directory
|
||||
///
|
||||
/// This will discover all `.txt` files in the directory and run them as test scripts.
|
||||
|
|
|
|||
|
|
@ -301,6 +301,15 @@ impl TestEnvironment {
|
|||
|
||||
result
|
||||
}
|
||||
|
||||
/// Preserve the work directory by preventing TempDir cleanup
|
||||
/// Returns the path to the preserved directory
|
||||
pub fn preserve_work_dir(self) -> std::path::PathBuf {
|
||||
let work_dir = self.work_dir.clone();
|
||||
// Leak the TempDir to prevent cleanup
|
||||
std::mem::forget(self._temp_dir);
|
||||
work_dir
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle work directory preservation on failure
|
||||
if params.preserve_work_on_failure {
|
||||
let preserved_path = env.preserve_work_dir();
|
||||
eprintln!("Test failed. Work directory preserved at: {}", preserved_path.display());
|
||||
eprintln!("You can inspect the test environment:");
|
||||
eprintln!(" cd {}", preserved_path.display());
|
||||
eprintln!(" ls -la");
|
||||
}
|
||||
|
||||
// Wrap error with script context for non-update cases or non-output errors
|
||||
return Err(Error::script_error(
|
||||
&script_file,
|
||||
|
|
@ -90,6 +99,14 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
|
|||
|
||||
// Check for early termination
|
||||
if env.should_skip {
|
||||
// Handle work directory preservation for skipped tests if configured
|
||||
if params.preserve_work_on_failure {
|
||||
let preserved_path = env.preserve_work_dir();
|
||||
eprintln!("Test skipped. Work directory preserved at: {}", preserved_path.display());
|
||||
eprintln!("You can inspect the test environment:");
|
||||
eprintln!(" cd {}", preserved_path.display());
|
||||
eprintln!(" ls -la");
|
||||
}
|
||||
return Err(Error::Generic("Test skipped".to_string()));
|
||||
}
|
||||
if env.should_stop {
|
||||
|
|
@ -102,10 +119,19 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
|
|||
apply_script_updates(script_path, &content, &updates)?;
|
||||
}
|
||||
|
||||
// Wait for any remaining background processes
|
||||
// Wait for any remaining background processes - handle failures here too
|
||||
let background_names: Vec<String> = env.background_processes.keys().cloned().collect();
|
||||
for name in background_names {
|
||||
env.wait_for_background(&name)?;
|
||||
if let Err(e) = env.wait_for_background(&name) {
|
||||
if params.preserve_work_on_failure {
|
||||
let preserved_path = env.preserve_work_dir();
|
||||
eprintln!("Test failed during background process cleanup. Work directory preserved at: {}", preserved_path.display());
|
||||
eprintln!("You can inspect the test environment:");
|
||||
eprintln!(" cd {}", preserved_path.display());
|
||||
eprintln!(" ls -la");
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ pub struct RunParams {
|
|||
pub conditions: HashMap<String, bool>,
|
||||
/// Whether to update test scripts with actual output
|
||||
pub update_scripts: bool,
|
||||
/// Whether to preserve working directories when tests fail
|
||||
pub preserve_work_on_failure: bool,
|
||||
}
|
||||
|
||||
impl RunParams {
|
||||
|
|
@ -52,6 +54,7 @@ impl RunParams {
|
|||
setup: None,
|
||||
conditions,
|
||||
update_scripts,
|
||||
preserve_work_on_failure: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +85,12 @@ impl RunParams {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set whether to preserve working directories when tests fail
|
||||
pub fn preserve_work_on_failure(mut self, preserve: bool) -> Self {
|
||||
self.preserve_work_on_failure = preserve;
|
||||
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
|
||||
|
|
|
|||
160
tests/preserve_work.rs
Normal file
160
tests/preserve_work.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
//! Tests for work directory preservation functionality
|
||||
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_preserve_work_on_failure_disabled_by_default() {
|
||||
// Create a failing test
|
||||
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 "hello"
|
||||
stdout "goodbye"
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("failing_test.txt"), test_content).unwrap();
|
||||
|
||||
let result = testscript_rs::testscript::run(testdata_dir.to_string_lossy()).execute();
|
||||
|
||||
// Should fail
|
||||
assert!(result.is_err());
|
||||
|
||||
// Work directory should be cleaned up (we can't easily test this since TempDir cleanup is automatic)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_work_on_failure_enabled() {
|
||||
// Create a failing test
|
||||
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 "hello"
|
||||
stdout "goodbye"
|
||||
|
||||
-- test_file.txt --
|
||||
This is test content
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("failing_test.txt"), test_content).unwrap();
|
||||
|
||||
// We need to capture stderr to see the preservation message
|
||||
// Since we can't easily capture stderr from the current process,
|
||||
// we'll create a subprocess that runs our test
|
||||
let output = Command::new("cargo")
|
||||
.args(&["run", "--example", "test_runner"])
|
||||
.current_dir("/home/runner/work/testscript-rs/testscript-rs")
|
||||
.env("TESTSCRIPT_TEST_DIR", testdata_dir.to_string_lossy().as_ref())
|
||||
.env("TESTSCRIPT_PRESERVE_WORK", "true")
|
||||
.output();
|
||||
|
||||
if let Ok(output) = output {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
// Should contain preservation message
|
||||
assert!(stderr.contains("Work directory preserved at:"));
|
||||
} else {
|
||||
// Fallback test - just ensure the API works
|
||||
let result = testscript_rs::testscript::run(testdata_dir.to_string_lossy())
|
||||
.preserve_work_on_failure(true)
|
||||
.execute();
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_work_on_success_no_preservation() {
|
||||
// Create a passing test
|
||||
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 "hello"
|
||||
stdout "hello"
|
||||
|
||||
-- test_file.txt --
|
||||
This is test content
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("passing_test.txt"), test_content).unwrap();
|
||||
|
||||
let result = testscript_rs::testscript::run(testdata_dir.to_string_lossy())
|
||||
.preserve_work_on_failure(true)
|
||||
.execute();
|
||||
|
||||
// Should succeed and not preserve directory
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_work_with_skip() {
|
||||
// Create a test that skips
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let testdata_dir = temp_dir.path().join("testdata");
|
||||
fs::create_dir(&testdata_dir).unwrap();
|
||||
|
||||
let test_content = r#"skip this test is skipped
|
||||
|
||||
-- test_file.txt --
|
||||
This is test content
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("skip_test.txt"), test_content).unwrap();
|
||||
|
||||
let result = testscript_rs::testscript::run(testdata_dir.to_string_lossy())
|
||||
.preserve_work_on_failure(true)
|
||||
.execute();
|
||||
|
||||
// Should fail due to skip, but this tests that the preserve logic handles skip correctly
|
||||
assert!(result.is_err());
|
||||
if let Err(e) = result {
|
||||
// The error might be wrapped in script context, so just check for skip-related content
|
||||
let error_str = e.to_string();
|
||||
assert!(error_str.contains("SKIP") || error_str.contains("Test skipped") || error_str.contains("skip"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_work_with_background_process_failure() {
|
||||
// Create a test that fails during background process cleanup
|
||||
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 sleep 10 &
|
||||
wait sleep
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("bg_test.txt"), test_content).unwrap();
|
||||
|
||||
// We don't assert success/failure here since sleep behavior varies, but the test exercises the code
|
||||
let _result = testscript_rs::testscript::run(testdata_dir.to_string_lossy())
|
||||
.preserve_work_on_failure(true)
|
||||
.execute();
|
||||
|
||||
// We don't assert success/failure here since sleep behavior varies, but the test exercises the code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_preserve_work_method_exists() {
|
||||
// Test that the API exists and can be chained
|
||||
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 "hello"
|
||||
stdout "hello"
|
||||
"#;
|
||||
|
||||
fs::write(testdata_dir.join("simple_test.txt"), test_content).unwrap();
|
||||
|
||||
let result = testscript_rs::testscript::run(testdata_dir.to_string_lossy())
|
||||
.preserve_work_on_failure(true)
|
||||
.preserve_work_on_failure(false) // Can be chained and overridden
|
||||
.execute();
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue