use crate::domain::model::issue::IssueView;
use crate::domain::model::status::StatusesConfig;
use super::helpers::{is_terminal, percentiles};
use super::Percentiles;
pub(super) fn compute_lead_time(
filtered: &IssueView<'_>,
statuses: &StatusesConfig,
) -> Option<Percentiles> {
let mut lt: Vec<f64> = filtered
.iter()
.filter(|i| i.status.terminal)
.filter_map(|i| {
i.events
.lead_time(&i.date, is_terminal(statuses))
.map(|d| d.as_days())
})
.collect();
lt.sort_by(|a, b| a.total_cmp(b));
percentiles(<)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::Issue;
use crate::domain::model::status::StatusesConfig;
use crate::domain::usecases::issue::tests::{enrich_issue, feature, ir, IssueFixture};
#[test]
fn returns_none_when_no_closed_issues() {
scenario()
.given(feature("Open").status("open").date("2026-03-01"))
.when_lead_time()
.then_none();
}
#[test]
fn computes_percentiles_from_closed_issues() {
scenario()
.given(
feature("T1")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-05T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.given(
feature("T2")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-09T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.given(
feature("T3")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-13T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_lead_time()
.then_percentiles(8.0, 12.0, 12.0);
}
#[test]
fn open_issues_are_excluded() {
scenario()
.given(
feature("Closed")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-05T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.given(feature("Open").status("open").date("2026-03-01"))
.when_lead_time()
.then_p50(4.0);
}
struct Scenario {
issues: Vec<Issue>,
}
fn scenario() -> Scenario {
Scenario { issues: vec![] }
}
impl Scenario {
fn given(mut self, fixture: IssueFixture) -> Self {
let id = self.issues.len() as u64 + 1;
let mut issue = fixture.build(ir(id));
enrich_issue(&mut issue, &StatusesConfig::default_issue());
self.issues.push(issue);
self
}
fn when_lead_time(self) -> LeadTimeOutcome {
let refs = IssueView::from_slice(&self.issues);
let result = compute_lead_time(&refs, &StatusesConfig::default_issue());
LeadTimeOutcome { result }
}
}
struct LeadTimeOutcome {
result: Option<Percentiles>,
}
impl LeadTimeOutcome {
fn then_none(self) -> Self {
assert!(self.result.is_none(), "expected None, got Some");
self
}
fn then_p50(self, expected: f64) -> Self {
let p = self.result.as_ref().expect("expected Some percentiles");
assert_eq!(p.p50, expected, "p50 mismatch");
self
}
fn then_percentiles(self, p50: f64, p85: f64, p95: f64) -> Self {
let p = self.result.as_ref().expect("expected Some percentiles");
assert_eq!(p.p50, p50, "p50 mismatch");
assert_eq!(p.p85, p85, "p85 mismatch");
assert_eq!(p.p95, p95, "p95 mismatch");
self
}
}
}