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
//! Shared capture and assertion helpers for the configuration discovery tests.
//!
//! [`capture_events`] runs a closure under a capturing subscriber and
//! [`find_event`] locates one emitted event, so the tracing and layer test
//! modules share a single implementation of both.
//!
//! [`EventAssertion`] pairs a captured tracing event with the path under
//! assertion so the discovery tests can check bounded field rendering, privacy,
//! and snapshot normalization without threading repeated `&str`/`&Path`
//! arguments through every helper.
use crate::test_tracing_capture::with_test_subscriber;
use anyhow::{Context, Result, ensure};
use std::path::Path;
use tracing_subscriber::filter::LevelFilter;
use super::diagnostics::path_hash;
/// Run `test` under a TRACE-level capturing subscriber.
///
/// Returns the closure's value alongside every event it emitted, so a test can
/// assert on both the outcome and its instrumentation.
pub(super) fn capture_events<T, E>(
test: impl FnOnce() -> std::result::Result<T, E>,
) -> std::result::Result<(T, Vec<String>), E> {
with_test_subscriber(LevelFilter::TRACE, |captured| {
let value = test()?;
Ok((value, captured.snapshot()))
})
}
/// Return the first captured event containing `message`.
///
/// Fails with the full event list when no event matches, so assertion failures
/// show what was actually emitted.
pub(super) fn find_event<'a>(events: &'a [String], message: &str) -> Result<&'a String> {
events
.iter()
.find(|event| event.contains(message))
.with_context(|| format!("expected event containing {message:?} in {events:?}"))
}
/// Bundles a captured tracing event with the path under assertion so the
/// discovery-test helpers share that context instead of threading repeated
/// `&str`/`&Path` arguments through every call.
pub(super) struct EventAssertion<'a> {
event: &'a str,
path: &'a Path,
}
impl<'a> EventAssertion<'a> {
/// Bind `event` and `path` together for the assertion helpers below.
pub(super) fn new(event: &'a str, path: &'a Path) -> Self {
Self { event, path }
}
/// Assert the event carries the bounded `path_hash` without exposing a path.
pub(super) fn ensure_bounded_path_fields(&self) -> Result<()> {
let hash = path_hash(self.path);
ensure!(
self.event.contains(&format!("path_hash=\"{hash}\""))
|| self.event.contains(&format!("path_hash=Some(\"{hash}\")"))
|| self.event.contains(&format!("path_hash={hash}")),
"event should include path hash for {}: {}",
self.path.display(),
self.event
);
self.ensure_raw_path_absent()?;
Ok(())
}
/// Assert the event never leaks the raw path string or its file name.
pub(super) fn ensure_raw_path_absent(&self) -> Result<()> {
ensure!(
!self.event.contains(self.path.to_string_lossy().as_ref()),
"event should not include raw path {}: {}",
self.path.display(),
self.event
);
if let Some(file_name) = self.path.file_name() {
ensure!(
!self.event.contains(file_name.to_string_lossy().as_ref()),
"event should not include path file name {}: {}",
file_name.to_string_lossy(),
self.event
);
}
Ok(())
}
/// Assert the event leaks neither the raw path nor the `formatted_error`.
pub(super) fn ensure_private_event_fields(&self, formatted_error: &str) -> Result<()> {
self.ensure_raw_path_absent()?;
ensure!(
!self.event.contains(formatted_error),
"event should not include formatted error text: {}",
self.event
);
Ok(())
}
/// Return the event with the runtime path hash replaced by a stable
/// placeholder, after asserting the bounded fields are present.
///
/// The caller performs the `insta` assertion so snapshot names stay bound to
/// the test module rather than this helper module.
pub(super) fn normalize_path_hash(&self) -> Result<String> {
self.ensure_bounded_path_fields()?;
Ok(self.event.replace(&path_hash(self.path), "[path_hash]"))
}
}