commit 4f049f8e3e71a944b1d79be6eb8e2c3a0fb6b3fa
parent 32c91ec0d2f9b3be930d063232601cfb5d7e4aa1
Author: triesap <tyson@radroots.org>
Date: Wed, 4 Mar 2026 20:56:36 +0000
xtask: close strict coverage gaps
Diffstat:
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");