nmbrs_runtime/readouts/builtins/
scope_close.rs1use std::fmt::Write as _;
9
10use crate::lifecycle::SubjectKind;
11use crate::readouts::buf::ReadoutBuf;
12use crate::readouts::context::ReadoutContext;
13use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
14
15pub struct ScopeClose;
16
17impl Readout for ScopeClose {
18 fn name(&self) -> &'static str {
19 "scope_close"
20 }
21 fn accepts(&self) -> &'static [SubjectKind] {
22 &[SubjectKind::Scope]
23 }
24
25 fn render(
26 &self,
27 ctx: &dyn ReadoutContext,
28 lod: Lod,
29 mode: ContentMode,
30 _opts: &ReadoutOptions,
31 out: &mut dyn ReadoutBuf,
32 ) -> usize {
33 match (lod, mode) {
34 (Lod::Compact, ContentMode::Value) => render_compact(ctx, out),
35 (Lod::Labeled, ContentMode::Value) => render_labeled(ctx, out),
36 (Lod::Expanded, ContentMode::Value) => render_expanded(ctx, out),
37 (_, ContentMode::Explanation) => render_explanation(out),
38 }
39 }
40}
41
42fn render_compact(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
43 let color = ctx.use_color();
44 let cyan = if color { "\x1b[36m" } else { "" };
45 let reset = if color { "\x1b[0m" } else { "" };
46 let mut tmp = String::with_capacity(16);
47 let _ = write!(&mut tmp, "{cyan}└{reset}");
48 let len = tmp.len();
49 let _ = out.write_str(&tmp);
50 len
51}
52
53fn render_labeled(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
54 let color = ctx.use_color();
55 let cyan = if color { "\x1b[36m" } else { "" };
56 let italic = if color { "\x1b[3m" } else { "" };
57 let reset = if color { "\x1b[0m" } else { "" };
58 let name = ctx.subject_name();
59 let mut tmp = String::with_capacity(48);
60 if name.is_empty() {
61 let _ = write!(&mut tmp, "{cyan}└─{reset} {italic}end{reset}");
62 } else {
63 let _ = write!(&mut tmp, "{cyan}└─{reset} {italic}end {name}{reset}");
64 }
65 let len = tmp.len();
66 let _ = out.write_str(&tmp);
67 len
68}
69
70fn render_expanded(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
71 let color = ctx.use_color();
72 let cyan = if color { "\x1b[36m" } else { "" };
73 let italic = if color { "\x1b[3m" } else { "" };
74 let reset = if color { "\x1b[0m" } else { "" };
75 let name = ctx.subject_name();
76 let elapsed = ctx.elapsed_secs();
77 let mut tmp = String::with_capacity(80);
78 if name.is_empty() {
79 let _ = write!(
80 &mut tmp,
81 "{cyan}└─{reset} {italic}end{reset}\n elapsed: {elapsed:.2}s"
82 );
83 } else {
84 let _ = write!(
85 &mut tmp,
86 "{cyan}└─{reset} {italic}end {name}{reset}\n elapsed: {elapsed:.2}s"
87 );
88 }
89 let len = tmp.len();
90 let _ = out.write_str(&tmp);
91 len
92}
93
94fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
95 let s = "└─ end {scope-name} — closing marker for a do_while / do_until / group scope";
96 let _ = out.write_str(s);
97 s.len()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::readouts::buf::StringBuf;
104
105 #[derive(Default)]
106 struct TestCtx {
107 name: String,
108 use_color: bool,
109 elapsed: f64,
110 }
111 impl ReadoutContext for TestCtx {
112 fn subject_name(&self) -> &str {
113 &self.name
114 }
115 fn subject_seq(&self) -> Option<(usize, usize)> {
116 None
117 }
118 fn subject_labels(&self) -> &str {
119 ""
120 }
121 fn cycles_completed(&self) -> u64 {
122 0
123 }
124 fn cycles_total(&self) -> u64 {
125 0
126 }
127 fn ops_ok(&self) -> u64 {
128 0
129 }
130 fn errors(&self) -> u64 {
131 0
132 }
133 fn retries(&self) -> u64 {
134 0
135 }
136 fn concurrency(&self) -> usize {
137 0
138 }
139 fn elapsed_secs(&self) -> f64 {
140 self.elapsed
141 }
142 fn consumed(&self) -> u64 {
143 0
144 }
145 fn status_metric_chips(&self) -> String {
146 String::new()
147 }
148 fn depth_indent(&self) -> &str {
149 ""
150 }
151 fn use_color(&self) -> bool {
152 self.use_color
153 }
154 fn event(&self) -> crate::lifecycle::EventType {
155 crate::lifecycle::EventType::ScopeEnd
156 }
157 }
158
159 fn render(ctx: &TestCtx, lod: Lod) -> String {
160 let mut s = String::new();
161 let mut buf = StringBuf::new(&mut s);
162 ScopeClose.render(
163 ctx,
164 lod,
165 ContentMode::Value,
166 &ReadoutOptions::new(),
167 &mut buf,
168 );
169 s
170 }
171
172 #[test]
173 fn labeled_renders_close_glyph_and_name() {
174 let ctx = TestCtx {
175 name: "do_while".into(),
176 ..Default::default()
177 };
178 assert_eq!(render(&ctx, Lod::Labeled), "└─ end do_while");
179 }
180
181 #[test]
182 fn compact_thinner_than_labeled() {
183 let ctx = TestCtx {
184 name: "do_while".into(),
185 ..Default::default()
186 };
187 assert_eq!(render(&ctx, Lod::Compact), "└");
188 }
189
190 #[test]
191 fn expanded_includes_elapsed() {
192 let ctx = TestCtx {
193 name: "do_until".into(),
194 elapsed: 12.345,
195 ..Default::default()
196 };
197 let out = render(&ctx, Lod::Expanded);
198 assert!(out.contains("do_until"));
199 assert!(out.contains("elapsed: 12.35s"));
200 }
201}