use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum TraceEntry {
Step(Cow<'static, str>),
Kv {
key: Cow<'static, str>,
value: String,
},
}
thread_local! {
static ACTIVE: RefCell<Option<Vec<TraceEntry>>> = const { RefCell::new(None) };
}
pub struct Trace {
previous: Option<Vec<TraceEntry>>,
}
impl Trace {
#[must_use]
pub fn new() -> Self {
let previous = ACTIVE.with(|cell| cell.borrow_mut().replace(Vec::new()));
Self { previous }
}
pub fn step(&mut self, message: impl Into<Cow<'static, str>>) {
let entry = TraceEntry::Step(message.into());
ACTIVE.with(|cell| {
if let Some(entries) = cell.borrow_mut().as_mut() {
entries.push(entry);
}
});
}
pub fn kv(&mut self, key: impl Into<Cow<'static, str>>, value: impl fmt::Display) {
let entry = TraceEntry::Kv {
key: key.into(),
value: value.to_string(),
};
ACTIVE.with(|cell| {
if let Some(entries) = cell.borrow_mut().as_mut() {
entries.push(entry);
}
});
}
#[must_use]
pub fn entries(&self) -> Vec<TraceEntry> {
snapshot()
}
}
impl Default for Trace {
fn default() -> Self {
Self::new()
}
}
impl Drop for Trace {
fn drop(&mut self) {
ACTIVE.with(|cell| *cell.borrow_mut() = self.previous.take());
}
}
pub(crate) fn snapshot() -> Vec<TraceEntry> {
ACTIVE.with(|cell| cell.borrow().clone().unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ErrorKind, OrFail, TestError, TestResult};
use test_better_matchers::{check, eq, is_true};
#[test]
fn steps_and_kv_are_recorded_in_order() -> TestResult {
let mut trace = Trace::new();
trace.step("first");
trace.kv("key", 42);
trace.step("second");
let entries = trace.entries();
check!(entries.len()).satisfies(eq(3)).or_fail()?;
check!(entries[0].clone())
.satisfies(eq(TraceEntry::Step("first".into())))
.or_fail()?;
check!(entries[1].clone())
.satisfies(eq(TraceEntry::Kv {
key: "key".into(),
value: "42".to_string(),
}))
.or_fail()?;
check!(entries[2].clone())
.satisfies(eq(TraceEntry::Step("second".into())))
.or_fail()?;
Ok(())
}
#[test]
fn an_error_built_within_a_trace_snapshots_it() -> TestResult {
let mut trace = Trace::new();
trace.step("doing the thing");
let error = TestError::new(ErrorKind::Assertion);
check!(error.trace.len()).satisfies(eq(1)).or_fail()?;
check!(error.trace[0].clone())
.satisfies(eq(TraceEntry::Step("doing the thing".into())))
.or_fail()?;
Ok(())
}
#[test]
fn an_error_built_with_no_trace_in_scope_has_an_empty_trace() -> TestResult {
let error = TestError::new(ErrorKind::Assertion);
check!(error.trace.is_empty())
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn dropping_a_trace_ends_the_scope() -> TestResult {
{
let mut trace = Trace::new();
trace.step("inside the scope");
}
let error = TestError::new(ErrorKind::Assertion);
check!(error.trace.is_empty())
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn nested_traces_compose_and_restore() -> TestResult {
let mut outer = Trace::new();
outer.step("outer step");
{
let mut inner = Trace::new();
inner.step("inner step");
check!(inner.entries().len()).satisfies(eq(1)).or_fail()?;
}
let entries = outer.entries();
check!(entries.len()).satisfies(eq(1)).or_fail()?;
check!(entries[0].clone())
.satisfies(eq(TraceEntry::Step("outer step".into())))
.or_fail()?;
Ok(())
}
}