test_better_core/
trace.rs1use std::borrow::Cow;
17use std::cell::RefCell;
18use std::fmt;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[non_exhaustive]
24pub enum TraceEntry {
25 Step(Cow<'static, str>),
27 Kv {
29 key: Cow<'static, str>,
31 value: String,
33 },
34}
35
36thread_local! {
37 static ACTIVE: RefCell<Option<Vec<TraceEntry>>> = const { RefCell::new(None) };
40}
41
42pub struct Trace {
63 previous: Option<Vec<TraceEntry>>,
66}
67
68impl Trace {
69 #[must_use]
71 pub fn new() -> Self {
72 let previous = ACTIVE.with(|cell| cell.borrow_mut().replace(Vec::new()));
73 Self { previous }
74 }
75
76 pub fn step(&mut self, message: impl Into<Cow<'static, str>>) {
78 let entry = TraceEntry::Step(message.into());
79 ACTIVE.with(|cell| {
80 if let Some(entries) = cell.borrow_mut().as_mut() {
81 entries.push(entry);
82 }
83 });
84 }
85
86 pub fn kv(&mut self, key: impl Into<Cow<'static, str>>, value: impl fmt::Display) {
91 let entry = TraceEntry::Kv {
92 key: key.into(),
93 value: value.to_string(),
94 };
95 ACTIVE.with(|cell| {
96 if let Some(entries) = cell.borrow_mut().as_mut() {
97 entries.push(entry);
98 }
99 });
100 }
101
102 #[must_use]
104 pub fn entries(&self) -> Vec<TraceEntry> {
105 snapshot()
106 }
107}
108
109impl Default for Trace {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl Drop for Trace {
116 fn drop(&mut self) {
117 ACTIVE.with(|cell| *cell.borrow_mut() = self.previous.take());
118 }
119}
120
121pub(crate) fn snapshot() -> Vec<TraceEntry> {
126 ACTIVE.with(|cell| cell.borrow().clone().unwrap_or_default())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::{ErrorKind, OrFail, TestError, TestResult};
133 use test_better_matchers::{check, eq, is_true};
134
135 #[test]
136 fn steps_and_kv_are_recorded_in_order() -> TestResult {
137 let mut trace = Trace::new();
138 trace.step("first");
139 trace.kv("key", 42);
140 trace.step("second");
141 let entries = trace.entries();
142 check!(entries.len()).satisfies(eq(3)).or_fail()?;
143 check!(entries[0].clone())
144 .satisfies(eq(TraceEntry::Step("first".into())))
145 .or_fail()?;
146 check!(entries[1].clone())
147 .satisfies(eq(TraceEntry::Kv {
148 key: "key".into(),
149 value: "42".to_string(),
150 }))
151 .or_fail()?;
152 check!(entries[2].clone())
153 .satisfies(eq(TraceEntry::Step("second".into())))
154 .or_fail()?;
155 Ok(())
156 }
157
158 #[test]
159 fn an_error_built_within_a_trace_snapshots_it() -> TestResult {
160 let mut trace = Trace::new();
161 trace.step("doing the thing");
162 let error = TestError::new(ErrorKind::Assertion);
163 check!(error.trace.len()).satisfies(eq(1)).or_fail()?;
164 check!(error.trace[0].clone())
165 .satisfies(eq(TraceEntry::Step("doing the thing".into())))
166 .or_fail()?;
167 Ok(())
168 }
169
170 #[test]
171 fn an_error_built_with_no_trace_in_scope_has_an_empty_trace() -> TestResult {
172 let error = TestError::new(ErrorKind::Assertion);
173 check!(error.trace.is_empty())
174 .satisfies(is_true())
175 .or_fail()?;
176 Ok(())
177 }
178
179 #[test]
180 fn dropping_a_trace_ends_the_scope() -> TestResult {
181 {
182 let mut trace = Trace::new();
183 trace.step("inside the scope");
184 }
185 let error = TestError::new(ErrorKind::Assertion);
187 check!(error.trace.is_empty())
188 .satisfies(is_true())
189 .or_fail()?;
190 Ok(())
191 }
192
193 #[test]
194 fn nested_traces_compose_and_restore() -> TestResult {
195 let mut outer = Trace::new();
196 outer.step("outer step");
197 {
198 let mut inner = Trace::new();
199 inner.step("inner step");
200 check!(inner.entries().len()).satisfies(eq(1)).or_fail()?;
201 }
202 let entries = outer.entries();
204 check!(entries.len()).satisfies(eq(1)).or_fail()?;
205 check!(entries[0].clone())
206 .satisfies(eq(TraceEntry::Step("outer step".into())))
207 .or_fail()?;
208 Ok(())
209 }
210}