Skip to main content

oopsie_core/
test_utils.rs

1//! Shared helpers for the integration-test suites of the consumer crates.
2//!
3//! Backtrace and spantrace snapshots are platform- and toolchain-specific, so
4//! each snapshot is blessed and asserted only on the toolchain that produced
5//! it; the variance lives in the snapshot *filename* (channel + target triple)
6//! rather than in a cross-toolchain normalization battery. The filters here are
7//! therefore deliberately light: they only erase build-to-build noise
8//! (compilation hashes, line/column numbers, machine-specific paths).
9
10#[cfg(feature = "tracing")]
11use tracing_subscriber::prelude::*;
12
13#[doc(hidden)]
14pub mod __private {
15    pub use konst;
16    pub use target_triple::TARGET;
17}
18
19/// The toolchain channel (`stable` or `unstable`) embedded in snapshot file
20/// names so toolchain-specific snapshots stay in separate files.
21// Keyed on the granular feature (not the `unstable` umbrella) because the
22// Provider-API trace surfacing it gates is what actually changes snapshot
23// content.
24pub const CHANNEL: &str = if cfg!(feature = "unstable-error-generic-member-access") {
25    "unstable"
26} else {
27    "stable"
28};
29
30#[cfg(feature = "unstable-error-generic-member-access")]
31const _: () = assert!(matches!(CHANNEL.as_bytes(), b"unstable"));
32
33/// Build a snapshot name from a base label plus the channel and target triple,
34/// keeping platform- and toolchain-specific snapshots in separate files.
35#[macro_export]
36macro_rules! snap_name {
37    ($name:literal) => {{
38        $crate::test_utils::__private::konst::string::str_join!(
39            "_",
40            &[
41                $name,
42                $crate::test_utils::CHANNEL,
43                $crate::test_utils::__private::TARGET
44            ]
45        )
46    }};
47}
48
49/// Install a test subscriber with an `ErrorLayer` so span traces are captured
50/// for the duration of the returned guard.
51#[cfg(feature = "tracing")]
52#[must_use]
53pub fn init_test_subscriber() -> tracing::subscriber::DefaultGuard {
54    #[cfg(feature = "serde")]
55    let error_layer = crate::tracing::json_error_layer();
56    #[cfg(not(feature = "serde"))]
57    let error_layer = tracing_error::ErrorLayer::default();
58    let subscriber = tracing_subscriber::registry().with(error_layer);
59    tracing::subscriber::set_default(subscriber)
60}
61
62/// Install a test subscriber *without* an `ErrorLayer`, so captured span traces
63/// report [`Unsupported`](crate::SpanTraceStatus::Unsupported) — distinct from
64/// the no-subscriber [`Empty`](crate::SpanTraceStatus::Empty) case — for the
65/// duration of the returned guard.
66#[cfg(feature = "tracing")]
67#[must_use]
68pub fn init_test_subscriber_without_error_layer() -> tracing::subscriber::DefaultGuard {
69    tracing::subscriber::set_default(tracing_subscriber::registry())
70}
71
72/// Force backtrace capture on the current thread so snapshots are deterministic
73/// regardless of the ambient `RUST_BACKTRACE` environment.
74pub fn force_backtrace() {
75    crate::set_rust_backtrace_override(crate::RustBacktrace::Enabled);
76}
77
78/// `insta` snapshot redaction profiles that erase build-to-build noise
79/// (compilation hashes, line/column numbers, machine-specific paths) from
80/// backtrace and spantrace snapshots.
81pub mod settings {
82    use std::{
83        env,
84        path::{Path, PathBuf},
85        process::Command,
86        sync::LazyLock,
87    };
88
89    /// `insta::Settings` carrying the light, build-to-build normalization filters
90    /// shared by every backtrace/spantrace snapshot. Bind with [`insta::Settings::bind`].
91    #[must_use]
92    pub fn backtrace() -> insta::Settings {
93        static RUSTC_SYSROOT: LazyLock<String> = LazyLock::new(|| {
94            String::from_utf8(
95                Command::new("rustc")
96                    .arg("--print")
97                    .arg("sysroot")
98                    .output()
99                    .expect("failed to run rustc")
100                    .stdout,
101            )
102            .expect("invalid UTF-8 in rustc sysroot")
103            .trim()
104            .to_owned()
105        });
106
107        static WORKSPACE_ROOT: LazyLock<String> = LazyLock::new(|| {
108            Path::new(env!("CARGO_MANIFEST_DIR"))
109                .parent()
110                .unwrap()
111                .parent()
112                .unwrap()
113                .to_string_lossy()
114                .into_owned()
115        });
116
117        static CARGO_HOME: LazyLock<String> = LazyLock::new(|| {
118            env::var("CARGO_HOME")
119                .map_or_else(
120                    |_| {
121                        PathBuf::from(env::var("HOME").expect("HOME environment variable not set"))
122                            .join(".cargo")
123                    },
124                    PathBuf::from,
125                )
126                .to_string_lossy()
127                .into_owned()
128        });
129
130        let mut settings = insta::Settings::clone_current();
131        settings.add_filter(r"\[[0-9a-f]{7,16}\]", "[[HASH]]");
132        settings.add_filter(r"::h[0-9a-f]{7,16}\b", "::h[HASH]");
133        settings.add_filter(r"\/[a-f0-9]+\/", "/[HASH]/");
134        settings.add_filter(r"rs:\d+(:\d+)?", "rs:[LOC]");
135        settings.add_filter(&WORKSPACE_ROOT, "[WORKSPACE]");
136        settings.add_filter(&RUSTC_SYSROOT, "[SYS_ROOT]");
137        settings.add_filter(&CARGO_HOME, "[CARGO_HOME]/");
138        // Stdlib path normalization: local `[SYS_ROOT]/lib/rustlib/src/rust/library/`
139        // and CI `/rustc/[HASH]/library/` both → `[STDLIB]/library/`.
140        settings.add_filter(
141            r"\[SYS_ROOT\]/lib/rustlib/src/rust/library/",
142            "[STDLIB]/library/",
143        );
144        settings.add_filter(r"/rustc/\[HASH\]/library/", "[STDLIB]/library/");
145        // JSON snapshots carry line/column as numeric fields rather than `rs:N:C`.
146        settings.add_filter(r#""line":\s*\d+"#, r#""line": 42"#);
147        settings.add_filter(r#""column":\s*\d+"#, r#""column": 69"#);
148        // macOS test threads bottom out in a libc frame whose exact symbol
149        // varies between runs; the render path peels this OS tail, but the
150        // erased path serializes raw frames, so normalize it here.
151        settings.add_filter(r"__pthread\w*", "[OS_TAIL]");
152        settings
153    }
154}
155
156/// Run a block with a named [`settings`] redaction profile bound, so the
157/// snapshots asserted inside it use the shared normalization filters.
158#[macro_export]
159macro_rules! redact {
160    ($name:ident, $bl:block) => {
161        $crate::test_utils::settings::$name().bind(|| $bl)
162    };
163}