use anyhow::{
Context,
Result,
};
use portable_pty::CommandBuilder;
use serde::{
Deserialize,
Serialize,
};
use super::common;
pub async fn badge_number_of_tests(
writer: &mut dyn std::io::Write,
package: &cargo_metadata::Package,
) -> Result<()> {
let mut logger = cargo_plugin_utils::logger::Logger::new();
logger.status("Generating", "test count badge");
let test_count = get_test_count(&mut logger, package).await?;
if let Some(count) = test_count {
let badge_url = format!("https://img.shields.io/badge/tests-{}-blue", count);
let badge_markdown = format!("[](tests/)", badge_url);
writeln!(writer, "{}", badge_markdown)?;
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestCountCache {
package: String,
cache_key: String,
test_count: u32,
}
async fn get_test_count(
logger: &mut cargo_plugin_utils::logger::Logger,
package: &cargo_metadata::Package,
) -> Result<Option<u32>> {
if let Some(cached) = load_test_count_cache(package).await? {
let current_key = common::compute_cache_key(package).await?;
if cached.cache_key == current_key && package.name == cached.package {
return Ok(Some(cached.test_count));
}
}
let package_name = package.name.clone();
let output = cargo_plugin_utils::logger::run_subprocess(
logger,
move || {
let mut cmd = CommandBuilder::new("cargo");
cmd.arg("test");
cmd.arg("--package");
cmd.arg(package_name.as_str());
cmd.arg("--no-run");
cmd.arg("--message-format");
cmd.arg("json");
cmd
},
None,
)
.await?;
if !output.success() {
return Ok(None);
}
let stdout = output
.stdout_str()
.context("Failed to parse cargo test output")?;
let mut test_count = 0;
let package_id_prefix = format!("{}@", package.name);
for line in stdout.lines() {
let Ok(json) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if json.get("reason") != Some(&serde_json::Value::String("compiler-artifact".to_string())) {
continue;
}
let is_our_package = json
.get("package_id")
.and_then(|id| id.as_str())
.map(|id| id.starts_with(&package_id_prefix))
.unwrap_or(false);
if !is_our_package {
continue;
}
let is_test = json
.get("target")
.and_then(|t| t.get("kind"))
.and_then(|k| k.as_array())
.map(|kinds| kinds.contains(&serde_json::Value::String("test".to_string())))
.unwrap_or(false);
if !is_test {
continue;
}
if let Some(executable) = json.get("executable")
&& executable.is_string()
{
test_count += 1;
}
}
if test_count > 0 {
save_test_count_cache(package, test_count).await?;
return Ok(Some(test_count));
}
let package_name = package.name.clone();
let compile_output = cargo_plugin_utils::logger::run_subprocess(
logger,
{
let package_name = package_name.clone();
move || {
let mut cmd = CommandBuilder::new("cargo");
cmd.arg("test");
cmd.arg("--package");
cmd.arg(package_name.as_str());
cmd.arg("--no-run");
cmd
}
},
None,
)
.await?;
if !compile_output.success() {
return Ok(None);
}
let list_output = cargo_plugin_utils::logger::run_subprocess(
logger,
move || {
let mut cmd = CommandBuilder::new("cargo");
cmd.arg("test");
cmd.arg("--package");
cmd.arg(package_name.as_str());
cmd.arg("--");
cmd.arg("--list");
cmd
},
None,
)
.await?;
if list_output.success() {
let list_stdout = list_output
.stdout_str()
.context("Failed to parse cargo test --list output")?;
let count = list_stdout
.lines()
.filter(|line| line.contains(": test"))
.count() as u32;
if count > 0 {
save_test_count_cache(package, count).await?;
return Ok(Some(count));
}
}
Ok(None)
}
async fn load_test_count_cache(
_package: &cargo_metadata::Package,
) -> Result<Option<TestCountCache>> {
let cache_path = common::get_badge_cache_path("test-count")?;
if !cache_path.exists() {
return Ok(None);
}
let contents = tokio::fs::read_to_string(&cache_path)
.await
.context("Failed to read cache file")?;
let cache: TestCountCache =
serde_json::from_str(&contents).context("Failed to parse cache file")?;
Ok(Some(cache))
}
async fn save_test_count_cache(package: &cargo_metadata::Package, test_count: u32) -> Result<()> {
let cache_key = common::compute_cache_key(package).await?;
let cache = TestCountCache {
package: package.name.to_string(),
cache_key,
test_count,
};
let cache_path = common::get_badge_cache_path("test-count")?;
if let Some(parent) = cache_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.context("Failed to create cache directory")?;
}
let json = serde_json::to_string_pretty(&cache).context("Failed to serialize cache")?;
tokio::fs::write(&cache_path, json)
.await
.context("Failed to write cache file")?;
Ok(())
}