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//!
6//! [![Coverage Status](https://coveralls.io/repos/github/facet-rs/facet-testhelpers/badge.svg?branch=main)](https://coveralls.io/github/facet-rs/facet?branch=main)
7//! [![crates.io](https://img.shields.io/crates/v/facet-testhelpers.svg)](https://crates.io/crates/facet-testhelpers)
8//! [![documentation](https://docs.rs/facet-testhelpers/badge.svg)](https://docs.rs/facet-testhelpers)
9//! [![MIT/Apache-2.0 licensed](https://img.shields.io/crates/l/facet-testhelpers.svg)](./LICENSE)
10//! [![Discord](https://img.shields.io/discord/1379550208551026748?logo=discord&label=discord)](https://discord.gg/JhD7CwCJ8F)
11//!
12//! Lightweight test helpers: a log facade that always does tracing (with colors),
13//! and color-backtrace using the btparse backend.
14//!
15//! ## Usage
16//!
17//! Add this to your test files:
18//!
19//! ```rust
20//! #[facet_testhelpers::test]
21//! fn my_test() {
22//!     log::info!("This will be printed with color!");
23//!     // Your test code here
24//! }
25//! ```
26//!
27//! The test macro sets up a simple logger that works with both `cargo test` and `cargo nextest run`.
28//!
29//! ### Recommendation
30//!
31//! While this crate works with regular `cargo test`, we recommend using [`cargo-nextest`](https://nexte.st) for:
32//! - Process-per-test isolation
33//! - Faster parallel test execution
34//! - Better test output and reporting
35//!
36//! Install with:
37//! ```bash
38//! cargo install cargo-nextest
39//! ```
40//!
41//! Then run tests with:
42//! ```bash
43//! cargo nextest run
44//! ```
45//!
46#![doc = include_str!("../readme-footer.md")]
47
48pub use facet_testhelpers_macros::test;
49
50use std::sync::LazyLock;
51use std::time::Instant;
52use tracing_subscriber::filter::Targets;
53use tracing_subscriber::fmt::format::Writer;
54use tracing_subscriber::fmt::time::FormatTime;
55use tracing_subscriber::layer::SubscriberExt;
56use tracing_subscriber::util::SubscriberInitExt;
57
58static START_TIME: LazyLock<Instant> = LazyLock::new(Instant::now);
59
60struct Uptime;
61
62impl FormatTime for Uptime {
63    fn format_time(&self, w: &mut Writer<'_>) -> core::fmt::Result {
64        let elapsed = START_TIME.elapsed();
65        let secs = elapsed.as_secs();
66        let millis = elapsed.subsec_millis();
67        write!(w, "{:4}.{:03}s", secs, millis)
68    }
69}
70
71/// Lazy initialization of the global tracing subscriber.
72///
73/// This ensures the subscriber is set up exactly once, regardless of how many
74/// tests run in the same process.
75static SUBSCRIBER_INIT: LazyLock<()> = LazyLock::new(|| {
76    // Force start time initialization
77    let _ = *START_TIME;
78
79    let verbosity = color_backtrace::Verbosity::Medium;
80
81    // Install color-backtrace for better panic output (with forced backtraces and colors)
82    color_backtrace::BacktracePrinter::new()
83        .verbosity(verbosity)
84        .add_frame_filter(Box::new(|frames| {
85            frames.retain(|frame| {
86                let dominated_by_noise = |name: &str| {
87                    // Test harness internals
88                    name.starts_with("test::run_test")
89                        || name.starts_with("test::__rust_begin_short_backtrace")
90                        // Panic/unwind machinery
91                        || name.starts_with("std::panicking::")
92                        || name.starts_with("std::panic::")
93                        || name.starts_with("core::panicking::")
94                        // Thread spawning
95                        || name.starts_with("std::thread::Builder::spawn_unchecked_")
96                        || name.starts_with("std::sys::thread::")
97                        || name.starts_with("std::sys::backtrace::")
98                        // FnOnce::call_once trampolines in std/core/alloc
99                        || name.starts_with("core::ops::function::FnOnce::call_once")
100                        || name.starts_with("<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once")
101                        // AssertUnwindSafe wrapper
102                        || name.starts_with("<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once")
103                        // Low-level threading primitives
104                        || name.starts_with("__pthread")
105                };
106                match &frame.name {
107                    Some(name) => !dominated_by_noise(name),
108                    None => true,
109                }
110            })
111        }))
112        .install(Box::new(termcolor::StandardStream::stderr(
113            termcolor::ColorChoice::AlwaysAnsi,
114        )));
115
116    let filter = std::env::var("FACET_LOG")
117        .ok()
118        .and_then(|s| s.parse::<Targets>().ok())
119        .unwrap_or_else(|| {
120            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)");
121            Targets::new().with_default(tracing::Level::DEBUG)
122        });
123
124    fn is_set_to_1(key: &str) -> bool {
125        match std::env::var(key) {
126            Ok(val) => val == "1",
127            Err(_) => false,
128        }
129    }
130
131    let is_verbose = is_set_to_1("FACET_LOG_VERBOSE");
132    if !is_verbose {
133        eprintln!(
134            "You can set FACET_LOG_VERBOSE=1 to see targets, files and line numbers for each tracing message"
135        );
136    }
137
138    tracing_subscriber::registry()
139        .with(
140            tracing_subscriber::fmt::layer()
141                .with_ansi(true)
142                .with_timer(Uptime)
143                .with_target(false)
144                .with_level(true)
145                .with_file(is_verbose)
146                .with_line_number(is_verbose)
147                .compact(),
148        )
149        .with(filter)
150        .try_init()
151        .ok();
152});
153
154/// Set up a tracing subscriber for tests.
155///
156/// This function ensures the subscriber is initialized exactly once using
157/// [`LazyLock`], making it safe to use with both `cargo test` and
158/// `cargo nextest run`.
159///
160/// # Recommendation
161///
162/// While this works with regular `cargo test`, we recommend using
163/// `cargo nextest run` for:
164/// - Process-per-test isolation
165/// - Faster parallel test execution
166/// - Better test output and reporting
167///
168/// Install nextest with: `cargo install cargo-nextest`
169///
170/// For more information, visit: <https://nexte.st>
171pub fn setup() {
172    // Print a helpful message if not using nextest
173    let is_nextest = std::env::var("NEXTEST").as_deref() == Ok("1");
174    if !is_nextest {
175        static NEXTEST_WARNING: LazyLock<()> = LazyLock::new(|| {
176            eprintln!(
177                "💡 Tip: Consider using `cargo nextest run` for better test output and performance."
178            );
179            eprintln!("   Install with: cargo install cargo-nextest");
180            eprintln!("   More info: https://nexte.st");
181            eprintln!();
182        });
183        #[allow(clippy::let_unit_value)]
184        let _ = *NEXTEST_WARNING;
185    }
186
187    // Ensure the subscriber is initialized
188    #[allow(clippy::let_unit_value)]
189    let _ = *SUBSCRIBER_INIT;
190}
191
192/// An error type that panics when it's built (such as when you use `?`
193/// to coerce to it)
194#[derive(Debug)]
195pub struct IPanic;
196
197impl<E> From<E> for IPanic
198where
199    E: core::error::Error + Send + Sync,
200{
201    #[track_caller]
202    fn from(value: E) -> Self {
203        panic!("from: {}: {value}", core::panic::Location::caller())
204    }
205}