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 TruncatedPhases;
impl Readout for TruncatedPhases {
fn name(&self) -> &'static str {
"truncated_phases"
}
fn accepts(&self) -> &'static [SubjectKind] {
&[SubjectKind::Session]
}
fn render(
&self,
ctx: &dyn ReadoutContext,
lod: Lod,
mode: ContentMode,
_opts: &ReadoutOptions,
out: &mut dyn ReadoutBuf,
) -> usize {
let count = ctx.session_phases_truncated();
if count == 0 {
return 0;
}
let color = ctx.use_color();
match (lod, mode) {
(Lod::Compact, ContentMode::Value) => render_compact(count, color, out),
(Lod::Labeled, ContentMode::Value) => render_labeled(count, color, out),
(Lod::Expanded, ContentMode::Value) => render_expanded(count, color, out),
(_, ContentMode::Explanation) => render_explanation(out),
}
}
}
fn render_compact(count: usize, color: bool, out: &mut dyn ReadoutBuf) -> usize {
let dim = if color { "\x1b[2m" } else { "" };
let reset = if color { "\x1b[0m" } else { "" };
let mut tmp = String::with_capacity(48);
let _ = write!(&mut tmp, "{dim}(… {count} more){reset}");
let len = tmp.len();
let _ = out.write_str(&tmp);
len
}
fn render_labeled(count: usize, color: bool, out: &mut dyn ReadoutBuf) -> usize {
let dim = if color { "\x1b[2m" } else { "" };
let reset = if color { "\x1b[0m" } else { "" };
let suffix = if count == 1 { "" } else { "s" };
let mut tmp = String::with_capacity(128);
let _ = write!(
&mut tmp,
"{dim}(… and {count} more phase{suffix} not listed){reset}\n\
{dim}tip: run with dryrun=phase to see the full plan{reset}",
);
let len = tmp.len();
let _ = out.write_str(&tmp);
len
}
fn render_expanded(count: usize, color: bool, out: &mut dyn ReadoutBuf) -> usize {
let dim = if color { "\x1b[2m" } else { "" };
let reset = if color { "\x1b[0m" } else { "" };
let suffix = if count == 1 { "" } else { "s" };
let mut tmp = String::with_capacity(192);
let _ = write!(
&mut tmp,
"{dim}post-failure tail truncated to keep the summary readable{reset}\n\
{dim}(… and {count} more phase{suffix} not listed){reset}\n\
{dim}tip: run with dryrun=phase to see the full plan{reset}",
);
let len = tmp.len();
let _ = out.write_str(&tmp);
len
}
fn render_explanation(out: &mut dyn ReadoutBuf) -> usize {
let s = "(… and <count> more phases not listed) — \
tail of the phase list trimmed past the last \
failure; tip points at dryrun=phase for the \
full plan";
let _ = out.write_str(s);
s.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::readouts::buf::StringBuf;
struct TestCtx {
truncated: usize,
}
impl ReadoutContext for TestCtx {
fn subject_name(&self) -> &str {
"session"
}
fn session_phases_truncated(&self) -> usize {
self.truncated
}
fn event(&self) -> crate::lifecycle::EventType {
crate::lifecycle::EventType::SessionEnd
}
}
fn render(ctx: &TestCtx, lod: Lod) -> String {
let mut s = String::new();
let mut buf = StringBuf::new(&mut s);
TruncatedPhases.render(
ctx,
lod,
ContentMode::Value,
&ReadoutOptions::new(),
&mut buf,
);
s
}
#[test]
fn zero_count_renders_no_bytes() {
let ctx = TestCtx { truncated: 0 };
let mut s = String::new();
let mut buf = StringBuf::new(&mut s);
let n = TruncatedPhases.render(
&ctx,
Lod::Labeled,
ContentMode::Value,
&ReadoutOptions::new(),
&mut buf,
);
assert_eq!(n, 0);
assert!(s.is_empty());
}
#[test]
fn labeled_matches_pre_engine_format() {
let ctx = TestCtx { truncated: 7 };
assert_eq!(
render(&ctx, Lod::Labeled),
"(… and 7 more phases not listed)\n\
tip: run with dryrun=phase to see the full plan",
);
}
#[test]
fn singular_one_phase_drops_plural_suffix() {
let ctx = TestCtx { truncated: 1 };
assert!(render(&ctx, Lod::Labeled).contains("1 more phase not listed"));
}
#[test]
fn compact_packs_into_one_line() {
let ctx = TestCtx { truncated: 12 };
assert_eq!(render(&ctx, Lod::Compact), "(… 12 more)");
}
#[test]
fn expanded_adds_context_line() {
let ctx = TestCtx { truncated: 3 };
let s = render(&ctx, Lod::Expanded);
assert!(s.contains("post-failure tail truncated"));
assert!(s.contains("3 more phases"));
assert!(s.contains("dryrun=phase"));
assert_eq!(s.lines().count(), 3);
}
#[test]
fn explanation_describes_the_rollup() {
let ctx = TestCtx { truncated: 5 };
let mut s = String::new();
let mut buf = StringBuf::new(&mut s);
let n = TruncatedPhases.render(
&ctx,
Lod::Labeled,
ContentMode::Explanation,
&ReadoutOptions::new(),
&mut buf,
);
assert!(n > 0);
assert!(s.contains("<count>"));
assert!(s.contains("dryrun=phase"));
}
}