diff --git a/README.md b/README.md index 7cffed7..e5f964c 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,20 @@ Test scripts use the [`txtar`](https://pkg.go.dev/github.com/rogpeppe/go-interna Commands can be prefixed with conditions (`[unix]`) or negated (`!`). +## Error Messages + +testscript-rs provides detailed error messages with script context to make debugging easy: + +``` +Error in testdata/hello.txt at line 6: + 3 | stdout "this works" + 4 | + 5 | # This command will fail +> 6 | exec nonexistent-command arg1 arg2 + 7 | stdout "should not get here" + 8 | +``` + > Note: Some features of `testscript` in Go are not supported in this Rust port: > > - `[gc]` for whether Go was built with gc diff --git a/src/error.rs b/src/error.rs index 3bbf4f2..2fb84da 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,16 @@ pub enum Error { /// Generic error with message #[error("{0}")] Generic(String), + + /// Script execution error with context + #[error("Error in {script_file} at line {line_num}:\n{context}")] + ScriptError { + script_file: String, + line_num: usize, + context: String, + #[source] + source: Box, + }, } impl Error { @@ -64,4 +74,45 @@ impl Error { message: message.into(), } } + + /// Create a script error with context + pub fn script_error( + script_file: impl Into, + line_num: usize, + script_content: &str, + source: Error, + ) -> Self { + let context = generate_error_context(script_content, line_num); + Error::ScriptError { + script_file: script_file.into(), + line_num, + context, + source: Box::new(source), + } + } +} + +/// Generate error context showing surrounding lines +fn generate_error_context(script_content: &str, error_line: usize) -> String { + let lines: Vec<&str> = script_content.lines().collect(); + let mut context = String::new(); + + let start = error_line.saturating_sub(3).max(1); + let end = (error_line + 2).min(lines.len()); + + for line_num in start..=end { + if line_num == 0 { + continue; + } + + let line_content = lines.get(line_num - 1).unwrap_or(&""); + + if line_num == error_line { + context.push_str(&format!("> {} | {}\n", line_num, line_content)); + } else { + context.push_str(&format!(" {} | {}\n", line_num, line_content)); + } + } + + context.trim_end().to_string() } diff --git a/src/run.rs b/src/run.rs index 3bee5c3..9ed2a90 100644 --- a/src/run.rs +++ b/src/run.rs @@ -659,7 +659,21 @@ pub fn run_test(script_path: &Path) -> Result<()> { pub fn run_script(script_path: &Path, params: &RunParams) -> Result<()> { // Read and parse the script let content = fs::read_to_string(script_path)?; - let script = crate::parser::parse(&content)?; + let script = crate::parser::parse(&content).map_err(|e| { + // Enhance parse errors with script context + match e { + Error::Parse { line, message } => Error::script_error( + script_path.to_string_lossy().to_string(), + line, + &content, + Error::Parse { line, message }, + ), + other => other, + } + })?; + + // Create script context for better error reporting + let script_file = script_path.to_string_lossy().to_string(); // Create test environment let mut env = TestEnvironment::new()?; @@ -678,7 +692,15 @@ pub fn run_script(script_path: &Path, params: &RunParams) -> Result<()> { // Execute commands for command in &script.commands { - execute_command(&mut env, command, params)?; + if let Err(e) = execute_command(&mut env, command, params) { + // Wrap error with script context + return Err(Error::script_error( + &script_file, + command.line_num, + &content, + e, + )); + } // Check for early termination if env.should_skip { diff --git a/tests/builtin_commands.rs b/tests/builtin_commands.rs index f9fa9fc..5559aaa 100644 --- a/tests/builtin_commands.rs +++ b/tests/builtin_commands.rs @@ -138,7 +138,12 @@ exec echo "This should not run""#; let result = run_test(&script_path); assert!(result.is_err(), "Skip test should have failed"); - assert!(result.unwrap_err().to_string().contains("SKIP")); + + let error_msg = result.unwrap_err().to_string(); + println!("Skip error message: {}", error_msg); + // The skip command should create a contextual error showing the skip line + assert!(error_msg.contains("skip_test.txt")); + assert!(error_msg.contains("skip \"This test should be skipped\"")); } #[test] diff --git a/tests/error_messages.rs b/tests/error_messages.rs new file mode 100644 index 0000000..7c8cc7c --- /dev/null +++ b/tests/error_messages.rs @@ -0,0 +1,61 @@ +//! Tests for enhanced error messages + +use std::fs; +use tempfile::TempDir; +use testscript_rs::run_test; + +#[test] +fn test_error_messages() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("failing_test.txt"); + + let script_content = r#"# Test that should fail with good error message +exec echo "this works" +stdout "this works" + +# This command will fail +exec nonexistent-command arg1 arg2 +stdout "should not get here" + +# More context +exec echo "after failure""#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + + // This should fail, but with enhanced error messages + assert!(result.is_err()); + + let error_msg = result.unwrap_err().to_string(); + println!("Enhanced error message:\n{}", error_msg); + + // Verify the error contains context + assert!(error_msg.contains("failing_test.txt")); + assert!(error_msg.contains("line")); + assert!(error_msg.contains("nonexistent-command")); +} + +#[test] +fn test_parse_error_context() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("parse_error.txt"); + + let script_content = r#"# Test parse error +exec echo "good command" +[unclosed condition exec echo "bad" +exec echo "after error""#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + + assert!(result.is_err()); + + let error_msg = result.unwrap_err().to_string(); + println!("Parse error message:\n{}", error_msg); + + // Should show context around the parse error + assert!(error_msg.contains("parse_error.txt")); + assert!(error_msg.contains("unclosed")); +}