-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathredact.rs
More file actions
166 lines (144 loc) · 6.56 KB
/
redact.rs
File metadata and controls
166 lines (144 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
use std::borrow::Cow;
#[expect(
clippy::disallowed_types,
reason = "String mutation required by regex replace and cow_replace APIs"
)]
fn redact_string(s: &mut String, redactions: &[(&str, &str)]) {
use cow_utils::CowUtils as _;
for (from, to) in redactions {
if let Cow::Owned(mut replaced) = s.as_str().cow_replace(from, to) {
if cfg!(windows) {
// Normalize backslashes to forward slashes on Windows
replaced = replaced.cow_replace("\\", "/").into_owned();
// Collapse double slashes that arise when an escaped path separator (\\)
// is only partially replaced (e.g., Debug-format paths end with \\")
while replaced.contains("//") {
replaced = replaced.cow_replace("//", "/").into_owned();
}
}
*s = replaced;
}
}
}
#[expect(
clippy::disallowed_types,
reason = "String required by regex replace_all and cow_replace APIs; Path required for CARGO_MANIFEST_DIR path manipulation"
)]
pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
// On Windows, canonicalize() may produce verbatim paths (\\?\C:\...) while
// child processes report paths without the prefix. Try both variants.
let workspace_root_stripped = workspace_root.strip_prefix(r"\\?\").unwrap_or(workspace_root);
// On Windows, paths displayed via Debug format ({:?}) have backslashes escaped
// to double-backslashes. Create escaped variants to match Debug-format output.
// The full escaped variant (with \\?\ prefix) must be tried first since it's
// the longest match and prevents leaving a stray "\\?\" in the output.
let workspace_root_full_escaped = {
use cow_utils::CowUtils as _;
workspace_root.cow_replace('\\', r"\\").into_owned()
};
let workspace_root_stripped_escaped = {
use cow_utils::CowUtils as _;
workspace_root_stripped.cow_replace('\\', r"\\").into_owned()
};
let mut redactions: Vec<(&str, &str)> = vec![
(workspace_root, "<workspace>"),
(workspace_root_stripped, "<workspace>"),
(manifest_dir.as_str(), "<manifest_dir>"),
];
// Add escaped variants (longest first for correct matching)
if workspace_root_full_escaped != workspace_root {
redactions.insert(0, (&workspace_root_full_escaped, "<workspace>"));
}
if workspace_root_stripped_escaped != workspace_root_stripped
&& workspace_root_stripped_escaped != workspace_root_full_escaped
{
redactions.insert(1, (&workspace_root_stripped_escaped, "<workspace>"));
}
redact_string(&mut output, &redactions);
// Redact durations like "0ns", "123ms" or "1.23s" to "<duration>"
let duration_regex = regex::Regex::new(r"\d+(\.\d+)?(ns|ms|s)").unwrap();
output = duration_regex.replace_all(&output, "<duration>").into_owned();
// Normalize the ", <duration> saved" suffix in cache hit summaries.
// When tools are fast (e.g., Rust binaries), saved time may be 0ns and the
// runner omits the suffix entirely. Stripping it ensures stable snapshots.
let saved_regex = regex::Regex::new(r",? <duration> saved").unwrap();
output = saved_regex.replace_all(&output, "").into_owned();
// Strip "in total" from verbose performance summary (includes time details
// that may be omitted when saved time is 0).
{
use cow_utils::CowUtils as _;
if let Cow::Owned(replaced) = output.as_str().cow_replace(" in total", "") {
output = replaced;
}
}
// Redact thread counts like "using 10 threads" to "using <n> threads"
let thread_regex = regex::Regex::new(r"using \d+ threads").unwrap();
output = thread_regex.replace_all(&output, "using <n> threads").into_owned();
// Remove Node.js experimental warnings (e.g., Type Stripping warnings)
let node_warning_regex =
regex::Regex::new(r"(?m)^\(node:\d+\) ExperimentalWarning:.*\n?").unwrap();
output = node_warning_regex.replace_all(&output, "").into_owned();
let node_trace_warning_regex = regex::Regex::new(
r"(?m)^\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)\n?",
)
.unwrap();
output = node_trace_warning_regex.replace_all(&output, "").into_owned();
// Remove nondeterministic mise warnings from shell startup in cross-platform runners.
let mise_warning_regex = regex::Regex::new(r"(?m)^mise WARN\s+.*\n?").unwrap();
output = mise_warning_regex.replace_all(&output, "").into_owned();
// Remove ^C echo that Unix terminal drivers emit when ETX (0x03) is written
// to the PTY. Windows ConPTY does not echo it.
{
use cow_utils::CowUtils as _;
if let Cow::Owned(replaced) = output.as_str().cow_replace("^C", "") {
output = replaced;
}
}
// Sort consecutive diagnostic blocks to handle non-deterministic tool output
// (e.g., oxlint reports warnings in arbitrary order due to multi-threading).
// Each block starts with " ! " and ends at the next empty line.
output = sort_diagnostic_blocks(&output);
output
}
#[expect(
clippy::disallowed_types,
reason = "String return required because join produces a String"
)]
fn sort_diagnostic_blocks(output: &str) -> String {
let parts: Vec<&str> = output.split('\n').collect();
let mut result: Vec<&str> = Vec::new();
let mut i = 0;
while i < parts.len() {
if parts[i].starts_with(" ! ") {
let mut blocks: Vec<Vec<&str>> = Vec::new();
loop {
if i >= parts.len() || !parts[i].starts_with(" ! ") {
break;
}
let mut block: Vec<&str> = Vec::new();
while i < parts.len() && !parts[i].is_empty() {
block.push(parts[i]);
i += 1;
}
blocks.push(block);
// Skip the empty line separator between blocks
if i < parts.len() && parts[i].is_empty() {
i += 1;
}
}
blocks.sort();
for (j, block) in blocks.iter().enumerate() {
result.extend_from_slice(block);
// Restore empty line separators (between blocks + trailing)
if j < blocks.len() - 1 || i <= parts.len() {
result.push("");
}
}
} else {
result.push(parts[i]);
i += 1;
}
}
result.join("\n")
}