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