use anyhow::{
Context,
Result,
};
use portable_pty::CommandBuilder;
use serde::{
Deserialize,
Serialize,
};
use super::common;
pub async fn badge_coverage(
writer: &mut dyn std::io::Write,
package: &cargo_metadata::Package,
) -> Result<()> {
let mut logger = cargo_plugin_utils::logger::Logger::new();
logger.status("Generating", "coverage badge");
let coverage = get_coverage_percentage(&mut logger, package).await?;
if let Some(coverage) = coverage {
let color = if coverage >= 80 {
"brightgreen"
} else if coverage >= 60 {
"green"
} else if coverage >= 40 {
"yellow"
} else {
"red"
};
let badge_url = format!(
"https://img.shields.io/badge/coverage-{}%25-{}",
coverage, color
);
let link_target = if let Some(repo) = &package.repository {
if repo.contains("github.com") {
format!("{}/actions", repo)
} else {
repo.clone()
}
} else {
"coverage/".to_string()
};
let badge_markdown = format!("[]({})", badge_url, link_target);
writeln!(writer, "{}", badge_markdown)?;
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CoverageCache {
package: String,
cache_key: String,
coverage: u8,
}
async fn get_coverage_percentage(
logger: &mut cargo_plugin_utils::logger::Logger,
package: &cargo_metadata::Package,
) -> Result<Option<u8>> {
if let Some(cached) = load_coverage_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.coverage));
}
}
let version_output = cargo_plugin_utils::logger::run_subprocess(
logger,
|| {
let mut cmd = CommandBuilder::new("cargo");
cmd.arg("llvm-cov");
cmd.arg("--version");
cmd
},
None,
)
.await?;
if !version_output.success() {
eprintln!(
"Warning: cargo-llvm-cov is not installed. Install it with: cargo binstall cargo-llvm-cov (or cargo install cargo-llvm-cov)"
);
return Ok(None);
}
let package_name = package.name.clone();
let output = cargo_plugin_utils::logger::run_subprocess(
logger,
move || {
let mut cmd = CommandBuilder::new("cargo");
cmd.arg("llvm-cov");
cmd.arg("--package");
cmd.arg(package_name.as_str());
cmd.arg("--summary-only");
cmd.arg("--json");
cmd
},
None,
)
.await?;
if !output.success() {
return Ok(None);
}
let stdout = output
.stdout_str()
.context("Failed to parse cargo-llvm-cov output")?;
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&stdout)
&& let Some(data) = json.get("data").and_then(|d| d.as_array())
&& let Some(first_data) = data.first()
&& let Some(percent) = first_data
.get("totals")
.and_then(|t| t.get("lines"))
.and_then(|l| l.get("percent"))
.and_then(|p| p.as_f64())
{
let coverage = percent.round() as u8;
save_coverage_cache(package, coverage).await?;
return Ok(Some(coverage));
}
Ok(None)
}
async fn load_coverage_cache(_package: &cargo_metadata::Package) -> Result<Option<CoverageCache>> {
let cache_path = common::get_badge_cache_path("coverage")?;
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: CoverageCache =
serde_json::from_str(&contents).context("Failed to parse cache file")?;
Ok(Some(cache))
}
async fn save_coverage_cache(package: &cargo_metadata::Package, coverage: u8) -> Result<()> {
let cache_key = common::compute_cache_key(package).await?;
let cache = CoverageCache {
package: package.name.to_string(),
cache_key,
coverage,
};
let cache_path = common::get_badge_cache_path("coverage")?;
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(())
}