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

improve error messages with context (#13)

Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
Jason Hall 2025-09-26 19:55:13 -04:00 committed by GitHub
parent 38bef8a370
commit 3a8e953f70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 3 deletions

View file

@ -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

View file

@ -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<Error>,
},
}
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<String>,
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()
}

View file

@ -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 {

View file

@ -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]

61
tests/error_messages.rs Normal file
View file

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