lib

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

coverage.rs (216587B)


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