omni_dev/coverage/
merge.rs1use std::path::Path;
9
10use anyhow::{bail, Result};
11
12use super::model::CoverageReport;
13
14pub fn require_executable_lines(label: &str, report: &CoverageReport) -> Result<()> {
25 if report.total_lines() == 0 {
26 bail!(
27 "coverage shard {label} has no executable lines; \
28 a failed or empty shard would silently lower the merged coverage"
29 );
30 }
31 Ok(())
32}
33
34pub fn prefix_mismatch(label: &str, report: &CoverageReport, prefix: &Path) -> Option<String> {
49 let prefix = prefix.to_string_lossy();
50 let prefix_slash = format!("{}/", prefix.trim_end_matches('/'));
51 let mut absolute = 0_usize;
52 for path in report.files.keys() {
53 if path.starts_with(&prefix_slash) {
54 return None;
55 }
56 if path.starts_with('/') {
57 absolute += 1;
58 }
59 }
60 (absolute > 0).then(|| {
61 format!(
62 "coverage shard {label}: none of its {absolute} absolute file path(s) is under \
63 `{}`, so it was probably measured under a different workspace root and its \
64 files will not line up with the diff; pass --strip-prefix with that root, or \
65 run every shard under the same one",
66 prefix.trim_end_matches('/')
67 )
68 })
69}
70
71#[cfg(test)]
72#[allow(clippy::unwrap_used, clippy::expect_used)]
73mod tests {
74 use super::*;
75 use crate::coverage::model::FileCoverage;
76
77 fn report(files: &[(&str, &[(u32, u64)])]) -> CoverageReport {
78 let mut report = CoverageReport::new();
79 for (path, lines) in files {
80 let mut file = FileCoverage::new(*path);
81 for &(line, hits) in *lines {
82 file.record(line, hits);
83 }
84 report.insert(file);
85 }
86 report
87 }
88
89 #[test]
90 fn an_empty_report_is_rejected_by_name() {
91 let message = require_executable_lines("shard-2.lcov", &CoverageReport::new())
92 .unwrap_err()
93 .to_string();
94 assert!(message.contains("shard-2.lcov"), "{message}");
95 assert!(message.contains("no executable lines"), "{message}");
96 }
97
98 #[test]
99 fn a_report_whose_files_have_no_lines_is_rejected() {
100 let report = report(&[("src/a.rs", &[])]);
102 assert!(require_executable_lines("s", &report).is_err());
103 }
104
105 #[test]
106 fn a_report_with_executable_lines_passes() {
107 let report = report(&[("src/a.rs", &[(1, 0)])]);
108 require_executable_lines("s", &report).unwrap();
109 }
110
111 #[test]
112 fn a_shard_under_another_root_is_flagged() {
113 let report = report(&[
114 ("/Users/dev/work/repo/src/a.rs", &[(1, 1)]),
115 ("/Users/dev/work/repo/src/b.rs", &[(1, 1)]),
116 ]);
117 let message =
118 prefix_mismatch("shard-2.lcov", &report, Path::new("/home/runner/work/repo")).unwrap();
119 assert!(message.contains("shard-2.lcov"), "{message}");
120 assert!(message.contains("2 absolute file path(s)"), "{message}");
121 assert!(message.contains("`/home/runner/work/repo`"), "{message}");
122 assert!(message.contains("--strip-prefix"), "{message}");
123 }
124
125 #[test]
126 fn a_shard_with_some_in_tree_paths_is_not_flagged() {
127 let report = report(&[
130 ("/home/runner/work/repo/src/a.rs", &[(1, 1)]),
131 ("/rustc/abc123/library/core/src/option.rs", &[(1, 1)]),
132 ]);
133 assert_eq!(
134 prefix_mismatch("s", &report, Path::new("/home/runner/work/repo")),
135 None
136 );
137 }
138
139 #[test]
140 fn relative_paths_are_not_flagged() {
141 let report = report(&[("src/a.rs", &[(1, 1)])]);
142 assert_eq!(
143 prefix_mismatch("s", &report, Path::new("/home/runner/work/repo")),
144 None
145 );
146 }
147
148 #[test]
149 fn a_prefix_that_is_only_a_string_prefix_does_not_match() {
150 let report = report(&[("/work/repo-two/src/a.rs", &[(1, 1)])]);
153 assert!(prefix_mismatch("s", &report, Path::new("/work/repo")).is_some());
154 }
155
156 #[test]
157 fn a_trailing_slash_on_the_prefix_is_ignored() {
158 let report = report(&[("/work/repo/src/a.rs", &[(1, 1)])]);
159 assert_eq!(
160 prefix_mismatch("s", &report, Path::new("/work/repo/")),
161 None
162 );
163 }
164}