lib

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

coverage.rs (188167B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::ffi::OsString;
      4 use std::fs;
      5 use std::path::Path;
      6 use std::path::PathBuf;
      7 use std::process::Command;
      8 use std::{collections::BTreeMap, collections::BTreeSet, io::Write};
      9 
     10 use serde::Deserialize;
     11 use serde::Serialize;
     12 
     13 #[derive(Debug, Clone)]
     14 pub struct CoverageSummary {
     15     pub functions_percent: f64,
     16     pub summary_lines_percent: f64,
     17     pub summary_regions_percent: f64,
     18 }
     19 
     20 #[derive(Debug, Clone, Copy)]
     21 struct DetailedCoverageSummary {
     22     functions_percent: f64,
     23     regions_percent: f64,
     24 }
     25 
     26 #[derive(Debug, Clone, Copy)]
     27 pub enum ExecutableSource {
     28     Da,
     29     LfLh,
     30 }
     31 
     32 #[derive(Debug, Clone)]
     33 pub struct LcovCoverage {
     34     pub executable_total: u64,
     35     pub executable_covered: u64,
     36     pub executable_percent: f64,
     37     pub executable_source: ExecutableSource,
     38     pub branch_total: u64,
     39     pub branch_covered: u64,
     40     pub branches_available: bool,
     41     pub branch_percent: Option<f64>,
     42 }
     43 
     44 #[derive(Debug, Clone, Copy)]
     45 pub struct CoverageThresholds {
     46     pub fail_under_exec_lines: f64,
     47     pub fail_under_functions: f64,
     48     pub fail_under_regions: f64,
     49     pub fail_under_branches: f64,
     50     pub require_branches: bool,
     51 }
     52 
     53 #[derive(Debug, Clone)]
     54 pub struct CoverageGateResult {
     55     pub pass: bool,
     56     pub fail_reasons: Vec<String>,
     57 }
     58 
     59 #[derive(Debug, Serialize, Deserialize)]
     60 struct CoverageGateReport {
     61     scope: String,
     62     thresholds: CoverageGateReportThresholds,
     63     measured: CoverageGateReportMeasured,
     64     counts: CoverageGateReportCounts,
     65     result: CoverageGateReportResult,
     66 }
     67 
     68 #[derive(Debug, Serialize, Deserialize)]
     69 struct CoverageGateReportThresholds {
     70     executable_lines: f64,
     71     functions: f64,
     72     regions: f64,
     73     branches: f64,
     74     branches_required: bool,
     75 }
     76 
     77 #[derive(Debug, Serialize, Deserialize)]
     78 struct CoverageGateReportMeasured {
     79     executable_lines_percent: f64,
     80     executable_lines_source: String,
     81     functions_percent: f64,
     82     branches_percent: Option<f64>,
     83     branches_available: bool,
     84     summary_lines_percent: f64,
     85     summary_regions_percent: f64,
     86 }
     87 
     88 #[derive(Debug, Serialize, Deserialize)]
     89 struct CoverageGateReportCounts {
     90     executable_lines: CoverageCount,
     91     branches: CoverageCount,
     92 }
     93 
     94 #[derive(Debug, Serialize, Deserialize)]
     95 struct CoverageCount {
     96     covered: u64,
     97     total: u64,
     98 }
     99 
    100 #[derive(Debug, Serialize, Deserialize)]
    101 struct CoverageGateReportResult {
    102     pass: bool,
    103     fail_reasons: Vec<String>,
    104 }
    105 
    106 #[derive(Debug, Deserialize)]
    107 struct LlvmCovSummaryRoot {
    108     data: Vec<LlvmCovSummaryData>,
    109 }
    110 
    111 #[derive(Debug, Deserialize)]
    112 struct LlvmCovSummaryData {
    113     totals: LlvmCovSummaryTotals,
    114 }
    115 
    116 #[derive(Debug, Deserialize)]
    117 struct LlvmCovSummaryTotals {
    118     functions: LlvmCovSummaryMetric,
    119     lines: LlvmCovSummaryMetric,
    120     regions: LlvmCovSummaryMetric,
    121 }
    122 
    123 #[derive(Debug, Deserialize)]
    124 struct LlvmCovSummaryMetric {
    125     percent: f64,
    126 }
    127 
    128 #[derive(Debug, Deserialize)]
    129 struct LlvmCovDetailsRoot {
    130     data: Vec<LlvmCovDetailsData>,
    131 }
    132 
    133 #[derive(Debug, Deserialize)]
    134 struct LlvmCovDetailsData {
    135     #[serde(default)]
    136     functions: Vec<LlvmCovFunction>,
    137 }
    138 
    139 #[derive(Debug, Deserialize)]
    140 struct LlvmCovFunction {
    141     count: u64,
    142     #[serde(default)]
    143     filenames: Vec<String>,
    144     #[serde(default)]
    145     regions: Vec<[u64; 8]>,
    146 }
    147 
    148 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
    149 struct FunctionCoverageKey {
    150     filenames: Vec<String>,
    151     regions: Vec<RegionCoverageKey>,
    152 }
    153 
    154 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
    155 struct RegionCoverageKey {
    156     line_start: u64,
    157     column_start: u64,
    158     line_end: u64,
    159     column_end: u64,
    160     kind: u64,
    161 }
    162 
    163 #[derive(Debug, Deserialize)]
    164 #[serde(deny_unknown_fields)]
    165 pub(crate) struct CoveragePolicyFile {
    166     gate: CoveragePolicyGate,
    167     required: CoverageRequiredList,
    168     #[serde(default)]
    169     overrides: BTreeMap<String, CoveragePolicyOverride>,
    170 }
    171 
    172 #[derive(Debug, Deserialize)]
    173 #[serde(deny_unknown_fields)]
    174 pub(crate) struct CoveragePolicyGate {
    175     fail_under_exec_lines: f64,
    176     fail_under_functions: f64,
    177     fail_under_regions: f64,
    178     fail_under_branches: f64,
    179     require_branches: bool,
    180 }
    181 
    182 #[derive(Debug, Deserialize)]
    183 #[serde(deny_unknown_fields)]
    184 pub(crate) struct CoverageRequiredList {
    185     crates: Vec<String>,
    186 }
    187 
    188 #[derive(Debug, Deserialize)]
    189 #[serde(deny_unknown_fields)]
    190 pub(crate) struct CoveragePolicyOverride {
    191     fail_under_exec_lines: Option<f64>,
    192     fail_under_functions: Option<f64>,
    193     fail_under_regions: Option<f64>,
    194     fail_under_branches: Option<f64>,
    195     require_branches: Option<bool>,
    196     temporary: bool,
    197     reason: String,
    198 }
    199 
    200 #[derive(Debug, Deserialize)]
    201 struct WorkspaceManifest {
    202     workspace: WorkspaceMembers,
    203 }
    204 
    205 #[derive(Debug, Deserialize)]
    206 struct WorkspaceMembers {
    207     members: Vec<String>,
    208 }
    209 
    210 #[derive(Debug, Deserialize)]
    211 struct PackageManifest {
    212     package: PackageSection,
    213 }
    214 
    215 #[derive(Debug, Deserialize)]
    216 struct PackageSection {
    217     name: String,
    218 }
    219 
    220 #[derive(Debug, Deserialize, Default)]
    221 struct CoverageProfilesFile {
    222     #[serde(default)]
    223     profiles: CoverageProfilesSection,
    224 }
    225 
    226 #[derive(Debug, Deserialize, Default)]
    227 struct CoverageProfilesSection {
    228     #[serde(default)]
    229     default: CoverageProfileRaw,
    230     #[serde(default)]
    231     crates: BTreeMap<String, CoverageProfileRaw>,
    232 }
    233 
    234 #[derive(Debug, Deserialize, Default, Clone)]
    235 struct CoverageProfileRaw {
    236     no_default_features: Option<bool>,
    237     features: Option<Vec<String>>,
    238     test_threads: Option<u32>,
    239 }
    240 
    241 #[derive(Debug, Clone)]
    242 struct CoverageProfile {
    243     no_default_features: bool,
    244     features: Vec<String>,
    245     test_threads: Option<u32>,
    246 }
    247 
    248 #[cfg_attr(not(test), allow(dead_code))]
    249 pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> {
    250     read_summary_for_scope(path, None)
    251 }
    252 
    253 fn read_summary_for_scope(path: &Path, scope: Option<&str>) -> Result<CoverageSummary, String> {
    254     let raw = match fs::read_to_string(path) {
    255         Ok(raw) => raw,
    256         Err(err) => return Err(format!("failed to read summary {}: {err}", path.display())),
    257     };
    258     let parsed: LlvmCovSummaryRoot = match serde_json::from_str(&raw) {
    259         Ok(parsed) => parsed,
    260         Err(err) => return Err(format!("failed to parse summary {}: {err}", path.display())),
    261     };
    262     let totals = match parsed.data.first() {
    263         Some(entry) => &entry.totals,
    264         None => return Err(format!("summary data is empty in {}", path.display())),
    265     };
    266 
    267     let mut summary = CoverageSummary {
    268         functions_percent: totals.functions.percent,
    269         summary_lines_percent: totals.lines.percent,
    270         summary_regions_percent: totals.regions.percent,
    271     };
    272 
    273     let details_path = coverage_details_path(path);
    274     if details_path.exists() {
    275         let normalized = read_detailed_summary(&details_path, scope)?;
    276         if (summary.functions_percent - 100.0).abs() < f64::EPSILON {
    277             summary.summary_regions_percent = normalized.regions_percent;
    278         }
    279     }
    280 
    281     Ok(summary)
    282 }
    283 
    284 fn coverage_details_path(summary_path: &Path) -> PathBuf {
    285     summary_path
    286         .parent()
    287         .unwrap_or_else(|| Path::new("."))
    288         .join("coverage-details.json")
    289 }
    290 
    291 fn read_detailed_summary(
    292     path: &Path,
    293     scope: Option<&str>,
    294 ) -> Result<DetailedCoverageSummary, String> {
    295     let raw = match fs::read_to_string(path) {
    296         Ok(raw) => raw,
    297         Err(err) => {
    298             return Err(format!(
    299                 "failed to read coverage details {}: {err}",
    300                 path.display()
    301             ));
    302         }
    303     };
    304     let parsed: LlvmCovDetailsRoot = match serde_json::from_str(&raw) {
    305         Ok(parsed) => parsed,
    306         Err(err) => {
    307             return Err(format!(
    308                 "failed to parse coverage details {}: {err}",
    309                 path.display()
    310             ));
    311         }
    312     };
    313     let Some(entry) = parsed.data.first() else {
    314         return Err(format!(
    315             "coverage details data is empty in {}",
    316             path.display()
    317         ));
    318     };
    319 
    320     let mut functions_by_key: BTreeMap<FunctionCoverageKey, Vec<&LlvmCovFunction>> =
    321         BTreeMap::new();
    322     for function in &entry.functions {
    323         if function.filenames.is_empty() || function.regions.is_empty() {
    324             continue;
    325         }
    326         let key = FunctionCoverageKey {
    327             filenames: function.filenames.clone(),
    328             regions: function
    329                 .regions
    330                 .iter()
    331                 .map(|region| RegionCoverageKey {
    332                     line_start: region[0],
    333                     column_start: region[1],
    334                     line_end: region[2],
    335                     column_end: region[3],
    336                     kind: region[7],
    337                 })
    338                 .collect(),
    339         };
    340         functions_by_key.entry(key).or_default().push(function);
    341     }
    342 
    343     if functions_by_key.is_empty() {
    344         return Err(format!(
    345             "coverage details functions are empty in {}",
    346             path.display()
    347         ));
    348     }
    349 
    350     let mut regions_total = 0_u64;
    351     let mut regions_covered = 0_u64;
    352     let mut functions_total = 0_u64;
    353     let mut functions_covered = 0_u64;
    354     let mut source_cache: BTreeMap<String, Option<String>> = BTreeMap::new();
    355     let scope_filter = scope.map(scope_path_fragment);
    356     for variants in functions_by_key.values() {
    357         if let Some(scope_filter) = scope_filter.as_deref()
    358             && !variants.iter().any(|function| {
    359                 function
    360                     .filenames
    361                     .first()
    362                     .is_some_and(|filename| filename.contains(scope_filter))
    363             })
    364         {
    365             continue;
    366         }
    367         let primary_filename = variants
    368             .iter()
    369             .filter_map(|function| function.filenames.first())
    370             .find(|filename| {
    371                 scope_filter
    372                     .as_deref()
    373                     .is_none_or(|scope_filter| filename.contains(scope_filter))
    374             })
    375             .map(String::as_str);
    376         if primary_filename.is_some_and(|filename| {
    377             is_ignorable_detail_function(filename, variants, &mut source_cache)
    378         }) {
    379             continue;
    380         }
    381         functions_total = functions_total.saturating_add(1);
    382         if variants.iter().any(|function| function.count > 0) {
    383             functions_covered = functions_covered.saturating_add(1);
    384         }
    385         let mut group_regions: BTreeMap<RegionCoverageKey, bool> = BTreeMap::new();
    386         for function in variants {
    387             for region in &function.regions {
    388                 let key = RegionCoverageKey {
    389                     line_start: region[0],
    390                     column_start: region[1],
    391                     line_end: region[2],
    392                     column_end: region[3],
    393                     kind: region[7],
    394                 };
    395                 let covered = region[4] > 0;
    396                 group_regions
    397                     .entry(key)
    398                     .and_modify(|existing| *existing |= covered)
    399                     .or_insert(covered);
    400             }
    401         }
    402         for (region, covered) in group_regions {
    403             if !covered
    404                 && primary_filename.is_some_and(|filename| {
    405                     is_ignorable_synthetic_region(filename, &region, &mut source_cache)
    406                 })
    407             {
    408                 continue;
    409             }
    410             regions_total = regions_total.saturating_add(1);
    411             if covered {
    412                 regions_covered = regions_covered.saturating_add(1);
    413             }
    414         }
    415     }
    416 
    417     Ok(DetailedCoverageSummary {
    418         functions_percent: percentage(functions_covered, functions_total),
    419         regions_percent: percentage(regions_covered, regions_total),
    420     })
    421 }
    422 
    423 fn is_ignorable_detail_function(
    424     filename: &str,
    425     variants: &[&LlvmCovFunction],
    426     source_cache: &mut BTreeMap<String, Option<String>>,
    427 ) -> bool {
    428     let source = source_cache
    429         .entry(filename.to_string())
    430         .or_insert_with(|| fs::read_to_string(filename).ok());
    431     let Some(source) = source.as_ref() else {
    432         return false;
    433     };
    434     variants.iter().all(|function| {
    435         function
    436             .regions
    437             .iter()
    438             .all(|region| is_cfg_test_source_line(source, region[0]))
    439     })
    440 }
    441 
    442 fn scope_path_fragment(scope: &str) -> String {
    443     let crate_dir = scope.strip_prefix("radroots_").unwrap_or(scope);
    444     format!("/crates/{crate_dir}/src/")
    445 }
    446 
    447 fn percentage(covered: u64, total: u64) -> f64 {
    448     if total == 0 {
    449         100.0
    450     } else {
    451         (covered as f64 / total as f64) * 100.0
    452     }
    453 }
    454 
    455 fn is_ignorable_synthetic_region(
    456     filename: &str,
    457     region: &RegionCoverageKey,
    458     source_cache: &mut BTreeMap<String, Option<String>>,
    459 ) -> bool {
    460     let source = source_cache
    461         .entry(filename.to_string())
    462         .or_insert_with(|| fs::read_to_string(filename).ok());
    463     let Some(source) = source.as_ref() else {
    464         return false;
    465     };
    466     if is_cfg_test_source_line(source, region.line_start) {
    467         return true;
    468     }
    469     if region.line_start != region.line_end {
    470         return false;
    471     }
    472     let Some(line) = source
    473         .lines()
    474         .nth(region.line_start.saturating_sub(1) as usize)
    475     else {
    476         return false;
    477     };
    478     let start = region.column_start.saturating_sub(1) as usize;
    479     let end = region.column_end.saturating_sub(1) as usize;
    480     let slice = line.get(start..end);
    481     if region.column_end == region.column_start + 1 && slice == Some("?") {
    482         return true;
    483     }
    484     if line.contains("unreachable!()") {
    485         return true;
    486     }
    487     if line.contains("assert!(matches!(") && slice == Some("matches!") {
    488         return true;
    489     }
    490 
    491     filename.ends_with("/tests.rs")
    492         && line.contains("panic!(\"unexpected")
    493         && matches!(slice, Some("other") | Some("panic!"))
    494 }
    495 
    496 fn is_ignorable_lcov_source_line(
    497     filename: &str,
    498     line_number: u64,
    499     source_cache: &mut BTreeMap<String, Option<String>>,
    500 ) -> bool {
    501     let source = source_cache
    502         .entry(filename.to_string())
    503         .or_insert_with(|| fs::read_to_string(filename).ok());
    504     let Some(source) = source.as_ref() else {
    505         return false;
    506     };
    507     if is_cfg_test_source_line(source, line_number) {
    508         return true;
    509     }
    510     let Some(line) = source.lines().nth(line_number.saturating_sub(1) as usize) else {
    511         return false;
    512     };
    513     let trimmed = line.trim();
    514     matches!(
    515         trimmed,
    516         ")?" | ")?;"
    517             | ")?)"
    518             | ")?, "
    519             | ")?,"
    520             | "})?"
    521             | "})?;"
    522             | "})?,"
    523             | "])?;"
    524             | "}"
    525             | "},"
    526             | "};"
    527     ) || line.contains("unreachable!()")
    528         || line.contains("panic!(\"expected")
    529         || line.contains("panic!(\"unexpected")
    530 }
    531 
    532 fn is_cfg_test_source_line(source: &str, line_number: u64) -> bool {
    533     let mut pending_cfg_test = false;
    534     let mut test_depth: Option<i64> = None;
    535     for (index, line) in source.lines().enumerate() {
    536         let current_line = index as u64 + 1;
    537         let trimmed = line.trim();
    538         let mut started_test_block = false;
    539         if trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("#[cfg(all(test,") {
    540             pending_cfg_test = true;
    541         } else if pending_cfg_test && trimmed.starts_with("mod tests") && trimmed.contains('{') {
    542             test_depth = Some(brace_delta(trimmed));
    543             pending_cfg_test = false;
    544             started_test_block = true;
    545         }
    546         let in_test = pending_cfg_test || test_depth.is_some();
    547         if current_line == line_number {
    548             return in_test;
    549         }
    550         if started_test_block {
    551             continue;
    552         }
    553         if let Some(depth) = test_depth.as_mut() {
    554             *depth += brace_delta(trimmed);
    555             if *depth <= 0 {
    556                 test_depth = None;
    557             }
    558         }
    559     }
    560     false
    561 }
    562 
    563 fn brace_delta(line: &str) -> i64 {
    564     let opens = line.bytes().filter(|byte| *byte == b'{').count() as i64;
    565     let closes = line.bytes().filter(|byte| *byte == b'}').count() as i64;
    566     opens - closes
    567 }
    568 
    569 impl CoveragePolicyFile {
    570     pub(crate) fn thresholds(&self) -> CoverageThresholds {
    571         CoverageThresholds {
    572             fail_under_exec_lines: self.gate.fail_under_exec_lines,
    573             fail_under_functions: self.gate.fail_under_functions,
    574             fail_under_regions: self.gate.fail_under_regions,
    575             fail_under_branches: self.gate.fail_under_branches,
    576             require_branches: self.gate.require_branches,
    577         }
    578     }
    579 
    580     pub(crate) fn thresholds_for_scope(&self, scope: &str) -> CoverageThresholds {
    581         let base = self.thresholds();
    582         let Some(override_policy) = self.overrides.get(scope) else {
    583             return base;
    584         };
    585         CoverageThresholds {
    586             fail_under_exec_lines: override_policy
    587                 .fail_under_exec_lines
    588                 .unwrap_or(base.fail_under_exec_lines),
    589             fail_under_functions: override_policy
    590                 .fail_under_functions
    591                 .unwrap_or(base.fail_under_functions),
    592             fail_under_regions: override_policy
    593                 .fail_under_regions
    594                 .unwrap_or(base.fail_under_regions),
    595             fail_under_branches: override_policy
    596                 .fail_under_branches
    597                 .unwrap_or(base.fail_under_branches),
    598             require_branches: override_policy
    599                 .require_branches
    600                 .unwrap_or(base.require_branches),
    601         }
    602     }
    603 
    604     pub(crate) fn required_crates(&self) -> Result<Vec<String>, String> {
    605         if self.required.crates.is_empty() {
    606             return Err("coverage required crates list must not be empty".to_string());
    607         }
    608         let mut seen = BTreeSet::new();
    609         for crate_name in &self.required.crates {
    610             if crate_name.trim().is_empty() {
    611                 return Err(
    612                     "coverage required crates list includes an empty crate name".to_string()
    613                 );
    614             }
    615             if !seen.insert(crate_name.clone()) {
    616                 return Err(format!(
    617                     "coverage required crates list includes duplicate crate {crate_name}"
    618                 ));
    619             }
    620         }
    621         Ok(self.required.crates.clone())
    622     }
    623 
    624     fn validate_overrides(&self) -> Result<(), String> {
    625         let required_crates = self.required_crates()?;
    626         let required_set: BTreeSet<_> = required_crates.into_iter().collect();
    627         let base = self.thresholds();
    628         for (crate_name, override_policy) in &self.overrides {
    629             if !required_set.contains(crate_name) {
    630                 return Err(format!(
    631                     "coverage override {crate_name} must target a required crate"
    632                 ));
    633             }
    634             if !override_policy.temporary {
    635                 return Err(format!(
    636                     "coverage override {crate_name} must set temporary = true"
    637                 ));
    638             }
    639             if override_policy.reason.trim().is_empty() {
    640                 return Err(format!(
    641                     "coverage override {crate_name} must include a non-empty reason"
    642                 ));
    643             }
    644             validate_override_threshold(
    645                 crate_name,
    646                 "fail_under_exec_lines",
    647                 override_policy.fail_under_exec_lines,
    648                 base.fail_under_exec_lines,
    649             )?;
    650             validate_override_threshold(
    651                 crate_name,
    652                 "fail_under_functions",
    653                 override_policy.fail_under_functions,
    654                 base.fail_under_functions,
    655             )?;
    656             validate_override_threshold(
    657                 crate_name,
    658                 "fail_under_regions",
    659                 override_policy.fail_under_regions,
    660                 base.fail_under_regions,
    661             )?;
    662             validate_override_threshold(
    663                 crate_name,
    664                 "fail_under_branches",
    665                 override_policy.fail_under_branches,
    666                 base.fail_under_branches,
    667             )?;
    668             if override_policy.require_branches == Some(true) && !base.require_branches {
    669                 return Err(format!(
    670                     "coverage override {crate_name} require_branches cannot be stricter than the global gate"
    671                 ));
    672             }
    673         }
    674         Ok(())
    675     }
    676 
    677     pub(crate) fn required_crate_entries(&self) -> &[String] {
    678         &self.required.crates
    679     }
    680 }
    681 
    682 fn validate_override_threshold(
    683     crate_name: &str,
    684     label: &str,
    685     value: Option<f64>,
    686     global: f64,
    687 ) -> Result<(), String> {
    688     let Some(value) = value else {
    689         return Ok(());
    690     };
    691     if !value.is_finite() {
    692         return Err(format!(
    693             "coverage override {crate_name} {label} must be finite"
    694         ));
    695     }
    696     if !(0.0..=100.0).contains(&value) {
    697         return Err(format!(
    698             "coverage override {crate_name} {label} must be within 0..=100"
    699         ));
    700     }
    701     if value > global {
    702         return Err(format!(
    703             "coverage override {crate_name} {label} must not exceed the global gate"
    704         ));
    705     }
    706     Ok(())
    707 }
    708 
    709 pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf {
    710     root.join("contracts").join("coverage.toml")
    711 }
    712 
    713 pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> {
    714     let raw = match fs::read_to_string(path) {
    715         Ok(raw) => raw,
    716         Err(err) => {
    717             return Err(format!(
    718                 "failed to read coverage policy {}: {err}",
    719                 path.display()
    720             ));
    721         }
    722     };
    723     let parsed: CoveragePolicyFile = match toml::from_str(&raw) {
    724         Ok(parsed) => parsed,
    725         Err(err) => {
    726             return Err(format!(
    727                 "failed to parse coverage policy {}: {err}",
    728                 path.display()
    729             ));
    730         }
    731     };
    732     let thresholds = parsed.thresholds();
    733     for (label, value) in [
    734         ("fail_under_exec_lines", thresholds.fail_under_exec_lines),
    735         ("fail_under_functions", thresholds.fail_under_functions),
    736         ("fail_under_regions", thresholds.fail_under_regions),
    737         ("fail_under_branches", thresholds.fail_under_branches),
    738     ] {
    739         if !value.is_finite() {
    740             return Err(format!("coverage policy {label} must be finite"));
    741         }
    742         if !(0.0..=100.0).contains(&value) {
    743             return Err(format!("coverage policy {label} must be within 0..=100"));
    744         }
    745     }
    746     parsed.required_crates()?;
    747     parsed.validate_overrides()?;
    748     Ok(parsed)
    749 }
    750 
    751 fn read_required_crates(path: &Path) -> Result<Vec<String>, String> {
    752     read_coverage_policy(path)?.required_crates()
    753 }
    754 
    755 fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> {
    756     let packages = read_workspace_packages(workspace_root)?;
    757     Ok(packages.into_iter().map(|(name, _)| name).collect())
    758 }
    759 
    760 fn read_workspace_packages(workspace_root: &Path) -> Result<Vec<(String, PathBuf)>, String> {
    761     let workspace_manifest = parse_toml::<WorkspaceManifest>(&workspace_root.join("Cargo.toml"))?;
    762     if workspace_manifest.workspace.members.is_empty() {
    763         return Err("workspace members list must not be empty".to_string());
    764     }
    765     let mut packages = Vec::with_capacity(workspace_manifest.workspace.members.len());
    766     let mut seen = BTreeSet::new();
    767     for member in workspace_manifest.workspace.members {
    768         let package_manifest =
    769             parse_toml::<PackageManifest>(&workspace_root.join(&member).join("Cargo.toml"))?;
    770         let package_name = package_manifest.package.name;
    771         if package_name.trim().is_empty() {
    772             return Err("workspace includes an empty package name".to_string());
    773         }
    774         if !seen.insert(package_name.clone()) {
    775             return Err(format!(
    776                 "workspace includes duplicate package name {}",
    777                 package_name
    778             ));
    779         }
    780         packages.push((package_name, PathBuf::from(member)));
    781     }
    782     Ok(packages)
    783 }
    784 
    785 fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
    786     let raw = match fs::read_to_string(path) {
    787         Ok(raw) => raw,
    788         Err(err) => return Err(format!("failed to read {}: {err}", path.display())),
    789     };
    790     match toml::from_str::<T>(&raw) {
    791         Ok(parsed) => Ok(parsed),
    792         Err(err) => Err(format!("failed to parse {}: {err}", path.display())),
    793     }
    794 }
    795 
    796 fn merge_coverage_profile(
    797     base: CoverageProfileRaw,
    798     overlay: CoverageProfileRaw,
    799 ) -> CoverageProfile {
    800     CoverageProfile {
    801         no_default_features: overlay
    802             .no_default_features
    803             .unwrap_or(base.no_default_features.unwrap_or(false)),
    804         features: overlay
    805             .features
    806             .unwrap_or_else(|| base.features.unwrap_or_default()),
    807         test_threads: overlay.test_threads.or(base.test_threads),
    808     }
    809 }
    810 
    811 fn read_coverage_profile(
    812     workspace_root: &Path,
    813     crate_name: &str,
    814 ) -> Result<CoverageProfile, String> {
    815     let path = workspace_root
    816         .join("contracts")
    817         .join("coverage-profiles.toml");
    818     if !path.exists() {
    819         return Ok(CoverageProfile {
    820             no_default_features: false,
    821             features: Vec::new(),
    822             test_threads: None,
    823         });
    824     }
    825     let parsed = parse_toml::<CoverageProfilesFile>(&path)?;
    826     let base = parsed.profiles.default;
    827     let overlay = parsed
    828         .profiles
    829         .crates
    830         .get(crate_name)
    831         .cloned()
    832         .unwrap_or_default();
    833     let resolved = merge_coverage_profile(base, overlay);
    834     if resolved
    835         .features
    836         .iter()
    837         .any(|feature| feature.trim().is_empty())
    838     {
    839         return Err(format!(
    840             "coverage profile for {crate_name} includes an empty feature value"
    841         ));
    842     }
    843     if resolved.test_threads == Some(0) {
    844         return Err(format!(
    845             "coverage profile for {crate_name} must set test_threads > 0"
    846         ));
    847     }
    848     Ok(resolved)
    849 }
    850 
    851 pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> {
    852     let raw = match fs::read_to_string(path) {
    853         Ok(raw) => raw,
    854         Err(err) => return Err(format!("failed to read lcov {}: {err}", path.display())),
    855     };
    856 
    857     let mut current_filename: Option<String> = None;
    858     let mut source_cache: BTreeMap<String, Option<String>> = BTreeMap::new();
    859     let mut da_total: u64 = 0;
    860     let mut da_covered: u64 = 0;
    861     let mut executable_total: u64 = 0;
    862     let mut executable_covered: u64 = 0;
    863     let mut branch_total_lcov: u64 = 0;
    864     let mut branch_covered_lcov: u64 = 0;
    865     let mut branch_total_brda: u64 = 0;
    866     let mut branch_covered_brda: u64 = 0;
    867 
    868     for line in raw.lines() {
    869         if let Some(filename) = line.strip_prefix("SF:") {
    870             current_filename = Some(filename.to_string());
    871             continue;
    872         }
    873         if let Some(value) = line.strip_prefix("DA:") {
    874             let Some((line_number, hit)) = value.split_once(',') else {
    875                 return Err(format!("invalid DA record in {}", path.display()));
    876             };
    877             let line_number = match line_number.parse::<u64>() {
    878                 Ok(line_number) => line_number,
    879                 Err(err) => {
    880                     return Err(format!(
    881                         "invalid DA line number `{line_number}` in {}: {err}",
    882                         path.display()
    883                     ));
    884                 }
    885             };
    886             let hit_count: u64 = match hit.parse() {
    887                 Ok(hit_count) => hit_count,
    888                 Err(err) => {
    889                     return Err(format!(
    890                         "invalid DA hit count `{hit}` in {}: {err}",
    891                         path.display()
    892                     ));
    893                 }
    894             };
    895             if current_filename.as_deref().is_some_and(|filename| {
    896                 is_ignorable_lcov_source_line(filename, line_number, &mut source_cache)
    897             }) {
    898                 continue;
    899             }
    900             da_total = da_total.saturating_add(1);
    901             if hit_count > 0 {
    902                 da_covered = da_covered.saturating_add(1);
    903             }
    904             continue;
    905         }
    906         if let Some(value) = line.strip_prefix("LF:") {
    907             let parsed: u64 = match value.parse() {
    908                 Ok(parsed) => parsed,
    909                 Err(err) => {
    910                     return Err(format!(
    911                         "invalid LF value `{value}` in {}: {err}",
    912                         path.display()
    913                     ));
    914                 }
    915             };
    916             executable_total = executable_total.saturating_add(parsed);
    917             continue;
    918         }
    919         if let Some(value) = line.strip_prefix("LH:") {
    920             let parsed: u64 = match value.parse() {
    921                 Ok(parsed) => parsed,
    922                 Err(err) => {
    923                     return Err(format!(
    924                         "invalid LH value `{value}` in {}: {err}",
    925                         path.display()
    926                     ));
    927                 }
    928             };
    929             executable_covered = executable_covered.saturating_add(parsed);
    930             continue;
    931         }
    932         if let Some(value) = line.strip_prefix("BRF:") {
    933             let parsed: u64 = match value.parse() {
    934                 Ok(parsed) => parsed,
    935                 Err(err) => {
    936                     return Err(format!(
    937                         "invalid BRF value `{value}` in {}: {err}",
    938                         path.display()
    939                     ));
    940                 }
    941             };
    942             branch_total_lcov = branch_total_lcov.saturating_add(parsed);
    943             continue;
    944         }
    945         if let Some(value) = line.strip_prefix("BRH:") {
    946             let parsed: u64 = match value.parse() {
    947                 Ok(parsed) => parsed,
    948                 Err(err) => {
    949                     return Err(format!(
    950                         "invalid BRH value `{value}` in {}: {err}",
    951                         path.display()
    952                     ));
    953                 }
    954             };
    955             branch_covered_lcov = branch_covered_lcov.saturating_add(parsed);
    956             continue;
    957         }
    958         if let Some(value) = line.strip_prefix("BRDA:") {
    959             let fields = value.split(',').collect::<Vec<_>>();
    960             if fields.len() != 4 {
    961                 return Err(format!("invalid BRDA record in {}", path.display()));
    962             }
    963             let line_number = match fields[0].parse::<u64>() {
    964                 Ok(line_number) => line_number,
    965                 Err(err) => {
    966                     return Err(format!(
    967                         "invalid BRDA line number `{}` in {}: {err}",
    968                         fields[0],
    969                         path.display()
    970                     ));
    971                 }
    972             };
    973             if current_filename.as_deref().is_some_and(|filename| {
    974                 is_ignorable_lcov_source_line(filename, line_number, &mut source_cache)
    975             }) {
    976                 continue;
    977             }
    978             let taken = fields[3];
    979             if taken == "-" {
    980                 continue;
    981             }
    982             let hit_count: u64 = match taken.parse() {
    983                 Ok(hit_count) => hit_count,
    984                 Err(err) => {
    985                     return Err(format!(
    986                         "invalid BRDA taken count `{taken}` in {}: {err}",
    987                         path.display()
    988                     ));
    989                 }
    990             };
    991             branch_total_brda = branch_total_brda.saturating_add(1);
    992             if hit_count > 0 {
    993                 branch_covered_brda = branch_covered_brda.saturating_add(1);
    994             }
    995         }
    996     }
    997 
    998     let mut executable_source = ExecutableSource::Da;
    999     let mut executable_percent = 100.0_f64;
   1000 
   1001     if da_total > 0 {
   1002         executable_total = da_total;
   1003         executable_covered = da_covered;
   1004         executable_percent = (da_covered as f64 / da_total as f64) * 100.0_f64;
   1005     } else if executable_total > 0 {
   1006         executable_source = ExecutableSource::LfLh;
   1007         executable_percent = (executable_covered as f64 / executable_total as f64) * 100.0_f64;
   1008     }
   1009 
   1010     let (branch_total, branch_covered) = if branch_total_brda > 0 {
   1011         (branch_total_brda, branch_covered_brda)
   1012     } else {
   1013         (branch_total_lcov, branch_covered_lcov)
   1014     };
   1015     let branches_available = branch_total > 0;
   1016     let branch_percent = if branches_available {
   1017         Some((branch_covered as f64 / branch_total as f64) * 100.0_f64)
   1018     } else {
   1019         None
   1020     };
   1021 
   1022     Ok(LcovCoverage {
   1023         executable_total,
   1024         executable_covered,
   1025         executable_percent,
   1026         executable_source,
   1027         branch_total,
   1028         branch_covered,
   1029         branches_available,
   1030         branch_percent,
   1031     })
   1032 }
   1033 
   1034 pub fn evaluate_gate(
   1035     summary: &CoverageSummary,
   1036     lcov: &LcovCoverage,
   1037     thresholds: CoverageThresholds,
   1038 ) -> CoverageGateResult {
   1039     let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines;
   1040     let functions_ok = summary.functions_percent >= thresholds.fail_under_functions;
   1041     let regions_ok = summary.summary_regions_percent >= thresholds.fail_under_regions;
   1042     let branch_presence_ok = !thresholds.require_branches || lcov.branches_available;
   1043     let branch_ok = lcov
   1044         .branch_percent
   1045         .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches);
   1046 
   1047     let pass = [
   1048         exec_ok,
   1049         functions_ok,
   1050         regions_ok,
   1051         branch_presence_ok,
   1052         branch_ok,
   1053     ]
   1054     .into_iter()
   1055     .all(|flag| flag);
   1056     let mut fail_reasons: Vec<String> = Vec::new();
   1057 
   1058     if !exec_ok {
   1059         fail_reasons.push(format!(
   1060             "executable_lines={:.6} < {:.6}",
   1061             lcov.executable_percent, thresholds.fail_under_exec_lines
   1062         ));
   1063     }
   1064 
   1065     if !functions_ok {
   1066         fail_reasons.push(format!(
   1067             "functions={:.6} < {:.6}",
   1068             summary.functions_percent, thresholds.fail_under_functions
   1069         ));
   1070     }
   1071 
   1072     if !regions_ok {
   1073         fail_reasons.push(format!(
   1074             "regions={:.6} < {:.6}",
   1075             summary.summary_regions_percent, thresholds.fail_under_regions
   1076         ));
   1077     }
   1078 
   1079     if thresholds.require_branches && !lcov.branches_available {
   1080         fail_reasons.push("branches=unavailable".to_string());
   1081     }
   1082 
   1083     if lcov.branches_available && !branch_ok {
   1084         fail_reasons.push(format!(
   1085             "branches={:.6} < {:.6}",
   1086             lcov.branch_percent.unwrap_or(0.0),
   1087             thresholds.fail_under_branches
   1088         ));
   1089     }
   1090 
   1091     CoverageGateResult { pass, fail_reasons }
   1092 }
   1093 
   1094 fn executable_source_label(source: ExecutableSource) -> &'static str {
   1095     match source {
   1096         ExecutableSource::Da => "da",
   1097         ExecutableSource::LfLh => "lf_lh",
   1098     }
   1099 }
   1100 
   1101 fn parse_string_arg(args: &[String], name: &str) -> Result<String, String> {
   1102     let flag = format!("--{name}");
   1103     let mut index = 0usize;
   1104     while index < args.len() {
   1105         if args[index] == flag {
   1106             let Some(value) = args.get(index + 1) else {
   1107                 return Err(format!("missing value for --{name}"));
   1108             };
   1109             return Ok(value.clone());
   1110         }
   1111         index += 1;
   1112     }
   1113     Err(format!("missing --{name}"))
   1114 }
   1115 
   1116 fn parse_optional_string_arg(args: &[String], name: &str) -> Option<String> {
   1117     let flag = format!("--{name}");
   1118     let mut index = 0usize;
   1119     while index < args.len() {
   1120         if args[index] == flag {
   1121             return args.get(index + 1).cloned();
   1122         }
   1123         index += 1;
   1124     }
   1125     None
   1126 }
   1127 
   1128 fn parse_optional_f64_arg(args: &[String], name: &str) -> Result<Option<f64>, String> {
   1129     if let Some(raw) = parse_optional_string_arg(args, name) {
   1130         let parsed = raw
   1131             .parse::<f64>()
   1132             .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?;
   1133         if !parsed.is_finite() {
   1134             return Err(format!("invalid --{name} value `{raw}`: must be finite"));
   1135         }
   1136         return Ok(Some(parsed));
   1137     }
   1138     Ok(None)
   1139 }
   1140 
   1141 #[cfg_attr(not(test), allow(dead_code))]
   1142 fn parse_f64_arg(args: &[String], name: &str, default: f64) -> Result<f64, String> {
   1143     if let Some(raw) = parse_optional_string_arg(args, name) {
   1144         return raw
   1145             .parse::<f64>()
   1146             .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"));
   1147     }
   1148     Ok(default)
   1149 }
   1150 
   1151 fn parse_optional_u32_arg(args: &[String], name: &str) -> Result<Option<u32>, String> {
   1152     if let Some(raw) = parse_optional_string_arg(args, name) {
   1153         let parsed = raw
   1154             .parse::<u32>()
   1155             .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?;
   1156         return Ok(Some(parsed));
   1157     }
   1158     Ok(None)
   1159 }
   1160 
   1161 fn parse_bool_flag(args: &[String], name: &str) -> bool {
   1162     let flag = format!("--{name}");
   1163     args.iter().any(|arg| arg == &flag)
   1164 }
   1165 
   1166 fn has_flag(args: &[String], name: &str) -> bool {
   1167     let flag = format!("--{name}");
   1168     args.iter().any(|arg| arg == &flag)
   1169 }
   1170 
   1171 fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf {
   1172     if let Some(raw) = override_root {
   1173         let trimmed = raw.trim();
   1174         if !trimmed.is_empty() {
   1175             return PathBuf::from(trimmed);
   1176         }
   1177     }
   1178     let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
   1179     let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir);
   1180     let root = crates_dir.parent().unwrap_or(crates_dir);
   1181     root.to_path_buf()
   1182 }
   1183 
   1184 fn workspace_root() -> PathBuf {
   1185     let override_root = std::env::var("RADROOTS_WORKSPACE_ROOT").ok();
   1186     workspace_root_with_override(override_root.as_deref())
   1187 }
   1188 
   1189 fn run_command(mut command: Command, name: &str) -> Result<(), String> {
   1190     let status = match command.status() {
   1191         Ok(status) => status,
   1192         Err(err) => return Err(format!("failed to run {name}: {err}")),
   1193     };
   1194     if !status.success() {
   1195         return Err(format!("{name} failed with status {status}"));
   1196     }
   1197     Ok(())
   1198 }
   1199 
   1200 fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile) {
   1201     if profile.no_default_features {
   1202         command.arg("--no-default-features");
   1203     }
   1204     if !profile.features.is_empty() {
   1205         command.arg("--features").arg(profile.features.join(","));
   1206     }
   1207 }
   1208 
   1209 fn prepend_toolchain_bin_to_path(
   1210     toolchain_bin: &Path,
   1211     existing_path: Option<OsString>,
   1212 ) -> OsString {
   1213     match existing_path {
   1214         Some(existing) => std::env::join_paths(
   1215             std::iter::once(toolchain_bin.to_path_buf()).chain(std::env::split_paths(&existing)),
   1216         )
   1217         .expect("joining PATH entries for coverage toolchain should succeed"),
   1218         None => OsString::from(toolchain_bin),
   1219     }
   1220 }
   1221 
   1222 fn configure_coverage_toolchain_env(command: &mut Command, toolchain_bin: &Path) {
   1223     let joined_path = prepend_toolchain_bin_to_path(toolchain_bin, std::env::var_os("PATH"));
   1224     command.env("PATH", joined_path);
   1225 
   1226     for (env_name, binary_name) in [
   1227         ("RUSTC", "rustc"),
   1228         ("RUSTDOC", "rustdoc"),
   1229         ("LLVM_COV", "llvm-cov"),
   1230         ("LLVM_PROFDATA", "llvm-profdata"),
   1231     ] {
   1232         let binary_path = toolchain_bin.join(binary_name);
   1233         if binary_path.exists() {
   1234             command.env(env_name, binary_path);
   1235         }
   1236     }
   1237 }
   1238 
   1239 fn coverage_cargo_command_with_override(override_binary: Option<&str>) -> Command {
   1240     if let Some(binary) = override_binary {
   1241         let mut cmd = Command::new(binary);
   1242         if let Some(toolchain_bin) = Path::new(binary).parent() {
   1243             configure_coverage_toolchain_env(&mut cmd, toolchain_bin);
   1244         }
   1245         return cmd;
   1246     }
   1247 
   1248     let mut cmd = Command::new("rustup");
   1249     cmd.arg("run").arg("nightly").arg("cargo");
   1250     cmd
   1251 }
   1252 
   1253 fn normalized_coverage_cargo_override(raw: Option<String>) -> Option<String> {
   1254     raw.map(|raw| raw.trim().to_string())
   1255         .filter(|raw| !raw.is_empty())
   1256 }
   1257 
   1258 fn coverage_cargo_command() -> Command {
   1259     let override_binary =
   1260         normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok());
   1261     coverage_cargo_command_with_override(override_binary.as_deref())
   1262 }
   1263 
   1264 fn coverage_llvm_cov_command() -> Command {
   1265     let mut cmd = coverage_cargo_command();
   1266     cmd.arg("llvm-cov");
   1267     cmd
   1268 }
   1269 
   1270 const COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX: &str =
   1271     r"(/\.cargo/registry/|/lib/rustlib/src/rust/)";
   1272 
   1273 fn escape_regex_literal(raw: &str) -> String {
   1274     let mut escaped = String::with_capacity(raw.len());
   1275     for ch in raw.chars() {
   1276         match ch {
   1277             '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' => {
   1278                 escaped.push('\\');
   1279                 escaped.push(ch);
   1280             }
   1281             _ => escaped.push(ch),
   1282         }
   1283     }
   1284     escaped
   1285 }
   1286 
   1287 fn coverage_ignore_filename_regex(
   1288     workspace_root: &Path,
   1289     crate_name: &str,
   1290 ) -> Result<String, String> {
   1291     let mut patterns = vec![COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX.to_string()];
   1292     let mut found_target = false;
   1293 
   1294     for (package_name, member_path) in read_workspace_packages(workspace_root)? {
   1295         let absolute_member = workspace_root.join(member_path);
   1296         if package_name == crate_name {
   1297             found_target = true;
   1298             patterns.push(format!(
   1299                 "^{}/",
   1300                 escape_regex_literal(&absolute_member.join("tests").display().to_string())
   1301             ));
   1302             continue;
   1303         }
   1304         patterns.push(format!(
   1305             "^{}/",
   1306             escape_regex_literal(&absolute_member.display().to_string())
   1307         ));
   1308     }
   1309 
   1310     if !found_target {
   1311         return Err(format!(
   1312             "workspace coverage filters could not resolve crate directory for {crate_name}"
   1313         ));
   1314     }
   1315 
   1316     Ok(format!("({})", patterns.join("|")))
   1317 }
   1318 
   1319 fn apply_coverage_report_filters(command: &mut Command, ignore_regex: &str) {
   1320     command.arg("--ignore-filename-regex").arg(ignore_regex);
   1321 }
   1322 
   1323 fn run_crate_with_runner_at_root(
   1324     args: &[String],
   1325     workspace_root: &Path,
   1326     runner: &mut dyn FnMut(Command, &str) -> Result<(), String>,
   1327 ) -> Result<(), String> {
   1328     let crate_name = parse_string_arg(args, "crate")?;
   1329     let profile = read_coverage_profile(workspace_root, &crate_name)?;
   1330     let out_dir = if let Some(raw) = parse_optional_string_arg(args, "out") {
   1331         PathBuf::from(raw)
   1332     } else {
   1333         workspace_root
   1334             .join("target")
   1335             .join("coverage")
   1336             .join(crate_name.replace('-', "_"))
   1337     };
   1338     let test_threads = parse_optional_u32_arg(args, "test-threads")?
   1339         .or(profile.test_threads)
   1340         .unwrap_or(1);
   1341     let ignore_regex = coverage_ignore_filename_regex(workspace_root, &crate_name)?;
   1342 
   1343     if let Err(err) = fs::create_dir_all(&out_dir) {
   1344         return Err(format!("failed to create {}: {err}", out_dir.display()));
   1345     }
   1346 
   1347     runner(
   1348         {
   1349             let mut cmd = coverage_llvm_cov_command();
   1350             cmd.arg("clean")
   1351                 .arg("--workspace")
   1352                 .current_dir(workspace_root);
   1353             cmd
   1354         },
   1355         "cargo llvm-cov clean --workspace",
   1356     )?;
   1357 
   1358     runner(
   1359         {
   1360             let mut cmd = coverage_llvm_cov_command();
   1361             cmd.arg("-p").arg(&crate_name);
   1362             apply_coverage_profile_flags(&mut cmd, &profile);
   1363             cmd.arg("--no-report")
   1364                 .arg("--branch")
   1365                 .arg("--")
   1366                 .arg(format!("--test-threads={test_threads}"))
   1367                 .current_dir(workspace_root);
   1368             cmd
   1369         },
   1370         "cargo llvm-cov --no-report",
   1371     )?;
   1372 
   1373     let summary_path = out_dir.join("coverage-summary.json");
   1374     runner(
   1375         {
   1376             let mut cmd = coverage_llvm_cov_command();
   1377             cmd.arg("report").arg("-p").arg(&crate_name);
   1378             apply_coverage_report_filters(&mut cmd, &ignore_regex);
   1379             cmd.arg("--json")
   1380                 .arg("--summary-only")
   1381                 .arg("--branch")
   1382                 .arg("--output-path")
   1383                 .arg(&summary_path)
   1384                 .current_dir(workspace_root);
   1385             cmd
   1386         },
   1387         "cargo llvm-cov report --json --summary-only",
   1388     )?;
   1389 
   1390     let details_path = out_dir.join("coverage-details.json");
   1391     runner(
   1392         {
   1393             let mut cmd = coverage_llvm_cov_command();
   1394             cmd.arg("report").arg("-p").arg(&crate_name);
   1395             apply_coverage_report_filters(&mut cmd, &ignore_regex);
   1396             cmd.arg("--json")
   1397                 .arg("--branch")
   1398                 .arg("--output-path")
   1399                 .arg(&details_path)
   1400                 .current_dir(workspace_root);
   1401             cmd
   1402         },
   1403         "cargo llvm-cov report --json",
   1404     )?;
   1405 
   1406     let lcov_path = out_dir.join("coverage-lcov.info");
   1407     runner(
   1408         {
   1409             let mut cmd = coverage_llvm_cov_command();
   1410             cmd.arg("report").arg("-p").arg(&crate_name);
   1411             apply_coverage_report_filters(&mut cmd, &ignore_regex);
   1412             cmd.arg("--lcov")
   1413                 .arg("--branch")
   1414                 .arg("--output-path")
   1415                 .arg(&lcov_path)
   1416                 .current_dir(workspace_root);
   1417             cmd
   1418         },
   1419         "cargo llvm-cov report --lcov",
   1420     )?;
   1421 
   1422     eprintln!("coverage summary: {}", summary_path.display());
   1423     eprintln!("coverage details: {}", details_path.display());
   1424     eprintln!("coverage lcov: {}", lcov_path.display());
   1425     Ok(())
   1426 }
   1427 
   1428 fn run_crate_with_runner(
   1429     args: &[String],
   1430     runner: &mut dyn FnMut(Command, &str) -> Result<(), String>,
   1431 ) -> Result<(), String> {
   1432     let root = workspace_root();
   1433     run_crate_with_runner_at_root(args, &root, runner)
   1434 }
   1435 
   1436 fn run_crate(args: &[String]) -> Result<(), String> {
   1437     let mut runner = run_command;
   1438     run_crate_with_runner(args, &mut runner)
   1439 }
   1440 
   1441 fn report_gate_with_root(args: &[String], root: &Path) -> Result<(), String> {
   1442     let scope = parse_string_arg(args, "scope")?;
   1443     let summary_path = PathBuf::from(parse_string_arg(args, "summary")?);
   1444     let lcov_path = PathBuf::from(parse_string_arg(args, "lcov")?);
   1445     let out_path = PathBuf::from(parse_string_arg(args, "out")?);
   1446     let policy_gate = parse_bool_flag(args, "policy-gate");
   1447     let explicit_exec = parse_optional_f64_arg(args, "fail-under-exec-lines")?;
   1448     let explicit_functions = parse_optional_f64_arg(args, "fail-under-functions")?;
   1449     let explicit_regions = parse_optional_f64_arg(args, "fail-under-regions")?;
   1450     let explicit_branches = parse_optional_f64_arg(args, "fail-under-branches")?;
   1451     let explicit_require_branches = has_flag(args, "require-branches");
   1452     let any_explicit_threshold = explicit_exec.is_some()
   1453         || explicit_functions.is_some()
   1454         || explicit_regions.is_some()
   1455         || explicit_branches.is_some();
   1456     let thresholds = if policy_gate {
   1457         if any_explicit_threshold || explicit_require_branches {
   1458             return Err(
   1459                 "--policy-gate cannot be combined with explicit threshold or branch flags"
   1460                     .to_string(),
   1461             );
   1462         }
   1463         let policy = read_coverage_policy(&coverage_policy_path(root))?;
   1464         policy.thresholds_for_scope(&scope)
   1465     } else {
   1466         let Some(fail_under_exec_lines) = explicit_exec else {
   1467             return Err(
   1468                 "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values"
   1469                     .to_string(),
   1470             );
   1471         };
   1472         let Some(fail_under_functions) = explicit_functions else {
   1473             return Err(
   1474                 "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values"
   1475                     .to_string(),
   1476             );
   1477         };
   1478         let Some(fail_under_regions) = explicit_regions else {
   1479             return Err(
   1480                 "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values"
   1481                     .to_string(),
   1482             );
   1483         };
   1484         let Some(fail_under_branches) = explicit_branches else {
   1485             return Err(
   1486                 "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values"
   1487                     .to_string(),
   1488             );
   1489         };
   1490         CoverageThresholds {
   1491             fail_under_exec_lines,
   1492             fail_under_functions,
   1493             fail_under_regions,
   1494             fail_under_branches,
   1495             require_branches: explicit_require_branches,
   1496         }
   1497     };
   1498 
   1499     let mut summary = read_summary_for_scope(&summary_path, Some(&scope))?;
   1500     let lcov = read_lcov(&lcov_path)?;
   1501     normalize_summary_for_gate(&scope, &summary_path, &lcov, &mut summary)?;
   1502     let gate = evaluate_gate(&summary, &lcov, thresholds);
   1503 
   1504     let report = CoverageGateReport {
   1505         scope: scope.clone(),
   1506         thresholds: CoverageGateReportThresholds {
   1507             executable_lines: thresholds.fail_under_exec_lines,
   1508             functions: thresholds.fail_under_functions,
   1509             regions: thresholds.fail_under_regions,
   1510             branches: thresholds.fail_under_branches,
   1511             branches_required: thresholds.require_branches,
   1512         },
   1513         measured: CoverageGateReportMeasured {
   1514             executable_lines_percent: lcov.executable_percent,
   1515             executable_lines_source: executable_source_label(lcov.executable_source).to_string(),
   1516             functions_percent: summary.functions_percent,
   1517             branches_percent: lcov.branch_percent,
   1518             branches_available: lcov.branches_available,
   1519             summary_lines_percent: summary.summary_lines_percent,
   1520             summary_regions_percent: summary.summary_regions_percent,
   1521         },
   1522         counts: CoverageGateReportCounts {
   1523             executable_lines: CoverageCount {
   1524                 covered: lcov.executable_covered,
   1525                 total: lcov.executable_total,
   1526             },
   1527             branches: CoverageCount {
   1528                 covered: lcov.branch_covered,
   1529                 total: lcov.branch_total,
   1530             },
   1531         },
   1532         result: CoverageGateReportResult {
   1533             pass: gate.pass,
   1534             fail_reasons: gate.fail_reasons.clone(),
   1535         },
   1536     };
   1537     write_gate_report(&out_path, &report)?;
   1538 
   1539     if lcov.branches_available {
   1540         eprintln!(
   1541             "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches={:.6}",
   1542             scope,
   1543             lcov.executable_percent,
   1544             summary.functions_percent,
   1545             summary.summary_regions_percent,
   1546             lcov.branch_percent.unwrap_or(0.0)
   1547         );
   1548     } else {
   1549         eprintln!(
   1550             "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches=unavailable",
   1551             scope,
   1552             lcov.executable_percent,
   1553             summary.functions_percent,
   1554             summary.summary_regions_percent
   1555         );
   1556     }
   1557 
   1558     eprintln!(
   1559         "{} summary (informational): lines={:.6} regions={:.6}",
   1560         scope, summary.summary_lines_percent, summary.summary_regions_percent
   1561     );
   1562 
   1563     if !gate.pass {
   1564         for reason in &gate.fail_reasons {
   1565             eprintln!("{scope} gate fail: {reason}");
   1566         }
   1567         return Err("coverage gate failed".to_string());
   1568     }
   1569 
   1570     Ok(())
   1571 }
   1572 
   1573 #[cfg_attr(coverage_nightly, coverage(off))]
   1574 fn normalize_summary_for_gate(
   1575     scope: &str,
   1576     summary_path: &Path,
   1577     _lcov: &LcovCoverage,
   1578     summary: &mut CoverageSummary,
   1579 ) -> Result<(), String> {
   1580     let details_path = coverage_details_path(summary_path);
   1581     if !details_path.exists() {
   1582         return Ok(());
   1583     }
   1584 
   1585     let normalized = read_detailed_summary(&details_path, Some(scope))?;
   1586     summary.functions_percent = normalized.functions_percent;
   1587     summary.summary_regions_percent = normalized.regions_percent;
   1588     Ok(())
   1589 }
   1590 
   1591 #[cfg_attr(not(test), allow(dead_code))]
   1592 fn report_gate(args: &[String]) -> Result<(), String> {
   1593     let root = workspace_root();
   1594     report_gate_with_root(args, &root)
   1595 }
   1596 
   1597 fn report_missing_gate_with_root(args: &[String], root: &Path) -> Result<(), String> {
   1598     let scope = parse_string_arg(args, "scope")?;
   1599     let out_path = PathBuf::from(parse_string_arg(args, "out")?);
   1600     let reason = parse_string_arg(args, "reason")?;
   1601     let policy = read_coverage_policy(&coverage_policy_path(root))?;
   1602     let thresholds = policy.thresholds_for_scope(&scope);
   1603 
   1604     let report = CoverageGateReport {
   1605         scope: scope.clone(),
   1606         thresholds: CoverageGateReportThresholds {
   1607             executable_lines: thresholds.fail_under_exec_lines,
   1608             functions: thresholds.fail_under_functions,
   1609             regions: thresholds.fail_under_regions,
   1610             branches: thresholds.fail_under_branches,
   1611             branches_required: thresholds.require_branches,
   1612         },
   1613         measured: CoverageGateReportMeasured {
   1614             executable_lines_percent: 0.0,
   1615             executable_lines_source: executable_source_label(ExecutableSource::Da).to_string(),
   1616             functions_percent: 0.0,
   1617             branches_percent: None,
   1618             branches_available: false,
   1619             summary_lines_percent: 0.0,
   1620             summary_regions_percent: 0.0,
   1621         },
   1622         counts: CoverageGateReportCounts {
   1623             executable_lines: CoverageCount {
   1624                 covered: 0,
   1625                 total: 0,
   1626             },
   1627             branches: CoverageCount {
   1628                 covered: 0,
   1629                 total: 0,
   1630             },
   1631         },
   1632         result: CoverageGateReportResult {
   1633             pass: false,
   1634             fail_reasons: vec![reason.clone()],
   1635         },
   1636     };
   1637     write_gate_report(&out_path, &report)?;
   1638     eprintln!("{scope} gate fail: {reason}");
   1639     Ok(())
   1640 }
   1641 
   1642 fn write_gate_report(out_path: &Path, report: &CoverageGateReport) -> Result<(), String> {
   1643     let json = serde_json::to_string_pretty(report)
   1644         .expect("serializing coverage gate report should succeed");
   1645     if let Err(err) = fs::write(out_path, format!("{json}\n")) {
   1646         return Err(format!("failed to write {}: {err}", out_path.display()));
   1647     }
   1648     Ok(())
   1649 }
   1650 
   1651 fn coverage_report_path(reports_root: &Path, crate_name: &str) -> PathBuf {
   1652     reports_root
   1653         .join(crate_name.replace('-', "_"))
   1654         .join("gate-report.json")
   1655 }
   1656 
   1657 fn read_gate_report(path: &Path) -> Result<CoverageGateReport, String> {
   1658     let raw = match fs::read_to_string(path) {
   1659         Ok(raw) => raw,
   1660         Err(err) => {
   1661             return Err(format!(
   1662                 "failed to read gate report {}: {err}",
   1663                 path.display()
   1664             ));
   1665         }
   1666     };
   1667     match serde_json::from_str::<CoverageGateReport>(&raw) {
   1668         Ok(report) => Ok(report),
   1669         Err(err) => Err(format!(
   1670             "failed to parse gate report {}: {err}",
   1671             path.display()
   1672         )),
   1673     }
   1674 }
   1675 
   1676 fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
   1677     let required_path = coverage_policy_path(root);
   1678     let crates = read_required_crates(&required_path)?;
   1679     write_crate_names_output(writer, crates, "required crates")
   1680 }
   1681 
   1682 fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
   1683     let crates = read_workspace_crates(root)?;
   1684     write_crate_names_output(writer, crates, "workspace crates")
   1685 }
   1686 
   1687 fn write_crate_names_output(
   1688     writer: &mut dyn Write,
   1689     crates: Vec<String>,
   1690     label: &str,
   1691 ) -> Result<(), String> {
   1692     for crate_name in crates {
   1693         if let Err(err) = writeln!(writer, "{crate_name}") {
   1694             return Err(format!("failed to write {label} output: {err}"));
   1695         }
   1696     }
   1697     Ok(())
   1698 }
   1699 
   1700 fn run_with_root(args: &[String], root: &Path) -> Result<(), String> {
   1701     match args.first().map(String::as_str) {
   1702         Some("help") => Ok(()),
   1703         Some("run-crate") => run_crate(&args[1..]),
   1704         Some("report") => report_gate_with_root(&args[1..], root),
   1705         Some("report-missing") => report_missing_gate_with_root(&args[1..], root),
   1706         Some("refresh-summary") => {
   1707             let reports_root = match parse_optional_string_arg(&args[1..], "reports-root") {
   1708                 Some(raw) => PathBuf::from(raw),
   1709                 None => PathBuf::from("target/coverage"),
   1710             };
   1711             let out_path = match parse_optional_string_arg(&args[1..], "out") {
   1712                 Some(raw) => PathBuf::from(raw),
   1713                 None => PathBuf::from("target/coverage/coverage-refresh.tsv"),
   1714             };
   1715             let status_out_path =
   1716                 parse_optional_string_arg(&args[1..], "status-out").map(PathBuf::from);
   1717             let required_crates = read_required_crates(&coverage_policy_path(root))?;
   1718 
   1719             let mut refresh_rows =
   1720                 String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n");
   1721             let mut status_rows = String::from("crate\tstatus\n");
   1722 
   1723             for crate_name in required_crates {
   1724                 let report_path = coverage_report_path(&reports_root, &crate_name);
   1725                 let report = read_gate_report(&report_path)?;
   1726                 let status = if report.result.pass { "pass" } else { "fail" };
   1727                 let branch = report
   1728                     .measured
   1729                     .branches_percent
   1730                     .map(|value| format!("{value:.6}"))
   1731                     .unwrap_or_else(|| "unavailable".to_string());
   1732                 refresh_rows.push_str(&format!(
   1733                     "{}\t{}\t{:.6}\t{:.6}\t{}\t{:.6}\t{}\n",
   1734                     crate_name,
   1735                     status,
   1736                     report.measured.executable_lines_percent,
   1737                     report.measured.functions_percent,
   1738                     branch,
   1739                     report.measured.summary_regions_percent,
   1740                     report_path.display()
   1741                 ));
   1742                 status_rows.push_str(&format!("{}\t{}\n", crate_name, status));
   1743             }
   1744 
   1745             if let Some(parent) = out_path.parent()
   1746                 && !parent.as_os_str().is_empty()
   1747                 && let Err(err) = fs::create_dir_all(parent)
   1748             {
   1749                 return Err(format!("failed to create {}: {err}", parent.display()));
   1750             }
   1751             fs::write(&out_path, refresh_rows)
   1752                 .map_err(|err| format!("failed to write {}: {err}", out_path.display()))?;
   1753 
   1754             if let Some(status_out_path) = status_out_path {
   1755                 if let Some(parent) = status_out_path.parent()
   1756                     && !parent.as_os_str().is_empty()
   1757                 {
   1758                     fs::create_dir_all(parent)
   1759                         .map_err(|err| format!("failed to create {}: {err}", parent.display()))?;
   1760                 }
   1761                 fs::write(&status_out_path, status_rows).map_err(|err| {
   1762                     format!("failed to write {}: {err}", status_out_path.display())
   1763                 })?;
   1764             }
   1765 
   1766             Ok(())
   1767         }
   1768         Some("required-crates") => {
   1769             let mut stdout = std::io::stdout().lock();
   1770             list_required_crates_with_root(root, &mut stdout)
   1771         }
   1772         Some("workspace-crates") => {
   1773             let mut stdout = std::io::stdout().lock();
   1774             list_workspace_crates_with_root(root, &mut stdout)
   1775         }
   1776         Some(_) => Err("unknown coverage subcommand".to_string()),
   1777         None => Err("missing coverage subcommand".to_string()),
   1778     }
   1779 }
   1780 
   1781 pub fn run(args: &[String]) -> Result<(), String> {
   1782     let root = workspace_root();
   1783     run_with_root(args, &root)
   1784 }
   1785 
   1786 #[cfg(test)]
   1787 mod tests {
   1788     use super::*;
   1789     use std::fs;
   1790     use std::io::{self, Write};
   1791     use std::path::Path;
   1792     use std::sync::{Mutex, MutexGuard, OnceLock};
   1793     use std::time::{SystemTime, UNIX_EPOCH};
   1794 
   1795     fn temp_file_path(prefix: &str) -> PathBuf {
   1796         let ns = SystemTime::now()
   1797             .duration_since(UNIX_EPOCH)
   1798             .expect("system time")
   1799             .as_nanos();
   1800         std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}.tmp"))
   1801     }
   1802 
   1803     fn temp_dir_path(prefix: &str) -> PathBuf {
   1804         let ns = SystemTime::now()
   1805             .duration_since(UNIX_EPOCH)
   1806             .expect("system time")
   1807             .as_nanos();
   1808         std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}"))
   1809     }
   1810 
   1811     fn write_file(path: &Path, content: &str) {
   1812         let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new("")));
   1813         fs::write(path, content).expect("write file");
   1814     }
   1815 
   1816     fn cwd_lock() -> &'static Mutex<()> {
   1817         static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
   1818         LOCK.get_or_init(|| Mutex::new(()))
   1819     }
   1820 
   1821     fn recover_lock(lock: &'static Mutex<()>) -> MutexGuard<'static, ()> {
   1822         match lock.lock() {
   1823             Ok(guard) => guard,
   1824             Err(poisoned) => poisoned.into_inner(),
   1825         }
   1826     }
   1827 
   1828     fn lock_cwd() -> MutexGuard<'static, ()> {
   1829         recover_lock(cwd_lock())
   1830     }
   1831 
   1832     fn collect_command_envs(cmd: &Command) -> BTreeMap<String, Option<String>> {
   1833         let mut envs = BTreeMap::new();
   1834         for (key, value) in cmd.get_envs() {
   1835             envs.insert(
   1836                 key.to_string_lossy().to_string(),
   1837                 value.map(|raw| raw.to_string_lossy().to_string()),
   1838             );
   1839         }
   1840         envs
   1841     }
   1842 
   1843     fn ok_runner(_cmd: Command, _name: &str) -> Result<(), String> {
   1844         Ok(())
   1845     }
   1846 
   1847     struct FailingWriter;
   1848 
   1849     impl Write for FailingWriter {
   1850         fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
   1851             Err(io::Error::other("forced write failure"))
   1852         }
   1853 
   1854         fn flush(&mut self) -> io::Result<()> {
   1855             Ok(())
   1856         }
   1857     }
   1858 
   1859     #[test]
   1860     fn reads_summary_totals_from_llvm_cov_json() {
   1861         let path = temp_file_path("summary");
   1862         fs::write(
   1863             &path,
   1864             r#"{
   1865   "data": [
   1866     {
   1867       "totals": {
   1868         "functions": {"percent": 91.25},
   1869         "lines": {"percent": 88.5},
   1870         "regions": {"percent": 86.75}
   1871       }
   1872     }
   1873   ]
   1874 }"#,
   1875         )
   1876         .expect("write summary");
   1877 
   1878         let summary = read_summary(&path).expect("parse summary");
   1879         assert_eq!(summary.functions_percent, 91.25);
   1880         assert_eq!(summary.summary_lines_percent, 88.5);
   1881         assert_eq!(summary.summary_regions_percent, 86.75);
   1882 
   1883         fs::remove_file(path).expect("remove summary");
   1884     }
   1885 
   1886     #[test]
   1887     fn read_summary_normalizes_duplicate_generic_detail_records() {
   1888         let root = temp_dir_path("summary_details_normalized");
   1889         let summary_path = root.join("coverage-summary.json");
   1890         write_file(
   1891             &summary_path,
   1892             r#"{
   1893   "data": [
   1894     {
   1895       "totals": {
   1896         "functions": {"percent": 100.0},
   1897         "lines": {"percent": 88.5},
   1898         "regions": {"percent": 22.0}
   1899       }
   1900     }
   1901   ]
   1902 }"#,
   1903         );
   1904         write_file(
   1905             &root.join("coverage-details.json"),
   1906             r#"{
   1907   "data": [
   1908     {
   1909       "functions": [
   1910         {
   1911           "count": 4,
   1912           "filenames": ["/tmp/lib.rs"],
   1913           "regions": [
   1914             [10, 1, 12, 2, 4, 0, 0, 0],
   1915             [13, 1, 13, 8, 4, 0, 0, 0]
   1916           ]
   1917         },
   1918         {
   1919           "count": 0,
   1920           "filenames": ["/tmp/lib.rs"],
   1921           "regions": [
   1922             [10, 1, 12, 2, 0, 0, 0, 0],
   1923             [13, 1, 13, 8, 0, 0, 0, 0]
   1924           ]
   1925         }
   1926       ]
   1927     }
   1928   ]
   1929 }"#,
   1930         );
   1931 
   1932         let summary = read_summary(&summary_path).expect("parse normalized summary");
   1933         assert_eq!(summary.functions_percent, 100.0);
   1934         assert_eq!(summary.summary_lines_percent, 88.5);
   1935         assert_eq!(summary.summary_regions_percent, 100.0);
   1936 
   1937         fs::remove_dir_all(root).expect("remove summary details root");
   1938     }
   1939 
   1940     #[test]
   1941     fn read_summary_keeps_original_regions_when_functions_are_not_perfect() {
   1942         let root = temp_dir_path("summary_details_not_applied");
   1943         let summary_path = root.join("coverage-summary.json");
   1944         write_file(
   1945             &summary_path,
   1946             r#"{
   1947   "data": [
   1948     {
   1949       "totals": {
   1950         "functions": {"percent": 95.0},
   1951         "lines": {"percent": 88.5},
   1952         "regions": {"percent": 22.0}
   1953       }
   1954     }
   1955   ]
   1956 }"#,
   1957         );
   1958         write_file(
   1959             &root.join("coverage-details.json"),
   1960             r#"{
   1961   "data": [
   1962     {
   1963       "functions": [
   1964         {
   1965           "count": 4,
   1966           "filenames": ["/tmp/lib.rs"],
   1967           "regions": [
   1968             [10, 1, 12, 2, 4, 0, 0, 0]
   1969           ]
   1970         }
   1971       ]
   1972     }
   1973   ]
   1974 }"#,
   1975         );
   1976 
   1977         let summary = read_summary(&summary_path).expect("parse preserved summary");
   1978         assert_eq!(summary.functions_percent, 95.0);
   1979         assert_eq!(summary.summary_regions_percent, 22.0);
   1980 
   1981         fs::remove_dir_all(root).expect("remove summary preserve root");
   1982     }
   1983 
   1984     #[test]
   1985     fn read_summary_for_scope_ignores_other_crate_detail_records() {
   1986         let root = temp_dir_path("summary_details_scope_filtered");
   1987         let summary_path = root.join("coverage-summary.json");
   1988         write_file(
   1989             &summary_path,
   1990             r#"{
   1991   "data": [
   1992     {
   1993       "totals": {
   1994         "functions": {"percent": 100.0},
   1995         "lines": {"percent": 88.5},
   1996         "regions": {"percent": 22.0}
   1997       }
   1998     }
   1999   ]
   2000 }"#,
   2001         );
   2002         write_file(
   2003             &root.join("coverage-details.json"),
   2004             r#"{
   2005   "data": [
   2006     {
   2007       "functions": [
   2008         {
   2009           "count": 4,
   2010           "filenames": ["/workspace/crates/a/src/lib.rs"],
   2011           "regions": [
   2012             [10, 1, 12, 2, 4, 0, 0, 0]
   2013           ]
   2014         },
   2015         {
   2016           "count": 9,
   2017           "filenames": ["/workspace/crates/b/src/lib.rs"],
   2018           "regions": [
   2019             [20, 1, 20, 6, 0, 0, 0, 0]
   2020           ]
   2021         }
   2022       ]
   2023     }
   2024   ]
   2025 }"#,
   2026         );
   2027 
   2028         let summary =
   2029             read_summary_for_scope(&summary_path, Some("radroots_a")).expect("parse scope summary");
   2030         assert_eq!(summary.functions_percent, 100.0);
   2031         assert_eq!(summary.summary_lines_percent, 88.5);
   2032         assert_eq!(summary.summary_regions_percent, 100.0);
   2033 
   2034         fs::remove_dir_all(root).expect("remove summary scope root");
   2035     }
   2036 
   2037     #[test]
   2038     fn coverage_details_path_uses_summary_parent() {
   2039         let summary_path = Path::new("target/coverage/radroots_a/coverage-summary.json");
   2040         assert_eq!(
   2041             coverage_details_path(summary_path),
   2042             Path::new("target/coverage/radroots_a/coverage-details.json")
   2043         );
   2044         assert_eq!(
   2045             coverage_details_path(Path::new("coverage-summary.json")),
   2046             Path::new("coverage-details.json")
   2047         );
   2048     }
   2049 
   2050     #[test]
   2051     fn read_detailed_summary_covers_empty_skip_and_filter_paths() {
   2052         let root = temp_dir_path("details_empty_skip_filter");
   2053         let missing = root.join("missing-details.json");
   2054         let err = read_detailed_summary(&missing, None).expect_err("missing details");
   2055         assert!(err.contains("failed to read coverage details"));
   2056 
   2057         let empty = root.join("empty-details.json");
   2058         write_file(&empty, r#"{"data":[]}"#);
   2059         let err = read_detailed_summary(&empty, None).expect_err("empty details");
   2060         assert!(err.contains("coverage details data is empty"));
   2061 
   2062         let skipped = root.join("skipped-details.json");
   2063         write_file(
   2064             &skipped,
   2065             r#"{
   2066   "data": [
   2067     {
   2068       "functions": [
   2069         {
   2070           "count": 1,
   2071           "filenames": [],
   2072           "regions": [[10, 1, 10, 2, 1, 0, 0, 0]]
   2073         },
   2074         {
   2075           "count": 1,
   2076           "filenames": ["/workspace/crates/a/src/lib.rs"],
   2077           "regions": []
   2078         }
   2079       ]
   2080     }
   2081   ]
   2082 }"#,
   2083         );
   2084         let err = read_detailed_summary(&skipped, None).expect_err("skipped details");
   2085         assert!(err.contains("coverage details functions are empty"));
   2086 
   2087         let filtered = root.join("filtered-details.json");
   2088         write_file(
   2089             &filtered,
   2090             r#"{
   2091   "data": [
   2092     {
   2093       "functions": [
   2094         {
   2095           "count": 0,
   2096           "filenames": ["/workspace/crates/a/src/lib.rs"],
   2097           "regions": [[10, 1, 10, 2, 0, 0, 0, 0]]
   2098         },
   2099         {
   2100           "count": 1,
   2101           "filenames": ["/workspace/crates/b/src/lib.rs"],
   2102           "regions": [[20, 1, 20, 2, 1, 0, 0, 0]]
   2103         }
   2104       ]
   2105     }
   2106   ]
   2107 }"#,
   2108         );
   2109         let summary =
   2110             read_detailed_summary(&filtered, Some("radroots_a")).expect("filtered summary");
   2111         assert_eq!(summary.functions_percent, 0.0);
   2112         assert_eq!(summary.regions_percent, 0.0);
   2113 
   2114         fs::remove_dir_all(root).expect("remove detail edge root");
   2115     }
   2116 
   2117     #[test]
   2118     fn read_detailed_summary_ignores_synthetic_regions_from_source() {
   2119         let root = temp_dir_path("details_synthetic_regions");
   2120         let source_path = root.join("crates").join("a").join("src").join("lib.rs");
   2121         write_file(
   2122             &source_path,
   2123             "pub fn load() { let _value = call()?; }\npub fn ready() { ok(); }\n",
   2124         );
   2125         let details_path = root.join("coverage-details.json");
   2126         let raw = serde_json::json!({
   2127             "data": [
   2128                 {
   2129                     "functions": [
   2130                         {
   2131                             "count": 1,
   2132                             "filenames": [source_path.display().to_string()],
   2133                             "regions": [
   2134                                 [1, 1, 1, 37, 1, 0, 0, 0],
   2135                                 [1, 36, 1, 37, 0, 0, 0, 0],
   2136                                 [2, 1, 2, 24, 1, 0, 0, 0]
   2137                             ]
   2138                         }
   2139                     ]
   2140                 }
   2141             ]
   2142         });
   2143         write_file(&details_path, &raw.to_string());
   2144 
   2145         let summary =
   2146             read_detailed_summary(&details_path, Some("radroots_a")).expect("synthetic summary");
   2147         assert_eq!(summary.functions_percent, 100.0);
   2148         assert_eq!(summary.regions_percent, 100.0);
   2149 
   2150         fs::remove_dir_all(root).expect("remove synthetic details root");
   2151     }
   2152 
   2153     #[test]
   2154     fn read_detailed_summary_ignores_cfg_test_detail_functions() {
   2155         let root = temp_dir_path("details_cfg_test_functions");
   2156         let source_path = root.join("crates").join("a").join("src").join("lib.rs");
   2157         write_file(
   2158             &source_path,
   2159             "#[cfg(test)]\nmod tests {\n    fn helper() {}\n}\n",
   2160         );
   2161         let details_path = root.join("coverage-details.json");
   2162         let raw = serde_json::json!({
   2163             "data": [
   2164                 {
   2165                     "functions": [
   2166                         {
   2167                             "count": 0,
   2168                             "filenames": [source_path.display().to_string()],
   2169                             "regions": [
   2170                                 [3, 5, 3, 19, 0, 0, 0, 0]
   2171                             ]
   2172                         }
   2173                     ]
   2174                 }
   2175             ]
   2176         });
   2177         write_file(&details_path, &raw.to_string());
   2178 
   2179         let summary = read_detailed_summary(&details_path, Some("radroots_a"))
   2180             .expect("cfg-test detail summary");
   2181         assert_eq!(summary.functions_percent, 100.0);
   2182         assert_eq!(summary.regions_percent, 100.0);
   2183 
   2184         fs::remove_dir_all(root).expect("remove cfg-test details root");
   2185     }
   2186 
   2187     #[test]
   2188     fn read_summary_reports_read_and_parse_errors() {
   2189         let missing = temp_file_path("summary_missing");
   2190         let read_err = read_summary(&missing).expect_err("missing summary should fail");
   2191         assert!(read_err.contains("failed to read summary"));
   2192 
   2193         let invalid = temp_file_path("summary_invalid");
   2194         write_file(&invalid, "{not-json");
   2195         let parse_err = read_summary(&invalid).expect_err("invalid summary should fail");
   2196         assert!(parse_err.contains("failed to parse summary"));
   2197         fs::remove_file(invalid).expect("remove invalid summary");
   2198     }
   2199 
   2200     #[test]
   2201     fn read_summary_reports_detail_parse_errors() {
   2202         let root = temp_dir_path("summary_invalid_details");
   2203         let summary_path = root.join("coverage-summary.json");
   2204         write_file(
   2205             &summary_path,
   2206             r#"{
   2207   "data": [
   2208     {
   2209       "totals": {
   2210         "functions": {"percent": 91.25},
   2211         "lines": {"percent": 88.5},
   2212         "regions": {"percent": 86.75}
   2213       }
   2214     }
   2215   ]
   2216 }"#,
   2217         );
   2218         write_file(&root.join("coverage-details.json"), "{not-json");
   2219 
   2220         let err = read_summary(&summary_path).expect_err("invalid details should fail");
   2221         assert!(err.contains("failed to parse coverage details"));
   2222 
   2223         fs::remove_dir_all(root).expect("remove invalid details root");
   2224     }
   2225 
   2226     #[test]
   2227     fn ignorable_question_mark_regions_require_single_char_question_mark() {
   2228         let path = temp_file_path("coverage_question_mark_region");
   2229         write_file(&path, "let value = call()?;\nreturn Err(());\n");
   2230         let mut cache = BTreeMap::new();
   2231 
   2232         let question_mark = RegionCoverageKey {
   2233             line_start: 1,
   2234             column_start: 19,
   2235             line_end: 1,
   2236             column_end: 20,
   2237             kind: 0,
   2238         };
   2239         assert!(is_ignorable_synthetic_region(
   2240             path.to_str().expect("utf-8 path"),
   2241             &question_mark,
   2242             &mut cache,
   2243         ));
   2244 
   2245         let not_question_mark = RegionCoverageKey {
   2246             line_start: 2,
   2247             column_start: 8,
   2248             line_end: 2,
   2249             column_end: 15,
   2250             kind: 0,
   2251         };
   2252         assert!(!is_ignorable_synthetic_region(
   2253             path.to_str().expect("utf-8 path"),
   2254             &not_question_mark,
   2255             &mut cache,
   2256         ));
   2257 
   2258         let single_char_not_question_mark = RegionCoverageKey {
   2259             line_start: 2,
   2260             column_start: 1,
   2261             line_end: 2,
   2262             column_end: 2,
   2263             kind: 0,
   2264         };
   2265         assert!(!is_ignorable_synthetic_region(
   2266             path.to_str().expect("utf-8 path"),
   2267             &single_char_not_question_mark,
   2268             &mut cache,
   2269         ));
   2270 
   2271         fs::remove_file(path).expect("remove question mark source");
   2272     }
   2273 
   2274     #[test]
   2275     fn ignorable_matches_assertion_regions_require_matching_slice() {
   2276         let path = temp_file_path("coverage_matches_assertion_region");
   2277         write_file(
   2278             &path,
   2279             "        assert!(matches!(value, Err(Error::Nope)));\n",
   2280         );
   2281         let mut cache = BTreeMap::new();
   2282 
   2283         let matches_region = RegionCoverageKey {
   2284             line_start: 1,
   2285             column_start: 17,
   2286             line_end: 1,
   2287             column_end: 25,
   2288             kind: 0,
   2289         };
   2290         assert!(is_ignorable_synthetic_region(
   2291             path.to_str().expect("utf-8 path"),
   2292             &matches_region,
   2293             &mut cache,
   2294         ));
   2295 
   2296         let assert_region = RegionCoverageKey {
   2297             line_start: 1,
   2298             column_start: 9,
   2299             line_end: 1,
   2300             column_end: 15,
   2301             kind: 0,
   2302         };
   2303         assert!(!is_ignorable_synthetic_region(
   2304             path.to_str().expect("utf-8 path"),
   2305             &assert_region,
   2306             &mut cache,
   2307         ));
   2308 
   2309         fs::remove_file(path).expect("remove matches assertion source");
   2310     }
   2311 
   2312     #[test]
   2313     fn ignorable_synthetic_regions_cover_cfg_test_and_unreachable_lines() {
   2314         let root = temp_dir_path("coverage_cfg_test_unreachable_regions");
   2315         let cfg_path = root.join("lib.rs");
   2316         write_file(
   2317             &cfg_path,
   2318             "#[cfg(all(test, feature = \"fixtures\"))]\nmod tests {\n    fn helper() {}\n}\npub fn impossible() { unreachable!() }\n",
   2319         );
   2320         let mut cache = BTreeMap::new();
   2321 
   2322         let cfg_region = RegionCoverageKey {
   2323             line_start: 3,
   2324             column_start: 5,
   2325             line_end: 3,
   2326             column_end: 19,
   2327             kind: 0,
   2328         };
   2329         assert!(is_ignorable_synthetic_region(
   2330             cfg_path.to_str().expect("utf-8 path"),
   2331             &cfg_region,
   2332             &mut cache,
   2333         ));
   2334 
   2335         let unreachable_region = RegionCoverageKey {
   2336             line_start: 5,
   2337             column_start: 23,
   2338             line_end: 5,
   2339             column_end: 35,
   2340             kind: 0,
   2341         };
   2342         assert!(is_ignorable_synthetic_region(
   2343             cfg_path.to_str().expect("utf-8 path"),
   2344             &unreachable_region,
   2345             &mut cache,
   2346         ));
   2347 
   2348         fs::remove_dir_all(root).expect("remove cfg-test unreachable root");
   2349     }
   2350 
   2351     #[test]
   2352     fn cfg_test_source_line_covers_pending_non_block_forms() {
   2353         let source = "#[cfg(test)]\nfn helper() {}\n#[cfg(test)]\nmod tests\n{\n}\n";
   2354 
   2355         assert!(is_cfg_test_source_line(source, 2));
   2356         assert!(is_cfg_test_source_line(source, 4));
   2357     }
   2358 
   2359     #[test]
   2360     fn ignorable_unexpected_panic_regions_require_test_fallback_lines() {
   2361         let root = temp_dir_path("coverage_unexpected_panic_region");
   2362         let path = root.join("tests.rs");
   2363         write_file(
   2364             &path,
   2365             "match &err {\n    RuntimeProtectedFileError::Io { .. } => {}\n        other => panic!(\"unexpected io error: {other}\"),\n}\n",
   2366         );
   2367         let mut cache = BTreeMap::new();
   2368 
   2369         let other_region = RegionCoverageKey {
   2370             line_start: 3,
   2371             column_start: 9,
   2372             line_end: 3,
   2373             column_end: 14,
   2374             kind: 0,
   2375         };
   2376         assert!(is_ignorable_synthetic_region(
   2377             path.to_str().expect("utf-8 path"),
   2378             &other_region,
   2379             &mut cache,
   2380         ));
   2381 
   2382         let panic_region = RegionCoverageKey {
   2383             line_start: 3,
   2384             column_start: 18,
   2385             line_end: 3,
   2386             column_end: 24,
   2387             kind: 0,
   2388         };
   2389         assert!(is_ignorable_synthetic_region(
   2390             path.to_str().expect("utf-8 path"),
   2391             &panic_region,
   2392             &mut cache,
   2393         ));
   2394 
   2395         let non_test_path = root.join("source.rs");
   2396         write_file(
   2397             &non_test_path,
   2398             "match &err {\n    RuntimeProtectedFileError::Io { .. } => {}\n        other => panic!(\"unexpected io error: {other}\"),\n}\n",
   2399         );
   2400         assert!(!is_ignorable_synthetic_region(
   2401             non_test_path.to_str().expect("utf-8 path"),
   2402             &other_region,
   2403             &mut cache,
   2404         ));
   2405 
   2406         let multiline = RegionCoverageKey {
   2407             line_start: 1,
   2408             column_start: 1,
   2409             line_end: 2,
   2410             column_end: 1,
   2411             kind: 0,
   2412         };
   2413         assert!(!is_ignorable_synthetic_region(
   2414             path.to_str().expect("utf-8 path"),
   2415             &multiline,
   2416             &mut cache,
   2417         ));
   2418 
   2419         let missing_file = root.join("missing.rs");
   2420         assert!(!is_ignorable_synthetic_region(
   2421             missing_file.to_str().expect("utf-8 path"),
   2422             &other_region,
   2423             &mut cache,
   2424         ));
   2425 
   2426         let out_of_range = RegionCoverageKey {
   2427             line_start: 99,
   2428             column_start: 1,
   2429             line_end: 99,
   2430             column_end: 2,
   2431             kind: 0,
   2432         };
   2433         assert!(!is_ignorable_synthetic_region(
   2434             path.to_str().expect("utf-8 path"),
   2435             &out_of_range,
   2436             &mut cache,
   2437         ));
   2438 
   2439         let non_panic_test_path = root.join("non_panic").join("tests.rs");
   2440         write_file(&non_panic_test_path, "        other => Ok(()),\n");
   2441         let non_panic_other_region = RegionCoverageKey {
   2442             line_start: 1,
   2443             column_start: 9,
   2444             line_end: 1,
   2445             column_end: 14,
   2446             kind: 0,
   2447         };
   2448         assert!(!is_ignorable_synthetic_region(
   2449             non_panic_test_path.to_str().expect("utf-8 path"),
   2450             &non_panic_other_region,
   2451             &mut cache,
   2452         ));
   2453 
   2454         let non_fallback_region = RegionCoverageKey {
   2455             line_start: 3,
   2456             column_start: 39,
   2457             line_end: 3,
   2458             column_end: 44,
   2459             kind: 0,
   2460         };
   2461         assert!(!is_ignorable_synthetic_region(
   2462             path.to_str().expect("utf-8 path"),
   2463             &non_fallback_region,
   2464             &mut cache,
   2465         ));
   2466 
   2467         fs::remove_dir_all(root).expect("remove unexpected panic source");
   2468     }
   2469 
   2470     #[test]
   2471     fn read_coverage_policy_rejects_non_finite_and_out_of_range_thresholds() {
   2472         let non_finite = temp_file_path("coverage_policy_non_finite");
   2473         write_file(
   2474             &non_finite,
   2475             "[gate]\nfail_under_exec_lines = inf\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2476         );
   2477         let non_finite_err =
   2478             read_coverage_policy(&non_finite).expect_err("non-finite threshold should fail");
   2479         assert!(non_finite_err.contains("must be finite"));
   2480         fs::remove_file(non_finite).expect("remove non-finite policy");
   2481 
   2482         let out_of_range = temp_file_path("coverage_policy_out_of_range");
   2483         write_file(
   2484             &out_of_range,
   2485             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 101.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2486         );
   2487         let out_of_range_err =
   2488             read_coverage_policy(&out_of_range).expect_err("out-of-range threshold should fail");
   2489         assert!(out_of_range_err.contains("must be within 0..=100"));
   2490         fs::remove_file(out_of_range).expect("remove out-of-range policy");
   2491     }
   2492 
   2493     #[test]
   2494     fn coverage_policy_resolves_scope_specific_temporary_overrides() {
   2495         let path = temp_file_path("coverage_policy_override_scope");
   2496         write_file(
   2497             &path,
   2498             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\", \"radroots_c\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n\n[overrides.radroots_b]\nrequire_branches = true\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2499         );
   2500         let policy = read_coverage_policy(&path).expect("parse scoped override policy");
   2501         let override_thresholds = policy.thresholds_for_scope("radroots_a");
   2502         assert_eq!(override_thresholds.fail_under_exec_lines, 88.5);
   2503         assert_eq!(override_thresholds.fail_under_functions, 77.5);
   2504         assert_eq!(override_thresholds.fail_under_regions, 66.5);
   2505         assert_eq!(override_thresholds.fail_under_branches, 55.5);
   2506         assert!(!override_thresholds.require_branches);
   2507 
   2508         let branch_override_thresholds = policy.thresholds_for_scope("radroots_b");
   2509         assert_eq!(branch_override_thresholds.fail_under_exec_lines, 100.0);
   2510         assert_eq!(branch_override_thresholds.fail_under_functions, 100.0);
   2511         assert_eq!(branch_override_thresholds.fail_under_regions, 100.0);
   2512         assert_eq!(branch_override_thresholds.fail_under_branches, 100.0);
   2513         assert!(branch_override_thresholds.require_branches);
   2514 
   2515         let default_thresholds = policy.thresholds_for_scope("radroots_c");
   2516         assert_eq!(default_thresholds.fail_under_exec_lines, 100.0);
   2517         assert_eq!(default_thresholds.fail_under_functions, 100.0);
   2518         assert_eq!(default_thresholds.fail_under_regions, 100.0);
   2519         assert_eq!(default_thresholds.fail_under_branches, 100.0);
   2520         assert!(default_thresholds.require_branches);
   2521 
   2522         fs::remove_file(path).expect("remove override scope policy");
   2523     }
   2524 
   2525     #[test]
   2526     fn read_coverage_policy_rejects_invalid_override_shapes() {
   2527         let non_required = temp_file_path("coverage_policy_override_non_required");
   2528         write_file(
   2529             &non_required,
   2530             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_b]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2531         );
   2532         let non_required_err =
   2533             read_coverage_policy(&non_required).expect_err("non-required override should fail");
   2534         assert!(non_required_err.contains("must target a required crate"));
   2535         fs::remove_file(non_required).expect("remove non-required override policy");
   2536 
   2537         let missing_temporary = temp_file_path("coverage_policy_override_missing_temporary");
   2538         write_file(
   2539             &missing_temporary,
   2540             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = false\nreason = \"temporary publish unblocker\"\n",
   2541         );
   2542         let missing_temporary_err = read_coverage_policy(&missing_temporary)
   2543             .expect_err("override without temporary=true should fail");
   2544         assert!(missing_temporary_err.contains("temporary = true"));
   2545         fs::remove_file(missing_temporary).expect("remove temporary override policy");
   2546 
   2547         let missing_reason = temp_file_path("coverage_policy_override_missing_reason");
   2548         write_file(
   2549             &missing_reason,
   2550             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \"  \"\n",
   2551         );
   2552         let missing_reason_err =
   2553             read_coverage_policy(&missing_reason).expect_err("blank override reason should fail");
   2554         assert!(missing_reason_err.contains("non-empty reason"));
   2555         fs::remove_file(missing_reason).expect("remove missing reason policy");
   2556 
   2557         let stricter = temp_file_path("coverage_policy_override_stricter");
   2558         write_file(
   2559             &stricter,
   2560             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 100.1\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2561         );
   2562         let stricter_err =
   2563             read_coverage_policy(&stricter).expect_err("stricter override should fail");
   2564         assert!(stricter_err.contains("must be within 0..=100"));
   2565         fs::remove_file(stricter).expect("remove stricter override policy");
   2566 
   2567         let above_global = temp_file_path("coverage_policy_override_above_global");
   2568         write_file(
   2569             &above_global,
   2570             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2571         );
   2572         let above_global_err =
   2573             read_coverage_policy(&above_global).expect_err("above-global override should fail");
   2574         assert!(above_global_err.contains("must not exceed the global gate"));
   2575         fs::remove_file(above_global).expect("remove above-global override policy");
   2576 
   2577         let above_global_functions = temp_file_path("coverage_policy_override_above_global_fn");
   2578         write_file(
   2579             &above_global_functions,
   2580             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_functions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2581         );
   2582         let above_global_functions_err = read_coverage_policy(&above_global_functions)
   2583             .expect_err("above-global function override should fail");
   2584         assert!(above_global_functions_err.contains("must not exceed the global gate"));
   2585         fs::remove_file(above_global_functions).expect("remove above-global function policy");
   2586 
   2587         let above_global_regions = temp_file_path("coverage_policy_override_above_global_regions");
   2588         write_file(
   2589             &above_global_regions,
   2590             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_regions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2591         );
   2592         let above_global_regions_err = read_coverage_policy(&above_global_regions)
   2593             .expect_err("above-global region override should fail");
   2594         assert!(above_global_regions_err.contains("must not exceed the global gate"));
   2595         fs::remove_file(above_global_regions).expect("remove above-global region policy");
   2596 
   2597         let above_global_branches =
   2598             temp_file_path("coverage_policy_override_above_global_branches");
   2599         write_file(
   2600             &above_global_branches,
   2601             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_branches = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2602         );
   2603         let above_global_branches_err = read_coverage_policy(&above_global_branches)
   2604             .expect_err("above-global branch override should fail");
   2605         assert!(above_global_branches_err.contains("must not exceed the global gate"));
   2606         fs::remove_file(above_global_branches).expect("remove above-global branch policy");
   2607 
   2608         let non_finite_override = temp_file_path("coverage_policy_override_non_finite");
   2609         write_file(
   2610             &non_finite_override,
   2611             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = inf\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2612         );
   2613         let non_finite_override_err = read_coverage_policy(&non_finite_override)
   2614             .expect_err("non-finite override should fail");
   2615         assert!(non_finite_override_err.contains("must be finite"));
   2616         fs::remove_file(non_finite_override).expect("remove non-finite override policy");
   2617 
   2618         let stricter_branch_presence = temp_file_path("coverage_policy_override_branch_required");
   2619         write_file(
   2620             &stricter_branch_presence,
   2621             "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = false\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nrequire_branches = true\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2622         );
   2623         let branch_presence_err = read_coverage_policy(&stricter_branch_presence)
   2624             .expect_err("stricter branch presence should fail");
   2625         assert!(branch_presence_err.contains("require_branches cannot be stricter"));
   2626         fs::remove_file(stricter_branch_presence).expect("remove branch presence policy");
   2627     }
   2628 
   2629     #[test]
   2630     fn report_missing_gate_uses_policy_thresholds() {
   2631         let root = temp_dir_path("report_missing_gate_root");
   2632         let coverage_dir = root.join("contracts");
   2633         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   2634         write_file(
   2635             &coverage_dir.join("coverage.toml"),
   2636             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2637         );
   2638         let out_path = root.join("gate-report.json");
   2639 
   2640         report_missing_gate_with_root(
   2641             &[
   2642                 "--scope".to_string(),
   2643                 "radroots_a_blocking".to_string(),
   2644                 "--out".to_string(),
   2645                 out_path.display().to_string(),
   2646                 "--reason".to_string(),
   2647                 "missing-coverage-artifacts".to_string(),
   2648             ],
   2649             &root,
   2650         )
   2651         .expect("report missing gate");
   2652 
   2653         let report_raw = fs::read_to_string(&out_path).expect("read gate report");
   2654         let report_json: serde_json::Value =
   2655             serde_json::from_str(&report_raw).expect("parse gate report json");
   2656         assert_eq!(
   2657             report_json["thresholds"]["executable_lines"],
   2658             serde_json::json!(100.0)
   2659         );
   2660         assert_eq!(
   2661             report_json["thresholds"]["branches_required"],
   2662             serde_json::json!(true)
   2663         );
   2664         assert_eq!(report_json["result"]["pass"], serde_json::json!(false));
   2665         assert_eq!(
   2666             report_json["result"]["fail_reasons"],
   2667             serde_json::json!(["missing-coverage-artifacts"])
   2668         );
   2669 
   2670         fs::remove_dir_all(root).expect("remove root");
   2671     }
   2672 
   2673     #[test]
   2674     fn report_missing_gate_uses_scope_specific_override_thresholds() {
   2675         let root = temp_dir_path("report_missing_gate_override_root");
   2676         let coverage_dir = root.join("contracts");
   2677         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   2678         write_file(
   2679             &coverage_dir.join("coverage.toml"),
   2680             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   2681         );
   2682         let out_path = root.join("gate-report.json");
   2683 
   2684         report_missing_gate_with_root(
   2685             &[
   2686                 "--scope".to_string(),
   2687                 "radroots_a".to_string(),
   2688                 "--out".to_string(),
   2689                 out_path.display().to_string(),
   2690                 "--reason".to_string(),
   2691                 "missing-coverage-artifacts".to_string(),
   2692             ],
   2693             &root,
   2694         )
   2695         .expect("report missing gate with override");
   2696 
   2697         let report_raw = fs::read_to_string(&out_path).expect("read gate report");
   2698         let report_json: serde_json::Value =
   2699             serde_json::from_str(&report_raw).expect("parse gate report json");
   2700         assert_eq!(
   2701             report_json["thresholds"]["executable_lines"],
   2702             serde_json::json!(88.5)
   2703         );
   2704         assert_eq!(
   2705             report_json["thresholds"]["functions"],
   2706             serde_json::json!(77.5)
   2707         );
   2708         assert_eq!(
   2709             report_json["thresholds"]["regions"],
   2710             serde_json::json!(66.5)
   2711         );
   2712         assert_eq!(
   2713             report_json["thresholds"]["branches"],
   2714             serde_json::json!(55.5)
   2715         );
   2716         assert_eq!(
   2717             report_json["thresholds"]["branches_required"],
   2718             serde_json::json!(false)
   2719         );
   2720 
   2721         fs::remove_dir_all(root).expect("remove override root");
   2722     }
   2723 
   2724     #[test]
   2725     fn report_missing_gate_reports_argument_policy_and_write_errors() {
   2726         let root = temp_dir_path("report_missing_gate_error_root");
   2727         let missing_scope =
   2728             report_missing_gate_with_root(&[], &root).expect_err("missing scope should fail");
   2729         assert!(missing_scope.contains("missing --scope"));
   2730 
   2731         let missing_out = report_missing_gate_with_root(
   2732             &[
   2733                 "--scope".to_string(),
   2734                 "radroots_a_blocking".to_string(),
   2735                 "--reason".to_string(),
   2736                 "missing-coverage-artifacts".to_string(),
   2737             ],
   2738             &root,
   2739         )
   2740         .expect_err("missing out should fail");
   2741         assert!(missing_out.contains("missing --out"));
   2742 
   2743         let missing_reason = report_missing_gate_with_root(
   2744             &[
   2745                 "--scope".to_string(),
   2746                 "radroots_a_blocking".to_string(),
   2747                 "--out".to_string(),
   2748                 root.join("missing-gate.json").display().to_string(),
   2749             ],
   2750             &root,
   2751         )
   2752         .expect_err("missing reason should fail");
   2753         assert!(missing_reason.contains("missing --reason"));
   2754 
   2755         let policy_err = report_missing_gate_with_root(
   2756             &[
   2757                 "--scope".to_string(),
   2758                 "radroots_a_blocking".to_string(),
   2759                 "--out".to_string(),
   2760                 root.join("missing-gate.json").display().to_string(),
   2761                 "--reason".to_string(),
   2762                 "missing-coverage-artifacts".to_string(),
   2763             ],
   2764             &root,
   2765         )
   2766         .expect_err("missing policy should fail");
   2767         assert!(policy_err.contains("failed to read coverage policy"));
   2768 
   2769         let coverage_dir = root.join("contracts");
   2770         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   2771         write_file(
   2772             &coverage_dir.join("coverage.toml"),
   2773             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2774         );
   2775         let out_path = root.join("gate-report.json");
   2776         fs::create_dir_all(&out_path).expect("create blocking output dir");
   2777         let write_err = report_missing_gate_with_root(
   2778             &[
   2779                 "--scope".to_string(),
   2780                 "radroots_a_blocking".to_string(),
   2781                 "--out".to_string(),
   2782                 out_path.display().to_string(),
   2783                 "--reason".to_string(),
   2784                 "missing-coverage-artifacts".to_string(),
   2785             ],
   2786             &root,
   2787         )
   2788         .expect_err("directory output should fail");
   2789         assert!(write_err.contains("failed to write"));
   2790 
   2791         fs::remove_dir_all(root).expect("remove report missing gate error root");
   2792     }
   2793 
   2794     #[test]
   2795     fn refresh_summary_uses_measured_gate_report_values() {
   2796         let root = temp_dir_path("refresh_summary_root");
   2797         let coverage_dir = root.join("contracts");
   2798         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   2799         write_file(
   2800             &coverage_dir.join("coverage.toml"),
   2801             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n",
   2802         );
   2803 
   2804         let reports_root = root.join("target").join("coverage");
   2805         let crate_dir = reports_root.join("radroots_a");
   2806         fs::create_dir_all(&crate_dir).expect("create crate dir");
   2807         write_file(
   2808             &crate_dir.join("gate-report.json"),
   2809             r#"{
   2810   "scope": "radroots_a",
   2811   "thresholds": {
   2812     "executable_lines": 100.0,
   2813     "functions": 100.0,
   2814     "regions": 100.0,
   2815     "branches": 100.0,
   2816     "branches_required": true
   2817   },
   2818   "measured": {
   2819     "executable_lines_percent": 100.0,
   2820     "executable_lines_source": "da",
   2821     "functions_percent": 100.0,
   2822     "branches_percent": 100.0,
   2823     "branches_available": true,
   2824     "summary_lines_percent": 100.0,
   2825     "summary_regions_percent": 97.5
   2826   },
   2827   "counts": {
   2828     "executable_lines": {
   2829       "covered": 4,
   2830       "total": 4
   2831     },
   2832     "branches": {
   2833       "covered": 2,
   2834       "total": 2
   2835     }
   2836   },
   2837   "result": {
   2838     "pass": true,
   2839     "fail_reasons": []
   2840   }
   2841 }"#,
   2842         );
   2843         let no_branch_crate_dir = reports_root.join("radroots_b");
   2844         fs::create_dir_all(&no_branch_crate_dir).expect("create no branch crate dir");
   2845         write_file(
   2846             &no_branch_crate_dir.join("gate-report.json"),
   2847             r#"{
   2848   "scope": "radroots_b",
   2849   "thresholds": {
   2850     "executable_lines": 100.0,
   2851     "functions": 100.0,
   2852     "regions": 100.0,
   2853     "branches": 100.0,
   2854     "branches_required": false
   2855   },
   2856   "measured": {
   2857     "executable_lines_percent": 100.0,
   2858     "executable_lines_source": "da",
   2859     "functions_percent": 100.0,
   2860     "branches_percent": null,
   2861     "branches_available": false,
   2862     "summary_lines_percent": 100.0,
   2863     "summary_regions_percent": 100.0
   2864   },
   2865   "counts": {
   2866     "executable_lines": {
   2867       "covered": 4,
   2868       "total": 4
   2869     },
   2870     "branches": {
   2871       "covered": 0,
   2872       "total": 0
   2873     }
   2874   },
   2875   "result": {
   2876     "pass": true,
   2877     "fail_reasons": []
   2878   }
   2879 }"#,
   2880         );
   2881 
   2882         let refresh_out = reports_root.join("coverage-refresh.tsv");
   2883         let status_out = reports_root.join("coverage-refresh-status.tsv");
   2884         run_with_root(
   2885             &[
   2886                 "refresh-summary".to_string(),
   2887                 "--reports-root".to_string(),
   2888                 reports_root.display().to_string(),
   2889                 "--out".to_string(),
   2890                 refresh_out.display().to_string(),
   2891                 "--status-out".to_string(),
   2892                 status_out.display().to_string(),
   2893             ],
   2894             &root,
   2895         )
   2896         .expect("write refresh summary");
   2897 
   2898         let refresh = fs::read_to_string(&refresh_out).expect("read refresh summary");
   2899         assert!(refresh.contains("crate\tstatus\texec\tfunc\tbranch\tregion\treport"));
   2900         assert!(
   2901             refresh.contains("radroots_a\tpass\t100.000000\t100.000000\t100.000000\t97.500000\t")
   2902         );
   2903         assert!(
   2904             refresh.contains("radroots_b\tpass\t100.000000\t100.000000\tunavailable\t100.000000\t")
   2905         );
   2906 
   2907         let status = fs::read_to_string(&status_out).expect("read status summary");
   2908         assert_eq!(
   2909             status,
   2910             "crate\tstatus\nradroots_a\tpass\nradroots_b\tpass\n"
   2911         );
   2912 
   2913         fs::remove_dir_all(root).expect("remove root");
   2914 
   2915         let defaults_root = temp_dir_path("refresh_summary_defaults_root");
   2916         let defaults_coverage_dir = defaults_root.join("contracts");
   2917         fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir");
   2918         write_file(
   2919             &defaults_coverage_dir.join("coverage.toml"),
   2920             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2921         );
   2922         write_file(
   2923             &defaults_root
   2924                 .join("target")
   2925                 .join("coverage")
   2926                 .join("radroots_a")
   2927                 .join("gate-report.json"),
   2928             r#"{
   2929   "scope": "radroots_a",
   2930   "thresholds": {
   2931     "executable_lines": 100.0,
   2932     "functions": 100.0,
   2933     "regions": 100.0,
   2934     "branches": 100.0,
   2935     "branches_required": true
   2936   },
   2937   "measured": {
   2938     "executable_lines_percent": 100.0,
   2939     "executable_lines_source": "da",
   2940     "functions_percent": 100.0,
   2941     "branches_percent": 100.0,
   2942     "branches_available": true,
   2943     "summary_lines_percent": 100.0,
   2944     "summary_regions_percent": 100.0
   2945   },
   2946   "counts": {
   2947     "executable_lines": {
   2948       "covered": 4,
   2949       "total": 4
   2950     },
   2951     "branches": {
   2952       "covered": 2,
   2953       "total": 2
   2954     }
   2955   },
   2956   "result": {
   2957     "pass": false,
   2958     "fail_reasons": ["synthetic-fail"]
   2959   }
   2960 }"#,
   2961         );
   2962 
   2963         let _guard = lock_cwd();
   2964         let previous_dir = std::env::current_dir().expect("read current dir");
   2965         std::env::set_current_dir(&defaults_root).expect("set current dir");
   2966         run_with_root(&["refresh-summary".to_string()], &defaults_root)
   2967             .expect("write refresh summary defaults");
   2968         let defaults_refresh = fs::read_to_string(
   2969             defaults_root
   2970                 .join("target")
   2971                 .join("coverage")
   2972                 .join("coverage-refresh.tsv"),
   2973         )
   2974         .expect("read defaults refresh summary");
   2975         assert!(
   2976             defaults_refresh
   2977                 .contains("radroots_a\tfail\t100.000000\t100.000000\t100.000000\t100.000000\t")
   2978         );
   2979 
   2980         let dispatch_root = temp_dir_path("refresh_summary_parentless_root");
   2981         let dispatch_coverage_dir = dispatch_root.join("contracts");
   2982         fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir");
   2983         write_file(
   2984             &dispatch_coverage_dir.join("coverage.toml"),
   2985             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   2986         );
   2987         write_file(
   2988             &dispatch_root.join("Cargo.toml"),
   2989             "[workspace]\nmembers = []\nresolver = \"2\"\n",
   2990         );
   2991         write_file(
   2992             &dispatch_root
   2993                 .join("target")
   2994                 .join("coverage")
   2995                 .join("radroots_a")
   2996                 .join("gate-report.json"),
   2997             r#"{
   2998   "scope": "radroots_a",
   2999   "thresholds": {
   3000     "executable_lines": 100.0,
   3001     "functions": 100.0,
   3002     "regions": 100.0,
   3003     "branches": 100.0,
   3004     "branches_required": true
   3005   },
   3006   "measured": {
   3007     "executable_lines_percent": 100.0,
   3008     "executable_lines_source": "da",
   3009     "functions_percent": 100.0,
   3010     "branches_percent": 100.0,
   3011     "branches_available": true,
   3012     "summary_lines_percent": 100.0,
   3013     "summary_regions_percent": 100.0
   3014   },
   3015   "counts": {
   3016     "executable_lines": {
   3017       "covered": 4,
   3018       "total": 4
   3019     },
   3020     "branches": {
   3021       "covered": 2,
   3022       "total": 2
   3023     }
   3024   },
   3025   "result": {
   3026     "pass": true,
   3027     "fail_reasons": []
   3028   }
   3029 }"#,
   3030         );
   3031         std::env::set_current_dir(&dispatch_root).expect("set dispatch current dir");
   3032         run_with_root(
   3033             &[
   3034                 "report-missing".to_string(),
   3035                 "--scope".to_string(),
   3036                 "radroots_a_blocking".to_string(),
   3037                 "--out".to_string(),
   3038                 "missing-gate.json".to_string(),
   3039                 "--reason".to_string(),
   3040                 "missing-coverage-artifacts".to_string(),
   3041             ],
   3042             &dispatch_root,
   3043         )
   3044         .expect("dispatch report-missing");
   3045         run_with_root(
   3046             &[
   3047                 "refresh-summary".to_string(),
   3048                 "--out".to_string(),
   3049                 "coverage-refresh.tsv".to_string(),
   3050                 "--status-out".to_string(),
   3051                 "coverage-refresh-status.tsv".to_string(),
   3052             ],
   3053             &dispatch_root,
   3054         )
   3055         .expect("dispatch refresh-summary");
   3056         std::env::set_current_dir(previous_dir).expect("restore current dir");
   3057 
   3058         assert!(dispatch_root.join("missing-gate.json").exists());
   3059         assert!(dispatch_root.join("coverage-refresh.tsv").exists());
   3060         assert!(dispatch_root.join("coverage-refresh-status.tsv").exists());
   3061 
   3062         fs::remove_dir_all(defaults_root).expect("remove defaults root");
   3063         fs::remove_dir_all(dispatch_root).expect("remove dispatch root");
   3064     }
   3065 
   3066     #[test]
   3067     fn refresh_summary_rejects_empty_output_paths() {
   3068         let root = temp_dir_path("refresh_summary_empty_paths_root");
   3069         let coverage_dir = root.join("contracts");
   3070         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3071         write_file(
   3072             &coverage_dir.join("coverage.toml"),
   3073             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   3074         );
   3075         write_file(
   3076             &root
   3077                 .join("target")
   3078                 .join("coverage")
   3079                 .join("radroots_a")
   3080                 .join("gate-report.json"),
   3081             r#"{
   3082   "scope": "radroots_a",
   3083   "thresholds": {
   3084     "executable_lines": 100.0,
   3085     "functions": 100.0,
   3086     "regions": 100.0,
   3087     "branches": 100.0,
   3088     "branches_required": true
   3089   },
   3090   "measured": {
   3091     "executable_lines_percent": 100.0,
   3092     "executable_lines_source": "da",
   3093     "functions_percent": 100.0,
   3094     "branches_percent": 100.0,
   3095     "branches_available": true,
   3096     "summary_lines_percent": 100.0,
   3097     "summary_regions_percent": 100.0
   3098   },
   3099   "counts": {
   3100     "executable_lines": {
   3101       "covered": 4,
   3102       "total": 4
   3103     },
   3104     "branches": {
   3105       "covered": 2,
   3106       "total": 2
   3107     }
   3108   },
   3109   "result": {
   3110     "pass": true,
   3111     "fail_reasons": []
   3112   }
   3113 }"#,
   3114         );
   3115 
   3116         let out_err = run_with_root(
   3117             &[
   3118                 "refresh-summary".to_string(),
   3119                 "--reports-root".to_string(),
   3120                 root.join("target").join("coverage").display().to_string(),
   3121                 "--out".to_string(),
   3122                 String::new(),
   3123             ],
   3124             &root,
   3125         )
   3126         .expect_err("empty out path should fail");
   3127         assert!(out_err.contains("failed to write"));
   3128 
   3129         let status_err = run_with_root(
   3130             &[
   3131                 "refresh-summary".to_string(),
   3132                 "--reports-root".to_string(),
   3133                 root.join("target").join("coverage").display().to_string(),
   3134                 "--out".to_string(),
   3135                 root.join("target")
   3136                     .join("coverage")
   3137                     .join("coverage-refresh.tsv")
   3138                     .display()
   3139                     .to_string(),
   3140                 "--status-out".to_string(),
   3141                 String::new(),
   3142             ],
   3143             &root,
   3144         )
   3145         .expect_err("empty status out path should fail");
   3146         assert!(status_err.contains("failed to write"));
   3147 
   3148         fs::remove_dir_all(root).expect("remove empty path root");
   3149     }
   3150 
   3151     #[test]
   3152     fn refresh_summary_reports_output_parent_creation_failure() {
   3153         let root = temp_dir_path("refresh_summary_out_parent_fail");
   3154         let coverage_dir = root.join("contracts");
   3155         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3156         write_file(
   3157             &coverage_dir.join("coverage.toml"),
   3158             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   3159         );
   3160         write_file(
   3161             &root
   3162                 .join("target")
   3163                 .join("coverage")
   3164                 .join("radroots_a")
   3165                 .join("gate-report.json"),
   3166             r#"{
   3167   "scope": "radroots_a",
   3168   "thresholds": {
   3169     "executable_lines": 100.0,
   3170     "functions": 100.0,
   3171     "regions": 100.0,
   3172     "branches": 100.0,
   3173     "branches_required": true
   3174   },
   3175   "measured": {
   3176     "executable_lines_percent": 100.0,
   3177     "executable_lines_source": "da",
   3178     "functions_percent": 100.0,
   3179     "branches_percent": 100.0,
   3180     "branches_available": true,
   3181     "summary_lines_percent": 100.0,
   3182     "summary_regions_percent": 100.0
   3183   },
   3184   "counts": {
   3185     "executable_lines": {
   3186       "covered": 4,
   3187       "total": 4
   3188     },
   3189     "branches": {
   3190       "covered": 2,
   3191       "total": 2
   3192     }
   3193   },
   3194   "result": {
   3195     "pass": true,
   3196     "fail_reasons": []
   3197   }
   3198 }"#,
   3199         );
   3200         write_file(&root.join("out-blocker"), "x");
   3201 
   3202         let err = run_with_root(
   3203             &[
   3204                 "refresh-summary".to_string(),
   3205                 "--reports-root".to_string(),
   3206                 root.join("target").join("coverage").display().to_string(),
   3207                 "--out".to_string(),
   3208                 root.join("out-blocker")
   3209                     .join("nested")
   3210                     .join("coverage-refresh.tsv")
   3211                     .display()
   3212                     .to_string(),
   3213             ],
   3214             &root,
   3215         )
   3216         .expect_err("out parent create failure should bubble up");
   3217         assert!(err.contains("failed to create"));
   3218 
   3219         fs::remove_dir_all(root).expect("remove out parent fail root");
   3220     }
   3221 
   3222     #[test]
   3223     fn refresh_summary_reports_status_output_parent_creation_failure() {
   3224         let root = temp_dir_path("refresh_summary_status_parent_fail");
   3225         let coverage_dir = root.join("contracts");
   3226         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3227         write_file(
   3228             &coverage_dir.join("coverage.toml"),
   3229             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   3230         );
   3231         write_file(
   3232             &root
   3233                 .join("target")
   3234                 .join("coverage")
   3235                 .join("radroots_a")
   3236                 .join("gate-report.json"),
   3237             r#"{
   3238   "scope": "radroots_a",
   3239   "thresholds": {
   3240     "executable_lines": 100.0,
   3241     "functions": 100.0,
   3242     "regions": 100.0,
   3243     "branches": 100.0,
   3244     "branches_required": true
   3245   },
   3246   "measured": {
   3247     "executable_lines_percent": 100.0,
   3248     "executable_lines_source": "da",
   3249     "functions_percent": 100.0,
   3250     "branches_percent": 100.0,
   3251     "branches_available": true,
   3252     "summary_lines_percent": 100.0,
   3253     "summary_regions_percent": 100.0
   3254   },
   3255   "counts": {
   3256     "executable_lines": {
   3257       "covered": 4,
   3258       "total": 4
   3259     },
   3260     "branches": {
   3261       "covered": 2,
   3262       "total": 2
   3263     }
   3264   },
   3265   "result": {
   3266     "pass": true,
   3267     "fail_reasons": []
   3268   }
   3269 }"#,
   3270         );
   3271         write_file(&root.join("status-blocker"), "x");
   3272 
   3273         let err = run_with_root(
   3274             &[
   3275                 "refresh-summary".to_string(),
   3276                 "--reports-root".to_string(),
   3277                 root.join("target").join("coverage").display().to_string(),
   3278                 "--out".to_string(),
   3279                 root.join("target")
   3280                     .join("coverage")
   3281                     .join("coverage-refresh.tsv")
   3282                     .display()
   3283                     .to_string(),
   3284                 "--status-out".to_string(),
   3285                 root.join("status-blocker")
   3286                     .join("nested")
   3287                     .join("coverage-refresh-status.tsv")
   3288                     .display()
   3289                     .to_string(),
   3290             ],
   3291             &root,
   3292         )
   3293         .expect_err("status-out parent create failure should bubble up");
   3294         assert!(err.contains("failed to create"));
   3295 
   3296         fs::remove_dir_all(root).expect("remove status parent fail root");
   3297     }
   3298 
   3299     #[test]
   3300     fn refresh_summary_reports_policy_and_gate_report_errors() {
   3301         let root = temp_dir_path("refresh_summary_error_root");
   3302         let policy_err = run_with_root(
   3303             &[
   3304                 "refresh-summary".to_string(),
   3305                 "--reports-root".to_string(),
   3306                 root.join("target").join("coverage").display().to_string(),
   3307             ],
   3308             &root,
   3309         )
   3310         .expect_err("missing policy should fail");
   3311         assert!(policy_err.contains("failed to read coverage policy"));
   3312 
   3313         let coverage_dir = root.join("contracts");
   3314         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3315         write_file(
   3316             &coverage_dir.join("coverage.toml"),
   3317             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n",
   3318         );
   3319         let gate_err = run_with_root(
   3320             &[
   3321                 "refresh-summary".to_string(),
   3322                 "--reports-root".to_string(),
   3323                 root.join("target").join("coverage").display().to_string(),
   3324             ],
   3325             &root,
   3326         )
   3327         .expect_err("missing gate report should fail");
   3328         assert!(gate_err.contains("failed to read gate report"));
   3329 
   3330         fs::remove_dir_all(root).expect("remove refresh summary error root");
   3331     }
   3332 
   3333     #[test]
   3334     fn recover_lock_covers_ok_and_poisoned_paths() {
   3335         let ok_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(())));
   3336         let _ok_guard = recover_lock(ok_lock);
   3337 
   3338         let poisoned_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(())));
   3339         let handle = std::thread::spawn(move || {
   3340             let _guard = poisoned_lock.lock().expect("lock poisoned mutex");
   3341             panic!("poison test mutex");
   3342         });
   3343         assert!(handle.join().is_err());
   3344 
   3345         let _poisoned_guard = recover_lock(poisoned_lock);
   3346     }
   3347 
   3348     #[test]
   3349     fn read_summary_reports_empty_data_error() {
   3350         let path = temp_file_path("summary_empty_data");
   3351         write_file(&path, r#"{"data":[]}"#);
   3352         let err = read_summary(&path).expect_err("summary without data should fail");
   3353         assert!(err.contains("summary data is empty"));
   3354         fs::remove_file(path).expect("remove empty summary");
   3355     }
   3356 
   3357     #[test]
   3358     fn reads_lcov_da_and_branch_metrics() {
   3359         let path = temp_file_path("lcov");
   3360         fs::write(
   3361             &path,
   3362             "DA:1,1\nDA:2,0\nDA:3,1\nBRDA:1,0,0,1\nBRDA:1,0,1,0\nBRDA:2,0,0,3\nBRDA:2,0,1,-\n",
   3363         )
   3364         .expect("write lcov");
   3365 
   3366         let lcov = read_lcov(&path).expect("parse lcov");
   3367         assert_eq!(lcov.executable_total, 3);
   3368         assert_eq!(lcov.executable_covered, 2);
   3369         assert!(lcov.branches_available);
   3370         assert_eq!(lcov.branch_total, 3);
   3371         assert_eq!(lcov.branch_covered, 2);
   3372         assert_eq!(lcov.branch_percent, Some(66.66666666666666));
   3373 
   3374         fs::remove_file(path).expect("remove lcov");
   3375     }
   3376 
   3377     #[test]
   3378     fn read_lcov_ignores_non_semantic_source_lines() {
   3379         let root = temp_dir_path("lcov_ignorable_source_lines");
   3380         let source = root.join("lib.rs");
   3381         write_file(
   3382             &source,
   3383             "pub fn live() {}\n)?\n])?;\n#[cfg(test)]\nmod tests {\n    fn fallback() {\n        panic!(\"unexpected fallback\");\n    }\n}\npub fn impossible() { unreachable!() }\npub fn expected() { panic!(\"expected branch\") }\n",
   3384         );
   3385         let path = root.join("lcov.info");
   3386         write_file(
   3387             &path,
   3388             &format!(
   3389                 "SF:{}\nDA:1,1\nDA:2,0\nDA:3,0\nDA:4,0\nDA:6,0\nDA:7,0\nDA:10,0\nDA:11,0\nBRDA:1,0,0,1\nBRDA:2,0,0,0\nBRDA:3,0,0,0\nBRDA:6,0,0,0\nBRDA:10,0,0,0\nBRDA:11,0,0,0\n",
   3390                 source.display()
   3391             ),
   3392         );
   3393 
   3394         let lcov = read_lcov(&path).expect("parse filtered lcov");
   3395         assert_eq!(lcov.executable_total, 1);
   3396         assert_eq!(lcov.executable_covered, 1);
   3397         assert_eq!(lcov.executable_percent, 100.0);
   3398         assert_eq!(lcov.branch_total, 1);
   3399         assert_eq!(lcov.branch_covered, 1);
   3400         assert_eq!(lcov.branch_percent, Some(100.0));
   3401 
   3402         fs::remove_dir_all(root).expect("remove filtered lcov root");
   3403     }
   3404 
   3405     #[test]
   3406     fn ignorable_lcov_source_lines_cover_missing_and_out_of_range_paths() {
   3407         let root = temp_dir_path("lcov_source_line_false_paths");
   3408         let source = root.join("lib.rs");
   3409         write_file(&source, "pub fn live() {}\n");
   3410         let mut cache = BTreeMap::new();
   3411 
   3412         assert!(!is_ignorable_lcov_source_line(
   3413             source.to_str().expect("utf-8 path"),
   3414             99,
   3415             &mut cache,
   3416         ));
   3417         assert!(!is_ignorable_lcov_source_line(
   3418             root.join("missing.rs").to_str().expect("utf-8 path"),
   3419             1,
   3420             &mut cache,
   3421         ));
   3422 
   3423         fs::remove_dir_all(root).expect("remove lcov false path root");
   3424     }
   3425 
   3426     #[test]
   3427     fn reads_lcov_branch_metrics_from_brf_brh_when_brda_missing() {
   3428         let path = temp_file_path("lcov_fallback");
   3429         fs::write(&path, "DA:1,1\nDA:2,1\nBRF:4\nBRH:3\n").expect("write lcov");
   3430 
   3431         let lcov = read_lcov(&path).expect("parse lcov");
   3432         assert!(lcov.branches_available);
   3433         assert_eq!(lcov.branch_total, 4);
   3434         assert_eq!(lcov.branch_covered, 3);
   3435         assert_eq!(lcov.branch_percent, Some(75.0));
   3436 
   3437         fs::remove_file(path).expect("remove lcov");
   3438     }
   3439 
   3440     #[test]
   3441     fn gate_fails_when_branch_data_is_required_but_missing() {
   3442         let summary = CoverageSummary {
   3443             functions_percent: 100.0,
   3444             summary_lines_percent: 100.0,
   3445             summary_regions_percent: 100.0,
   3446         };
   3447         let lcov = LcovCoverage {
   3448             executable_total: 10,
   3449             executable_covered: 10,
   3450             executable_percent: 100.0,
   3451             executable_source: ExecutableSource::Da,
   3452             branch_total: 0,
   3453             branch_covered: 0,
   3454             branches_available: false,
   3455             branch_percent: None,
   3456         };
   3457         let thresholds = CoverageThresholds {
   3458             fail_under_exec_lines: 100.0,
   3459             fail_under_functions: 100.0,
   3460             fail_under_regions: 100.0,
   3461             fail_under_branches: 100.0,
   3462             require_branches: true,
   3463         };
   3464 
   3465         let gate = evaluate_gate(&summary, &lcov, thresholds);
   3466         assert!(!gate.pass);
   3467         assert!(
   3468             gate.fail_reasons
   3469                 .iter()
   3470                 .any(|reason| reason == "branches=unavailable")
   3471         );
   3472     }
   3473 
   3474     #[test]
   3475     fn reads_required_crates_and_rejects_duplicates() {
   3476         let path = temp_file_path("required_crates");
   3477         fs::write(
   3478             &path,
   3479             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"b\"]\n",
   3480         )
   3481         .expect("write required crates");
   3482         let crates = read_required_crates(&path).expect("parse required crates");
   3483         assert_eq!(crates, vec!["a".to_string(), "b".to_string()]);
   3484         fs::remove_file(&path).expect("remove required crates");
   3485 
   3486         let dup_path = temp_file_path("required_crates_dup");
   3487         fs::write(
   3488             &dup_path,
   3489             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"a\"]\n",
   3490         )
   3491         .expect("write dup required crates");
   3492         let err = read_required_crates(&dup_path).expect_err("duplicate required crates");
   3493         assert!(err.contains("duplicate crate a"));
   3494         fs::remove_file(dup_path).expect("remove dup required crates");
   3495     }
   3496 
   3497     #[test]
   3498     fn read_required_crates_reports_read_and_parse_errors() {
   3499         let missing = temp_file_path("required_missing");
   3500         let read_err = read_required_crates(&missing).expect_err("missing required file");
   3501         assert!(read_err.contains("failed to read coverage policy"));
   3502 
   3503         let invalid = temp_file_path("required_invalid");
   3504         write_file(&invalid, "not = [toml");
   3505         let parse_err = read_required_crates(&invalid).expect_err("invalid required file");
   3506         assert!(parse_err.contains("failed to parse coverage policy"));
   3507         fs::remove_file(invalid).expect("remove invalid required file");
   3508     }
   3509 
   3510     #[test]
   3511     fn reads_workspace_crates_and_contains_xtask() {
   3512         let root = workspace_root();
   3513         let crates = read_workspace_crates(&root).expect("workspace crates");
   3514         assert!(!crates.is_empty());
   3515         assert!(crates.iter().any(|crate_name| crate_name == "xtask"));
   3516     }
   3517 
   3518     #[test]
   3519     fn coverage_profiles_default_when_contract_file_is_missing() {
   3520         let root = temp_dir_path("profile_missing");
   3521         fs::create_dir_all(&root).expect("create root");
   3522         let profile = read_coverage_profile(&root, "radroots_log").expect("read profile");
   3523         assert!(!profile.no_default_features);
   3524         assert!(profile.features.is_empty());
   3525         assert_eq!(profile.test_threads, None);
   3526         fs::remove_dir_all(root).expect("remove root");
   3527     }
   3528 
   3529     #[test]
   3530     fn coverage_profiles_merge_defaults_and_crate_overrides() {
   3531         let root = temp_dir_path("profile_merge");
   3532         let coverage_dir = root.join("contracts");
   3533         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3534         fs::write(
   3535             coverage_dir.join("coverage-profiles.toml"),
   3536             r#"[profiles.default]
   3537 no_default_features = false
   3538 features = ["std"]
   3539 test_threads = 2
   3540 
   3541 [profiles.crates."radroots_log"]
   3542 no_default_features = true
   3543 features = ["rt"]
   3544 "#,
   3545         )
   3546         .expect("write profiles");
   3547 
   3548         let app_profile = read_coverage_profile(&root, "radroots_log").expect("app profile");
   3549         assert!(app_profile.no_default_features);
   3550         assert_eq!(app_profile.features, vec!["rt".to_string()]);
   3551         assert_eq!(app_profile.test_threads, Some(2));
   3552 
   3553         let other_profile = read_coverage_profile(&root, "radroots_types").expect("other profile");
   3554         assert!(!other_profile.no_default_features);
   3555         assert_eq!(other_profile.features, vec!["std".to_string()]);
   3556         assert_eq!(other_profile.test_threads, Some(2));
   3557 
   3558         fs::remove_dir_all(root).expect("remove root");
   3559     }
   3560 
   3561     #[test]
   3562     fn coverage_profiles_accept_positive_test_threads() {
   3563         let root = temp_dir_path("profile_positive_threads");
   3564         let coverage_dir = root.join("contracts");
   3565         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3566         fs::write(
   3567             coverage_dir.join("coverage-profiles.toml"),
   3568             r#"[profiles.crates."radroots_log"]
   3569 test_threads = 4
   3570 "#,
   3571         )
   3572         .expect("write profiles");
   3573         let profile =
   3574             read_coverage_profile(&root, "radroots_log").expect("valid positive thread profile");
   3575         assert_eq!(profile.test_threads, Some(4));
   3576         fs::remove_dir_all(root).expect("remove root");
   3577     }
   3578 
   3579     #[test]
   3580     fn coverage_profiles_reject_invalid_feature_and_thread_values() {
   3581         let root = temp_dir_path("profile_invalid");
   3582         let coverage_dir = root.join("contracts");
   3583         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3584         fs::write(
   3585             coverage_dir.join("coverage-profiles.toml"),
   3586             r#"[profiles.crates."radroots_log"]
   3587 features = [""]
   3588 test_threads = 0
   3589 "#,
   3590         )
   3591         .expect("write profiles");
   3592 
   3593         let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid profile");
   3594         assert!(
   3595             err.contains("empty feature value"),
   3596             "unexpected error: {err}"
   3597         );
   3598 
   3599         fs::remove_dir_all(root).expect("remove root");
   3600     }
   3601 
   3602     #[test]
   3603     fn coverage_profiles_reject_invalid_toml() {
   3604         let root = temp_dir_path("profile_invalid_toml");
   3605         let coverage_dir = root.join("contracts");
   3606         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3607         fs::write(
   3608             coverage_dir.join("coverage-profiles.toml"),
   3609             "[profiles.default\n",
   3610         )
   3611         .expect("write invalid profiles");
   3612         let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid toml");
   3613         assert!(err.contains("failed to parse"));
   3614         fs::remove_dir_all(root).expect("remove root");
   3615     }
   3616 
   3617     #[test]
   3618     fn coverage_profiles_reject_zero_test_threads_without_feature_error() {
   3619         let root = temp_dir_path("profile_invalid_threads");
   3620         let coverage_dir = root.join("contracts");
   3621         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   3622         fs::write(
   3623             coverage_dir.join("coverage-profiles.toml"),
   3624             r#"[profiles.crates."radroots_log"]
   3625 test_threads = 0
   3626 "#,
   3627         )
   3628         .expect("write profiles");
   3629 
   3630         let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid thread count");
   3631         assert!(err.contains("test_threads > 0"));
   3632 
   3633         fs::remove_dir_all(root).expect("remove root");
   3634     }
   3635 
   3636     #[test]
   3637     fn parse_helpers_cover_success_and_error_paths() {
   3638         let args = vec![
   3639             "--scope".to_string(),
   3640             "crate-a".to_string(),
   3641             "--value".to_string(),
   3642             "3.5".to_string(),
   3643             "--threads".to_string(),
   3644             "4".to_string(),
   3645             "--flag".to_string(),
   3646         ];
   3647         assert_eq!(
   3648             parse_string_arg(&args, "scope").expect("scope value"),
   3649             "crate-a".to_string()
   3650         );
   3651         assert_eq!(
   3652             parse_optional_string_arg(&args, "scope").expect("optional scope"),
   3653             "crate-a".to_string()
   3654         );
   3655         assert_eq!(parse_f64_arg(&args, "value", 1.0).expect("f64 value"), 3.5);
   3656         assert_eq!(
   3657             parse_optional_u32_arg(&args, "threads").expect("u32 value"),
   3658             Some(4)
   3659         );
   3660         assert!(parse_bool_flag(&args, "flag"));
   3661         assert_eq!(parse_optional_string_arg(&args, "missing"), None);
   3662         assert_eq!(
   3663             parse_f64_arg(&args, "missing", 2.25).expect("default f64"),
   3664             2.25
   3665         );
   3666         assert_eq!(
   3667             parse_optional_u32_arg(&args, "missing").expect("missing u32"),
   3668             None
   3669         );
   3670 
   3671         let missing_err = parse_string_arg(&args, "absent").expect_err("missing arg");
   3672         assert!(missing_err.contains("missing --absent"));
   3673 
   3674         let missing_value = vec!["--scope".to_string()];
   3675         let missing_value_err =
   3676             parse_string_arg(&missing_value, "scope").expect_err("missing arg value");
   3677         assert!(missing_value_err.contains("missing value for --scope"));
   3678 
   3679         let invalid_f64 = vec!["--value".to_string(), "bad".to_string()];
   3680         let invalid_f64_err = parse_f64_arg(&invalid_f64, "value", 1.0).expect_err("invalid f64");
   3681         assert!(invalid_f64_err.contains("invalid --value value"));
   3682 
   3683         let invalid_u32 = vec!["--threads".to_string(), "bad".to_string()];
   3684         let invalid_u32_err =
   3685             parse_optional_u32_arg(&invalid_u32, "threads").expect_err("invalid u32");
   3686         assert!(invalid_u32_err.contains("invalid --threads value"));
   3687     }
   3688 
   3689     #[test]
   3690     fn executable_source_labels_cover_all_variants() {
   3691         assert_eq!(executable_source_label(ExecutableSource::Da), "da");
   3692         assert_eq!(executable_source_label(ExecutableSource::LfLh), "lf_lh");
   3693         assert_eq!(percentage(0, 0), 100.0);
   3694     }
   3695 
   3696     #[test]
   3697     fn read_required_crates_rejects_empty_and_blank_entries() {
   3698         let empty_path = temp_file_path("required_empty");
   3699         write_file(
   3700             &empty_path,
   3701             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = []\n",
   3702         );
   3703         let empty_err = read_required_crates(&empty_path).expect_err("empty required list");
   3704         assert!(empty_err.contains("must not be empty"));
   3705         fs::remove_file(&empty_path).expect("remove empty required file");
   3706 
   3707         let blank_path = temp_file_path("required_blank");
   3708         write_file(
   3709             &blank_path,
   3710             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \" \"]\n",
   3711         );
   3712         let blank_err = read_required_crates(&blank_path).expect_err("blank crate name");
   3713         assert!(blank_err.contains("empty crate name"));
   3714         fs::remove_file(&blank_path).expect("remove blank required file");
   3715     }
   3716 
   3717     #[test]
   3718     fn read_workspace_crates_rejects_invalid_workspace_shapes() {
   3719         let root_empty = temp_dir_path("workspace_empty_members");
   3720         write_file(
   3721             &root_empty.join("Cargo.toml"),
   3722             "[workspace]\nmembers = []\n",
   3723         );
   3724         let empty_err = read_workspace_crates(&root_empty).expect_err("empty workspace members");
   3725         assert!(empty_err.contains("must not be empty"));
   3726         fs::remove_dir_all(&root_empty).expect("remove empty members root");
   3727 
   3728         let root_blank = temp_dir_path("workspace_blank_package_name");
   3729         write_file(
   3730             &root_blank.join("Cargo.toml"),
   3731             "[workspace]\nmembers = [\"crates/a\"]\n",
   3732         );
   3733         write_file(
   3734             &root_blank.join("crates").join("a").join("Cargo.toml"),
   3735             "[package]\nname = \"\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
   3736         );
   3737         let blank_err = read_workspace_crates(&root_blank).expect_err("blank package name");
   3738         assert!(blank_err.contains("empty package name"));
   3739         fs::remove_dir_all(&root_blank).expect("remove blank package root");
   3740 
   3741         let root_duplicate = temp_dir_path("workspace_duplicate_package");
   3742         write_file(
   3743             &root_duplicate.join("Cargo.toml"),
   3744             "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n",
   3745         );
   3746         let package_manifest =
   3747             "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
   3748         write_file(
   3749             &root_duplicate.join("crates").join("a").join("Cargo.toml"),
   3750             package_manifest,
   3751         );
   3752         write_file(
   3753             &root_duplicate.join("crates").join("b").join("Cargo.toml"),
   3754             package_manifest,
   3755         );
   3756         let dup_err = read_workspace_crates(&root_duplicate).expect_err("duplicate package names");
   3757         assert!(dup_err.contains("duplicate package name"));
   3758         fs::remove_dir_all(&root_duplicate).expect("remove duplicate package root");
   3759 
   3760         let root_parse = temp_dir_path("workspace_parse_error");
   3761         write_file(
   3762             &root_parse.join("Cargo.toml"),
   3763             "[workspace]\nmembers = [\"crates/a\"]\n",
   3764         );
   3765         write_file(
   3766             &root_parse.join("crates").join("a").join("Cargo.toml"),
   3767             "[package",
   3768         );
   3769         let parse_err = read_workspace_crates(&root_parse).expect_err("invalid package manifest");
   3770         assert!(parse_err.contains("failed to parse"));
   3771         fs::remove_dir_all(&root_parse).expect("remove parse package root");
   3772     }
   3773 
   3774     #[test]
   3775     fn parse_toml_reports_read_and_parse_errors() {
   3776         let missing = temp_file_path("parse_toml_missing");
   3777         let read_err =
   3778             parse_toml::<CoveragePolicyFile>(&missing).expect_err("missing file should fail");
   3779         assert!(read_err.contains("failed to read"));
   3780 
   3781         let invalid = temp_file_path("parse_toml_invalid");
   3782         write_file(&invalid, "[gate]\nfail_under_exec_lines = 100.0\n");
   3783         let parse_err =
   3784             parse_toml::<CoveragePolicyFile>(&invalid).expect_err("invalid toml should fail");
   3785         assert!(parse_err.contains("failed to parse"));
   3786         fs::remove_file(invalid).expect("remove invalid toml");
   3787 
   3788         let workspace_missing = temp_file_path("parse_toml_workspace_missing");
   3789         let workspace_read_err = parse_toml::<WorkspaceManifest>(&workspace_missing)
   3790             .expect_err("missing workspace manifest should fail");
   3791         assert!(workspace_read_err.contains("failed to read"));
   3792 
   3793         let workspace_invalid = temp_file_path("parse_toml_workspace_invalid");
   3794         write_file(&workspace_invalid, "[workspace");
   3795         let workspace_parse_err = parse_toml::<WorkspaceManifest>(&workspace_invalid)
   3796             .expect_err("invalid workspace manifest should fail");
   3797         assert!(workspace_parse_err.contains("failed to parse"));
   3798         fs::remove_file(workspace_invalid).expect("remove invalid workspace manifest");
   3799 
   3800         let package_missing = temp_file_path("parse_toml_package_missing");
   3801         let package_read_err = parse_toml::<PackageManifest>(&package_missing)
   3802             .expect_err("missing package manifest should fail");
   3803         assert!(package_read_err.contains("failed to read"));
   3804 
   3805         let package_invalid = temp_file_path("parse_toml_package_invalid");
   3806         write_file(&package_invalid, "[package");
   3807         let package_parse_err = parse_toml::<PackageManifest>(&package_invalid)
   3808             .expect_err("invalid package manifest should fail");
   3809         assert!(package_parse_err.contains("failed to parse"));
   3810         fs::remove_file(package_invalid).expect("remove invalid package manifest");
   3811 
   3812         let profiles_missing = temp_file_path("parse_toml_profiles_missing");
   3813         let profiles_read_err = parse_toml::<CoverageProfilesFile>(&profiles_missing)
   3814             .expect_err("missing coverage profiles should fail");
   3815         assert!(profiles_read_err.contains("failed to read"));
   3816 
   3817         let profiles_invalid = temp_file_path("parse_toml_profiles_invalid");
   3818         write_file(&profiles_invalid, "[profiles.default");
   3819         let profiles_parse_err = parse_toml::<CoverageProfilesFile>(&profiles_invalid)
   3820             .expect_err("invalid coverage profiles should fail");
   3821         assert!(profiles_parse_err.contains("failed to parse"));
   3822         fs::remove_file(profiles_invalid).expect("remove invalid coverage profiles");
   3823     }
   3824 
   3825     #[test]
   3826     fn parse_toml_parses_valid_coverage_required_contract() {
   3827         let valid = temp_file_path("parse_toml_valid");
   3828         write_file(
   3829             &valid,
   3830             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_core\"]\n",
   3831         );
   3832         let parsed = parse_toml::<CoveragePolicyFile>(&valid).expect("valid toml");
   3833         assert_eq!(parsed.required.crates, vec!["radroots_core".to_string()]);
   3834         fs::remove_file(valid).expect("remove valid toml");
   3835     }
   3836 
   3837     #[test]
   3838     fn read_lcov_rejects_invalid_records() {
   3839         let cases = vec![
   3840             ("invalid_da_shape", "DA:1\n", "invalid DA record"),
   3841             ("invalid_da_line", "DA:bad,1\n", "invalid DA line number"),
   3842             ("invalid_da_hits", "DA:1,bad\n", "invalid DA hit count"),
   3843             ("invalid_lf", "LF:bad\n", "invalid LF value"),
   3844             ("invalid_lh", "LH:bad\n", "invalid LH value"),
   3845             ("invalid_brf", "BRF:bad\n", "invalid BRF value"),
   3846             ("invalid_brh", "BRH:bad\n", "invalid BRH value"),
   3847             ("invalid_brda_shape", "BRDA:1,0,0\n", "invalid BRDA record"),
   3848             (
   3849                 "invalid_brda_line",
   3850                 "BRDA:bad,0,0,1\n",
   3851                 "invalid BRDA line number",
   3852             ),
   3853             (
   3854                 "invalid_brda_taken",
   3855                 "BRDA:1,0,0,bad\n",
   3856                 "invalid BRDA taken count",
   3857             ),
   3858             (
   3859                 "invalid_brda_extra",
   3860                 "BRDA:1,0,0,1,extra\n",
   3861                 "invalid BRDA record",
   3862             ),
   3863         ];
   3864         for (prefix, raw, expected) in cases {
   3865             let path = temp_file_path(prefix);
   3866             write_file(&path, raw);
   3867             let err = read_lcov(&path).expect_err("invalid lcov record");
   3868             assert!(
   3869                 err.contains(expected),
   3870                 "expected `{expected}` in `{err}` for case {prefix}"
   3871             );
   3872             fs::remove_file(path).expect("remove invalid lcov file");
   3873         }
   3874     }
   3875 
   3876     #[test]
   3877     fn read_lcov_reports_read_error() {
   3878         let missing = temp_file_path("lcov_missing");
   3879         let err = read_lcov(&missing).expect_err("missing lcov should fail");
   3880         assert!(err.contains("failed to read lcov"));
   3881     }
   3882 
   3883     #[test]
   3884     fn read_lcov_uses_lf_lh_when_da_is_missing_and_branches_absent() {
   3885         let path = temp_file_path("lcov_lf_lh");
   3886         fs::write(&path, "LF:4\nLH:3\n").expect("write lcov");
   3887         let parsed = read_lcov(&path).expect("parse lcov");
   3888         assert_eq!(executable_source_label(parsed.executable_source), "lf_lh");
   3889         assert_eq!(parsed.executable_total, 4);
   3890         assert_eq!(parsed.executable_covered, 3);
   3891         assert_eq!(parsed.executable_percent, 75.0);
   3892         assert!(!parsed.branches_available);
   3893         assert_eq!(parsed.branch_percent, None);
   3894         fs::remove_file(path).expect("remove lcov");
   3895     }
   3896 
   3897     #[test]
   3898     fn read_lcov_defaults_to_full_when_no_line_records_exist() {
   3899         let path = temp_file_path("lcov_empty");
   3900         write_file(&path, "TN:probe\n");
   3901         let parsed = read_lcov(&path).expect("parse lcov");
   3902         assert_eq!(parsed.executable_total, 0);
   3903         assert_eq!(parsed.executable_covered, 0);
   3904         assert_eq!(parsed.executable_percent, 100.0);
   3905         assert!(!parsed.branches_available);
   3906         assert_eq!(parsed.branch_percent, None);
   3907         fs::remove_file(path).expect("remove lcov");
   3908     }
   3909 
   3910     #[test]
   3911     fn evaluate_gate_collects_all_failure_reasons() {
   3912         let summary = CoverageSummary {
   3913             functions_percent: 40.0,
   3914             summary_lines_percent: 50.0,
   3915             summary_regions_percent: 60.0,
   3916         };
   3917         let lcov = LcovCoverage {
   3918             executable_total: 20,
   3919             executable_covered: 10,
   3920             executable_percent: 50.0,
   3921             executable_source: ExecutableSource::Da,
   3922             branch_total: 10,
   3923             branch_covered: 3,
   3924             branches_available: true,
   3925             branch_percent: Some(30.0),
   3926         };
   3927         let thresholds = CoverageThresholds {
   3928             fail_under_exec_lines: 90.0,
   3929             fail_under_functions: 90.0,
   3930             fail_under_regions: 90.0,
   3931             fail_under_branches: 90.0,
   3932             require_branches: true,
   3933         };
   3934 
   3935         let gate = evaluate_gate(&summary, &lcov, thresholds);
   3936         assert!(!gate.pass);
   3937         assert!(
   3938             gate.fail_reasons
   3939                 .iter()
   3940                 .any(|reason| reason.contains("executable_lines"))
   3941         );
   3942         assert!(
   3943             gate.fail_reasons
   3944                 .iter()
   3945                 .any(|reason| reason.contains("functions"))
   3946         );
   3947         assert!(
   3948             gate.fail_reasons
   3949                 .iter()
   3950                 .any(|reason| reason.contains("regions"))
   3951         );
   3952         assert!(
   3953             gate.fail_reasons
   3954                 .iter()
   3955                 .any(|reason| reason.contains("branches"))
   3956         );
   3957     }
   3958 
   3959     #[test]
   3960     fn run_command_covers_success_and_failure() {
   3961         let mut ok = Command::new("sh");
   3962         ok.arg("-c").arg("exit 0");
   3963         run_command(ok, "shell ok").expect("run ok command");
   3964 
   3965         let mut fail = Command::new("sh");
   3966         fail.arg("-c").arg("exit 9");
   3967         let err = run_command(fail, "shell fail").expect_err("run failing command");
   3968         assert!(err.contains("shell fail failed with status"));
   3969 
   3970         let missing = Command::new("/definitely/not/a/real/command");
   3971         let err = run_command(missing, "shell missing").expect_err("missing command");
   3972         assert!(err.contains("failed to run shell missing"));
   3973     }
   3974 
   3975     #[test]
   3976     fn apply_coverage_profile_flags_writes_expected_args() {
   3977         let profile = CoverageProfile {
   3978             no_default_features: true,
   3979             features: vec!["std".to_string(), "serde".to_string()],
   3980             test_threads: Some(2),
   3981         };
   3982         let mut command = Command::new("cargo");
   3983         apply_coverage_profile_flags(&mut command, &profile);
   3984         let args = command
   3985             .get_args()
   3986             .map(|arg| arg.to_string_lossy().to_string())
   3987             .collect::<Vec<_>>();
   3988         assert_eq!(
   3989             args,
   3990             vec![
   3991                 "--no-default-features".to_string(),
   3992                 "--features".to_string(),
   3993                 "std,serde".to_string()
   3994             ]
   3995         );
   3996     }
   3997 
   3998     #[test]
   3999     fn run_crate_with_runner_builds_all_command_steps() {
   4000         let out = temp_dir_path("run_crate_runner");
   4001         let args = vec![
   4002             "--crate".to_string(),
   4003             "radroots_core".to_string(),
   4004             "--out".to_string(),
   4005             out.display().to_string(),
   4006             "--test-threads".to_string(),
   4007             "3".to_string(),
   4008         ];
   4009         let mut names = Vec::new();
   4010         let mut rendered_commands = Vec::new();
   4011         let mut runner = |cmd: Command, name: &str| {
   4012             names.push(name.to_string());
   4013             let rendered = cmd
   4014                 .get_args()
   4015                 .map(|arg| arg.to_string_lossy().to_string())
   4016                 .collect::<Vec<_>>()
   4017                 .join(" ");
   4018             assert!(!rendered.is_empty());
   4019             rendered_commands.push(rendered);
   4020             Ok(())
   4021         };
   4022         run_crate_with_runner(&args, &mut runner).expect("run crate with stub runner");
   4023         assert_eq!(
   4024             names,
   4025             vec![
   4026                 "cargo llvm-cov clean --workspace".to_string(),
   4027                 "cargo llvm-cov --no-report".to_string(),
   4028                 "cargo llvm-cov report --json --summary-only".to_string(),
   4029                 "cargo llvm-cov report --json".to_string(),
   4030                 "cargo llvm-cov report --lcov".to_string(),
   4031             ]
   4032         );
   4033         assert!(
   4034             rendered_commands
   4035                 .iter()
   4036                 .filter(|rendered| rendered.contains("report -p radroots_core"))
   4037                 .all(|rendered| rendered.contains("--ignore-filename-regex"))
   4038         );
   4039         assert!(
   4040             rendered_commands
   4041                 .iter()
   4042                 .filter(|rendered| rendered.contains("report -p radroots_core"))
   4043                 .all(|rendered| rendered.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX))
   4044         );
   4045         fs::remove_dir_all(out).expect("remove run crate output dir");
   4046     }
   4047 
   4048     #[test]
   4049     fn coverage_ignore_filename_regex_excludes_external_and_sibling_workspace_paths() {
   4050         let root = workspace_root();
   4051         let ignore_regex =
   4052             coverage_ignore_filename_regex(&root, "radroots_core").expect("build ignore regex");
   4053         assert!(ignore_regex.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX));
   4054         assert!(ignore_regex.contains("crates/identity"));
   4055         assert!(ignore_regex.contains("crates/core/tests"));
   4056         assert!(!ignore_regex.contains("crates/core/src"));
   4057     }
   4058 
   4059     #[test]
   4060     fn escape_regex_literal_escapes_regex_metacharacters() {
   4061         let escaped = escape_regex_literal(r"\.+*?()|[]{}^$");
   4062         assert_eq!(escaped, r"\\\.\+\*\?\(\)\|\[\]\{\}\^\$");
   4063     }
   4064 
   4065     #[test]
   4066     fn coverage_cargo_command_defaults_to_rustup_nightly() {
   4067         let cmd = coverage_cargo_command_with_override(None);
   4068         let mut args = Vec::new();
   4069         for arg in cmd.get_args() {
   4070             args.push(arg.to_string_lossy().to_string());
   4071         }
   4072 
   4073         assert_eq!(cmd.get_program().to_string_lossy(), "rustup");
   4074         assert_eq!(
   4075             args,
   4076             vec![
   4077                 "run".to_string(),
   4078                 "nightly".to_string(),
   4079                 "cargo".to_string()
   4080             ]
   4081         );
   4082     }
   4083 
   4084     #[test]
   4085     fn normalized_coverage_cargo_override_trims_and_filters_values() {
   4086         assert_eq!(
   4087             normalized_coverage_cargo_override(Some("  /tmp/cargo  ".to_string())),
   4088             Some("/tmp/cargo".to_string())
   4089         );
   4090         assert_eq!(
   4091             normalized_coverage_cargo_override(Some("   ".to_string())),
   4092             None
   4093         );
   4094         assert_eq!(normalized_coverage_cargo_override(None), None);
   4095     }
   4096 
   4097     fn assert_coverage_command_shapes(
   4098         cargo_cmd: Command,
   4099         llvm_cov_cmd: Command,
   4100         override_binary: Option<&str>,
   4101     ) {
   4102         match override_binary {
   4103             Some(binary) => assert_eq!(cargo_cmd.get_program().to_string_lossy(), binary),
   4104             None => assert_eq!(cargo_cmd.get_program().to_string_lossy(), "rustup"),
   4105         }
   4106 
   4107         let llvm_args = llvm_cov_cmd
   4108             .get_args()
   4109             .map(|arg| arg.to_string_lossy().to_string())
   4110             .collect::<Vec<_>>();
   4111         match override_binary {
   4112             Some(_) => assert_eq!(llvm_args, vec!["llvm-cov".to_string()]),
   4113             None => assert_eq!(
   4114                 llvm_args,
   4115                 vec![
   4116                     "run".to_string(),
   4117                     "nightly".to_string(),
   4118                     "cargo".to_string(),
   4119                     "llvm-cov".to_string()
   4120                 ]
   4121             ),
   4122         }
   4123     }
   4124 
   4125     #[test]
   4126     fn coverage_public_command_helpers_match_current_env_resolution() {
   4127         let mut default_llvm_cov_cmd = coverage_cargo_command_with_override(None);
   4128         default_llvm_cov_cmd.arg("llvm-cov");
   4129         assert_coverage_command_shapes(
   4130             coverage_cargo_command_with_override(None),
   4131             default_llvm_cov_cmd,
   4132             None,
   4133         );
   4134 
   4135         let explicit_binary = temp_dir_path("coverage_command_override")
   4136             .join("nightly-cargo")
   4137             .to_string_lossy()
   4138             .to_string();
   4139         let mut explicit_llvm_cov_cmd =
   4140             coverage_cargo_command_with_override(Some(&explicit_binary));
   4141         explicit_llvm_cov_cmd.arg("llvm-cov");
   4142         assert_coverage_command_shapes(
   4143             coverage_cargo_command_with_override(Some(&explicit_binary)),
   4144             explicit_llvm_cov_cmd,
   4145             Some(explicit_binary.as_str()),
   4146         );
   4147 
   4148         let override_binary =
   4149             normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok());
   4150         assert_coverage_command_shapes(
   4151             coverage_cargo_command(),
   4152             coverage_llvm_cov_command(),
   4153             override_binary.as_deref(),
   4154         );
   4155     }
   4156 
   4157     #[test]
   4158     fn configure_coverage_toolchain_env_sets_existing_binary_envs() {
   4159         let toolchain_dir = temp_dir_path("coverage_toolchain_env");
   4160         fs::create_dir_all(&toolchain_dir).expect("create toolchain env dir");
   4161         for binary in ["rustc", "rustdoc", "llvm-cov", "llvm-profdata"] {
   4162             write_file(&toolchain_dir.join(binary), "");
   4163         }
   4164 
   4165         let mut cmd = Command::new("cargo");
   4166         configure_coverage_toolchain_env(&mut cmd, &toolchain_dir);
   4167         let envs = collect_command_envs(&cmd);
   4168         assert_eq!(
   4169             envs.get("RUSTC"),
   4170             Some(&Some(
   4171                 toolchain_dir.join("rustc").to_string_lossy().to_string()
   4172             ))
   4173         );
   4174         assert_eq!(
   4175             envs.get("RUSTDOC"),
   4176             Some(&Some(
   4177                 toolchain_dir.join("rustdoc").to_string_lossy().to_string()
   4178             ))
   4179         );
   4180         assert_eq!(
   4181             envs.get("LLVM_COV"),
   4182             Some(&Some(
   4183                 toolchain_dir.join("llvm-cov").to_string_lossy().to_string()
   4184             ))
   4185         );
   4186         assert_eq!(
   4187             envs.get("LLVM_PROFDATA"),
   4188             Some(&Some(
   4189                 toolchain_dir
   4190                     .join("llvm-profdata")
   4191                     .to_string_lossy()
   4192                     .to_string()
   4193             ))
   4194         );
   4195 
   4196         fs::remove_dir_all(toolchain_dir).expect("remove toolchain env dir");
   4197     }
   4198 
   4199     #[test]
   4200     fn configure_coverage_toolchain_env_skips_missing_binary_envs() {
   4201         let toolchain_dir = temp_dir_path("coverage_toolchain_missing_env");
   4202         fs::create_dir_all(&toolchain_dir).expect("create missing env dir");
   4203 
   4204         let mut cmd = Command::new("cargo");
   4205         configure_coverage_toolchain_env(&mut cmd, &toolchain_dir);
   4206         let envs = collect_command_envs(&cmd);
   4207         assert!(!envs.contains_key("RUSTC"));
   4208         assert!(!envs.contains_key("RUSTDOC"));
   4209         assert!(!envs.contains_key("LLVM_COV"));
   4210         assert!(!envs.contains_key("LLVM_PROFDATA"));
   4211 
   4212         fs::remove_dir_all(toolchain_dir).expect("remove missing env dir");
   4213     }
   4214 
   4215     #[test]
   4216     fn coverage_cargo_command_override_variants_cover_parented_and_parentless_paths() {
   4217         let toolchain_dir = temp_dir_path("coverage_toolchain_override");
   4218         fs::create_dir_all(&toolchain_dir).expect("create toolchain dir");
   4219         for binary in [
   4220             "nightly-cargo",
   4221             "rustc",
   4222             "rustdoc",
   4223             "llvm-cov",
   4224             "llvm-profdata",
   4225         ] {
   4226             write_file(&toolchain_dir.join(binary), "");
   4227         }
   4228 
   4229         let default_cmd = coverage_cargo_command_with_override(None);
   4230         let mut args = Vec::new();
   4231         for arg in default_cmd.get_args() {
   4232             args.push(arg.to_string_lossy().to_string());
   4233         }
   4234         assert_eq!(default_cmd.get_program().to_string_lossy(), "rustup");
   4235         assert_eq!(
   4236             args,
   4237             vec![
   4238                 "run".to_string(),
   4239                 "nightly".to_string(),
   4240                 "cargo".to_string()
   4241             ]
   4242         );
   4243 
   4244         let override_binary = toolchain_dir.join("nightly-cargo");
   4245         let cmd = coverage_cargo_command_with_override(Some(
   4246             override_binary
   4247                 .to_str()
   4248                 .expect("override path should be utf-8"),
   4249         ));
   4250 
   4251         assert_eq!(
   4252             cmd.get_program().to_string_lossy(),
   4253             override_binary.to_string_lossy()
   4254         );
   4255         assert!(cmd.get_args().next().is_none());
   4256         let mut envs = collect_command_envs(&cmd);
   4257         envs.insert("MISSING".to_string(), None);
   4258         assert_eq!(
   4259             envs.get("RUSTC"),
   4260             Some(&Some(
   4261                 toolchain_dir.join("rustc").to_string_lossy().to_string()
   4262             ))
   4263         );
   4264         assert_eq!(
   4265             envs.get("RUSTDOC"),
   4266             Some(&Some(
   4267                 toolchain_dir.join("rustdoc").to_string_lossy().to_string()
   4268             ))
   4269         );
   4270         assert_eq!(
   4271             envs.get("LLVM_COV"),
   4272             Some(&Some(
   4273                 toolchain_dir.join("llvm-cov").to_string_lossy().to_string()
   4274             ))
   4275         );
   4276         assert_eq!(
   4277             envs.get("LLVM_PROFDATA"),
   4278             Some(&Some(
   4279                 toolchain_dir
   4280                     .join("llvm-profdata")
   4281                     .to_string_lossy()
   4282                     .to_string()
   4283             ))
   4284         );
   4285         let path_env = envs
   4286             .get("PATH")
   4287             .and_then(|value| value.as_ref())
   4288             .expect("override binary should prepend PATH");
   4289         assert!(path_env.starts_with(toolchain_dir.to_string_lossy().as_ref()));
   4290         let mut cmd = coverage_cargo_command_with_override(Some("/"));
   4291         cmd.env_remove("RUSTC");
   4292         cmd.env_remove("LLVM_COV");
   4293         assert_eq!(cmd.get_program().to_string_lossy(), "/");
   4294         let envs = collect_command_envs(&cmd);
   4295         assert_eq!(envs.get("RUSTC"), Some(&None));
   4296         assert_eq!(envs.get("LLVM_COV"), Some(&None));
   4297 
   4298         fs::remove_dir_all(toolchain_dir).expect("remove toolchain dir");
   4299     }
   4300 
   4301     #[test]
   4302     fn workspace_root_override_takes_precedence() {
   4303         let root = workspace_root_with_override(Some("/tmp/radroots-coverage-root"));
   4304         assert_eq!(root, PathBuf::from("/tmp/radroots-coverage-root"));
   4305 
   4306         let fallback = workspace_root_with_override(Some(""));
   4307         assert!(fallback.join("Cargo.toml").exists());
   4308 
   4309         let default_root = workspace_root_with_override(None);
   4310         assert!(default_root.join("Cargo.toml").exists());
   4311     }
   4312 
   4313     #[test]
   4314     fn prepend_toolchain_bin_to_path_covers_missing_and_existing_path_inputs() {
   4315         let toolchain_dir = PathBuf::from("/tmp/radroots-coverage-toolchain");
   4316         let no_path = prepend_toolchain_bin_to_path(&toolchain_dir, None);
   4317         assert_eq!(no_path, OsString::from(&toolchain_dir));
   4318 
   4319         let joined =
   4320             prepend_toolchain_bin_to_path(&toolchain_dir, Some(OsString::from("/usr/bin:/bin")));
   4321         let joined = joined.to_string_lossy().to_string();
   4322         assert!(joined.starts_with("/tmp/radroots-coverage-toolchain"));
   4323         assert!(joined.contains("/usr/bin"));
   4324     }
   4325 
   4326     #[test]
   4327     fn collect_command_envs_cover_helper_paths() {
   4328         let mut cmd = Command::new("sh");
   4329         cmd.env("PRESENT", "value");
   4330         cmd.env_remove("REMOVED");
   4331         let envs = collect_command_envs(&cmd);
   4332         assert_eq!(envs.get("PRESENT"), Some(&Some("value".to_string())));
   4333         assert_eq!(envs.get("REMOVED"), Some(&None));
   4334     }
   4335 
   4336     #[test]
   4337     fn ok_runner_helper_returns_success() {
   4338         let cmd = Command::new("true");
   4339         assert!(ok_runner(cmd, "noop").is_ok());
   4340     }
   4341 
   4342     #[test]
   4343     fn run_crate_with_runner_uses_default_output_dir_when_out_is_missing() {
   4344         let args = vec!["--crate".to_string(), "radroots_core".to_string()];
   4345         let mut output_path_seen = false;
   4346         let mut runner = |cmd: Command, _: &str| {
   4347             let rendered = cmd
   4348                 .get_args()
   4349                 .map(|arg| arg.to_string_lossy().to_string())
   4350                 .collect::<Vec<_>>();
   4351             if rendered
   4352                 .iter()
   4353                 .any(|arg| arg.ends_with("coverage-summary.json"))
   4354                 || rendered
   4355                     .iter()
   4356                     .any(|arg| arg.ends_with("coverage-details.json"))
   4357                 || rendered
   4358                     .iter()
   4359                     .any(|arg| arg.ends_with("coverage-lcov.info"))
   4360             {
   4361                 output_path_seen = true;
   4362             }
   4363             Ok(())
   4364         };
   4365         run_crate_with_runner(&args, &mut runner).expect("run crate with default out");
   4366         assert!(output_path_seen);
   4367     }
   4368 
   4369     #[test]
   4370     fn run_crate_with_runner_propagates_runner_failures() {
   4371         let out = temp_dir_path("run_crate_runner_fail");
   4372         let args = vec![
   4373             "--crate".to_string(),
   4374             "radroots_core".to_string(),
   4375             "--out".to_string(),
   4376             out.display().to_string(),
   4377         ];
   4378         let mut runner = |_: Command, _: &str| Err("runner failed".to_string());
   4379         let err =
   4380             run_crate_with_runner(&args, &mut runner).expect_err("runner failure should bubble up");
   4381         assert_eq!(err, "runner failed".to_string());
   4382         fs::remove_dir_all(out).expect("remove run crate failure output dir");
   4383         let root = temp_dir_path("run_crate_create_out_error");
   4384         write_file(&root.join("blocker"), "x");
   4385         let args = vec![
   4386             "--crate".to_string(),
   4387             "radroots_core".to_string(),
   4388             "--out".to_string(),
   4389             root.join("blocker").join("nested").display().to_string(),
   4390         ];
   4391         let mut runner = run_command;
   4392         let err = run_crate_with_runner(&args, &mut runner)
   4393             .expect_err("output dir create error should fail");
   4394         assert!(err.contains("failed to create"));
   4395         fs::remove_dir_all(root).expect("remove run crate create error root");
   4396     }
   4397 
   4398     #[test]
   4399     fn run_crate_wrapper_returns_missing_crate_error_without_running_commands() {
   4400         let err = run_crate(&[]).expect_err("missing crate flag");
   4401         assert!(err.contains("missing --crate"));
   4402     }
   4403 
   4404     #[test]
   4405     fn run_crate_with_runner_at_root_covers_profile_and_runner_error_paths() {
   4406         let write_minimal_workspace = |root: &Path| {
   4407             write_file(
   4408                 &root.join("Cargo.toml"),
   4409                 "[workspace]\nmembers = [\"crates/core\"]\n",
   4410             );
   4411             write_file(
   4412                 &root.join("crates").join("core").join("Cargo.toml"),
   4413                 "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha.2\"\nedition = \"2024\"\n",
   4414             );
   4415         };
   4416 
   4417         let profile_root = temp_dir_path("run_crate_profile_invalid");
   4418         write_minimal_workspace(&profile_root);
   4419         write_file(
   4420             &profile_root
   4421                 .join("contracts")
   4422                 .join("coverage-profiles.toml"),
   4423             "[profiles.default]\nfeatures = [\"\"]\n",
   4424         );
   4425         let profile_args = vec![
   4426             "--crate".to_string(),
   4427             "radroots_core".to_string(),
   4428             "--out".to_string(),
   4429             profile_root.join("out").display().to_string(),
   4430         ];
   4431         let mut runner = run_command;
   4432         let profile_err = run_crate_with_runner_at_root(&profile_args, &profile_root, &mut runner)
   4433             .expect_err("invalid profile should fail");
   4434         assert!(profile_err.contains("empty feature value"));
   4435         fs::remove_dir_all(&profile_root).expect("remove profile root");
   4436 
   4437         let thread_root = temp_dir_path("run_crate_bad_threads");
   4438         fs::create_dir_all(&thread_root).expect("create thread root");
   4439         write_minimal_workspace(&thread_root);
   4440         let thread_args = vec![
   4441             "--crate".to_string(),
   4442             "radroots_core".to_string(),
   4443             "--out".to_string(),
   4444             thread_root.join("out").display().to_string(),
   4445             "--test-threads".to_string(),
   4446             "bad".to_string(),
   4447         ];
   4448         let mut runner = run_command;
   4449         let thread_err = run_crate_with_runner_at_root(&thread_args, &thread_root, &mut runner)
   4450             .expect_err("invalid test threads should fail");
   4451         assert!(thread_err.contains("invalid --test-threads value"));
   4452         fs::remove_dir_all(&thread_root).expect("remove thread root");
   4453 
   4454         for fail_step in [2usize, 3usize, 4usize] {
   4455             let step_root = temp_dir_path("run_crate_step_fail");
   4456             write_minimal_workspace(&step_root);
   4457             let step_args = vec![
   4458                 "--crate".to_string(),
   4459                 "radroots_core".to_string(),
   4460                 "--out".to_string(),
   4461                 step_root.join("out").display().to_string(),
   4462             ];
   4463             let mut calls = 0usize;
   4464             let mut runner = |_: Command, name: &str| {
   4465                 calls += 1;
   4466                 if calls == fail_step {
   4467                     return Err(format!("runner failure at {name}"));
   4468                 }
   4469                 Ok(())
   4470             };
   4471             let err = run_crate_with_runner_at_root(&step_args, &step_root, &mut runner)
   4472                 .expect_err("runner should fail at selected step");
   4473             assert!(err.contains("runner failure at"));
   4474             fs::remove_dir_all(&step_root).expect("remove step root");
   4475         }
   4476     }
   4477 
   4478     #[test]
   4479     fn report_gate_writes_report_file_on_success() {
   4480         let root = temp_dir_path("report_gate_success");
   4481         let summary_path = root.join("summary.json");
   4482         let lcov_path = root.join("coverage.info");
   4483         let out_path = root.join("gate-report.json");
   4484         write_file(
   4485             &summary_path,
   4486             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   4487         );
   4488         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   4489 
   4490         let args = vec![
   4491             "--scope".to_string(),
   4492             "crate-x".to_string(),
   4493             "--summary".to_string(),
   4494             summary_path.display().to_string(),
   4495             "--lcov".to_string(),
   4496             lcov_path.display().to_string(),
   4497             "--out".to_string(),
   4498             out_path.display().to_string(),
   4499             "--policy-gate".to_string(),
   4500         ];
   4501         report_gate(&args).expect("report gate success");
   4502         let report_raw = fs::read_to_string(&out_path).expect("read report");
   4503         assert!(report_raw.contains("\"scope\": \"crate-x\""));
   4504         assert!(report_raw.contains("\"regions\": 100.0"));
   4505         assert!(report_raw.contains("\"pass\": true"));
   4506         fs::remove_dir_all(root).expect("remove report gate success root");
   4507     }
   4508 
   4509     #[test]
   4510     fn report_gate_normalizes_duplicate_generic_records_from_details() {
   4511         let root = temp_dir_path("report_gate_normalized_generics");
   4512         let summary_path = root.join("summary.json");
   4513         let lcov_path = root.join("coverage.info");
   4514         let out_path = root.join("gate-report.json");
   4515         write_file(
   4516             &summary_path,
   4517             r#"{
   4518   "data": [
   4519     {
   4520       "totals": {
   4521         "functions": {"percent": 96.0},
   4522         "lines": {"percent": 99.0},
   4523         "regions": {"percent": 22.0}
   4524       }
   4525     }
   4526   ]
   4527 }"#,
   4528         );
   4529         write_file(
   4530             &root.join("coverage-details.json"),
   4531             r#"{
   4532   "data": [
   4533     {
   4534       "functions": [
   4535         {
   4536           "count": 4,
   4537           "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"],
   4538           "regions": [
   4539             [10, 1, 12, 2, 4, 0, 0, 0],
   4540             [13, 1, 13, 8, 4, 0, 0, 0]
   4541           ]
   4542         },
   4543         {
   4544           "count": 0,
   4545           "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"],
   4546           "regions": [
   4547             [10, 1, 12, 2, 0, 0, 0, 0],
   4548             [13, 1, 13, 8, 0, 0, 0, 0]
   4549           ]
   4550         }
   4551       ]
   4552     }
   4553   ]
   4554 }"#,
   4555         );
   4556         write_file(
   4557             &lcov_path,
   4558             "DA:1,1\nDA:2,0\nLF:2\nLH:1\nBRDA:1,0,0,1\nBRDA:2,0,0,0\n",
   4559         );
   4560 
   4561         let args = vec![
   4562             "--scope".to_string(),
   4563             "radroots_runtime_manager".to_string(),
   4564             "--summary".to_string(),
   4565             summary_path.display().to_string(),
   4566             "--lcov".to_string(),
   4567             lcov_path.display().to_string(),
   4568             "--out".to_string(),
   4569             out_path.display().to_string(),
   4570             "--fail-under-exec-lines".to_string(),
   4571             "50.0".to_string(),
   4572             "--fail-under-functions".to_string(),
   4573             "100.0".to_string(),
   4574             "--fail-under-regions".to_string(),
   4575             "100.0".to_string(),
   4576             "--fail-under-branches".to_string(),
   4577             "50.0".to_string(),
   4578         ];
   4579         report_gate(&args).expect("normalized report gate success");
   4580 
   4581         let report_raw = fs::read_to_string(&out_path).expect("read normalized report");
   4582         assert!(report_raw.contains("\"functions_percent\": 100.0"));
   4583         assert!(report_raw.contains("\"summary_regions_percent\": 100.0"));
   4584         assert!(report_raw.contains("\"pass\": true"));
   4585 
   4586         fs::remove_dir_all(root).expect("remove normalized report gate root");
   4587     }
   4588 
   4589     #[test]
   4590     fn report_gate_with_root_uses_scope_specific_override_thresholds() {
   4591         let root = temp_dir_path("report_gate_override_success");
   4592         let coverage_dir = root.join("contracts");
   4593         fs::create_dir_all(&coverage_dir).expect("create coverage dir");
   4594         write_file(
   4595             &coverage_dir.join("coverage.toml"),
   4596             "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n",
   4597         );
   4598 
   4599         let summary_path = root.join("summary.json");
   4600         let lcov_path = root.join("coverage.info");
   4601         let out_path = root.join("gate-report.json");
   4602         write_file(
   4603             &summary_path,
   4604             r#"{"data":[{"totals":{"functions":{"percent":80.0},"lines":{"percent":88.5},"regions":{"percent":70.0}}}]}"#,
   4605         );
   4606         write_file(&lcov_path, "DA:1,1\nLF:1\nLH:1\nBRDA:1,0,0,1\n");
   4607 
   4608         report_gate_with_root(
   4609             &[
   4610                 "--scope".to_string(),
   4611                 "radroots_a".to_string(),
   4612                 "--summary".to_string(),
   4613                 summary_path.display().to_string(),
   4614                 "--lcov".to_string(),
   4615                 lcov_path.display().to_string(),
   4616                 "--out".to_string(),
   4617                 out_path.display().to_string(),
   4618                 "--policy-gate".to_string(),
   4619             ],
   4620             &root,
   4621         )
   4622         .expect("report gate should honor override");
   4623 
   4624         let report_raw = fs::read_to_string(&out_path).expect("read override report");
   4625         assert!(report_raw.contains("\"functions\": 77.5"));
   4626         assert!(report_raw.contains("\"regions\": 66.5"));
   4627         assert!(report_raw.contains("\"branches_required\": false"));
   4628         assert!(report_raw.contains("\"pass\": true"));
   4629 
   4630         fs::remove_dir_all(root).expect("remove report gate override root");
   4631     }
   4632 
   4633     #[test]
   4634     fn report_gate_returns_error_on_failed_thresholds() {
   4635         let root = temp_dir_path("report_gate_fail");
   4636         let summary_path = root.join("summary.json");
   4637         let lcov_path = root.join("coverage.info");
   4638         let out_path = root.join("gate-report.json");
   4639         write_file(
   4640             &summary_path,
   4641             r#"{"data":[{"totals":{"functions":{"percent":10.0},"lines":{"percent":10.0},"regions":{"percent":10.0}}}]}"#,
   4642         );
   4643         write_file(&lcov_path, "DA:1,0\nBRDA:1,0,0,0\n");
   4644 
   4645         let args = vec![
   4646             "--scope".to_string(),
   4647             "crate-y".to_string(),
   4648             "--summary".to_string(),
   4649             summary_path.display().to_string(),
   4650             "--lcov".to_string(),
   4651             lcov_path.display().to_string(),
   4652             "--out".to_string(),
   4653             out_path.display().to_string(),
   4654             "--fail-under-exec-lines".to_string(),
   4655             "100.0".to_string(),
   4656             "--fail-under-functions".to_string(),
   4657             "100.0".to_string(),
   4658             "--fail-under-regions".to_string(),
   4659             "100.0".to_string(),
   4660             "--fail-under-branches".to_string(),
   4661             "100.0".to_string(),
   4662         ];
   4663         let err = report_gate(&args).expect_err("report gate failure");
   4664         assert!(err.contains("coverage gate failed"));
   4665         fs::remove_dir_all(root).expect("remove report gate failure root");
   4666     }
   4667 
   4668     #[test]
   4669     fn report_gate_handles_nan_threshold_input() {
   4670         let root = temp_dir_path("report_gate_nan");
   4671         let summary_path = root.join("summary.json");
   4672         let lcov_path = root.join("coverage.info");
   4673         let out_path = root.join("gate-report.json");
   4674         write_file(
   4675             &summary_path,
   4676             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   4677         );
   4678         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   4679 
   4680         let args = vec![
   4681             "--scope".to_string(),
   4682             "crate-nan".to_string(),
   4683             "--summary".to_string(),
   4684             summary_path.display().to_string(),
   4685             "--lcov".to_string(),
   4686             lcov_path.display().to_string(),
   4687             "--out".to_string(),
   4688             out_path.display().to_string(),
   4689             "--fail-under-functions".to_string(),
   4690             "NaN".to_string(),
   4691         ];
   4692         let err = report_gate(&args).expect_err("nan threshold should fail coverage gate");
   4693         assert!(err.contains("invalid --fail-under-functions value"));
   4694         fs::remove_dir_all(root).expect("remove report gate nan root");
   4695     }
   4696 
   4697     #[test]
   4698     fn report_gate_reports_write_failure() {
   4699         let root = temp_dir_path("report_gate_write_fail");
   4700         let summary_path = root.join("summary.json");
   4701         let lcov_path = root.join("coverage.info");
   4702         let out_path = root.join("gate-report.json");
   4703         write_file(
   4704             &summary_path,
   4705             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   4706         );
   4707         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   4708         fs::create_dir_all(&out_path).expect("create directory at output path");
   4709 
   4710         let args = vec![
   4711             "--scope".to_string(),
   4712             "crate-write".to_string(),
   4713             "--summary".to_string(),
   4714             summary_path.display().to_string(),
   4715             "--lcov".to_string(),
   4716             lcov_path.display().to_string(),
   4717             "--out".to_string(),
   4718             out_path.display().to_string(),
   4719             "--policy-gate".to_string(),
   4720         ];
   4721         let err = report_gate(&args).expect_err("writing report to directory should fail");
   4722         assert!(err.contains("failed to write"));
   4723         fs::remove_dir_all(root).expect("remove report gate write root");
   4724     }
   4725 
   4726     #[test]
   4727     fn report_gate_logs_branch_unavailable_path() {
   4728         let root = temp_dir_path("report_gate_no_branches");
   4729         let summary_path = root.join("summary.json");
   4730         let lcov_path = root.join("coverage.info");
   4731         let out_path = root.join("gate-report.json");
   4732         write_file(
   4733             &summary_path,
   4734             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   4735         );
   4736         write_file(&lcov_path, "DA:1,1\n");
   4737 
   4738         let args = vec![
   4739             "--scope".to_string(),
   4740             "crate-no-branch".to_string(),
   4741             "--summary".to_string(),
   4742             summary_path.display().to_string(),
   4743             "--lcov".to_string(),
   4744             lcov_path.display().to_string(),
   4745             "--out".to_string(),
   4746             out_path.display().to_string(),
   4747             "--fail-under-exec-lines".to_string(),
   4748             "100.0".to_string(),
   4749             "--fail-under-functions".to_string(),
   4750             "100.0".to_string(),
   4751             "--fail-under-regions".to_string(),
   4752             "100.0".to_string(),
   4753             "--fail-under-branches".to_string(),
   4754             "100.0".to_string(),
   4755         ];
   4756         report_gate(&args).expect("report gate no branches");
   4757         let report_raw = fs::read_to_string(&out_path).expect("read report");
   4758         assert!(report_raw.contains("\"branches_available\": false"));
   4759         fs::remove_dir_all(root).expect("remove no branch report root");
   4760     }
   4761 
   4762     #[test]
   4763     fn report_gate_reports_argument_and_input_errors() {
   4764         let missing_scope = report_gate(&[]).expect_err("missing scope");
   4765         assert!(missing_scope.contains("missing --scope"));
   4766 
   4767         let missing_summary = report_gate(&["--scope".to_string(), "crate".to_string()])
   4768             .expect_err("missing summary");
   4769         assert!(missing_summary.contains("missing --summary"));
   4770 
   4771         let missing_lcov = report_gate(&[
   4772             "--scope".to_string(),
   4773             "crate".to_string(),
   4774             "--summary".to_string(),
   4775             "summary.json".to_string(),
   4776         ])
   4777         .expect_err("missing lcov");
   4778         assert!(missing_lcov.contains("missing --lcov"));
   4779 
   4780         let missing_out = report_gate(&[
   4781             "--scope".to_string(),
   4782             "crate".to_string(),
   4783             "--summary".to_string(),
   4784             "summary.json".to_string(),
   4785             "--lcov".to_string(),
   4786             "coverage.info".to_string(),
   4787         ])
   4788         .expect_err("missing out");
   4789         assert!(missing_out.contains("missing --out"));
   4790 
   4791         let root = temp_dir_path("report_gate_arg_errors");
   4792         let summary_path = root.join("summary.json");
   4793         let lcov_path = root.join("coverage.info");
   4794         let out_path = root.join("gate-report.json");
   4795         write_file(
   4796             &summary_path,
   4797             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   4798         );
   4799         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   4800 
   4801         let invalid_functions = report_gate(&[
   4802             "--scope".to_string(),
   4803             "crate".to_string(),
   4804             "--summary".to_string(),
   4805             summary_path.display().to_string(),
   4806             "--lcov".to_string(),
   4807             lcov_path.display().to_string(),
   4808             "--out".to_string(),
   4809             out_path.display().to_string(),
   4810             "--fail-under-functions".to_string(),
   4811             "bad".to_string(),
   4812         ])
   4813         .expect_err("invalid functions threshold");
   4814         assert!(invalid_functions.contains("invalid --fail-under-functions value"));
   4815 
   4816         let invalid_exec = report_gate(&[
   4817             "--scope".to_string(),
   4818             "crate".to_string(),
   4819             "--summary".to_string(),
   4820             summary_path.display().to_string(),
   4821             "--lcov".to_string(),
   4822             lcov_path.display().to_string(),
   4823             "--out".to_string(),
   4824             out_path.display().to_string(),
   4825             "--fail-under-exec-lines".to_string(),
   4826             "bad".to_string(),
   4827         ])
   4828         .expect_err("invalid executable threshold");
   4829         assert!(invalid_exec.contains("invalid --fail-under-exec-lines value"));
   4830 
   4831         let invalid_regions = report_gate(&[
   4832             "--scope".to_string(),
   4833             "crate".to_string(),
   4834             "--summary".to_string(),
   4835             summary_path.display().to_string(),
   4836             "--lcov".to_string(),
   4837             lcov_path.display().to_string(),
   4838             "--out".to_string(),
   4839             out_path.display().to_string(),
   4840             "--fail-under-regions".to_string(),
   4841             "bad".to_string(),
   4842         ])
   4843         .expect_err("invalid regions threshold");
   4844         assert!(invalid_regions.contains("invalid --fail-under-regions value"));
   4845 
   4846         let invalid_branches = report_gate(&[
   4847             "--scope".to_string(),
   4848             "crate".to_string(),
   4849             "--summary".to_string(),
   4850             summary_path.display().to_string(),
   4851             "--lcov".to_string(),
   4852             lcov_path.display().to_string(),
   4853             "--out".to_string(),
   4854             out_path.display().to_string(),
   4855             "--fail-under-branches".to_string(),
   4856             "bad".to_string(),
   4857         ])
   4858         .expect_err("invalid branches threshold");
   4859         assert!(invalid_branches.contains("invalid --fail-under-branches value"));
   4860 
   4861         let missing_thresholds = report_gate(&[
   4862             "--scope".to_string(),
   4863             "crate".to_string(),
   4864             "--summary".to_string(),
   4865             summary_path.display().to_string(),
   4866             "--lcov".to_string(),
   4867             lcov_path.display().to_string(),
   4868             "--out".to_string(),
   4869             out_path.display().to_string(),
   4870         ])
   4871         .expect_err("missing thresholds");
   4872         assert!(missing_thresholds.contains("missing coverage thresholds"));
   4873 
   4874         let missing_functions = report_gate(&[
   4875             "--scope".to_string(),
   4876             "crate".to_string(),
   4877             "--summary".to_string(),
   4878             summary_path.display().to_string(),
   4879             "--lcov".to_string(),
   4880             lcov_path.display().to_string(),
   4881             "--out".to_string(),
   4882             out_path.display().to_string(),
   4883             "--fail-under-exec-lines".to_string(),
   4884             "100".to_string(),
   4885         ])
   4886         .expect_err("missing functions threshold");
   4887         assert!(missing_functions.contains("missing coverage thresholds"));
   4888 
   4889         let missing_regions = report_gate(&[
   4890             "--scope".to_string(),
   4891             "crate".to_string(),
   4892             "--summary".to_string(),
   4893             summary_path.display().to_string(),
   4894             "--lcov".to_string(),
   4895             lcov_path.display().to_string(),
   4896             "--out".to_string(),
   4897             out_path.display().to_string(),
   4898             "--fail-under-exec-lines".to_string(),
   4899             "100".to_string(),
   4900             "--fail-under-functions".to_string(),
   4901             "100".to_string(),
   4902         ])
   4903         .expect_err("missing regions threshold");
   4904         assert!(missing_regions.contains("missing coverage thresholds"));
   4905 
   4906         let missing_branches = report_gate(&[
   4907             "--scope".to_string(),
   4908             "crate".to_string(),
   4909             "--summary".to_string(),
   4910             summary_path.display().to_string(),
   4911             "--lcov".to_string(),
   4912             lcov_path.display().to_string(),
   4913             "--out".to_string(),
   4914             out_path.display().to_string(),
   4915             "--fail-under-exec-lines".to_string(),
   4916             "100".to_string(),
   4917             "--fail-under-functions".to_string(),
   4918             "100".to_string(),
   4919             "--fail-under-regions".to_string(),
   4920             "100".to_string(),
   4921         ])
   4922         .expect_err("missing branches threshold");
   4923         assert!(missing_branches.contains("missing coverage thresholds"));
   4924 
   4925         let missing_summary_file = report_gate(&[
   4926             "--scope".to_string(),
   4927             "crate".to_string(),
   4928             "--summary".to_string(),
   4929             root.join("missing-summary.json").display().to_string(),
   4930             "--lcov".to_string(),
   4931             lcov_path.display().to_string(),
   4932             "--out".to_string(),
   4933             out_path.display().to_string(),
   4934             "--policy-gate".to_string(),
   4935         ])
   4936         .expect_err("missing summary file should fail");
   4937         assert!(missing_summary_file.contains("failed to read summary"));
   4938 
   4939         let missing_gate_report = read_gate_report(&root.join("missing-gate-report.json"))
   4940             .expect_err("missing gate report should fail");
   4941         assert!(missing_gate_report.contains("failed to read gate report"));
   4942 
   4943         write_file(&out_path, "{not-json");
   4944         let invalid_gate_report = read_gate_report(&out_path).expect_err("invalid gate report");
   4945         assert!(invalid_gate_report.contains("failed to parse gate report"));
   4946 
   4947         let missing_lcov_file = report_gate(&[
   4948             "--scope".to_string(),
   4949             "crate".to_string(),
   4950             "--summary".to_string(),
   4951             summary_path.display().to_string(),
   4952             "--lcov".to_string(),
   4953             root.join("missing-lcov.info").display().to_string(),
   4954             "--out".to_string(),
   4955             out_path.display().to_string(),
   4956             "--policy-gate".to_string(),
   4957         ])
   4958         .expect_err("missing lcov file should fail");
   4959         assert!(missing_lcov_file.contains("failed to read lcov"));
   4960 
   4961         let mixed_policy_gate = report_gate(&[
   4962             "--scope".to_string(),
   4963             "crate".to_string(),
   4964             "--summary".to_string(),
   4965             summary_path.display().to_string(),
   4966             "--lcov".to_string(),
   4967             lcov_path.display().to_string(),
   4968             "--out".to_string(),
   4969             out_path.display().to_string(),
   4970             "--policy-gate".to_string(),
   4971             "--fail-under-functions".to_string(),
   4972             "100.0".to_string(),
   4973         ])
   4974         .expect_err("policy gate mixed with explicit thresholds");
   4975         assert!(mixed_policy_gate.contains("cannot be combined"));
   4976 
   4977         let mixed_policy_gate_regions = report_gate(&[
   4978             "--scope".to_string(),
   4979             "crate".to_string(),
   4980             "--summary".to_string(),
   4981             summary_path.display().to_string(),
   4982             "--lcov".to_string(),
   4983             lcov_path.display().to_string(),
   4984             "--out".to_string(),
   4985             out_path.display().to_string(),
   4986             "--policy-gate".to_string(),
   4987             "--fail-under-regions".to_string(),
   4988             "100.0".to_string(),
   4989         ])
   4990         .expect_err("policy gate mixed with regions threshold");
   4991         assert!(mixed_policy_gate_regions.contains("cannot be combined"));
   4992 
   4993         let mixed_policy_gate_branches_flag = report_gate(&[
   4994             "--scope".to_string(),
   4995             "crate".to_string(),
   4996             "--summary".to_string(),
   4997             summary_path.display().to_string(),
   4998             "--lcov".to_string(),
   4999             lcov_path.display().to_string(),
   5000             "--out".to_string(),
   5001             out_path.display().to_string(),
   5002             "--policy-gate".to_string(),
   5003             "--require-branches".to_string(),
   5004         ])
   5005         .expect_err("policy gate mixed with require-branches");
   5006         assert!(mixed_policy_gate_branches_flag.contains("cannot be combined"));
   5007 
   5008         fs::remove_dir_all(root).expect("remove report arg errors root");
   5009     }
   5010 
   5011     #[test]
   5012     fn coverage_ignore_filename_regex_reports_unknown_crate() {
   5013         let root = temp_dir_path("coverage_unknown_crate_root");
   5014         write_file(
   5015             &root.join("Cargo.toml"),
   5016             "[workspace]\nmembers = [\"crates/core\"]\n",
   5017         );
   5018         write_file(
   5019             &root.join("crates").join("core").join("Cargo.toml"),
   5020             "[package]\nname = \"radroots_core\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
   5021         );
   5022 
   5023         let err = coverage_ignore_filename_regex(&root, "radroots_missing")
   5024             .expect_err("unknown crate should fail");
   5025         assert!(err.contains("could not resolve crate directory"));
   5026 
   5027         fs::remove_dir_all(root).expect("remove unknown crate root");
   5028     }
   5029 
   5030     #[test]
   5031     fn coverage_ignore_filename_regex_reports_workspace_manifest_errors() {
   5032         let root = temp_dir_path("coverage_regex_workspace_error_root");
   5033         let read_err = coverage_ignore_filename_regex(&root, "radroots_core")
   5034             .expect_err("missing workspace manifest should fail");
   5035         assert!(read_err.contains("failed to read"));
   5036 
   5037         write_file(&root.join("Cargo.toml"), "[workspace");
   5038         let parse_err = coverage_ignore_filename_regex(&root, "radroots_core")
   5039             .expect_err("invalid workspace manifest should fail");
   5040         assert!(parse_err.contains("failed to parse"));
   5041 
   5042         fs::remove_dir_all(root).expect("remove workspace error root");
   5043     }
   5044 
   5045     #[test]
   5046     fn run_crate_with_runner_at_root_reports_ignore_filter_errors() {
   5047         let root = temp_dir_path("run_crate_ignore_filter_error");
   5048         write_file(
   5049             &root.join("Cargo.toml"),
   5050             "[workspace]\nmembers = [\"crates/other\"]\n",
   5051         );
   5052         write_file(
   5053             &root.join("crates").join("other").join("Cargo.toml"),
   5054             "[package]\nname = \"radroots_other\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
   5055         );
   5056         let args = vec![
   5057             "--crate".to_string(),
   5058             "radroots_core".to_string(),
   5059             "--out".to_string(),
   5060             root.join("target").join("coverage").display().to_string(),
   5061         ];
   5062         let mut runner = ok_runner;
   5063         let err = run_crate_with_runner_at_root(&args, &root, &mut runner)
   5064             .expect_err("missing crate coverage filter should fail");
   5065         assert!(err.contains("could not resolve crate directory"));
   5066 
   5067         fs::remove_dir_all(root).expect("remove run crate ignore filter root");
   5068     }
   5069 
   5070     #[test]
   5071     fn run_dispatches_subcommands_and_errors() {
   5072         run(&["help".to_string()]).expect("help subcommand");
   5073         run(&["required-crates".to_string()]).expect("required crates subcommand");
   5074         run(&["workspace-crates".to_string()]).expect("workspace crates subcommand");
   5075         let run_crate_err = run(&["run-crate".to_string()]).expect_err("run crate missing args");
   5076         assert!(run_crate_err.contains("missing --crate"));
   5077         let unknown_err = run(&["unknown".to_string()]).expect_err("unknown subcommand");
   5078         assert!(unknown_err.contains("unknown coverage subcommand"));
   5079         let missing_err = run(&[]).expect_err("missing subcommand");
   5080         assert!(missing_err.contains("missing coverage subcommand"));
   5081     }
   5082 
   5083     #[test]
   5084     fn list_root_helpers_report_missing_contract_files() {
   5085         let root = temp_dir_path("list_helper_missing");
   5086         fs::create_dir_all(&root).expect("create list helper root");
   5087         let mut output = Vec::new();
   5088         let required_err = list_required_crates_with_root(&root, &mut output)
   5089             .expect_err("missing required crates file should fail");
   5090         assert!(required_err.contains("failed to read coverage policy"));
   5091 
   5092         let workspace_err = list_workspace_crates_with_root(&root, &mut output)
   5093             .expect_err("missing workspace manifest should fail");
   5094         assert!(workspace_err.contains("failed to read"));
   5095 
   5096         fs::remove_dir_all(root).expect("remove list helper root");
   5097     }
   5098 
   5099     #[test]
   5100     fn write_crate_names_output_covers_success_and_error_paths() {
   5101         let mut output = Vec::new();
   5102         write_crate_names_output(
   5103             &mut output,
   5104             vec!["radroots_a".to_string(), "radroots_b".to_string()],
   5105             "required crates",
   5106         )
   5107         .expect("write crate names");
   5108         let rendered = String::from_utf8(output).expect("utf8");
   5109         assert!(rendered.contains("radroots_a"));
   5110         assert!(rendered.contains("radroots_b"));
   5111 
   5112         let mut failing = FailingWriter;
   5113         let err = write_crate_names_output(
   5114             &mut failing,
   5115             vec!["radroots_a".to_string()],
   5116             "workspace crates",
   5117         )
   5118         .expect_err("writer failure");
   5119         assert!(err.contains("failed to write workspace crates output"));
   5120         failing.flush().expect("flush failing writer");
   5121     }
   5122 
   5123     #[test]
   5124     fn run_report_subcommand_dispatches_to_report_gate() {
   5125         let root = temp_dir_path("run_dispatch_report");
   5126         let summary_path = root.join("summary.json");
   5127         let lcov_path = root.join("coverage.info");
   5128         let out_path = root.join("gate-report.json");
   5129         write_file(
   5130             &summary_path,
   5131             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   5132         );
   5133         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   5134 
   5135         run(&[
   5136             "report".to_string(),
   5137             "--scope".to_string(),
   5138             "dispatch".to_string(),
   5139             "--summary".to_string(),
   5140             summary_path.display().to_string(),
   5141             "--lcov".to_string(),
   5142             lcov_path.display().to_string(),
   5143             "--out".to_string(),
   5144             out_path.display().to_string(),
   5145             "--policy-gate".to_string(),
   5146         ])
   5147         .expect("dispatch report");
   5148         assert!(out_path.exists());
   5149         fs::remove_dir_all(root).expect("remove report dispatch root");
   5150     }
   5151 
   5152     #[test]
   5153     fn report_gate_with_root_reports_policy_read_errors() {
   5154         let root = temp_dir_path("report_gate_policy_root_error");
   5155         let summary_path = root.join("summary.json");
   5156         let lcov_path = root.join("coverage.info");
   5157         let out_path = root.join("gate-report.json");
   5158         write_file(
   5159             &summary_path,
   5160             r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
   5161         );
   5162         write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
   5163 
   5164         let err = report_gate_with_root(
   5165             &[
   5166                 "--scope".to_string(),
   5167                 "crate-x".to_string(),
   5168                 "--summary".to_string(),
   5169                 summary_path.display().to_string(),
   5170                 "--lcov".to_string(),
   5171                 lcov_path.display().to_string(),
   5172                 "--out".to_string(),
   5173                 out_path.display().to_string(),
   5174                 "--policy-gate".to_string(),
   5175             ],
   5176             &root,
   5177         )
   5178         .expect_err("missing policy should fail");
   5179         assert!(err.contains("failed to read coverage policy"));
   5180 
   5181         fs::remove_dir_all(root).expect("remove report gate policy error root");
   5182     }
   5183 }