use std::collections::HashMap;
use std::io;
use alba_engine::RunEvent;
use super::{LineSink, Renderer, format_duration, print_summary, status_label};
pub struct GroupedRenderer {
buffers: HashMap<String, Vec<String>>,
clear_between_runs: bool,
affected_by: Option<String>,
out: LineSink<io::Stdout>,
err: LineSink<io::Stderr>,
}
impl GroupedRenderer {
pub fn new(clear_between_runs: bool) -> Self {
Self {
buffers: HashMap::new(),
clear_between_runs,
affected_by: None,
out: LineSink::stdout(),
err: LineSink::stderr(),
}
}
fn flush(&mut self, id: &str, header: &str) {
self.out.line(header);
for line in self.buffers.remove(id).unwrap_or_default() {
self.out.line(&line);
}
}
}
impl Renderer for GroupedRenderer {
fn handle(&mut self, event: &RunEvent) {
if super::handle_watch_event(event, &mut self.out, &mut self.err, self.clear_between_runs) {
return;
}
match event {
RunEvent::BeamStarted { id } | RunEvent::BeamCached { id } => {
self.buffers.entry(id.0.clone()).or_default();
}
RunEvent::BeamOutput { id, line, .. } => {
self.buffers
.entry(id.0.clone())
.or_default()
.push(line.text.clone());
}
RunEvent::BeamFinished {
id,
status,
duration,
} => {
let header = if matches!(status, alba_engine::BeamStatus::Cached) {
format!(
"\u{2500}\u{2500} {} ({} original run, cached) \u{2500}\u{2500}",
id.0,
format_duration(*duration)
)
} else {
format!(
"\u{2500}\u{2500} {} ({}, {}) \u{2500}\u{2500}",
id.0,
format_duration(*duration),
status_label(status)
)
};
self.flush(&id.0, &header);
}
RunEvent::RunFinished { summary } => {
let mut orphans: Vec<String> = self.buffers.keys().cloned().collect();
orphans.sort();
for id in orphans {
let header = format!("\u{2500}\u{2500} {id} (unfinished) \u{2500}\u{2500}");
self.flush(&id, &header);
}
print_summary(&mut self.err, summary, self.affected_by.as_deref());
}
RunEvent::RunStarted { affected_by, .. } => self.affected_by = affected_by.clone(),
RunEvent::WatchWaiting { .. } | RunEvent::WatchTriggered { .. } => {}
RunEvent::ProjectBroken { diagnostic } => {
self.err.line(diagnostic.trim_end());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alba_core::BeamId;
use alba_engine::RunSummary;
use alba_executors::{OutputLine, Stream};
fn output(id: &str, text: &str) -> RunEvent {
RunEvent::BeamOutput {
id: BeamId(id.to_string()),
line: OutputLine {
stream: Stream::Stdout,
text: text.to_string(),
},
replayed: false,
}
}
#[test]
fn the_final_event_drains_any_beam_left_unfinished() {
let mut renderer = GroupedRenderer::new(false);
renderer.handle(&output("orphan", "would be lost"));
assert!(renderer.buffers.contains_key("orphan"));
renderer.handle(&RunEvent::RunFinished {
summary: RunSummary::default(),
});
assert!(
renderer.buffers.is_empty(),
"a beam left unfinished must still be flushed, not dropped"
);
}
}