use std::collections::BTreeMap;
use super::event::{Outcome, ProgressEvent, Stage};
pub const LOG_CAPACITY: usize = 500;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TargetRow {
pub target: String,
pub done: u64,
pub total: Option<u64>,
pub outcome: Option<Outcome>,
pub detail: Option<String>,
}
impl TargetRow {
pub fn fraction(&self) -> Option<f64> {
match self.total {
Some(t) if t > 0 => Some((self.done as f64 / t as f64).min(1.0)),
_ if self.outcome.is_some() => Some(1.0),
_ => None,
}
}
pub fn is_running(&self) -> bool {
self.outcome.is_none()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StageSummary {
pub running: usize,
pub completed: usize,
pub failed: usize,
pub skipped: usize,
}
impl StageSummary {
pub fn total(&self) -> usize {
self.running + self.completed + self.failed + self.skipped
}
pub fn is_started(&self) -> bool {
self.total() > 0
}
}
#[derive(Debug, Clone, Default)]
pub struct ProgressAggregate {
rows: BTreeMap<Stage, Vec<TargetRow>>,
log: std::collections::VecDeque<String>,
dropped: u64,
}
impl ProgressAggregate {
pub fn new() -> Self {
Self::default()
}
pub fn apply(&mut self, event: ProgressEvent) {
let rows = self.rows.entry(event.stage).or_default();
let idx = rows.iter().position(|r| r.target == event.target);
let idx = match idx {
Some(i) => i,
None => {
rows.push(TargetRow {
target: event.target.clone(),
done: 0,
total: None,
outcome: None,
detail: None,
});
rows.len() - 1
}
};
let row = &mut rows[idx];
row.done = event.done.max(row.done);
if event.total.is_some() {
row.total = event.total;
}
if event.detail.is_some() {
row.detail = event.detail.clone();
}
if event.outcome.is_some() {
row.outcome = event.outcome.clone();
}
if let Some(line) = activity_line(&event) {
self.push_log(line);
}
}
pub fn set_dropped(&mut self, dropped: u64) {
self.dropped = dropped;
}
pub fn dropped(&self) -> u64 {
self.dropped
}
pub fn push_activity(&mut self, line: String) {
self.push_log(line);
}
pub fn rows(&self, stage: Stage) -> &[TargetRow] {
self.rows.get(&stage).map_or(&[], Vec::as_slice)
}
pub fn summary(&self, stage: Stage) -> StageSummary {
let mut s = StageSummary::default();
for row in self.rows(stage) {
match &row.outcome {
None => s.running += 1,
Some(Outcome::Completed) => s.completed += 1,
Some(Outcome::Failed { .. }) => s.failed += 1,
Some(Outcome::Skipped { .. }) => s.skipped += 1,
}
}
s
}
pub fn log(&self) -> impl DoubleEndedIterator<Item = &String> {
self.log.iter()
}
pub fn is_empty(&self) -> bool {
self.rows.values().all(Vec::is_empty)
}
pub fn is_settled(&self) -> bool {
!self.is_empty() && self.rows.values().flatten().all(|r| !r.is_running())
}
fn push_log(&mut self, line: String) {
while self.log.len() >= LOG_CAPACITY {
self.log.pop_front();
}
self.log.push_back(line);
}
}
fn activity_line(event: &ProgressEvent) -> Option<String> {
let status = match &event.outcome {
Some(o) => o.label(),
None if event.detail.is_some() => "…",
None => return None,
};
let ts = event.at.format("%H:%M:%S");
let mut line = format!("{ts} {} {} — {status}", event.stage.label(), event.target);
if let Some(d) = event.detail.as_deref() {
line.push_str(": ");
line.push_str(d);
}
Some(line)
}