workspacer_test_coverage/
test_coverage_command.rs

1// ---------------- [ File: workspacer-test-coverage/src/test_coverage_command.rs ]
2crate::ix!();
3
4#[derive(Debug)]
5pub struct TestCoverageCommand {
6    stdout: String,
7    stderr: String,
8}
9
10impl TestCoverageCommand {
11    /// Additional method to run tarpaulin with `--package <crate_name>`
12    pub async fn run_with_package(workspace_path: &std::path::Path, crate_name: &str) 
13        -> Result<Self, TestCoverageError> 
14    {
15        let output = tokio::process::Command::new("cargo")
16            .arg("tarpaulin")
17            .arg("--out")
18            .arg("Json")
19            .arg("--package")
20            .arg(crate_name)
21            .arg("--quiet")
22            .current_dir(workspace_path)
23            .output()
24            .await
25            .map_err(|e| TestCoverageError::CommandError { io: e.into() })?;
26
27        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
28        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
29
30        info!("stdout (crate coverage): {}", stdout);
31        info!("stderr (crate coverage): {}", stderr);
32
33        if output.status.success() {
34            Ok(Self { stdout, stderr })
35        } else {
36            if stderr.contains("Test failed during run") {
37                error!("Test coverage run failed due to test failure for crate '{}'.", crate_name);
38                Err(TestCoverageError::TestFailure {
39                    stderr: Some(stderr),
40                    stdout: Some(stdout),
41                })
42            } else {
43                error!("Test coverage run failed for unknown reasons (crate='{}').", crate_name);
44                Err(TestCoverageError::UnknownError { 
45                    stdout: Some(stdout),
46                    stderr: Some(stderr),
47                })
48            }
49        }
50    }
51}
52
53impl TestCoverageCommand {
54
55    // Add a method to access stdout
56    pub fn stdout(&self) -> &str {
57        &self.stdout
58    }
59
60    // Add a method to access stderr
61    pub fn stderr(&self) -> &str {
62        &self.stderr
63    }
64
65    // Add a method to check if stdout is empty
66    pub fn is_stdout_empty(&self) -> bool {
67        self.stdout.trim().is_empty()
68    }
69
70    // Add a method to check if stderr contains errors
71    pub fn has_stderr_errors(&self) -> bool {
72        self.stderr.contains("error")
73    }
74
75    // Add a method to parse JSON output
76    pub fn parse_json_output(&self) -> Result<serde_json::Value, TestCoverageError> {
77        serde_json::from_str(&self.stdout).map_err(|e| {
78            error!("Failed to parse coverage report: {}", e);
79            TestCoverageError::CoverageParseError
80        })
81    }
82
83    pub async fn run_in(workspace_path: impl AsRef<Path>) 
84        -> Result<Self,TestCoverageError> 
85    {
86        // Run `cargo tarpaulin` in the workspace directory to collect test coverage
87        let output = tokio::process::Command::new("cargo")
88            .arg("tarpaulin")
89            .arg("--out")
90            .arg("Json")
91            .arg("--")
92            .arg("--quiet")
93            .current_dir(workspace_path)
94            .output()
95            .await
96            .map_err(|e| TestCoverageError::CommandError { io: e.into() })?;
97
98        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
99        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
100
101        // Log the stdout and stderr for debugging purposes
102        info!("stdout: {}", stdout);
103        info!("stderr: {}", stderr);
104
105        match output.status.success() {
106
107            true => Ok(Self { stdout, stderr }),
108
109            // If the tarpaulin command failed, check if the failure was due to tests failing
110            false => {
111                if stderr.contains("Test failed during run") {
112                    error!("Test coverage run failed due to test failure.");
113                    Err(TestCoverageError::TestFailure {
114                        stderr: Some(stderr),
115                        stdout: Some(stdout),
116                    })
117                } else {
118                    error!("Test coverage run failed for unknown reasons.");
119                    Err(TestCoverageError::UnknownError { 
120                        stdout: Some(stdout),
121                        stderr: Some(stderr), 
122                    })
123                }
124            }
125        }
126    }
127}
128
129#[cfg(test)]
130mod test_test_coverage_command_real {
131    use super::*;
132    use std::path::PathBuf;
133    use tempfile::tempdir;
134    use workspacer_3p::tokio::process::Command;
135    use workspacer_3p::tokio;
136
137    /// If tests fail, tarpaulin should return a non-zero exit code and mention "Test failed during run".
138    #[traced_test]
139    async fn test_run_in_test_failure() {
140        trace!("Beginning test_run_in_test_failure...");
141
142        let tmp_dir = tempdir().expect("Failed to create temp directory");
143        let path = tmp_dir.path();
144
145        let init_status = Command::new("cargo")
146            .arg("init")
147            .arg("--bin")
148            .arg("--vcs")
149            .arg("none")
150            .current_dir(path)
151            .output()
152            .await
153            .expect("Failed to spawn `cargo init`");
154
155        if !init_status.status.success() {
156            warn!("Skipping test because `cargo init` failed with status: {:?}", init_status.status);
157            return;
158        }
159
160        // Insert a failing test
161        let main_rs = path.join("src").join("main.rs");
162        let code = r#"
163            fn main() {}
164        #[test]
165        fn test_fail() { assert_eq!(1+1,3); }
166        "#;
167        tokio::fs::write(&main_rs, code)
168            .await
169            .expect("Failed to write test code to main.rs");
170
171        // Now run coverage
172        let coverage_cmd_result = TestCoverageCommand::run_in(path).await;
173        match coverage_cmd_result {
174            Err(TestCoverageError::TestFailure { stderr, stdout }) => {
175                info!("test_run_in_test_failure stderr: {:?}", stderr);
176                info!("test_run_in_test_failure stdout: {:?}", stdout);
177            }
178            Ok(_) => panic!("Expected coverage to fail on a failing test, but got success"),
179            other => panic!("Expected TestFailure, got: {:?}", other),
180        }
181    }
182
183    #[traced_test]
184    async fn test_run_in_succeeds_plaintext_or_json() {
185        trace!("Beginning test_run_in_succeeds_plaintext_or_json...");
186
187        let tmp_dir = tempdir().expect("Failed to create temp directory");
188        let path = tmp_dir.path();
189
190        let init_status = Command::new("cargo")
191            .arg("init")
192            .arg("--bin")
193            .arg("--vcs")
194            .arg("none")
195            .current_dir(path)
196            .output()
197            .await
198            .expect("Failed to spawn `cargo init`");
199
200        if !init_status.status.success() {
201            warn!("Skipping test because `cargo init` failed with status: {:?}", init_status.status);
202            return;
203        }
204
205        // Insert a passing test
206        let main_rs = path.join("src").join("main.rs");
207        let code = r#"
208            fn main() {}
209        #[test]
210        fn test_ok(){ assert_eq!(2+2,4); }
211        "#;
212        tokio::fs::write(&main_rs, code)
213            .await
214            .expect("Failed to write test code to main.rs");
215
216        // Run coverage
217        let coverage_cmd = match TestCoverageCommand::run_in(path).await {
218            Ok(cmd) => cmd,
219            Err(e) => panic!("Expected coverage to succeed but got an error: {:?}", e),
220        };
221
222        info!("stdout after coverage run:\n{}", coverage_cmd.stdout());
223        info!("stderr after coverage run:\n{}", coverage_cmd.stderr());
224
225        // Attempt to parse coverage
226        match coverage_cmd.generate_report() {
227            Ok(report) => {
228                info!("Parsed coverage report: {:?}", report);
229                // Additional assertions or checks can be done here
230            }
231            Err(e) => {
232                warn!("Coverage parse failed (could be plain text or JSON not recognized). Error: {:?}", e);
233            }
234        }
235    }
236}