Skip to main content

covy_core/
model.rs

1use roaring::RoaringBitmap;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// Source format of a coverage report.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum CoverageFormat {
8    Lcov,
9    Cobertura,
10    JaCoCo,
11    GoCov,
12    LlvmCov,
13}
14
15impl std::fmt::Display for CoverageFormat {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            CoverageFormat::Lcov => write!(f, "lcov"),
19            CoverageFormat::Cobertura => write!(f, "cobertura"),
20            CoverageFormat::JaCoCo => write!(f, "jacoco"),
21            CoverageFormat::GoCov => write!(f, "gocov"),
22            CoverageFormat::LlvmCov => write!(f, "llvm-cov"),
23        }
24    }
25}
26
27/// Coverage data for a single file.
28#[derive(Debug, Clone)]
29pub struct FileCoverage {
30    /// Lines that were executed at least once.
31    pub lines_covered: RoaringBitmap,
32    /// Lines that are instrumented (could be executed).
33    pub lines_instrumented: RoaringBitmap,
34    /// Branch coverage: (line, block) → taken count. Optional.
35    pub branches: BTreeMap<(u32, u32), u64>,
36    /// Function coverage: name → hit count. Optional.
37    pub functions: BTreeMap<String, u64>,
38}
39
40impl FileCoverage {
41    pub fn new() -> Self {
42        Self {
43            lines_covered: RoaringBitmap::new(),
44            lines_instrumented: RoaringBitmap::new(),
45            branches: BTreeMap::new(),
46            functions: BTreeMap::new(),
47        }
48    }
49
50    /// Line coverage percentage (0.0–100.0). Returns None if no instrumented lines.
51    pub fn line_coverage_pct(&self) -> Option<f64> {
52        let instrumented = self.lines_instrumented.len() as f64;
53        if instrumented == 0.0 {
54            return None;
55        }
56        Some((self.lines_covered.len() as f64 / instrumented) * 100.0)
57    }
58
59    /// Merge another FileCoverage into this one (OR for bitmaps, sum for counts).
60    pub fn merge(&mut self, other: &FileCoverage) {
61        self.lines_covered |= &other.lines_covered;
62        self.lines_instrumented |= &other.lines_instrumented;
63        for (&key, &count) in &other.branches {
64            *self.branches.entry(key).or_insert(0) += count;
65        }
66        for (name, &count) in &other.functions {
67            *self.functions.entry(name.clone()).or_insert(0) += count;
68        }
69    }
70}
71
72impl Default for FileCoverage {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78/// Aggregated coverage data from one or more reports.
79#[derive(Debug, Clone)]
80pub struct CoverageData {
81    /// Per-file coverage, keyed by relative path.
82    pub files: BTreeMap<String, FileCoverage>,
83    /// Source format(s) that produced this data.
84    pub format: Option<CoverageFormat>,
85    /// When the coverage was ingested (Unix timestamp).
86    pub timestamp: u64,
87}
88
89impl CoverageData {
90    pub fn new() -> Self {
91        Self {
92            files: BTreeMap::new(),
93            format: None,
94            timestamp: std::time::SystemTime::now()
95                .duration_since(std::time::UNIX_EPOCH)
96                .unwrap_or_default()
97                .as_secs(),
98        }
99    }
100
101    /// Total line coverage percentage across all files.
102    pub fn total_coverage_pct(&self) -> Option<f64> {
103        let mut total_covered = 0u64;
104        let mut total_instrumented = 0u64;
105        for fc in self.files.values() {
106            total_covered += fc.lines_covered.len();
107            total_instrumented += fc.lines_instrumented.len();
108        }
109        if total_instrumented == 0 {
110            return None;
111        }
112        Some((total_covered as f64 / total_instrumented as f64) * 100.0)
113    }
114
115    /// Merge another CoverageData into this one.
116    pub fn merge(&mut self, other: &CoverageData) {
117        for (path, fc) in &other.files {
118            self.files
119                .entry(path.clone())
120                .or_insert_with(FileCoverage::new)
121                .merge(fc);
122        }
123    }
124}
125
126impl Default for CoverageData {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132/// Status of a file in a diff.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134pub enum DiffStatus {
135    Added,
136    Modified,
137    Deleted,
138    Renamed,
139}
140
141/// Diff information for a single file.
142#[derive(Debug, Clone)]
143pub struct FileDiff {
144    pub path: String,
145    pub old_path: Option<String>,
146    pub status: DiffStatus,
147    /// Lines changed in the new version of the file.
148    pub changed_lines: RoaringBitmap,
149}
150
151/// Counts of issues on changed lines, used for issue gate reporting.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct IssueGateCounts {
154    pub changed_errors: u32,
155    pub changed_warnings: u32,
156    pub changed_notes: u32,
157    pub total_issues: usize,
158}
159
160/// Result of a quality gate evaluation.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct QualityGateResult {
163    pub passed: bool,
164    pub total_coverage_pct: Option<f64>,
165    pub changed_coverage_pct: Option<f64>,
166    pub new_file_coverage_pct: Option<f64>,
167    pub violations: Vec<String>,
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub issue_counts: Option<IssueGateCounts>,
170}
171
172/// Repository snapshot for cache invalidation.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct RepoSnapshot {
175    /// BLAKE3 Merkle root of all file hashes.
176    pub merkle_root: String,
177    /// Per-file content hashes.
178    pub file_hashes: BTreeMap<String, String>,
179}