1#![warn(missing_docs)]
2use std::cell::{Cell, RefCell};
10use std::time::Duration;
11
12pub use jigs_core::JigMeta;
13pub use jigs_core::Status;
14
15#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct Entry {
19 pub meta: &'static JigMeta,
21 pub depth: usize,
23 pub duration: Duration,
25 pub ok: bool,
27 pub error: Option<String>,
29}
30
31impl Entry {
32 #[must_use]
34 pub fn name(&self) -> &'static str {
35 self.meta.name
36 }
37
38 #[must_use]
40 pub fn new(
41 meta: &'static JigMeta,
42 depth: usize,
43 duration: Duration,
44 ok: bool,
45 error: Option<String>,
46 ) -> Self {
47 Self {
48 meta,
49 depth,
50 duration,
51 ok,
52 error,
53 }
54 }
55}
56
57thread_local! {
58 static DEPTH: Cell<usize> = const { Cell::new(0) };
59 static BUFFER: RefCell<Vec<Entry>> = const { RefCell::new(Vec::new()) };
60}
61
62#[must_use]
65pub fn enter(meta: &'static JigMeta) -> usize {
66 let depth = DEPTH.with(|d| {
67 let v = d.get();
68 d.set(v + 1);
69 v
70 });
71 BUFFER.with(|b| {
72 let mut buf = b.borrow_mut();
73 let idx = buf.len();
74 buf.push(Entry {
75 meta,
76 depth,
77 duration: Duration::ZERO,
78 ok: true,
79 error: None,
80 });
81 idx
82 })
83}
84
85pub fn exit(idx: usize, duration: Duration, ok: bool, error: Option<String>) {
88 DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
89 BUFFER.with(|b| {
90 let mut buf = b.borrow_mut();
91 buf[idx].duration = duration;
92 buf[idx].ok = ok;
93 buf[idx].error = error;
94 });
95}
96
97#[must_use]
100pub fn take() -> Vec<Entry> {
101 DEPTH.with(|d| d.set(0));
102 BUFFER.with(|b| std::mem::take(&mut *b.borrow_mut()))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn meta(name: &'static str) -> &'static JigMeta {
110 Box::leak(Box::new(JigMeta {
111 name,
112 file: "",
113 line: 0,
114 kind: "Response",
115 input: "Request",
116 input_type: "",
117 output_type: "",
118 is_async: false,
119 module: "",
120 chain: &[],
121 }))
122 }
123
124 #[test]
125 fn enter_exit_records_one_entry() {
126 let m = meta("step");
127 let idx = enter(m);
128 exit(idx, Duration::from_micros(42), true, None);
129 let entries = take();
130 assert_eq!(entries.len(), 1);
131 assert_eq!(entries[0].name(), "step");
132 assert_eq!(entries[0].depth, 0);
133 assert!(entries[0].ok);
134 assert_eq!(entries[0].duration, Duration::from_micros(42));
135 assert!(entries[0].error.is_none());
136 }
137
138 #[test]
139 fn depth_increases_for_nested_calls() {
140 let outer = meta("outer");
141 let inner = meta("inner");
142 let i0 = enter(outer);
143 let i1 = enter(inner);
144 exit(i1, Duration::ZERO, true, None);
145 exit(i0, Duration::ZERO, true, None);
146 let entries = take();
147 assert_eq!(entries[0].depth, 0);
148 assert_eq!(entries[1].depth, 1);
149 }
150
151 #[test]
152 fn error_is_recorded() {
153 let m = meta("fail");
154 let idx = enter(m);
155 exit(idx, Duration::ZERO, false, Some("boom".into()));
156 let entries = take();
157 assert!(!entries[0].ok);
158 assert_eq!(entries[0].error.as_deref(), Some("boom"));
159 }
160
161 #[test]
162 fn take_drains_buffer() {
163 let m = meta("x");
164 let idx = enter(m);
165 exit(idx, Duration::ZERO, true, None);
166 let first = take();
167 let second = take();
168 assert_eq!(first.len(), 1);
169 assert!(second.is_empty());
170 }
171
172 #[test]
173 fn take_resets_depth() {
174 let m = meta("a");
175 let idx = enter(m);
176 exit(idx, Duration::ZERO, true, None);
177 let _ = take();
178 let idx2 = enter(m);
179 exit(idx2, Duration::ZERO, true, None);
180 let entries = take();
181 assert_eq!(entries[0].depth, 0);
182 }
183
184 #[test]
185 fn entry_exposes_full_meta() {
186 let m = meta("with_meta");
187 let idx = enter(m);
188 exit(idx, Duration::ZERO, true, None);
189 let entries = take();
190 assert!(std::ptr::eq(entries[0].meta, m));
191 }
192}