lib

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

commit 48f91406bb5c8c00df603e5443af841e08f27c0a
parent a56f203b325a094c6245a51aff05796d18944656
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Mar 2026 18:30:29 +0000

coverage: centralize strict coverage policy

Diffstat:
Mcontract/README.md | 8++++----
Mcontract/coverage/POLICY.md | 16++++++++++------
Acontract/coverage/policy.toml | 35+++++++++++++++++++++++++++++++++++
Dcontract/coverage/required-crates.toml | 32--------------------------------
Dcontract/coverage/rollout.toml | 135-------------------------------------------------------------------------------
Mcrates/xtask/src/contract.rs | 558+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/xtask/src/coverage.rs | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcrates/xtask/src/export_ts.rs | 24+++++++++++-------------
Mcrates/xtask/src/main.rs | 10+++++-----
Mdocs/nix.md | 2+-
Mnix/common.nix | 8++------
Mnix/toolchains.nix | 11+----------
Arust-toolchain-coverage.toml | 4++++
Mscripts/ci/release_preflight.sh | 8++------
14 files changed, 548 insertions(+), 561 deletions(-)

diff --git a/contract/README.md b/contract/README.md @@ -56,11 +56,11 @@ Repository guards also enforce: Coverage governance is defined under `contract/coverage/`: -- policy thresholds: `contract/coverage/POLICY.md` -- rollout order: `contract/coverage/rollout.toml` -- required crate list: `contract/coverage/required-crates.toml` +- machine-readable policy: `contract/coverage/policy.toml` +- human policy notes: `contract/coverage/POLICY.md` +- per-crate profiles: `contract/coverage/profiles.toml` -Required Rust crates are gated at `100/100/100` (exec lines, functions, branches). +Required Rust crates are gated at `100/100/100/100` (exec lines, functions, branches, regions), with branch records required. ## Release Policy diff --git a/contract/coverage/POLICY.md b/contract/coverage/POLICY.md @@ -1,6 +1,7 @@ # Radroots Core Libraries Rust Coverage Policy This document defines the required coverage gate for the Radroots Core Libraries Rust workspace. +The authoritative machine-readable contract is `contract/coverage/policy.toml`. ## gate contract @@ -25,13 +26,16 @@ All four thresholds are release-blocking. - a crate cannot be promoted to required unless it is at 100/100/100/100 - once required, the crate remains blocking on every pull request and push to `master` -## rollout contract +## required crate contract -- start with `radroots-core` as the first required crate -- expand required coverage crate-by-crate -- full workspace required coverage is only enabled after every required crate is green -- required blocking crate list is tracked in `contract/coverage/required-crates.toml` +- every workspace crate is required +- the required blocking crate list is tracked in `contract/coverage/policy.toml` +- workspace membership changes must update `contract/coverage/policy.toml` in the same change ## local override policy -Local override env vars may exist for smoke runs, but ci must run with default strict thresholds and required branch data. +Local override env vars may exist for smoke runs, but canonical release and coverage lanes must read the strict gate from `contract/coverage/policy.toml`. + +## toolchain pin + +The pinned nightly used for coverage lives in `rust-toolchain-coverage.toml`. diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml @@ -0,0 +1,35 @@ +[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = [ + "radroots-core", + "radroots-types", + "radroots-events", + "radroots-identity", + "radroots-trade", + "radroots-events-codec", + "radroots-events-codec-wasm", + "radroots-replica-db-schema", + "xtask", + "radroots-events-indexed", + "radroots-log", + "radroots-net-core", + "radroots-net", + "radroots-nostr", + "radroots-nostr-accounts", + "radroots-nostr-ndb", + "radroots-nostr-runtime", + "radroots-runtime", + "radroots-sql-core", + "radroots-sql-wasm-core", + "radroots-sql-wasm-bridge", + "radroots-replica-sync", + "radroots-replica-db", + "radroots-replica-sync-wasm", + "radroots-replica-db-wasm", +] diff --git a/contract/coverage/required-crates.toml b/contract/coverage/required-crates.toml @@ -1,32 +0,0 @@ -[required] -crates = [ - "radroots-core", - "radroots-types", - "radroots-events", - "radroots-identity", - "radroots-trade", - "radroots-events-codec", - "radroots-events-codec-wasm", - "radroots-replica-db-schema", - "xtask", - "radroots-events-indexed", - "radroots-log", - "radroots-net-core", - "radroots-net", - "radroots-nostr", - "radroots-nostr-accounts", - "radroots-nostr-ndb", - "radroots-nostr-runtime", - "radroots-runtime", - "radroots-sql-core", - "radroots-sql-wasm-core", - "radroots-sql-wasm-bridge", - "radroots-replica-sync", - "radroots-replica-db", - "radroots-replica-sync-wasm", - "radroots-replica-db-wasm", -] - -[policy] -mode = "blocking" -threshold_profile = "strict_100" diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml @@ -1,135 +0,0 @@ -[policy] -fail_under_exec_lines = 100.0 -fail_under_functions = 100.0 -fail_under_regions = 100.0 -fail_under_branches = 100.0 -require_branches = true - -[rollout] -strategy = "crate-by-crate" -entry_crate = "radroots-core" - -[[rollout.crates]] -name = "radroots-core" -status = "required" -order = 1 - -[[rollout.crates]] -name = "radroots-types" -status = "required" -order = 2 - -[[rollout.crates]] -name = "radroots-events" -status = "required" -order = 3 - -[[rollout.crates]] -name = "radroots-identity" -status = "required" -order = 4 - -[[rollout.crates]] -name = "radroots-trade" -status = "required" -order = 5 - -[[rollout.crates]] -name = "radroots-events-codec" -status = "required" -order = 6 - -[[rollout.crates]] -name = "radroots-events-codec-wasm" -status = "required" -order = 7 - -[[rollout.crates]] -name = "radroots-replica-db-schema" -status = "required" -order = 8 - -[[rollout.crates]] -name = "xtask" -status = "required" -order = 9 - -[[rollout.crates]] -name = "radroots-events-indexed" -status = "required" -order = 10 - -[[rollout.crates]] -name = "radroots-log" -status = "required" -order = 11 - -[[rollout.crates]] -name = "radroots-net-core" -status = "required" -order = 12 - -[[rollout.crates]] -name = "radroots-net" -status = "required" -order = 13 - -[[rollout.crates]] -name = "radroots-nostr" -status = "required" -order = 14 - -[[rollout.crates]] -name = "radroots-nostr-accounts" -status = "required" -order = 15 - -[[rollout.crates]] -name = "radroots-nostr-ndb" -status = "required" -order = 16 - -[[rollout.crates]] -name = "radroots-nostr-runtime" -status = "required" -order = 17 - -[[rollout.crates]] -name = "radroots-runtime" -status = "required" -order = 18 - -[[rollout.crates]] -name = "radroots-sql-core" -status = "required" -order = 19 - -[[rollout.crates]] -name = "radroots-sql-wasm-core" -status = "required" -order = 20 - -[[rollout.crates]] -name = "radroots-sql-wasm-bridge" -status = "required" -order = 21 - -[[rollout.crates]] -name = "radroots-replica-sync" -status = "required" -order = 22 - -[[rollout.crates]] -name = "radroots-replica-db" -status = "required" -order = 23 - -[[rollout.crates]] -name = "radroots-replica-sync-wasm" -status = "required" -order = 24 - -[[rollout.crates]] -name = "radroots-replica-db-wasm" -status = "required" -order = 25 diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] +use crate::coverage::{read_coverage_policy, CoverageThresholds}; use serde::Deserialize; use std::collections::{BTreeMap, BTreeSet}; use std::fs; @@ -117,28 +118,13 @@ enum PackagePublish { Registries(Vec<String>), } -#[derive(Debug, Deserialize)] -struct CoverageRolloutFile { - rollout: CoverageRolloutSection, -} - -#[derive(Debug, Deserialize)] -struct CoverageRolloutSection { - crates: Vec<CoverageRolloutCrate>, -} - -#[derive(Debug, Deserialize)] -struct CoverageRolloutCrate { - name: String, - status: String, - order: u32, -} - +#[cfg_attr(not(test), allow(dead_code))] #[derive(Debug, Deserialize)] struct CoverageRequiredFile { required: CoverageRequiredSection, } +#[cfg_attr(not(test), allow(dead_code))] #[derive(Debug, Deserialize)] struct CoverageRequiredSection { crates: Vec<String>, @@ -180,6 +166,7 @@ fn contract_root(workspace_root: &Path) -> PathBuf { #[derive(Debug)] struct WorkspacePackageRecord { name: String, + #[cfg_attr(not(test), allow(dead_code))] manifest_path: PathBuf, publish_enabled: bool, manifest_value: toml::Value, @@ -222,6 +209,7 @@ fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String> .collect()) } +#[cfg_attr(not(test), allow(dead_code))] fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, PathBuf>, String> { let mut manifests = BTreeMap::new(); for record in workspace_package_records(workspace_root)? { @@ -235,12 +223,18 @@ fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, Ok(manifests) } -fn load_coverage_rollout(contract_root: &Path) -> Result<CoverageRolloutFile, String> { - parse_toml::<CoverageRolloutFile>(&contract_root.join("coverage").join("rollout.toml")) +#[cfg_attr(not(test), allow(dead_code))] +fn load_coverage_required(contract_root: &Path) -> Result<CoverageRequiredFile, String> { + let policy = load_coverage_policy(contract_root)?; + Ok(CoverageRequiredFile { + required: CoverageRequiredSection { + crates: policy.required_crates()?, + }, + }) } -fn load_coverage_required(contract_root: &Path) -> Result<CoverageRequiredFile, String> { - parse_toml::<CoverageRequiredFile>(&contract_root.join("coverage").join("required-crates.toml")) +fn load_coverage_policy(contract_root: &Path) -> Result<crate::coverage::CoveragePolicyFile, String> { + read_coverage_policy(&contract_root.join("coverage").join("policy.toml")) } fn load_release_contract(contract_root: &Path) -> Result<ReleaseContractFile, String> { @@ -417,7 +411,16 @@ fn load_coverage_refresh_rows( let func = parse_coverage_percent(parts[3], "func", &crate_name)?; let branch = parse_coverage_percent(parts[4], "branch", &crate_name)?; let region = parse_coverage_percent(parts[5], "region", &crate_name)?; - rows.insert(crate_name, (status, exec, func, branch, region)); + if rows + .insert(crate_name.clone(), (status, exec, func, branch, region)) + .is_some() + { + return Err(format!( + "duplicate coverage row for crate {} in {}", + crate_name, + report_path.display() + )); + } } Ok(rows) } @@ -425,6 +428,7 @@ fn load_coverage_refresh_rows( fn validate_required_coverage_summary( workspace_root: &Path, required_crates: &BTreeSet<String>, + thresholds: CoverageThresholds, ) -> Result<(), String> { let rows = load_coverage_refresh_rows(workspace_root)?; for crate_name in required_crates { @@ -440,10 +444,22 @@ fn validate_required_coverage_summary( crate_name, status )); } - if *exec < 100.0 || *func < 100.0 || *branch < 100.0 || *region < 100.0 { + if *exec < thresholds.fail_under_exec_lines + || *func < thresholds.fail_under_functions + || *branch < thresholds.fail_under_branches + || *region < thresholds.fail_under_regions + { return Err(format!( - "required coverage crate {} must be 100/100/100/100, found {}/{}/{}/{}", - crate_name, exec, func, branch, region + "required coverage crate {} must satisfy coverage policy {},{},{},{}, found {}/{}/{}/{}", + crate_name, + thresholds.fail_under_exec_lines, + thresholds.fail_under_functions, + thresholds.fail_under_branches, + thresholds.fail_under_regions, + exec, + func, + branch, + region )); } } @@ -540,100 +556,43 @@ fn validate_core_unit_dimension_variant_order(workspace_root: &Path) -> Result<( Ok(()) } -fn validate_coverage_rollout_parity( +fn validate_coverage_policy_parity( workspace_root: &Path, contract_root: &Path, ) -> Result<(), String> { let workspace_packages = workspace_package_names(workspace_root)? .into_iter() .collect::<BTreeSet<_>>(); - let rollout = load_coverage_rollout(contract_root)?; - if rollout.rollout.crates.is_empty() { - return Err("coverage rollout crates list must not be empty".to_string()); - } - let mut rollout_packages = BTreeSet::new(); - let mut rollout_status = BTreeMap::new(); - let mut orders = Vec::with_capacity(rollout.rollout.crates.len()); - for entry in &rollout.rollout.crates { - if !matches!(entry.status.as_str(), "required" | "planned") { - return Err(format!( - "coverage rollout status must be required or planned for {}", - entry.name - )); - } - if !rollout_packages.insert(entry.name.clone()) { - return Err(format!("duplicate coverage rollout crate {}", entry.name)); - } - rollout_status.insert(entry.name.clone(), entry.status.clone()); - orders.push(entry.order); - } - let mut sorted_orders = orders; - sorted_orders.sort_unstable(); - for (index, order) in sorted_orders.iter().enumerate() { - let expected = (index + 1) as u32; - if *order != expected { - return Err(format!( - "coverage rollout order must be contiguous from 1; expected {expected} but found {order}" - )); - } + let policy = load_coverage_policy(contract_root)?; + let thresholds = policy.thresholds(); + if thresholds.fail_under_exec_lines != 100.0 + || thresholds.fail_under_functions != 100.0 + || thresholds.fail_under_regions != 100.0 + || thresholds.fail_under_branches != 100.0 + || !thresholds.require_branches + { + return Err( + "coverage policy must enforce 100/100/100/100 with required branches".to_string(), + ); } - if workspace_packages != rollout_packages { + let required_packages = policy.required_crates()?.into_iter().collect::<BTreeSet<_>>(); + if workspace_packages != required_packages { let missing = workspace_packages - .difference(&rollout_packages) + .difference(&required_packages) .cloned() .collect::<BTreeSet<_>>(); - let extra = rollout_packages + let extra = required_packages .difference(&workspace_packages) .cloned() .collect::<BTreeSet<_>>(); return Err(format!( - "coverage rollout missing workspace crates: {}; coverage rollout includes unknown crates: {}", + "coverage policy missing workspace crates: {}; coverage policy includes unknown crates: {}", join_set(&missing), join_set(&extra) )); } - let required = load_coverage_required(contract_root)?; - if required.required.crates.is_empty() { - return Err("coverage required crates list must not be empty".to_string()); - } - let mut required_set = BTreeSet::new(); - for crate_name in &required.required.crates { - if !required_set.insert(crate_name.clone()) { - return Err(format!("duplicate coverage required crate {}", crate_name)); - } - if !workspace_packages.contains(crate_name) { - return Err(format!( - "coverage required crate is not a workspace crate: {}", - crate_name - )); - } - let status = &rollout_status[crate_name]; - if status != "required" { - return Err(format!( - "coverage required crate {} must have rollout status required, found {}", - crate_name, status - )); - } - } - - let rollout_required = rollout_status - .iter() - .filter(|(_, status)| *status == "required") - .map(|(name, _)| name.clone()) - .collect::<BTreeSet<_>>(); - if rollout_required != required_set { - let missing = rollout_required - .difference(&required_set) - .cloned() - .collect::<BTreeSet<_>>(); - return Err(format!( - "coverage required list missing rollout required crates: {}", - join_set(&missing) - )); - } - Ok(()) } @@ -761,14 +720,14 @@ pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> { validate_contract_bundle(&bundle)?; let release = load_release_contract(&bundle.root).expect("validated contract includes release metadata"); - let required = load_coverage_required(&bundle.root) + let policy = load_coverage_policy(&bundle.root) .expect("validated contract includes coverage metadata"); let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates") .expect("validated contract enforces unique publish.crates"); - let required_crates = collect_unique_set(&required.required.crates, "required.crates") + let required_crates = collect_unique_set(&policy.required_crates()?, "required.crates") .expect("validated contract enforces unique required.crates"); validate_publish_package_metadata(workspace_root, &publish_crates)?; - validate_required_coverage_summary(workspace_root, &required_crates)?; + validate_required_coverage_summary(workspace_root, &required_crates, policy.thresholds())?; Ok(()) } @@ -895,7 +854,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { .parent() .expect("contract root must have a workspace parent"); validate_core_unit_dimension_variant_order(workspace_root)?; - validate_coverage_rollout_parity(workspace_root, &bundle.root)?; + validate_coverage_policy_parity(workspace_root, &bundle.root)?; validate_release_publish_policy( workspace_root, &bundle.root, @@ -935,6 +894,16 @@ mod tests { fs::write(path, content).expect("write file"); } + fn strict_thresholds() -> CoverageThresholds { + CoverageThresholds { + fail_under_exec_lines: 100.0, + fail_under_functions: 100.0, + fail_under_regions: 100.0, + fail_under_branches: 100.0, + require_branches: true, + } + } + fn create_synthetic_workspace(prefix: &str) -> PathBuf { let root = temp_root(prefix); write_file( @@ -1028,21 +997,16 @@ manifest_file = "export-manifest.json" "#, ); write_file( - &root.join("contract").join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] -"#, - ); - write_file( - &root - .join("contract") - .join("coverage") - .join("required-crates.toml"), - r#"[required] -crates = ["radroots-a"] + &root.join("contract").join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-b"] "#, ); write_file( @@ -1068,7 +1032,7 @@ crates = ["radroots-a"] .join("target") .join("coverage") .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots-b\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ); root } @@ -1171,24 +1135,23 @@ pub enum RadrootsCoreUnitDimension { } #[test] - fn coverage_rollout_includes_workspace_crates() { + fn coverage_policy_includes_workspace_crates() { let root = workspace_root(); let workspace_names = workspace_package_names(&root) .expect("workspace crates") .into_iter() .collect::<BTreeSet<_>>(); - let rollout = load_coverage_rollout(&root.join("contract")).expect("coverage rollout"); - let rollout_names = rollout - .rollout - .crates - .iter() - .map(|entry| entry.name.clone()) + let policy = load_coverage_policy(&root.join("contract")).expect("coverage policy"); + let required_names = policy + .required_crates() + .expect("required crates") + .into_iter() .collect::<BTreeSet<_>>(); - assert_eq!(workspace_names, rollout_names); + assert_eq!(workspace_names, required_names); } #[test] - fn coverage_required_crates_match_rollout_required_status() { + fn coverage_required_crates_match_policy_required_status() { let root = workspace_root(); let contract_root = root.join("contract"); let required = load_coverage_required(&contract_root).expect("coverage required"); @@ -1197,15 +1160,13 @@ pub enum RadrootsCoreUnitDimension { .crates .into_iter() .collect::<BTreeSet<_>>(); - let rollout = load_coverage_rollout(&contract_root).expect("coverage rollout"); - let rollout_required = rollout - .rollout - .crates - .iter() - .filter(|entry| entry.status == "required") - .map(|entry| entry.name.clone()) + let policy = load_coverage_policy(&contract_root).expect("coverage policy"); + let policy_required = policy + .required_crates() + .expect("policy required crates") + .into_iter() .collect::<BTreeSet<_>>(); - assert_eq!(required_names, rollout_required); + assert_eq!(required_names, policy_required); } #[test] @@ -1230,34 +1191,35 @@ pub enum RadrootsCoreUnitDimension { let required = ["radroots-core".to_string()] .into_iter() .collect::<BTreeSet<_>>(); - validate_required_coverage_summary(&root, &required).expect("coverage summary"); + validate_required_coverage_summary(&root, &required, strict_thresholds()) + .expect("coverage summary"); fs::write( coverage_dir.join("coverage-refresh.tsv"), "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t99.9\t100.0\t100.0\tfile\n", ) .expect("write function coverage file"); - let func_err = validate_required_coverage_summary(&root, &required) + let func_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) .expect_err("function coverage below 100"); - assert!(func_err.contains("must be 100/100/100/100")); + assert!(func_err.contains("must satisfy coverage policy")); fs::write( coverage_dir.join("coverage-refresh.tsv"), "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t99.9\t100.0\tfile\n", ) .expect("write branch coverage file"); - let branch_err = validate_required_coverage_summary(&root, &required) + let branch_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) .expect_err("branch coverage below 100"); - assert!(branch_err.contains("must be 100/100/100/100")); + assert!(branch_err.contains("must satisfy coverage policy")); fs::write( coverage_dir.join("coverage-refresh.tsv"), "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\t99.9\tfile\n", ) .expect("write region coverage file"); - let region_err = validate_required_coverage_summary(&root, &required) + let region_err = validate_required_coverage_summary(&root, &required, strict_thresholds()) .expect_err("region coverage below 100"); - assert!(region_err.contains("must be 100/100/100/100")); + assert!(region_err.contains("must satisfy coverage policy")); let _ = fs::remove_dir_all(&root); } @@ -1390,21 +1352,21 @@ members = ["crates/a", "crates/b"] let required = ["radroots-a".to_string()] .into_iter() .collect::<BTreeSet<_>>(); - let non_pass = - validate_required_coverage_summary(&root, &required).expect_err("non-pass status"); + let non_pass = validate_required_coverage_summary(&root, &required, strict_thresholds()) + .expect_err("non-pass status"); assert!(non_pass.contains("non-pass status")); write_file( &coverage_dir.join("coverage-refresh.tsv"), "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t99.9\t100\t100\t100\tfile\n", ); - let below_100 = - validate_required_coverage_summary(&root, &required).expect_err("coverage below 100"); - assert!(below_100.contains("must be 100/100/100/100")); + let below_100 = validate_required_coverage_summary(&root, &required, strict_thresholds()) + .expect_err("coverage below 100"); + assert!(below_100.contains("must satisfy coverage policy")); let missing = ["missing".to_string()].into_iter().collect::<BTreeSet<_>>(); - let missing_err = - validate_required_coverage_summary(&root, &missing).expect_err("missing required row"); + let missing_err = validate_required_coverage_summary(&root, &missing, strict_thresholds()) + .expect_err("missing required row"); assert!(missing_err.contains("missing from coverage-refresh.tsv")); let _ = fs::remove_dir_all(root); @@ -1450,109 +1412,111 @@ members = ["crates/a", "crates/b"] } #[test] - fn coverage_rollout_parity_reports_contract_errors() { - let root = create_synthetic_workspace("rollout_errors"); + fn coverage_policy_parity_reports_contract_errors() { + let root = create_synthetic_workspace("coverage_policy_errors"); let contract_root = root.join("contract"); write_file( - &contract_root.join("coverage").join("rollout.toml"), - "[rollout]\ncrates = []\n", - ); - let empty_rollout = - validate_coverage_rollout_parity(&root, &contract_root).expect_err("empty rollout"); - assert!(empty_rollout.contains("must not be empty")); + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true - write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "invalid", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] +[required] +crates = [] "#, ); - let invalid_status = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("invalid rollout status"); - assert!(invalid_status.contains("status must be required or planned")); + let empty_required = + validate_coverage_policy_parity(&root, &contract_root).expect_err("empty required"); + assert!(empty_required.contains("required crates list must not be empty")); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-a", status = "planned", order = 2 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 99.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-b"] "#, ); - let duplicate = - validate_coverage_rollout_parity(&root, &contract_root).expect_err("duplicate rollout"); - assert!(duplicate.contains("duplicate coverage rollout crate")); + let invalid_gate = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("invalid policy thresholds"); + assert!(invalid_gate.contains("100/100/100/100")); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "planned", order = 3 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-a"] "#, ); - let bad_order = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("non-contiguous rollout order"); - assert!(bad_order.contains("must be contiguous from 1")); + let duplicate_required = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("duplicate required crate"); + assert!(duplicate_required.contains("duplicate crate")); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = false + +[required] +crates = ["radroots-a", "radroots-b"] "#, ); - let missing_workspace = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("missing workspace crate in rollout"); - assert!(missing_workspace.contains("missing workspace crates")); + let branches_optional = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("branches must be required"); + assert!(branches_optional.contains("required branches")); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a"] "#, ); - write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = []\n", - ); - let required_empty = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("empty required list"); - assert!(required_empty.contains("required crates list must not be empty")); + let missing_workspace = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("missing workspace crate in policy"); + assert!(missing_workspace.contains("missing workspace crates")); write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"radroots-a\", \"radroots-a\"]\n", - ); - let required_duplicate = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("duplicate required crate"); - assert!(required_duplicate.contains("duplicate coverage required crate")); + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true - write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"unknown\"]\n", +[required] +crates = ["unknown"] +"#, ); - let required_unknown = validate_coverage_rollout_parity(&root, &contract_root) + let required_unknown = validate_coverage_policy_parity(&root, &contract_root) .expect_err("unknown required crate"); - assert!(required_unknown.contains("not a workspace crate")); - - write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"radroots-b\"]\n", - ); - let required_status = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("required status mismatch"); - assert!(required_status.contains("must have rollout status required")); + assert!(required_unknown.contains("includes unknown crates")); let _ = fs::remove_dir_all(root); } @@ -2046,38 +2010,33 @@ publish = false } #[test] - fn rollout_release_and_bundle_loaders_report_parse_and_read_errors() { - let root = create_synthetic_workspace("rollout_release_loader_errors"); + fn coverage_release_and_bundle_loaders_report_parse_and_read_errors() { + let root = create_synthetic_workspace("coverage_release_loader_errors"); let contract_root = root.join("contract"); - let missing_workspace = temp_root("rollout_missing_workspace_manifest"); - let rollout_workspace_err = - validate_coverage_rollout_parity(&missing_workspace, &contract_root) - .expect_err("rollout workspace manifest read error"); - assert!(rollout_workspace_err.contains("Cargo.toml")); + let missing_workspace = temp_root("coverage_missing_workspace_manifest"); + let policy_workspace_err = + validate_coverage_policy_parity(&missing_workspace, &contract_root) + .expect_err("coverage workspace manifest read error"); + assert!(policy_workspace_err.contains("Cargo.toml")); let _ = fs::remove_dir_all(&missing_workspace); - let _ = fs::remove_file(contract_root.join("coverage").join("rollout.toml")); - let rollout_load_err = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("rollout read error"); - assert!(rollout_load_err.contains("rollout.toml")); - write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] -"#, - ); - - let _ = fs::remove_file(contract_root.join("coverage").join("required-crates.toml")); - let required_load_err = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("required read error"); - assert!(required_load_err.contains("required-crates.toml")); + let _ = fs::remove_file(contract_root.join("coverage").join("policy.toml")); + let policy_load_err = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("coverage policy read error"); + assert!(policy_load_err.contains("policy.toml")); write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"radroots-a\"]\n", + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-b"] +"#, ); let missing_release = temp_root("release_missing_workspace_manifest"); @@ -2224,11 +2183,11 @@ require_conformance_vectors = true missing_required .join("contract") .join("coverage") - .join("required-crates.toml"), + .join("policy.toml"), ); let missing_required_err = validate_release_preflight(&missing_required).expect_err("missing required list"); - assert!(missing_required_err.contains("required-crates.toml")); + assert!(missing_required_err.contains("policy.toml")); let _ = fs::remove_dir_all(&missing_required); let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); @@ -2260,12 +2219,12 @@ crates = ["radroots-a"] &duplicate_required .join("contract") .join("coverage") - .join("required-crates.toml"), - "[required]\ncrates = [\"radroots-a\", \"radroots-a\"]\n", + .join("policy.toml"), + "[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-a\"]\n", ); let duplicate_required_err = validate_release_preflight(&duplicate_required).expect_err("duplicate required crates"); - assert!(duplicate_required_err.contains("duplicate coverage required crate")); + assert!(duplicate_required_err.contains("duplicate crate")); let _ = fs::remove_dir_all(&duplicate_required); let publish_metadata = create_synthetic_workspace("preflight_publish_metadata"); @@ -2297,8 +2256,8 @@ edition = "2024" } #[test] - fn load_contract_bundle_and_validation_report_version_export_and_rollout_errors() { - let root = create_synthetic_workspace("bundle_version_export_and_rollout_errors"); + fn load_contract_bundle_and_validation_report_version_export_and_coverage_errors() { + let root = create_synthetic_workspace("bundle_version_export_and_coverage_errors"); write_file(&root.join("contract").join("version.toml"), "[contract"); let version_parse_err = load_contract_bundle(&root).expect_err("invalid version file"); assert!(version_parse_err.contains("version.toml")); @@ -2366,16 +2325,20 @@ Volume, "#, ); write_file( - &root.join("contract").join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "invalid", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] + &root.join("contract").join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = false + +[required] +crates = ["radroots-a", "radroots-b"] "#, ); - let rollout_err = validate_contract_bundle(&bundle).expect_err("rollout validation"); - assert!(rollout_err.contains("status must be required or planned")); + let policy_err = validate_contract_bundle(&bundle).expect_err("coverage policy validation"); + assert!(policy_err.contains("100/100/100/100")); let _ = fs::remove_dir_all(&root); } @@ -2409,7 +2372,7 @@ crates = [ .into_iter() .collect::<BTreeSet<_>>(); let missing_refresh_err = - validate_required_coverage_summary(&missing_refresh_root, &required) + validate_required_coverage_summary(&missing_refresh_root, &required, strict_thresholds()) .expect_err("missing refresh should fail"); assert!(missing_refresh_err.contains("coverage-refresh.tsv")); let _ = fs::remove_dir_all(&missing_refresh_root); @@ -2515,44 +2478,43 @@ Volume, } #[test] - fn rollout_and_release_additional_error_branches_are_reported() { - let root = create_synthetic_workspace("rollout_release_extra_errors"); + fn coverage_and_release_additional_error_branches_are_reported() { + let root = create_synthetic_workspace("coverage_release_extra_errors"); let contract_root = root.join("contract"); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "required", order = 2 }, - { name = "radroots-extra", status = "planned", order = 3 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-b", "radroots-extra"] "#, ); - write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"radroots-a\"]\n", - ); - let rollout_extra = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("rollout unknown crate"); - assert!(rollout_extra.contains("includes unknown crates")); + let coverage_extra = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("coverage unknown crate"); + assert!(coverage_extra.contains("includes unknown crates")); write_file( - &contract_root.join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "required", order = 2 }, -] + &contract_root.join("coverage").join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a"] "#, ); - write_file( - &contract_root.join("coverage").join("required-crates.toml"), - "[required]\ncrates = [\"radroots-a\"]\n", - ); - let required_list_mismatch = validate_coverage_rollout_parity(&root, &contract_root) - .expect_err("required list must include rollout required crates"); - assert!(required_list_mismatch.contains("missing rollout required crates")); + let required_list_mismatch = validate_coverage_policy_parity(&root, &contract_root) + .expect_err("required list must match workspace crates"); + assert!(required_list_mismatch.contains("missing workspace crates")); write_file( &contract_root.join("release").join("publish-set.toml"), diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -119,12 +119,25 @@ struct LlvmCovSummaryMetric { } #[derive(Debug, Deserialize)] -struct CoverageRequiredContract { +#[serde(deny_unknown_fields)] +pub(crate) struct CoveragePolicyFile { + gate: CoveragePolicyGate, required: CoverageRequiredList, } #[derive(Debug, Deserialize)] -struct CoverageRequiredList { +#[serde(deny_unknown_fields)] +pub(crate) struct CoveragePolicyGate { + fail_under_exec_lines: f64, + fail_under_functions: f64, + fail_under_regions: f64, + fail_under_branches: f64, + require_branches: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageRequiredList { crates: Vec<String>, } @@ -197,40 +210,79 @@ pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { }) } -fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { +impl CoveragePolicyFile { + pub(crate) fn thresholds(&self) -> CoverageThresholds { + CoverageThresholds { + fail_under_exec_lines: self.gate.fail_under_exec_lines, + fail_under_functions: self.gate.fail_under_functions, + fail_under_regions: self.gate.fail_under_regions, + fail_under_branches: self.gate.fail_under_branches, + require_branches: self.gate.require_branches, + } + } + + pub(crate) fn required_crates(&self) -> Result<Vec<String>, String> { + if self.required.crates.is_empty() { + return Err("coverage required crates list must not be empty".to_string()); + } + let mut seen = BTreeSet::new(); + for crate_name in &self.required.crates { + if crate_name.trim().is_empty() { + return Err("coverage required crates list includes an empty crate name".to_string()); + } + if !seen.insert(crate_name.clone()) { + return Err(format!( + "coverage required crates list includes duplicate crate {crate_name}" + )); + } + } + Ok(self.required.crates.clone()) + } +} + +pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf { + root.join("contract").join("coverage").join("policy.toml") +} + +pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> { let raw = match fs::read_to_string(path) { Ok(raw) => raw, Err(err) => { return Err(format!( - "failed to read required crates {}: {err}", + "failed to read coverage policy {}: {err}", path.display() )); } }; - let parsed: CoverageRequiredContract = match toml::from_str(&raw) { + let parsed: CoveragePolicyFile = match toml::from_str(&raw) { Ok(parsed) => parsed, Err(err) => { return Err(format!( - "failed to parse required crates {}: {err}", + "failed to parse coverage policy {}: {err}", path.display() )); } }; - if parsed.required.crates.is_empty() { - return Err("coverage required crates list must not be empty".to_string()); - } - let mut seen = BTreeSet::new(); - for crate_name in &parsed.required.crates { - if crate_name.trim().is_empty() { - return Err("coverage required crates list includes an empty crate name".to_string()); + let thresholds = parsed.thresholds(); + for (label, value) in [ + ("fail_under_exec_lines", thresholds.fail_under_exec_lines), + ("fail_under_functions", thresholds.fail_under_functions), + ("fail_under_regions", thresholds.fail_under_regions), + ("fail_under_branches", thresholds.fail_under_branches), + ] { + if !value.is_finite() { + return Err(format!("coverage policy {label} must be finite")); } - if !seen.insert(crate_name.clone()) { - return Err(format!( - "coverage required crates list includes duplicate crate {crate_name}" - )); + if !(0.0..=100.0).contains(&value) { + return Err(format!("coverage policy {label} must be within 0..=100")); } } - Ok(parsed.required.crates) + parsed.required_crates()?; + Ok(parsed) +} + +fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { + read_coverage_policy(path)?.required_crates() } fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> { @@ -567,6 +619,20 @@ fn parse_optional_string_arg(args: &[String], name: &str) -> Option<String> { None } +fn parse_optional_f64_arg(args: &[String], name: &str) -> Result<Option<f64>, String> { + if let Some(raw) = parse_optional_string_arg(args, name) { + let parsed = raw + .parse::<f64>() + .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?; + if !parsed.is_finite() { + return Err(format!("invalid --{name} value `{raw}`: must be finite")); + } + return Ok(Some(parsed)); + } + Ok(None) +} + +#[cfg_attr(not(test), allow(dead_code))] fn parse_f64_arg(args: &[String], name: &str, default: f64) -> Result<f64, String> { if let Some(raw) = parse_optional_string_arg(args, name) { return raw @@ -591,6 +657,11 @@ fn parse_bool_flag(args: &[String], name: &str) -> bool { args.iter().any(|arg| arg == &flag) } +fn has_flag(args: &[String], name: &str) -> bool { + let flag = format!("--{name}"); + args.iter().any(|arg| arg == &flag) +} + fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf { if let Some(raw) = override_root { let trimmed = raw.trim(); @@ -756,12 +827,57 @@ fn report_gate(args: &[String]) -> Result<(), String> { let summary_path = PathBuf::from(parse_string_arg(args, "summary")?); let lcov_path = PathBuf::from(parse_string_arg(args, "lcov")?); let out_path = PathBuf::from(parse_string_arg(args, "out")?); - let thresholds = CoverageThresholds { - fail_under_exec_lines: parse_f64_arg(args, "fail-under-exec-lines", 100.0)?, - fail_under_functions: parse_f64_arg(args, "fail-under-functions", 100.0)?, - fail_under_regions: parse_f64_arg(args, "fail-under-regions", 100.0)?, - fail_under_branches: parse_f64_arg(args, "fail-under-branches", 100.0)?, - require_branches: parse_bool_flag(args, "require-branches"), + let policy_gate = parse_bool_flag(args, "policy-gate"); + let explicit_exec = parse_optional_f64_arg(args, "fail-under-exec-lines")?; + let explicit_functions = parse_optional_f64_arg(args, "fail-under-functions")?; + let explicit_regions = parse_optional_f64_arg(args, "fail-under-regions")?; + let explicit_branches = parse_optional_f64_arg(args, "fail-under-branches")?; + let explicit_require_branches = has_flag(args, "require-branches"); + let any_explicit_threshold = explicit_exec.is_some() + || explicit_functions.is_some() + || explicit_regions.is_some() + || explicit_branches.is_some(); + let thresholds = if policy_gate { + if any_explicit_threshold || explicit_require_branches { + return Err( + "--policy-gate cannot be combined with explicit threshold or branch flags" + .to_string(), + ); + } + let root = workspace_root(); + read_coverage_policy(&coverage_policy_path(&root))?.thresholds() + } else { + let Some(fail_under_exec_lines) = explicit_exec else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_functions) = explicit_functions else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_regions) = explicit_regions else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_branches) = explicit_branches else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + CoverageThresholds { + fail_under_exec_lines, + fail_under_functions, + fail_under_regions, + fail_under_branches, + require_branches: explicit_require_branches, + } }; let summary = read_summary(&summary_path)?; @@ -843,10 +959,7 @@ fn report_gate(args: &[String]) -> Result<(), String> { } fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { - let required_path = root - .join("contract") - .join("coverage") - .join("required-crates.toml"); + let required_path = coverage_policy_path(root); let crates = read_required_crates(&required_path)?; write_crate_names_output(writer, crates, "required crates") } @@ -1054,14 +1167,21 @@ mod tests { #[test] fn reads_required_crates_and_rejects_duplicates() { let path = temp_file_path("required_crates"); - fs::write(&path, "[required]\ncrates = [\"a\", \"b\"]\n").expect("write required crates"); + fs::write( + &path, + "[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", + ) + .expect("write required crates"); let crates = read_required_crates(&path).expect("parse required crates"); assert_eq!(crates, vec!["a".to_string(), "b".to_string()]); fs::remove_file(&path).expect("remove required crates"); let dup_path = temp_file_path("required_crates_dup"); - fs::write(&dup_path, "[required]\ncrates = [\"a\", \"a\"]\n") - .expect("write dup required crates"); + fs::write( + &dup_path, + "[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", + ) + .expect("write dup required crates"); let err = read_required_crates(&dup_path).expect_err("duplicate required crates"); assert!(err.contains("duplicate crate a")); fs::remove_file(dup_path).expect("remove dup required crates"); @@ -1071,12 +1191,12 @@ mod tests { fn read_required_crates_reports_read_and_parse_errors() { let missing = temp_file_path("required_missing"); let read_err = read_required_crates(&missing).expect_err("missing required file"); - assert!(read_err.contains("failed to read required crates")); + assert!(read_err.contains("failed to read coverage policy")); let invalid = temp_file_path("required_invalid"); write_file(&invalid, "not = [toml"); let parse_err = read_required_crates(&invalid).expect_err("invalid required file"); - assert!(parse_err.contains("failed to parse required crates")); + assert!(parse_err.contains("failed to parse coverage policy")); fs::remove_file(invalid).expect("remove invalid required file"); } @@ -1265,13 +1385,19 @@ test_threads = 0 #[test] fn read_required_crates_rejects_empty_and_blank_entries() { let empty_path = temp_file_path("required_empty"); - write_file(&empty_path, "[required]\ncrates = []\n"); + write_file( + &empty_path, + "[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", + ); let empty_err = read_required_crates(&empty_path).expect_err("empty required list"); assert!(empty_err.contains("must not be empty")); fs::remove_file(&empty_path).expect("remove empty required file"); let blank_path = temp_file_path("required_blank"); - write_file(&blank_path, "[required]\ncrates = [\"a\", \" \"]\n"); + write_file( + &blank_path, + "[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", + ); let blank_err = read_required_crates(&blank_path).expect_err("blank crate name"); assert!(blank_err.contains("empty crate name")); fs::remove_file(&blank_path).expect("remove blank required file"); @@ -1338,13 +1464,13 @@ test_threads = 0 fn parse_toml_reports_read_and_parse_errors() { let missing = temp_file_path("parse_toml_missing"); let read_err = - parse_toml::<CoverageRequiredContract>(&missing).expect_err("missing file should fail"); + parse_toml::<CoveragePolicyFile>(&missing).expect_err("missing file should fail"); assert!(read_err.contains("failed to read")); let invalid = temp_file_path("parse_toml_invalid"); - write_file(&invalid, "[required]\ncrates = [\n"); + write_file(&invalid, "[gate]\nfail_under_exec_lines = 100.0\n"); let parse_err = - parse_toml::<CoverageRequiredContract>(&invalid).expect_err("invalid toml should fail"); + parse_toml::<CoveragePolicyFile>(&invalid).expect_err("invalid toml should fail"); assert!(parse_err.contains("failed to parse")); fs::remove_file(invalid).expect("remove invalid toml"); } @@ -1352,8 +1478,11 @@ test_threads = 0 #[test] fn parse_toml_parses_valid_coverage_required_contract() { let valid = temp_file_path("parse_toml_valid"); - write_file(&valid, "[required]\ncrates = [\"radroots-core\"]\n"); - let parsed = parse_toml::<CoverageRequiredContract>(&valid).expect("valid toml"); + write_file( + &valid, + "[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", + ); + let parsed = parse_toml::<CoveragePolicyFile>(&valid).expect("valid toml"); assert_eq!(parsed.required.crates, vec!["radroots-core".to_string()]); fs::remove_file(valid).expect("remove valid toml"); } @@ -1729,7 +1858,7 @@ test_threads = 0 lcov_path.display().to_string(), "--out".to_string(), out_path.display().to_string(), - "--require-branches".to_string(), + "--policy-gate".to_string(), ]; report_gate(&args).expect("report gate success"); let report_raw = fs::read_to_string(&out_path).expect("read report"); @@ -1799,7 +1928,7 @@ test_threads = 0 "NaN".to_string(), ]; let err = report_gate(&args).expect_err("nan threshold should fail coverage gate"); - assert!(err.contains("coverage gate failed")); + assert!(err.contains("invalid --fail-under-functions value")); fs::remove_dir_all(root).expect("remove report gate nan root"); } @@ -1825,7 +1954,7 @@ test_threads = 0 lcov_path.display().to_string(), "--out".to_string(), out_path.display().to_string(), - "--require-branches".to_string(), + "--policy-gate".to_string(), ]; let err = report_gate(&args).expect_err("writing report to directory should fail"); assert!(err.contains("failed to write")); @@ -1853,6 +1982,14 @@ test_threads = 0 lcov_path.display().to_string(), "--out".to_string(), out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100.0".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), + "--fail-under-branches".to_string(), + "100.0".to_string(), ]; report_gate(&args).expect("report gate no branches"); let report_raw = fs::read_to_string(&out_path).expect("read report"); @@ -1959,6 +2096,19 @@ test_threads = 0 .expect_err("invalid branches threshold"); assert!(invalid_branches.contains("invalid --fail-under-branches value")); + let missing_thresholds = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + ]) + .expect_err("missing thresholds"); + assert!(missing_thresholds.contains("missing coverage thresholds")); + let missing_summary_file = report_gate(&[ "--scope".to_string(), "crate".to_string(), @@ -1968,6 +2118,7 @@ test_threads = 0 lcov_path.display().to_string(), "--out".to_string(), out_path.display().to_string(), + "--policy-gate".to_string(), ]) .expect_err("missing summary file should fail"); assert!(missing_summary_file.contains("failed to read summary")); @@ -1981,10 +2132,27 @@ test_threads = 0 root.join("missing-lcov.info").display().to_string(), "--out".to_string(), out_path.display().to_string(), + "--policy-gate".to_string(), ]) .expect_err("missing lcov file should fail"); assert!(missing_lcov_file.contains("failed to read lcov")); + let mixed_policy_gate = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + ]) + .expect_err("policy gate mixed with explicit thresholds"); + assert!(mixed_policy_gate.contains("cannot be combined")); + fs::remove_dir_all(root).expect("remove report arg errors root"); } @@ -2008,7 +2176,7 @@ test_threads = 0 let mut output = Vec::new(); let required_err = list_required_crates_with_root(&root, &mut output) .expect_err("missing required crates file should fail"); - assert!(required_err.contains("failed to read required crates")); + assert!(required_err.contains("failed to read coverage policy")); let workspace_err = list_workspace_crates_with_root(&root, &mut output) .expect_err("missing workspace manifest should fail"); @@ -2063,7 +2231,7 @@ test_threads = 0 lcov_path.display().to_string(), "--out".to_string(), out_path.display().to_string(), - "--require-branches".to_string(), + "--policy-gate".to_string(), ]) .expect("dispatch report"); assert!(out_path.exists()); diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -629,21 +629,19 @@ manifest_file = "export-manifest.json" "#, ); write_file( - &root.join("contract").join("coverage").join("rollout.toml"), - r#"[rollout] -crates = [ - { name = "radroots-a", status = "required", order = 1 }, - { name = "radroots-b", status = "planned", order = 2 }, -] -"#, - ); - write_file( &root .join("contract") .join("coverage") - .join("required-crates.toml"), - r#"[required] -crates = ["radroots-a"] + .join("policy.toml"), + r#"[gate] +fail_under_exec_lines = 100.0 +fail_under_functions = 100.0 +fail_under_regions = 100.0 +fail_under_branches = 100.0 +require_branches = true + +[required] +crates = ["radroots-a", "radroots-b"] "#, ); write_file( @@ -669,7 +667,7 @@ crates = ["radroots-a"] .join("target") .join("coverage") .join("coverage-refresh.tsv"), - "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots-b\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ); root } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -22,7 +22,7 @@ fn usage() { eprintln!(" cargo xtask sdk coverage required-crates"); eprintln!(" cargo xtask sdk coverage workspace-crates"); eprintln!( - " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--fail-under-exec-lines <pct>] [--fail-under-functions <pct>] [--fail-under-regions <pct>] [--fail-under-branches <pct>] [--require-branches]" + " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--policy-gate | (--fail-under-exec-lines <pct> --fail-under-functions <pct> --fail-under-regions <pct> --fail-under-branches <pct> [--require-branches])]" ); } @@ -352,11 +352,11 @@ mod tests { let required_raw = fs::read_to_string( root.join("contract") .join("coverage") - .join("required-crates.toml"), + .join("policy.toml"), ) - .expect("read required crates contract"); + .expect("read coverage policy contract"); let required_toml = - toml::from_str::<toml::Value>(&required_raw).expect("parse required crates contract"); + toml::from_str::<toml::Value>(&required_raw).expect("parse coverage policy contract"); let required_crates = required_toml .get("required") .and_then(toml::Value::as_table) @@ -400,7 +400,7 @@ mod tests { lcov_path.display().to_string(), "--out".to_string(), gate_out.display().to_string(), - "--require-branches".to_string(), + "--policy-gate".to_string(), ]) .expect("coverage report"); diff --git a/docs/nix.md b/docs/nix.md @@ -62,7 +62,7 @@ nix develop .#release The shells provide: - Rust `1.92.0` with `wasm32-unknown-unknown` -- pinned nightly cargo for coverage +- pinned nightly cargo for coverage from `rust-toolchain-coverage.toml` - `wasm-pack` - `cargo-llvm-cov` - `pkg-config` diff --git a/nix/common.nix b/nix/common.nix @@ -201,7 +201,7 @@ let mkdir -p "''${run_dir}" status="ok" - if ! cargo run -q -p xtask -- sdk coverage run-crate --crate "''${crate}" --out "''${run_dir}" --test-threads 1; then + if ! cargo run -q -p xtask -- sdk coverage run-crate --crate "''${crate}" --out "''${run_dir}"; then status="run-failed" fi @@ -318,11 +318,7 @@ EOF --summary "''${crate_dir}/coverage-summary.json" \ --lcov "''${crate_dir}/coverage-lcov.info" \ --out "''${crate_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-regions 100 \ - --fail-under-branches 100 \ - --require-branches + --policy-gate done < "$required_crates_file" ''; in diff --git a/nix/toolchains.nix b/nix/toolchains.nix @@ -9,9 +9,6 @@ let "rust-src" "rustfmt" ]; - coverageExtensions = stableExtensions ++ [ - "llvm-tools-preview" - ]; in { stable = pkgs.rust-bin.stable.${stableVersion}.default.override { @@ -19,11 +16,5 @@ in targets = stableTargets; }; - coverage = pkgs.rust-bin.selectLatestNightlyWith ( - nightly: - nightly.default.override { - extensions = coverageExtensions; - targets = stableTargets; - } - ); + coverage = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain-coverage.toml; } diff --git a/rust-toolchain-coverage.toml b/rust-toolchain-coverage.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2026-03-19" +components = ["clippy", "rust-analyzer", "rust-src", "rustfmt", "llvm-tools-preview"] +targets = ["wasm32-unknown-unknown"] diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh @@ -22,17 +22,13 @@ while IFS= read -r crate; do out_dir="target/coverage/${safe_crate}" mkdir -p "$out_dir" - cargo run -q -p xtask -- sdk coverage run-crate --crate "$crate" --out "$out_dir" --test-threads 1 + cargo run -q -p xtask -- sdk coverage run-crate --crate "$crate" --out "$out_dir" cargo run -q -p xtask -- sdk coverage report \ --scope "${crate}" \ --summary "${out_dir}/coverage-summary.json" \ --lcov "${out_dir}/coverage-lcov.info" \ --out "${out_dir}/gate-report.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-regions 100 \ - --fail-under-branches 100 \ - --require-branches + --policy-gate printf "%s\tpass\t100.0\t100.0\t100.0\t100.0\t%s\n" "$crate" "${out_dir}/gate-report.json" >> target/coverage/coverage-refresh.tsv printf "%s\tpass\n" "$crate" >> target/coverage/coverage-refresh-status.tsv