1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct ReportTotals {
8 #[serde(default)]
9 pub files: i64,
10 #[serde(default)]
11 pub lines: i64,
12 #[serde(default)]
13 pub hits: i64,
14 #[serde(default)]
15 pub misses: i64,
16 #[serde(default)]
17 pub partials: i64,
18 #[serde(default)]
19 pub coverage: Option<serde_json::Value>,
20 #[serde(default)]
21 pub branches: i64,
22 #[serde(default)]
23 pub methods: i64,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ReportFile {
29 pub name: String,
30 pub totals: ReportTotals,
31 #[serde(default)]
32 pub line_coverage: Vec<serde_json::Value>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CoverageReport {
38 pub totals: ReportTotals,
39 #[serde(default)]
40 pub files: Vec<ReportFile>,
41 #[serde(default)]
42 pub commit_file_url: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct MissFile {
48 pub name: String,
49 pub misses: i64,
50 pub lines: i64,
51 pub hits: i64,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub coverage: Option<serde_json::Value>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct MissFilesResult {
59 pub totals: ReportTotals,
60 pub files: Vec<MissFile>,
61 pub returned: usize,
62}
63
64#[must_use]
68pub fn miss_files_from_report(report: &CoverageReport, limit: usize) -> Vec<MissFile> {
69 let mut out: Vec<MissFile> = report
70 .files
71 .iter()
72 .filter(|f| f.totals.misses > 0)
73 .map(|f| MissFile {
74 name: f.name.clone(),
75 misses: f.totals.misses,
76 lines: f.totals.lines,
77 hits: f.totals.hits,
78 coverage: f.totals.coverage.clone(),
79 })
80 .collect();
81 out.sort_by(|a, b| b.misses.cmp(&a.misses).then_with(|| a.name.cmp(&b.name)));
82 if limit > 0 && out.len() > limit {
83 out.truncate(limit);
84 }
85 out
86}
87
88#[must_use]
90pub fn miss_files_result(report: &CoverageReport, limit: usize) -> MissFilesResult {
91 let files = miss_files_from_report(report, limit);
92 let returned = files.len();
93 MissFilesResult {
94 totals: report.totals.clone(),
95 files,
96 returned,
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use serde_json::json;
104
105 fn file(name: &str, misses: i64) -> ReportFile {
106 ReportFile {
107 name: name.to_string(),
108 totals: ReportTotals {
109 misses,
110 lines: 10,
111 hits: 10 - misses,
112 coverage: Some(json!(90.0)),
113 ..ReportTotals::default()
114 },
115 line_coverage: Vec::new(),
116 }
117 }
118
119 #[test]
120 fn miss_files_filters_sorts_and_limits() {
121 let report = CoverageReport {
122 totals: ReportTotals {
123 misses: 20,
124 ..ReportTotals::default()
125 },
126 files: vec![
127 file("a.rs", 0),
128 file("b.rs", 5),
129 file("c.rs", 12),
130 file("d.rs", 3),
131 file("e.rs", 5),
132 ],
133 commit_file_url: None,
134 };
135 let result = miss_files_result(&report, 2);
136 assert_eq!(result.returned, 2);
137 assert_eq!(result.totals.misses, 20);
138 assert_eq!(result.files.len(), 2);
139 assert_eq!(result.files[0].name, "c.rs");
140 assert_eq!(result.files[0].misses, 12);
141 assert_eq!(result.files[0].coverage, Some(json!(90.0)));
142 assert_eq!(result.files[1].name, "b.rs");
143 assert_eq!(result.files[1].misses, 5);
144
145 let tied = miss_files_from_report(&report, 0);
147 assert_eq!(tied.iter().filter(|f| f.misses == 5).count(), 2);
148 let names: Vec<&str> = tied
149 .iter()
150 .filter(|f| f.misses == 5)
151 .map(|f| f.name.as_str())
152 .collect();
153 assert_eq!(names, ["b.rs", "e.rs"]);
154 let debug = format!("{result:?}");
155 assert!(debug.contains("returned"));
156 }
157
158 #[test]
159 fn miss_files_empty_when_no_misses() {
160 let report = CoverageReport {
161 totals: ReportTotals::default(),
162 files: vec![file("ok.rs", 0)],
163 commit_file_url: None,
164 };
165 let result = miss_files_result(&report, 30);
166 assert!(result.files.is_empty());
167 assert_eq!(result.returned, 0);
168 }
169
170 #[test]
171 fn miss_files_limit_zero_returns_all() {
172 let report = CoverageReport {
173 totals: ReportTotals::default(),
174 files: vec![file("a.rs", 1), file("b.rs", 2), file("c.rs", 3)],
175 commit_file_url: None,
176 };
177 let files = miss_files_from_report(&report, 0);
178 assert_eq!(files.len(), 3);
179 assert_eq!(files[0].name, "c.rs");
180 let result = miss_files_result(&report, 0);
181 assert_eq!(result.returned, 3);
182 assert_eq!(result.files.len(), 3);
183 }
184}