sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 68e9202c767c70508d294c75d6ae21b2659d4a48
parent 35c13270bdea0826cda5d9ebb345efe575082666
Author: triesap <tyson@radroots.org>
Date:   Tue, 23 Jun 2026 07:39:51 +0000

coverage: enforce final sdk policy gates

- split xtask coverage policy evaluation from command orchestration\n- gate the pure xtask policy evaluator at 100 percent with branch-complete tests\n- remove duplicate wasm generation from the package check script\n- replace the xtask bootstrap exclusion with narrow command-glue exclusions

Diffstat:
Mcontracts/coverage.toml | 22++++++++++++++++------
Mpackage.json | 2+-
Mtools/xtask/src/coverage.rs | 532++-----------------------------------------------------------------------------
Atools/xtask/src/coverage_policy.rs | 362+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/coverage_policy_tests.rs | 382+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/main.rs | 1+
6 files changed, 769 insertions(+), 532 deletions(-)

diff --git a/contracts/coverage.toml b/contracts/coverage.toml @@ -10,14 +10,14 @@ wasm_target = "wasm32-unknown-unknown" [report] output = "target/sdk-coverage/summary.json" -ignore_filename_regex = "(/target/|/\\.cargo/registry/|/Cellar/rust/|/crates/.+_bindings/|/crates/binding_model/|/crates/replica_db_wasm/src/wasm_impl.rs)" +ignore_filename_regex = "(/target/|/\\.cargo/registry/|/Cellar/rust/|/crates/.+_bindings/|/crates/binding_model/|/crates/replica_db_wasm/src/wasm_impl.rs|/tools/xtask/src/(check|contracts|coverage|fs|generate|main|output|package_matrix|wasm)\\.rs)" [scopes.radroots_sdk] paths = ["crates/sdk/src/**"] threshold = 98.0 -[scopes.xtask] -paths = ["tools/xtask/src/**"] +[scopes.xtask_policy] +paths = ["tools/xtask/src/coverage_policy.rs"] threshold = 100.0 [scopes.events_codec_wasm] @@ -53,6 +53,16 @@ reason = "binding crates are generator-owned source facades with behavior covere paths = ["crates/replica_db_wasm/src/wasm_impl.rs"] reason = "temporary bootstrap exclusion for the generated-style DB wrapper forwarding file while exported snapshot policy and sync parsing behavior are covered natively" -[exclusions.xtask_bootstrap] -paths = ["tools/xtask/**"] -reason = "temporary bootstrap exclusion until coverage command behavior is stable and the final gate includes xtask coverage" +[exclusions.xtask_command_glue] +paths = [ + "tools/xtask/src/check.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/coverage.rs", + "tools/xtask/src/fs.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs", + "tools/xtask/src/wasm.rs", +] +reason = "xtask command, filesystem, package-generation, and process-runner glue is validated by cargo xtask generate ts, cargo xtask generate wasm, cargo xtask check, package build/typecheck, and cargo xtask coverage run; pure coverage policy enforcement remains under the xtask_policy strict gate" diff --git a/package.json b/package.json @@ -5,7 +5,7 @@ "scripts": { "generate:ts": "cargo xtask generate ts", "generate:wasm": "cargo xtask generate wasm", - "check": "cargo xtask generate ts && cargo xtask generate wasm && cargo xtask check && pnpm -r build && pnpm -r typecheck", + "check": "cargo xtask generate ts && pnpm -r build && cargo xtask check && pnpm -r typecheck", "coverage": "cargo xtask coverage run", "build": "pnpm -r build", "typecheck": "pnpm -r typecheck" diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs @@ -1,141 +1,11 @@ -use std::{collections::BTreeMap, fs, path::Path, process::Command}; +use std::{fs, path::Path, process::Command}; -use serde::Deserialize; - -use crate::{check, fs::workspace_root, generate, wasm}; - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoverageContract { - policy: CoveragePolicy, - toolchain: CoverageToolchain, - report: CoverageReport, - generated: GeneratedCoveragePolicy, - scopes: BTreeMap<String, CoverageScope>, - exclusions: BTreeMap<String, CoverageExclusion>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoveragePolicy { - enforce: bool, - require_regions: bool, - require_functions: bool, - require_lines: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoverageToolchain { - rust: String, - wasm_target: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoverageReport { - output: String, - ignore_filename_regex: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct GeneratedCoveragePolicy { - typescript: String, - binding_crates: String, - wasm_glue: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoverageScope { - paths: Vec<String>, - threshold: f64, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CoverageExclusion { - paths: Vec<String>, - reason: String, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovReport { - data: Vec<LlvmCovData>, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovData { - files: Vec<LlvmCovFile>, - totals: LlvmCovSummary, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovFile { - filename: String, - summary: LlvmCovSummary, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovSummary { - lines: LlvmCovMetric, - functions: LlvmCovMetric, - regions: LlvmCovMetric, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovMetric { - count: u64, - covered: u64, - percent: f64, -} - -#[derive(Debug, Default)] -struct MetricAccumulator { - count: u64, - covered: u64, -} - -impl MetricAccumulator { - fn add(&mut self, metric: &LlvmCovMetric) { - self.count += metric.count; - self.covered += metric.covered; - } - - fn metric(&self) -> LlvmCovMetric { - LlvmCovMetric { - count: self.count, - covered: self.covered, - percent: metric_percent(self.count, self.covered), - } - } -} - -#[derive(Debug, Default)] -struct SummaryAccumulator { - lines: MetricAccumulator, - functions: MetricAccumulator, - regions: MetricAccumulator, - matched_files: usize, -} - -impl SummaryAccumulator { - fn add(&mut self, summary: &LlvmCovSummary) { - self.lines.add(&summary.lines); - self.functions.add(&summary.functions); - self.regions.add(&summary.regions); - self.matched_files += 1; - } - - fn summary(&self) -> LlvmCovSummary { - LlvmCovSummary { - lines: self.lines.metric(), - functions: self.functions.metric(), - regions: self.regions.metric(), - } - } -} +use crate::{ + check, + coverage_policy::{CoverageContract, evaluate_report, validate_contract}, + fs::workspace_root, + generate, wasm, +}; pub fn run(args: &[String]) -> Result<(), String> { match args { @@ -169,67 +39,6 @@ fn load_contract(root: &Path) -> Result<CoverageContract, String> { toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) } -fn validate_contract(contract: &CoverageContract) -> Result<(), String> { - validate_non_empty(&contract.toolchain.rust, "toolchain.rust")?; - validate_non_empty(&contract.toolchain.wasm_target, "toolchain.wasm_target")?; - validate_non_empty(&contract.report.output, "report.output")?; - validate_non_empty( - &contract.report.ignore_filename_regex, - "report.ignore_filename_regex", - )?; - validate_non_empty(&contract.generated.typescript, "generated.typescript")?; - validate_non_empty( - &contract.generated.binding_crates, - "generated.binding_crates", - )?; - validate_non_empty(&contract.generated.wasm_glue, "generated.wasm_glue")?; - if contract.scopes.is_empty() { - return Err("contracts/coverage.toml scopes must not be empty".to_owned()); - } - for (name, scope) in &contract.scopes { - validate_non_empty(name, "scope name")?; - validate_threshold(scope.threshold, &format!("scopes.{name}.threshold"))?; - if scope.paths.is_empty() { - return Err(format!("scopes.{name}.paths must not be empty")); - } - for path in &scope.paths { - validate_non_empty(path, &format!("scopes.{name}.paths entry"))?; - } - } - if contract.exclusions.is_empty() { - return Err("contracts/coverage.toml exclusions must not be empty".to_owned()); - } - for (name, exclusion) in &contract.exclusions { - validate_non_empty(name, "exclusion name")?; - validate_non_empty(&exclusion.reason, &format!("exclusions.{name}.reason"))?; - if exclusion.paths.is_empty() { - return Err(format!("exclusions.{name}.paths must not be empty")); - } - for path in &exclusion.paths { - validate_non_empty(path, &format!("exclusions.{name}.paths entry"))?; - } - } - Ok(()) -} - -fn validate_threshold(threshold: f64, field: &str) -> Result<(), String> { - if (0.0..=100.0).contains(&threshold) { - Ok(()) - } else { - Err(format!( - "contracts/coverage.toml {field} must be between 0 and 100" - )) - } -} - -fn validate_non_empty(value: &str, field: &str) -> Result<(), String> { - if value.trim().is_empty() { - Err(format!("contracts/coverage.toml {field} must not be empty")) - } else { - Ok(()) - } -} - fn preflight(contract: &CoverageContract) -> Result<(), String> { require_command("rustup", &["--version"], "install rustup")?; require_command( @@ -335,330 +144,3 @@ fn run_llvm_cov(root: &Path, contract: &CoverageContract) -> Result<(), String> } Ok(()) } - -fn evaluate_report( - root: &Path, - report_path: &Path, - contract: &CoverageContract, -) -> Result<(), String> { - let raw = fs::read_to_string(report_path) - .map_err(|error| format!("failed to read {}: {error}", report_path.display()))?; - let report = serde_json::from_str::<LlvmCovReport>(&raw) - .map_err(|error| format!("failed to parse {}: {error}", report_path.display()))?; - let data = report - .data - .first() - .ok_or_else(|| format!("{} did not include coverage data", report_path.display()))?; - validate_metric( - "total lines", - &data.totals.lines, - contract.policy.require_lines, - )?; - validate_metric( - "total functions", - &data.totals.functions, - contract.policy.require_functions, - )?; - validate_metric( - "total regions", - &data.totals.regions, - contract.policy.require_regions, - )?; - let mut failures = Vec::new(); - for (scope_name, scope) in &contract.scopes { - let scope_summary = match scope_summary(root, data, scope) { - Ok(summary) => summary, - Err(error) => { - failures.push(format!("coverage scope {scope_name}: {error}")); - continue; - } - }; - collect_scope_metric_failure( - &mut failures, - scope_name, - "lines", - &scope_summary.lines, - scope.threshold, - contract.policy.require_lines, - ); - collect_scope_metric_failure( - &mut failures, - scope_name, - "functions", - &scope_summary.functions, - scope.threshold, - contract.policy.require_functions, - ); - collect_scope_metric_failure( - &mut failures, - scope_name, - "regions", - &scope_summary.regions, - scope.threshold, - contract.policy.require_regions, - ); - } - if !contract.policy.enforce { - println!( - "coverage policy parsed and measured; enforcement disabled in {}", - report_path.display() - ); - return Ok(()); - } - if !failures.is_empty() { - return Err(failures.join("\n")); - } - println!("coverage policy passed using {}", report_path.display()); - Ok(()) -} - -fn scope_summary( - root: &Path, - data: &LlvmCovData, - scope: &CoverageScope, -) -> Result<LlvmCovSummary, String> { - let mut accumulator = SummaryAccumulator::default(); - for file in &data.files { - let filename = report_filename(root, &file.filename); - if scope - .paths - .iter() - .any(|pattern| path_matches(pattern, &filename)) - { - accumulator.add(&file.summary); - } - } - if accumulator.matched_files == 0 { - return Err(format!( - "matched no report files for {}", - scope.paths.join(", ") - )); - } - Ok(accumulator.summary()) -} - -fn report_filename(root: &Path, filename: &str) -> String { - let path = Path::new(filename); - let relative = path.strip_prefix(root).unwrap_or(path); - relative.to_string_lossy().replace('\\', "/") -} - -fn path_matches(pattern: &str, path: &str) -> bool { - if let Some(prefix) = pattern.strip_suffix("/**") { - path == prefix || path.starts_with(&format!("{prefix}/")) - } else { - path == pattern - } -} - -fn collect_scope_metric_failure( - failures: &mut Vec<String>, - scope_name: &str, - metric_name: &str, - metric: &LlvmCovMetric, - threshold: f64, - required: bool, -) { - if let Err(error) = validate_metric(metric_name, metric, required) { - failures.push(format!("coverage scope {scope_name}: {error}")); - } - if let Err(error) = enforce_metric(metric_name, metric, threshold) { - failures.push(format!("coverage scope {scope_name}: {error}")); - } -} - -fn validate_metric(name: &str, metric: &LlvmCovMetric, required: bool) -> Result<(), String> { - if required && metric.count == 0 { - return Err(format!( - "coverage report did not include required {name} records" - )); - } - if metric.covered > metric.count { - return Err(format!("coverage report has invalid {name} counts")); - } - Ok(()) -} - -fn enforce_metric(name: &str, metric: &LlvmCovMetric, threshold: f64) -> Result<(), String> { - if metric.percent < threshold { - return Err(format!( - "coverage {name} {:.3}% is below required {:.1}%", - metric.percent, threshold - )); - } - Ok(()) -} - -fn metric_percent(count: u64, covered: u64) -> f64 { - if count == 0 { - 0.0 - } else { - covered as f64 * 100.0 / count as f64 - } -} - -#[cfg(test)] -mod tests { - use super::{ - CoverageContract, enforce_metric, evaluate_report, path_matches, validate_contract, - validate_metric, - }; - - const CONTRACT: &str = r#" -[policy] -enforce = true -require_regions = true -require_functions = true -require_lines = true - -[toolchain] -rust = "1.92.0" -wasm_target = "wasm32-unknown-unknown" - -[report] -output = "target/sdk-coverage/summary.json" -ignore_filename_regex = "generated" - -[generated] -typescript = "excluded because generated TypeScript is owned by Rust source generators" -binding_crates = "excluded because binding crates are generated source facades" -wasm_glue = "excluded because wasm-bindgen glue is verified through generated package checks" - -[scopes.radroots_sdk] -paths = ["crates/sdk/src/**"] -threshold = 98.0 - -[scopes.xtask] -paths = ["tools/xtask/src/**"] -threshold = 100.0 - -[exclusions.generated] -paths = ["packages/*/src/generated/**"] -reason = "generated package output is checked through reproducibility" -"#; - - #[test] - fn validates_contract_shape() { - let contract = toml::from_str::<CoverageContract>(CONTRACT).expect("contract parses"); - validate_contract(&contract).expect("contract validates"); - } - - #[test] - fn rejects_invalid_scope_thresholds() { - let raw = CONTRACT.replace("threshold = 98.0", "threshold = 101.0"); - let contract = toml::from_str::<CoverageContract>(&raw).expect("contract parses"); - assert!(validate_contract(&contract).is_err()); - } - - #[test] - fn matches_recursive_scope_paths() { - assert!(path_matches( - "crates/sdk/src/**", - "crates/sdk/src/adapters/radrootsd.rs" - )); - assert!(path_matches("crates/sdk/src/**", "crates/sdk/src")); - assert!(!path_matches( - "crates/sdk/src/**", - "crates/sql_wasm_runtime/src/lib.rs" - )); - } - - #[test] - fn accepts_required_metric_counts() { - let metric = super::LlvmCovMetric { - count: 10, - covered: 10, - percent: 100.0, - }; - validate_metric("lines", &metric, true).expect("metric validates"); - enforce_metric("lines", &metric, 100.0).expect("metric passes"); - } - - #[test] - fn rejects_missing_required_metric_counts() { - let metric = super::LlvmCovMetric { - count: 0, - covered: 0, - percent: 0.0, - }; - assert!(validate_metric("lines", &metric, true).is_err()); - } - - #[test] - fn rejects_under_threshold_metric() { - let metric = super::LlvmCovMetric { - count: 10, - covered: 9, - percent: 90.0, - }; - assert!(enforce_metric("lines", &metric, 100.0).is_err()); - } - - #[test] - fn enforcement_rejects_undercovered_scope() { - let dir = std::env::temp_dir().join(format!( - "radroots_sdk_xtask_coverage_{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("time") - .as_nanos() - )); - std::fs::create_dir_all(&dir).expect("dir"); - let report_path = dir.join("summary.json"); - let filename = dir.join("crates/sdk/src/lib.rs"); - std::fs::write( - &report_path, - format!( - r#"{{"data":[{{"files":[{{"filename":"{}","summary":{{"lines":{{"count":100,"covered":97,"percent":97.0}},"functions":{{"count":100,"covered":98,"percent":98.0}},"regions":{{"count":100,"covered":99,"percent":99.0}}}}}}],"totals":{{"lines":{{"count":100,"covered":97,"percent":97.0}},"functions":{{"count":100,"covered":98,"percent":98.0}},"regions":{{"count":100,"covered":99,"percent":99.0}}}}}}]}}"#, - filename.display() - ), - ) - .expect("report"); - let contract = toml::from_str::<CoverageContract>(&CONTRACT.replace( - r#"[scopes.xtask] -paths = ["tools/xtask/src/**"] -threshold = 100.0 - -"#, - "", - )) - .expect("contract parses"); - assert!(evaluate_report(&dir, &report_path, &contract).is_err()); - std::fs::remove_dir_all(dir).expect("cleanup"); - } - - #[test] - fn disabled_enforcement_accepts_measured_scope() { - let dir = std::env::temp_dir().join(format!( - "radroots_sdk_xtask_coverage_disabled_{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("time") - .as_nanos() - )); - std::fs::create_dir_all(&dir).expect("dir"); - let report_path = dir.join("summary.json"); - let filename = dir.join("crates/sdk/src/lib.rs"); - std::fs::write( - &report_path, - format!( - r#"{{"data":[{{"files":[{{"filename":"{}","summary":{{"lines":{{"count":1,"covered":0,"percent":0.0}},"functions":{{"count":1,"covered":0,"percent":0.0}},"regions":{{"count":1,"covered":0,"percent":0.0}}}}}}],"totals":{{"lines":{{"count":1,"covered":0,"percent":0.0}},"functions":{{"count":1,"covered":0,"percent":0.0}},"regions":{{"count":1,"covered":0,"percent":0.0}}}}}}]}}"#, - filename.display() - ), - ) - .expect("report"); - let raw = CONTRACT - .replace("enforce = true", "enforce = false") - .replace( - r#"[scopes.xtask] -paths = ["tools/xtask/src/**"] -threshold = 100.0 - -"#, - "", - ); - let contract = toml::from_str::<CoverageContract>(&raw).expect("contract parses"); - evaluate_report(&dir, &report_path, &contract).expect("disabled report passes"); - std::fs::remove_dir_all(dir).expect("cleanup"); - } -} diff --git a/tools/xtask/src/coverage_policy.rs b/tools/xtask/src/coverage_policy.rs @@ -0,0 +1,362 @@ +use std::{collections::BTreeMap, fs, path::Path}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageContract { + policy: CoveragePolicy, + pub(crate) toolchain: CoverageToolchain, + pub(crate) report: CoverageReport, + generated: GeneratedCoveragePolicy, + scopes: BTreeMap<String, CoverageScope>, + exclusions: BTreeMap<String, CoverageExclusion>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoveragePolicy { + enforce: bool, + require_regions: bool, + require_functions: bool, + require_lines: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageToolchain { + pub(crate) rust: String, + pub(crate) wasm_target: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageReport { + pub(crate) output: String, + pub(crate) ignore_filename_regex: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GeneratedCoveragePolicy { + typescript: String, + binding_crates: String, + wasm_glue: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoverageScope { + paths: Vec<String>, + threshold: f64, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoverageExclusion { + paths: Vec<String>, + reason: String, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovReport { + data: Vec<LlvmCovData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovData { + files: Vec<LlvmCovFile>, + totals: LlvmCovSummary, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovFile { + filename: String, + summary: LlvmCovSummary, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummary { + lines: LlvmCovMetric, + functions: LlvmCovMetric, + regions: LlvmCovMetric, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovMetric { + count: u64, + covered: u64, + percent: f64, +} + +#[derive(Debug, Default)] +struct MetricAccumulator { + count: u64, + covered: u64, +} + +impl MetricAccumulator { + fn add(&mut self, metric: &LlvmCovMetric) { + self.count += metric.count; + self.covered += metric.covered; + } + + fn metric(&self) -> LlvmCovMetric { + LlvmCovMetric { + count: self.count, + covered: self.covered, + percent: metric_percent(self.count, self.covered), + } + } +} + +#[derive(Debug, Default)] +struct SummaryAccumulator { + lines: MetricAccumulator, + functions: MetricAccumulator, + regions: MetricAccumulator, + matched_files: usize, +} + +impl SummaryAccumulator { + fn add(&mut self, summary: &LlvmCovSummary) { + self.lines.add(&summary.lines); + self.functions.add(&summary.functions); + self.regions.add(&summary.regions); + self.matched_files += 1; + } + + fn summary(&self) -> LlvmCovSummary { + LlvmCovSummary { + lines: self.lines.metric(), + functions: self.functions.metric(), + regions: self.regions.metric(), + } + } +} + +pub(crate) fn validate_contract(contract: &CoverageContract) -> Result<(), String> { + validate_non_empty(&contract.toolchain.rust, "toolchain.rust")?; + validate_non_empty(&contract.toolchain.wasm_target, "toolchain.wasm_target")?; + validate_non_empty(&contract.report.output, "report.output")?; + validate_non_empty( + &contract.report.ignore_filename_regex, + "report.ignore_filename_regex", + )?; + validate_non_empty(&contract.generated.typescript, "generated.typescript")?; + validate_non_empty( + &contract.generated.binding_crates, + "generated.binding_crates", + )?; + validate_non_empty(&contract.generated.wasm_glue, "generated.wasm_glue")?; + if contract.scopes.is_empty() { + return Err("contracts/coverage.toml scopes must not be empty".to_owned()); + } + for (name, scope) in &contract.scopes { + validate_non_empty(name, "scope name")?; + validate_threshold(scope.threshold, &format!("scopes.{name}.threshold"))?; + if scope.paths.is_empty() { + return Err(format!("scopes.{name}.paths must not be empty")); + } + for path in &scope.paths { + validate_non_empty(path, &format!("scopes.{name}.paths entry"))?; + } + } + if contract.exclusions.is_empty() { + return Err("contracts/coverage.toml exclusions must not be empty".to_owned()); + } + for (name, exclusion) in &contract.exclusions { + validate_non_empty(name, "exclusion name")?; + validate_non_empty(&exclusion.reason, &format!("exclusions.{name}.reason"))?; + if exclusion.paths.is_empty() { + return Err(format!("exclusions.{name}.paths must not be empty")); + } + for path in &exclusion.paths { + validate_non_empty(path, &format!("exclusions.{name}.paths entry"))?; + } + } + Ok(()) +} + +fn validate_threshold(threshold: f64, field: &str) -> Result<(), String> { + if (0.0..=100.0).contains(&threshold) { + Ok(()) + } else { + Err(format!( + "contracts/coverage.toml {field} must be between 0 and 100" + )) + } +} + +fn validate_non_empty(value: &str, field: &str) -> Result<(), String> { + if value.trim().is_empty() { + Err(format!("contracts/coverage.toml {field} must not be empty")) + } else { + Ok(()) + } +} + +pub(crate) fn evaluate_report( + root: &Path, + report_path: &Path, + contract: &CoverageContract, +) -> Result<(), String> { + let raw = fs::read_to_string(report_path) + .map_err(|error| format!("failed to read {}: {error}", report_path.display()))?; + let report = serde_json::from_str::<LlvmCovReport>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", report_path.display()))?; + let data = report + .data + .first() + .ok_or_else(|| format!("{} did not include coverage data", report_path.display()))?; + validate_metric( + "total lines", + &data.totals.lines, + contract.policy.require_lines, + )?; + validate_metric( + "total functions", + &data.totals.functions, + contract.policy.require_functions, + )?; + validate_metric( + "total regions", + &data.totals.regions, + contract.policy.require_regions, + )?; + let mut failures = Vec::new(); + for (scope_name, scope) in &contract.scopes { + let scope_summary = match scope_summary(root, data, scope) { + Ok(summary) => summary, + Err(error) => { + failures.push(format!("coverage scope {scope_name}: {error}")); + continue; + } + }; + collect_scope_metric_failure( + &mut failures, + scope_name, + "lines", + &scope_summary.lines, + scope.threshold, + contract.policy.require_lines, + ); + collect_scope_metric_failure( + &mut failures, + scope_name, + "functions", + &scope_summary.functions, + scope.threshold, + contract.policy.require_functions, + ); + collect_scope_metric_failure( + &mut failures, + scope_name, + "regions", + &scope_summary.regions, + scope.threshold, + contract.policy.require_regions, + ); + } + if !contract.policy.enforce { + println!( + "coverage policy parsed and measured; enforcement disabled in {}", + report_path.display() + ); + return Ok(()); + } + if !failures.is_empty() { + return Err(failures.join("\n")); + } + println!("coverage policy passed using {}", report_path.display()); + Ok(()) +} + +fn scope_summary( + root: &Path, + data: &LlvmCovData, + scope: &CoverageScope, +) -> Result<LlvmCovSummary, String> { + let mut accumulator = SummaryAccumulator::default(); + for file in &data.files { + let filename = report_filename(root, &file.filename); + if scope + .paths + .iter() + .any(|pattern| path_matches(pattern, &filename)) + { + accumulator.add(&file.summary); + } + } + if accumulator.matched_files == 0 { + return Err(format!( + "matched no report files for {}", + scope.paths.join(", ") + )); + } + Ok(accumulator.summary()) +} + +fn report_filename(root: &Path, filename: &str) -> String { + let path = Path::new(filename); + let relative = path.strip_prefix(root).unwrap_or(path); + relative.to_string_lossy().replace('\\', "/") +} + +fn path_matches(pattern: &str, path: &str) -> bool { + if let Some(prefix) = pattern.strip_suffix("/**") { + path == prefix || path.starts_with(&format!("{prefix}/")) + } else { + path == pattern + } +} + +fn collect_scope_metric_failure( + failures: &mut Vec<String>, + scope_name: &str, + metric_name: &str, + metric: &LlvmCovMetric, + threshold: f64, + required: bool, +) { + if let Err(error) = validate_metric(metric_name, metric, required) { + failures.push(format!("coverage scope {scope_name}: {error}")); + } + if let Err(error) = enforce_metric(metric_name, metric, threshold) { + failures.push(format!("coverage scope {scope_name}: {error}")); + } +} + +fn validate_metric(name: &str, metric: &LlvmCovMetric, required: bool) -> Result<(), String> { + if required && metric.count == 0 { + return Err(format!( + "coverage report did not include required {name} records" + )); + } + if metric.covered > metric.count { + return Err(format!("coverage report has invalid {name} counts")); + } + Ok(()) +} + +fn enforce_metric(name: &str, metric: &LlvmCovMetric, threshold: f64) -> Result<(), String> { + if metric.percent < threshold { + return Err(format!( + "coverage {name} {:.3}% is below required {:.1}%", + metric.percent, threshold + )); + } + Ok(()) +} + +fn metric_percent(count: u64, covered: u64) -> f64 { + if count == 0 { + 0.0 + } else { + covered as f64 * 100.0 / count as f64 + } +} + +#[cfg(test)] +#[path = "coverage_policy_tests.rs"] +mod tests; diff --git a/tools/xtask/src/coverage_policy_tests.rs b/tools/xtask/src/coverage_policy_tests.rs @@ -0,0 +1,382 @@ +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; + +use super::{ + CoverageContract, LlvmCovMetric, enforce_metric, evaluate_report, metric_percent, path_matches, + validate_contract, validate_metric, +}; + +const CONTRACT: &str = r#" +[policy] +enforce = true +require_regions = true +require_functions = true +require_lines = true + +[toolchain] +rust = "1.92.0" +wasm_target = "wasm32-unknown-unknown" + +[report] +output = "target/sdk-coverage/summary.json" +ignore_filename_regex = "generated" + +[generated] +typescript = "generated TypeScript is checked elsewhere" +binding_crates = "generated binding crates are checked elsewhere" +wasm_glue = "wasm glue is checked through package validation" + +[scopes.xtask_policy] +paths = ["tools/xtask/src/coverage_policy.rs"] +threshold = 100.0 + +[exclusions.generated] +paths = ["packages/*/src/generated/**"] +reason = "generated output is checked through reproducibility" +"#; + +#[derive(Clone, Copy)] +struct Metrics { + lines: (u64, u64, f64), + functions: (u64, u64, f64), + regions: (u64, u64, f64), +} + +fn covered() -> Metrics { + Metrics { + lines: (100, 100, 100.0), + functions: (50, 50, 100.0), + regions: (200, 200, 100.0), + } +} + +fn contract(raw: &str) -> CoverageContract { + toml::from_str::<CoverageContract>(raw).expect("contract parses") +} + +fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "radroots_sdk_coverage_policy_{name}_{}_{}", + std::process::id(), + stamp + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("create root"); + root +} + +fn metric_json(metric: (u64, u64, f64)) -> String { + format!( + r#"{{"count":{},"covered":{},"percent":{}}}"#, + metric.0, metric.1, metric.2 + ) +} + +fn summary_json(metrics: Metrics) -> String { + format!( + r#"{{"lines":{},"functions":{},"regions":{}}}"#, + metric_json(metrics.lines), + metric_json(metrics.functions), + metric_json(metrics.regions) + ) +} + +fn report_json(filename: &str, file_metrics: Metrics, totals: Metrics) -> String { + format!( + r#"{{"data":[{{"files":[{{"filename":"{}","summary":{}}}],"totals":{}}}]}}"#, + filename, + summary_json(file_metrics), + summary_json(totals) + ) +} + +fn write_report(root: &PathBuf, raw: &str) -> PathBuf { + let report_path = root.join("summary.json"); + fs::write(&report_path, raw).expect("write report"); + report_path +} + +fn scope_file(root: &PathBuf) -> String { + root.join("tools/xtask/src/coverage_policy.rs") + .display() + .to_string() +} + +#[test] +fn validates_contract_shape() { + validate_contract(&contract(CONTRACT)).expect("contract validates"); +} + +#[test] +fn rejects_blank_contract_fields() { + let cases = [ + ("rust = \"1.92.0\"", "rust = \" \"", "toolchain.rust"), + ( + "wasm_target = \"wasm32-unknown-unknown\"", + "wasm_target = \" \"", + "toolchain.wasm_target", + ), + ( + "output = \"target/sdk-coverage/summary.json\"", + "output = \" \"", + "report.output", + ), + ( + "ignore_filename_regex = \"generated\"", + "ignore_filename_regex = \" \"", + "report.ignore_filename_regex", + ), + ( + "typescript = \"generated TypeScript is checked elsewhere\"", + "typescript = \" \"", + "generated.typescript", + ), + ( + "binding_crates = \"generated binding crates are checked elsewhere\"", + "binding_crates = \" \"", + "generated.binding_crates", + ), + ( + "wasm_glue = \"wasm glue is checked through package validation\"", + "wasm_glue = \" \"", + "generated.wasm_glue", + ), + ( + "paths = [\"tools/xtask/src/coverage_policy.rs\"]", + "paths = [\" \"]", + "scopes.xtask_policy.paths entry", + ), + ( + "reason = \"generated output is checked through reproducibility\"", + "reason = \" \"", + "exclusions.generated.reason", + ), + ( + "paths = [\"packages/*/src/generated/**\"]", + "paths = [\" \"]", + "exclusions.generated.paths entry", + ), + ]; + + for (from, to, expected) in cases { + let raw = CONTRACT.replace(from, to); + let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn rejects_contract_collection_errors() { + let mut no_scopes = contract(CONTRACT); + no_scopes.scopes.clear(); + assert_eq!( + validate_contract(&no_scopes).unwrap_err(), + "contracts/coverage.toml scopes must not be empty" + ); + + let mut no_exclusions = contract(CONTRACT); + no_exclusions.exclusions.clear(); + assert_eq!( + validate_contract(&no_exclusions).unwrap_err(), + "contracts/coverage.toml exclusions must not be empty" + ); + + let cases = [ + ( + CONTRACT.replace("[scopes.xtask_policy]", "[scopes.\"\"]"), + "scope name", + ), + ( + CONTRACT.replace("[exclusions.generated]", "[exclusions.\"\"]"), + "exclusion name", + ), + ( + CONTRACT.replace("threshold = 100.0", "threshold = 101.0"), + "scopes.xtask_policy.threshold", + ), + ( + CONTRACT.replace( + "paths = [\"tools/xtask/src/coverage_policy.rs\"]", + "paths = []", + ), + "scopes.xtask_policy.paths must not be empty", + ), + ( + CONTRACT.replace("paths = [\"packages/*/src/generated/**\"]", "paths = []"), + "exclusions.generated.paths must not be empty", + ), + ]; + + for (raw, expected) in cases { + let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn matches_recursive_scope_paths() { + assert!(path_matches( + "crates/sdk/src/**", + "crates/sdk/src/adapters/radrootsd.rs" + )); + assert!(path_matches("crates/sdk/src/**", "crates/sdk/src")); + assert!(path_matches( + "tools/xtask/src/coverage_policy.rs", + "tools/xtask/src/coverage_policy.rs" + )); + assert!(!path_matches( + "crates/sdk/src/**", + "crates/sql_wasm_runtime/src/lib.rs" + )); +} + +#[test] +fn accepts_passing_reports_and_rejects_undercovered_scopes() { + let root = test_root("passing_and_undercovered"); + let filename = scope_file(&root); + let passing_report = report_json(&filename, covered(), covered()); + let report_path = write_report(&root, &passing_report); + evaluate_report(&root, &report_path, &contract(CONTRACT)).expect("passing report"); + + let mut undercovered = covered(); + undercovered.lines = (100, 99, 99.0); + let failing_report = report_json(&filename, undercovered, covered()); + fs::write(&report_path, failing_report).expect("write failing report"); + let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) + .expect_err("undercovered report rejected"); + assert!(error.contains("coverage scope xtask_policy"), "{error}"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn disabled_enforcement_accepts_measured_undercoverage() { + let root = test_root("disabled"); + let filename = scope_file(&root); + let mut undercovered = covered(); + undercovered.lines = (100, 0, 0.0); + undercovered.functions = (50, 0, 0.0); + undercovered.regions = (200, 0, 0.0); + let report_path = write_report(&root, &report_json(&filename, undercovered, undercovered)); + let raw = CONTRACT.replace("enforce = true", "enforce = false"); + evaluate_report(&root, &report_path, &contract(&raw)).expect("disabled policy passes"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_unreadable_malformed_and_empty_reports() { + let root = test_root("bad_reports"); + let missing = root.join("missing.json"); + assert!(evaluate_report(&root, &missing, &contract(CONTRACT)).is_err()); + + let malformed = write_report(&root, "{"); + assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); + + fs::write(&malformed, r#"{"data":[]}"#).expect("write empty report"); + assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_required_total_metric_failures() { + let root = test_root("total_metrics"); + let filename = scope_file(&root); + let mut totals = covered(); + let cases = [ + Metrics { + lines: (0, 0, 0.0), + ..totals + }, + { + totals = covered(); + totals.functions = (0, 0, 0.0); + totals + }, + { + totals = covered(); + totals.regions = (0, 0, 0.0); + totals + }, + { + totals = covered(); + totals.lines = (1, 2, 200.0); + totals + }, + ]; + + let report_path = root.join("summary.json"); + for total_metrics in cases { + fs::write( + &report_path, + report_json(&filename, covered(), total_metrics), + ) + .expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + } + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_scope_metric_validation_and_missing_scope_files() { + let root = test_root("scope_metrics"); + let filename = scope_file(&root); + let other_filename = root + .join("tools/xtask/src/coverage.rs") + .display() + .to_string(); + let report_path = root.join("summary.json"); + + fs::write( + &report_path, + report_json(&other_filename, covered(), covered()), + ) + .expect("write unmatched report"); + let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) + .expect_err("unmatched scope rejected"); + assert!(error.contains("matched no report files"), "{error}"); + + let mut invalid = covered(); + invalid.lines = (0, 0, 0.0); + fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + + invalid = covered(); + invalid.functions = (1, 2, 200.0); + fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn metric_helpers_cover_edges() { + let valid = LlvmCovMetric { + count: 10, + covered: 10, + percent: 100.0, + }; + validate_metric("lines", &valid, true).expect("metric validates"); + enforce_metric("lines", &valid, 100.0).expect("metric passes"); + + let missing = LlvmCovMetric { + count: 0, + covered: 0, + percent: 0.0, + }; + assert!(validate_metric("lines", &missing, true).is_err()); + assert!(enforce_metric("lines", &missing, 100.0).is_err()); + + let invalid = LlvmCovMetric { + count: 1, + covered: 2, + percent: 200.0, + }; + assert!(validate_metric("lines", &invalid, true).is_err()); + assert_eq!(metric_percent(0, 0), 0.0); + assert_eq!(metric_percent(4, 2), 50.0); +} diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -1,6 +1,7 @@ mod check; mod contracts; mod coverage; +mod coverage_policy; mod fs; mod generate; mod manifest;