use serde::{Deserialize, Serialize};
use super::category::StatusCategory;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RollupHistogram {
pub queued: u32,
pub active: u32,
pub stalled: u32,
pub resolved: u32,
pub cancelled: u32,
}
impl RollupHistogram {
pub fn from_categories(it: impl IntoIterator<Item = StatusCategory>) -> Self {
let mut h = Self::default();
for cat in it {
match cat {
StatusCategory::Queued => h.queued += 1,
StatusCategory::Active => h.active += 1,
StatusCategory::Stalled => h.stalled += 1,
StatusCategory::Resolved => h.resolved += 1,
StatusCategory::Cancelled => h.cancelled += 1,
StatusCategory::Unknown => {}
}
}
h
}
pub fn total(&self) -> u32 {
self.queued + self.active + self.stalled + self.resolved + self.cancelled
}
pub fn category(&self) -> StatusCategory {
let ranked = [
(self.active, StatusCategory::Active),
(self.stalled, StatusCategory::Stalled),
(self.queued, StatusCategory::Queued),
(self.resolved, StatusCategory::Resolved),
(self.cancelled, StatusCategory::Cancelled),
];
ranked
.into_iter()
.find(|(n, _)| *n > 0)
.map(|(_, cat)| cat)
.unwrap_or(StatusCategory::Unknown)
}
}
#[cfg(test)]
pub mod strategy {
use super::RollupHistogram;
use proptest::prelude::*;
prop_compose! {
pub fn rollup_histogram()(
queued in 0u32..=10,
active in 0u32..=10,
stalled in 0u32..=10,
resolved in 0u32..=10,
cancelled in 0u32..=10,
) -> RollupHistogram {
RollupHistogram { queued, active, stalled, resolved, cancelled }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use StatusCategory::{
Active as A, Cancelled as C, Queued as Q, Resolved as R, Stalled as S, Unknown as U,
};
proptest! {
#[test]
fn total_equals_sum_of_buckets(h in strategy::rollup_histogram()) {
prop_assert_eq!(
h.total(),
h.queued + h.active + h.stalled + h.resolved + h.cancelled
);
}
#[test]
fn empty_histogram_categorises_as_unknown(_ in Just(())) {
prop_assert_eq!(RollupHistogram::default().category(), StatusCategory::Unknown);
}
}
#[test]
fn from_categories_counts_each_bucket() {
let h = RollupHistogram::from_categories([Q, Q, A, S, S, S, R, C]);
assert_eq!(h.queued, 2);
assert_eq!(h.active, 1);
assert_eq!(h.stalled, 3);
assert_eq!(h.resolved, 1);
assert_eq!(h.cancelled, 1);
}
#[test]
fn unknown_children_are_not_counted() {
let h = RollupHistogram::from_categories([U, U, Q, S]);
assert_eq!(h.total(), 2, "Unknown is dropped from the histogram");
assert_eq!(h.queued, 1);
assert_eq!(h.stalled, 1);
}
#[test]
fn empty_iterator_is_all_zero() {
let h = RollupHistogram::from_categories(std::iter::empty());
assert_eq!(h.total(), 0);
assert_eq!(h.category(), U);
}
#[test]
fn category_picks_highest_rank_non_zero_bucket() {
assert_eq!(
RollupHistogram::from_categories([A, S, Q, R, C]).category(),
A
);
assert_eq!(RollupHistogram::from_categories([S, Q, R, C]).category(), S);
assert_eq!(RollupHistogram::from_categories([Q, R, C]).category(), Q);
assert_eq!(RollupHistogram::from_categories([R, C]).category(), R);
assert_eq!(RollupHistogram::from_categories([C]).category(), C);
}
#[test]
fn category_matches_direct_fold() {
let cases: &[&[StatusCategory]] = &[
&[Q, Q, S],
&[Q, Q, Q, A],
&[S, S, R],
&[Q, R],
&[Q, C],
&[R, R, R, R, R, R, R, R, R, S],
&[R, R, R, R, R, R, R, R, R, C],
&[R, C, C],
&[C, C, C, C],
&[Q, A, S, R, C],
&[U, U, Q, Q],
];
for children in cases {
let from_histogram =
RollupHistogram::from_categories(children.iter().copied()).category();
let from_fold = StatusCategory::fold_categories(children.iter().copied()).unwrap_or(U);
assert_eq!(
from_histogram, from_fold,
"histogram/fold mismatch on {children:?}: \
histogram={from_histogram}, fold={from_fold}"
);
}
}
}