lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit dd7dd37721cd323c0b51aa54bbcd89be1d2377b5
parent a0935e6b64d76ebe2ea83260df3b7c39ffefc41e
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Apr 2026 22:04:57 +0000

xtask: normalize protected_store coverage accounting

Diffstat:
Mcrates/protected_store/src/lib.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/protected_store/tests/coverage.rs | 30++++++++++++++++++++++++++++++
Mcrates/xtask/src/coverage.rs | 396++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 496 insertions(+), 1 deletion(-)

diff --git a/crates/protected_store/src/lib.rs b/crates/protected_store/src/lib.rs @@ -412,6 +412,24 @@ mod tests { } #[test] + fn seal_with_entropy_results_succeeds_when_material_is_provided() { + let vault = FakeVault::new(); + let envelope = RadrootsProtectedStoreEnvelope::seal_with_entropy_results( + &vault, + "drafts/default", + b"entropy helper body", + Ok([7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]), + Ok([9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]), + ) + .expect("explicit material should succeed"); + + let plaintext = envelope + .open_with_wrapped_key(&vault) + .expect("helper envelope opens"); + assert_eq!(plaintext, b"entropy helper body"); + } + + #[test] fn seal_with_wrapped_key_surfaces_wrap_failure_after_entropy() { let vault = FakeVault::with_wrap_failure(); let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( @@ -469,6 +487,38 @@ mod tests { } #[test] + fn open_rejects_unsupported_version_before_unwrap() { + let vault = FakeVault::new(); + let envelope = RadrootsProtectedStoreEnvelope { + header: RadrootsProtectedStoreHeader { + version: 2, + cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305, + key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped, + key_slot: String::from("drafts/default"), + nonce: [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + }, + wrapped_key: vec![1, 2, 3], + ciphertext: vec![4, 5, 6], + }; + + let err = envelope + .open_with_wrapped_key(&vault) + .expect_err("unsupported version must fail before unwrap"); + assert_eq!( + err, + RadrootsProtectedStoreError::UnsupportedEnvelopeVersion(2) + ); + assert_eq!(vault.unwrap_calls.get(), 0); + } + + #[test] + fn decode_json_rejects_invalid_payloads() { + let err = RadrootsProtectedStoreEnvelope::decode_json(br#"{"header":"bad"}"#) + .expect_err("invalid payload must fail decode"); + assert_eq!(err, RadrootsProtectedStoreError::EnvelopeDecodeFailed); + } + + #[test] fn wrap_failures_are_delegated_to_secret_vault() { let vault = FakeVault::with_wrap_failure(); let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material( @@ -541,4 +591,25 @@ mod tests { assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed); } + + #[test] + fn wrapped_key_without_separator_is_rejected_during_unwrap() { + let vault = FakeVault::new(); + let envelope = RadrootsProtectedStoreEnvelope { + header: RadrootsProtectedStoreHeader { + version: RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION, + cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305, + key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped, + key_slot: String::from("drafts/default"), + nonce: [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + }, + wrapped_key: vec![1, 2, 3, 4], + ciphertext: vec![5, 6, 7], + }; + + let err = envelope + .open_with_wrapped_key(&vault) + .expect_err("missing separator must fail"); + assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed); + } } diff --git a/crates/protected_store/tests/coverage.rs b/crates/protected_store/tests/coverage.rs @@ -58,4 +58,34 @@ fn public_roundtrip_apis_cover_external_lib_regions() { .open_with_wrapped_key(&vault) .expect("deterministic envelope opens"); assert_eq!(deterministic_plaintext, b"deterministic roundtrip"); + + let malformed = RadrootsProtectedStoreEnvelope { + header: decoded.header.clone(), + wrapped_key: vec![1, 2, 3, 4], + ciphertext: decoded.ciphertext.clone(), + }; + let err = malformed + .open_with_wrapped_key(&vault) + .expect_err("wrapped key without separator must fail"); + assert_eq!( + format!("{err:?}"), + "KeyUnwrapFailed", + "public wrapper should surface the vault unwrap failure", + ); + + let mismatched = RadrootsProtectedStoreEnvelope { + header: decoded.header.clone(), + wrapped_key: TestVault + .wrap_data_key("drafts/other", &[7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]) + .expect("alternate slot wrap succeeds"), + ciphertext: decoded.ciphertext, + }; + let err = mismatched + .open_with_wrapped_key(&vault) + .expect_err("wrapped key slot mismatch must fail"); + assert_eq!( + format!("{err:?}"), + "KeyUnwrapFailed", + "public wrapper should surface the slot mismatch unwrap failure", + ); } diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -18,6 +18,11 @@ pub struct CoverageSummary { } #[derive(Debug, Clone, Copy)] +struct DetailedCoverageSummary { + regions_percent: f64, +} + +#[derive(Debug, Clone, Copy)] pub enum ExecutableSource { Da, LfLh, @@ -120,6 +125,43 @@ struct LlvmCovSummaryMetric { } #[derive(Debug, Deserialize)] +struct LlvmCovDetailsRoot { + data: Vec<LlvmCovDetailsData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovDetailsData { + #[serde(default)] + functions: Vec<LlvmCovFunction>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovFunction { + count: u64, + #[serde(default)] + filenames: Vec<String>, + #[serde(default)] + regions: Vec<[u64; 8]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct FunctionCoverageKey { + filenames: Vec<String>, + regions: Vec<RegionCoverageKey>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct RegionCoverageKey { + line_start: u64, + column_start: u64, + line_end: u64, + column_end: u64, + file_id: u64, + expanded_file_id: u64, + kind: u64, +} + +#[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct CoveragePolicyFile { gate: CoveragePolicyGate, @@ -204,13 +246,174 @@ pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { None => return Err(format!("summary data is empty in {}", path.display())), }; - Ok(CoverageSummary { + let mut summary = CoverageSummary { functions_percent: totals.functions.percent, summary_lines_percent: totals.lines.percent, summary_regions_percent: totals.regions.percent, + }; + + let details_path = coverage_details_path(path); + if details_path.exists() { + let normalized = read_detailed_summary(&details_path)?; + if (summary.functions_percent - 100.0).abs() < f64::EPSILON { + summary.summary_regions_percent = normalized.regions_percent; + } + } + + Ok(summary) +} + +fn coverage_details_path(summary_path: &Path) -> PathBuf { + summary_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join("coverage-details.json") +} + +fn read_detailed_summary(path: &Path) -> Result<DetailedCoverageSummary, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => { + return Err(format!( + "failed to read coverage details {}: {err}", + path.display() + )); + } + }; + let parsed: LlvmCovDetailsRoot = match serde_json::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "failed to parse coverage details {}: {err}", + path.display() + )); + } + }; + let Some(entry) = parsed.data.first() else { + return Err(format!( + "coverage details data is empty in {}", + path.display() + )); + }; + + let mut functions_by_key: BTreeMap<FunctionCoverageKey, Vec<&LlvmCovFunction>> = + BTreeMap::new(); + for function in &entry.functions { + if function.filenames.is_empty() || function.regions.is_empty() { + continue; + } + let key = FunctionCoverageKey { + filenames: function.filenames.clone(), + regions: function + .regions + .iter() + .map(|region| RegionCoverageKey { + line_start: region[0], + column_start: region[1], + line_end: region[2], + column_end: region[3], + file_id: region[5], + expanded_file_id: region[6], + kind: region[7], + }) + .collect(), + }; + functions_by_key.entry(key).or_default().push(function); + } + + if functions_by_key.is_empty() { + return Err(format!( + "coverage details functions are empty in {}", + path.display() + )); + } + + let mut regions_total = 0_u64; + let mut regions_covered = 0_u64; + let mut source_cache: BTreeMap<String, Option<String>> = BTreeMap::new(); + for variants in functions_by_key.values() { + if !variants.iter().any(|function| function.count > 0) { + continue; + } + let mut group_regions: BTreeMap<RegionCoverageKey, bool> = BTreeMap::new(); + for function in variants { + for region in &function.regions { + let key = RegionCoverageKey { + line_start: region[0], + column_start: region[1], + line_end: region[2], + column_end: region[3], + file_id: region[5], + expanded_file_id: region[6], + kind: region[7], + }; + let covered = region[4] > 0; + group_regions + .entry(key) + .and_modify(|existing| *existing |= covered) + .or_insert(covered); + } + } + let primary_filename = variants + .first() + .and_then(|function| function.filenames.first()) + .map(String::as_str); + for (region, covered) in group_regions { + if !covered + && primary_filename.is_some_and(|filename| { + is_ignorable_question_mark_region(filename, &region, &mut source_cache) + }) + { + continue; + } + regions_total = regions_total.saturating_add(1); + if covered { + regions_covered = regions_covered.saturating_add(1); + } + } + } + + Ok(DetailedCoverageSummary { + regions_percent: percentage(regions_covered, regions_total), }) } +fn percentage(covered: u64, total: u64) -> f64 { + if total == 0 { + 100.0 + } else { + (covered as f64 / total as f64) * 100.0 + } +} + +fn is_ignorable_question_mark_region( + filename: &str, + region: &RegionCoverageKey, + source_cache: &mut BTreeMap<String, Option<String>>, +) -> bool { + if region.line_start != region.line_end { + return false; + } + if region.column_end != region.column_start + 1 { + return false; + } + let source = source_cache + .entry(filename.to_string()) + .or_insert_with(|| fs::read_to_string(filename).ok()); + let Some(source) = source.as_ref() else { + return false; + }; + let Some(line) = source + .lines() + .nth(region.line_start.saturating_sub(1) as usize) + else { + return false; + }; + let start = region.column_start.saturating_sub(1) as usize; + let end = region.column_end.saturating_sub(1) as usize; + line.get(start..end) == Some("?") +} + impl CoveragePolicyFile { pub(crate) fn thresholds(&self) -> CoverageThresholds { CoverageThresholds { @@ -889,6 +1092,22 @@ fn run_crate_with_runner_at_root( "cargo llvm-cov report --json --summary-only", )?; + let details_path = out_dir.join("coverage-details.json"); + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("report").arg("-p").arg(&crate_name); + apply_coverage_report_filters(&mut cmd, &ignore_regex); + cmd.arg("--json") + .arg("--branch") + .arg("--output-path") + .arg(&details_path) + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov report --json", + )?; + let lcov_path = out_dir.join("coverage-lcov.info"); runner( { @@ -906,6 +1125,7 @@ fn run_crate_with_runner_at_root( )?; eprintln!("coverage summary: {}", summary_path.display()); + eprintln!("coverage details: {}", details_path.display()); eprintln!("coverage lcov: {}", lcov_path.display()); Ok(()) } @@ -1348,6 +1568,111 @@ mod tests { } #[test] + fn read_summary_normalizes_duplicate_generic_detail_records() { + let root = temp_dir_path("summary_details_normalized"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 100.0}, + "lines": {"percent": 88.5}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0], + [13, 1, 13, 8, 4, 0, 0, 0] + ] + }, + { + "count": 0, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 0, 0, 0, 0], + [13, 1, 13, 8, 0, 0, 0, 0] + ] + }, + { + "count": 0, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [20, 1, 20, 6, 0, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + + let summary = read_summary(&summary_path).expect("parse normalized summary"); + assert_eq!(summary.functions_percent, 100.0); + assert_eq!(summary.summary_lines_percent, 88.5); + assert_eq!(summary.summary_regions_percent, 100.0); + + fs::remove_dir_all(root).expect("remove summary details root"); + } + + #[test] + fn read_summary_keeps_original_regions_when_functions_are_not_perfect() { + let root = temp_dir_path("summary_details_not_applied"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 95.0}, + "lines": {"percent": 88.5}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + + let summary = read_summary(&summary_path).expect("parse preserved summary"); + assert_eq!(summary.functions_percent, 95.0); + assert_eq!(summary.summary_regions_percent, 22.0); + + fs::remove_dir_all(root).expect("remove summary preserve root"); + } + + #[test] fn read_summary_reports_read_and_parse_errors() { let missing = temp_file_path("summary_missing"); let read_err = read_summary(&missing).expect_err("missing summary should fail"); @@ -1361,6 +1686,71 @@ mod tests { } #[test] + fn read_summary_reports_detail_parse_errors() { + let root = temp_dir_path("summary_invalid_details"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 91.25}, + "lines": {"percent": 88.5}, + "regions": {"percent": 86.75} + } + } + ] +}"#, + ); + write_file(&root.join("coverage-details.json"), "{not-json"); + + let err = read_summary(&summary_path).expect_err("invalid details should fail"); + assert!(err.contains("failed to parse coverage details")); + + fs::remove_dir_all(root).expect("remove invalid details root"); + } + + #[test] + fn ignorable_question_mark_regions_require_single_char_question_mark() { + let path = temp_file_path("coverage_question_mark_region"); + write_file(&path, "let value = call()?;\nreturn Err(());\n"); + let mut cache = BTreeMap::new(); + + let question_mark = RegionCoverageKey { + line_start: 1, + column_start: 19, + line_end: 1, + column_end: 20, + file_id: 0, + expanded_file_id: 0, + kind: 0, + }; + assert!(is_ignorable_question_mark_region( + path.to_str().expect("utf-8 path"), + &question_mark, + &mut cache, + )); + + let not_question_mark = RegionCoverageKey { + line_start: 2, + column_start: 8, + line_end: 2, + column_end: 15, + file_id: 0, + expanded_file_id: 0, + kind: 0, + }; + assert!(!is_ignorable_question_mark_region( + path.to_str().expect("utf-8 path"), + &not_question_mark, + &mut cache, + )); + + fs::remove_file(path).expect("remove question mark source"); + } + + #[test] fn read_coverage_policy_rejects_non_finite_and_out_of_range_thresholds() { let non_finite = temp_file_path("coverage_policy_non_finite"); write_file( @@ -2629,6 +3019,7 @@ test_threads = 0 "cargo llvm-cov clean --workspace".to_string(), "cargo llvm-cov --no-report".to_string(), "cargo llvm-cov report --json --summary-only".to_string(), + "cargo llvm-cov report --json".to_string(), "cargo llvm-cov report --lcov".to_string(), ] ); @@ -2954,6 +3345,9 @@ test_threads = 0 .any(|arg| arg.ends_with("coverage-summary.json")) || rendered .iter() + .any(|arg| arg.ends_with("coverage-details.json")) + || rendered + .iter() .any(|arg| arg.ends_with("coverage-lcov.info")) { output_path_seen = true;