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//! [](https://coveralls.io/github/facet-rs/facet?branch=main)
7//! [](https://crates.io/crates/facet-testhelpers)
8//! [](https://docs.rs/facet-testhelpers)
9//! [](./LICENSE)
10//! [](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}