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    let verbosity = color_backtrace::Verbosity::Medium;
39
40    // Install color-backtrace for better panic output (with forced backtraces and colors)
41    color_backtrace::BacktracePrinter::new()
42        .verbosity(verbosity)
43        .add_frame_filter(Box::new(|frames| {
44            frames.retain(|frame| {
45                let dominated_by_noise = |name: &str| {
46                    // Test harness internals
47                    name.starts_with("test::run_test")
48                        || name.starts_with("test::__rust_begin_short_backtrace")
49                        // Panic/unwind machinery
50                        || name.starts_with("std::panicking::")
51                        || name.starts_with("std::panic::")
52                        || name.starts_with("core::panicking::")
53                        // Thread spawning
54                        || name.starts_with("std::thread::Builder::spawn_unchecked_")
55                        || name.starts_with("std::sys::thread::")
56                        || name.starts_with("std::sys::backtrace::")
57                        // FnOnce::call_once trampolines in std/core/alloc
58                        || name.starts_with("core::ops::function::FnOnce::call_once")
59                        || name.starts_with("<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once")
60                        // AssertUnwindSafe wrapper
61                        || name.starts_with("<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once")
62                        // Low-level threading primitives
63                        || name.starts_with("__pthread")
64                };
65                match &frame.name {
66                    Some(name) => !dominated_by_noise(name),
67                    None => true,
68                }
69            })
70        }))
71        .install(Box::new(termcolor::StandardStream::stderr(
72            termcolor::ColorChoice::AlwaysAnsi,
73        )));
74
75    let filter = std::env::var("FACET_LOG")
76        .ok()
77        .and_then(|s| s.parse::<Targets>().ok())
78        .unwrap_or_else(|| {
79            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)");
80            Targets::new().with_default(tracing::Level::DEBUG)
81        });
82
83    fn is_set_to_1(key: &str) -> bool {
84        match std::env::var(key) {
85            Ok(val) => val == "1",
86            Err(_) => false,
87        }
88    }
89
90    let is_verbose = is_set_to_1("FACET_LOG_VERBOSE");
91    if !is_verbose {
92        eprintln!(
93            "You can set FACET_LOG_VERBOSE=1 to see targets, files and line numbers for each tracing message"
94        );
95    }
96
97    tracing_subscriber::registry()
98        .with(
99            tracing_subscriber::fmt::layer()
100                .with_ansi(true)
101                .with_timer(Uptime)
102                .with_target(false)
103                .with_level(true)
104                .with_file(is_verbose)
105                .with_line_number(is_verbose)
106                .compact(),
107        )
108        .with(filter)
109        .try_init()
110        .ok();
111});
112
113/// Set up a tracing subscriber for tests.
114///
115/// This function ensures the subscriber is initialized exactly once using
116/// [`LazyLock`], making it safe to use with both `cargo test` and
117/// `cargo nextest run`.
118///
119/// # Recommendation
120///
121/// While this works with regular `cargo test`, we recommend using
122/// `cargo nextest run` for:
123/// - Process-per-test isolation
124/// - Faster parallel test execution
125/// - Better test output and reporting
126///
127/// Install nextest with: `cargo install cargo-nextest`
128///
129/// For more information, visit: <https://nexte.st>
130pub fn setup() {
131    // Print a helpful message if not using nextest
132    let is_nextest = std::env::var("NEXTEST").as_deref() == Ok("1");
133    if !is_nextest {
134        static NEXTEST_WARNING: LazyLock<()> = LazyLock::new(|| {
135            eprintln!(
136                "💡 Tip: Consider using `cargo nextest run` for better test output and performance."
137            );
138            eprintln!("   Install with: cargo install cargo-nextest");
139            eprintln!("   More info: https://nexte.st");
140            eprintln!();
141        });
142        #[allow(clippy::let_unit_value)]
143        let _ = *NEXTEST_WARNING;
144    }
145
146    // Ensure the subscriber is initialized
147    #[allow(clippy::let_unit_value)]
148    let _ = *SUBSCRIBER_INIT;
149}
150
151/// An error type that panics when it's built (such as when you use `?`
152/// to coerce to it)
153#[derive(Debug)]
154pub struct IPanic;
155
156impl<E> From<E> for IPanic
157where
158    E: core::error::Error + Send + Sync,
159{
160    #[track_caller]
161    fn from(value: E) -> Self {
162        panic!("from: {}: {value}", core::panic::Location::caller())
163    }
164}