use std::collections::HashSet;
use super::OpRow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Activity {
pub total: usize,
pub errors: usize,
}
pub fn activity(rows: &[OpRow]) -> Activity {
Activity {
total: rows.len(),
errors: live_failures(rows).into_iter().filter(|&live| live).count(),
}
}
impl Activity {
pub fn chip(&self) -> String {
if self.errors > 0 {
format!("activity · {} ops · {} ⚠", self.total, self.errors)
} else {
format!("activity · {} ops", self.total)
}
}
}
pub fn live_failures(rows: &[OpRow]) -> Vec<bool> {
let mut retired: HashSet<(String, String)> = HashSet::new();
let mut live: Vec<bool> = rows
.iter()
.rev()
.map(|row| {
if row.failed() {
return !retired.contains(&verb_key(row));
}
retired.insert(verb_key(row));
false
})
.collect();
live.reverse();
live
}
fn verb_key(row: &OpRow) -> (String, String) {
let verb = row
.argv
.split_whitespace()
.take(2)
.collect::<Vec<&str>>()
.join(" ");
(row.cwd.clone(), verb)
}
#[cfg(test)]
mod tests {
use super::super::OpEntry;
use super::*;
fn row(argv: &str, cwd: &str, exit: i32) -> OpRow {
OpRow::from(&OpEntry {
ts: "TS".into(),
argv: argv.split(' ').map(String::from).collect(),
cwd: cwd.into(),
exit,
stdout: String::new(),
stderr: if exit == 0 {
String::new()
} else {
"boom".into()
},
})
}
#[test]
fn a_later_clean_run_of_the_same_verb_retires_the_failure() {
let rows = [row("lernie prime", "/w", 2), row("lernie prime", "/w", 0)];
assert_eq!(live_failures(&rows), vec![false, false]);
assert_eq!(activity(&rows).errors, 0, "the chip counts no live failure");
}
#[test]
fn a_failure_with_no_later_clean_run_stays_live() {
let rows = [row("lernie prime", "/w", 0), row("lernie prime", "/w", 2)];
assert_eq!(live_failures(&rows), vec![false, true]);
assert_eq!(activity(&rows).errors, 1);
}
#[test]
fn retirement_is_scoped_to_the_cwd_it_ran_in() {
let rows = [row("lernie prime", "/a", 2), row("lernie prime", "/b", 0)];
assert_eq!(
live_failures(&rows),
vec![true, false],
"a clean run elsewhere leaves this project's failure live"
);
}
#[test]
fn the_verb_key_is_binary_plus_subcommand_not_the_operands() {
let same = [row("bl close bl-1", "/p", 1), row("bl close bl-2", "/p", 0)];
assert_eq!(live_failures(&same), vec![false, false]);
let other = [
row("bl close bl-1", "/p", 1),
row("bl list --json", "/p", 0),
];
assert_eq!(live_failures(&other), vec![true, false]);
let bare = [row("bz", "/p", 1), row("bz", "/p", 0)];
assert_eq!(live_failures(&bare), vec![false, false]);
}
#[test]
fn only_live_failures_reach_the_chip() {
let rows = [
row("lernie prime", "/w", 2), row("bl close bl-1", "/p", 1),
row("lernie prime", "/w", 0),
];
let a = activity(&rows);
assert_eq!(
a,
Activity {
total: 3,
errors: 1
}
);
assert_eq!(a.chip(), "activity · 3 ops · 1 ⚠");
assert_eq!(activity(&rows[2..]).chip(), "activity · 1 ops");
assert_eq!(activity(&[]).chip(), "activity · 0 ops");
assert!(live_failures(&[]).is_empty());
}
}