workspacer_test_coverage/
test_coverage_report.rs

1// ---------------- [ File: workspacer-test-coverage/src/test_coverage_report.rs ]
2crate::ix!();
3
4#[derive(Debug)]
5pub struct TestCoverageReport {
6    total_coverage: f32,   // Coverage percentage
7    covered_lines:  usize, // Total lines covered
8    missed_lines:   usize, // Total lines missed
9    total_lines:    usize, // Total lines in the project
10}
11
12impl TryFrom<&serde_json::Value> for TestCoverageReport {
13
14    type Error = TestCoverageError;
15
16    /// Constructor that creates a `TestCoverageReport` from JSON data.
17    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        // Handle case where no lines were instrumented or coverage is NaN
24        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    /// Creates a new `TestCoverageReport` with given parameters.
36    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    /// Constructor that creates a `TestCoverageReport` from a plain text coverage summary.
51    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    /// Returns the total code coverage as a percentage.
68    pub fn total_coverage(&self) -> f32 {
69        self.total_coverage
70    }
71
72    /// Returns the number of lines covered by tests.
73    pub fn covered_lines(&self) -> usize {
74        self.covered_lines
75    }
76
77    /// Returns the number of lines missed by tests.
78    pub fn missed_lines(&self) -> usize {
79        self.missed_lines
80    }
81
82    /// Returns the total number of lines in the workspace.
83    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        // ---------------------------------------------------------------------
100        // 1) Prepare a minimal Cargo project in a temp directory
101        // ---------------------------------------------------------------------
102        let tmp_dir = tempdir().expect("Failed to create temp directory for test");
103        let project_path = tmp_dir.path();
104
105        // Run `cargo init --bin --vcs none` to create a basic binary crate
106        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        // Write a small main.rs with at least one test
121        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        // ---------------------------------------------------------------------
137        // 2) Actually run `cargo tarpaulin --out Json --quiet` in that directory
138        // ---------------------------------------------------------------------
139        // Make sure tarpaulin is installed or the test will fail.
140        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                // If tarpaulin isn't installed or the spawn fails, handle it:
154                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 tarpaulin returned non-zero, check if tests failed or coverage had issues.
165        if !output.status.success() {
166            // Possibly the test or coverage failed. For a healthy scenario, we expect success:
167            panic!("Coverage tool failed. stderr:\n{}", stderr);
168        }
169
170        // ---------------------------------------------------------------------
171        // 3) Construct our TestCoverageCommand object and parse
172        // ---------------------------------------------------------------------
173        let coverage_cmd = TestCoverageCommand {
174            stdout,
175            stderr,
176        };
177
178        // If your coverage tool sometimes prints a plaintext summary (like "80.0% coverage, 8/10 lines covered")
179        // you can rely on `from_maybe_plaintext_coverage_summary`. 
180        // If it prints JSON, your code tries `parse_json_output` => `try_from(&json)`.
181        // The `generate_report()` internally tries plaintext first, then JSON. 
182        // So we just do:
183        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        // ---------------------------------------------------------------------
189        // 4) Inspect the final TestCoverageReport
190        // ---------------------------------------------------------------------
191        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        // Some minimal assertion: we expect at least 1 covered line
199        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}