#![cfg_attr(coverage_nightly, coverage(off))]
use super::parsing::{build_coverage_map, try_load_coverage_json, try_load_lcov_info};
use super::types::CoverageCache;
use std::collections::HashMap;
use std::path::Path;
#[cfg_attr(coverage_nightly, coverage(off))]
pub(super) fn load_coverage_from_cache(
cache_path: &Path,
head_hash: &str,
project_root: &Path,
) -> Option<HashMap<String, HashMap<usize, u64>>> {
let cache_json = std::fs::read_to_string(cache_path).ok()?;
let cache: CoverageCache = serde_json::from_str(&cache_json).ok()?;
if let Some(cached_mtime) = cache.coverage_mtime {
if let Some((current_mtime, _)) =
get_profdata_mtime_and_dir(project_root, cache.profdata_dir.as_deref())
{
if current_mtime <= cached_mtime {
return Some(cache.files); }
return None; }
}
if cache.git_hash != head_hash {
return None;
}
Some(cache.files)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn dir_mtime(dir: &Path) -> Option<u64> {
std::fs::metadata(dir)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn target_dir_from_cargo_config(
config_path: &Path,
project_root: &Path,
) -> Vec<std::path::PathBuf> {
let content = match std::fs::read_to_string(config_path) {
Ok(c) => c,
Err(_) => return vec![],
};
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if !trimmed.starts_with("target-dir") {
return None;
}
let val = trimmed.split('=').nth(1)?;
let dir = val.trim().trim_matches('"').trim_matches('\'');
let target_path = if std::path::Path::new(dir).is_absolute() {
std::path::PathBuf::from(dir)
} else {
project_root.join(dir)
};
Some(target_path.join("llvm-cov-target"))
})
.collect()
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn mnt_target_candidates(project_root: &Path) -> Vec<std::path::PathBuf> {
let canonical = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
let project_name = match canonical.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => return vec![],
};
let entries = match std::fs::read_dir("/mnt") {
Ok(e) => e,
Err(_) => return vec![],
};
entries
.flatten()
.map(|e| {
e.path()
.join("targets")
.join(project_name)
.join("llvm-cov-target")
})
.collect()
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn collect_fast_candidates(
project_root: &Path,
stored_path: Option<&str>,
) -> Vec<std::path::PathBuf> {
let mut candidates: Vec<std::path::PathBuf> = Vec::with_capacity(8);
if let Some(p) = stored_path {
candidates.push(std::path::PathBuf::from(p));
}
if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
candidates.push(std::path::PathBuf::from(&target_dir).join("llvm-cov-target"));
}
candidates.extend(target_dir_from_cargo_config(
&project_root.join(".cargo/config.toml"),
project_root,
));
let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| format!("{h}/.cargo"))
.unwrap_or_default()
});
if !cargo_home.is_empty() {
let global_config = std::path::PathBuf::from(&cargo_home).join("config.toml");
candidates.extend(target_dir_from_cargo_config(&global_config, project_root));
}
candidates.extend(mnt_target_candidates(project_root));
let default_target = project_root.join("target");
if let Ok(canonical) = default_target.canonicalize() {
candidates.push(canonical.join("llvm-cov-target"));
}
candidates.push(default_target.join("llvm-cov-target"));
candidates
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(super) fn get_profdata_mtime_fast(
project_root: &Path,
stored_path: Option<&str>,
) -> Option<(u64, String)> {
let candidates = collect_fast_candidates(project_root, stored_path);
if let Some(p) = stored_path {
if let Some(mtime) = dir_mtime(std::path::Path::new(p)) {
return Some((mtime, p.to_string()));
}
}
for dir in &candidates {
if let Some(mtime) = dir_mtime(dir) {
return Some((mtime, dir.to_string_lossy().to_string()));
}
}
None
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn cargo_metadata_target_dir(project_root: &Path) -> Vec<std::path::PathBuf> {
let mut result = Vec::new();
for toolchain_arg in &["+nightly", "+stable"] {
let output = match std::process::Command::new("cargo")
.args([toolchain_arg, "metadata", "--no-deps", "--format-version=1"])
.current_dir(project_root)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
{
Ok(o) if o.status.success() => o,
_ => continue,
};
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(dir) = extract_target_directory(&stdout) {
result.push(std::path::PathBuf::from(dir).join("llvm-cov-target"));
}
}
result
}
fn extract_target_directory(json: &str) -> Option<&str> {
let idx = json.find("\"target_directory\":\"")?;
let rest = json.get(idx + 20..)?;
let end = rest.find('"')?;
rest.get(..end)
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(super) fn get_profdata_mtime_and_dir(
project_root: &Path,
stored_path: Option<&str>,
) -> Option<(u64, String)> {
if let Some(p) = stored_path {
if let Some(mtime) = dir_mtime(std::path::Path::new(p)) {
return Some((mtime, p.to_string()));
}
}
let candidates = collect_fast_candidates(project_root, None);
for dir in &candidates {
if let Some(mtime) = dir_mtime(dir) {
return Some((mtime, dir.to_string_lossy().to_string()));
}
}
for dir in cargo_metadata_target_dir(project_root) {
if let Some(mtime) = dir_mtime(&dir) {
return Some((mtime, dir.to_string_lossy().to_string()));
}
}
None
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(super) fn run_cargo_llvm_cov_and_cache(
project_root: &Path,
cache_path: &Path,
head_hash: &str,
) -> Result<HashMap<String, HashMap<usize, u64>>, String> {
if let Some(cov) = try_load_lcov_info(project_root) {
write_coverage_cache(cache_path, head_hash, project_root, &cov);
return Ok(cov);
}
if let Some(cov) = try_load_coverage_json(project_root) {
write_coverage_cache(cache_path, head_hash, project_root, &cov);
return Ok(cov);
}
if get_profdata_mtime_fast(project_root, None).is_none() {
return Err("No coverage data available.\n\n\
To generate it, run:\n \
cargo llvm-cov test --lib --no-report\n\n\
Then re-run with --coverage-gaps.\n\
Or pass --coverage-file <path> to use existing coverage JSON."
.to_string());
}
eprintln!("Generating coverage report...");
let output = run_llvm_cov_subprocess(project_root)?;
let json = String::from_utf8_lossy(&output.stdout);
let file_coverage = build_coverage_map(&json, project_root)?;
write_coverage_cache(cache_path, head_hash, project_root, &file_coverage);
Ok(file_coverage)
}
pub(super) fn write_coverage_cache(
cache_path: &Path,
head_hash: &str,
project_root: &Path,
files: &HashMap<String, HashMap<usize, u64>>,
) {
let (mtime, dir) = get_profdata_mtime_and_dir(project_root, None)
.map(|(m, d)| (Some(m), Some(d)))
.unwrap_or((None, None));
let cache = CoverageCache {
git_hash: head_hash.to_string(),
coverage_mtime: mtime,
profdata_dir: dir,
files: files.clone(),
};
if let Ok(cache_json) = serde_json::to_string(&cache) {
let _ = std::fs::create_dir_all(project_root.join(".pmat"));
let _ = std::fs::write(cache_path, cache_json);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(super) fn write_negative_coverage_cache(
cache_path: &Path,
head_hash: &str,
project_root: &Path,
) {
let (mtime, dir) = get_profdata_mtime_fast(project_root, None)
.map(|(m, d)| (Some(m), Some(d)))
.unwrap_or((None, None));
let cache = CoverageCache {
git_hash: head_hash.to_string(),
coverage_mtime: mtime,
profdata_dir: dir,
files: HashMap::new(),
};
if let Ok(cache_json) = serde_json::to_string(&cache) {
let _ = std::fs::create_dir_all(project_root.join(".pmat"));
let _ = std::fs::write(cache_path, cache_json);
}
}
fn run_llvm_cov_subprocess(project_root: &Path) -> Result<std::process::Output, String> {
use std::process::{Command, Stdio};
let mut child = Command::new("cargo")
.args(["+nightly", "llvm-cov", "report", "--json"])
.current_dir(project_root)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.or_else(|_| {
Command::new("cargo")
.args(["llvm-cov", "report", "--json"])
.current_dir(project_root)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.map_err(|e| format!("cargo llvm-cov report --json failed to spawn: {e}"))?;
let stdout_handle = child
.stdout
.take()
.ok_or_else(|| "Failed to capture stdout from cargo llvm-cov".to_string())?;
let stderr_handle = child
.stderr
.take()
.ok_or_else(|| "Failed to capture stderr from cargo llvm-cov".to_string())?;
let (stdout, stderr) = spawn_reader_threads(stdout_handle, stderr_handle);
wait_with_timeout(&mut child, std::time::Duration::from_secs(30))?;
let status = child
.wait()
.map_err(|e| format!("Failed to wait on cargo llvm-cov: {e}"))?;
let stdout = stdout
.join()
.map_err(|_| "stdout reader thread panicked".to_string())?;
let stderr = stderr
.join()
.map_err(|_| "stderr reader thread panicked".to_string())?;
let output = std::process::Output {
status,
stdout,
stderr,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"No coverage data available.\n\nTo generate it, run:\n \
cargo llvm-cov test --lib --no-report\n\nThen re-run with --coverage or --coverage-gaps.\n\
Or pass --coverage-file <path> to use existing coverage JSON.\n\n\
cargo llvm-cov report --json stderr: {}",
stderr.lines().take(3).collect::<Vec<_>>().join("\n")
));
}
Ok(output)
}
fn spawn_reader_threads(
mut stdout_handle: std::process::ChildStdout,
mut stderr_handle: std::process::ChildStderr,
) -> (
std::thread::JoinHandle<Vec<u8>>,
std::thread::JoinHandle<Vec<u8>>,
) {
let stdout_thread = std::thread::spawn(move || -> Vec<u8> {
use std::io::Read;
let mut buf = Vec::new();
let _ = stdout_handle.read_to_end(&mut buf);
buf
});
let stderr_thread = std::thread::spawn(move || -> Vec<u8> {
use std::io::Read;
let mut buf = Vec::new();
let _ = stderr_handle.read_to_end(&mut buf);
buf
});
(stdout_thread, stderr_thread)
}
fn wait_with_timeout(
child: &mut std::process::Child,
timeout: std::time::Duration,
) -> Result<(), String> {
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => return Ok(()),
Ok(None) if start.elapsed() > timeout => {
let _ = child.kill();
let _ = child.wait();
return Err("No coverage data available.\n\n\
cargo llvm-cov report --json timed out after 30s.\n\
This usually means corrupted profdata. Try:\n \
cargo llvm-cov clean\n \
cargo llvm-cov test --lib --no-report\n\n\
Then re-run with --coverage or --coverage-gaps.\n\
Or pass --coverage-file <path> to use existing coverage JSON."
.to_string());
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(e) => return Err(format!("Failed to wait on cargo llvm-cov: {e}")),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn write(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(path, content).expect("write");
}
#[test]
fn test_dir_mtime_returns_none_for_missing_dir() {
assert!(dir_mtime(Path::new("/tmp/absolutely-does-not-exist-aaa")).is_none());
}
#[test]
fn test_dir_mtime_returns_some_for_existing_dir() {
let tmp = TempDir::new().unwrap();
let mtime = dir_mtime(tmp.path());
assert!(
mtime.is_some(),
"expected Some mtime for {}",
tmp.path().display()
);
assert!(mtime.unwrap() > 0);
}
#[test]
fn test_target_dir_from_cargo_config_missing_file_returns_empty() {
let tmp = TempDir::new().unwrap();
let result = target_dir_from_cargo_config(&tmp.path().join("nonexistent.toml"), tmp.path());
assert!(result.is_empty());
}
#[test]
fn test_target_dir_from_cargo_config_no_target_dir_key_returns_empty() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join("config.toml");
write(&cfg, "[build]\nrustflags = []\n");
let result = target_dir_from_cargo_config(&cfg, tmp.path());
assert!(result.is_empty());
}
#[test]
fn test_target_dir_from_cargo_config_relative_resolves_against_project_root() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join("config.toml");
write(&cfg, "target-dir = \"custom-target\"\n");
let result = target_dir_from_cargo_config(&cfg, tmp.path());
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
tmp.path().join("custom-target").join("llvm-cov-target")
);
}
#[test]
fn test_target_dir_from_cargo_config_absolute_preserved() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join("config.toml");
write(&cfg, "target-dir = \"/mnt/custom\"\n");
let result = target_dir_from_cargo_config(&cfg, tmp.path());
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
std::path::PathBuf::from("/mnt/custom/llvm-cov-target")
);
}
#[test]
fn test_target_dir_from_cargo_config_single_quotes_accepted() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join("config.toml");
write(&cfg, "target-dir = 'with-single-quotes'\n");
let result = target_dir_from_cargo_config(&cfg, tmp.path());
assert_eq!(result.len(), 1);
assert_eq!(
result[0],
tmp.path()
.join("with-single-quotes")
.join("llvm-cov-target")
);
}
#[test]
fn test_mnt_target_candidates_returns_vec_shaped_like_project_name() {
let tmp = TempDir::new().unwrap();
let project_name = tmp
.path()
.canonicalize()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let out = mnt_target_candidates(tmp.path());
for p in out {
assert!(
p.to_string_lossy().contains(&project_name),
"path missing project name: {}",
p.display()
);
assert!(p.ends_with("llvm-cov-target"));
}
}
#[test]
fn test_collect_fast_candidates_uses_stored_path_first() {
let tmp = TempDir::new().unwrap();
let stored = "/tmp/my-stored-path";
let out = collect_fast_candidates(tmp.path(), Some(stored));
assert_eq!(out[0], std::path::PathBuf::from(stored));
}
#[test]
fn test_collect_fast_candidates_includes_default_target_dir() {
let tmp = TempDir::new().unwrap();
let out = collect_fast_candidates(tmp.path(), None);
let default = tmp.path().join("target").join("llvm-cov-target");
assert!(
out.contains(&default),
"default target dir missing; got: {out:?}"
);
}
#[test]
fn test_collect_fast_candidates_honors_cargo_target_dir_env_var() {
let tmp = TempDir::new().unwrap();
let marker = "/tmp/test-cargo-target-dir-marker";
let orig = std::env::var("CARGO_TARGET_DIR").ok();
std::env::set_var("CARGO_TARGET_DIR", marker);
let out = collect_fast_candidates(tmp.path(), None);
match orig {
Some(v) => std::env::set_var("CARGO_TARGET_DIR", v),
None => std::env::remove_var("CARGO_TARGET_DIR"),
}
let expected = std::path::PathBuf::from(marker).join("llvm-cov-target");
assert!(
out.contains(&expected),
"expected marker path to be in candidates"
);
}
#[test]
fn test_collect_fast_candidates_picks_up_project_local_cargo_config() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join(".cargo").join("config.toml");
write(&cfg, "target-dir = \"custom\"\n");
let out = collect_fast_candidates(tmp.path(), None);
let expected = tmp.path().join("custom").join("llvm-cov-target");
assert!(
out.contains(&expected),
"expected project-local target-dir to be picked up; got: {out:?}"
);
}
#[test]
fn test_extract_target_directory_parses_basic_json() {
let json = r#"{"target_directory":"/tmp/some/target","other":"x"}"#;
assert_eq!(extract_target_directory(json), Some("/tmp/some/target"));
}
#[test]
fn test_extract_target_directory_missing_key_returns_none() {
let json = r#"{"other":"x"}"#;
assert_eq!(extract_target_directory(json), None);
}
#[test]
fn test_extract_target_directory_unterminated_value_returns_none() {
let json = r#"{"target_directory":"still-open"#;
assert_eq!(extract_target_directory(json), None);
}
#[test]
fn test_extract_target_directory_handles_embedded_json_fragment() {
let json = r#"{"packages":[],"target_directory":"/x/y","version":1}"#;
assert_eq!(extract_target_directory(json), Some("/x/y"));
}
#[test]
fn test_load_cache_missing_file_returns_none() {
let tmp = TempDir::new().unwrap();
assert!(
load_coverage_from_cache(&tmp.path().join("missing.json"), "abc1234", tmp.path(),)
.is_none()
);
}
#[test]
fn test_load_cache_corrupt_json_returns_none() {
let tmp = TempDir::new().unwrap();
let p = tmp.path().join("cache.json");
write(&p, "{not valid json");
assert!(load_coverage_from_cache(&p, "abc1234", tmp.path()).is_none());
}
#[test]
fn test_load_cache_hash_mismatch_returns_none_when_no_mtime() {
let tmp = TempDir::new().unwrap();
let cache = CoverageCache {
git_hash: "oldhash".to_string(),
coverage_mtime: None,
profdata_dir: None,
files: HashMap::new(),
};
let p = tmp.path().join("cache.json");
write(&p, &serde_json::to_string(&cache).unwrap());
assert!(load_coverage_from_cache(&p, "newhash", tmp.path()).is_none());
}
#[test]
fn test_load_cache_hash_match_returns_files_when_no_mtime() {
let tmp = TempDir::new().unwrap();
let mut files = HashMap::new();
let mut hits = HashMap::new();
hits.insert(1usize, 2u64);
files.insert("src/foo.rs".to_string(), hits);
let cache = CoverageCache {
git_hash: "abc".to_string(),
coverage_mtime: None,
profdata_dir: None,
files: files.clone(),
};
let p = tmp.path().join("cache.json");
write(&p, &serde_json::to_string(&cache).unwrap());
let loaded = load_coverage_from_cache(&p, "abc", tmp.path()).expect("some");
assert_eq!(loaded, files);
}
#[test]
fn test_write_coverage_cache_writes_json_and_creates_pmat_dir() {
let tmp = TempDir::new().unwrap();
let cache_path = tmp.path().join(".pmat").join("coverage-cache.json");
let mut files = HashMap::new();
let mut hits = HashMap::new();
hits.insert(5usize, 1u64);
files.insert("src/lib.rs".to_string(), hits.clone());
write_coverage_cache(&cache_path, "sha-xyz", tmp.path(), &files);
assert!(cache_path.exists(), "cache file missing");
let content = std::fs::read_to_string(&cache_path).unwrap();
let cache: CoverageCache = serde_json::from_str(&content).unwrap();
assert_eq!(cache.git_hash, "sha-xyz");
assert_eq!(cache.files.get("src/lib.rs"), Some(&hits));
}
#[test]
fn test_write_negative_coverage_cache_writes_empty_files_map() {
let tmp = TempDir::new().unwrap();
let cache_path = tmp.path().join(".pmat").join("neg.json");
write_negative_coverage_cache(&cache_path, "sha-neg", tmp.path());
assert!(cache_path.exists());
let cache: CoverageCache =
serde_json::from_str(&std::fs::read_to_string(&cache_path).unwrap()).unwrap();
assert_eq!(cache.git_hash, "sha-neg");
assert!(cache.files.is_empty());
}
#[test]
fn test_get_profdata_mtime_fast_returns_stored_path_when_extant() {
let tmp = TempDir::new().unwrap();
let stored = tmp.path().to_string_lossy().into_owned();
let out = get_profdata_mtime_fast(tmp.path(), Some(&stored));
assert!(out.is_some(), "expected Some for extant stored_path");
let (_, path) = out.unwrap();
assert_eq!(path, stored);
}
#[test]
fn test_get_profdata_mtime_fast_returns_none_on_empty_project() {
let tmp = TempDir::new().unwrap();
let orig = std::env::var("CARGO_TARGET_DIR").ok();
std::env::remove_var("CARGO_TARGET_DIR");
let out = get_profdata_mtime_fast(tmp.path(), None);
match orig {
Some(v) => std::env::set_var("CARGO_TARGET_DIR", v),
None => std::env::remove_var("CARGO_TARGET_DIR"),
}
let _ = out; }
#[test]
fn test_load_cache_with_matching_mtime_bypasses_git_hash() {
let tmp = TempDir::new().unwrap();
let profdata = tmp.path().join("target").join("llvm-cov-target");
std::fs::create_dir_all(&profdata).unwrap();
let current_mtime = dir_mtime(&profdata).expect("mtime");
let cache = CoverageCache {
git_hash: "wrong-hash".to_string(),
coverage_mtime: Some(current_mtime),
profdata_dir: Some(profdata.to_string_lossy().into_owned()),
files: {
let mut m = HashMap::new();
m.insert("x.rs".to_string(), HashMap::new());
m
},
};
let cache_path = tmp.path().join("cache.json");
write(&cache_path, &serde_json::to_string(&cache).unwrap());
let loaded = load_coverage_from_cache(&cache_path, "any-hash", tmp.path());
assert!(
loaded.is_some(),
"cached mtime should validate regardless of git hash"
);
assert!(loaded.unwrap().contains_key("x.rs"));
}
#[test]
fn test_load_cache_stale_mtime_returns_none() {
let tmp = TempDir::new().unwrap();
let profdata = tmp.path().join("target").join("llvm-cov-target");
std::fs::create_dir_all(&profdata).unwrap();
let cache = CoverageCache {
git_hash: "h".to_string(),
coverage_mtime: Some(0), profdata_dir: Some(profdata.to_string_lossy().into_owned()),
files: HashMap::new(),
};
let cache_path = tmp.path().join("cache.json");
write(&cache_path, &serde_json::to_string(&cache).unwrap());
let loaded = load_coverage_from_cache(&cache_path, "h", tmp.path());
assert!(loaded.is_none(), "stale mtime must invalidate cache");
}
}