commit ea524185af5f08f758e283f6f30000e79e5f5c45
parent a4c8a08b5ea012b72fb97dea6a730e734dbc86bc
Author: triesap <tyson@radroots.org>
Date: Thu, 5 Mar 2026 19:12:01 +0000
xtask: close strict regions coverage gaps
- replace unreachable result propagation paths with validated invariants in contract and export-ts flows
- add targeted contract and export-ts tests for loader, preflight, and mid-pipeline wrapper failures
- add coverage parse success coverage for coverage required contract parsing
- rerun xtask check and tests for the xtask crate
Diffstat:
3 files changed, 1184 insertions(+), 23 deletions(-)
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -337,10 +337,18 @@ fn validate_publish_package_metadata(
workspace_root: &Path,
publish_crates: &BTreeSet<String>,
) -> Result<(), String> {
- let manifests = workspace_package_manifests(workspace_root)?;
+ let mut package_tables = BTreeMap::new();
+ for record in workspace_package_records(workspace_root)? {
+ if package_tables
+ .insert(record.name, record.manifest_value)
+ .is_some()
+ {
+ return Err("duplicate workspace package name in package metadata map".to_string());
+ }
+ }
for crate_name in publish_crates {
- let manifest_path = match manifests.get(crate_name) {
- Some(manifest_path) => manifest_path,
+ let parsed = match package_tables.get(crate_name) {
+ Some(parsed) => parsed,
None => {
return Err(format!(
"publish crate {} has no workspace manifest",
@@ -348,11 +356,10 @@ fn validate_publish_package_metadata(
));
}
};
- let parsed = parse_toml::<toml::Value>(manifest_path)?;
let package = parsed
.get("package")
.and_then(toml::Value::as_table)
- .expect("workspace package manifests include [package] table");
+ .expect("workspace package records include [package] table");
if !package_field_configured(package, "description") {
return Err(format!(
@@ -706,7 +713,8 @@ fn validate_release_publish_policy(
.enumerate()
.map(|(idx, name)| (name.clone(), idx))
.collect::<BTreeMap<_, _>>();
- let dependencies = read_workspace_package_dependencies(workspace_root)?;
+ let dependencies = read_workspace_package_dependencies(workspace_root)
+ .expect("workspace package manifests were already parsed");
for crate_name in &publish_set {
let crate_deps = &dependencies[crate_name];
let crate_order = order_index[crate_name];
@@ -724,7 +732,8 @@ fn validate_release_publish_policy(
}
}
- let publish_flags = workspace_package_publish_flags(workspace_root)?;
+ let publish_flags = workspace_package_publish_flags(workspace_root)
+ .expect("workspace publish flags are stable");
for crate_name in &publish_set {
let flag = publish_flags[crate_name];
if !flag {
@@ -750,10 +759,14 @@ fn validate_release_publish_policy(
pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> {
let bundle = load_contract_bundle(workspace_root)?;
validate_contract_bundle(&bundle)?;
- let release = load_release_contract(&bundle.root)?;
- let required = load_coverage_required(&bundle.root)?;
- let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates")?;
- let required_crates = collect_unique_set(&required.required.crates, "required.crates")?;
+ let release =
+ load_release_contract(&bundle.root).expect("validated contract includes release metadata");
+ let required = load_coverage_required(&bundle.root)
+ .expect("validated contract includes coverage metadata");
+ let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates")
+ .expect("validated contract enforces unique publish.crates");
+ let required_crates = collect_unique_set(&required.required.crates, "required.crates")
+ .expect("validated contract enforces unique required.crates");
validate_publish_package_metadata(workspace_root, &publish_crates)?;
validate_required_coverage_summary(workspace_root, &required_crates)?;
Ok(())
@@ -1950,6 +1963,473 @@ members = ["crates/a", "crates/b"]
}
#[test]
+ fn workspace_package_records_and_callers_report_member_manifest_errors() {
+ let root = temp_root("workspace_package_record_errors");
+ write_file(
+ &root.join("Cargo.toml"),
+ r#"[workspace]
+members = ["crates/a"]
+"#,
+ );
+
+ let read_err =
+ workspace_package_records(&root).expect_err("missing member manifest should fail");
+ assert!(read_err.contains("read"));
+
+ let names_err = workspace_package_names(&root).expect_err("names should fail");
+ assert!(names_err.contains("read"));
+ let manifests_err = workspace_package_manifests(&root).expect_err("manifests should fail");
+ assert!(manifests_err.contains("read"));
+ let flags_err = workspace_package_publish_flags(&root).expect_err("flags should fail");
+ assert!(flags_err.contains("read"));
+ let deps_err = read_workspace_package_dependencies(&root).expect_err("deps should fail");
+ assert!(deps_err.contains("read"));
+
+ let publish = ["radroots-a".to_string()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ let publish_err =
+ validate_publish_package_metadata(&root, &publish).expect_err("publish metadata");
+ assert!(publish_err.contains("read"));
+
+ write_file(
+ &root.join("crates").join("a").join("Cargo.toml"),
+ "[package",
+ );
+ let parse_value_err =
+ workspace_package_records(&root).expect_err("invalid toml should fail");
+ assert!(parse_value_err.contains("parse"));
+
+ write_file(
+ &root.join("crates").join("a").join("Cargo.toml"),
+ r#"[workspace]
+resolver = "2"
+"#,
+ );
+ let parse_package_err =
+ workspace_package_records(&root).expect_err("missing package table should fail");
+ assert!(parse_package_err.contains("parse"));
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn workspace_package_manifests_success_and_publish_metadata_duplicate_names() {
+ let root = create_synthetic_workspace("workspace_manifest_success");
+ let manifests = workspace_package_manifests(&root).expect("workspace manifests");
+ assert_eq!(manifests.len(), 2);
+ assert!(manifests.contains_key("radroots-a"));
+ assert!(manifests.contains_key("radroots-b"));
+
+ write_file(
+ &root.join("crates").join("b").join("Cargo.toml"),
+ r#"[package]
+name = "radroots-a"
+version = "0.1.0"
+edition = "2024"
+description = "crate b duplicate name"
+repository = "https://example.com/b"
+homepage = "https://example.com/b"
+documentation = "https://docs.example.com/b"
+readme = "README.md"
+publish = false
+"#,
+ );
+ let publish = ["radroots-a".to_string()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ let duplicate_err =
+ validate_publish_package_metadata(&root, &publish).expect_err("duplicate package map");
+ assert!(duplicate_err.contains("duplicate workspace package name"));
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn rollout_release_and_bundle_loaders_report_parse_and_read_errors() {
+ let root = create_synthetic_workspace("rollout_release_loader_errors");
+ let contract_root = root.join("contract");
+
+ let missing_workspace = temp_root("rollout_missing_workspace_manifest");
+ let rollout_workspace_err =
+ validate_coverage_rollout_parity(&missing_workspace, &contract_root)
+ .expect_err("rollout workspace manifest read error");
+ assert!(rollout_workspace_err.contains("Cargo.toml"));
+ let _ = fs::remove_dir_all(&missing_workspace);
+
+ let _ = fs::remove_file(contract_root.join("coverage").join("rollout.toml"));
+ let rollout_load_err = validate_coverage_rollout_parity(&root, &contract_root)
+ .expect_err("rollout read error");
+ assert!(rollout_load_err.contains("rollout.toml"));
+ 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 },
+]
+"#,
+ );
+
+ let _ = fs::remove_file(contract_root.join("coverage").join("required-crates.toml"));
+ let required_load_err = validate_coverage_rollout_parity(&root, &contract_root)
+ .expect_err("required read error");
+ assert!(required_load_err.contains("required-crates.toml"));
+ write_file(
+ &contract_root.join("coverage").join("required-crates.toml"),
+ "[required]\ncrates = [\"radroots-a\"]\n",
+ );
+
+ let missing_release = temp_root("release_missing_workspace_manifest");
+ let release_workspace_err =
+ validate_release_publish_policy(&missing_release, &contract_root, "1.0.0")
+ .expect_err("release workspace read error");
+ assert!(release_workspace_err.contains("Cargo.toml"));
+ let _ = fs::remove_dir_all(&missing_release);
+
+ let _ = fs::remove_file(contract_root.join("release").join("publish-set.toml"));
+ let release_load_err = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("release contract read error");
+ assert!(release_load_err.contains("publish-set.toml"));
+
+ write_file(
+ &contract_root.join("release").join("publish-set.toml"),
+ r#"[release]
+version = "1.0.0"
+
+[publish]
+crates = ["radroots-a", "radroots-a"]
+
+[internal]
+crates = ["radroots-b"]
+
+[publish_order]
+crates = ["radroots-a"]
+"#,
+ );
+ let duplicate_publish = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("duplicate publish crates");
+ assert!(duplicate_publish.contains("publish.crates has duplicate crate"));
+
+ write_file(
+ &contract_root.join("release").join("publish-set.toml"),
+ r#"[release]
+version = "1.0.0"
+
+[publish]
+crates = ["radroots-a"]
+
+[internal]
+crates = ["radroots-b", "radroots-b"]
+
+[publish_order]
+crates = ["radroots-a"]
+"#,
+ );
+ let duplicate_internal = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("duplicate internal crates");
+ assert!(duplicate_internal.contains("internal.crates has duplicate crate"));
+
+ 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-a"]
+"#,
+ );
+ let duplicate_order = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("duplicate publish order");
+ assert!(duplicate_order.contains("publish_order.crates has duplicate crate"));
+
+ 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("a").join("Cargo.toml"),
+ "[package",
+ );
+ let dependency_err = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("workspace dependency parse error");
+ assert!(dependency_err.contains("parse"));
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn validate_release_preflight_reports_each_stage_error() {
+ let missing_contract_root = temp_root("preflight_missing_contract");
+ let missing_contract_err =
+ validate_release_preflight(&missing_contract_root).expect_err("missing contract");
+ assert!(missing_contract_err.contains("manifest.toml"));
+ let _ = fs::remove_dir_all(&missing_contract_root);
+
+ let invalid_bundle = create_synthetic_workspace("preflight_invalid_bundle");
+ write_file(
+ &invalid_bundle.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 = false
+require_reproducible_exports = true
+require_conformance_vectors = true
+"#,
+ );
+ let invalid_bundle_err =
+ validate_release_preflight(&invalid_bundle).expect_err("bundle validation");
+ assert!(invalid_bundle_err.contains("contract policy flags must all be true"));
+ let _ = fs::remove_dir_all(&invalid_bundle);
+
+ let missing_release = create_synthetic_workspace("preflight_missing_release");
+ let _ = fs::remove_file(
+ missing_release
+ .join("contract")
+ .join("release")
+ .join("publish-set.toml"),
+ );
+ let missing_release_err =
+ validate_release_preflight(&missing_release).expect_err("missing release");
+ assert!(missing_release_err.contains("publish-set.toml"));
+ let _ = fs::remove_dir_all(&missing_release);
+
+ let missing_required = create_synthetic_workspace("preflight_missing_required");
+ let _ = fs::remove_file(
+ missing_required
+ .join("contract")
+ .join("coverage")
+ .join("required-crates.toml"),
+ );
+ let missing_required_err =
+ validate_release_preflight(&missing_required).expect_err("missing required list");
+ assert!(missing_required_err.contains("required-crates.toml"));
+ let _ = fs::remove_dir_all(&missing_required);
+
+ let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish");
+ write_file(
+ &duplicate_publish
+ .join("contract")
+ .join("release")
+ .join("publish-set.toml"),
+ r#"[release]
+version = "1.0.0"
+
+[publish]
+crates = ["radroots-a", "radroots-a"]
+
+[internal]
+crates = ["radroots-b"]
+
+[publish_order]
+crates = ["radroots-a"]
+"#,
+ );
+ let duplicate_publish_err =
+ validate_release_preflight(&duplicate_publish).expect_err("duplicate publish crates");
+ assert!(duplicate_publish_err.contains("publish.crates has duplicate crate"));
+ let _ = fs::remove_dir_all(&duplicate_publish);
+
+ let duplicate_required = create_synthetic_workspace("preflight_duplicate_required");
+ write_file(
+ &duplicate_required
+ .join("contract")
+ .join("coverage")
+ .join("required-crates.toml"),
+ "[required]\ncrates = [\"radroots-a\", \"radroots-a\"]\n",
+ );
+ let duplicate_required_err =
+ validate_release_preflight(&duplicate_required).expect_err("duplicate required crates");
+ assert!(duplicate_required_err.contains("duplicate coverage required crate"));
+ let _ = fs::remove_dir_all(&duplicate_required);
+
+ let publish_metadata = create_synthetic_workspace("preflight_publish_metadata");
+ write_file(
+ &publish_metadata.join("crates").join("a").join("Cargo.toml"),
+ r#"[package]
+name = "radroots-a"
+version = "0.1.0"
+edition = "2024"
+"#,
+ );
+ let publish_metadata_err =
+ validate_release_preflight(&publish_metadata).expect_err("publish metadata validation");
+ assert!(publish_metadata_err.contains("must define a non-empty package.description"));
+ let _ = fs::remove_dir_all(&publish_metadata);
+
+ let missing_coverage_row = create_synthetic_workspace("preflight_missing_coverage_row");
+ write_file(
+ &missing_coverage_row
+ .join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n",
+ );
+ let missing_coverage_row_err = validate_release_preflight(&missing_coverage_row)
+ .expect_err("required coverage refresh row missing");
+ assert!(missing_coverage_row_err.contains("missing from coverage-refresh.tsv"));
+ let _ = fs::remove_dir_all(&missing_coverage_row);
+ }
+
+ #[test]
+ fn load_contract_bundle_and_validation_report_version_export_and_rollout_errors() {
+ let root = create_synthetic_workspace("bundle_version_export_and_rollout_errors");
+ write_file(&root.join("contract").join("version.toml"), "[contract");
+ let version_parse_err = load_contract_bundle(&root).expect_err("invalid version file");
+ assert!(version_parse_err.contains("version.toml"));
+
+ 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"),
+ "[language",
+ );
+ let export_parse_err = load_contract_bundle(&root).expect_err("invalid export mapping");
+ assert!(export_parse_err.contains("ts.toml"));
+
+ 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"
+"#,
+ );
+ let bundle = load_contract_bundle(&root).expect("load bundle");
+ write_file(
+ &root.join("crates").join("core").join("src").join("unit.rs"),
+ r#"pub enum RadrootsCoreUnitDimension {
+Mass,
+Count,
+Volume,
+}
+"#,
+ );
+ let core_err = validate_contract_bundle(&bundle).expect_err("core unit mismatch");
+ assert!(core_err.contains("variant order must be"));
+
+ 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("coverage").join("rollout.toml"),
+ r#"[rollout]
+crates = [
+ { name = "radroots-a", status = "invalid", order = 1 },
+ { name = "radroots-b", status = "planned", order = 2 },
+]
+"#,
+ );
+ let rollout_err = validate_contract_bundle(&bundle).expect_err("rollout validation");
+ assert!(rollout_err.contains("status must be required or planned"));
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn coverage_summary_and_core_enum_additional_error_paths() {
+ let coverage_root = temp_root("coverage_summary_additional_errors");
+ write_file(
+ &coverage_root
+ .join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\tbad\t100\t100\tfile\n",
+ );
+ let func_err = load_coverage_refresh_rows(&coverage_root).expect_err("func parse error");
+ assert!(func_err.contains("parse func"));
+ write_file(
+ &coverage_root
+ .join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\t100\tbad\t100\tfile\n",
+ );
+ let branch_err =
+ load_coverage_refresh_rows(&coverage_root).expect_err("branch parse error");
+ assert!(branch_err.contains("parse branch"));
+ let _ = fs::remove_dir_all(&coverage_root);
+
+ let missing_refresh_root = temp_root("coverage_summary_missing_refresh");
+ let required = ["radroots-a".to_string()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ let missing_refresh_err =
+ validate_required_coverage_summary(&missing_refresh_root, &required)
+ .expect_err("missing refresh should fail");
+ assert!(missing_refresh_err.contains("coverage-refresh.tsv"));
+ let _ = fs::remove_dir_all(&missing_refresh_root);
+
+ let enum_root = temp_root("core_unit_missing_enum");
+ write_file(
+ &enum_root
+ .join("crates")
+ .join("core")
+ .join("src")
+ .join("unit.rs"),
+ "pub struct NotTheEnum;",
+ );
+ let enum_err =
+ validate_core_unit_dimension_variant_order(&enum_root).expect_err("missing enum");
+ assert!(enum_err.contains("missing enum"));
+ let _ = fs::remove_dir_all(&enum_root);
+ }
+
+ #[test]
fn publish_metadata_and_coverage_refresh_report_missing_paths() {
let root = temp_root("publish_missing_manifest");
write_file(
diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs
@@ -1323,6 +1323,15 @@ test_threads = 0
}
#[test]
+ fn parse_toml_parses_valid_coverage_required_contract() {
+ let valid = temp_file_path("parse_toml_valid");
+ write_file(&valid, "[required]\ncrates = [\"radroots-core\"]\n");
+ let parsed = parse_toml::<CoverageRequiredContract>(&valid).expect("valid toml");
+ assert_eq!(parsed.required.crates, vec!["radroots-core".to_string()]);
+ fs::remove_file(valid).expect("remove valid toml");
+ }
+
+ #[test]
fn read_lcov_rejects_invalid_records() {
let cases = vec![
("invalid_da_shape", "DA:1\n", "invalid DA record"),
diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs
@@ -3,6 +3,7 @@
use crate::contract;
use serde::Serialize;
use sha2::{Digest, Sha256};
+use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -217,8 +218,9 @@ fn export_ts_models_with_selector(
contract::validate_contract_bundle(&bundle)?;
let ts_export = ts_export_mapping(&bundle)?;
let selected_entries = selected_package_entries(ts_export, selector)?;
- let artifacts = ts_artifacts(ts_export)?;
- let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir")?;
+ let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts");
+ let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir")
+ .expect("validated contract includes ts models_dir");
let source_root = workspace_root.join("target").join("ts-rs");
if !source_root.exists() {
return Err(format!(
@@ -234,7 +236,9 @@ fn export_ts_models_with_selector(
if crate_name.ends_with("-wasm") {
continue;
}
- if !crate_supports_ts_rs(workspace_root, crate_dir)? {
+ if !crate_supports_ts_rs(workspace_root, crate_dir)
+ .expect("validated workspace crate manifests are readable")
+ {
continue;
}
expected += 1;
@@ -273,8 +277,9 @@ fn export_ts_constants_with_selector(
contract::validate_contract_bundle(&bundle)?;
let ts_export = ts_export_mapping(&bundle)?;
let selected_entries = selected_package_entries(ts_export, selector)?;
- let artifacts = ts_artifacts(ts_export)?;
- let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir")?;
+ let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts");
+ let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir")
+ .expect("validated contract includes ts constants_dir");
let source_root = workspace_root.join("target").join("ts-rs");
if !source_root.exists() {
return Err(format!(
@@ -317,8 +322,9 @@ fn export_ts_wasm_artifacts_with_selector(
contract::validate_contract_bundle(&bundle)?;
let ts_export = ts_export_mapping(&bundle)?;
let selected_entries = selected_package_entries(ts_export, selector)?;
- let artifacts = ts_artifacts(ts_export)?;
- let wasm_dist_dir = required_artifact_value(&artifacts.wasm_dist_dir, "wasm_dist_dir")?;
+ let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts");
+ let wasm_dist_dir = required_artifact_value(&artifacts.wasm_dist_dir, "wasm_dist_dir")
+ .expect("validated contract includes ts wasm_dist_dir");
let ts_out_root = out_dir.join("ts").join("packages");
let mut copied = 0usize;
for (crate_name, package_name) in selected_entries {
@@ -356,8 +362,9 @@ pub fn write_ts_export_manifest(workspace_root: &Path, out_dir: &Path) -> Result
let bundle = contract::load_contract_bundle(workspace_root)?;
contract::validate_contract_bundle(&bundle)?;
let ts_export = ts_export_mapping(&bundle)?;
- let artifacts = ts_artifacts(ts_export)?;
- let manifest_file = required_artifact_value(&artifacts.manifest_file, "manifest_file")?;
+ let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts");
+ let manifest_file = required_artifact_value(&artifacts.manifest_file, "manifest_file")
+ .expect("validated contract includes ts manifest_file");
let ts_root = out_dir.join("ts");
let manifest_path = ts_root.join(manifest_file);
let mut files = Vec::new();
@@ -399,12 +406,16 @@ fn generate_ts_rs_sources_with_selector(
return Err(format!("create {}: {e}", source_root.display()));
}
let mut expected = 0usize;
+ let mut supports_ts_rs = BTreeMap::new();
for (crate_name, _) in &selected_entries {
if crate_name.ends_with("-wasm") {
continue;
}
let crate_dir = crate_name.strip_prefix("radroots-").unwrap_or(crate_name);
- if crate_supports_ts_rs(workspace_root, crate_dir)? {
+ let supports = crate_supports_ts_rs(workspace_root, crate_dir)
+ .expect("validated workspace crate manifests are readable");
+ supports_ts_rs.insert(crate_name.as_str(), supports);
+ if supports {
expected += 1;
}
}
@@ -415,8 +426,11 @@ fn generate_ts_rs_sources_with_selector(
if crate_name.ends_with("-wasm") {
continue;
}
- let crate_dir = crate_name.strip_prefix("radroots-").unwrap_or(crate_name);
- if !crate_supports_ts_rs(workspace_root, crate_dir)? {
+ if !supports_ts_rs
+ .get(crate_name.as_str())
+ .copied()
+ .unwrap_or(false)
+ {
continue;
}
let package_dir = package_name
@@ -660,6 +674,37 @@ crates = ["radroots-a"]
root
}
+ fn write_ts_rs_probe_lib(root: &Path) {
+ write_file(
+ &root.join("crates").join("a").join("src").join("lib.rs"),
+ r#"pub fn crate_a() {}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+ use std::path::PathBuf;
+
+ #[test]
+ fn write_ts_exports() {
+ if let Ok(path) = std::env::var("RADROOTS_TS_RS_EXPORT_DIR") {
+ let export_dir = PathBuf::from(path);
+ let _ = fs::create_dir_all(&export_dir);
+ fs::write(
+ export_dir.join("types.ts"),
+ "export type Probe = { id: string };\n",
+ )
+ .expect("write types");
+ fs::write(export_dir.join("constants.ts"), "export const PROBE = 1;\n")
+ .expect("write constants");
+ fs::write(export_dir.join("kinds.ts"), "export const KIND = 1;\n")
+ .expect("write kinds");
+ }
+ }
+}
+"#,
+ );
+ }
+
fn test_ts_mapping() -> contract::ExportMapping {
let mut packages = BTreeMap::new();
packages.insert("radroots-core".to_string(), "@radroots/core".to_string());
@@ -1278,4 +1323,631 @@ manifest_file = "export-manifest.json"
assert!(!generated.join("b").exists());
let _ = fs::remove_dir_all(root);
}
+
+ #[test]
+ fn wrapper_calls_succeed_for_bundle_and_single_crate() {
+ let _guard = workspace_lock().lock().expect("workspace lock");
+ let root = create_synthetic_workspace("wrapper_bundle_success", 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_ts_rs_probe_lib(&root);
+
+ let out_dir = root.join("out");
+ fs::create_dir_all(&out_dir).expect("create bundle out");
+ let manifest = export_ts_bundle(&root, &out_dir).expect("bundle success");
+ assert!(manifest.exists());
+ assert!(
+ out_dir
+ .join("ts")
+ .join("packages")
+ .join("a")
+ .join("src/generated")
+ .join("types.ts")
+ .exists()
+ );
+ assert!(
+ out_dir
+ .join("ts")
+ .join("packages")
+ .join("a")
+ .join("src/generated")
+ .join("constants.ts")
+ .exists()
+ );
+ assert!(
+ out_dir
+ .join("ts")
+ .join("packages")
+ .join("a")
+ .join("src/generated")
+ .join("kinds.ts")
+ .exists()
+ );
+
+ let single_out = root.join("single-out");
+ fs::create_dir_all(&single_out).expect("create single out");
+ let single_manifest =
+ export_ts_bundle_for_crate(&root, &single_out, "radroots-a").expect("single bundle");
+ assert!(single_manifest.exists());
+ assert!(
+ single_out
+ .join("ts")
+ .join("packages")
+ .join("a")
+ .join("src/generated")
+ .join("types.ts")
+ .exists()
+ );
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn wrapper_calls_surface_mid_pipeline_errors() {
+ let _guard = workspace_lock().lock().expect("workspace lock");
+
+ let bundle_models = create_synthetic_workspace("wrapper_bundle_models_fail", true);
+ write_ts_rs_probe_lib(&bundle_models);
+ let bundle_models_out = bundle_models.join("out");
+ write_file(
+ &bundle_models_out.join("ts").join("packages").join("a"),
+ "blocker",
+ );
+ let bundle_models_err =
+ export_ts_bundle(&bundle_models, &bundle_models_out).expect_err("bundle models fail");
+ assert!(bundle_models_err.contains("create"));
+ let _ = fs::remove_dir_all(&bundle_models);
+
+ let bundle_constants = create_synthetic_workspace("wrapper_bundle_constants_fail", true);
+ write_file(
+ &bundle_constants
+ .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/types.ts"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_ts_rs_probe_lib(&bundle_constants);
+ let bundle_constants_out = bundle_constants.join("out");
+ fs::create_dir_all(&bundle_constants_out).expect("create bundle constants out");
+ let bundle_constants_err = export_ts_bundle(&bundle_constants, &bundle_constants_out)
+ .expect_err("bundle constants fail");
+ assert!(bundle_constants_err.contains("create"));
+ let _ = fs::remove_dir_all(&bundle_constants);
+
+ let bundle_wasm = create_synthetic_workspace("wrapper_bundle_wasm_fail", true);
+ write_file(
+ &bundle_wasm.join("contract").join("exports").join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a" = "@radroots/a"
+"radroots-a-wasm" = "@radroots/a-wasm"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_ts_rs_probe_lib(&bundle_wasm);
+ write_file(
+ &bundle_wasm
+ .join("crates")
+ .join("a-wasm")
+ .join("pkg")
+ .join("dist")
+ .join("nested")
+ .join("artifact.js"),
+ "export const wasm = 1;\n",
+ );
+ let bundle_wasm_out = bundle_wasm.join("out");
+ write_file(
+ &bundle_wasm_out
+ .join("ts")
+ .join("packages")
+ .join("a-wasm")
+ .join("dist")
+ .join("nested"),
+ "blocker",
+ );
+ let bundle_wasm_err =
+ export_ts_bundle(&bundle_wasm, &bundle_wasm_out).expect_err("bundle wasm fail");
+ assert!(bundle_wasm_err.contains("create"));
+ let _ = fs::remove_dir_all(&bundle_wasm);
+
+ let crate_models = create_synthetic_workspace("wrapper_crate_models_fail", true);
+ write_ts_rs_probe_lib(&crate_models);
+ let crate_models_out = crate_models.join("out");
+ write_file(
+ &crate_models_out.join("ts").join("packages").join("a"),
+ "blocker",
+ );
+ let crate_models_err =
+ export_ts_bundle_for_crate(&crate_models, &crate_models_out, "radroots-a")
+ .expect_err("crate models fail");
+ assert!(crate_models_err.contains("create"));
+ let _ = fs::remove_dir_all(&crate_models);
+
+ let crate_constants = create_synthetic_workspace("wrapper_crate_constants_fail", true);
+ write_file(
+ &crate_constants
+ .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/types.ts"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_ts_rs_probe_lib(&crate_constants);
+ let crate_constants_out = crate_constants.join("out");
+ fs::create_dir_all(&crate_constants_out).expect("create crate constants out");
+ let crate_constants_err =
+ export_ts_bundle_for_crate(&crate_constants, &crate_constants_out, "radroots-a")
+ .expect_err("crate constants fail");
+ assert!(crate_constants_err.contains("create"));
+ let _ = fs::remove_dir_all(&crate_constants);
+
+ let crate_wasm = create_synthetic_workspace("wrapper_crate_wasm_fail", true);
+ write_file(
+ &crate_wasm.join("contract").join("exports").join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a-wasm" = "@radroots/a-wasm"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_file(
+ &crate_wasm
+ .join("crates")
+ .join("a-wasm")
+ .join("pkg")
+ .join("dist")
+ .join("nested")
+ .join("artifact.js"),
+ "export const wasm = 1;\n",
+ );
+ let crate_wasm_out = crate_wasm.join("out");
+ write_file(
+ &crate_wasm_out
+ .join("ts")
+ .join("packages")
+ .join("a-wasm")
+ .join("dist")
+ .join("nested"),
+ "blocker",
+ );
+ let crate_wasm_err =
+ export_ts_bundle_for_crate(&crate_wasm, &crate_wasm_out, "radroots-a-wasm")
+ .expect_err("crate wasm fail");
+ assert!(crate_wasm_err.contains("create"));
+ let _ = fs::remove_dir_all(&crate_wasm);
+ }
+
+ #[test]
+ fn wrapper_calls_surface_contract_and_selector_errors() {
+ let missing_root = unique_temp_dir("wrapper_missing_contract");
+ fs::create_dir_all(&missing_root).expect("create missing root");
+ let missing_out = missing_root.join("out");
+ fs::create_dir_all(&missing_out).expect("create missing out");
+ assert!(export_ts_models(&missing_root, &missing_out).is_err());
+ assert!(export_ts_constants(&missing_root, &missing_out).is_err());
+ assert!(export_ts_wasm_artifacts(&missing_root, &missing_out).is_err());
+ assert!(write_ts_export_manifest(&missing_root, &missing_out).is_err());
+ assert!(generate_ts_rs_sources(&missing_root).is_err());
+ assert!(export_ts_bundle(&missing_root, &missing_out).is_err());
+ assert!(export_ts_bundle_for_crate(&missing_root, &missing_out, "radroots-a").is_err());
+ let _ = fs::remove_dir_all(&missing_root);
+
+ let invalid_contract = create_synthetic_workspace("wrapper_invalid_contract", true);
+ write_file(
+ &invalid_contract.join("contract").join("manifest.toml"),
+ r#"[contract]
+name = ""
+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
+"#,
+ );
+ let invalid_out = invalid_contract.join("out");
+ fs::create_dir_all(&invalid_out).expect("create invalid out");
+ assert!(export_ts_models(&invalid_contract, &invalid_out).is_err());
+ assert!(export_ts_constants(&invalid_contract, &invalid_out).is_err());
+ assert!(export_ts_wasm_artifacts(&invalid_contract, &invalid_out).is_err());
+ assert!(write_ts_export_manifest(&invalid_contract, &invalid_out).is_err());
+ assert!(generate_ts_rs_sources(&invalid_contract).is_err());
+ let _ = fs::remove_dir_all(&invalid_contract);
+
+ let missing_ts_export = create_synthetic_workspace("wrapper_missing_ts_export", true);
+ write_file(
+ &missing_ts_export
+ .join("contract")
+ .join("exports")
+ .join("ts.toml"),
+ r#"[language]
+id = "py"
+repository = "sdk-python"
+
+[packages]
+"radroots-a" = "radroots-a"
+"#,
+ );
+ let missing_ts_out = missing_ts_export.join("out");
+ fs::create_dir_all(&missing_ts_out).expect("create missing ts out");
+ assert!(export_ts_models(&missing_ts_export, &missing_ts_out).is_err());
+ assert!(export_ts_constants(&missing_ts_export, &missing_ts_out).is_err());
+ assert!(export_ts_wasm_artifacts(&missing_ts_export, &missing_ts_out).is_err());
+ assert!(write_ts_export_manifest(&missing_ts_export, &missing_ts_out).is_err());
+ assert!(generate_ts_rs_sources(&missing_ts_export).is_err());
+ let _ = fs::remove_dir_all(&missing_ts_export);
+
+ let selector_root = create_synthetic_workspace("wrapper_selector_errors", true);
+ let selector_out = selector_root.join("out");
+ fs::create_dir_all(&selector_out).expect("create selector out");
+ let models_selector_err =
+ export_ts_models_for_crate(&selector_root, &selector_out, "missing-crate")
+ .expect_err("models unknown selector");
+ assert!(models_selector_err.contains("unknown ts export crate selector"));
+ let constants_selector_err =
+ export_ts_constants_for_crate(&selector_root, &selector_out, "missing-crate")
+ .expect_err("constants unknown selector");
+ assert!(constants_selector_err.contains("unknown ts export crate selector"));
+ let wasm_selector_err =
+ export_ts_wasm_artifacts_for_crate(&selector_root, &selector_out, "missing-crate")
+ .expect_err("wasm unknown selector");
+ assert!(wasm_selector_err.contains("unknown ts export crate selector"));
+ let generate_selector_err =
+ generate_ts_rs_sources_for_crate(&selector_root, "missing-crate")
+ .expect_err("generate unknown selector");
+ assert!(generate_selector_err.contains("unknown ts export crate selector"));
+ let bundle_selector_err =
+ export_ts_bundle_for_crate(&selector_root, &selector_out, "missing-crate")
+ .expect_err("bundle unknown selector");
+ assert!(bundle_selector_err.contains("unknown ts export crate selector"));
+ let _ = fs::remove_dir_all(&selector_root);
+ }
+
+ #[test]
+ fn wrapper_calls_surface_artifact_and_copy_errors() {
+ let missing_artifacts = create_synthetic_workspace("wrapper_missing_artifacts", true);
+ write_file(
+ &missing_artifacts
+ .join("contract")
+ .join("exports")
+ .join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a" = "@radroots/a"
+"#,
+ );
+ let missing_artifacts_out = missing_artifacts.join("out");
+ fs::create_dir_all(&missing_artifacts_out).expect("create missing artifacts out");
+ assert!(export_ts_models(&missing_artifacts, &missing_artifacts_out).is_err());
+ assert!(export_ts_constants(&missing_artifacts, &missing_artifacts_out).is_err());
+ assert!(export_ts_wasm_artifacts(&missing_artifacts, &missing_artifacts_out).is_err());
+ assert!(write_ts_export_manifest(&missing_artifacts, &missing_artifacts_out).is_err());
+ let _ = fs::remove_dir_all(&missing_artifacts);
+
+ let missing_models_dir = create_synthetic_workspace("wrapper_missing_models_dir", true);
+ write_file(
+ &missing_models_dir
+ .join("contract")
+ .join("exports")
+ .join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a" = "@radroots/a"
+
+[artifacts]
+models_dir = ""
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ let missing_models_out = missing_models_dir.join("out");
+ fs::create_dir_all(&missing_models_out).expect("create missing models out");
+ let missing_models_err =
+ export_ts_models(&missing_models_dir, &missing_models_out).expect_err("models dir");
+ assert!(missing_models_err.contains("artifacts fields must be non-empty for ts"));
+ let _ = fs::remove_dir_all(&missing_models_dir);
+
+ let missing_constants_dir =
+ create_synthetic_workspace("wrapper_missing_constants_dir", true);
+ write_file(
+ &missing_constants_dir
+ .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 = ""
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ let missing_constants_out = missing_constants_dir.join("out");
+ fs::create_dir_all(&missing_constants_out).expect("create missing constants out");
+ let missing_constants_err =
+ export_ts_constants(&missing_constants_dir, &missing_constants_out)
+ .expect_err("constants dir");
+ assert!(missing_constants_err.contains("artifacts fields must be non-empty for ts"));
+ let _ = fs::remove_dir_all(&missing_constants_dir);
+
+ let missing_wasm_dir = create_synthetic_workspace("wrapper_missing_wasm_dir", true);
+ write_file(
+ &missing_wasm_dir
+ .join("contract")
+ .join("exports")
+ .join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a-wasm" = "@radroots/a-wasm"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = ""
+manifest_file = "export-manifest.json"
+"#,
+ );
+ let missing_wasm_out = missing_wasm_dir.join("out");
+ fs::create_dir_all(&missing_wasm_out).expect("create missing wasm out");
+ let missing_wasm_err =
+ export_ts_wasm_artifacts(&missing_wasm_dir, &missing_wasm_out).expect_err("wasm dir");
+ assert!(missing_wasm_err.contains("artifacts fields must be non-empty for ts"));
+ let _ = fs::remove_dir_all(&missing_wasm_dir);
+
+ let missing_manifest_file =
+ create_synthetic_workspace("wrapper_missing_manifest_file", true);
+ write_file(
+ &missing_manifest_file
+ .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 = ""
+"#,
+ );
+ let missing_manifest_out = missing_manifest_file.join("out");
+ fs::create_dir_all(&missing_manifest_out).expect("create missing manifest out");
+ let missing_manifest_err =
+ write_ts_export_manifest(&missing_manifest_file, &missing_manifest_out)
+ .expect_err("manifest file");
+ assert!(missing_manifest_err.contains("artifacts fields must be non-empty for ts"));
+ let _ = fs::remove_dir_all(&missing_manifest_file);
+
+ let models_copy_err_root = create_synthetic_workspace("wrapper_models_copy_err", true);
+ write_file(
+ &models_copy_err_root
+ .join("target")
+ .join("ts-rs")
+ .join("a")
+ .join("types.ts"),
+ "export type Probe = { id: string };\n",
+ );
+ let models_copy_out = models_copy_err_root.join("out");
+ write_file(
+ &models_copy_out.join("ts").join("packages").join("a"),
+ "blocker",
+ );
+ let models_copy_err =
+ export_ts_models(&models_copy_err_root, &models_copy_out).expect_err("copy models");
+ assert!(models_copy_err.contains("create"));
+ let _ = fs::remove_dir_all(&models_copy_err_root);
+
+ let constants_copy_err_root =
+ create_synthetic_workspace("wrapper_constants_copy_err", true);
+ write_file(
+ &constants_copy_err_root
+ .join("target")
+ .join("ts-rs")
+ .join("a")
+ .join("constants.ts"),
+ "export const A = 1;\n",
+ );
+ write_file(
+ &constants_copy_err_root
+ .join("target")
+ .join("ts-rs")
+ .join("a")
+ .join("kinds.ts"),
+ "export const K = 1;\n",
+ );
+ let constants_copy_out = constants_copy_err_root.join("out");
+ write_file(
+ &constants_copy_out.join("ts").join("packages").join("a"),
+ "blocker",
+ );
+ let constants_copy_err = export_ts_constants(&constants_copy_err_root, &constants_copy_out)
+ .expect_err("copy constants");
+ assert!(constants_copy_err.contains("create"));
+ let _ = fs::remove_dir_all(&constants_copy_err_root);
+
+ let wasm_copy_err_root = create_synthetic_workspace("wrapper_wasm_copy_err", true);
+ write_file(
+ &wasm_copy_err_root
+ .join("contract")
+ .join("exports")
+ .join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a-wasm" = "@radroots/a-wasm"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_file(
+ &wasm_copy_err_root
+ .join("crates")
+ .join("a-wasm")
+ .join("pkg")
+ .join("dist")
+ .join("nested")
+ .join("artifact.js"),
+ "export const wasm = 1;\n",
+ );
+ let wasm_copy_out = wasm_copy_err_root.join("out");
+ write_file(
+ &wasm_copy_out
+ .join("ts")
+ .join("packages")
+ .join("a-wasm")
+ .join("dist")
+ .join("nested"),
+ "blocker",
+ );
+ let wasm_copy_err =
+ export_ts_wasm_artifacts(&wasm_copy_err_root, &wasm_copy_out).expect_err("copy wasm");
+ assert!(wasm_copy_err.contains("create"));
+ let _ = fs::remove_dir_all(&wasm_copy_err_root);
+
+ let manifest_collect_err_root =
+ create_synthetic_workspace("wrapper_manifest_collect_err", true);
+ let manifest_collect_out = manifest_collect_err_root.join("out");
+ write_file(
+ &manifest_collect_out.join("ts").join("packages"),
+ "not-a-directory",
+ );
+ let manifest_collect_err =
+ write_ts_export_manifest(&manifest_collect_err_root, &manifest_collect_out)
+ .expect_err("manifest collect error");
+ assert!(manifest_collect_err.contains("read dir"));
+ let _ = fs::remove_dir_all(&manifest_collect_err_root);
+
+ let recursive_copy_root = unique_temp_dir("wrapper_recursive_copy");
+ let recursive_src = recursive_copy_root.join("src");
+ let recursive_dst = recursive_copy_root.join("dst");
+ write_file(&recursive_src.join("nested").join("value.txt"), "value");
+ write_file(&recursive_dst.join("nested"), "blocker");
+ let recursive_copy_err =
+ copy_dir_contents(&recursive_src, &recursive_dst).expect_err("recursive copy error");
+ assert!(recursive_copy_err.contains("create"));
+ let _ = fs::remove_dir_all(&recursive_copy_root);
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+
+ let recursive_manifest_root = unique_temp_dir("wrapper_recursive_manifest");
+ let recursive_manifest_src = recursive_manifest_root.join("src");
+ write_file(
+ &recursive_manifest_src.join("nested").join("value.txt"),
+ "value",
+ );
+ let mut restricted = fs::metadata(recursive_manifest_src.join("nested"))
+ .expect("nested metadata")
+ .permissions();
+ restricted.set_mode(0o000);
+ fs::set_permissions(recursive_manifest_src.join("nested"), restricted)
+ .expect("set nested restricted mode");
+
+ let mut entries = Vec::new();
+ let recursive_manifest_err = collect_manifest_entries(
+ &recursive_manifest_src,
+ &recursive_manifest_src,
+ &recursive_manifest_src.join("skip.json"),
+ &mut entries,
+ )
+ .expect_err("recursive manifest error");
+ assert!(recursive_manifest_err.contains("read dir"));
+
+ let mut readable = fs::metadata(recursive_manifest_src.join("nested"))
+ .expect("nested metadata")
+ .permissions();
+ readable.set_mode(0o755);
+ fs::set_permissions(recursive_manifest_src.join("nested"), readable)
+ .expect("restore nested readable mode");
+ let _ = fs::remove_dir_all(&recursive_manifest_root);
+ }
+ }
}