commit b9cb676cc5c2d2f4535b2186caff2d31350b47e3
parent b41c2e949131356fea27f7938f72574d30bc4c53
Author: triesap <tyson@radroots.org>
Date: Wed, 4 Mar 2026 21:42:23 +0000
coverage: enforce regions as fourth strict gate metric
- add fail-under-regions to xtask coverage report thresholds and gate evaluation
- include regions in coverage gate report payloads, cli help, and failure reasons
- require region column in coverage-refresh.tsv and enforce 100/100/100/100 in contract validation
- wire regions into release preflight and sdk coverage ci workflows with updated policy/rollout docs
Diffstat:
8 files changed, 85 insertions(+), 33 deletions(-)
diff --git a/.github/workflows/sdk-coverage-ci.yml b/.github/workflows/sdk-coverage-ci.yml
@@ -52,6 +52,7 @@ jobs:
--out "${run_dir}/coverage-gate-summary.json" \
--fail-under-exec-lines 0 \
--fail-under-functions 0 \
+ --fail-under-regions 0 \
--fail-under-branches 0; then
status="report-failed"
fi
@@ -63,6 +64,7 @@ jobs:
"thresholds": {
"executable_lines": 0,
"functions": 0,
+ "regions": 0,
"branches": 0,
"branches_required": false
},
@@ -114,6 +116,7 @@ JSON
--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
done < /tmp/radroots_required_coverage_crates.txt
diff --git a/contract/coverage/POLICY.md b/contract/coverage/POLICY.md
@@ -6,10 +6,11 @@ This document defines the required coverage gate for the oss rust workspace.
- executable lines coverage: 100.0
- function coverage: 100.0
+- region coverage: 100.0
- branch coverage: 100.0
- branch records must be present in lcov data
-All three thresholds are release-blocking.
+All four thresholds are release-blocking.
## toolchain contract
@@ -21,7 +22,7 @@ All three thresholds are release-blocking.
## enforcement contract
- run coverage checks per crate, not only aggregate workspace totals
-- a crate cannot be promoted to required unless it is at 100/100/100
+- 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
diff --git a/contract/coverage/rollout.toml b/contract/coverage/rollout.toml
@@ -1,6 +1,7 @@
[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
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -364,7 +364,7 @@ fn parse_coverage_percent(raw: &str, field: &str, crate_name: &str) -> Result<f6
fn load_coverage_refresh_rows(
workspace_root: &Path,
-) -> Result<BTreeMap<String, (String, f64, f64, f64)>, String> {
+) -> Result<BTreeMap<String, (String, f64, f64, f64, f64)>, String> {
let report_path = workspace_root
.join("target")
.join("coverage")
@@ -380,9 +380,9 @@ fn load_coverage_refresh_rows(
continue;
}
let parts = trimmed.split('\t').collect::<Vec<_>>();
- if parts.len() < 5 {
+ if parts.len() < 6 {
return Err(format!(
- "coverage row must have at least 5 columns in {}: {}",
+ "coverage row must have at least 6 columns in {}: {}",
report_path.display(),
trimmed
));
@@ -392,7 +392,8 @@ fn load_coverage_refresh_rows(
let exec = parse_coverage_percent(parts[2], "exec", &crate_name)?;
let func = parse_coverage_percent(parts[3], "func", &crate_name)?;
let branch = parse_coverage_percent(parts[4], "branch", &crate_name)?;
- rows.insert(crate_name, (status, exec, func, branch));
+ let region = parse_coverage_percent(parts[5], "region", &crate_name)?;
+ rows.insert(crate_name, (status, exec, func, branch, region));
}
Ok(rows)
}
@@ -403,7 +404,7 @@ fn validate_required_coverage_summary(
) -> Result<(), String> {
let rows = load_coverage_refresh_rows(workspace_root)?;
for crate_name in required_crates {
- let (status, exec, func, branch) = rows.get(crate_name).ok_or_else(|| {
+ let (status, exec, func, branch, region) = rows.get(crate_name).ok_or_else(|| {
format!(
"required coverage crate {} missing from coverage-refresh.tsv",
crate_name
@@ -415,10 +416,10 @@ fn validate_required_coverage_summary(
crate_name, status
));
}
- if *exec < 100.0 || *func < 100.0 || *branch < 100.0 {
+ if *exec < 100.0 || *func < 100.0 || *branch < 100.0 || *region < 100.0 {
return Err(format!(
- "required coverage crate {} must be 100/100/100, found {}/{}/{}",
- crate_name, exec, func, branch
+ "required coverage crate {} must be 100/100/100/100, found {}/{}/{}/{}",
+ crate_name, exec, func, branch, region
));
}
}
@@ -1037,7 +1038,7 @@ crates = ["radroots-a"]
.join("target")
.join("coverage")
.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\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\n",
);
root
}
@@ -1193,7 +1194,7 @@ pub enum RadrootsCoreUnitDimension {
fs::create_dir_all(&coverage_dir).expect("create coverage dir");
fs::write(
coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\tfile\n",
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n",
)
.expect("write coverage file");
let required = ["radroots-core".to_string()]
@@ -1203,21 +1204,30 @@ pub enum RadrootsCoreUnitDimension {
fs::write(
coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t99.9\t100.0\tfile\n",
+ "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)
.expect_err("function coverage below 100");
- assert!(func_err.contains("must be 100/100/100"));
+ assert!(func_err.contains("must be 100/100/100/100"));
fs::write(
coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t99.9\tfile\n",
+ "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)
.expect_err("branch coverage below 100");
- assert!(branch_err.contains("must be 100/100/100"));
+ assert!(branch_err.contains("must be 100/100/100/100"));
+
+ 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)
+ .expect_err("region coverage below 100");
+ assert!(region_err.contains("must be 100/100/100/100"));
let _ = fs::remove_dir_all(&root);
}
@@ -1323,21 +1333,29 @@ members = ["crates/a", "crates/b"]
write_file(
&coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nbad-row\n",
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nbad-row\n",
);
let bad_row = load_coverage_refresh_rows(&root).expect_err("invalid coverage row");
- assert!(bad_row.contains("at least 5 columns"));
+ assert!(bad_row.contains("at least 6 columns"));
write_file(
&coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\tnot-a-number\t100\t100\tfile\n",
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\tnot-a-number\t100\t100\t100\tfile\n",
);
let bad_percent = load_coverage_refresh_rows(&root).expect_err("invalid coverage percent");
assert!(bad_percent.contains("parse exec"));
write_file(
&coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tfail\t100\t100\t100\tfile\n",
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\t100\t100\tnot-a-number\tfile\n",
+ );
+ let bad_region =
+ load_coverage_refresh_rows(&root).expect_err("invalid region coverage percent");
+ assert!(bad_region.contains("parse region"));
+
+ write_file(
+ &coverage_dir.join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tfail\t100\t100\t100\t100\tfile\n",
);
let required = ["radroots-a".to_string()]
.into_iter()
@@ -1348,11 +1366,11 @@ members = ["crates/a", "crates/b"]
write_file(
&coverage_dir.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\t99.9\t100\t100\tfile\n",
+ "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"));
+ assert!(below_100.contains("must be 100/100/100/100"));
let missing = ["missing".to_string()].into_iter().collect::<BTreeSet<_>>();
let missing_err =
@@ -1963,7 +1981,7 @@ readme = { workspace = true }
.join("target")
.join("coverage")
.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\n\nradroots-a\tpass\t100\t100\t100\tfile\n",
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n\nradroots-a\tpass\t100\t100\t100\t100\tfile\n",
);
let rows = load_coverage_refresh_rows(&root).expect("rows");
assert_eq!(rows.len(), 1);
diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs
@@ -38,6 +38,7 @@ pub struct LcovCoverage {
pub struct CoverageThresholds {
pub fail_under_exec_lines: f64,
pub fail_under_functions: f64,
+ pub fail_under_regions: f64,
pub fail_under_branches: f64,
pub require_branches: bool,
}
@@ -61,6 +62,7 @@ struct CoverageGateReport {
struct CoverageGateReportThresholds {
executable_lines: f64,
functions: f64,
+ regions: f64,
branches: f64,
branches_required: bool,
}
@@ -478,12 +480,13 @@ pub fn evaluate_gate(
) -> CoverageGateResult {
let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines;
let functions_ok = summary.functions_percent >= thresholds.fail_under_functions;
+ let regions_ok = summary.summary_regions_percent >= thresholds.fail_under_regions;
let branch_presence_ok = !thresholds.require_branches || lcov.branches_available;
let branch_ok = lcov
.branch_percent
.is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches);
- let pass = exec_ok && functions_ok && branch_presence_ok && branch_ok;
+ let pass = exec_ok && functions_ok && regions_ok && branch_presence_ok && branch_ok;
let mut fail_reasons: Vec<String> = Vec::new();
if !exec_ok {
@@ -500,6 +503,13 @@ pub fn evaluate_gate(
));
}
+ if !regions_ok {
+ fail_reasons.push(format!(
+ "regions={:.6} < {:.6}",
+ summary.summary_regions_percent, thresholds.fail_under_regions
+ ));
+ }
+
if thresholds.require_branches && !lcov.branches_available {
fail_reasons.push("branches=unavailable".to_string());
}
@@ -704,6 +714,7 @@ fn report_gate(args: &[String]) -> Result<(), String> {
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"),
};
@@ -717,6 +728,7 @@ fn report_gate(args: &[String]) -> Result<(), String> {
thresholds: CoverageGateReportThresholds {
executable_lines: thresholds.fail_under_exec_lines,
functions: thresholds.fail_under_functions,
+ regions: thresholds.fail_under_regions,
branches: thresholds.fail_under_branches,
branches_required: thresholds.require_branches,
},
@@ -753,16 +765,20 @@ fn report_gate(args: &[String]) -> Result<(), String> {
if lcov.branches_available {
eprintln!(
- "{} coverage: executable_lines={:.6} functions={:.6} branches={:.6}",
+ "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches={:.6}",
scope,
lcov.executable_percent,
summary.functions_percent,
+ summary.summary_regions_percent,
lcov.branch_percent.unwrap_or(0.0)
);
} else {
eprintln!(
- "{} coverage: executable_lines={:.6} functions={:.6} branches=unavailable",
- scope, lcov.executable_percent, summary.functions_percent
+ "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches=unavailable",
+ scope,
+ lcov.executable_percent,
+ summary.functions_percent,
+ summary.summary_regions_percent
);
}
@@ -968,6 +984,7 @@ mod tests {
let thresholds = 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,
};
@@ -1342,6 +1359,7 @@ test_threads = 0
let thresholds = CoverageThresholds {
fail_under_exec_lines: 90.0,
fail_under_functions: 90.0,
+ fail_under_regions: 90.0,
fail_under_branches: 90.0,
require_branches: true,
};
@@ -1361,6 +1379,11 @@ test_threads = 0
assert!(
gate.fail_reasons
.iter()
+ .any(|reason| reason.contains("regions"))
+ );
+ assert!(
+ gate.fail_reasons
+ .iter()
.any(|reason| reason.contains("branches"))
);
}
@@ -1522,6 +1545,7 @@ test_threads = 0
report_gate(&args).expect("report gate success");
let report_raw = fs::read_to_string(&out_path).expect("read report");
assert!(report_raw.contains("\"scope\": \"crate-x\""));
+ assert!(report_raw.contains("\"regions\": 100.0"));
assert!(report_raw.contains("\"pass\": true"));
fs::remove_dir_all(root).expect("remove report gate success root");
}
@@ -1551,6 +1575,8 @@ test_threads = 0
"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(),
];
diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs
@@ -655,7 +655,7 @@ crates = ["radroots-a"]
.join("target")
.join("coverage")
.join("coverage-refresh.tsv"),
- "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\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\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>"
+ " 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]"
);
}
@@ -374,10 +374,12 @@ mod tests {
.and_then(|table| table.get("crates"))
.and_then(toml::Value::as_array)
.expect("required crates array");
- let mut rows = String::from("crate\tstatus\texec\tfunc\tbranch\treport\n");
+ let mut rows = String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n");
for crate_name in required_crates {
let crate_name = crate_name.as_str().expect("required crate name");
- rows.push_str(&format!("{crate_name}\tpass\t100.0\t100.0\t100.0\tfile\n"));
+ rows.push_str(&format!(
+ "{crate_name}\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n"
+ ));
}
fs::write(&coverage_refresh_path, rows).expect("write coverage refresh");
diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh
@@ -13,7 +13,7 @@ trap 'rm -f "$required_file"' EXIT
cargo run -q -p xtask -- sdk coverage required-crates > "$required_file"
mkdir -p target/coverage
-printf "crate\tstatus\texec\tfunc\tbranch\treport\n" > target/coverage/coverage-refresh.tsv
+printf "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n" > target/coverage/coverage-refresh.tsv
printf "crate\tstatus\n" > target/coverage/coverage-refresh-status.tsv
while IFS= read -r crate; do
@@ -30,10 +30,11 @@ while IFS= read -r crate; do
--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
- printf "%s\tpass\t100.0\t100.0\t100.0\t%s\n" "$crate" "${out_dir}/gate-report.json" >> target/coverage/coverage-refresh.tsv
+ 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
done < "$required_file"