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:
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();
+ }
+}