ssstar_testing/
logging.rs

1//! Logging in tests is important for troubleshooting, but works very differently then in
2//! production.
3use crate::Result;
4use std::{
5    cell::RefCell,
6    future::Future,
7    io::Write,
8    panic::AssertUnwindSafe,
9    sync::{Arc, Mutex},
10    time::Duration,
11};
12use tracing_subscriber::fmt::MakeWriter;
13
14/// An implementation of `MakeWriter` that captures all log events for a single test
15#[derive(Clone)]
16struct TestWriter {
17    log_events: Arc<Mutex<Vec<u8>>>,
18}
19
20impl TestWriter {
21    fn new() -> Self {
22        Self {
23            log_events: Arc::new(Mutex::new(Vec::<u8>::new())),
24        }
25    }
26
27    /// Clear the writer's buffer, returning the current contents as a string
28    /// Panics of non-UTF8 text has been written to the buffer
29    fn take_string(&self) -> String {
30        let mut guard = self.log_events.lock().unwrap();
31
32        let buffer: Vec<u8> = std::mem::take(&mut guard);
33
34        String::from_utf8(buffer).unwrap()
35    }
36}
37
38impl<'a> Write for &'a TestWriter {
39    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
40        let mut guard = self.log_events.lock().unwrap();
41
42        // Vec already implements Write
43        guard.write(buf)
44    }
45
46    fn flush(&mut self) -> std::io::Result<()> {
47        // no flush needed when you write to a fix
48        Ok(())
49    }
50}
51
52impl<'a> MakeWriter<'a> for TestWriter {
53    type Writer = &'a Self;
54
55    fn make_writer(&'a self) -> Self::Writer {
56        self
57    }
58}
59
60/// Run a test with logging enabled.
61///
62/// This takes the place of `tokio::test` because it needs to initialize the tokio runtime in some
63/// more customized way to ensure logging is done correctly.
64///
65/// It initializes a single `tracing` Dispatch object for this test only, as well as a dedicated
66/// tokio runtime.  It configures `tracing` to log to a buffer, and dumps this buffer to the
67/// console at the end of the test or in the event of a panic.  Importantly, and unlike the default
68/// behavior you get when initializing tracing, this automatically initializes all threads in the
69/// tokio runtime for this test, to use that same logging config.  So this will pick up all log
70/// events in tokio async tasks as well, but only those for this specific test.
71///
72/// This makes the log output for each test much more actionable because it's not interspersed with
73/// log events from other tests.
74pub fn test_with_logging(test: impl Future<Output = Result<()>>) -> Result<()> {
75    // All log events for this test will be stored in this vec.
76    let test_writer = TestWriter::new();
77
78    let dispatch = {
79        use tracing_subscriber::prelude::*;
80        use tracing_subscriber::{fmt, EnvFilter};
81
82        // Note the use of `TestWriter` here which writes the log events to stdout in a way that
83        // Rust unit tests are able to capture (at least on the main thread)
84        let format = fmt::layer()
85            .with_level(true) // include level in output
86            .with_target(true) // targets aren't that useful but filters operate on targets so they're important to know
87            .with_thread_ids(true) // thread IDs are helpful when multiple parallel tests are running at the same time
88            .with_thread_names(false) // but thread names are pretty shit
89            .with_writer(test_writer.clone());
90
91        // Get the log filter from the RUST_LOG env var, or if not set use a reasonable default
92        let filter = EnvFilter::try_from_default_env()
93            .or_else(|_| EnvFilter::try_new("h2=warn,hyper=info,rustls=info,aws=info,debug"))
94            .unwrap();
95
96        // Create a `fmt` subscriber that uses our custom event format, and set it
97        // as the default.
98        let subscriber = tracing_subscriber::registry().with(filter).with(format);
99
100        tracing::Dispatch::new(subscriber)
101    };
102
103    let dispatch = Arc::new(dispatch);
104
105    // This dispatch contains the logging config for this particular test.  It needs to be made the
106    // default dispatcher in each thread as well.
107
108    tracing::dispatcher::with_default(&dispatch, || {
109        std::thread_local! {
110            static THREAD_DISPATCHER_GUARD: RefCell<Option<tracing::subscriber::DefaultGuard>> = RefCell::new(None);
111        }
112
113        let mut builder = tokio::runtime::Builder::new_multi_thread();
114        builder.enable_all();
115        {
116            let dispatch = dispatch.clone();
117            builder.on_thread_start(move || {
118                let dispatch = dispatch.clone();
119
120                THREAD_DISPATCHER_GUARD.with(|cell| {
121                    cell.replace(Some(tracing::dispatcher::set_default(&dispatch)));
122                })
123            });
124        }
125
126        builder.on_thread_stop(|| {
127            // Drop the dispatcher guard so it's no longer the thread-local default
128            THREAD_DISPATCHER_GUARD.with(|cell| cell.replace(None));
129        });
130
131        let runtime = builder.build()?;
132
133        // I'm pretty sure Tokio `Runtime` structs are safe to pass across unwind boundaries, but I
134        // don't know for sure
135        // It's not realistic to require all test futures to be explicitly unwind safe, so I'll
136        // just assume they're safe too
137        let result = std::panic::catch_unwind(AssertUnwindSafe(move || {
138            let result = runtime.block_on(test);
139            runtime.shutdown_timeout(Duration::from_secs(10));
140
141            result
142        }));
143
144        // Test has run, maybe succeed maybe failed maybe panicked
145        // Print all of the log events now
146        let log_events = test_writer.take_string();
147
148        println!("Log events from this test: \n{}", log_events);
149
150        match result {
151            Ok(result) => {
152                // Test did not panic, so return the result the test did complete with
153                result
154            }
155            Err(err) => {
156                // Test panicked.  Just re-throw the panic now that we've written the log output
157                std::panic::resume_unwind(err)
158            }
159        }
160    })?;
161
162    Ok(())
163}