omni_dev/coverage/
merge.rs1use std::path::Path;
9
10use anyhow::{bail, Result};
11
12use super::model::CoverageReport;
13
14pub fn check_shard(
22 label: &str,
23 report: &CoverageReport,
24 prefix: Option<&Path>,
25 warnings: &mut Vec<String>,
26) -> Result<()> {
27 require_executable_lines(label, report)?;
28 if let Some(prefix) = prefix {
29 warnings.extend(prefix_mismatch(label, report, prefix));
30 }
31 Ok(())
32}
33
34pub fn require_executable_lines(label: &str, report: &CoverageReport) -> Result<()> {
45 if report.total_lines() == 0 {
46 bail!(
47 "coverage shard {label} has no executable lines; \
48 a failed or empty shard would silently lower the merged coverage"
49 );
50 }
51 Ok(())
52}
53
54pub fn prefix_mismatch(label: &str, report: &CoverageReport, prefix: &Path) -> Option<String> {
69 let prefix = prefix.to_string_lossy();
70 let prefix_slash = format!("{}/", prefix.trim_end_matches('/'));
71 let mut absolute = 0_usize;
72 for path in report.files.keys() {
73 if path.starts_with(&prefix_slash) {
74 return None;
75 }
76 if path.starts_with('/') {
77 absolute += 1;
78 }
79 }
80 (absolute > 0).then(|| {
81 format!(
82 "coverage shard {label}: none of its {absolute} absolute file path(s) is under \
83 `{}`, so it was probably measured under a different workspace root and its \
84 files will not line up with the other shards or the diff; pass --strip-prefix \
85 with that root, or run every shard under the same one",
86 prefix.trim_end_matches('/')
87 )
88 })
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::expect_used)]
93mod tests {
94 use super::*;
95 use crate::coverage::model::FileCoverage;
96
97 fn report(files: &[(&str, &[(u32, u64)])]) -> CoverageReport {
98 let mut report = CoverageReport::new();
99 for (path, lines) in files {
100 let mut file = FileCoverage::new(*path);
101 for &(line, hits) in *lines {
102 file.record(line, hits);
103 }
104 report.insert(file);
105 }
106 report
107 }
108
109 #[test]
110 fn an_empty_report_is_rejected_by_name() {
111 let message = require_executable_lines("shard-2.lcov", &CoverageReport::new())
112 .unwrap_err()
113 .to_string();
114 assert!(message.contains("shard-2.lcov"), "{message}");
115 assert!(message.contains("no executable lines"), "{message}");
116 }
117
118 #[test]
119 fn a_report_whose_files_have_no_lines_is_rejected() {
120 let report = report(&[("src/a.rs", &[])]);
122 assert!(require_executable_lines("s", &report).is_err());
123 }
124
125 #[test]
126 fn a_report_with_executable_lines_passes() {
127 let report = report(&[("src/a.rs", &[(1, 0)])]);
128 require_executable_lines("s", &report).unwrap();
129 }
130
131 #[test]
132 fn check_shard_fails_an_empty_shard_and_notes_a_foreign_one() {
133 let mut warnings = Vec::new();
134 let prefix = Path::new("/home/runner/work/repo");
135
136 let empty = check_shard("s", &CoverageReport::new(), Some(prefix), &mut warnings);
137 assert!(empty.is_err());
138 assert!(warnings.is_empty());
139
140 let foreign = report(&[("/Users/dev/work/repo/src/a.rs", &[(1, 1)])]);
141 check_shard("s", &foreign, Some(prefix), &mut warnings).unwrap();
142 assert_eq!(warnings.len(), 1, "{warnings:?}");
143
144 check_shard("s", &foreign, None, &mut warnings).unwrap();
146 assert_eq!(warnings.len(), 1, "{warnings:?}");
147 }
148
149 #[test]
150 fn a_shard_under_another_root_is_flagged() {
151 let report = report(&[
152 ("/Users/dev/work/repo/src/a.rs", &[(1, 1)]),
153 ("/Users/dev/work/repo/src/b.rs", &[(1, 1)]),
154 ]);
155 let message =
156 prefix_mismatch("shard-2.lcov", &report, Path::new("/home/runner/work/repo")).unwrap();
157 assert!(message.contains("shard-2.lcov"), "{message}");
158 assert!(message.contains("2 absolute file path(s)"), "{message}");
159 assert!(message.contains("`/home/runner/work/repo`"), "{message}");
160 assert!(message.contains("--strip-prefix"), "{message}");
161 }
162
163 #[test]
164 fn a_shard_with_some_in_tree_paths_is_not_flagged() {
165 let report = report(&[
168 ("/home/runner/work/repo/src/a.rs", &[(1, 1)]),
169 ("/rustc/abc123/library/core/src/option.rs", &[(1, 1)]),
170 ]);
171 assert_eq!(
172 prefix_mismatch("s", &report, Path::new("/home/runner/work/repo")),
173 None
174 );
175 }
176
177 #[test]
178 fn relative_paths_are_not_flagged() {
179 let report = report(&[("src/a.rs", &[(1, 1)])]);
180 assert_eq!(
181 prefix_mismatch("s", &report, Path::new("/home/runner/work/repo")),
182 None
183 );
184 }
185
186 #[test]
187 fn a_prefix_that_is_only_a_string_prefix_does_not_match() {
188 let report = report(&[("/work/repo-two/src/a.rs", &[(1, 1)])]);
191 assert!(prefix_mismatch("s", &report, Path::new("/work/repo")).is_some());
192 }
193
194 #[test]
195 fn a_trailing_slash_on_the_prefix_is_ignored() {
196 let report = report(&[("/work/repo/src/a.rs", &[(1, 1)])]);
197 assert_eq!(
198 prefix_mismatch("s", &report, Path::new("/work/repo/")),
199 None
200 );
201 }
202}