use std::collections::HashMap;
use crate::domain::model::issue::{Issue, IssueRelationship};
use crate::domain::model::record_ref::IssueRef;
use crate::domain::model::status::RollupHistogram;
pub fn compute_status_rollup(direct_children: &[&Issue]) -> Option<RollupHistogram> {
if direct_children.is_empty() {
return None;
}
Some(RollupHistogram::from_categories(
direct_children.iter().map(|c| c.status.category),
))
}
pub fn index_issues_by_id(issues: &[Issue]) -> HashMap<IssueRef, &Issue> {
issues.iter().map(|i| (i.id.clone(), i)).collect()
}
pub fn compute_status_rollup_via_map(
issue: &Issue,
by_id: &HashMap<IssueRef, &Issue>,
) -> Option<RollupHistogram> {
let child_refs: Vec<&Issue> = issue
.links
.iter()
.filter(|l| l.relationship == IssueRelationship::ParentOf)
.filter_map(|l| by_id.get(&l.target).copied())
.collect();
compute_status_rollup(&child_refs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::status::{StatusCategory, StatusesConfig};
use crate::domain::usecases::issue::tests::{enrich_issue, feature, ir, IssueFixture};
#[test]
fn no_children_returns_none() {
scenario().when_rollup().then_none();
}
#[test]
fn single_child_carries_its_category() {
scenario()
.with_child(feature("Child").status("open"))
.when_rollup()
.then_histogram_total(1)
.then_category(StatusCategory::Queued);
}
#[test]
fn histogram_counts_each_category_bucket() {
scenario()
.with_child(feature("A").status("open"))
.with_child(feature("B").status("open"))
.with_child(feature("C").status("in-progress"))
.with_child(feature("D").status("closed"))
.with_child(feature("E").status("closed"))
.when_rollup()
.then_histogram_total(5)
.then_queued(2)
.then_active(1)
.then_resolved(2)
.then_category(StatusCategory::Active);
}
#[test]
fn fully_resolved_with_one_unstarted_keeps_queued_signal() {
scenario()
.with_child(feature("A").status("closed"))
.with_child(feature("B").status("closed"))
.with_child(feature("C").status("closed"))
.with_child(feature("D").status("open"))
.when_rollup()
.then_category(StatusCategory::Queued);
}
struct Scenario {
children: Vec<Issue>,
}
fn scenario() -> Scenario {
Scenario { children: vec![] }
}
impl Scenario {
fn with_child(mut self, fixture: IssueFixture) -> Self {
let id = self.children.len() as u64 + 1;
let mut child = fixture.build(ir(id));
enrich_issue(&mut child, &StatusesConfig::default_issue());
self.children.push(child);
self
}
fn when_rollup(self) -> RollupOutcome {
let refs: Vec<&Issue> = self.children.iter().collect();
let result = compute_status_rollup(&refs);
RollupOutcome { result }
}
}
struct RollupOutcome {
result: Option<RollupHistogram>,
}
impl RollupOutcome {
fn then_none(self) -> Self {
assert!(self.result.is_none(), "expected None");
self
}
fn unwrap_some(&self) -> &RollupHistogram {
self.result.as_ref().expect("expected Some(histogram)")
}
fn then_histogram_total(self, expected: u32) -> Self {
assert_eq!(self.unwrap_some().total(), expected, "total mismatch");
self
}
fn then_queued(self, expected: u32) -> Self {
assert_eq!(self.unwrap_some().queued, expected, "queued mismatch");
self
}
fn then_active(self, expected: u32) -> Self {
assert_eq!(self.unwrap_some().active, expected, "active mismatch");
self
}
fn then_resolved(self, expected: u32) -> Self {
assert_eq!(self.unwrap_some().resolved, expected, "resolved mismatch");
self
}
fn then_category(self, expected: StatusCategory) -> Self {
assert_eq!(self.unwrap_some().category(), expected, "category mismatch");
self
}
}
}