Skip to main content

zebra_test/
lib.rs

1//! Miscellaneous test code for Zebra.
2#![doc(html_favicon_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-favicon-128.png")]
3#![doc(html_logo_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-icon.png")]
4#![doc(html_root_url = "https://docs.rs/zebra_test")]
5// Each lazy_static variable uses additional recursion
6#![recursion_limit = "512"]
7
8use std::sync::Once;
9
10use color_eyre::section::PanicMessage;
11use once_cell::sync::Lazy;
12use owo_colors::OwoColorize;
13use tracing_error::ErrorLayer;
14use tracing_subscriber::{fmt, prelude::*, EnvFilter};
15
16#[allow(missing_docs)]
17pub mod command;
18
19pub mod mock_service;
20pub mod net;
21pub mod network_addr;
22pub mod prelude;
23pub mod service_extensions;
24pub mod transcript;
25pub mod vectors;
26pub mod zip0143;
27pub mod zip0243;
28pub mod zip0244;
29
30/// A single-threaded Tokio runtime that can be shared between tests.
31/// This runtime should be used for tests that need a single thread for consistent timings.
32///
33/// This shared runtime should be used in tests that use shared background tasks. An example is
34/// with shared global `Lazy<BatchVerifier>` types, because they spawn a background task when they
35/// are first initialized. This background task is stopped when the runtime is shut down, so having
36/// a runtime per test means that only the first test actually manages to successfully use the
37/// background task. Using the shared runtime allows the background task to keep running for the
38/// other tests that also use it.
39///
40/// A shared runtime should not be used in tests that need to pause and resume the Tokio timer.
41/// This is because multiple tests might be sharing the runtime at the same time, so there could be
42/// conflicts with pausing and resuming the timer at incorrect points. Even if only one test runs
43/// at a time, there's a risk of a test finishing while the timer is paused (due to a test failure,
44/// for example) and that means that the next test will already start with an incorrect timer
45/// state.
46pub static SINGLE_THREADED_RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| {
47    tokio::runtime::Builder::new_current_thread()
48        .enable_all()
49        .build()
50        .expect("Failed to create Tokio runtime")
51});
52
53/// A multi-threaded Tokio runtime that can be shared between tests.
54/// This runtime should be used for tests that spawn blocking threads.
55///
56/// See [`SINGLE_THREADED_RUNTIME`] for details.
57pub static MULTI_THREADED_RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| {
58    tokio::runtime::Builder::new_multi_thread()
59        .enable_all()
60        .build()
61        .expect("Failed to create Tokio runtime")
62});
63
64static INIT: Once = Once::new();
65
66/// Initialize global and thread-specific settings for tests,
67/// such as tracing configs, panic hooks, and `cargo insta` settings.
68///
69/// This function should be called at the start of every test.
70///
71/// It returns a drop guard that must be stored in a variable, so that it
72/// gets dropped when the test finishes.
73#[must_use]
74pub fn init() -> impl Drop {
75    // Per-test
76
77    // Settings for threads that snapshots data using `insta`
78
79    let mut settings = insta::Settings::clone_current();
80    settings.set_prepend_module_to_snapshot(false);
81    let drop_guard = settings.bind_to_scope();
82
83    // Globals
84
85    INIT.call_once(|| {
86        let fmt_layer = fmt::layer().with_target(false);
87        // Use the RUST_LOG env var, or by default:
88        //  - warn for most tests, and
89        //  - for some modules, hide expected warn logs
90        let filter_layer = EnvFilter::try_from_default_env()
91            .unwrap_or_else(|_| {
92                // These filters apply when RUST_LOG isn't set
93                EnvFilter::try_new("warn")
94                    .unwrap()
95                    .add_directive("zebra_consensus=error".parse().unwrap())
96                    .add_directive("zebra_network=error".parse().unwrap())
97                    .add_directive("zebra_state=error".parse().unwrap())
98                    .add_directive("zebrad=error".parse().unwrap())
99                    .add_directive("tor_circmgr=error".parse().unwrap())
100            })
101            // These filters apply on top of RUST_LOG.
102            // Avoid adding filters to this list, because users can't override them.
103            //
104            // (There are currently no always-on directives.)
105            ;
106
107        tracing_subscriber::registry()
108            .with(filter_layer)
109            .with(fmt_layer)
110            .with(ErrorLayer::default())
111            .init();
112
113        let hook_result = color_eyre::config::HookBuilder::default()
114            .add_frame_filter(Box::new(|frames| {
115                let mut displayed = std::collections::HashSet::new();
116                let filters = &[
117                    "tokio::",
118                    "<futures_util::",
119                    "std::panic",
120                    "test::run_test_in_process",
121                    "core::ops::function::FnOnce::call_once",
122                    "std::thread::local",
123                    "<core::future::",
124                    "<alloc::boxed::Box",
125                    "<std::panic::AssertUnwindSafe",
126                    "core::result::Result",
127                    "<T as futures_util",
128                    "<tracing_futures::Instrumented",
129                    "test::assert_test_result",
130                    "spandoc::",
131                ];
132
133                frames.retain(|frame| {
134                    let loc = (frame.lineno, &frame.filename);
135                    let inserted = displayed.insert(loc);
136
137                    if !inserted {
138                        return false;
139                    }
140
141                    !filters.iter().any(|f| {
142                        let name = if let Some(name) = frame.name.as_ref() {
143                            name.as_str()
144                        } else {
145                            return true;
146                        };
147
148                        name.starts_with(f)
149                    })
150                });
151            }))
152            .panic_message(SkipTestReturnedErrPanicMessages)
153            .install();
154
155        // Another color-eyre hook might already be installed, for example by a test
156        // harness or by code under test. In that case, keep using the existing hook.
157        if let Err(error) = hook_result {
158            tracing::info!(?error, "color-eyre hook is already installed, keeping it");
159        }
160    });
161
162    drop_guard
163}
164
165/// Initialize globals for tests that need a separate Tokio runtime instance.
166///
167/// This is generally used in proptests, which don't support the `#[tokio::test]` attribute.
168///
169/// If a runtime needs to be shared between tests, use the [`SINGLE_THREADED_RUNTIME`] or
170/// [`MULTI_THREADED_RUNTIME`] instances instead.
171///
172/// See also the [`init`] function, which is called by this function.
173pub fn init_async() -> (tokio::runtime::Runtime, impl Drop) {
174    let drop_guard = init();
175
176    (
177        tokio::runtime::Builder::new_current_thread()
178            .enable_all()
179            .build()
180            .expect("Failed to create Tokio runtime"),
181        drop_guard,
182    )
183}
184
185struct SkipTestReturnedErrPanicMessages;
186
187impl PanicMessage for SkipTestReturnedErrPanicMessages {
188    fn display(
189        &self,
190        pi: &std::panic::PanicHookInfo<'_>,
191        f: &mut std::fmt::Formatter<'_>,
192    ) -> std::fmt::Result {
193        let payload = pi
194            .payload()
195            .downcast_ref::<String>()
196            .map(String::as_str)
197            .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
198            .unwrap_or("<non string panic payload>");
199
200        // skip panic output that is clearly from tests that returned an `Err`
201        // and assume that the test handler has already printed the value inside
202        // of the `Err`.
203        if payload.contains("the test returned a termination value with a non-zero status code") {
204            return write!(f, "---- end of test output ----");
205        }
206
207        writeln!(f, "{}", "\nThe application panicked (crashed).".red())?;
208
209        write!(f, "Message:  ")?;
210        writeln!(f, "{}", payload.cyan())?;
211
212        // If known, print panic location.
213        write!(f, "Location: ")?;
214        if let Some(loc) = pi.location() {
215            write!(f, "{}", loc.file().purple())?;
216            write!(f, ":")?;
217            write!(f, "{}", loc.line().purple())?;
218        } else {
219            write!(f, "<unknown>")?;
220        }
221
222        Ok(())
223    }
224}