use std::fmt::Write as _;
use crate::lifecycle::SubjectKind;
use crate::readouts::buf::ReadoutBuf;
use crate::readouts::context::ReadoutContext;
use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
pub struct ScopeHeader;
impl Readout for ScopeHeader {
fn name(&self) -> &'static str {
"scope_header"
}
fn accepts(&self) -> &'static [SubjectKind] {
&[SubjectKind::Iteration]
}
fn render(
&self,
ctx: &dyn ReadoutContext,
lod: Lod,
mode: ContentMode,
_opts: &ReadoutOptions,
out: &mut dyn ReadoutBuf,
) -> usize {
match (lod, mode) {
(Lod::Compact, ContentMode::Value) => render_value(ctx, out, false),
(Lod::Labeled, ContentMode::Value) => render_value(ctx, out, false),
(Lod::Expanded, ContentMode::Value) => render_value(ctx, out, true),
(_, ContentMode::Explanation) => render_explanation(out),
}
}
}
fn render_value(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf, expanded: bool) -> usize {
let color = ctx.use_color();
let cyan = if color { "\x1b[36m" } else { "" };
let italic = if color { "\x1b[3m" } else { "" };
let reset = if color { "\x1b[0m" } else { "" };
let name = ctx.subject_name();
let labels = ctx.subject_labels();
let mut tmp = String::with_capacity(64);
if expanded && !labels.is_empty() {
let _ = write!(&mut tmp, "{cyan}·{reset} {italic}{name}{reset}\n {labels}");
} else {
let _ = write!(&mut tmp, "{cyan}·{reset} {italic}{name}{reset}");
}
let len = tmp.len();
let _ = out.write_str(&tmp);
len
}
fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
let s = "· {scope-name} — header for the surrounding for_each / do_while scope iteration";
let _ = out.write_str(s);
s.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::readouts::buf::StringBuf;
#[derive(Default)]
struct TestCtx {
name: String,
labels: String,
use_color: bool,
}
impl ReadoutContext for TestCtx {
fn subject_name(&self) -> &str {
&self.name
}
fn subject_seq(&self) -> Option<(usize, usize)> {
None
}
fn subject_labels(&self) -> &str {
&self.labels
}
fn cycles_completed(&self) -> u64 {
0
}
fn cycles_total(&self) -> u64 {
0
}
fn ops_ok(&self) -> u64 {
0
}
fn errors(&self) -> u64 {
0
}
fn retries(&self) -> u64 {
0
}
fn concurrency(&self) -> usize {
0
}
fn elapsed_secs(&self) -> f64 {
0.0
}
fn consumed(&self) -> u64 {
0
}
fn status_metric_chips(&self) -> String {
String::new()
}
fn depth_indent(&self) -> &str {
""
}
fn use_color(&self) -> bool {
self.use_color
}
fn event(&self) -> crate::lifecycle::EventType {
crate::lifecycle::EventType::EachStart
}
}
fn render(ctx: &TestCtx, lod: Lod, mode: ContentMode) -> String {
let mut s = String::new();
let mut buf = StringBuf::new(&mut s);
ScopeHeader.render(ctx, lod, mode, &ReadoutOptions::new(), &mut buf);
s
}
#[test]
fn labeled_no_color() {
let ctx = TestCtx {
name: "for_each profile=label_00".into(),
..Default::default()
};
assert_eq!(
render(&ctx, Lod::Labeled, ContentMode::Value),
"· for_each profile=label_00",
);
}
#[test]
fn compact_matches_labeled_for_simple_form() {
let ctx = TestCtx {
name: "for_each k=10".into(),
..Default::default()
};
assert_eq!(
render(&ctx, Lod::Compact, ContentMode::Value),
render(&ctx, Lod::Labeled, ContentMode::Value),
);
}
#[test]
fn expanded_with_labels_shows_iteration_tuple() {
let ctx = TestCtx {
name: "for_combinations".into(),
labels: "(profile=alpha), (k=10, limit=100)".into(),
..Default::default()
};
let out = render(&ctx, Lod::Expanded, ContentMode::Value);
assert!(out.contains("for_combinations"));
assert!(out.contains("(profile=alpha), (k=10, limit=100)"));
assert!(
out.lines().count() >= 2,
"expanded should be multi-line: {out}"
);
}
#[test]
fn explanation_describes_the_row() {
let ctx = TestCtx {
name: "for_each".into(),
..Default::default()
};
let out = render(&ctx, Lod::Labeled, ContentMode::Explanation);
assert!(
out.contains("scope-name"),
"expected 'scope-name' descriptor: {out}"
);
}
#[test]
fn ansi_emitted_when_color_enabled() {
let ctx = TestCtx {
name: "for_each k".into(),
use_color: true,
..Default::default()
};
let out = render(&ctx, Lod::Labeled, ContentMode::Value);
assert!(out.contains("\x1b[36m"), "missing cyan: {out:?}");
assert!(out.contains("\x1b[3m"), "missing italic: {out:?}");
assert!(out.contains("\x1b[0m"), "missing reset: {out:?}");
}
}