commit 48f91406bb5c8c00df603e5443af841e08f27c0a
parent a56f203b325a094c6245a51aff05796d18944656
Author: triesap <tyson@radroots.org>
Date: Fri, 20 Mar 2026 18:30:29 +0000
coverage: centralize strict coverage policy
Diffstat:
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