1
0
Fork 0
mirror of https://github.com/imjasonh/testscript-rs synced 2026-07-11 18:39:35 +00:00

Add UpdateScripts functionality for test maintenance (#20)

- [x] Analyze existing codebase structure and architecture
- [x] Review test files to understand expected UpdateScripts
functionality
- [x] Understand current API and parameter structure
- [x] Add `update_scripts` parameter to RunParams struct
- [x] Add `update_scripts` method to Builder API
- [x] Support UPDATE_SCRIPTS environment variable detection
- [x] Implement update functionality in stdout/stderr command execution
- [x] Modify output comparison to capture and update actual output when
in update mode
- [x] Add script file modification logic to update test files
- [x] Create focused tests for update functionality
- [x] Validate changes work end-to-end
- [x] Update documentation in README.md
- [x] Clean up temporary files
- [x] Move UpdateScripts section to bottom of README per review feedback
- [x] Fix code formatting issues with cargo fmt

## Implementation Summary

Successfully implemented complete UpdateScripts functionality for test
maintenance in testscript-rs:

### Key Features
- **API support**: `.update_scripts(true)` method on Builder
- **Environment variable**: `UPDATE_SCRIPTS=1` environment variable
support
- **Smart updating**: Only updates `stdout`/`stderr` expectations,
preserves file structure
- **Proper quoting**: Handles complex output with appropriate shell
quoting
- **Error handling**: Continues execution instead of failing in update
mode

### API Usage
```rust
// Via API
testscript::run("testdata")
    .update_scripts(true)
    .execute()
    .unwrap();

// Via environment variable  
UPDATE_SCRIPTS=1 cargo test
```

### Implementation Details
- Added `update_scripts: bool` field to `RunParams`
- Modified `run_script_impl` to collect updates instead of failing on
mismatches
- Implemented `apply_script_updates` function for file modification
- Added comprehensive test coverage with 4 test cases
- Updated README.md with usage examples (moved to bottom per review
feedback)
- Fixed code formatting issues identified by CI

### Testing
- All existing tests continue to pass
- New functionality tested via API and environment variable
- Manual validation confirms proper file updating
- Normal mode behavior preserved (still fails on mismatches when
disabled)
- Code passes formatting checks and linting

The implementation matches Go's testscript behavior and provides
essential test maintenance functionality.

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Add UpdateScripts functionality for test
maintenance</issue_title>
> <issue_description>## Feature Request: UpdateScripts (Test Maintenance
Mode)
> 
> Go's testscript supports an `UpdateScripts` parameter that
automatically updates test files with actual command output. This is
invaluable for test maintenance.
> 
> ## Proposed API
> 
> ```rust
> testscript::run("testdata")
>     .update_scripts(true)
>     .execute()
>     .unwrap();
> ```
> 
> Or via environment variable:
> ```bash
> UPDATE_SCRIPTS=1 cargo test
> ```
> 
> ## How It Works
> 
> When enabled, instead of failing on output mismatches:
> 1. **Capture actual output**
> 2. **Update the test file** with the actual output
> 3. **Continue to next test**
> 
> ## Example
> 
> Before (failing test):
> ```
> exec my-tool --version
> stdout "my-tool 1.0"
> ```
> 
> After running with update mode:
> ```
> exec my-tool --version
> stdout "my-tool 2.1.0"
> ```
> 
> ## Benefits
> 
> - **Easy test maintenance**: Update expected outputs after changes
> - **Reduce manual work**: No hand-editing of test files
> - **Go compatibility**: Matches upstream behavior
> - **Development workflow**: Essential for evolving CLI tools
> 
> ## Implementation Notes
> 
> - Should only update `stdout`/`stderr` expectations
> - Preserve comments and file structure
> - Add safety checks to prevent accidental
overwrites</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>
Fixes imjasonh/testscript-rs#8

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start
the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
Copilot 2025-09-26 21:47:09 -04:00 committed by GitHub
parent 0c4315ece8
commit fdb53b03fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 298 additions and 2 deletions

View file

@ -6,6 +6,17 @@ use crate::run::{environment::TestEnvironment, params::RunParams};
use std::fs;
use std::path::Path;
/// Information about a script update needed when in update mode
#[derive(Debug, Clone)]
pub struct ScriptUpdate {
/// The line number where the stdout/stderr command appears
pub line_num: usize,
/// The command name (stdout or stderr)
pub command_name: String,
/// The new expected output
pub new_output: String,
}
/// Run a single script with the given parameters - main implementation
pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
// Read and parse the script
@ -41,10 +52,34 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
setup(&env)?;
}
// Track script updates if we're in update mode
let mut updates = Vec::new();
// Execute commands
for command in &script.commands {
if let Err(e) = execute_command(&mut env, command, params) {
// Wrap error with script context
let result = execute_command(&mut env, command, params);
if let Err(e) = result {
// If we're in update mode and this is an output comparison error, capture the update
if params.update_scripts {
if let Error::OutputCompare {
expected: _,
actual,
} = &e
{
if command.name == "stdout" || command.name == "stderr" {
updates.push(ScriptUpdate {
line_num: command.line_num,
command_name: command.name.clone(),
new_output: actual.clone(),
});
// Continue instead of failing
continue;
}
}
}
// Wrap error with script context for non-update cases or non-output errors
return Err(Error::script_error(
&script_file,
command.line_num,
@ -62,6 +97,11 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
}
}
// Apply updates if any were collected
if !updates.is_empty() && params.update_scripts {
apply_script_updates(script_path, &content, &updates)?;
}
// Wait for any remaining background processes
let background_names: Vec<String> = env.background_processes.keys().cloned().collect();
for name in background_names {
@ -71,6 +111,63 @@ pub fn run_script_impl(script_path: &Path, params: &RunParams) -> Result<()> {
Ok(())
}
/// Apply script updates to the actual file
fn apply_script_updates(script_path: &Path, content: &str, updates: &[ScriptUpdate]) -> Result<()> {
let lines: Vec<&str> = content.lines().collect();
let mut updated_lines = Vec::new();
let mut i = 0;
while i < lines.len() {
let line_num = i + 1; // Line numbers are 1-based
// Check if this line needs to be updated
if let Some(update) = updates.iter().find(|u| u.line_num == line_num) {
// This is a stdout/stderr command that needs updating
let line = lines[i];
let cmd_part = format!("{} ", update.command_name);
if line.trim_start().starts_with(&cmd_part) {
// Extract the indentation from the original line
let indent = line.len() - line.trim_start().len();
let indent_str = " ".repeat(indent);
// Create the updated line with proper quoting
let quoted_output = if update.new_output.contains(' ')
|| update.new_output.contains('\n')
|| update.new_output.contains('"')
{
// Use proper shell quoting for complex strings
format!("\"{}\"", update.new_output.replace('"', "\\\""))
} else if update.new_output.is_empty() {
"\"-\"".to_string()
} else {
update.new_output.clone()
};
updated_lines.push(format!(
"{}{} {}",
indent_str, update.command_name, quoted_output
));
} else {
// Shouldn't happen, but preserve the original line if it doesn't match
updated_lines.push(line.to_string());
}
} else {
// Keep the original line
updated_lines.push(lines[i].to_string());
}
i += 1;
}
// Write the updated content back to the file
let updated_content = updated_lines.join("\n");
if updated_content != content {
fs::write(script_path, updated_content)?;
}
Ok(())
}
/// Execute a single command
fn execute_command(env: &mut TestEnvironment, command: &Command, params: &RunParams) -> Result<()> {
// For negated commands, we expect them to fail