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 (`!`).
|
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:
|
> Note: Some features of `testscript` in Go are not supported in this Rust port:
|
||||||
>
|
>
|
||||||
> - `[gc]` for whether Go was built with gc
|
> - `[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
|
/// Generic error with message
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Generic(String),
|
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 {
|
impl Error {
|
||||||
|
|
@ -64,4 +74,45 @@ impl Error {
|
||||||
message: message.into(),
|
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<()> {
|
pub fn run_script(script_path: &Path, params: &RunParams) -> Result<()> {
|
||||||
// Read and parse the script
|
// Read and parse the script
|
||||||
let content = fs::read_to_string(script_path)?;
|
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
|
// Create test environment
|
||||||
let mut env = TestEnvironment::new()?;
|
let mut env = TestEnvironment::new()?;
|
||||||
|
|
@ -678,7 +692,15 @@ pub fn run_script(script_path: &Path, params: &RunParams) -> Result<()> {
|
||||||
|
|
||||||
// Execute commands
|
// Execute commands
|
||||||
for command in &script.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
|
// Check for early termination
|
||||||
if env.should_skip {
|
if env.should_skip {
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,12 @@ exec echo "This should not run""#;
|
||||||
|
|
||||||
let result = run_test(&script_path);
|
let result = run_test(&script_path);
|
||||||
assert!(result.is_err(), "Skip test should have failed");
|
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]
|
#[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