Skip to content

Commit beaba46

Browse files
authored
[5/n] [test-utils] turn redactor into the builder pattern (#6786)
In an upcoming PR I want to redact everything except UUIDs. The most convenient way to do that is to use the builder pattern for the redactor.
1 parent 93d6974 commit beaba46

File tree

3 files changed

+109
-120
lines changed

3 files changed

+109
-120
lines changed

dev-tools/omdb/tests/test_all_output.rs

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ use nexus_types::deployment::Blueprint;
1616
use nexus_types::deployment::SledFilter;
1717
use nexus_types::deployment::UnstableReconfiguratorState;
1818
use omicron_test_utils::dev::test_cmds::path_to_executable;
19-
use omicron_test_utils::dev::test_cmds::redact_extra;
2019
use omicron_test_utils::dev::test_cmds::run_command;
21-
use omicron_test_utils::dev::test_cmds::ExtraRedactions;
20+
use omicron_test_utils::dev::test_cmds::Redactor;
2221
use slog_error_chain::InlineErrorChain;
2322
use std::fmt::Write;
2423
use std::net::IpAddr;
@@ -203,19 +202,21 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) {
203202
// ControlPlaneTestContext.
204203
];
205204

206-
let mut redactions = ExtraRedactions::new();
207-
redactions
208-
.variable_length("tmp_path", tmppath.as_str())
209-
.fixed_length("blueprint_id", &initial_blueprint_id)
210-
.variable_length(
205+
let mut redactor = Redactor::default();
206+
redactor
207+
.extra_variable_length("tmp_path", tmppath.as_str())
208+
.extra_fixed_length("blueprint_id", &initial_blueprint_id)
209+
.extra_variable_length(
211210
"cockroachdb_fingerprint",
212211
&initial_blueprint.cockroachdb_fingerprint,
213212
);
213+
214214
let crdb_version =
215215
initial_blueprint.cockroachdb_setting_preserve_downgrade.to_string();
216216
if initial_blueprint.cockroachdb_setting_preserve_downgrade.is_set() {
217-
redactions.variable_length("cockroachdb_version", &crdb_version);
217+
redactor.extra_variable_length("cockroachdb_version", &crdb_version);
218218
}
219+
219220
for args in invocations {
220221
println!("running commands with args: {:?}", args);
221222
let p = postgres_url.to_string();
@@ -234,7 +235,7 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) {
234235
},
235236
&cmd_path,
236237
args,
237-
Some(&redactions),
238+
&redactor,
238239
)
239240
.await;
240241
}
@@ -444,14 +445,7 @@ async fn do_run<F>(
444445
) where
445446
F: FnOnce(Exec) -> Exec + Send + 'static,
446447
{
447-
do_run_extra(
448-
output,
449-
modexec,
450-
cmd_path,
451-
args,
452-
Some(&ExtraRedactions::new()),
453-
)
454-
.await;
448+
do_run_extra(output, modexec, cmd_path, args, &Redactor::default()).await;
455449
}
456450

457451
async fn do_run_no_redactions<F>(
@@ -462,30 +456,23 @@ async fn do_run_no_redactions<F>(
462456
) where
463457
F: FnOnce(Exec) -> Exec + Send + 'static,
464458
{
465-
do_run_extra(output, modexec, cmd_path, args, None).await;
459+
do_run_extra(output, modexec, cmd_path, args, &Redactor::noop()).await;
466460
}
467461

468462
async fn do_run_extra<F>(
469463
output: &mut String,
470464
modexec: F,
471465
cmd_path: &Path,
472466
args: &[&str],
473-
extra_redactions: Option<&ExtraRedactions<'_>>,
467+
redactor: &Redactor<'_>,
474468
) where
475469
F: FnOnce(Exec) -> Exec + Send + 'static,
476470
{
477471
write!(
478472
output,
479473
"EXECUTING COMMAND: {} {:?}\n",
480474
cmd_path.file_name().expect("missing command").to_string_lossy(),
481-
args.iter()
482-
.map(|r| {
483-
extra_redactions.map_or_else(
484-
|| r.to_string(),
485-
|redactions| redact_extra(r, redactions),
486-
)
487-
})
488-
.collect::<Vec<_>>()
475+
args.iter().map(|r| redactor.do_redact(r)).collect::<Vec<_>>()
489476
)
490477
.unwrap();
491478

@@ -521,21 +508,11 @@ async fn do_run_extra<F>(
521508
write!(output, "termination: {:?}\n", exit_status).unwrap();
522509
write!(output, "---------------------------------------------\n").unwrap();
523510
write!(output, "stdout:\n").unwrap();
524-
525-
if let Some(extra_redactions) = extra_redactions {
526-
output.push_str(&redact_extra(&stdout_text, extra_redactions));
527-
} else {
528-
output.push_str(&stdout_text);
529-
}
511+
output.push_str(&redactor.do_redact(&stdout_text));
530512

531513
write!(output, "---------------------------------------------\n").unwrap();
532514
write!(output, "stderr:\n").unwrap();
533-
534-
if let Some(extra_redactions) = extra_redactions {
535-
output.push_str(&redact_extra(&stderr_text, extra_redactions));
536-
} else {
537-
output.push_str(&stderr_text);
538-
}
515+
output.push_str(&redactor.do_redact(&stderr_text));
539516

540517
write!(output, "=============================================\n").unwrap();
541518
}

dev-tools/reconfigurator-cli/tests/test_basic.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use omicron_test_utils::dev::poll::wait_for_condition;
1919
use omicron_test_utils::dev::poll::CondCheckError;
2020
use omicron_test_utils::dev::test_cmds::assert_exit_code;
2121
use omicron_test_utils::dev::test_cmds::path_to_executable;
22-
use omicron_test_utils::dev::test_cmds::redact_variable;
2322
use omicron_test_utils::dev::test_cmds::run_command;
23+
use omicron_test_utils::dev::test_cmds::Redactor;
2424
use omicron_test_utils::dev::test_cmds::EXIT_SUCCESS;
2525
use omicron_uuid_kinds::SledUuid;
2626
use slog::debug;
@@ -43,7 +43,7 @@ fn test_basic() {
4343
let exec = Exec::cmd(path_to_cli()).arg("tests/input/cmds.txt");
4444
let (exit_status, stdout_text, stderr_text) = run_command(exec);
4545
assert_exit_code(exit_status, EXIT_SUCCESS, &stderr_text);
46-
let stdout_text = redact_variable(&stdout_text);
46+
let stdout_text = Redactor::default().do_redact(&stdout_text);
4747
assert_contents("tests/output/cmd-stdout", &stdout_text);
4848
assert_contents("tests/output/cmd-stderr", &stderr_text);
4949
}

test-utils/src/dev/test_cmds.rs

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,83 @@ pub fn error_for_enoent() -> String {
125125
/// invocation to invocation (e.g., assigned TCP port numbers, timestamps)
126126
///
127127
/// This allows use to use expectorate to verify the shape of the CLI output.
128-
pub fn redact_variable(input: &str) -> String {
128+
#[derive(Clone, Debug)]
129+
pub struct Redactor<'a> {
130+
basic: bool,
131+
uuids: bool,
132+
extra: Vec<(&'a str, String)>,
133+
}
134+
135+
impl Default for Redactor<'_> {
136+
fn default() -> Self {
137+
Self { basic: true, uuids: true, extra: Vec::new() }
138+
}
139+
}
140+
141+
impl<'a> Redactor<'a> {
142+
/// Create a new redactor that does not do any redactions.
143+
pub fn noop() -> Self {
144+
Self { basic: false, uuids: false, extra: Vec::new() }
145+
}
146+
147+
pub fn basic(&mut self, basic: bool) -> &mut Self {
148+
self.basic = basic;
149+
self
150+
}
151+
152+
pub fn uuids(&mut self, uuids: bool) -> &mut Self {
153+
self.uuids = uuids;
154+
self
155+
}
156+
157+
pub fn extra_fixed_length(
158+
&mut self,
159+
name: &str,
160+
text_to_redact: &'a str,
161+
) -> &mut Self {
162+
// Use the same number of chars as the number of bytes in
163+
// text_to_redact. We're almost entirely in ASCII-land so they're the
164+
// same, and getting the length right is nice but doesn't matter for
165+
// correctness.
166+
//
167+
// A technically more correct impl would use unicode-width, but ehhh.
168+
let replacement = fill_redaction_text(name, text_to_redact.len());
169+
self.extra.push((text_to_redact, replacement));
170+
self
171+
}
172+
173+
pub fn extra_variable_length(
174+
&mut self,
175+
name: &str,
176+
text_to_redact: &'a str,
177+
) -> &mut Self {
178+
let replacement = format!("<{}_REDACTED>", name.to_uppercase());
179+
self.extra.push((text_to_redact, replacement));
180+
self
181+
}
182+
183+
pub fn do_redact(&self, input: &str) -> String {
184+
// Perform extra redactions at the beginning, not the end. This is because
185+
// some of the built-in redactions in redact_variable might match a
186+
// substring of something that should be handled by extra_redactions (e.g.
187+
// a temporary path).
188+
let mut s = input.to_owned();
189+
for (name, replacement) in &self.extra {
190+
s = s.replace(name, replacement);
191+
}
192+
193+
if self.basic {
194+
s = redact_basic(&s);
195+
}
196+
if self.uuids {
197+
s = redact_uuids(&s);
198+
}
199+
200+
s
201+
}
202+
}
203+
204+
fn redact_basic(input: &str) -> String {
129205
// Replace TCP port numbers. We include the localhost
130206
// characters to avoid catching any random sequence of numbers.
131207
let s = regex::Regex::new(r"\[::1\]:\d{4,5}")
@@ -141,19 +217,6 @@ pub fn redact_variable(input: &str) -> String {
141217
.replace_all(&s, "127.0.0.1:REDACTED_PORT")
142218
.to_string();
143219

144-
// Replace uuids.
145-
//
146-
// The length of a UUID is 32 nibbles for the hex encoding of a u128 + 4
147-
// dashes = 36.
148-
const UUID_LEN: usize = 36;
149-
let s = regex::Regex::new(
150-
"[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-\
151-
[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}",
152-
)
153-
.unwrap()
154-
.replace_all(&s, fill_redaction_text("uuid", UUID_LEN))
155-
.to_string();
156-
157220
// Replace timestamps.
158221
//
159222
// Format: RFC 3339 (ISO 8601)
@@ -213,63 +276,14 @@ pub fn redact_variable(input: &str) -> String {
213276
s
214277
}
215278

216-
/// Redact text from a string, allowing for extra redactions to be specified.
217-
pub fn redact_extra(
218-
input: &str,
219-
extra_redactions: &ExtraRedactions<'_>,
220-
) -> String {
221-
// Perform extra redactions at the beginning, not the end. This is because
222-
// some of the built-in redactions in redact_variable might match a
223-
// substring of something that should be handled by extra_redactions (e.g.
224-
// a temporary path).
225-
let mut s = input.to_owned();
226-
for (name, replacement) in &extra_redactions.redactions {
227-
s = s.replace(name, replacement);
228-
}
229-
redact_variable(&s)
230-
}
231-
232-
/// Represents a list of extra redactions for [`redact_variable`].
233-
///
234-
/// Extra redactions are applied in-order, before any builtin redactions.
235-
#[derive(Clone, Debug, Default)]
236-
pub struct ExtraRedactions<'a> {
237-
// A pair of redaction and replacement strings.
238-
redactions: Vec<(&'a str, String)>,
239-
}
240-
241-
impl<'a> ExtraRedactions<'a> {
242-
pub fn new() -> Self {
243-
Self { redactions: Vec::new() }
244-
}
245-
246-
pub fn fixed_length(
247-
&mut self,
248-
name: &str,
249-
text_to_redact: &'a str,
250-
) -> &mut Self {
251-
// Use the same number of chars as the number of bytes in
252-
// text_to_redact. We're almost entirely in ASCII-land so they're the
253-
// same, and getting the length right is nice but doesn't matter for
254-
// correctness.
255-
//
256-
// A technically more correct impl would use unicode-width, but ehhh.
257-
let replacement = fill_redaction_text(name, text_to_redact.len());
258-
self.redactions.push((text_to_redact, replacement));
259-
self
260-
}
261-
262-
pub fn variable_length(
263-
&mut self,
264-
name: &str,
265-
text_to_redact: &'a str,
266-
) -> &mut Self {
267-
let gen = format!("<{}_REDACTED>", name.to_uppercase());
268-
let replacement = gen.to_string();
269-
270-
self.redactions.push((text_to_redact, replacement));
271-
self
272-
}
279+
fn redact_uuids(input: &str) -> String {
280+
// The length of a UUID is 32 nibbles for the hex encoding of a u128 + 4
281+
// dashes = 36.
282+
const UUID_LEN: usize = 36;
283+
regex::Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
284+
.unwrap()
285+
.replace_all(&input, fill_redaction_text("uuid", UUID_LEN))
286+
.to_string()
273287
}
274288

275289
fn fill_redaction_text(name: &str, text_to_redact_len: usize) -> String {
@@ -309,13 +323,11 @@ mod tests {
309323
let input = "time: 123ms, path: /var/tmp/tmp.456ms123s, \
310324
path2: /short, \
311325
path3: /variable-length/path";
312-
let actual = redact_extra(
313-
input,
314-
ExtraRedactions::new()
315-
.fixed_length("tp", "/var/tmp/tmp.456ms123s")
316-
.fixed_length("short_redact", "/short")
317-
.variable_length("variable", "/variable-length/path"),
318-
);
326+
let actual = Redactor::default()
327+
.extra_fixed_length("tp", "/var/tmp/tmp.456ms123s")
328+
.extra_fixed_length("short_redact", "/short")
329+
.extra_variable_length("variable", "/variable-length/path")
330+
.do_redact(input);
319331
assert_eq!(
320332
actual,
321333
"time: <REDACTED DURATION>ms, path: ....<REDACTED_TP>....., \
@@ -347,7 +359,7 @@ mod tests {
347359
for time in times {
348360
let input = format!("{:?}", time);
349361
assert_eq!(
350-
redact_variable(&input),
362+
Redactor::default().do_redact(&input),
351363
"<REDACTED_TIMESTAMP>",
352364
"Failed to redact {:?}",
353365
time

0 commit comments

Comments
 (0)