workspacer_test_coverage/
test_coverage_command.rs1crate::ix!();
3
4#[derive(Debug)]
5pub struct TestCoverageCommand {
6 stdout: String,
7 stderr: String,
8}
9
10impl TestCoverageCommand {
11 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 pub fn stdout(&self) -> &str {
57 &self.stdout
58 }
59
60 pub fn stderr(&self) -> &str {
62 &self.stderr
63 }
64
65 pub fn is_stdout_empty(&self) -> bool {
67 self.stdout.trim().is_empty()
68 }
69
70 pub fn has_stderr_errors(&self) -> bool {
72 self.stderr.contains("error")
73 }
74
75 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 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 info!("stdout: {}", stdout);
103 info!("stderr: {}", stderr);
104
105 match output.status.success() {
106
107 true => Ok(Self { stdout, stderr }),
108
109 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 #[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 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 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 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 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 match coverage_cmd.generate_report() {
227 Ok(report) => {
228 info!("Parsed coverage report: {:?}", report);
229 }
231 Err(e) => {
232 warn!("Coverage parse failed (could be plain text or JSON not recognized). Error: {:?}", e);
233 }
234 }
235 }
236}