Skip to main content

facet_testhelpers/
lib.rs

1#![warn(missing_docs)]
2#![warn(clippy::std_instead_of_core)]
3#![warn(clippy::std_instead_of_alloc)]
4#![forbid(unsafe_code)]
5#![doc = include_str!("../README.md")]
6
7pub use facet_testhelpers_macros::test;
8
9use std::sync::LazyLock;
10use std::time::Instant;
11use tracing_subscriber::filter::Targets;
12use tracing_subscriber::fmt::format::Writer;
13use tracing_subscriber::fmt::time::FormatTime;
14use tracing_subscriber::layer::SubscriberExt;
15use tracing_subscriber::util::SubscriberInitExt;
16
17static START_TIME: LazyLock<Instant> = LazyLock::new(Instant::now);
18
19struct Uptime;
20
21impl FormatTime for Uptime {
22    fn format_time(&self, w: &mut Writer<'_>) -> core::fmt::Result {
23        let elapsed = START_TIME.elapsed();
24        let secs = elapsed.as_secs();
25        let millis = elapsed.subsec_millis();
26        write!(w, "{:4}.{:03}s", secs, millis)
27    }
28}
29
30/// Lazy initialization of the global tracing subscriber.
31///
32/// This ensures the subscriber is set up exactly once, regardless of how many
33/// tests run in the same process.
34static SUBSCRIBER_INIT: LazyLock<()> = LazyLock::new(|| {
35    // Force start time initialization
36    let _ = *START_TIME;
37
38    #[cfg(miri)]
39    let verbosity = color_backtrace::Verbosity::Medium;
40
41    #[cfg(not(miri))]
42    let verbosity = color_backtrace::Verbosity::Full;
43
44    // Install color-backtrace for better panic output (with forced backtraces and colors)
45    color_backtrace::BacktracePrinter::new()
46        .verbosity(verbosity)
47        .add_frame_filter(Box::new(|frames| {
48            frames.retain(|frame| {
49                let dominated_by_noise = |name: &str| {
50                    // Test harness internals
51                    name.starts_with("test::run_test")
52                        || name.starts_with("test::__rust_begin_short_backtrace")
53                        // Panic/unwind machinery
54                        || name.starts_with("std::panicking::")
55                        || name.starts_with("std::panic::")
56                        || name.starts_with("core::panicking::")
57                        // Thread spawning
58                        || name.starts_with("std::thread::Builder::spawn_unchecked_")
59                        || name.starts_with("std::sys::thread::")
60                        || name.starts_with("std::sys::backtrace::")
61                        // FnOnce::call_once trampolines in std/core/alloc
62                        || name.starts_with("core::ops::function::FnOnce::call_once")
63                        || name.starts_with("<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once")
64                        // AssertUnwindSafe wrapper
65                        || name.starts_with("<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once")
66                        // Low-level threading primitives
67                        || name.starts_with("__pthread")
68                };
69                match &frame.name {
70                    Some(name) => !dominated_by_noise(name),
71                    None => true,
72                }
73            })
74        }))
75        .install(Box::new(termcolor::StandardStream::stderr(
76            termcolor::ColorChoice::AlwaysAnsi,
77        )));
78
79    let filter = std::env::var("FACET_LOG")
80        .ok()
81        .and_then(|s| s.parse::<Targets>().ok())
82        .unwrap_or_else(|| {
83            eprintln!("Assuming FACET_LOG=debug (feel free to set the $FACET_LOG env var to override tracing filters) (note: $RUST_LOG doesn't do anything)");
84            Targets::new().with_default(tracing::Level::DEBUG)
85        });
86
87    tracing_subscriber::registry()
88        .with(
89            tracing_subscriber::fmt::layer()
90                .with_ansi(true)
91                .with_timer(Uptime)
92                .with_target(false)
93                .with_level(true)
94                // .with_file(true)
95                // .with_line_number(true)
96                .with_file(false)
97                .with_line_number(false)
98                .compact(),
99        )
100        .with(filter)
101        .try_init()
102        .ok();
103});
104
105/// Set up a tracing subscriber for tests.
106///
107/// This function ensures the subscriber is initialized exactly once using
108/// [`LazyLock`], making it safe to use with both `cargo test` and
109/// `cargo nextest run`.
110///
111/// # Recommendation
112///
113/// While this works with regular `cargo test`, we recommend using
114/// `cargo nextest run` for:
115/// - Process-per-test isolation
116/// - Faster parallel test execution
117/// - Better test output and reporting
118///
119/// Install nextest with: `cargo install cargo-nextest`
120///
121/// For more information, visit: <https://nexte.st>
122pub fn setup() {
123    // Print a helpful message if not using nextest
124    let is_nextest = std::env::var("NEXTEST").as_deref() == Ok("1");
125    if !is_nextest {
126        static NEXTEST_WARNING: LazyLock<()> = LazyLock::new(|| {
127            eprintln!(
128                "💡 Tip: Consider using `cargo nextest run` for better test output and performance."
129            );
130            eprintln!("   Install with: cargo install cargo-nextest");
131            eprintln!("   More info: https://nexte.st");
132            eprintln!();
133        });
134        #[allow(clippy::let_unit_value)]
135        let _ = *NEXTEST_WARNING;
136    }
137
138    // Ensure the subscriber is initialized
139    #[allow(clippy::let_unit_value)]
140    let _ = *SUBSCRIBER_INIT;
141}
142
143/// An error type that panics when it's built (such as when you use `?`
144/// to coerce to it)
145#[derive(Debug)]
146pub struct IPanic;
147
148impl<E> From<E> for IPanic
149where
150    E: core::error::Error + Send + Sync,
151{
152    #[track_caller]
153    fn from(value: E) -> Self {
154        panic!("from: {}: {value}", core::panic::Location::caller())
155    }
156}