Skip to main content

stratifykit_core/
coverage.rs

1/// floor(value / bucket_size) * bucket_size. `bucket_size == 0` is a caller error, expected to be
2/// checked and rejected at the caller's entry point, not deep inside this pure helper.
3pub fn bucket_floor(value: u32, bucket_size: u32) -> u32 {
4    (value / bucket_size) * bucket_size
5}
6
7pub fn mean_of(counts: &[usize]) -> f64 {
8    if counts.is_empty() {
9        0.0
10    } else {
11        counts.iter().sum::<usize>() as f64 / counts.len() as f64
12    }
13}
14
15/// Coverage classification for one bucket in an enumerated bucket space -- e.g. "every
16/// phase x side x eval-bucket combination", where a combination nobody observed still needs a
17/// verdict (`Missing`), not just silent absence from a tally.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BucketStatus {
20    Missing,
21    Under,
22    Ok,
23    Over,
24}
25
26impl std::fmt::Display for BucketStatus {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(match self {
29            BucketStatus::Missing => "MISSING",
30            BucketStatus::Under => "UNDER",
31            BucketStatus::Ok => "OK",
32            BucketStatus::Over => "OVER",
33        })
34    }
35}
36
37/// Ratio-to-mean classification: `count == 0` is always `Missing`; otherwise `Under`/`Over` when
38/// `count` falls outside `[under_ratio, over_ratio] * mean`, else `Ok`.
39pub fn classify_bucket(count: usize, mean: f64, under_ratio: f64, over_ratio: f64) -> BucketStatus {
40    if count == 0 {
41        return BucketStatus::Missing;
42    }
43    let count = count as f64;
44    if count < under_ratio * mean {
45        BucketStatus::Under
46    } else if count > over_ratio * mean {
47        BucketStatus::Over
48    } else {
49        BucketStatus::Ok
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn bucket_floor_rounds_down_to_multiple() {
59        assert_eq!(bucket_floor(45, 10), 40);
60        assert_eq!(bucket_floor(40, 10), 40);
61        assert_eq!(bucket_floor(9, 10), 0);
62    }
63
64    #[test]
65    fn mean_of_empty_is_zero() {
66        assert_eq!(mean_of(&[]), 0.0);
67    }
68
69    #[test]
70    fn mean_of_averages() {
71        assert_eq!(mean_of(&[2, 4, 6]), 4.0);
72    }
73
74    #[test]
75    fn classify_bucket_flags_missing_under_over_ok() {
76        assert_eq!(classify_bucket(0, 10.0, 0.5, 2.0), BucketStatus::Missing);
77        assert_eq!(classify_bucket(2, 10.0, 0.5, 2.0), BucketStatus::Under);
78        assert_eq!(classify_bucket(25, 10.0, 0.5, 2.0), BucketStatus::Over);
79        assert_eq!(classify_bucket(10, 10.0, 0.5, 2.0), BucketStatus::Ok);
80    }
81}