use std::collections::BTreeSet;
use crate::elements::Report;
pub(crate) const MAX_SHARDS: u32 = 65_536;
#[derive(Debug, Default)]
pub struct Merged {
pub report: Option<Report>,
pub fresh: usize,
pub stale: usize,
pub freshness_unavailable: bool,
pub never_tested: usize,
pub shards_seen: BTreeSet<u32>,
pub shard_count: Option<u32>,
pub inconsistent: Vec<String>,
pub detected: usize,
pub valid: usize,
pub withdrawn: usize,
pub unchecked: usize,
pub incompatible: usize,
}
impl Merged {
#[must_use]
pub fn score(&self) -> f64 {
if self.valid == 0 {
return 100.0;
}
#[expect(clippy::cast_precision_loss, reason = "mutant counts are far below the f64 integer limit")]
let ratio = self.detected as f64 / self.valid as f64;
ratio * 100.0
}
#[must_use]
pub fn scored(&self) -> Option<f64> {
(self.valid > 0).then(|| self.score())
}
#[must_use]
pub fn coverage(&self) -> f64 {
let Some(count) = self.shard_count.filter(|count| *count > 0) else {
return 100.0;
};
#[expect(clippy::cast_precision_loss, reason = "shard counts are small")]
let ratio = self.shards_seen.len() as f64 / f64::from(count);
ratio * 100.0
}
#[must_use]
pub fn missing_shards(&self) -> Vec<u32> {
self.shard_count
.map(|count| {
(0..count)
.filter(|index| !self.shards_seen.contains(index))
.take(MAX_SHARDS as usize)
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures;
use crate::fixtures::{mutant_result_at as mutant, report_with as report};
#[test]
fn rotation_coverage_reports_the_shards_that_were_missed() {
let merged = super::super::merge(
&[
(
"a".to_owned(),
fixtures::report_with(Some((0, 4)), 100, vec![fixtures::mutant_result_at("aaa", 1, "Killed")]),
),
(
"b".to_owned(),
fixtures::report_with(Some((2, 4)), 200, vec![fixtures::mutant_result_at("bbb", 2, "Killed")]),
),
],
300,
None,
);
assert_eq!(merged.shards_seen.len(), 2);
assert_eq!(merged.missing_shards(), vec![1, 3]);
assert!((merged.coverage() - 50.0).abs() < f64::EPSILON);
}
#[test]
fn unsharded_reports_merge_and_report_full_coverage() {
let merged = super::super::merge(&[("a".to_owned(), report(None, 100, vec![mutant("aaa", 1, "Killed")]))], 200, None);
assert!(merged.shards_seen.is_empty());
assert!((merged.coverage() - 100.0).abs() < f64::EPSILON);
assert!(merged.missing_shards().is_empty());
}
#[test]
fn an_unchecked_shard_count_is_reported_up_to_the_bound_and_no_further() {
let merged = Merged {
shard_count: Some(u32::MAX),
shards_seen: BTreeSet::from([0, 2]),
..Merged::default()
};
let missing = merged.missing_shards();
assert_eq!(missing.len(), MAX_SHARDS as usize);
assert_eq!(missing[0], 1);
assert_eq!(missing[1], 3);
}
#[test]
fn a_rotation_at_the_bound_still_reports_every_missing_shard() {
let merged = Merged {
shard_count: Some(MAX_SHARDS),
shards_seen: BTreeSet::from([0]),
..Merged::default()
};
assert_eq!(merged.missing_shards().len(), MAX_SHARDS as usize - 1);
}
}