lib

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

commit 93f8ff5630273b21597b09945ce657b7e7e04bd2
parent cb0aa5a5a261cca9da236f0332b8a1ecc754986a
Author: triesap <tyson@radroots.org>
Date:   Wed,  4 Mar 2026 19:22:03 +0000

xtask: harden coverage and contract test surface

- add targeted unit tests for cli dispatch, parser helpers, and workspace resolution
- refactor coverage and contract error paths to explicit match branches for deterministic reporting
- reduce export ts helper closure usage and improve failure path coverage
- keep xtask check and test lanes green while preparing stricter coverage gates

Diffstat:
Mcrates/xtask/src/contract.rs | 819++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/xtask/src/coverage.rs | 661++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/xtask/src/export_ts.rs | 146++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/xtask/src/main.rs | 257++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 1724 insertions(+), 159 deletions(-)

diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -163,8 +163,14 @@ struct ReleaseCrateSet { } fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { - let raw = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; - toml::from_str::<T>(&raw).map_err(|e| format!("parse {}: {e}", path.display())) + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(e) => return Err(format!("read {}: {e}", path.display())), + }; + match toml::from_str::<T>(&raw) { + Ok(parsed) => Ok(parsed), + Err(e) => Err(format!("parse {}: {e}", path.display())), + } } fn contract_root(workspace_root: &Path) -> PathBuf { @@ -316,14 +322,25 @@ fn validate_publish_package_metadata( ) -> Result<(), String> { let manifests = workspace_package_manifests(workspace_root)?; for crate_name in publish_crates { - let manifest_path = manifests - .get(crate_name) - .ok_or_else(|| format!("publish crate {} has no workspace manifest", crate_name))?; + let manifest_path = match manifests.get(crate_name) { + Some(manifest_path) => manifest_path, + None => { + return Err(format!( + "publish crate {} has no workspace manifest", + crate_name + )); + } + }; let parsed = parse_toml::<toml::Value>(manifest_path)?; - let package = parsed - .get("package") - .and_then(toml::Value::as_table) - .ok_or_else(|| format!("{} missing [package] table", manifest_path.display()))?; + let package = match parsed.get("package").and_then(toml::Value::as_table) { + Some(package) => package, + None => { + return Err(format!( + "{} missing [package] table", + manifest_path.display() + )); + } + }; if !package_field_configured(package, "description") { return Err(format!( @@ -344,8 +361,10 @@ fn validate_publish_package_metadata( } fn parse_coverage_percent(raw: &str, field: &str, crate_name: &str) -> Result<f64, String> { - raw.parse::<f64>() - .map_err(|e| format!("parse {} for {}: {e}", field, crate_name)) + match raw.parse::<f64>() { + Ok(value) => Ok(value), + Err(e) => Err(format!("parse {} for {}: {e}", field, crate_name)), + } } fn load_coverage_refresh_rows( @@ -355,8 +374,10 @@ fn load_coverage_refresh_rows( .join("target") .join("coverage") .join("coverage-refresh.tsv"); - let raw = fs::read_to_string(&report_path) - .map_err(|e| format!("read {}: {e}", report_path.display()))?; + let raw = match fs::read_to_string(&report_path) { + Ok(raw) => raw, + Err(e) => return Err(format!("read {}: {e}", report_path.display())), + }; let mut rows = BTreeMap::new(); for line in raw.lines().skip(1) { let trimmed = line.trim(); @@ -414,13 +435,15 @@ const CORE_UNIT_DIMENSION_ORDER: [&str; 3] = ["Count", "Mass", "Volume"]; fn extract_enum_body<'a>(source: &'a str, enum_name: &str) -> Result<&'a str, String> { let marker = format!("pub enum {enum_name}"); - let enum_start = source - .find(&marker) - .ok_or_else(|| format!("missing enum {enum_name}"))?; + let enum_start = match source.find(&marker) { + Some(index) => index, + None => return Err(format!("missing enum {enum_name}")), + }; let after_start = &source[enum_start..]; - let open_rel = after_start - .find('{') - .ok_or_else(|| format!("missing opening brace for enum {enum_name}"))?; + let open_rel = match after_start.find('{') { + Some(index) => index, + None => return Err(format!("missing opening brace for enum {enum_name}")), + }; let open_idx = enum_start + open_rel; let mut depth = 0usize; for (offset, ch) in source[open_idx..].char_indices() { @@ -480,8 +503,10 @@ fn validate_core_unit_dimension_variant_order(workspace_root: &Path) -> Result<( .join("core") .join("src") .join("unit.rs"); - let source = fs::read_to_string(&source_path) - .map_err(|e| format!("read {}: {e}", source_path.display()))?; + let source = match fs::read_to_string(&source_path) { + Ok(source) => source, + Err(e) => return Err(format!("read {}: {e}", source_path.display())), + }; let enum_body = extract_enum_body(&source, CORE_UNIT_DIMENSION_ENUM)?; let variants = parse_enum_variants(enum_body); let expected = CORE_UNIT_DIMENSION_ORDER @@ -714,19 +739,22 @@ fn validate_release_publish_policy( .collect::<BTreeMap<_, _>>(); let dependencies = read_workspace_package_dependencies(workspace_root)?; for crate_name in &publish_set { - let crate_deps = dependencies - .get(crate_name) - .ok_or_else(|| format!("missing dependency graph entry for {}", crate_name))?; - let crate_order = *order_index - .get(crate_name) - .ok_or_else(|| format!("missing publish order entry for {}", crate_name))?; + let crate_deps = match dependencies.get(crate_name) { + Some(crate_deps) => crate_deps, + None => return Err(format!("missing dependency graph entry for {}", crate_name)), + }; + let crate_order = match order_index.get(crate_name) { + Some(crate_order) => *crate_order, + None => return Err(format!("missing publish order entry for {}", crate_name)), + }; for dep in crate_deps { if !publish_set.contains(dep) { continue; } - let dep_order = *order_index - .get(dep) - .ok_or_else(|| format!("missing publish order entry for {}", dep))?; + let dep_order = match order_index.get(dep) { + Some(dep_order) => *dep_order, + None => return Err(format!("missing publish order entry for {}", dep)), + }; if dep_order >= crate_order { return Err(format!( "publish order must place dependency {} before {}", @@ -738,9 +766,10 @@ fn validate_release_publish_policy( let publish_flags = workspace_package_publish_flags(workspace_root)?; for crate_name in &publish_set { - let flag = publish_flags - .get(crate_name) - .ok_or_else(|| format!("missing publish flag entry for {}", crate_name))?; + let flag = match publish_flags.get(crate_name) { + Some(flag) => flag, + None => return Err(format!("missing publish flag entry for {}", crate_name)), + }; if !*flag { return Err(format!( "publish crate {} must not set publish = false", @@ -749,9 +778,10 @@ fn validate_release_publish_policy( } } for crate_name in &internal_set { - let flag = publish_flags - .get(crate_name) - .ok_or_else(|| format!("missing publish flag entry for {}", crate_name))?; + let flag = match publish_flags.get(crate_name) { + Some(flag) => flag, + None => return Err(format!("missing publish flag entry for {}", crate_name)), + }; if *flag { return Err(format!( "internal crate {} must set publish = false", @@ -781,10 +811,14 @@ pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, Str let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?; let exports_dir = root.join("exports"); let mut exports = Vec::new(); - let mut entries = fs::read_dir(&exports_dir) - .map_err(|e| format!("read dir {}: {e}", exports_dir.display()))? - .collect::<Result<Vec<_>, _>>() - .map_err(|e| format!("read dir entries {}: {e}", exports_dir.display()))?; + let read_dir = match fs::read_dir(&exports_dir) { + Ok(read_dir) => read_dir, + Err(e) => return Err(format!("read dir {}: {e}", exports_dir.display())), + }; + let mut entries = match read_dir.collect::<Result<Vec<_>, _>>() { + Ok(entries) => entries, + Err(e) => return Err(format!("read dir entries {}: {e}", exports_dir.display())), + }; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); @@ -840,10 +874,10 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { )); } if mapping.language.id == "ts" { - let artifacts = mapping - .artifacts - .as_ref() - .ok_or_else(|| "artifacts map is required for ts".to_string())?; + let artifacts = match mapping.artifacts.as_ref() { + Some(artifacts) => artifacts, + None => return Err("artifacts map is required for ts".to_string()), + }; if artifacts .models_dir .as_deref() @@ -911,7 +945,7 @@ mod tests { use super::*; use std::collections::BTreeSet; use std::fs; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; fn workspace_root() -> PathBuf { @@ -932,6 +966,151 @@ mod tests { root } + fn write_file(path: &Path, content: &str) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("create parent"); + } + fs::write(path, content).expect("write file"); + } + + fn create_synthetic_workspace(prefix: &str) -> PathBuf { + let root = temp_root(prefix); + write_file( + &root.join("Cargo.toml"), + r#"[workspace] +members = ["crates/a", "crates/b"] +resolver = "2" +"#, + ); + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +description = "crate a" +repository = "https://example.com/a" +homepage = "https://example.com/a" +documentation = "https://docs.example.com/a" +readme = "README.md" +"#, + ); + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + r#"[package] +name = "radroots-b" +version = "0.1.0" +edition = "2024" +publish = false +"#, + ); + write_file( + &root.join("crates").join("core").join("src").join("unit.rs"), + r#"pub enum RadrootsCoreUnitDimension { + Count, + Mass, + Volume, +} +"#, + ); + + write_file( + &root.join("contract").join("manifest.toml"), + r#"[contract] +name = "radroots-contract" +version = "1.0.0" +source = "synthetic" + +[surface] +model_crates = ["radroots-a"] +algorithm_crates = ["radroots-b"] +wasm_crates = ["radroots-a-wasm"] + +[policy] +exclude_internal_workspace_crates = true +require_reproducible_exports = true +require_conformance_vectors = true +"#, + ); + write_file( + &root.join("contract").join("version.toml"), + r#"[contract] +version = "1.0.0" +stability = "alpha" + +[semver] +major_on = ["breaking"] +minor_on = ["feature"] +patch_on = ["fix"] + +[compatibility] +requires_conformance_pass = true +requires_export_manifest_diff = true +requires_release_notes = true +"#, + ); + write_file( + &root.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +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"] +"#, + ); + write_file( + &root + .join("contract") + .join("release") + .join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + write_file( + &root + .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", + ); + root + } + #[test] fn validate_current_contract_bundle() { let root = workspace_root(); @@ -1125,4 +1304,554 @@ readme = { workspace = true } assert!(err.contains("package.description")); let _ = fs::remove_dir_all(&root); } + + #[test] + fn synthetic_workspace_validates_contract_and_release_preflight() { + let root = create_synthetic_workspace("synthetic_valid"); + let bundle = load_contract_bundle(&root).expect("load synthetic bundle"); + validate_contract_bundle(&bundle).expect("validate synthetic bundle"); + validate_release_preflight(&root).expect("validate synthetic preflight"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn helper_functions_cover_error_paths() { + let empty = collect_unique_set(&["".to_string()], "field").expect_err("empty value"); + assert!(empty.contains("field contains an empty crate name")); + let duplicate = collect_unique_set(&["a".to_string(), "a".to_string()], "field") + .expect_err("duplicate value"); + assert!(duplicate.contains("field has duplicate crate a")); + + let values = ["b".to_string(), "a".to_string()]; + let set = collect_unique_set(&values, "field").expect("unique values"); + assert_eq!(join_set(&set), "a, b".to_string()); + + assert!(package_publish_enabled(None)); + assert!(package_publish_enabled(Some(&PackagePublish::Bool(true)))); + assert!(!package_publish_enabled(Some(&PackagePublish::Bool(false)))); + assert!(package_publish_enabled(Some(&PackagePublish::Registries( + vec!["crates-io".to_string(),] + )))); + assert!(!package_publish_enabled(Some(&PackagePublish::Registries( + Vec::new() + )))); + + let mut package = toml::value::Table::new(); + package.insert("description".to_string(), toml::Value::Integer(42)); + assert!(!package_field_configured(&package, "description")); + } + + #[test] + fn workspace_package_manifests_reject_duplicate_package_names() { + let root = temp_root("workspace_manifest_duplicates"); + write_file( + &root.join("Cargo.toml"), + r#"[workspace] +members = ["crates/a", "crates/b"] +"#, + ); + let package_manifest = + "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + package_manifest, + ); + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + package_manifest, + ); + let err = workspace_package_manifests(&root) + .expect_err("duplicate package names in manifest map"); + assert!(err.contains("duplicate workspace package name in manifest map")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn coverage_refresh_parsing_and_summary_errors_are_reported() { + let root = temp_root("coverage_refresh_errors"); + let coverage_dir = root.join("target").join("coverage"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + + write_file( + &coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\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")); + + write_file( + &coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-a\tpass\tnot-a-number\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", + ); + let required = ["radroots-a".to_string()] + .into_iter() + .collect::<BTreeSet<_>>(); + let non_pass = + validate_required_coverage_summary(&root, &required).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\treport\nradroots-a\tpass\t99.9\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")); + + let missing = ["missing".to_string()].into_iter().collect::<BTreeSet<_>>(); + let missing_err = + validate_required_coverage_summary(&root, &missing).expect_err("missing required row"); + assert!(missing_err.contains("missing from coverage-refresh.tsv")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn enum_extract_and_parse_error_paths_are_reported() { + let missing = extract_enum_body("pub struct X;", "RadrootsCoreUnitDimension") + .expect_err("missing enum"); + assert!(missing.contains("missing enum")); + + let missing_brace = extract_enum_body( + "pub enum RadrootsCoreUnitDimension", + "RadrootsCoreUnitDimension", + ) + .expect_err("missing opening brace"); + assert!(missing_brace.contains("missing opening brace")); + + let missing_close = extract_enum_body( + "pub enum RadrootsCoreUnitDimension { Count, Mass", + "RadrootsCoreUnitDimension", + ) + .expect_err("missing closing brace"); + assert!(missing_close.contains("missing closing brace")); + + let variants = parse_enum_variants( + r#" + , + = 1, + // skip + #![cfg(test)] + Count, + "#, + ); + assert_eq!(variants, vec!["Count".to_string()]); + } + + #[test] + fn coverage_rollout_parity_reports_contract_errors() { + let root = create_synthetic_workspace("rollout_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")); + + 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 }, +] +"#, + ); + 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")); + + 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 }, +] +"#, + ); + let duplicate = + validate_coverage_rollout_parity(&root, &contract_root).expect_err("duplicate rollout"); + assert!(duplicate.contains("duplicate coverage rollout crate")); + + 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 }, +] +"#, + ); + 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")); + + write_file( + &contract_root.join("coverage").join("rollout.toml"), + r#"[rollout] +crates = [ + { name = "radroots-a", status = "required", order = 1 }, +] +"#, + ); + let missing_workspace = validate_coverage_rollout_parity(&root, &contract_root) + .expect_err("missing workspace crate in rollout"); + assert!(missing_workspace.contains("missing workspace crates")); + + 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 }, +] +"#, + ); + 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")); + + 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")); + + write_file( + &contract_root.join("coverage").join("required-crates.toml"), + "[required]\ncrates = [\"unknown\"]\n", + ); + let required_unknown = validate_coverage_rollout_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")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn release_publish_policy_reports_contract_errors() { + let root = create_synthetic_workspace("release_policy_errors"); + let contract_root = root.join("contract"); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let empty_version = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("empty release version"); + assert!(empty_version.contains("must not be empty")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "2.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let version_mismatch = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("release version mismatch"); + assert!(version_mismatch.contains("must match contract version")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-a"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let overlap = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("publish/internal overlap"); + assert!(overlap.contains("overlap is not allowed")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = [] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let missing_workspace = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("missing workspace crate"); + assert!(missing_workspace.contains("missing workspace crates")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = [] +"#, + ); + let missing_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("missing publish order entries"); + assert!(missing_publish_order.contains("missing publish crates")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a", "radroots-b"] +"#, + ); + let extra_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("extra publish order entries"); + assert!(extra_publish_order.contains("non-publish crates")); + + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +description = "crate a" +repository = "https://example.com/a" +homepage = "https://example.com/a" +documentation = "https://docs.example.com/a" +readme = "README.md" + +[dependencies] +radroots-b = { path = "../b" } +"#, + ); + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + r#"[package] +name = "radroots-b" +version = "0.1.0" +edition = "2024" +description = "crate b" +repository = "https://example.com/b" +homepage = "https://example.com/b" +documentation = "https://docs.example.com/b" +readme = "README.md" +"#, + ); + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a", "radroots-b"] + +[internal] +crates = [] + +[publish_order] +crates = ["radroots-a", "radroots-b"] +"#, + ); + let dependency_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("dependency order violation"); + assert!(dependency_order.contains("must place dependency")); + + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +publish = false +"#, + ); + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let publish_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("publish crate must be publishable"); + assert!(publish_flag.contains("must not set publish = false")); + + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +description = "crate a" +repository = "https://example.com/a" +homepage = "https://example.com/a" +documentation = "https://docs.example.com/a" +readme = "README.md" +"#, + ); + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + r#"[package] +name = "radroots-b" +version = "0.1.0" +edition = "2024" +"#, + ); + let internal_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("internal crate must be non-publishable"); + assert!(internal_flag.contains("must set publish = false")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn validate_contract_bundle_reports_required_field_errors() { + let root = create_synthetic_workspace("contract_bundle_errors"); + + let assert_bundle_error = |expected: &str, mutator: fn(&mut ContractBundle)| { + let mut bundle = load_contract_bundle(&root).expect("load bundle"); + mutator(&mut bundle); + let err = validate_contract_bundle(&bundle).expect_err("bundle validation error"); + assert!(err.contains(expected), "expected `{expected}` in `{err}`"); + }; + + assert_bundle_error("contract name is required", |bundle| { + bundle.manifest.contract.name.clear(); + }); + assert_bundle_error("contract version is required", |bundle| { + bundle.manifest.contract.version.clear(); + }); + assert_bundle_error("contract source is required", |bundle| { + bundle.manifest.contract.source.clear(); + }); + assert_bundle_error("surface.model_crates must not be empty", |bundle| { + bundle.manifest.surface.model_crates.clear(); + }); + assert_bundle_error("surface.algorithm_crates must not be empty", |bundle| { + bundle.manifest.surface.algorithm_crates.clear(); + }); + assert_bundle_error("surface.wasm_crates must not be empty", |bundle| { + bundle.manifest.surface.wasm_crates.clear(); + }); + assert_bundle_error("language.id is required", |bundle| { + bundle.exports[0].language.id.clear(); + }); + assert_bundle_error("language.repository is required", |bundle| { + bundle.exports[0].language.repository.clear(); + }); + assert_bundle_error("packages map is required", |bundle| { + bundle.exports[0].packages.clear(); + }); + assert_bundle_error("artifacts fields must be non-empty for ts", |bundle| { + bundle.exports[0] + .artifacts + .as_mut() + .expect("ts artifacts") + .models_dir = Some(String::new()); + }); + assert_bundle_error("version.contract.version is required", |bundle| { + bundle.version.contract.version.clear(); + }); + assert_bundle_error("version.contract.stability is required", |bundle| { + bundle.version.contract.stability.clear(); + }); + assert_bundle_error("version.semver rules must all be non-empty", |bundle| { + bundle.version.semver.major_on.clear(); + }); + assert_bundle_error( + "compatibility.requires_conformance_pass must be true", + |bundle| { + bundle.version.compatibility.requires_conformance_pass = false; + }, + ); + assert_bundle_error( + "compatibility.requires_export_manifest_diff must be true", + |bundle| { + bundle.version.compatibility.requires_export_manifest_diff = false; + }, + ); + assert_bundle_error( + "compatibility.requires_release_notes must be true", + |bundle| { + bundle.version.compatibility.requires_release_notes = false; + }, + ); + assert_bundle_error("contract policy flags must all be true", |bundle| { + bundle.manifest.policy.exclude_internal_workspace_crates = false; + }); + + let _ = fs::remove_dir_all(root); + } } diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -175,10 +175,14 @@ struct CoverageProfile { } pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { - let raw = fs::read_to_string(path) - .map_err(|err| format!("failed to read summary {}: {err}", path.display()))?; - let parsed: LlvmCovSummaryRoot = serde_json::from_str(&raw) - .map_err(|err| format!("failed to parse summary {}: {err}", path.display()))?; + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read summary {}: {err}", path.display())), + }; + let parsed: LlvmCovSummaryRoot = match serde_json::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => return Err(format!("failed to parse summary {}: {err}", path.display())), + }; let totals = parsed .data .first() @@ -193,10 +197,24 @@ pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { } fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { - let raw = fs::read_to_string(path) - .map_err(|err| format!("failed to read required crates {}: {err}", path.display()))?; - let parsed: CoverageRequiredContract = toml::from_str(&raw) - .map_err(|err| format!("failed to parse required crates {}: {err}", path.display()))?; + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => { + return Err(format!( + "failed to read required crates {}: {err}", + path.display() + )); + } + }; + let parsed: CoverageRequiredContract = match toml::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "failed to parse required crates {}: {err}", + path.display() + )); + } + }; if parsed.required.crates.is_empty() { return Err("coverage required crates list must not be empty".to_string()); } @@ -240,9 +258,14 @@ fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> { } fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { - let raw = fs::read_to_string(path) - .map_err(|err| format!("failed to read {}: {err}", path.display()))?; - toml::from_str::<T>(&raw).map_err(|err| format!("failed to parse {}: {err}", path.display())) + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read {}: {err}", path.display())), + }; + match toml::from_str::<T>(&raw) { + Ok(parsed) => Ok(parsed), + Err(err) => Err(format!("failed to parse {}: {err}", path.display())), + } } fn merge_coverage_profile( @@ -304,8 +327,10 @@ fn read_coverage_profile( } pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { - let raw = fs::read_to_string(path) - .map_err(|err| format!("failed to read lcov {}: {err}", path.display()))?; + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read lcov {}: {err}", path.display())), + }; let mut da_total: u64 = 0; let mut da_covered: u64 = 0; @@ -321,9 +346,15 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { let Some((_, hit)) = value.split_once(',') else { return Err(format!("invalid DA record in {}", path.display())); }; - let hit_count: u64 = hit.parse().map_err(|err| { - format!("invalid DA hit count `{hit}` in {}: {err}", path.display()) - })?; + let hit_count: u64 = match hit.parse() { + Ok(hit_count) => hit_count, + Err(err) => { + return Err(format!( + "invalid DA hit count `{hit}` in {}: {err}", + path.display() + )); + } + }; da_total = da_total.saturating_add(1); if hit_count > 0 { da_covered = da_covered.saturating_add(1); @@ -331,30 +362,54 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { continue; } if let Some(value) = line.strip_prefix("LF:") { - let parsed: u64 = value.parse().map_err(|err| { - format!("invalid LF value `{value}` in {}: {err}", path.display()) - })?; + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid LF value `{value}` in {}: {err}", + path.display() + )); + } + }; executable_total = executable_total.saturating_add(parsed); continue; } if let Some(value) = line.strip_prefix("LH:") { - let parsed: u64 = value.parse().map_err(|err| { - format!("invalid LH value `{value}` in {}: {err}", path.display()) - })?; + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid LH value `{value}` in {}: {err}", + path.display() + )); + } + }; executable_covered = executable_covered.saturating_add(parsed); continue; } if let Some(value) = line.strip_prefix("BRF:") { - let parsed: u64 = value.parse().map_err(|err| { - format!("invalid BRF value `{value}` in {}: {err}", path.display()) - })?; + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid BRF value `{value}` in {}: {err}", + path.display() + )); + } + }; branch_total_lcov = branch_total_lcov.saturating_add(parsed); continue; } if let Some(value) = line.strip_prefix("BRH:") { - let parsed: u64 = value.parse().map_err(|err| { - format!("invalid BRH value `{value}` in {}: {err}", path.display()) - })?; + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid BRH value `{value}` in {}: {err}", + path.display() + )); + } + }; branch_covered_lcov = branch_covered_lcov.saturating_add(parsed); continue; } @@ -378,12 +433,15 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { if taken == "-" { continue; } - let hit_count: u64 = taken.parse().map_err(|err| { - format!( - "invalid BRDA taken count `{taken}` in {}: {err}", - path.display() - ) - })?; + let hit_count: u64 = match taken.parse() { + Ok(hit_count) => hit_count, + Err(err) => { + return Err(format!( + "invalid BRDA taken count `{taken}` in {}: {err}", + path.display() + )); + } + }; branch_total_brda = branch_total_brda.saturating_add(1); if hit_count > 0 { branch_covered_brda = branch_covered_brda.saturating_add(1); @@ -536,19 +594,16 @@ fn parse_bool_flag(args: &[String], name: &str) -> bool { fn workspace_root() -> Result<PathBuf, String> { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let Some(crates_dir) = manifest_dir.parent() else { - return Err("failed to resolve crates dir".to_string()); - }; - let Some(root) = crates_dir.parent() else { - return Err("failed to resolve workspace root".to_string()); - }; + let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); + let root = crates_dir.parent().unwrap_or(crates_dir); Ok(root.to_path_buf()) } fn run_command(mut command: Command, name: &str) -> Result<(), String> { - let status = command - .status() - .map_err(|err| format!("failed to run {name}: {err}"))?; + let status = match command.status() { + Ok(status) => status, + Err(err) => return Err(format!("failed to run {name}: {err}")), + }; if !status.success() { return Err(format!("{name} failed with status {status}")); } @@ -564,7 +619,10 @@ fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile } } -fn run_crate(args: &[String]) -> Result<(), String> { +fn run_crate_with_runner<F>(args: &[String], mut runner: F) -> Result<(), String> +where + F: FnMut(Command, &str) -> Result<(), String>, +{ let crate_name = parse_string_arg(args, "crate")?; let workspace_root = workspace_root()?; let profile = read_coverage_profile(&workspace_root, &crate_name)?; @@ -583,7 +641,7 @@ fn run_crate(args: &[String]) -> Result<(), String> { fs::create_dir_all(&out_dir) .map_err(|err| format!("failed to create {}: {err}", out_dir.display()))?; - run_command( + runner( { let mut cmd = Command::new("rustup"); cmd.arg("run") @@ -598,7 +656,7 @@ fn run_crate(args: &[String]) -> Result<(), String> { "cargo llvm-cov clean --workspace", )?; - run_command( + runner( { let mut cmd = Command::new("rustup"); cmd.arg("run").arg("nightly").arg("cargo").arg("llvm-cov"); @@ -615,7 +673,7 @@ fn run_crate(args: &[String]) -> Result<(), String> { )?; let summary_path = out_dir.join("coverage-summary.json"); - run_command( + runner( { let mut cmd = Command::new("rustup"); cmd.arg("run").arg("nightly").arg("cargo").arg("llvm-cov"); @@ -632,7 +690,7 @@ fn run_crate(args: &[String]) -> Result<(), String> { )?; let lcov_path = out_dir.join("coverage-lcov.info"); - run_command( + runner( { let mut cmd = Command::new("rustup"); cmd.arg("run").arg("nightly").arg("cargo").arg("llvm-cov"); @@ -652,6 +710,10 @@ fn run_crate(args: &[String]) -> Result<(), String> { Ok(()) } +fn run_crate(args: &[String]) -> Result<(), String> { + run_crate_with_runner(args, run_command) +} + fn report_gate(args: &[String]) -> Result<(), String> { let scope = parse_string_arg(args, "scope")?; let summary_path = PathBuf::from(parse_string_arg(args, "summary")?); @@ -701,10 +763,13 @@ fn report_gate(args: &[String]) -> Result<(), String> { }, }; - let json = serde_json::to_string_pretty(&report) - .map_err(|err| format!("failed to encode coverage report json: {err}"))?; - fs::write(&out_path, format!("{json}\n")) - .map_err(|err| format!("failed to write {}: {err}", out_path.display()))?; + let json = match serde_json::to_string_pretty(&report) { + Ok(json) => json, + Err(err) => return Err(format!("failed to encode coverage report json: {err}")), + }; + if let Err(err) = fs::write(&out_path, format!("{json}\n")) { + return Err(format!("failed to write {}: {err}", out_path.display())); + } if lcov.branches_available { eprintln!( @@ -745,8 +810,9 @@ fn list_required_crates() -> Result<(), String> { let crates = read_required_crates(&required_path)?; let mut stdout = std::io::stdout().lock(); for crate_name in crates { - writeln!(stdout, "{crate_name}") - .map_err(|err| format!("failed to write required crates output: {err}"))?; + if let Err(err) = writeln!(stdout, "{crate_name}") { + return Err(format!("failed to write required crates output: {err}")); + } } Ok(()) } @@ -756,8 +822,9 @@ fn list_workspace_crates() -> Result<(), String> { let crates = read_workspace_crates(&root)?; let mut stdout = std::io::stdout().lock(); for crate_name in crates { - writeln!(stdout, "{crate_name}") - .map_err(|err| format!("failed to write workspace crates output: {err}"))?; + if let Err(err) = writeln!(stdout, "{crate_name}") { + return Err(format!("failed to write workspace crates output: {err}")); + } } Ok(()) } @@ -778,6 +845,7 @@ pub fn run(args: &[String]) -> Result<(), String> { mod tests { use super::*; use std::fs; + use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; fn temp_file_path(prefix: &str) -> PathBuf { @@ -796,6 +864,13 @@ mod tests { std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}")) } + fn write_file(path: &Path, content: &str) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("create parent"); + } + fs::write(path, content).expect("write file"); + } + #[test] fn reads_summary_totals_from_llvm_cov_json() { let path = temp_file_path("summary"); @@ -979,4 +1054,478 @@ test_threads = 0 fs::remove_dir_all(root).expect("remove root"); } + + #[test] + fn coverage_profiles_reject_zero_test_threads_without_feature_error() { + let root = temp_dir_path("profile_invalid_threads"); + let coverage_dir = root.join("contract").join("coverage"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("profiles.toml"), + r#"[profiles.crates."radroots-app-core"] +test_threads = 0 +"#, + ) + .expect("write profiles"); + + let err = + read_coverage_profile(&root, "radroots-app-core").expect_err("invalid thread count"); + assert!(err.contains("test_threads > 0")); + + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn parse_helpers_cover_success_and_error_paths() { + let args = vec![ + "--scope".to_string(), + "crate-a".to_string(), + "--value".to_string(), + "3.5".to_string(), + "--threads".to_string(), + "4".to_string(), + "--flag".to_string(), + ]; + assert_eq!( + parse_string_arg(&args, "scope").expect("scope value"), + "crate-a".to_string() + ); + assert_eq!( + parse_optional_string_arg(&args, "scope").expect("optional scope"), + "crate-a".to_string() + ); + assert_eq!(parse_f64_arg(&args, "value", 1.0).expect("f64 value"), 3.5); + assert_eq!( + parse_optional_u32_arg(&args, "threads").expect("u32 value"), + Some(4) + ); + assert!(parse_bool_flag(&args, "flag")); + assert_eq!(parse_optional_string_arg(&args, "missing"), None); + assert_eq!( + parse_f64_arg(&args, "missing", 2.25).expect("default f64"), + 2.25 + ); + assert_eq!( + parse_optional_u32_arg(&args, "missing").expect("missing u32"), + None + ); + + let missing_err = parse_string_arg(&args, "absent").expect_err("missing arg"); + assert!(missing_err.contains("missing --absent")); + + let missing_value = vec!["--scope".to_string()]; + let missing_value_err = + parse_string_arg(&missing_value, "scope").expect_err("missing arg value"); + assert!(missing_value_err.contains("missing value for --scope")); + + let invalid_f64 = vec!["--value".to_string(), "bad".to_string()]; + let invalid_f64_err = parse_f64_arg(&invalid_f64, "value", 1.0).expect_err("invalid f64"); + assert!(invalid_f64_err.contains("invalid --value value")); + + let invalid_u32 = vec!["--threads".to_string(), "bad".to_string()]; + let invalid_u32_err = + parse_optional_u32_arg(&invalid_u32, "threads").expect_err("invalid u32"); + assert!(invalid_u32_err.contains("invalid --threads value")); + } + + #[test] + fn executable_source_labels_cover_all_variants() { + assert_eq!(executable_source_label(ExecutableSource::Da), "da"); + assert_eq!(executable_source_label(ExecutableSource::LfLh), "lf_lh"); + } + + #[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"); + 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"); + 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"); + } + + #[test] + fn read_workspace_crates_rejects_invalid_workspace_shapes() { + let root_empty = temp_dir_path("workspace_empty_members"); + write_file( + &root_empty.join("Cargo.toml"), + "[workspace]\nmembers = []\n", + ); + let empty_err = read_workspace_crates(&root_empty).expect_err("empty workspace members"); + assert!(empty_err.contains("must not be empty")); + fs::remove_dir_all(&root_empty).expect("remove empty members root"); + + let root_blank = temp_dir_path("workspace_blank_package_name"); + write_file( + &root_blank.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\"]\n", + ); + write_file( + &root_blank.join("crates").join("a").join("Cargo.toml"), + "[package]\nname = \"\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ); + let blank_err = read_workspace_crates(&root_blank).expect_err("blank package name"); + assert!(blank_err.contains("empty package name")); + fs::remove_dir_all(&root_blank).expect("remove blank package root"); + + let root_duplicate = temp_dir_path("workspace_duplicate_package"); + write_file( + &root_duplicate.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n", + ); + let package_manifest = + "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; + write_file( + &root_duplicate.join("crates").join("a").join("Cargo.toml"), + package_manifest, + ); + write_file( + &root_duplicate.join("crates").join("b").join("Cargo.toml"), + package_manifest, + ); + let dup_err = read_workspace_crates(&root_duplicate).expect_err("duplicate package names"); + assert!(dup_err.contains("duplicate package name")); + fs::remove_dir_all(&root_duplicate).expect("remove duplicate package root"); + } + + #[test] + fn read_lcov_rejects_invalid_records() { + let cases = vec![ + ("invalid_da_shape", "DA:1\n", "invalid DA record"), + ("invalid_da_hits", "DA:1,bad\n", "invalid DA hit count"), + ("invalid_lf", "LF:bad\n", "invalid LF value"), + ("invalid_lh", "LH:bad\n", "invalid LH value"), + ("invalid_brf", "BRF:bad\n", "invalid BRF value"), + ("invalid_brh", "BRH:bad\n", "invalid BRH value"), + ("invalid_brda_shape", "BRDA:1,0,0\n", "invalid BRDA record"), + ( + "invalid_brda_taken", + "BRDA:1,0,0,bad\n", + "invalid BRDA taken count", + ), + ( + "invalid_brda_extra", + "BRDA:1,0,0,1,extra\n", + "invalid BRDA record", + ), + ]; + for (prefix, raw, expected) in cases { + let path = temp_file_path(prefix); + write_file(&path, raw); + let err = read_lcov(&path).expect_err("invalid lcov record"); + assert!( + err.contains(expected), + "expected `{expected}` in `{err}` for case {prefix}" + ); + fs::remove_file(path).expect("remove invalid lcov file"); + } + } + + #[test] + fn read_lcov_uses_lf_lh_when_da_is_missing_and_branches_absent() { + let path = temp_file_path("lcov_lf_lh"); + fs::write(&path, "LF:4\nLH:3\n").expect("write lcov"); + let parsed = read_lcov(&path).expect("parse lcov"); + assert!(matches!(parsed.executable_source, ExecutableSource::LfLh)); + assert_eq!(parsed.executable_total, 4); + assert_eq!(parsed.executable_covered, 3); + assert_eq!(parsed.executable_percent, 75.0); + assert!(!parsed.branches_available); + assert_eq!(parsed.branch_percent, None); + fs::remove_file(path).expect("remove lcov"); + } + + #[test] + fn evaluate_gate_collects_all_failure_reasons() { + let summary = CoverageSummary { + functions_percent: 40.0, + summary_lines_percent: 50.0, + summary_regions_percent: 60.0, + }; + let lcov = LcovCoverage { + executable_total: 20, + executable_covered: 10, + executable_percent: 50.0, + executable_source: ExecutableSource::Da, + branch_total: 10, + branch_covered: 3, + branches_available: true, + branch_percent: Some(30.0), + }; + let thresholds = CoverageThresholds { + fail_under_exec_lines: 90.0, + fail_under_functions: 90.0, + fail_under_branches: 90.0, + require_branches: true, + }; + + let gate = evaluate_gate(&summary, &lcov, thresholds); + assert!(!gate.pass); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("executable_lines")) + ); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("functions")) + ); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("branches")) + ); + } + + #[test] + fn run_command_covers_success_and_failure() { + let mut ok = Command::new("sh"); + ok.arg("-c").arg("exit 0"); + run_command(ok, "shell ok").expect("run ok command"); + + let mut fail = Command::new("sh"); + fail.arg("-c").arg("exit 9"); + let err = run_command(fail, "shell fail").expect_err("run failing command"); + assert!(err.contains("shell fail failed with status")); + } + + #[test] + fn apply_coverage_profile_flags_writes_expected_args() { + let profile = CoverageProfile { + no_default_features: true, + features: vec!["std".to_string(), "serde".to_string()], + test_threads: Some(2), + }; + let mut command = Command::new("cargo"); + apply_coverage_profile_flags(&mut command, &profile); + let args = command + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>(); + assert_eq!( + args, + vec![ + "--no-default-features".to_string(), + "--features".to_string(), + "std,serde".to_string() + ] + ); + } + + #[test] + fn run_crate_with_runner_builds_all_command_steps() { + let out = temp_dir_path("run_crate_runner"); + let args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + out.display().to_string(), + "--test-threads".to_string(), + "3".to_string(), + ]; + let mut names = Vec::new(); + run_crate_with_runner(&args, |cmd, name| { + names.push(name.to_string()); + let rendered = cmd + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>() + .join(" "); + assert!(!rendered.is_empty()); + Ok(()) + }) + .expect("run crate with stub runner"); + assert_eq!( + names, + vec![ + "cargo llvm-cov clean --workspace".to_string(), + "cargo llvm-cov --no-report".to_string(), + "cargo llvm-cov report --json --summary-only".to_string(), + "cargo llvm-cov report --lcov".to_string(), + ] + ); + fs::remove_dir_all(out).expect("remove run crate output dir"); + } + + #[test] + fn run_crate_with_runner_uses_default_output_dir_when_out_is_missing() { + let args = vec!["--crate".to_string(), "radroots-core".to_string()]; + let mut output_path_seen = false; + run_crate_with_runner(&args, |cmd, _| { + let rendered = cmd + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>(); + if rendered + .iter() + .any(|arg| arg.ends_with("coverage-summary.json")) + || rendered + .iter() + .any(|arg| arg.ends_with("coverage-lcov.info")) + { + output_path_seen = true; + } + Ok(()) + }) + .expect("run crate with default out"); + assert!(output_path_seen); + } + + #[test] + fn run_crate_with_runner_propagates_runner_failures() { + let out = temp_dir_path("run_crate_runner_fail"); + let args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + out.display().to_string(), + ]; + let err = run_crate_with_runner(&args, |_, _| Err("runner failed".to_string())) + .expect_err("runner failure should bubble up"); + assert_eq!(err, "runner failed".to_string()); + fs::remove_dir_all(out).expect("remove run crate failure output dir"); + } + + #[test] + fn run_crate_wrapper_returns_missing_crate_error_without_running_commands() { + let err = run_crate(&[]).expect_err("missing crate flag"); + assert!(err.contains("missing --crate")); + } + + #[test] + fn report_gate_writes_report_file_on_success() { + let root = temp_dir_path("report_gate_success"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let args = vec![ + "--scope".to_string(), + "crate-x".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(), + "--require-branches".to_string(), + ]; + 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("\"pass\": true")); + fs::remove_dir_all(root).expect("remove report gate success root"); + } + + #[test] + fn report_gate_returns_error_on_failed_thresholds() { + let root = temp_dir_path("report_gate_fail"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":10.0},"lines":{"percent":10.0},"regions":{"percent":10.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,0\nBRDA:1,0,0,0\n"); + + let args = vec![ + "--scope".to_string(), + "crate-y".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(), + "--fail-under-exec-lines".to_string(), + "100.0".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + "--fail-under-branches".to_string(), + "100.0".to_string(), + ]; + let err = report_gate(&args).expect_err("report gate failure"); + assert!(err.contains("coverage gate failed")); + fs::remove_dir_all(root).expect("remove report gate failure root"); + } + + #[test] + fn report_gate_logs_branch_unavailable_path() { + let root = temp_dir_path("report_gate_no_branches"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\n"); + + let args = vec![ + "--scope".to_string(), + "crate-no-branch".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(), + ]; + report_gate(&args).expect("report gate no branches"); + let report_raw = fs::read_to_string(&out_path).expect("read report"); + assert!(report_raw.contains("\"branches_available\": false")); + fs::remove_dir_all(root).expect("remove no branch report root"); + } + + #[test] + fn run_dispatches_subcommands_and_errors() { + run(&["help".to_string()]).expect("help subcommand"); + run(&["required-crates".to_string()]).expect("required crates subcommand"); + run(&["workspace-crates".to_string()]).expect("workspace crates subcommand"); + let unknown_err = run(&["unknown".to_string()]).expect_err("unknown subcommand"); + assert!(unknown_err.contains("unknown sdk coverage subcommand")); + let missing_err = run(&[]).expect_err("missing subcommand"); + assert!(missing_err.contains("missing sdk coverage subcommand")); + } + + #[test] + fn run_report_subcommand_dispatches_to_report_gate() { + let root = temp_dir_path("run_dispatch_report"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + run(&[ + "report".to_string(), + "--scope".to_string(), + "dispatch".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(), + "--require-branches".to_string(), + ]) + .expect("dispatch report"); + assert!(out_path.exists()); + fs::remove_dir_all(root).expect("remove report dispatch root"); + } } diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -29,18 +29,21 @@ fn to_package_dir(base: &Path, package_name: &str) -> PathBuf { fn ts_export_mapping( bundle: &contract::ContractBundle, ) -> Result<&contract::ExportMapping, String> { - bundle + if let Some(mapping) = bundle .exports .iter() .find(|mapping| mapping.language.id == "ts") - .ok_or_else(|| "missing ts export mapping".to_string()) + { + return Ok(mapping); + } + Err("missing ts export mapping".to_string()) } fn ts_artifacts(mapping: &contract::ExportMapping) -> Result<&contract::ExportArtifacts, String> { - mapping - .artifacts - .as_ref() - .ok_or_else(|| "missing ts artifacts mapping".to_string()) + if let Some(artifacts) = mapping.artifacts.as_ref() { + return Ok(artifacts); + } + Err("missing ts artifacts mapping".to_string()) } fn selected_package_entries<'a>( @@ -88,10 +91,12 @@ fn selected_package_entries<'a>( } fn required_artifact_value<'a>(value: &'a Option<String>, field: &str) -> Result<&'a str, String> { - value - .as_deref() - .filter(|item| !item.trim().is_empty()) - .ok_or_else(|| format!("missing ts artifacts.{field}")) + if let Some(raw) = value.as_deref() { + if !raw.trim().is_empty() { + return Ok(raw); + } + } + Err(format!("missing ts artifacts.{field}")) } fn crate_supports_ts_rs(workspace_root: &Path, crate_dir: &str) -> Result<bool, String> { @@ -102,8 +107,10 @@ fn crate_supports_ts_rs(workspace_root: &Path, crate_dir: &str) -> Result<bool, if !manifest.exists() { return Ok(false); } - let raw = - fs::read_to_string(&manifest).map_err(|e| format!("read {}: {e}", manifest.display()))?; + let raw = match fs::read_to_string(&manifest) { + Ok(raw) => raw, + Err(e) => return Err(format!("read {}: {e}", manifest.display())), + }; Ok(raw.contains("ts-rs")) } @@ -112,9 +119,13 @@ fn copy_if_exists(src: &Path, dst: &Path) -> Result<bool, String> { return Ok(false); } if let Some(parent) = dst.parent() { - fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; + if let Err(e) = fs::create_dir_all(parent) { + return Err(format!("create {}: {e}", parent.display())); + } + } + if let Err(e) = fs::copy(src, dst) { + return Err(format!("copy {} -> {}: {e}", src.display(), dst.display())); } - fs::copy(src, dst).map_err(|e| format!("copy {} -> {}: {e}", src.display(), dst.display()))?; Ok(true) } @@ -122,24 +133,36 @@ fn copy_dir_contents(src: &Path, dst: &Path) -> Result<usize, String> { if !src.exists() { return Ok(0); } - fs::create_dir_all(dst).map_err(|e| format!("create {}: {e}", dst.display()))?; + if let Err(e) = fs::create_dir_all(dst) { + return Err(format!("create {}: {e}", dst.display())); + } let mut copied = 0usize; - let mut entries = fs::read_dir(src) - .map_err(|e| format!("read dir {}: {e}", src.display()))? - .collect::<Result<Vec<_>, _>>() - .map_err(|e| format!("read dir entries {}: {e}", src.display()))?; + let read_dir = match fs::read_dir(src) { + Ok(entries) => entries, + Err(e) => return Err(format!("read dir {}: {e}", src.display())), + }; + let mut entries = match read_dir.collect::<Result<Vec<_>, _>>() { + Ok(entries) => entries, + Err(e) => return Err(format!("read dir entries {}: {e}", src.display())), + }; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); let target = dst.join(entry.file_name()); - let file_type = entry - .file_type() - .map_err(|e| format!("read type {}: {e}", path.display()))?; + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(e) => return Err(format!("read type {}: {e}", path.display())), + }; if file_type.is_dir() { copied += copy_dir_contents(&path, &target)?; } else if file_type.is_file() { - fs::copy(&path, &target) - .map_err(|e| format!("copy {} -> {}: {e}", path.display(), target.display()))?; + if let Err(e) = fs::copy(&path, &target) { + return Err(format!( + "copy {} -> {}: {e}", + path.display(), + target.display() + )); + } copied += 1; } } @@ -155,19 +178,24 @@ fn collect_manifest_entries( if !current.exists() { return Ok(()); } - let mut dir_entries = fs::read_dir(current) - .map_err(|e| format!("read dir {}: {e}", current.display()))? - .collect::<Result<Vec<_>, _>>() - .map_err(|e| format!("read dir entries {}: {e}", current.display()))?; + let read_dir = match fs::read_dir(current) { + Ok(entries) => entries, + Err(e) => return Err(format!("read dir {}: {e}", current.display())), + }; + let mut dir_entries = match read_dir.collect::<Result<Vec<_>, _>>() { + Ok(entries) => entries, + Err(e) => return Err(format!("read dir entries {}: {e}", current.display())), + }; dir_entries.sort_by_key(|entry| entry.file_name()); for entry in dir_entries { let path = entry.path(); if path == skip_path { continue; } - let file_type = entry - .file_type() - .map_err(|e| format!("read type {}: {e}", path.display()))?; + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(e) => return Err(format!("read type {}: {e}", path.display())), + }; if file_type.is_dir() { collect_manifest_entries(root, &path, skip_path, entries)?; continue; @@ -175,13 +203,15 @@ fn collect_manifest_entries( if !file_type.is_file() { continue; } - let bytes = fs::read(&path).map_err(|e| format!("read {}: {e}", path.display()))?; + let bytes = match fs::read(&path) { + Ok(bytes) => bytes, + Err(e) => return Err(format!("read {}: {e}", path.display())), + }; let digest = Sha256::digest(&bytes); - let relative = path - .strip_prefix(root) - .map_err(|e| format!("strip prefix {}: {e}", path.display()))? - .to_string_lossy() - .replace('\\', "/"); + let relative = match path.strip_prefix(root) { + Ok(relative) => relative.to_string_lossy().replace('\\', "/"), + Err(e) => return Err(format!("strip prefix {}: {e}", path.display())), + }; entries.push(ExportManifestEntry { path: relative, sha256: hex::encode(digest), @@ -351,12 +381,22 @@ pub fn write_ts_export_manifest(workspace_root: &Path, out_dir: &Path) -> Result files, }; if let Some(parent) = manifest_path.parent() { - fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; + if let Err(e) = fs::create_dir_all(parent) { + return Err(format!("create {}: {e}", parent.display())); + } + } + let bytes = match serde_json::to_vec_pretty(&manifest) { + Ok(bytes) => bytes, + Err(e) => { + return Err(format!( + "serialize manifest {}: {e}", + manifest_path.display() + )); + } + }; + if let Err(e) = fs::write(&manifest_path, bytes) { + return Err(format!("write {}: {e}", manifest_path.display())); } - let bytes = serde_json::to_vec_pretty(&manifest) - .map_err(|e| format!("serialize manifest {}: {e}", manifest_path.display()))?; - fs::write(&manifest_path, bytes) - .map_err(|e| format!("write {}: {e}", manifest_path.display()))?; Ok(manifest_path) } @@ -370,11 +410,13 @@ fn generate_ts_rs_sources_with_selector( let selected_entries = selected_package_entries(ts_export, selector)?; let source_root = workspace_root.join("target").join("ts-rs"); if source_root.exists() { - fs::remove_dir_all(&source_root) - .map_err(|e| format!("remove {}: {e}", source_root.display()))?; + if let Err(e) = fs::remove_dir_all(&source_root) { + return Err(format!("remove {}: {e}", source_root.display())); + } + } + if let Err(e) = fs::create_dir_all(&source_root) { + return Err(format!("create {}: {e}", source_root.display())); } - fs::create_dir_all(&source_root) - .map_err(|e| format!("create {}: {e}", source_root.display()))?; let mut expected = 0usize; for (crate_name, _) in &selected_entries { if crate_name.ends_with("-wasm") { @@ -401,8 +443,9 @@ fn generate_ts_rs_sources_with_selector( .strip_prefix("@radroots/") .unwrap_or(package_name); let export_dir = source_root.join(package_dir); - fs::create_dir_all(&export_dir) - .map_err(|e| format!("create {}: {e}", export_dir.display()))?; + if let Err(e) = fs::create_dir_all(&export_dir) { + return Err(format!("create {}: {e}", export_dir.display())); + } let status = Command::new("cargo") .arg("test") .arg("-q") @@ -412,8 +455,11 @@ fn generate_ts_rs_sources_with_selector( .arg("ts-rs") .env("RADROOTS_TS_RS_EXPORT_DIR", &export_dir) .current_dir(workspace_root) - .status() - .map_err(|e| format!("run cargo test for {crate_name}: {e}"))?; + .status(); + let status = match status { + Ok(status) => status, + Err(e) => return Err(format!("run cargo test for {crate_name}: {e}")), + }; if !status.success() { return Err(format!("cargo test failed for {crate_name}")); } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -28,12 +28,8 @@ fn usage() { fn workspace_root() -> Result<PathBuf, String> { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let Some(crates_dir) = manifest_dir.parent() else { - return Err("failed to resolve crates dir".to_string()); - }; - let Some(root) = crates_dir.parent() else { - return Err("failed to resolve workspace root".to_string()); - }; + let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); + let root = crates_dir.parent().unwrap_or(crates_dir); Ok(root.to_path_buf()) } @@ -188,8 +184,7 @@ fn run(args: &[String]) -> Result<(), String> { } } -fn main() -> ExitCode { - let args: Vec<String> = env::args().skip(1).collect(); +fn main_with_args(args: Vec<String>) -> ExitCode { if args.is_empty() { usage(); return ExitCode::from(2); @@ -203,3 +198,249 @@ fn main() -> ExitCode { } } } + +fn main() -> ExitCode { + main_with_args(env::args().skip(1).collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::sync::{Mutex, OnceLock}; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn workspace_lock() -> &'static Mutex<()> { + static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn unique_temp_dir(prefix: &str) -> PathBuf { + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_xtask_main_{prefix}_{ns}")) + } + + #[test] + fn workspace_root_resolves_and_parse_helpers_cover_branches() { + let root = workspace_root().expect("workspace root"); + assert!(root.join("Cargo.toml").exists()); + + let default_out = parse_out_dir(&[], &root).expect("default out dir"); + assert_eq!(default_out, root.join("target").join("sdk-export")); + + let custom_out = parse_out_dir(&["--out".to_string(), "custom/out".to_string()], &root) + .expect("custom out dir"); + assert_eq!(custom_out, PathBuf::from("custom/out")); + + let invalid_out = parse_out_dir(&["--bad".to_string()], &root).expect_err("invalid out"); + assert!(invalid_out.contains("invalid export args")); + + let parsed = parse_crate_out_dir( + &[ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + "my/out".to_string(), + ], + &root, + ) + .expect("parsed crate out"); + assert_eq!(parsed.0, "radroots-core".to_string()); + assert_eq!(parsed.1, PathBuf::from("my/out")); + + let missing_crate = parse_crate_out_dir(&["--out".to_string(), "x".to_string()], &root) + .expect_err("missing crate selector"); + assert!(missing_crate.contains("missing required --crate")); + + let invalid_crate_args = parse_crate_out_dir( + &[ + "--crate".to_string(), + "radroots-core".to_string(), + "--bad".to_string(), + ], + &root, + ) + .expect_err("invalid crate args"); + assert!(invalid_crate_args.contains("invalid export args")); + + let missing_crate_value = + parse_crate_out_dir(&["--crate".to_string()], &root).expect_err("missing crate value"); + assert!(missing_crate_value.contains("expected --crate <crate>")); + + let missing_out_value = parse_crate_out_dir( + &[ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + ], + &root, + ) + .expect_err("missing out value"); + assert!(missing_out_value.contains("expected --out <dir>")); + } + + #[test] + fn run_release_and_dispatchers_cover_error_paths() { + let unknown_release = + run_release(&["unknown".to_string()]).expect_err("unknown release subcommand"); + assert!(unknown_release.contains("unknown release subcommand")); + + let unknown_sdk = run_sdk(&["unknown".to_string()]).expect_err("unknown sdk subcommand"); + assert!(unknown_sdk.contains("unknown sdk subcommand")); + + let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); + assert!(unknown_root.contains("unknown command")); + } + + #[test] + fn export_wrappers_cover_success_and_error_paths() { + let _guard = workspace_lock() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let root = workspace_root().expect("workspace root"); + let out_dir = unique_temp_dir("export_wrappers"); + fs::create_dir_all(&out_dir).expect("create out dir"); + + let invalid_args = vec!["--bad".to_string()]; + assert!(export_ts_models(&invalid_args).is_err()); + assert!(export_ts_constants(&invalid_args).is_err()); + assert!(export_ts_wasm(&invalid_args).is_err()); + assert!(export_manifest(&invalid_args).is_err()); + assert!(export_ts(&invalid_args).is_err()); + assert!(export_ts_crate(&invalid_args).is_err()); + + let ts_rs_root = root.join("target").join("ts-rs"); + fs::create_dir_all(ts_rs_root.join("core")).expect("create ts-rs core dir"); + fs::write( + ts_rs_root.join("core").join("types.ts"), + "export type CoreProbe = { id: string };\n", + ) + .expect("write core types"); + + let args = vec!["--out".to_string(), out_dir.display().to_string()]; + export_manifest(&args).expect("export manifest"); + export_ts_wasm(&args).expect("export wasm"); + export_ts_constants(&args).expect("export constants"); + export_ts_models(&args).expect("export models"); + + let crate_args = vec![ + "--crate".to_string(), + "core".to_string(), + "--out".to_string(), + out_dir.display().to_string(), + ]; + export_ts_crate(&crate_args).expect("export ts crate"); + + let bundle_args = vec!["--out".to_string(), out_dir.display().to_string()]; + export_ts(&bundle_args).expect("export ts bundle"); + + assert!(out_dir.join("ts").exists()); + + let _ = fs::remove_dir_all(out_dir); + } + + #[test] + fn contract_and_coverage_dispatchers_execute() { + let _guard = workspace_lock() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let root = workspace_root().expect("workspace root"); + let out_dir = unique_temp_dir("coverage_dispatch"); + fs::create_dir_all(&out_dir).expect("create out dir"); + + let coverage_refresh_path = root + .join("target") + .join("coverage") + .join("coverage-refresh.tsv"); + if coverage_refresh_path.exists() { + fs::remove_file(&coverage_refresh_path).expect("remove existing coverage refresh"); + } + if !coverage_refresh_path.exists() { + if let Some(parent) = coverage_refresh_path.parent() { + fs::create_dir_all(parent).expect("create coverage parent"); + } + let required_raw = fs::read_to_string( + root.join("contract") + .join("coverage") + .join("required-crates.toml"), + ) + .expect("read required crates contract"); + let required_toml = toml::from_str::<toml::Value>(&required_raw) + .expect("parse required crates contract"); + let required_crates = required_toml + .get("required") + .and_then(toml::Value::as_table) + .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"); + 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")); + } + fs::write(&coverage_refresh_path, rows).expect("write coverage refresh"); + } + + validate_contract().expect("validate contract"); + release_preflight().expect("release preflight"); + run_sdk(&["coverage".to_string(), "help".to_string()]).expect("coverage help"); + run_sdk(&["coverage".to_string(), "required-crates".to_string()]) + .expect("coverage required crates"); + run_sdk(&["coverage".to_string(), "workspace-crates".to_string()]) + .expect("coverage workspace crates"); + + let summary_path = out_dir.join("summary.json"); + let lcov_path = out_dir.join("coverage.info"); + let gate_out = out_dir.join("gate-report.json"); + fs::write( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ) + .expect("write summary"); + fs::write(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n").expect("write lcov"); + run_sdk(&[ + "coverage".to_string(), + "report".to_string(), + "--scope".to_string(), + "main-test".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + gate_out.display().to_string(), + "--require-branches".to_string(), + ]) + .expect("coverage report"); + + run_sdk(&["release".to_string(), "preflight".to_string()]).expect("sdk release preflight"); + + run(&[ + "sdk".to_string(), + "coverage".to_string(), + "help".to_string(), + ]) + .expect("root run sdk coverage"); + + let _ = fs::remove_dir_all(out_dir); + } + + #[test] + fn usage_and_main_entrypoints_execute() { + usage(); + let empty_code = main_with_args(Vec::new()); + assert_eq!(empty_code, ExitCode::from(2)); + let success_code = main_with_args(vec![ + "sdk".to_string(), + "coverage".to_string(), + "help".to_string(), + ]); + assert_eq!(success_code, ExitCode::SUCCESS); + let failure_code = main_with_args(vec!["unknown".to_string()]); + assert_eq!(failure_code, ExitCode::from(2)); + let _ = main(); + } +}