Skip to content

Commit aeaa34c

Browse files
wan9chiclaude
andcommitted
test(e2e): let a step redact its output with its own patterns
Some steps remove several inputs at once, and vt reports whichever mismatch its parallel validation finds first, so the step's "cache miss" line changes from run to run. A new `redact` step key maps regexes to replacements; the harness applies them in key order to that step's output after the global redactions, and an invalid regex fails when the fixtures load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cb0bddc commit aeaa34c

6 files changed

Lines changed: 87 additions & 3 deletions

File tree

‎crates/vt_bin/tests/e2e_snapshots/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ steps = [
3636
Notes:
3737

3838
- An array step is shorthand for `{ argv = [...] }`. A string step such as `"vt run build"` doesn't parse, and the test binary panics while loading the fixtures, before any case runs.
39-
- A table step takes `argv` (required), `cwd` (relative to the fixture root, defaulting to the case's `cwd`), `comment` (shown under the step's heading in the snapshot), `envs` (`[name, value]` pairs), `interactions`, and `formatted-snapshot` (keep colors and styles in the snapshot). Other keys are an error.
39+
- A table step takes `argv` (required), `cwd` (relative to the fixture root, defaulting to the case's `cwd`), `comment` (shown under the step's heading in the snapshot), `envs` (`[name, value]` pairs), `interactions`, `formatted-snapshot` (keep colors and styles in the snapshot), and `redact`. Other keys are an error.
40+
- `redact` maps regexes to replacements for output that varies between runs, such as `redact = { 'cache miss: .*' = 'cache miss: <changed input>' }`. They rewrite the step's output in key order, after the built-in redactions, and a replacement can refer to capture groups as `$1`. TOML literal strings (`'…'`) keep regex backslashes unescaped. An invalid regex, like an unknown key, makes the test binary panic while loading the fixtures.
4041
- A case takes `name` (ASCII letters, digits, and `_` only; it also names the snapshot file), `steps`, and optionally `comment`, `cwd`, `cfg` (a Rust cfg expression such as `'target_os = "linux"'`; the case runs only where it matches), and `ignore` (the case runs only with `-- --ignored`).
4142
- `vt` and `vtt` are the binaries built for the test. Other programs are looked up in `PATH`, with `packages/tools/node_modules/.bin` (when installed) ahead of the inherited entries.
4243
- `write-key` accepts `up`, `down`, `enter`, `escape`, `backspace`, and `ctrl-c`.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "step-redact",
3+
"private": true
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[[e2e]]
2+
name = "step_redact_rewrites_only_its_own_output"
3+
comment = """
4+
A step's `redact` table rewrites regex matches in that step's output and leaves other steps' output alone.
5+
"""
6+
steps = [
7+
{ argv = [
8+
"vtt",
9+
"print",
10+
"value:",
11+
"8f3a1c",
12+
], comment = "`redact` replaces the value", redact = { 'value: \w+' = 'value: <value>' } },
13+
{ argv = [
14+
"vtt",
15+
"print",
16+
"value:",
17+
"8f3a1c",
18+
], comment = "no `redact`, so the value stays" },
19+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# step_redact_rewrites_only_its_own_output
2+
3+
A step's `redact` table rewrites regex matches in that step's output and leaves other steps' output alone.
4+
5+
## `vtt print value: 8f3a1c`
6+
7+
`redact` replaces the value
8+
9+
```
10+
value: <value>
11+
```
12+
13+
## `vtt print value: 8f3a1c`
14+
15+
no `redact`, so the value stays
16+
17+
```
18+
value: 8f3a1c
19+
```

‎crates/vt_bin/tests/e2e_snapshots/main.rs‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod redact;
22

33
use std::{
4+
collections::BTreeMap,
45
env::{self, join_paths, split_paths},
56
ffi::OsStr,
67
io::Write,
@@ -59,6 +60,10 @@ struct StepConfig {
5960
/// assertion. Default `false` keeps the plain-text behaviour.
6061
#[serde(default, rename = "formatted-snapshot")]
6162
formatted_snapshot: bool,
63+
/// Regex → replacement pairs for output that varies between runs, applied
64+
/// in key order after the global redactions.
65+
#[serde(default, deserialize_with = "deserialize_redactions")]
66+
redact: Vec<(regex::Regex, Str)>,
6267
}
6368

6469
impl Step {
@@ -148,6 +153,13 @@ impl Step {
148153
Self::Simple(_) => false,
149154
}
150155
}
156+
157+
fn redactions(&self) -> &[(regex::Regex, Str)] {
158+
match self {
159+
Self::Detailed(config) => &config.redact,
160+
Self::Simple(_) => &[],
161+
}
162+
}
151163
}
152164

153165
#[derive(serde::Deserialize, Debug, Clone)]
@@ -221,6 +233,22 @@ impl WriteKey {
221233
}
222234
}
223235

236+
/// Compiles a step's `redact` table in key order, so an invalid regex fails
237+
/// when the fixtures load.
238+
fn deserialize_redactions<'de, D>(deserializer: D) -> Result<Vec<(regex::Regex, Str)>, D::Error>
239+
where
240+
D: serde::Deserializer<'de>,
241+
{
242+
let table = <BTreeMap<Str, Str> as serde::Deserialize>::deserialize(deserializer)?;
243+
table
244+
.into_iter()
245+
.map(|(pattern, replacement)| {
246+
let regex = regex::Regex::new(pattern.as_str()).map_err(serde::de::Error::custom)?;
247+
Ok((regex, replacement))
248+
})
249+
.collect()
250+
}
251+
224252
fn deserialize_cfg_expr<'de, D>(
225253
deserializer: D,
226254
) -> Result<Option<cargo_platform::CfgExpr>, D::Error>
@@ -652,7 +680,7 @@ fn run_case(
652680
}
653681
}
654682

655-
e2e_outputs.push_str(&redact_e2e_output(output, e2e_stage_path_str));
683+
e2e_outputs.push_str(&redact_e2e_output(output, e2e_stage_path_str, step.redactions()));
656684

657685
// Skip remaining steps if timed out
658686
if matches!(termination_state, TerminationState::TimedOut) {

‎crates/vt_bin/tests/e2e_snapshots/redact.rs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::borrow::Cow;
22

3+
use vt_str::Str;
4+
35
#[expect(
46
clippy::disallowed_types,
57
reason = "String mutation required by regex replace and cow_replace APIs"
@@ -26,7 +28,11 @@ fn redact_string(s: &mut String, redactions: &[(&str, &str)]) {
2628
clippy::disallowed_types,
2729
reason = "String required by regex replace_all and cow_replace APIs; Path required for CARGO_MANIFEST_DIR path manipulation"
2830
)]
29-
pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String {
31+
pub fn redact_e2e_output(
32+
mut output: String,
33+
workspace_root: &str,
34+
step_redactions: &[(regex::Regex, Str)],
35+
) -> String {
3036
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
3137

3238
// On Windows, canonicalize() may produce verbatim paths (\\?\C:\...) while
@@ -124,6 +130,13 @@ pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String {
124130
let mise_warning_regex = regex::Regex::new(r"(?m)^mise WARN\s+.*\n?").unwrap();
125131
output = mise_warning_regex.replace_all(&output, "").into_owned();
126132

133+
// Apply the step's own `redact` patterns after the global redactions, so
134+
// they match the text the snapshot would otherwise show, and before
135+
// sorting, so diagnostic blocks they normalize sort the same every run.
136+
for (regex, replacement) in step_redactions {
137+
output = regex.replace_all(&output, replacement.as_str()).into_owned();
138+
}
139+
127140
// Sort consecutive diagnostic blocks to handle non-deterministic tool output
128141
// (e.g., oxlint reports warnings in arbitrary order due to multi-threading).
129142
// Each block starts with " ! " and ends at the next empty line.

0 commit comments

Comments
 (0)