1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// ---------------- [ File: workspacer-test-coverage/src/test_coverage_report.rs ]
crate::ix!();
#[derive(Debug)]
pub struct TestCoverageReport {
total_coverage: f32, // Coverage percentage
covered_lines: usize, // Total lines covered
missed_lines: usize, // Total lines missed
total_lines: usize, // Total lines in the project
}
impl TryFrom<&serde_json::Value> for TestCoverageReport {
type Error = TestCoverageError;
/// Constructor that creates a `TestCoverageReport` from JSON data.
fn try_from(coverage_data: &serde_json::Value) -> Result<Self, Self::Error> {
let total_lines = coverage_data["total_lines"].as_u64().unwrap_or(0) as usize;
let covered_lines = coverage_data["covered_lines"].as_u64().unwrap_or(0) as usize;
let total_coverage = coverage_data["total_coverage"].as_f64().unwrap_or(0.0) as f32;
let missed_lines = total_lines.saturating_sub(covered_lines);
// Handle case where no lines were instrumented or coverage is NaN
if total_lines == 0 || total_coverage.is_nan() {
error!("No lines were covered or the coverage report was invalid.");
return Err(TestCoverageError::CoverageParseError);
}
Ok(Self::new(total_coverage, covered_lines, missed_lines, total_lines))
}
}
impl TestCoverageReport {
/// Creates a new `TestCoverageReport` with given parameters.
pub fn new(
total_coverage: f32,
covered_lines: usize,
missed_lines: usize,
total_lines: usize
) -> Self {
Self {
total_coverage,
covered_lines,
missed_lines,
total_lines,
}
}
/// Constructor that creates a `TestCoverageReport` from a plain text coverage summary.
pub fn from_maybe_plaintext_coverage_summary(stdout: &str) -> Result<Self, TestCoverageError> {
let re = Regex::new(r"(\d+\.\d+)% coverage, (\d+)/(\d+) lines covered")
.map_err(|_| TestCoverageError::CoverageParseError)?;
if let Some(caps) = re.captures(stdout) {
let total_coverage = caps[1].parse::<f32>().unwrap_or(0.0);
let covered_lines = caps[2].parse::<usize>().unwrap_or(0);
let total_lines = caps[3].parse::<usize>().unwrap_or(0);
let missed_lines = total_lines.saturating_sub(covered_lines);
Ok(Self::new(total_coverage, covered_lines, missed_lines, total_lines))
} else {
Err(TestCoverageError::CoverageParseError)
}
}
/// Returns the total code coverage as a percentage.
pub fn total_coverage(&self) -> f32 {
self.total_coverage
}
/// Returns the number of lines covered by tests.
pub fn covered_lines(&self) -> usize {
self.covered_lines
}
/// Returns the number of lines missed by tests.
pub fn missed_lines(&self) -> usize {
self.missed_lines
}
/// Returns the total number of lines in the workspace.
pub fn total_lines(&self) -> usize {
self.total_lines
}
}
#[cfg(test)]
#[disable]
mod test_coverage_integration {
use super::*;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use workspacer_3p::tokio::process::Command;
use workspacer_3p::tokio;
#[tokio::test]
async fn test_parse_coverage_report_from_real_tarpaulin_run() {
// ---------------------------------------------------------------------
// 1) Prepare a minimal Cargo project in a temp directory
// ---------------------------------------------------------------------
let tmp_dir = tempdir().expect("Failed to create temp directory for test");
let project_path = tmp_dir.path();
// Run `cargo init --bin --vcs none` to create a basic binary crate
let init_status = Command::new("cargo")
.arg("init")
.arg("--bin")
.arg("--vcs")
.arg("none")
.current_dir(project_path)
.output()
.await
.expect("Failed to run `cargo init`");
assert!(
init_status.status.success(),
"cargo init must succeed in order to proceed"
);
// Write a small main.rs with at least one test
let src_main = project_path.join("src").join("main.rs");
let code = r#"
fn main() {
println!("Hello, coverage!");
}
#[test]
fn test_ok() {
assert_eq!(2 + 2, 4);
}
"#;
tokio::fs::write(&src_main, code)
.await
.expect("Failed to write main.rs test code");
// ---------------------------------------------------------------------
// 2) Actually run `cargo tarpaulin --out Json --quiet` in that directory
// ---------------------------------------------------------------------
// Make sure tarpaulin is installed or the test will fail.
let output = Command::new("cargo")
.arg("tarpaulin")
.arg("--out")
.arg("Json")
.arg("--")
.arg("--quiet")
.current_dir(&project_path)
.output()
.await;
let output = match output {
Ok(o) => o,
Err(e) => {
// If tarpaulin isn't installed or the spawn fails, handle it:
panic!("Failed to run cargo tarpaulin: {}", e);
}
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
eprintln!("tarpaulin stdout:\n{}", stdout);
eprintln!("tarpaulin stderr:\n{}", stderr);
// If tarpaulin returned non-zero, check if tests failed or coverage had issues.
if !output.status.success() {
// Possibly the test or coverage failed. For a healthy scenario, we expect success:
panic!("Coverage tool failed. stderr:\n{}", stderr);
}
// ---------------------------------------------------------------------
// 3) Construct our TestCoverageCommand object and parse
// ---------------------------------------------------------------------
let coverage_cmd = TestCoverageCommand {
stdout,
stderr,
};
// If your coverage tool sometimes prints a plaintext summary (like "80.0% coverage, 8/10 lines covered")
// you can rely on `from_maybe_plaintext_coverage_summary`.
// If it prints JSON, your code tries `parse_json_output` => `try_from(&json)`.
// The `generate_report()` internally tries plaintext first, then JSON.
// So we just do:
let coverage_report = match coverage_cmd.generate_report() {
Ok(report) => report,
Err(e) => panic!("Failed to parse coverage report: {:?}", e),
};
// ---------------------------------------------------------------------
// 4) Inspect the final TestCoverageReport
// ---------------------------------------------------------------------
eprintln!("Parsed coverage report:\n total_coverage: {}%\n covered_lines: {}\n missed_lines: {}\n total_lines: {}",
coverage_report.total_coverage(),
coverage_report.covered_lines(),
coverage_report.missed_lines(),
coverage_report.total_lines(),
);
// Some minimal assertion: we expect at least 1 covered line
assert!(
coverage_report.covered_lines() > 0,
"We expected at least one covered line in this project"
);
assert!(
coverage_report.total_coverage() > 0.0,
"We expected some non-zero coverage"
);
}
}