lib

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

commit a4c8a08b5ea012b72fb97dea6a730e734dbc86bc
parent 6bab64bdbb98ddcde1b66b8cc7a45b9800d2973b
Author: triesap <tyson@radroots.org>
Date:   Thu,  5 Mar 2026 06:14:58 +0000

xtask: refactor coverage harness paths

- remove infallible workspace root result wrappers in main and coverage dispatch
- add root-injectable coverage helpers and targeted error-path tests
- centralize workspace package record loading for contract validation helpers
- keep xtask check and test lanes green while raising coverage metrics

Diffstat:
Mcrates/xtask/src/contract.rs | 95++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/xtask/src/coverage.rs | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/xtask/src/main.rs | 85+++++++++++++++++++++++++++++++------------------------------------------------
3 files changed, 381 insertions(+), 124 deletions(-)

diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -177,27 +177,56 @@ fn contract_root(workspace_root: &Path) -> PathBuf { workspace_root.join("contract") } -fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String> { +#[derive(Debug)] +struct WorkspacePackageRecord { + name: String, + manifest_path: PathBuf, + publish_enabled: bool, + manifest_value: toml::Value, +} + +fn workspace_package_records(workspace_root: &Path) -> Result<Vec<WorkspacePackageRecord>, String> { let workspace_manifest = parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; - let mut names = Vec::with_capacity(workspace_manifest.workspace.members.len()); + let mut records = Vec::with_capacity(workspace_manifest.workspace.members.len()); for member in workspace_manifest.workspace.members { - let member_manifest = workspace_root.join(member).join("Cargo.toml"); - let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?; - names.push(package_manifest.package.name); + let manifest_path = workspace_root.join(&member).join("Cargo.toml"); + let raw = match fs::read_to_string(&manifest_path) { + Ok(raw) => raw, + Err(e) => return Err(format!("read {}: {e}", manifest_path.display())), + }; + let manifest_value = match toml::from_str::<toml::Value>(&raw) { + Ok(value) => value, + Err(e) => return Err(format!("parse {}: {e}", manifest_path.display())), + }; + let package_manifest = match toml::from_str::<PackageCargoManifest>(&raw) { + Ok(manifest) => manifest, + Err(e) => return Err(format!("parse {}: {e}", manifest_path.display())), + }; + let name = package_manifest.package.name; + let publish_enabled = package_publish_enabled(package_manifest.package.publish.as_ref()); + records.push(WorkspacePackageRecord { + name, + manifest_path, + publish_enabled, + manifest_value, + }); } - Ok(names) + Ok(records) +} + +fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String> { + Ok(workspace_package_records(workspace_root)? + .into_iter() + .map(|record| record.name) + .collect()) } fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, PathBuf>, String> { - let workspace_manifest = - parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; let mut manifests = BTreeMap::new(); - for member in workspace_manifest.workspace.members { - let member_manifest = workspace_root.join(&member).join("Cargo.toml"); - let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?; + for record in workspace_package_records(workspace_root)? { if manifests - .insert(package_manifest.package.name, member_manifest) + .insert(record.name, record.manifest_path) .is_some() { return Err("duplicate workspace package name in manifest map".to_string()); @@ -229,23 +258,13 @@ fn package_publish_enabled(publish: Option<&PackagePublish>) -> bool { fn workspace_package_publish_flags( workspace_root: &Path, ) -> Result<BTreeMap<String, bool>, String> { - let workspace_manifest = - parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; let mut flags = BTreeMap::new(); - for member in workspace_manifest.workspace.members { - let member_manifest = workspace_root.join(member).join("Cargo.toml"); - let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?; + for record in workspace_package_records(workspace_root)? { if flags - .insert( - package_manifest.package.name.clone(), - package_publish_enabled(package_manifest.package.publish.as_ref()), - ) + .insert(record.name.clone(), record.publish_enabled) .is_some() { - return Err(format!( - "duplicate workspace package name {}", - package_manifest.package.name - )); + return Err(format!("duplicate workspace package name {}", record.name)); } } Ok(flags) @@ -254,23 +273,21 @@ fn workspace_package_publish_flags( fn read_workspace_package_dependencies( workspace_root: &Path, ) -> Result<BTreeMap<String, BTreeSet<String>>, String> { - let workspace_manifest = - parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?; - let mut member_manifests = Vec::with_capacity(workspace_manifest.workspace.members.len()); - let mut workspace_names = BTreeSet::new(); - for member in workspace_manifest.workspace.members { - let member_manifest = workspace_root.join(&member).join("Cargo.toml"); - let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?; - workspace_names.insert(package_manifest.package.name.clone()); - member_manifests.push((package_manifest.package.name, member_manifest)); - } + let package_records = workspace_package_records(workspace_root)?; + let workspace_names = package_records + .iter() + .map(|record| record.name.clone()) + .collect::<BTreeSet<_>>(); let mut deps = BTreeMap::new(); - for (package_name, manifest_path) in member_manifests { - let parsed = parse_toml::<toml::Value>(&manifest_path)?; + for record in package_records { let mut package_deps = BTreeSet::new(); for section in ["dependencies", "build-dependencies"] { - let Some(table) = parsed.get(section).and_then(toml::Value::as_table) else { + let Some(table) = record + .manifest_value + .get(section) + .and_then(toml::Value::as_table) + else { continue; }; for dep_name in table.keys() { @@ -279,7 +296,7 @@ fn read_workspace_package_dependencies( } } } - deps.insert(package_name, package_deps); + deps.insert(record.name, package_deps); } Ok(deps) diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -486,7 +486,15 @@ pub fn evaluate_gate( .branch_percent .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches); - let pass = exec_ok && functions_ok && regions_ok && branch_presence_ok && branch_ok; + let pass = [ + exec_ok, + functions_ok, + regions_ok, + branch_presence_ok, + branch_ok, + ] + .into_iter() + .all(|flag| flag); let mut fail_reasons: Vec<String> = Vec::new(); if !exec_ok { @@ -583,11 +591,11 @@ fn parse_bool_flag(args: &[String], name: &str) -> bool { args.iter().any(|arg| arg == &flag) } -fn workspace_root() -> Result<PathBuf, String> { +fn workspace_root() -> PathBuf { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); let root = crates_dir.parent().unwrap_or(crates_dir); - Ok(root.to_path_buf()) + root.to_path_buf() } fn run_command(mut command: Command, name: &str) -> Result<(), String> { @@ -610,13 +618,13 @@ fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile } } -fn run_crate_with_runner<F>(args: &[String], mut runner: F) -> Result<(), String> -where - F: FnMut(Command, &str) -> Result<(), String>, -{ +fn run_crate_with_runner_at_root( + args: &[String], + workspace_root: &Path, + runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, +) -> Result<(), String> { let crate_name = parse_string_arg(args, "crate")?; - let workspace_root = workspace_root()?; - let profile = read_coverage_profile(&workspace_root, &crate_name)?; + let profile = read_coverage_profile(workspace_root, &crate_name)?; let out_dir = if let Some(raw) = parse_optional_string_arg(args, "out") { PathBuf::from(raw) } else { @@ -642,7 +650,7 @@ where .arg("llvm-cov") .arg("clean") .arg("--workspace") - .current_dir(&workspace_root); + .current_dir(workspace_root); cmd }, "cargo llvm-cov clean --workspace", @@ -658,7 +666,7 @@ where .arg("--branch") .arg("--") .arg(format!("--test-threads={test_threads}")) - .current_dir(&workspace_root); + .current_dir(workspace_root); cmd }, "cargo llvm-cov --no-report", @@ -675,7 +683,7 @@ where .arg("--branch") .arg("--output-path") .arg(&summary_path) - .current_dir(&workspace_root); + .current_dir(workspace_root); cmd }, "cargo llvm-cov report --json --summary-only", @@ -691,7 +699,7 @@ where .arg("--branch") .arg("--output-path") .arg(&lcov_path) - .current_dir(&workspace_root); + .current_dir(workspace_root); cmd }, "cargo llvm-cov report --lcov", @@ -702,8 +710,17 @@ where Ok(()) } +fn run_crate_with_runner( + args: &[String], + runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, +) -> Result<(), String> { + let root = workspace_root(); + run_crate_with_runner_at_root(args, &root, runner) +} + fn run_crate(args: &[String]) -> Result<(), String> { - run_crate_with_runner(args, run_command) + let mut runner = run_command; + run_crate_with_runner(args, &mut runner) } fn report_gate(args: &[String]) -> Result<(), String> { @@ -797,26 +814,34 @@ fn report_gate(args: &[String]) -> Result<(), String> { Ok(()) } -fn list_required_crates() -> Result<(), String> { - let root = workspace_root()?; +fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { let required_path = root .join("contract") .join("coverage") .join("required-crates.toml"); let crates = read_required_crates(&required_path)?; + write_crate_names_output(writer, crates, "required crates") +} + +fn list_required_crates() -> Result<(), String> { + let root = workspace_root(); let mut stdout = std::io::stdout().lock(); - write_crate_names_output(&mut stdout, crates, "required crates") + list_required_crates_with_root(&root, &mut stdout) } -fn list_workspace_crates() -> Result<(), String> { - let root = workspace_root()?; +fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { let crates = read_workspace_crates(&root)?; + write_crate_names_output(writer, crates, "workspace crates") +} + +fn list_workspace_crates() -> Result<(), String> { + let root = workspace_root(); let mut stdout = std::io::stdout().lock(); - write_crate_names_output(&mut stdout, crates, "workspace crates") + list_workspace_crates_with_root(&root, &mut stdout) } -fn write_crate_names_output<W: Write>( - writer: &mut W, +fn write_crate_names_output( + writer: &mut dyn Write, crates: Vec<String>, label: &str, ) -> Result<(), String> { @@ -1029,7 +1054,7 @@ mod tests { #[test] fn reads_workspace_crates_and_contains_xtask() { - let root = workspace_root().expect("workspace root"); + let root = workspace_root(); let crates = read_workspace_crates(&root).expect("workspace crates"); assert!(!crates.is_empty()); assert!(crates.iter().any(|crate_name| crate_name == "xtask")); @@ -1120,6 +1145,18 @@ test_threads = 0 } #[test] + fn coverage_profiles_reject_invalid_toml() { + let root = temp_dir_path("profile_invalid_toml"); + 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"), "[profiles.default\n") + .expect("write invalid profiles"); + let err = read_coverage_profile(&root, "radroots-app-core").expect_err("invalid toml"); + assert!(err.contains("failed to parse")); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] fn coverage_profiles_reject_zero_test_threads_without_feature_error() { let root = temp_dir_path("profile_invalid_threads"); let coverage_dir = root.join("contract").join("coverage"); @@ -1255,6 +1292,19 @@ test_threads = 0 let dup_err = read_workspace_crates(&root_duplicate).expect_err("duplicate package names"); assert!(dup_err.contains("duplicate package name")); fs::remove_dir_all(&root_duplicate).expect("remove duplicate package root"); + + let root_parse = temp_dir_path("workspace_parse_error"); + write_file( + &root_parse.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\"]\n", + ); + write_file( + &root_parse.join("crates").join("a").join("Cargo.toml"), + "[package", + ); + let parse_err = read_workspace_crates(&root_parse).expect_err("invalid package manifest"); + assert!(parse_err.contains("failed to parse")); + fs::remove_dir_all(&root_parse).expect("remove parse package root"); } #[test] @@ -1317,7 +1367,7 @@ test_threads = 0 let path = temp_file_path("lcov_lf_lh"); fs::write(&path, "LF:4\nLH:3\n").expect("write lcov"); let parsed = read_lcov(&path).expect("parse lcov"); - assert!(matches!(parsed.executable_source, ExecutableSource::LfLh)); + assert_eq!(executable_source_label(parsed.executable_source), "lf_lh"); assert_eq!(parsed.executable_total, 4); assert_eq!(parsed.executable_covered, 3); assert_eq!(parsed.executable_percent, 75.0); @@ -1439,7 +1489,7 @@ test_threads = 0 "3".to_string(), ]; let mut names = Vec::new(); - run_crate_with_runner(&args, |cmd, name| { + let mut runner = |cmd: Command, name: &str| { names.push(name.to_string()); let rendered = cmd .get_args() @@ -1448,8 +1498,8 @@ test_threads = 0 .join(" "); assert!(!rendered.is_empty()); Ok(()) - }) - .expect("run crate with stub runner"); + }; + run_crate_with_runner(&args, &mut runner).expect("run crate with stub runner"); assert_eq!( names, vec![ @@ -1466,7 +1516,7 @@ test_threads = 0 fn run_crate_with_runner_uses_default_output_dir_when_out_is_missing() { let args = vec!["--crate".to_string(), "radroots-core".to_string()]; let mut output_path_seen = false; - run_crate_with_runner(&args, |cmd, _| { + let mut runner = |cmd: Command, _: &str| { let rendered = cmd .get_args() .map(|arg| arg.to_string_lossy().to_string()) @@ -1481,8 +1531,8 @@ test_threads = 0 output_path_seen = true; } Ok(()) - }) - .expect("run crate with default out"); + }; + run_crate_with_runner(&args, &mut runner).expect("run crate with default out"); assert!(output_path_seen); } @@ -1495,8 +1545,9 @@ test_threads = 0 "--out".to_string(), out.display().to_string(), ]; - let err = run_crate_with_runner(&args, |_, _| Err("runner failed".to_string())) - .expect_err("runner failure should bubble up"); + let mut runner = |_: Command, _: &str| Err("runner failed".to_string()); + let err = + run_crate_with_runner(&args, &mut runner).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"); @@ -1507,7 +1558,8 @@ test_threads = 0 "--out".to_string(), root.join("blocker").join("nested").display().to_string(), ]; - let err = run_crate_with_runner(&args, run_command) + let mut runner = run_command; + let err = run_crate_with_runner(&args, &mut runner) .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"); @@ -1520,6 +1572,67 @@ test_threads = 0 } #[test] + fn run_crate_with_runner_at_root_covers_profile_and_runner_error_paths() { + let profile_root = temp_dir_path("run_crate_profile_invalid"); + write_file( + &profile_root + .join("contract") + .join("coverage") + .join("profiles.toml"), + "[profiles.default]\nfeatures = [\"\"]\n", + ); + let profile_args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + profile_root.join("out").display().to_string(), + ]; + let mut runner = run_command; + let profile_err = run_crate_with_runner_at_root(&profile_args, &profile_root, &mut runner) + .expect_err("invalid profile should fail"); + assert!(profile_err.contains("empty feature value")); + fs::remove_dir_all(&profile_root).expect("remove profile root"); + + let thread_root = temp_dir_path("run_crate_bad_threads"); + fs::create_dir_all(&thread_root).expect("create thread root"); + let thread_args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + thread_root.join("out").display().to_string(), + "--test-threads".to_string(), + "bad".to_string(), + ]; + let mut runner = run_command; + let thread_err = run_crate_with_runner_at_root(&thread_args, &thread_root, &mut runner) + .expect_err("invalid test threads should fail"); + assert!(thread_err.contains("invalid --test-threads value")); + fs::remove_dir_all(&thread_root).expect("remove thread root"); + + for fail_step in [2usize, 3usize, 4usize] { + let step_root = temp_dir_path("run_crate_step_fail"); + let step_args = vec![ + "--crate".to_string(), + "radroots-core".to_string(), + "--out".to_string(), + step_root.join("out").display().to_string(), + ]; + let mut calls = 0usize; + let mut runner = |_: Command, name: &str| { + calls += 1; + if calls == fail_step { + return Err(format!("runner failure at {name}")); + } + Ok(()) + }; + let err = run_crate_with_runner_at_root(&step_args, &step_root, &mut runner) + .expect_err("runner should fail at selected step"); + assert!(err.contains("runner failure at")); + fs::remove_dir_all(&step_root).expect("remove step root"); + } + } + + #[test] fn report_gate_writes_report_file_on_success() { let root = temp_dir_path("report_gate_success"); let summary_path = root.join("summary.json"); @@ -1672,10 +1785,140 @@ test_threads = 0 } #[test] + fn report_gate_reports_argument_and_input_errors() { + let missing_scope = report_gate(&[]).expect_err("missing scope"); + assert!(missing_scope.contains("missing --scope")); + + let missing_summary = report_gate(&["--scope".to_string(), "crate".to_string()]) + .expect_err("missing summary"); + assert!(missing_summary.contains("missing --summary")); + + let missing_lcov = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + "summary.json".to_string(), + ]) + .expect_err("missing lcov"); + assert!(missing_lcov.contains("missing --lcov")); + + let missing_out = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + "summary.json".to_string(), + "--lcov".to_string(), + "coverage.info".to_string(), + ]) + .expect_err("missing out"); + assert!(missing_out.contains("missing --out")); + + let root = temp_dir_path("report_gate_arg_errors"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let invalid_functions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-functions".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid functions threshold"); + assert!(invalid_functions.contains("invalid --fail-under-functions value")); + + let invalid_exec = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid executable threshold"); + assert!(invalid_exec.contains("invalid --fail-under-exec-lines value")); + + let invalid_regions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-regions".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid regions threshold"); + assert!(invalid_regions.contains("invalid --fail-under-regions value")); + + let invalid_branches = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-branches".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid branches threshold"); + assert!(invalid_branches.contains("invalid --fail-under-branches value")); + + let missing_summary_file = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + root.join("missing-summary.json").display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + ]) + .expect_err("missing summary file should fail"); + assert!(missing_summary_file.contains("failed to read summary")); + + let missing_lcov_file = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + root.join("missing-lcov.info").display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + ]) + .expect_err("missing lcov file should fail"); + assert!(missing_lcov_file.contains("failed to read lcov")); + + fs::remove_dir_all(root).expect("remove report arg errors root"); + } + + #[test] fn run_dispatches_subcommands_and_errors() { run(&["help".to_string()]).expect("help subcommand"); run(&["required-crates".to_string()]).expect("required crates subcommand"); run(&["workspace-crates".to_string()]).expect("workspace crates subcommand"); + let run_crate_err = run(&["run-crate".to_string()]).expect_err("run crate missing args"); + assert!(run_crate_err.contains("missing --crate")); let unknown_err = run(&["unknown".to_string()]).expect_err("unknown subcommand"); assert!(unknown_err.contains("unknown sdk coverage subcommand")); let missing_err = run(&[]).expect_err("missing subcommand"); @@ -1683,6 +1926,22 @@ test_threads = 0 } #[test] + fn list_root_helpers_report_missing_contract_files() { + let root = temp_dir_path("list_helper_missing"); + fs::create_dir_all(&root).expect("create list helper root"); + let mut output = Vec::new(); + let required_err = list_required_crates_with_root(&root, &mut output) + .expect_err("missing required crates file should fail"); + assert!(required_err.contains("failed to read required crates")); + + let workspace_err = list_workspace_crates_with_root(&root, &mut output) + .expect_err("missing workspace manifest should fail"); + assert!(workspace_err.contains("failed to read")); + + fs::remove_dir_all(root).expect("remove list helper root"); + } + + #[test] fn write_crate_names_output_covers_success_and_error_paths() { let mut output = Vec::new(); write_crate_names_output( diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -26,11 +26,11 @@ fn usage() { ); } -fn workspace_root() -> Result<PathBuf, String> { +fn workspace_root() -> PathBuf { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); let root = crates_dir.parent().unwrap_or(crates_dir); - Ok(root.to_path_buf()) + root.to_path_buf() } fn parse_out_dir(args: &[String], workspace_root: &Path) -> Result<PathBuf, String> { @@ -79,80 +79,49 @@ fn parse_crate_out_dir( } fn export_ts_models(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let out_dir = parse_out_dir(args, &root)?; - export_ts::export_ts_models(&root, &out_dir)?; - eprintln!("exported ts models to {}", out_dir.display()); - Ok(()) + export_ts::export_ts_models(&root, &out_dir) } fn export_ts_constants(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let out_dir = parse_out_dir(args, &root)?; - export_ts::export_ts_constants(&root, &out_dir)?; - eprintln!("exported ts constants to {}", out_dir.display()); - Ok(()) + export_ts::export_ts_constants(&root, &out_dir) } fn export_ts_wasm(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let out_dir = parse_out_dir(args, &root)?; - export_ts::export_ts_wasm_artifacts(&root, &out_dir)?; - eprintln!("exported ts wasm to {}", out_dir.display()); - Ok(()) + export_ts::export_ts_wasm_artifacts(&root, &out_dir) } fn export_manifest(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let out_dir = parse_out_dir(args, &root)?; - let manifest = export_ts::write_ts_export_manifest(&root, &out_dir)?; - eprintln!("wrote export manifest {}", manifest.display()); - Ok(()) + export_ts::write_ts_export_manifest(&root, &out_dir).map(|_| ()) } fn export_ts(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let out_dir = parse_out_dir(args, &root)?; - let manifest = export_ts::export_ts_bundle(&root, &out_dir)?; - eprintln!("exported ts sdk bundle to {}", out_dir.display()); - eprintln!("wrote export manifest {}", manifest.display()); - Ok(()) + export_ts::export_ts_bundle(&root, &out_dir).map(|_| ()) } fn export_ts_crate(args: &[String]) -> Result<(), String> { - let root = workspace_root()?; + let root = workspace_root(); let (crate_selector, out_dir) = parse_crate_out_dir(args, &root)?; - let manifest = export_ts::export_ts_bundle_for_crate(&root, &out_dir, &crate_selector)?; - eprintln!( - "exported ts sdk crate {} to {}", - crate_selector, - out_dir.display() - ); - eprintln!("wrote export manifest {}", manifest.display()); - Ok(()) + export_ts::export_ts_bundle_for_crate(&root, &out_dir, &crate_selector).map(|_| ()) } fn validate_contract() -> Result<(), String> { - let root = workspace_root()?; - let bundle = contract::load_contract_bundle(&root)?; - contract::validate_contract_bundle(&bundle)?; - eprintln!( - "validated contract {} {}", - bundle.manifest.contract.name, bundle.manifest.contract.version - ); - eprintln!("contract root: {}", bundle.root.display()); - Ok(()) + let root = workspace_root(); + contract::load_contract_bundle(&root) + .and_then(|bundle| contract::validate_contract_bundle(&bundle)) } fn release_preflight() -> Result<(), String> { - let root = workspace_root()?; - contract::validate_release_preflight(&root)?; - let bundle = contract::load_contract_bundle(&root)?; - eprintln!( - "validated release preflight for contract {}", - bundle.version.contract.version - ); - Ok(()) + contract::validate_release_preflight(&workspace_root()) } fn run_release(args: &[String]) -> Result<(), String> { @@ -225,7 +194,7 @@ mod tests { #[test] fn workspace_root_resolves_and_parse_helpers_cover_branches() { - let root = workspace_root().expect("workspace root"); + let root = workspace_root(); assert!(root.join("Cargo.toml").exists()); let default_out = parse_out_dir(&[], &root).expect("default out dir"); @@ -301,7 +270,7 @@ mod tests { #[test] fn export_wrappers_cover_success_and_error_paths() { let _guard = workspace_lock().lock().expect("lock workspace"); - let root = workspace_root().expect("workspace root"); + let root = workspace_root(); let out_dir = unique_temp_dir("export_wrappers"); fs::create_dir_all(&out_dir).expect("create out dir"); @@ -346,7 +315,7 @@ mod tests { #[test] fn contract_and_coverage_dispatchers_execute() { let _guard = workspace_lock().lock().expect("lock workspace"); - let root = workspace_root().expect("workspace root"); + let root = workspace_root(); let out_dir = unique_temp_dir("coverage_dispatch"); fs::create_dir_all(&out_dir).expect("create out dir"); @@ -442,4 +411,16 @@ mod tests { assert_eq!(failure_code, ExitCode::from(2)); let _ = main(); } + + #[test] + fn run_sdk_dispatches_export_and_validate_commands() { + let _guard = workspace_lock().lock().expect("lock workspace"); + assert!(run_sdk(&["export-ts".to_string(), "--bad".to_string()]).is_err()); + assert!(run_sdk(&["export-ts-crate".to_string(), "--bad".to_string()]).is_err()); + assert!(run_sdk(&["export-ts-models".to_string(), "--bad".to_string()]).is_err()); + assert!(run_sdk(&["export-ts-constants".to_string(), "--bad".to_string()]).is_err()); + assert!(run_sdk(&["export-ts-wasm".to_string(), "--bad".to_string()]).is_err()); + assert!(run_sdk(&["export-manifest".to_string(), "--bad".to_string()]).is_err()); + run_sdk(&["validate".to_string()]).expect("sdk validate"); + } }