crate::ix!();
#[derive(Debug)]
pub struct TestCoverageCommand {
stdout: String,
stderr: String,
}
impl TestCoverageCommand {
pub async fn run_with_package(workspace_path: &std::path::Path, crate_name: &str)
-> Result<Self, TestCoverageError>
{
let output = tokio::process::Command::new("cargo")
.arg("tarpaulin")
.arg("--out")
.arg("Json")
.arg("--package")
.arg(crate_name)
.arg("--quiet")
.current_dir(workspace_path)
.output()
.await
.map_err(|e| TestCoverageError::CommandError { io: e.into() })?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
info!("stdout (crate coverage): {}", stdout);
info!("stderr (crate coverage): {}", stderr);
if output.status.success() {
Ok(Self { stdout, stderr })
} else {
if stderr.contains("Test failed during run") {
error!("Test coverage run failed due to test failure for crate '{}'.", crate_name);
Err(TestCoverageError::TestFailure {
stderr: Some(stderr),
stdout: Some(stdout),
})
} else {
error!("Test coverage run failed for unknown reasons (crate='{}').", crate_name);
Err(TestCoverageError::UnknownError {
stdout: Some(stdout),
stderr: Some(stderr),
})
}
}
}
}
impl TestCoverageCommand {
pub fn stdout(&self) -> &str {
&self.stdout
}
pub fn stderr(&self) -> &str {
&self.stderr
}
pub fn is_stdout_empty(&self) -> bool {
self.stdout.trim().is_empty()
}
pub fn has_stderr_errors(&self) -> bool {
self.stderr.contains("error")
}
pub fn parse_json_output(&self) -> Result<serde_json::Value, TestCoverageError> {
serde_json::from_str(&self.stdout).map_err(|e| {
error!("Failed to parse coverage report: {}", e);
TestCoverageError::CoverageParseError
})
}
pub async fn run_in(workspace_path: impl AsRef<Path>)
-> Result<Self,TestCoverageError>
{
let output = tokio::process::Command::new("cargo")
.arg("tarpaulin")
.arg("--out")
.arg("Json")
.arg("--")
.arg("--quiet")
.current_dir(workspace_path)
.output()
.await
.map_err(|e| TestCoverageError::CommandError { io: e.into() })?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
info!("stdout: {}", stdout);
info!("stderr: {}", stderr);
match output.status.success() {
true => Ok(Self { stdout, stderr }),
false => {
if stderr.contains("Test failed during run") {
error!("Test coverage run failed due to test failure.");
Err(TestCoverageError::TestFailure {
stderr: Some(stderr),
stdout: Some(stdout),
})
} else {
error!("Test coverage run failed for unknown reasons.");
Err(TestCoverageError::UnknownError {
stdout: Some(stdout),
stderr: Some(stderr),
})
}
}
}
}
}
#[cfg(test)]
mod test_test_coverage_command_real {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
use workspacer_3p::tokio::process::Command;
use workspacer_3p::tokio;
#[traced_test]
async fn test_run_in_test_failure() {
trace!("Beginning test_run_in_test_failure...");
let tmp_dir = tempdir().expect("Failed to create temp directory");
let path = tmp_dir.path();
let init_status = Command::new("cargo")
.arg("init")
.arg("--bin")
.arg("--vcs")
.arg("none")
.current_dir(path)
.output()
.await
.expect("Failed to spawn `cargo init`");
if !init_status.status.success() {
warn!("Skipping test because `cargo init` failed with status: {:?}", init_status.status);
return;
}
let main_rs = path.join("src").join("main.rs");
let code = r#"
fn main() {}
#[test]
fn test_fail() { assert_eq!(1+1,3); }
"#;
tokio::fs::write(&main_rs, code)
.await
.expect("Failed to write test code to main.rs");
let coverage_cmd_result = TestCoverageCommand::run_in(path).await;
match coverage_cmd_result {
Err(TestCoverageError::TestFailure { stderr, stdout }) => {
info!("test_run_in_test_failure stderr: {:?}", stderr);
info!("test_run_in_test_failure stdout: {:?}", stdout);
}
Ok(_) => panic!("Expected coverage to fail on a failing test, but got success"),
other => panic!("Expected TestFailure, got: {:?}", other),
}
}
#[traced_test]
async fn test_run_in_succeeds_plaintext_or_json() {
trace!("Beginning test_run_in_succeeds_plaintext_or_json...");
let tmp_dir = tempdir().expect("Failed to create temp directory");
let path = tmp_dir.path();
let init_status = Command::new("cargo")
.arg("init")
.arg("--bin")
.arg("--vcs")
.arg("none")
.current_dir(path)
.output()
.await
.expect("Failed to spawn `cargo init`");
if !init_status.status.success() {
warn!("Skipping test because `cargo init` failed with status: {:?}", init_status.status);
return;
}
let main_rs = path.join("src").join("main.rs");
let code = r#"
fn main() {}
#[test]
fn test_ok(){ assert_eq!(2+2,4); }
"#;
tokio::fs::write(&main_rs, code)
.await
.expect("Failed to write test code to main.rs");
let coverage_cmd = match TestCoverageCommand::run_in(path).await {
Ok(cmd) => cmd,
Err(e) => panic!("Expected coverage to succeed but got an error: {:?}", e),
};
info!("stdout after coverage run:\n{}", coverage_cmd.stdout());
info!("stderr after coverage run:\n{}", coverage_cmd.stderr());
match coverage_cmd.generate_report() {
Ok(report) => {
info!("Parsed coverage report: {:?}", report);
}
Err(e) => {
warn!("Coverage parse failed (could be plain text or JSON not recognized). Error: {:?}", e);
}
}
}
}