use std::collections::{BTreeMap, BTreeSet};
use crate::model::apply::PathApplyOutcome;
use crate::model::batch::BatchResult;
use crate::model::names::{ProjectName, RelativePath};
use crate::model::resolver_coverage::ResolverClass;
use crate::model::verdict::{BumpStatus, SeriesSummary, Verdict, exit, non_self_tracked};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoundClearance {
Clean(ClearanceFacts),
ZeroDomain(ClearanceFacts),
Findings(ClearanceFacts),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClearanceFacts {
pub candidates: usize,
pub series: usize,
pub rows: usize,
pub tracked: u32,
pub verdicts: SeriesSummary,
pub reasons: ReasonHistogram,
pub bumps: BumpSummary,
pub apply: ApplyRollup,
pub target: TargetFindingsRollup,
pub suites: Vec<String>,
pub exit_code: i32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReasonHistogram(BTreeMap<&'static str, u32>);
impl ReasonHistogram {
fn record(&mut self, label: &'static str) {
*self.0.entry(label).or_insert(0) += 1;
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn total(&self) -> u32 {
self.0.values().copied().sum()
}
#[must_use]
pub fn count(&self, label: &str) -> u32 {
self.0.get(label).copied().unwrap_or(0)
}
#[must_use]
pub fn ranked(&self) -> Vec<(&'static str, u32)> {
let mut ranked: Vec<(&'static str, u32)> = self.0.iter().map(|(k, n)| (*k, *n)).collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
ranked
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BumpSummary {
pub total: u32,
pub snapshot_present: u32,
pub snapshot_missing: u32,
pub untracked: u32,
pub unassessed: u32,
pub introduced: u32,
}
impl BumpSummary {
#[must_use]
pub fn is_empty(&self) -> bool {
self.total == 0
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApplyRollup {
pub rows_with_forecast: usize,
pub conflicted_rows: usize,
pub conflicted_paths: Vec<RelativePath>,
pub by_kind: BTreeMap<&'static str, u32>,
pub unassessed_paths: u32,
}
impl ApplyRollup {
#[must_use]
pub fn has_conflict(&self) -> bool {
self.conflicted_rows > 0
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TargetFindingsRollup {
pub rows_with_findings: usize,
pub finding_rows: usize,
pub by_class: Vec<(ResolverClass, usize)>,
pub unclassified: usize,
}
impl TargetFindingsRollup {
#[must_use]
pub fn has_findings(&self) -> bool {
self.finding_rows > 0
}
}
impl RoundClearance {
#[must_use]
pub fn from_results(results: &[BatchResult], self_projects: &BTreeSet<ProjectName>) -> Self {
let mut commits = BTreeSet::new();
let mut series = BTreeSet::new();
let mut tracked: u32 = 0;
let mut verdicts = SeriesSummary::default();
let mut reasons = ReasonHistogram::default();
let mut suites = BTreeSet::new();
let mut bumps = BumpSummary::default();
let mut seen_bumps = BTreeSet::new();
let mut snapshot_missing_bump = false;
let mut apply = ApplyRollup::default();
let mut conflicted_paths = BTreeSet::new();
let mut target = TargetFindingsRollup::default();
let mut target_by_class: BTreeMap<ResolverClass, usize> = BTreeMap::new();
for row in results {
commits.insert(&row.commit);
series.insert(&row.series);
verdicts.compatible += row.verdict.summary.compatible;
verdicts.requires_adaptation += row.verdict.summary.requires_adaptation;
verdicts.incompatible += row.verdict.summary.incompatible;
verdicts.inapplicable += row.verdict.summary.inapplicable;
tracked = tracked.saturating_add(non_self_tracked(&row.verdict.results, self_projects));
for pin in &row.verdict.results {
if let Verdict::Inapplicable { reason } = &pin.verdict {
reasons.record(reason.as_str());
}
}
for suite in &row.diagnostics.suggested_suites {
suites.insert(suite.clone());
}
if let Some(forecast) = &row.apply {
apply.rows_with_forecast += 1;
if forecast.has_conflict() {
apply.conflicted_rows += 1;
}
for (path, outcome) in &forecast.paths {
match outcome {
PathApplyOutcome::Conflict { kind } => {
conflicted_paths.insert(path.clone());
*apply.by_kind.entry(kind.as_str()).or_insert(0) += 1;
}
PathApplyOutcome::Unassessed { .. } => apply.unassessed_paths += 1,
PathApplyOutcome::CleanExact | PathApplyOutcome::CleanDrifted { .. } => {}
}
}
}
if let Some(findings) = &row.target_findings {
target.rows_with_findings += 1;
if !findings.is_empty() {
target.finding_rows += 1;
}
for (class, n) in findings.by_class() {
*target_by_class.entry(class).or_insert(0) += n;
}
target.unclassified += findings.unclassified();
}
for bump in &row.diagnostics.pin_bumps {
if !seen_bumps.insert((&row.commit, &bump.dep, bump.to.as_str())) {
continue;
}
bumps.total += 1;
if bump.from.is_none() {
bumps.introduced += 1;
}
match &bump.status {
Some(BumpStatus::SnapshotPresent) => bumps.snapshot_present += 1,
Some(BumpStatus::SnapshotMissing { .. }) => {
bumps.snapshot_missing += 1;
snapshot_missing_bump = true;
}
Some(BumpStatus::Untracked) => bumps.untracked += 1,
None => bumps.unassessed += 1,
}
}
}
apply.conflicted_paths = conflicted_paths.into_iter().collect();
target.by_class = target_by_class.into_iter().collect();
let blocking = verdicts.is_blocking();
let exit_code = if blocking || apply.has_conflict() || target.has_findings() {
exit::NEEDS_ATTENTION
} else {
exit::OK
};
let facts = ClearanceFacts {
candidates: commits.len(),
series: series.len(),
rows: results.len(),
tracked,
verdicts,
reasons,
bumps,
apply,
target,
suites: suites.into_iter().collect(),
exit_code,
};
let all_inapplicable = facts.verdicts.compatible == 0
&& facts.verdicts.requires_adaptation == 0
&& facts.verdicts.incompatible == 0
&& facts.verdicts.inapplicable > 0;
if blocking
|| tracked > 0
|| snapshot_missing_bump
|| facts.apply.has_conflict()
|| facts.target.has_findings()
{
Self::Findings(facts)
} else if all_inapplicable {
Self::ZeroDomain(facts)
} else {
Self::Clean(facts)
}
}
#[must_use]
pub fn facts(&self) -> &ClearanceFacts {
match self {
Self::Clean(f) | Self::ZeroDomain(f) | Self::Findings(f) => f,
}
}
#[must_use]
pub fn is_clean(&self) -> bool {
matches!(self, Self::Clean(_) | Self::ZeroDomain(_))
}
}