lib

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

commit 32c91ec0d2f9b3be930d063232601cfb5d7e4aa1
parent 22767446c79d3a92514e849a0bc054af0b135c37
Author: triesap <tyson@radroots.org>
Date:   Wed,  4 Mar 2026 20:00:59 +0000

xtask: extend export ts error-path coverage

- add synthetic workspace fixtures for export and ts-rs generation failure-path tests
- add helper tests for missing ts mapping artifacts and selector resolution branches
- add filesystem error-path coverage for copy and manifest collection helpers
- keep xtask check and test lanes green while raising executable and branch coverage

Diffstat:
Mcrates/xtask/src/export_ts.rs | 464+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 455 insertions(+), 9 deletions(-)

diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -506,6 +506,7 @@ pub fn export_ts_bundle_for_crate( mod tests { use super::*; use std::collections::BTreeMap; + use std::path::Path; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -530,6 +531,167 @@ mod tests { std::env::temp_dir().join(format!("radroots_xtask_{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"); + } + + fn create_synthetic_workspace(prefix: &str, crate_a_ts_rs: bool) -> PathBuf { + let root = unique_temp_dir(prefix); + fs::create_dir_all(&root).expect("create root"); + write_file( + &root.join("Cargo.toml"), + r#"[workspace] +members = ["crates/a", "crates/b"] +resolver = "2" +"#, + ); + let crate_a_features = if crate_a_ts_rs { + "\n[features]\nts-rs = []\n" + } else { + "" + }; + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + &format!( + 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" +{}"#, + crate_a_features + ), + ); + write_file( + &root.join("crates").join("a").join("src").join("lib.rs"), + "pub fn crate_a() {}\n", + ); + 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("b").join("src").join("lib.rs"), + "pub fn crate_b() {}\n", + ); + 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 + } + fn test_ts_mapping() -> contract::ExportMapping { let mut packages = BTreeMap::new(); packages.insert("radroots-core".to_string(), "@radroots/core".to_string()); @@ -585,6 +747,87 @@ mod tests { } #[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"); + let bundle = contract::ContractBundle { + root: root.clone(), + manifest: contract::ContractManifest { + contract: contract::ManifestContract { + name: "name".to_string(), + version: "1.0.0".to_string(), + source: "source".to_string(), + }, + surface: contract::Surface { + model_crates: vec!["radroots-a".to_string()], + algorithm_crates: vec!["radroots-b".to_string()], + wasm_crates: vec![], + }, + policy: contract::Policy { + exclude_internal_workspace_crates: true, + require_reproducible_exports: true, + require_conformance_vectors: true, + }, + }, + version: contract::VersionPolicy { + contract: contract::VersionContract { + version: "1.0.0".to_string(), + stability: "alpha".to_string(), + }, + semver: contract::SemverRules { + major_on: vec!["breaking".to_string()], + minor_on: vec!["feature".to_string()], + patch_on: vec!["fix".to_string()], + }, + compatibility: contract::CompatibilityRules { + requires_conformance_pass: true, + requires_export_manifest_diff: true, + requires_release_notes: true, + }, + }, + exports: Vec::new(), + }; + let mapping_err = ts_export_mapping(&bundle).expect_err("missing ts mapping"); + assert!(mapping_err.contains("missing ts export mapping")); + + let mut packages = BTreeMap::new(); + packages.insert("radroots-a".to_string(), "@radroots/a".to_string()); + let no_artifacts = contract::ExportMapping { + language: contract::ExportLanguage { + id: "ts".to_string(), + repository: "sdk-typescript".to_string(), + }, + packages, + artifacts: None, + }; + let artifacts_err = ts_artifacts(&no_artifacts).expect_err("missing ts artifacts mapping"); + assert!(artifacts_err.contains("missing ts artifacts mapping")); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn selected_package_entries_supports_package_candidate_lookup() { + let mut packages = BTreeMap::new(); + packages.insert( + "radroots-special".to_string(), + "@radroots/special-pkg".to_string(), + ); + let mapping = contract::ExportMapping { + language: contract::ExportLanguage { + id: "ts".to_string(), + repository: "sdk-typescript".to_string(), + }, + packages, + artifacts: Some(contract::ExportArtifacts::default()), + }; + let selected = + selected_package_entries(&mapping, Some("special-pkg")).expect("package candidate"); + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].0.as_str(), "radroots-special"); + assert_eq!(selected[0].1.as_str(), "@radroots/special-pkg"); + } + + #[test] fn package_dir_and_artifact_helpers_validate_values() { let base = PathBuf::from("/tmp/base"); assert_eq!(to_package_dir(&base, "@radroots/core"), base.join("core")); @@ -602,6 +845,87 @@ mod tests { } #[test] + fn helper_error_paths_cover_copy_manifest_and_support_checks() { + let root = unique_temp_dir("helper_errors"); + fs::create_dir_all(&root).expect("create root"); + + let manifest_dir = root.join("crates").join("probe").join("Cargo.toml"); + fs::create_dir_all(&manifest_dir).expect("create directory in place of manifest"); + let supports_err = crate_supports_ts_rs(&root, "probe").expect_err("manifest read error"); + assert!(supports_err.contains("read")); + + let src_file = root.join("src").join("one.txt"); + write_file(&src_file, "one"); + let dst_parent_file = root.join("dst-parent-file"); + write_file(&dst_parent_file, "block"); + let create_err = copy_if_exists(&src_file, &dst_parent_file.join("out.txt")) + .expect_err("create parent error"); + assert!(create_err.contains("create")); + + let dst_file = root.join("dst-dir"); + fs::create_dir_all(&dst_file).expect("create destination directory"); + let copy_err = + copy_if_exists(&src_file, &dst_file).expect_err("copy to directory should fail"); + assert!(copy_err.contains("copy")); + + let missing_dir = root.join("missing-dir"); + assert_eq!( + copy_dir_contents(&missing_dir, &root.join("dst-missing")).expect("missing dir"), + 0 + ); + + let src_dir = root.join("src-dir"); + fs::create_dir_all(&src_dir).expect("create src dir"); + let dst_blocker = root.join("dst-blocker"); + write_file(&dst_blocker, "blocker"); + let dst_err = copy_dir_contents(&src_dir, &dst_blocker).expect_err("create dst error"); + assert!(dst_err.contains("create")); + + let src_file_not_dir = root.join("src-file-not-dir"); + write_file(&src_file_not_dir, "not dir"); + let read_dir_err = + copy_dir_contents(&src_file_not_dir, &root.join("dst-ok")).expect_err("read dir error"); + assert!(read_dir_err.contains("read dir")); + + let src_tree = root.join("src-tree"); + let dst_tree = root.join("dst-tree"); + fs::create_dir_all(&src_tree).expect("create src-tree"); + fs::create_dir_all(&dst_tree).expect("create dst-tree"); + write_file(&src_tree.join("entry.txt"), "entry"); + fs::create_dir_all(dst_tree.join("entry.txt")).expect("create colliding directory"); + let copy_tree_err = copy_dir_contents(&src_tree, &dst_tree).expect_err("copy tree error"); + assert!(copy_tree_err.contains("copy")); + + let current_is_file = root.join("manifest-file"); + write_file(&current_is_file, "x"); + let mut entries = Vec::new(); + let collect_err = collect_manifest_entries( + &root, + &current_is_file, + &root.join("skip.json"), + &mut entries, + ) + .expect_err("read dir error for file"); + assert!(collect_err.contains("read dir")); + + let other_root = unique_temp_dir("manifest_strip_prefix"); + fs::create_dir_all(&other_root).expect("create other root"); + write_file(&other_root.join("x.txt"), "x"); + let mut entries = Vec::new(); + let strip_err = collect_manifest_entries( + &root, + &other_root, + &other_root.join("skip.json"), + &mut entries, + ) + .expect_err("strip prefix error"); + assert!(strip_err.contains("strip prefix")); + fs::remove_dir_all(other_root).expect("remove other root"); + + fs::remove_dir_all(root).expect("remove helper root"); + } + + #[test] fn copy_helpers_and_manifest_collection_cover_file_paths() { let root = unique_temp_dir("copy_helpers"); let src_file = root.join("src").join("one.txt"); @@ -658,6 +982,12 @@ mod tests { "export type CoreProbe = { id: string };\n", ) .expect("write ts-rs model"); + let events_source = root.join("target").join("ts-rs").join("events"); + fs::create_dir_all(&events_source).expect("create events source"); + fs::write(events_source.join("constants.ts"), "export const A = 1;\n") + .expect("write events constants"); + fs::write(events_source.join("kinds.ts"), "export const K = 1;\n") + .expect("write events kinds"); let out_dir = root.join("target").join("xtask-export-tests").join( SystemTime::now() @@ -692,15 +1022,10 @@ mod tests { .join("events") .join(&constants_dir) .join("kinds.ts"); - let events_source = root.join("target").join("ts-rs").join("events"); - let constants_exists = events_source.join("constants.ts").exists(); - let kinds_exists = events_source.join("kinds.ts").exists(); - if constants_exists { - assert!(events_constants.exists()); - } - if kinds_exists { - assert!(events_kinds.exists()); - } + assert!(events_source.join("constants.ts").exists()); + assert!(events_source.join("kinds.ts").exists()); + assert!(events_constants.exists()); + assert!(events_kinds.exists()); export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm"); let manifest_path = write_ts_export_manifest(&root, &out_dir).expect("write manifest"); @@ -725,4 +1050,125 @@ mod tests { assert!(!crate_supports_ts_rs(&root, "missing").expect("missing crate")); fs::remove_dir_all(root).expect("remove temp root"); } + + #[test] + fn export_models_and_constants_report_missing_source_roots() { + let root = create_synthetic_workspace("export_missing_source", true); + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create out dir"); + + let models_err = export_ts_models(&root, &out_dir).expect_err("missing models source root"); + assert!(models_err.contains("missing ts-rs source root")); + + let constants_err = + export_ts_constants(&root, &out_dir).expect_err("missing constants source root"); + assert!(constants_err.contains("missing ts-rs source root")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn export_models_reports_when_expected_files_are_missing() { + let root = create_synthetic_workspace("export_models_missing_files", true); + fs::create_dir_all(root.join("target").join("ts-rs")).expect("create ts-rs root"); + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create out dir"); + + let err = export_ts_models(&root, &out_dir).expect_err("expected model files are missing"); + assert!(err.contains("no ts model files were exported")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn write_manifest_reports_write_failures() { + let root = create_synthetic_workspace("manifest_write_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 = "packages" +"#, + ); + let out_dir = root.join("out"); + fs::create_dir_all(out_dir.join("ts").join("packages")).expect("create packages directory"); + let err = write_ts_export_manifest(&root, &out_dir).expect_err("manifest write to dir"); + assert!(err.contains("write")); + 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"); + let remove_err = generate_ts_rs_sources(&root_remove) + .expect_err("remove existing source root should fail"); + assert!(remove_err.contains("remove")); + let _ = fs::remove_dir_all(root_remove); + + let root_create = create_synthetic_workspace("generate_create_fail", true); + let _ = fs::remove_dir_all(root_create.join("target")); + write_file(&root_create.join("target"), "blocker"); + let create_err = generate_ts_rs_sources(&root_create) + .expect_err("create source root parent should fail"); + assert!(create_err.contains("create")); + let _ = fs::remove_dir_all(root_create); + + let root_no_expected = create_synthetic_workspace("generate_no_expected", false); + let generated = generate_ts_rs_sources(&root_no_expected).expect("no expected crates"); + assert_eq!(generated, root_no_expected.join("target").join("ts-rs")); + let _ = fs::remove_dir_all(root_no_expected); + + let root_command_fail = create_synthetic_workspace("generate_command_fail", true); + write_file( + &root_command_fail + .join("crates") + .join("a") + .join("src") + .join("lib.rs"), + "pub fn broken( {\n", + ); + let command_fail_err = generate_ts_rs_sources(&root_command_fail) + .expect_err("cargo test failure should surface"); + assert!(command_fail_err.contains("cargo test failed for radroots-a")); + let _ = fs::remove_dir_all(root_command_fail); + } + + #[test] + fn generate_ts_rs_sources_succeeds_and_skips_non_ts_rs_crates() { + let _guard = workspace_lock().lock().expect("workspace lock"); + let root = create_synthetic_workspace("generate_skip_non_ts_rs", true); + write_file( + &root.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" +"radroots-b" = "@radroots/b" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_file( + &root.join("crates").join("a").join("src").join("lib.rs"), + "pub fn probe() {}\n", + ); + let generated = generate_ts_rs_sources(&root).expect("ts-rs generation should pass"); + assert!(generated.join("a").exists()); + assert!(!generated.join("b").exists()); + let _ = fs::remove_dir_all(root); + } }