use std::collections::BTreeMap;
use serde_json::Value;
const UNSPECIFIED: &str = "unspecified";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CompilationTally {
total: usize,
incomplete_by_reason: BTreeMap<String, usize>,
}
fn printable(reason: &str) -> String {
reason
.chars()
.map(|c| {
if c.is_control() {
c.escape_debug().to_string()
} else {
c.to_string()
}
})
.collect()
}
impl CompilationTally {
pub fn from_artifact(root: &Value) -> Self {
let entries = root
.get("context")
.and_then(|c| c.get("envelope"))
.and_then(|e| e.get("compilations"))
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
Self::from_states(entries.iter().filter_map(|entry| {
let state = entry.get("state")?.as_str()?;
let reason = entry.get("reason").and_then(Value::as_str).unwrap_or("");
Some((state, reason))
}))
}
pub fn from_states<'a>(states: impl IntoIterator<Item = (&'a str, &'a str)>) -> Self {
let mut tally = CompilationTally::default();
for (state, reason) in states {
match state {
"complete" => tally.total += 1,
"partial" | "failed" => {
tally.total += 1;
let reason = if reason.is_empty() {
UNSPECIFIED.to_string()
} else {
printable(reason)
};
*tally.incomplete_by_reason.entry(reason).or_insert(0) += 1;
}
_ => {}
}
}
tally
}
pub fn total(&self) -> usize {
self.total
}
pub fn partial(&self) -> usize {
self.incomplete_by_reason.values().sum()
}
pub fn none_complete(&self) -> bool {
self.total > 0 && self.partial() == self.total
}
pub fn text(&self) -> String {
self.incomplete_by_reason
.iter()
.map(|(reason, n)| format!("{reason} {n}"))
.collect::<Vec<_>>()
.join(", ")
}
pub fn summary(&self) -> Option<String> {
(self.partial() > 0).then(|| {
format!(
"{} of {} compilations: {}",
self.partial(),
self.total,
self.text()
)
})
}
pub fn detail(&self) -> Option<String> {
(self.partial() > 0).then(|| {
format!(
"{} of {} compilations partial: {}",
self.partial(),
self.total,
self.text()
)
})
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn complete_partial_and_failed_are_counted_and_every_other_state_is_not() {
let tally = CompilationTally::from_states([
("complete", "complete"),
("partial", "workspace-failure"),
("failed", "workspace-failure"),
("unsupported", "no-such-target"),
("excluded", "not-requested"),
("pending", "unknown"),
("Complete", "complete"),
]);
assert_eq!(tally.total(), 3);
assert_eq!(tally.partial(), 2);
assert_eq!(tally.text(), "workspace-failure 2");
}
#[test]
fn an_empty_reason_tallies_as_unspecified() {
let tally = CompilationTally::from_states([("partial", ""), ("failed", "binding-error")]);
assert_eq!(tally.text(), "binding-error 1, unspecified 1");
}
#[test]
fn a_control_character_in_a_reason_is_escaped_so_the_report_stays_one_line() {
let tally = CompilationTally::from_states([("partial", "line one\nline two\t\u{1b}[0m")]);
assert_eq!(tally.text(), "line one\\nline two\\t\\u{1b}[0m 1");
assert!(!tally.detail().unwrap().contains(char::is_control));
}
#[test]
fn reasons_are_listed_in_ascending_byte_order() {
let tally = CompilationTally::from_states([
("partial", "workspace-failure"),
("partial", "unrestored"),
("partial", "binding-error"),
("partial", "workspace-failure"),
("partial", "Zeta"),
]);
assert_eq!(
tally.text(),
"Zeta 1, binding-error 1, unrestored 1, workspace-failure 2"
);
}
#[test]
fn summary_and_detail_appear_only_with_an_incomplete_compilation() {
let healthy = CompilationTally::from_states([("complete", "complete")]);
assert_eq!(healthy.summary(), None);
assert_eq!(healthy.detail(), None);
assert!(!healthy.none_complete());
let partial = CompilationTally::from_states([
("complete", "complete"),
("complete", "complete"),
("complete", "complete"),
("partial", "workspace-failure"),
("partial", "workspace-failure"),
("partial", "workspace-failure"),
]);
assert_eq!(
partial.summary().as_deref(),
Some("3 of 6 compilations: workspace-failure 3")
);
assert_eq!(
partial.detail().as_deref(),
Some("3 of 6 compilations partial: workspace-failure 3")
);
assert!(!partial.none_complete());
}
#[test]
fn none_complete_needs_at_least_one_counted_compilation() {
assert!(!CompilationTally::default().none_complete());
assert!(!CompilationTally::from_states([("excluded", "not-requested")]).none_complete());
assert!(CompilationTally::from_states([("failed", "workspace-failure")]).none_complete());
}
#[test]
fn the_artifact_tally_reads_only_the_envelope_compilations() {
let root = json!({
"context": {"envelope": {"compilations": [
{"identity": {"projectName": "A"}, "state": "complete", "reason": "complete"},
{"identity": {"projectName": "B"}, "state": "partial", "reason": "workspace-failure"},
{"identity": {"projectName": "C"}, "state": "partial"},
{"identity": {"projectName": "D"}},
{"identity": {"projectName": "E"}, "state": 3, "reason": "binding-error"},
"not an object",
]}},
"compilations": [{"state": "partial", "reason": "elsewhere"}],
});
let tally = CompilationTally::from_artifact(&root);
assert_eq!(tally.total(), 3);
assert_eq!(tally.text(), "unspecified 1, workspace-failure 1");
}
#[test]
fn an_artifact_without_an_envelope_list_tallies_nothing() {
for root in [
json!({}),
json!({"context": {"envelope": {}}}),
json!({"context": {"envelope": {"compilations": {}}}}),
json!([]),
] {
assert_eq!(
CompilationTally::from_artifact(&root),
CompilationTally::default()
);
}
}
}