nmbrs_runtime/readouts/builtins/
session_banner.rs1use std::fmt::Write as _;
16
17use crate::lifecycle::SubjectKind;
18use crate::readouts::buf::ReadoutBuf;
19use crate::readouts::context::ReadoutContext;
20use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
21
22pub struct SessionBanner;
23
24impl Readout for SessionBanner {
25 fn name(&self) -> &'static str {
26 "session_banner"
27 }
28 fn accepts(&self) -> &'static [SubjectKind] {
29 &[SubjectKind::Session]
30 }
31
32 fn render(
33 &self,
34 ctx: &dyn ReadoutContext,
35 lod: Lod,
36 mode: ContentMode,
37 _opts: &ReadoutOptions,
38 out: &mut dyn ReadoutBuf,
39 ) -> usize {
40 match (lod, mode) {
41 (Lod::Compact, ContentMode::Value) => render_compact(ctx, out),
42 (Lod::Labeled, ContentMode::Value) => render_labeled(ctx, out),
43 (Lod::Expanded, ContentMode::Value) => render_expanded(ctx, out),
44 (_, ContentMode::Explanation) => render_explanation(out),
45 }
46 }
47}
48
49fn render_compact(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
50 let name = ctx.session_scenario_name();
51 if name.is_empty() {
52 return 0;
53 }
54 let color = ctx.use_color();
55 let bold = if color { "\x1b[1m" } else { "" };
56 let reset = if color { "\x1b[0m" } else { "" };
57 let mut tmp = String::with_capacity(name.len() + 8);
58 let _ = write!(&mut tmp, "{bold}{name}{reset}");
59 let len = tmp.len();
60 let _ = out.write_str(&tmp);
61 len
62}
63
64fn render_labeled(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
65 let name = ctx.session_scenario_name();
66 let workload = ctx.session_workload_file();
67 if name.is_empty() && workload.is_empty() {
68 return 0;
69 }
70 let color = ctx.use_color();
74 let bold = if color { "\x1b[1m" } else { "" };
75 let dim = if color { "\x1b[2m" } else { "" };
76 let reset = if color { "\x1b[0m" } else { "" };
77 let mut tmp = String::with_capacity(128);
78 if workload.is_empty() {
79 let _ = write!(&mut tmp, "{bold}session:{reset} {bold}{name}{reset}");
80 } else {
81 let _ = write!(
82 &mut tmp,
83 "{bold}session:{reset} {bold}{name}{reset} {dim}({workload}){reset}",
84 );
85 }
86 let len = tmp.len();
87 let _ = out.write_str(&tmp);
88 len
89}
90
91fn render_expanded(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
92 let name = ctx.session_scenario_name();
93 let workload = ctx.session_workload_file();
94 if name.is_empty() && workload.is_empty() {
95 return 0;
96 }
97 let color = ctx.use_color();
98 let bold = if color { "\x1b[1m" } else { "" };
99 let dim = if color { "\x1b[2m" } else { "" };
100 let reset = if color { "\x1b[0m" } else { "" };
101 let mut tmp = String::with_capacity(192);
102 let _ = write!(
103 &mut tmp,
104 "{bold}session{reset}\n {dim}scenario:{reset} {n}\n {dim}workload:{reset} {w}",
105 n = if name.is_empty() { "<unnamed>" } else { name },
106 w = if workload.is_empty() {
107 "<inline>"
108 } else {
109 workload
110 },
111 );
112 let len = tmp.len();
113 let _ = out.write_str(&tmp);
114 len
115}
116
117fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
118 let s = "session: <scenario-name> (<workload-file>)";
119 let _ = out.write_str(s);
120 s.len()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::readouts::buf::StringBuf;
127
128 #[derive(Default)]
129 struct TestCtx {
130 scenario: String,
131 workload: String,
132 }
133 impl ReadoutContext for TestCtx {
134 fn subject_name(&self) -> &str {
135 "session"
136 }
137 fn subject_seq(&self) -> Option<(usize, usize)> {
138 None
139 }
140 fn subject_labels(&self) -> &str {
141 ""
142 }
143 fn cycles_completed(&self) -> u64 {
144 0
145 }
146 fn cycles_total(&self) -> u64 {
147 0
148 }
149 fn ops_ok(&self) -> u64 {
150 0
151 }
152 fn errors(&self) -> u64 {
153 0
154 }
155 fn retries(&self) -> u64 {
156 0
157 }
158 fn concurrency(&self) -> usize {
159 0
160 }
161 fn elapsed_secs(&self) -> f64 {
162 0.0
163 }
164 fn consumed(&self) -> u64 {
165 0
166 }
167 fn status_metric_chips(&self) -> String {
168 String::new()
169 }
170 fn depth_indent(&self) -> &str {
171 ""
172 }
173 fn use_color(&self) -> bool {
174 false
175 }
176 fn event(&self) -> crate::lifecycle::EventType {
177 crate::lifecycle::EventType::SessionStart
178 }
179 fn session_scenario_name(&self) -> &str {
180 &self.scenario
181 }
182 fn session_workload_file(&self) -> &str {
183 &self.workload
184 }
185 }
186
187 fn render(ctx: &TestCtx, lod: Lod) -> String {
188 let mut s = String::new();
189 let mut buf = StringBuf::new(&mut s);
190 SessionBanner.render(
191 ctx,
192 lod,
193 ContentMode::Value,
194 &ReadoutOptions::new(),
195 &mut buf,
196 );
197 s
198 }
199
200 #[test]
201 fn labeled_renders_name_and_workload() {
202 let ctx = TestCtx {
203 scenario: "smoke".into(),
204 workload: "workloads/smoke.yaml".into(),
205 };
206 assert_eq!(
207 render(&ctx, Lod::Labeled),
208 "session: smoke (workloads/smoke.yaml)",
209 );
210 }
211
212 #[test]
213 fn labeled_drops_workload_tail_when_inline() {
214 let ctx = TestCtx {
215 scenario: "ad_hoc".into(),
216 workload: String::new(),
217 };
218 assert_eq!(render(&ctx, Lod::Labeled), "session: ad_hoc");
219 }
220
221 #[test]
222 fn compact_emits_just_name() {
223 let ctx = TestCtx {
224 scenario: "smoke".into(),
225 workload: "x.yaml".into(),
226 };
227 assert_eq!(render(&ctx, Lod::Compact), "smoke");
228 }
229
230 #[test]
231 fn expanded_breaks_onto_lines() {
232 let ctx = TestCtx {
233 scenario: "smoke".into(),
234 workload: "x.yaml".into(),
235 };
236 let out = render(&ctx, Lod::Expanded);
237 assert!(out.contains("scenario: smoke"));
238 assert!(out.contains("workload: x.yaml"));
239 assert!(out.lines().count() >= 3);
240 }
241
242 #[test]
243 fn empty_inputs_emit_zero() {
244 let ctx = TestCtx::default();
245 assert_eq!(render(&ctx, Lod::Labeled), "");
246 }
247}