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:
parent
38bef8a370
commit
3a8e953f70
5 changed files with 156 additions and 3 deletions
14
README.md
14
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
|
||||
|
|
|
|||
51
src/error.rs
51
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<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()
|
||||
}
|
||||
|
|
|
|||
26
src/run.rs
26
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 {
|
||||
|
|
|
|||
|
|
@ -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
61
tests/error_messages.rs
Normal 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"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue