workspacer_test_coverage/
test_coverage_report.rs1crate::ix!();
3
4#[derive(Debug)]
5pub struct TestCoverageReport {
6 total_coverage: f32, covered_lines: usize, missed_lines: usize, total_lines: usize, }
11
12impl TryFrom<&serde_json::Value> for TestCoverageReport {
13
14 type Error = TestCoverageError;
15
16 fn try_from(coverage_data: &serde_json::Value) -> Result<Self, Self::Error> {
18 let total_lines = coverage_data["total_lines"].as_u64().unwrap_or(0) as usize;
19 let covered_lines = coverage_data["covered_lines"].as_u64().unwrap_or(0) as usize;
20 let total_coverage = coverage_data["total_coverage"].as_f64().unwrap_or(0.0) as f32;
21 let missed_lines = total_lines.saturating_sub(covered_lines);
22
23 if total_lines == 0 || total_coverage.is_nan() {
25 error!("No lines were covered or the coverage report was invalid.");
26 return Err(TestCoverageError::CoverageParseError);
27 }
28
29 Ok(Self::new(total_coverage, covered_lines, missed_lines, total_lines))
30 }
31}
32
33impl TestCoverageReport {
34
35 pub fn new(
37 total_coverage: f32,
38 covered_lines: usize,
39 missed_lines: usize,
40 total_lines: usize
41 ) -> Self {
42 Self {
43 total_coverage,
44 covered_lines,
45 missed_lines,
46 total_lines,
47 }
48 }
49
50 pub fn from_maybe_plaintext_coverage_summary(stdout: &str) -> Result<Self, TestCoverageError> {
52 let re = Regex::new(r"(\d+\.\d+)% coverage, (\d+)/(\d+) lines covered")
53 .map_err(|_| TestCoverageError::CoverageParseError)?;
54
55 if let Some(caps) = re.captures(stdout) {
56 let total_coverage = caps[1].parse::<f32>().unwrap_or(0.0);
57 let covered_lines = caps[2].parse::<usize>().unwrap_or(0);
58 let total_lines = caps[3].parse::<usize>().unwrap_or(0);
59 let missed_lines = total_lines.saturating_sub(covered_lines);
60
61 Ok(Self::new(total_coverage, covered_lines, missed_lines, total_lines))
62 } else {
63 Err(TestCoverageError::CoverageParseError)
64 }
65 }
66
67 pub fn total_coverage(&self) -> f32 {
69 self.total_coverage
70 }
71
72 pub fn covered_lines(&self) -> usize {
74 self.covered_lines
75 }
76
77 pub fn missed_lines(&self) -> usize {
79 self.missed_lines
80 }
81
82 pub fn total_lines(&self) -> usize {
84 self.total_lines
85 }
86}
87
88#[cfg(test)]
89#[disable]
90mod test_coverage_integration {
91 use super::*;
92 use std::path::{Path, PathBuf};
93 use tempfile::tempdir;
94 use workspacer_3p::tokio::process::Command;
95 use workspacer_3p::tokio;
96
97 #[tokio::test]
98 async fn test_parse_coverage_report_from_real_tarpaulin_run() {
99 let tmp_dir = tempdir().expect("Failed to create temp directory for test");
103 let project_path = tmp_dir.path();
104
105 let init_status = Command::new("cargo")
107 .arg("init")
108 .arg("--bin")
109 .arg("--vcs")
110 .arg("none")
111 .current_dir(project_path)
112 .output()
113 .await
114 .expect("Failed to run `cargo init`");
115 assert!(
116 init_status.status.success(),
117 "cargo init must succeed in order to proceed"
118 );
119
120 let src_main = project_path.join("src").join("main.rs");
122 let code = r#"
123 fn main() {
124 println!("Hello, coverage!");
125 }
126
127 #[test]
128 fn test_ok() {
129 assert_eq!(2 + 2, 4);
130 }
131 "#;
132 tokio::fs::write(&src_main, code)
133 .await
134 .expect("Failed to write main.rs test code");
135
136 let output = Command::new("cargo")
141 .arg("tarpaulin")
142 .arg("--out")
143 .arg("Json")
144 .arg("--")
145 .arg("--quiet")
146 .current_dir(&project_path)
147 .output()
148 .await;
149
150 let output = match output {
151 Ok(o) => o,
152 Err(e) => {
153 panic!("Failed to run cargo tarpaulin: {}", e);
155 }
156 };
157
158 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
159 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
160
161 eprintln!("tarpaulin stdout:\n{}", stdout);
162 eprintln!("tarpaulin stderr:\n{}", stderr);
163
164 if !output.status.success() {
166 panic!("Coverage tool failed. stderr:\n{}", stderr);
168 }
169
170 let coverage_cmd = TestCoverageCommand {
174 stdout,
175 stderr,
176 };
177
178 let coverage_report = match coverage_cmd.generate_report() {
184 Ok(report) => report,
185 Err(e) => panic!("Failed to parse coverage report: {:?}", e),
186 };
187
188 eprintln!("Parsed coverage report:\n total_coverage: {}%\n covered_lines: {}\n missed_lines: {}\n total_lines: {}",
192 coverage_report.total_coverage(),
193 coverage_report.covered_lines(),
194 coverage_report.missed_lines(),
195 coverage_report.total_lines(),
196 );
197
198 assert!(
200 coverage_report.covered_lines() > 0,
201 "We expected at least one covered line in this project"
202 );
203 assert!(
204 coverage_report.total_coverage() > 0.0,
205 "We expected some non-zero coverage"
206 );
207 }
208}