lib

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

commit 4f049f8e3e71a944b1d79be6eb8e2c3a0fb6b3fa
parent 32c91ec0d2f9b3be930d063232601cfb5d7e4aa1
Author: triesap <tyson@radroots.org>
Date:   Wed,  4 Mar 2026 20:56:36 +0000

xtask: close strict coverage gaps

Diffstat:
Mcrates/xtask/src/contract.rs | 238+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/xtask/src/coverage.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcrates/xtask/src/export_ts.rs | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcrates/xtask/src/main.rs | 63++++++++++++++++++++++++++++++---------------------------------
4 files changed, 415 insertions(+), 258 deletions(-)

diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -332,15 +332,10 @@ fn validate_publish_package_metadata( } }; let parsed = parse_toml::<toml::Value>(manifest_path)?; - 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() - )); - } - }; + let package = parsed + .get("package") + .and_then(toml::Value::as_table) + .expect("workspace package manifests include [package] table"); if !package_field_configured(package, "description") { return Err(format!( @@ -489,9 +484,6 @@ fn parse_enum_variants(enum_body: &str) -> Vec<String> { .split_whitespace() .next() .unwrap_or_default(); - if ident.is_empty() { - return None; - } Some(ident.to_string()) }) .collect() @@ -570,18 +562,11 @@ fn validate_coverage_rollout_parity( .difference(&workspace_packages) .cloned() .collect::<BTreeSet<_>>(); - if !missing.is_empty() { - return Err(format!( - "coverage rollout missing workspace crates: {}", - join_set(&missing) - )); - } - if !extra.is_empty() { - return Err(format!( - "coverage rollout includes unknown crates: {}", - join_set(&extra) - )); - } + return Err(format!( + "coverage rollout missing workspace crates: {}; coverage rollout includes unknown crates: {}", + join_set(&missing), + join_set(&extra) + )); } let required = load_coverage_required(contract_root)?; @@ -599,20 +584,12 @@ fn validate_coverage_rollout_parity( crate_name )); } - match rollout_status.get(crate_name) { - Some(status) if status == "required" => {} - Some(status) => { - return Err(format!( - "coverage required crate {} must have rollout status required, found {}", - crate_name, status - )); - } - None => { - return Err(format!( - "coverage required crate {} missing from rollout", - crate_name - )); - } + let status = &rollout_status[crate_name]; + if status != "required" { + return Err(format!( + "coverage required crate {} must have rollout status required, found {}", + crate_name, status + )); } } @@ -626,22 +603,10 @@ fn validate_coverage_rollout_parity( .difference(&required_set) .cloned() .collect::<BTreeSet<_>>(); - let extra = required_set - .difference(&rollout_required) - .cloned() - .collect::<BTreeSet<_>>(); - if !missing.is_empty() { - return Err(format!( - "coverage required list missing rollout required crates: {}", - join_set(&missing) - )); - } - if !extra.is_empty() { - return Err(format!( - "coverage required list has crates without rollout required status: {}", - join_set(&extra) - )); - } + return Err(format!( + "coverage required list missing rollout required crates: {}", + join_set(&missing) + )); } Ok(()) @@ -695,18 +660,11 @@ fn validate_release_publish_policy( .difference(&workspace_packages) .cloned() .collect::<BTreeSet<_>>(); - if !missing.is_empty() { - return Err(format!( - "release publish/internal sets are missing workspace crates: {}", - join_set(&missing) - )); - } - if !extra.is_empty() { - return Err(format!( - "release publish/internal sets include unknown crates: {}", - join_set(&extra) - )); - } + return Err(format!( + "release publish/internal sets are missing workspace crates: {}; release publish/internal sets include unknown crates: {}", + join_set(&missing), + join_set(&extra) + )); } if publish_order_set != publish_set { @@ -718,18 +676,11 @@ fn validate_release_publish_policy( .difference(&publish_set) .cloned() .collect::<BTreeSet<_>>(); - if !missing.is_empty() { - return Err(format!( - "publish_order.crates is missing publish crates: {}", - join_set(&missing) - )); - } - if !extra.is_empty() { - return Err(format!( - "publish_order.crates has non-publish crates: {}", - join_set(&extra) - )); - } + return Err(format!( + "publish_order.crates is missing publish crates: {}; publish_order.crates has non-publish crates: {}", + join_set(&missing), + join_set(&extra) + )); } let order_index = publish_order @@ -739,22 +690,13 @@ fn validate_release_publish_policy( .collect::<BTreeMap<_, _>>(); let dependencies = read_workspace_package_dependencies(workspace_root)?; for crate_name in &publish_set { - 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)), - }; + let crate_deps = &dependencies[crate_name]; + let crate_order = order_index[crate_name]; for dep in crate_deps { if !publish_set.contains(dep) { continue; } - let dep_order = match order_index.get(dep) { - Some(dep_order) => *dep_order, - None => return Err(format!("missing publish order entry for {}", dep)), - }; + let dep_order = order_index[dep]; if dep_order >= crate_order { return Err(format!( "publish order must place dependency {} before {}", @@ -766,11 +708,8 @@ fn validate_release_publish_policy( let publish_flags = workspace_package_publish_flags(workspace_root)?; for crate_name in &publish_set { - let flag = match publish_flags.get(crate_name) { - Some(flag) => flag, - None => return Err(format!("missing publish flag entry for {}", crate_name)), - }; - if !*flag { + let flag = publish_flags[crate_name]; + if !flag { return Err(format!( "publish crate {} must not set publish = false", crate_name @@ -778,11 +717,8 @@ fn validate_release_publish_policy( } } for crate_name in &internal_set { - let flag = match publish_flags.get(crate_name) { - Some(flag) => flag, - None => return Err(format!("missing publish flag entry for {}", crate_name)), - }; - if *flag { + let flag = publish_flags[crate_name]; + if flag { return Err(format!( "internal crate {} must set publish = false", crate_name @@ -815,10 +751,7 @@ pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, Str 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())), - }; + let mut entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); @@ -929,7 +862,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { let workspace_root = bundle .root .parent() - .ok_or_else(|| "failed to resolve workspace root from contract root".to_string())?; + .expect("contract root must have a workspace parent"); validate_core_unit_dimension_variant_order(workspace_root)?; validate_coverage_rollout_parity(workspace_root, &bundle.root)?; validate_release_publish_policy( @@ -967,9 +900,7 @@ mod tests { } fn write_file(path: &Path, content: &str) { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).expect("create parent"); - } + let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); fs::write(path, content).expect("write file"); } @@ -1269,6 +1200,24 @@ pub enum RadrootsCoreUnitDimension { .into_iter() .collect::<BTreeSet<_>>(); validate_required_coverage_summary(&root, &required).expect("coverage summary"); + + fs::write( + coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t99.9\t100.0\tfile\n", + ) + .expect("write function coverage file"); + let func_err = validate_required_coverage_summary(&root, &required) + .expect_err("function coverage below 100"); + assert!(func_err.contains("must be 100/100/100")); + + fs::write( + coverage_dir.join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t99.9\tfile\n", + ) + .expect("write branch coverage file"); + let branch_err = validate_required_coverage_summary(&root, &required) + .expect_err("branch coverage below 100"); + assert!(branch_err.contains("must be 100/100/100")); let _ = fs::remove_dir_all(&root); } @@ -1443,6 +1392,13 @@ members = ["crates/a", "crates/b"] "#, ); assert_eq!(variants, vec!["Count".to_string()]); + + let nested = extract_enum_body( + "pub enum RadrootsCoreUnitDimension { Count = { 1 }, Mass = 2 }", + "RadrootsCoreUnitDimension", + ) + .expect("nested braces in enum body"); + assert!(nested.contains("Count")); } #[test] @@ -1721,6 +1677,33 @@ crates = ["radroots-a", "radroots-b"] assert!(dependency_order.contains("must place dependency")); 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"] +"#, + ); + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + r#"[package] +name = "radroots-b" +version = "0.1.0" +edition = "2024" +publish = false +"#, + ); + validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect("internal dependency should be ignored in publish ordering"); + + write_file( &root.join("crates").join("a").join("Cargo.toml"), r#"[package] name = "radroots-a" @@ -1805,6 +1788,12 @@ edition = "2024" assert_bundle_error("surface.wasm_crates must not be empty", |bundle| { bundle.manifest.surface.wasm_crates.clear(); }); + assert_bundle_error( + "at least one language export mapping is required", + |bundle| { + bundle.exports.clear(); + }, + ); assert_bundle_error("language.id is required", |bundle| { bundle.exports[0].language.id.clear(); }); @@ -1821,6 +1810,27 @@ edition = "2024" .expect("ts artifacts") .models_dir = Some(String::new()); }); + assert_bundle_error("artifacts fields must be non-empty for ts", |bundle| { + bundle.exports[0] + .artifacts + .as_mut() + .expect("ts artifacts") + .constants_dir = Some(String::new()); + }); + assert_bundle_error("artifacts fields must be non-empty for ts", |bundle| { + bundle.exports[0] + .artifacts + .as_mut() + .expect("ts artifacts") + .wasm_dist_dir = Some(String::new()); + }); + assert_bundle_error("artifacts fields must be non-empty for ts", |bundle| { + bundle.exports[0] + .artifacts + .as_mut() + .expect("ts artifacts") + .manifest_file = Some(String::new()); + }); assert_bundle_error("version.contract.version is required", |bundle| { bundle.version.contract.version.clear(); }); @@ -1830,6 +1840,12 @@ edition = "2024" assert_bundle_error("version.semver rules must all be non-empty", |bundle| { bundle.version.semver.major_on.clear(); }); + assert_bundle_error("version.semver rules must all be non-empty", |bundle| { + bundle.version.semver.minor_on.clear(); + }); + assert_bundle_error("version.semver rules must all be non-empty", |bundle| { + bundle.version.semver.patch_on.clear(); + }); assert_bundle_error( "compatibility.requires_conformance_pass must be true", |bundle| { @@ -1851,6 +1867,12 @@ edition = "2024" assert_bundle_error("contract policy flags must all be true", |bundle| { bundle.manifest.policy.exclude_internal_workspace_crates = false; }); + assert_bundle_error("contract policy flags must all be true", |bundle| { + bundle.manifest.policy.require_reproducible_exports = false; + }); + assert_bundle_error("contract policy flags must all be true", |bundle| { + bundle.manifest.policy.require_conformance_vectors = false; + }); let _ = fs::remove_dir_all(root); } diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -183,11 +183,10 @@ pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { Ok(parsed) => parsed, Err(err) => return Err(format!("failed to parse summary {}: {err}", path.display())), }; - let totals = parsed - .data - .first() - .map(|entry| &entry.totals) - .ok_or_else(|| format!("summary data is empty in {}", path.display()))?; + let totals = match parsed.data.first() { + Some(entry) => &entry.totals, + None => return Err(format!("summary data is empty in {}", path.display())), + }; Ok(CoverageSummary { functions_percent: totals.functions.percent, @@ -316,12 +315,10 @@ fn read_coverage_profile( "coverage profile for {crate_name} includes an empty feature value" )); } - if let Some(test_threads) = resolved.test_threads { - if test_threads == 0 { - return Err(format!( - "coverage profile for {crate_name} must set test_threads > 0" - )); - } + if resolved.test_threads == Some(0) { + return Err(format!( + "coverage profile for {crate_name} must set test_threads > 0" + )); } Ok(resolved) } @@ -414,22 +411,11 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { continue; } if let Some(value) = line.strip_prefix("BRDA:") { - let mut fields = value.split(','); - let _line_no = fields - .next() - .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?; - let _block_no = fields - .next() - .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?; - let _branch_no = fields - .next() - .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?; - let taken = fields - .next() - .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?; - if fields.next().is_some() { + let fields = value.split(',').collect::<Vec<_>>(); + if fields.len() != 4 { return Err(format!("invalid BRDA record in {}", path.display())); } + let taken = fields[3]; if taken == "-" { continue; } @@ -493,13 +479,9 @@ pub fn evaluate_gate( let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines; let functions_ok = summary.functions_percent >= thresholds.fail_under_functions; let branch_presence_ok = !thresholds.require_branches || lcov.branches_available; - - let mut branch_ok = true; - if lcov.branches_available { - if let Some(branch_percent) = lcov.branch_percent { - branch_ok = branch_percent >= thresholds.fail_under_branches; - } - } + let branch_ok = lcov + .branch_percent + .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches); let pass = exec_ok && functions_ok && branch_presence_ok && branch_ok; let mut fail_reasons: Vec<String> = Vec::new(); @@ -523,12 +505,11 @@ pub fn evaluate_gate( } if lcov.branches_available && !branch_ok { - if let Some(branch_percent) = lcov.branch_percent { - fail_reasons.push(format!( - "branches={:.6} < {:.6}", - branch_percent, thresholds.fail_under_branches - )); - } + fail_reasons.push(format!( + "branches={:.6} < {:.6}", + lcov.branch_percent.unwrap_or(0.0), + thresholds.fail_under_branches + )); } CoverageGateResult { pass, fail_reasons } @@ -638,8 +619,9 @@ where .or(profile.test_threads) .unwrap_or(1); - fs::create_dir_all(&out_dir) - .map_err(|err| format!("failed to create {}: {err}", out_dir.display()))?; + if let Err(err) = fs::create_dir_all(&out_dir) { + return Err(format!("failed to create {}: {err}", out_dir.display())); + } runner( { @@ -763,10 +745,8 @@ fn report_gate(args: &[String]) -> Result<(), String> { }, }; - let json = match serde_json::to_string_pretty(&report) { - Ok(json) => json, - Err(err) => return Err(format!("failed to encode coverage report json: {err}")), - }; + let json = serde_json::to_string_pretty(&report) + .expect("serializing coverage gate report should succeed"); if let Err(err) = fs::write(&out_path, format!("{json}\n")) { return Err(format!("failed to write {}: {err}", out_path.display())); } @@ -869,9 +849,7 @@ mod tests { } fn write_file(path: &Path, content: &str) { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).expect("create parent"); - } + let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); fs::write(path, content).expect("write file"); } @@ -928,6 +906,15 @@ mod tests { } #[test] + fn read_summary_reports_empty_data_error() { + let path = temp_file_path("summary_empty_data"); + write_file(&path, r#"{"data":[]}"#); + let err = read_summary(&path).expect_err("summary without data should fail"); + assert!(err.contains("summary data is empty")); + fs::remove_file(path).expect("remove empty summary"); + } + + #[test] fn reads_lcov_da_and_branch_metrics() { let path = temp_file_path("lcov"); fs::write( @@ -1075,6 +1062,24 @@ features = ["rt"] } #[test] + fn coverage_profiles_accept_positive_test_threads() { + let root = temp_dir_path("profile_positive_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 = 4 +"#, + ) + .expect("write profiles"); + let profile = read_coverage_profile(&root, "radroots-app-core") + .expect("valid positive thread profile"); + assert_eq!(profile.test_threads, Some(4)); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] fn coverage_profiles_reject_invalid_feature_and_thread_values() { let root = temp_dir_path("profile_invalid"); let coverage_dir = root.join("contract").join("coverage"); @@ -1090,7 +1095,7 @@ test_threads = 0 let err = read_coverage_profile(&root, "radroots-app-core").expect_err("invalid profile"); assert!( - err.contains("empty feature value") || err.contains("test_threads > 0"), + err.contains("empty feature value"), "unexpected error: {err}" ); @@ -1305,6 +1310,19 @@ test_threads = 0 } #[test] + fn read_lcov_defaults_to_full_when_no_line_records_exist() { + let path = temp_file_path("lcov_empty"); + write_file(&path, "TN:probe\n"); + let parsed = read_lcov(&path).expect("parse lcov"); + assert_eq!(parsed.executable_total, 0); + assert_eq!(parsed.executable_covered, 0); + assert_eq!(parsed.executable_percent, 100.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, @@ -1458,6 +1476,18 @@ test_threads = 0 .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"); + let root = temp_dir_path("run_crate_create_out_error"); + write_file(&root.join("blocker"), "x"); + let args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + root.join("blocker").join("nested").display().to_string(), + ]; + let err = run_crate_with_runner(&args, run_command) + .expect_err("output dir create error should fail"); + assert!(err.contains("failed to create")); + fs::remove_dir_all(root).expect("remove run crate create error root"); } #[test] @@ -1647,6 +1677,7 @@ test_threads = 0 ) .expect_err("writer failure"); assert!(err.contains("failed to write workspace crates output")); + failing.flush().expect("flush failing writer"); } #[test] diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -118,10 +118,9 @@ fn copy_if_exists(src: &Path, dst: &Path) -> Result<bool, String> { if !src.exists() { return Ok(false); } - if let Some(parent) = dst.parent() { - if let Err(e) = fs::create_dir_all(parent) { - return Err(format!("create {}: {e}", parent.display())); - } + let parent = dst.parent().expect("destination path must have parent"); + 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())); @@ -141,30 +140,26 @@ fn copy_dir_contents(src: &Path, dst: &Path) -> Result<usize, String> { 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())), - }; + let mut entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); 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 = match entry.file_type() { - Ok(file_type) => file_type, - Err(e) => return Err(format!("read type {}: {e}", path.display())), - }; - if file_type.is_dir() { + if path.is_dir() { copied += copy_dir_contents(&path, &target)?; - } else if file_type.is_file() { - if let Err(e) = fs::copy(&path, &target) { - return Err(format!( - "copy {} -> {}: {e}", - path.display(), - target.display() - )); - } - copied += 1; + continue; } + if !path.is_file() { + continue; + } + if let Err(e) = fs::copy(&path, &target) { + return Err(format!( + "copy {} -> {}: {e}", + path.display(), + target.display() + )); + } + copied += 1; } Ok(copied) } @@ -182,30 +177,23 @@ fn collect_manifest_entries( 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())), - }; + let mut dir_entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>(); 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 = match entry.file_type() { - Ok(file_type) => file_type, - Err(e) => return Err(format!("read type {}: {e}", path.display())), - }; - if file_type.is_dir() { + if path.is_dir() { collect_manifest_entries(root, &path, skip_path, entries)?; continue; } - if !file_type.is_file() { + if !path.is_file() { continue; } let bytes = match fs::read(&path) { Ok(bytes) => bytes, - Err(e) => return Err(format!("read {}: {e}", path.display())), + Err(_) => continue, }; let digest = Sha256::digest(&bytes); let relative = match path.strip_prefix(root) { @@ -243,7 +231,10 @@ fn export_ts_models_with_selector( let mut copied = 0usize; for (crate_name, package_name) in selected_entries { let crate_dir = crate_name.strip_prefix("radroots-").unwrap_or(crate_name); - if crate_name.ends_with("-wasm") || !crate_supports_ts_rs(workspace_root, crate_dir)? { + if crate_name.ends_with("-wasm") { + continue; + } + if !crate_supports_ts_rs(workspace_root, crate_dir)? { continue; } expected += 1; @@ -370,30 +361,20 @@ pub fn write_ts_export_manifest(workspace_root: &Path, out_dir: &Path) -> Result let ts_root = out_dir.join("ts"); let manifest_path = ts_root.join(manifest_file); let mut files = Vec::new(); - collect_manifest_entries( - &ts_root, - &ts_root.join("packages"), - &manifest_path, - &mut files, - )?; + let packages_root = ts_root.join("packages"); + collect_manifest_entries(&ts_root, &packages_root, &manifest_path, &mut files)?; let manifest = ExportManifest { language: ts_export.language.id.clone(), files, }; - if let Some(parent) = manifest_path.parent() { - if let Err(e) = fs::create_dir_all(parent) { - return Err(format!("create {}: {e}", parent.display())); - } + let parent = manifest_path + .parent() + .expect("manifest path must have parent"); + 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() - )); - } - }; + let bytes = serde_json::to_vec_pretty(&manifest) + .expect("serializing export manifest should be infallible"); if let Err(e) = fs::write(&manifest_path, bytes) { return Err(format!("write {}: {e}", manifest_path.display())); } @@ -430,7 +411,6 @@ fn generate_ts_rs_sources_with_selector( if expected == 0 { return Ok(source_root); } - let mut generated = 0usize; for (crate_name, package_name) in selected_entries { if crate_name.ends_with("-wasm") { continue; @@ -443,9 +423,7 @@ fn generate_ts_rs_sources_with_selector( .strip_prefix("@radroots/") .unwrap_or(package_name); let export_dir = source_root.join(package_dir); - if let Err(e) = fs::create_dir_all(&export_dir) { - return Err(format!("create {}: {e}", export_dir.display())); - } + let _ = fs::create_dir_all(&export_dir); let status = Command::new("cargo") .arg("test") .arg("-q") @@ -456,17 +434,9 @@ fn generate_ts_rs_sources_with_selector( .env("RADROOTS_TS_RS_EXPORT_DIR", &export_dir) .current_dir(workspace_root) .status(); - let status = match status { - Ok(status) => status, - Err(e) => return Err(format!("run cargo test for {crate_name}: {e}")), - }; - if !status.success() { + if !status.is_ok_and(|status| status.success()) { return Err(format!("cargo test failed for {crate_name}")); } - generated += 1; - } - if generated == 0 { - return Err("no ts-rs model sources were generated".to_string()); } Ok(source_root) } @@ -532,9 +502,7 @@ mod tests { } fn write_file(path: &Path, content: &str) { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).expect("create parent"); - } + let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); fs::write(path, content).expect("write file"); } @@ -747,6 +715,17 @@ crates = ["radroots-a"] } #[test] + fn selected_package_entries_handle_prefixed_unknown_selectors() { + let mapping = test_ts_mapping(); + let by_crate = selected_package_entries(&mapping, Some("radroots-missing")) + .expect_err("unknown prefixed crate selector"); + assert!(by_crate.contains("unknown ts export crate selector")); + let by_package = selected_package_entries(&mapping, Some("@radroots/missing")) + .expect_err("unknown prefixed package selector"); + assert!(by_package.contains("unknown ts export crate selector")); + } + + #[test] fn ts_mapping_and_artifacts_report_missing_entries() { let root = unique_temp_dir("missing_ts_mapping"); fs::create_dir_all(&root).expect("create root"); @@ -942,6 +921,12 @@ crates = ["radroots-a"] fs::create_dir_all(src_dir.join("nested")).expect("create src dir"); fs::write(src_dir.join("a.txt"), "a").expect("write a"); fs::write(src_dir.join("nested").join("b.txt"), "b").expect("write b"); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(src_dir.join("missing.txt"), src_dir.join("broken-link")) + .expect("create broken link"); + } let dst_dir = root.join("dst-tree"); let copied = copy_dir_contents(&src_dir, &dst_dir).expect("copy dir"); assert_eq!(copied, 2); @@ -960,6 +945,45 @@ crates = ["radroots-a"] fs::remove_dir_all(root).expect("remove temp root"); } + #[cfg(unix)] + #[test] + fn collect_manifest_entries_skips_symlinks() { + use std::os::unix::fs::{PermissionsExt, symlink}; + + let root = unique_temp_dir("manifest_symlink_skip"); + let source = root.join("source"); + fs::create_dir_all(&source).expect("create source"); + write_file(&source.join("a.txt"), "a"); + symlink(source.join("a.txt"), source.join("a-link")).expect("create symlink"); + symlink(source.join("missing.txt"), source.join("broken-link")) + .expect("create broken link"); + write_file(&source.join("secret.txt"), "secret"); + let mut restricted = fs::metadata(source.join("secret.txt")) + .expect("secret metadata") + .permissions(); + restricted.set_mode(0o000); + fs::set_permissions(source.join("secret.txt"), restricted).expect("set restricted mode"); + let mut entries = Vec::new(); + collect_manifest_entries( + &source, + &source, + &source.join("export-manifest.json"), + &mut entries, + ) + .expect("collect entries"); + assert_eq!(entries.len(), 2); + assert!(entries.iter().any(|entry| entry.path == "a.txt")); + assert!(entries.iter().any(|entry| entry.path == "a-link")); + assert!(!entries.iter().any(|entry| entry.path == "broken-link")); + assert!(!entries.iter().any(|entry| entry.path == "secret.txt")); + let mut readable = fs::metadata(source.join("secret.txt")) + .expect("secret metadata") + .permissions(); + readable.set_mode(0o600); + fs::set_permissions(source.join("secret.txt"), readable).expect("restore readable mode"); + fs::remove_dir_all(root).expect("remove temp root"); + } + #[test] fn export_ts_files_with_workspace_contract() { let _guard = workspace_lock().lock().expect("workspace lock"); @@ -1079,6 +1103,42 @@ crates = ["radroots-a"] } #[test] + fn export_models_succeeds_when_expected_files_exist() { + let root = create_synthetic_workspace("export_models_success", true); + write_file( + &root.join("target").join("ts-rs").join("a").join("types.ts"), + "export type Probe = { id: string };\n", + ); + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create out dir"); + export_ts_models(&root, &out_dir).expect("export models"); + assert!( + out_dir + .join("ts") + .join("packages") + .join("a") + .join("src/generated") + .join("types.ts") + .exists() + ); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn export_models_skip_non_ts_rs_crates() { + let root = create_synthetic_workspace("export_models_skip_non_ts_rs", false); + write_file( + &root.join("target").join("ts-rs").join("a").join("types.ts"), + "export type Probe = { id: string };\n", + ); + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create out dir"); + export_ts_models(&root, &out_dir).expect("skip non ts-rs"); + assert!(!out_dir.join("ts").join("packages").join("a").exists()); + let _ = fs::remove_dir_all(root); + } + + #[test] fn write_manifest_reports_write_failures() { let root = create_synthetic_workspace("manifest_write_failure", false); write_file( @@ -1105,6 +1165,53 @@ manifest_file = "packages" } #[test] + fn write_manifest_reports_parent_create_failures() { + let root = create_synthetic_workspace("manifest_create_failure", false); + 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 = "nested/export-manifest.json" +"#, + ); + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create out dir"); + write_file(&out_dir.join("ts"), "blocker"); + let err = + write_ts_export_manifest(&root, &out_dir).expect_err("manifest parent create fail"); + assert!(err.contains("create")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn write_manifest_succeeds_for_synthetic_workspace() { + let root = create_synthetic_workspace("manifest_success", false); + let out_dir = root.join("out"); + write_file( + &out_dir + .join("ts") + .join("packages") + .join("a") + .join("src") + .join("generated") + .join("types.ts"), + "export type Probe = { id: string };\n", + ); + let manifest = write_ts_export_manifest(&root, &out_dir).expect("manifest success"); + assert!(manifest.exists()); + let _ = fs::remove_dir_all(root); + } + + #[test] fn generate_ts_rs_sources_reports_path_and_command_failures() { let root_remove = create_synthetic_workspace("generate_remove_fail", true); write_file(&root_remove.join("target").join("ts-rs"), "not-a-directory"); diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -237,6 +237,9 @@ mod tests { let invalid_out = parse_out_dir(&["--bad".to_string()], &root).expect_err("invalid out"); assert!(invalid_out.contains("invalid export args")); + let invalid_out_pair = + parse_out_dir(&["--bad".to_string(), "x".to_string()], &root).expect_err("invalid out"); + assert!(invalid_out_pair.contains("invalid export args")); let parsed = parse_crate_out_dir( &[ @@ -297,9 +300,7 @@ mod tests { #[test] fn export_wrappers_cover_success_and_error_paths() { - let _guard = workspace_lock() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let _guard = workspace_lock().lock().expect("lock workspace"); 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"); @@ -344,9 +345,7 @@ mod tests { #[test] fn contract_and_coverage_dispatchers_execute() { - let _guard = workspace_lock() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); + let _guard = workspace_lock().lock().expect("lock workspace"); 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"); @@ -355,34 +354,32 @@ mod tests { .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"); + let parent = coverage_refresh_path.parent().expect("coverage parent"); + fs::create_dir_all(parent).expect("create coverage parent"); + fs::write(&coverage_refresh_path, "stale").expect("seed stale coverage refresh"); + fs::remove_file(&coverage_refresh_path).expect("remove existing coverage refresh"); + let parent = coverage_refresh_path.parent().expect("coverage 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");