nmbrs_runtime/readouts/builtins/
scope_header.rs1use std::fmt::Write as _;
29
30use crate::lifecycle::SubjectKind;
31use crate::readouts::buf::ReadoutBuf;
32use crate::readouts::context::ReadoutContext;
33use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
34
35pub struct ScopeHeader;
36
37impl Readout for ScopeHeader {
38 fn name(&self) -> &'static str {
39 "scope_header"
40 }
41 fn accepts(&self) -> &'static [SubjectKind] {
42 &[SubjectKind::Iteration]
43 }
44
45 fn render(
46 &self,
47 ctx: &dyn ReadoutContext,
48 lod: Lod,
49 mode: ContentMode,
50 _opts: &ReadoutOptions,
51 out: &mut dyn ReadoutBuf,
52 ) -> usize {
53 match (lod, mode) {
54 (Lod::Compact, ContentMode::Value) => render_value(ctx, out, false),
55 (Lod::Labeled, ContentMode::Value) => render_value(ctx, out, false),
56 (Lod::Expanded, ContentMode::Value) => render_value(ctx, out, true),
57 (_, ContentMode::Explanation) => render_explanation(out),
58 }
59 }
60}
61
62fn render_value(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf, expanded: bool) -> usize {
63 let color = ctx.use_color();
64 let cyan = if color { "\x1b[36m" } else { "" };
65 let italic = if color { "\x1b[3m" } else { "" };
66 let reset = if color { "\x1b[0m" } else { "" };
67 let name = ctx.subject_name();
68 let labels = ctx.subject_labels();
69
70 let mut tmp = String::with_capacity(64);
71 if expanded && !labels.is_empty() {
72 let _ = write!(&mut tmp, "{cyan}·{reset} {italic}{name}{reset}\n {labels}");
77 } else {
78 let _ = write!(&mut tmp, "{cyan}·{reset} {italic}{name}{reset}");
82 }
83 let len = tmp.len();
84 let _ = out.write_str(&tmp);
85 len
86}
87
88fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
89 let s = "· {scope-name} — header for the surrounding for_each / do_while scope iteration";
90 let _ = out.write_str(s);
91 s.len()
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::readouts::buf::StringBuf;
98
99 #[derive(Default)]
100 struct TestCtx {
101 name: String,
102 labels: String,
103 use_color: bool,
104 }
105 impl ReadoutContext for TestCtx {
106 fn subject_name(&self) -> &str {
107 &self.name
108 }
109 fn subject_seq(&self) -> Option<(usize, usize)> {
110 None
111 }
112 fn subject_labels(&self) -> &str {
113 &self.labels
114 }
115 fn cycles_completed(&self) -> u64 {
116 0
117 }
118 fn cycles_total(&self) -> u64 {
119 0
120 }
121 fn ops_ok(&self) -> u64 {
122 0
123 }
124 fn errors(&self) -> u64 {
125 0
126 }
127 fn retries(&self) -> u64 {
128 0
129 }
130 fn concurrency(&self) -> usize {
131 0
132 }
133 fn elapsed_secs(&self) -> f64 {
134 0.0
135 }
136 fn consumed(&self) -> u64 {
137 0
138 }
139 fn status_metric_chips(&self) -> String {
140 String::new()
141 }
142 fn depth_indent(&self) -> &str {
143 ""
144 }
145 fn use_color(&self) -> bool {
146 self.use_color
147 }
148 fn event(&self) -> crate::lifecycle::EventType {
149 crate::lifecycle::EventType::EachStart
150 }
151 }
152
153 fn render(ctx: &TestCtx, lod: Lod, mode: ContentMode) -> String {
154 let mut s = String::new();
155 let mut buf = StringBuf::new(&mut s);
156 ScopeHeader.render(ctx, lod, mode, &ReadoutOptions::new(), &mut buf);
157 s
158 }
159
160 #[test]
161 fn labeled_no_color() {
162 let ctx = TestCtx {
163 name: "for_each profile=label_00".into(),
164 ..Default::default()
165 };
166 assert_eq!(
167 render(&ctx, Lod::Labeled, ContentMode::Value),
168 "· for_each profile=label_00",
169 );
170 }
171
172 #[test]
173 fn compact_matches_labeled_for_simple_form() {
174 let ctx = TestCtx {
179 name: "for_each k=10".into(),
180 ..Default::default()
181 };
182 assert_eq!(
183 render(&ctx, Lod::Compact, ContentMode::Value),
184 render(&ctx, Lod::Labeled, ContentMode::Value),
185 );
186 }
187
188 #[test]
189 fn expanded_with_labels_shows_iteration_tuple() {
190 let ctx = TestCtx {
191 name: "for_combinations".into(),
192 labels: "(profile=alpha), (k=10, limit=100)".into(),
193 ..Default::default()
194 };
195 let out = render(&ctx, Lod::Expanded, ContentMode::Value);
196 assert!(out.contains("for_combinations"));
197 assert!(out.contains("(profile=alpha), (k=10, limit=100)"));
198 assert!(
200 out.lines().count() >= 2,
201 "expanded should be multi-line: {out}"
202 );
203 }
204
205 #[test]
206 fn explanation_describes_the_row() {
207 let ctx = TestCtx {
208 name: "for_each".into(),
209 ..Default::default()
210 };
211 let out = render(&ctx, Lod::Labeled, ContentMode::Explanation);
212 assert!(
213 out.contains("scope-name"),
214 "expected 'scope-name' descriptor: {out}"
215 );
216 }
217
218 #[test]
219 fn ansi_emitted_when_color_enabled() {
220 let ctx = TestCtx {
221 name: "for_each k".into(),
222 use_color: true,
223 ..Default::default()
224 };
225 let out = render(&ctx, Lod::Labeled, ContentMode::Value);
226 assert!(out.contains("\x1b[36m"), "missing cyan: {out:?}");
228 assert!(out.contains("\x1b[3m"), "missing italic: {out:?}");
229 assert!(out.contains("\x1b[0m"), "missing reset: {out:?}");
230 }
231}