lib

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

commit f628cba3e4a9b543a39565a38c97af9f81986041
parent 93f8ff5630273b21597b09945ce657b7e7e04bd2
Author: triesap <tyson@radroots.org>
Date:   Wed,  4 Mar 2026 19:35:16 +0000

xtask: expand coverage command error-path tests

- add read and parse failure tests for summary required crates and lcov inputs
- add run command spawn-failure and report write-failure coverage tests
- refactor crate list output through a writer helper for deterministic failure testing
- keep xtask check and test lanes green while raising executable coverage

Diffstat:
Mcrates/xtask/src/coverage.rs | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 157 insertions(+), 8 deletions(-)

diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -809,21 +809,24 @@ fn list_required_crates() -> Result<(), String> { .join("required-crates.toml"); let crates = read_required_crates(&required_path)?; let mut stdout = std::io::stdout().lock(); - for crate_name in crates { - if let Err(err) = writeln!(stdout, "{crate_name}") { - return Err(format!("failed to write required crates output: {err}")); - } - } - Ok(()) + write_crate_names_output(&mut stdout, crates, "required crates") } fn list_workspace_crates() -> Result<(), String> { let root = workspace_root()?; let crates = read_workspace_crates(&root)?; let mut stdout = std::io::stdout().lock(); + write_crate_names_output(&mut stdout, crates, "workspace crates") +} + +fn write_crate_names_output<W: Write>( + writer: &mut W, + crates: Vec<String>, + label: &str, +) -> Result<(), String> { for crate_name in crates { - if let Err(err) = writeln!(stdout, "{crate_name}") { - return Err(format!("failed to write workspace crates output: {err}")); + if let Err(err) = writeln!(writer, "{crate_name}") { + return Err(format!("failed to write {label} output: {err}")); } } Ok(()) @@ -845,6 +848,7 @@ pub fn run(args: &[String]) -> Result<(), String> { mod tests { use super::*; use std::fs; + use std::io::{self, Write}; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; @@ -871,6 +875,18 @@ mod tests { fs::write(path, content).expect("write file"); } + struct FailingWriter; + + impl Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result<usize> { + Err(io::Error::other("forced write failure")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + #[test] fn reads_summary_totals_from_llvm_cov_json() { let path = temp_file_path("summary"); @@ -899,6 +915,19 @@ mod tests { } #[test] + fn read_summary_reports_read_and_parse_errors() { + let missing = temp_file_path("summary_missing"); + let read_err = read_summary(&missing).expect_err("missing summary should fail"); + assert!(read_err.contains("failed to read summary")); + + let invalid = temp_file_path("summary_invalid"); + write_file(&invalid, "{not-json"); + let parse_err = read_summary(&invalid).expect_err("invalid summary should fail"); + assert!(parse_err.contains("failed to parse summary")); + fs::remove_file(invalid).expect("remove invalid summary"); + } + + #[test] fn reads_lcov_da_and_branch_metrics() { let path = temp_file_path("lcov"); fs::write( @@ -982,6 +1011,19 @@ mod tests { } #[test] + fn read_required_crates_reports_read_and_parse_errors() { + let missing = temp_file_path("required_missing"); + let read_err = read_required_crates(&missing).expect_err("missing required file"); + assert!(read_err.contains("failed to read required crates")); + + let invalid = temp_file_path("required_invalid"); + write_file(&invalid, "not = [toml"); + let parse_err = read_required_crates(&invalid).expect_err("invalid required file"); + assert!(parse_err.contains("failed to parse required crates")); + fs::remove_file(invalid).expect("remove invalid required file"); + } + + #[test] fn reads_workspace_crates_and_contains_xtask() { let root = workspace_root().expect("workspace root"); let crates = read_workspace_crates(&root).expect("workspace crates"); @@ -1194,6 +1236,21 @@ test_threads = 0 } #[test] + fn parse_toml_reports_read_and_parse_errors() { + let missing = temp_file_path("parse_toml_missing"); + let read_err = + parse_toml::<CoverageRequiredContract>(&missing).expect_err("missing file should fail"); + assert!(read_err.contains("failed to read")); + + let invalid = temp_file_path("parse_toml_invalid"); + write_file(&invalid, "[required]\ncrates = [\n"); + let parse_err = + parse_toml::<CoverageRequiredContract>(&invalid).expect_err("invalid toml should fail"); + assert!(parse_err.contains("failed to parse")); + fs::remove_file(invalid).expect("remove invalid toml"); + } + + #[test] fn read_lcov_rejects_invalid_records() { let cases = vec![ ("invalid_da_shape", "DA:1\n", "invalid DA record"), @@ -1227,6 +1284,13 @@ test_threads = 0 } #[test] + fn read_lcov_reports_read_error() { + let missing = temp_file_path("lcov_missing"); + let err = read_lcov(&missing).expect_err("missing lcov should fail"); + assert!(err.contains("failed to read lcov")); + } + + #[test] fn read_lcov_uses_lf_lh_when_da_is_missing_and_branches_absent() { let path = temp_file_path("lcov_lf_lh"); fs::write(&path, "LF:4\nLH:3\n").expect("write lcov"); @@ -1293,6 +1357,10 @@ test_threads = 0 fail.arg("-c").arg("exit 9"); let err = run_command(fail, "shell fail").expect_err("run failing command"); assert!(err.contains("shell fail failed with status")); + + let missing = Command::new("/definitely/not/a/real/command"); + let err = run_command(missing, "shell missing").expect_err("missing command"); + assert!(err.contains("failed to run shell missing")); } #[test] @@ -1462,6 +1530,64 @@ test_threads = 0 } #[test] + fn report_gate_handles_nan_threshold_input() { + let root = temp_dir_path("report_gate_nan"); + 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 args = vec![ + "--scope".to_string(), + "crate-nan".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(), + "NaN".to_string(), + ]; + let err = report_gate(&args).expect_err("nan threshold should fail coverage gate"); + assert!(err.contains("coverage gate failed")); + fs::remove_dir_all(root).expect("remove report gate nan root"); + } + + #[test] + fn report_gate_reports_write_failure() { + let root = temp_dir_path("report_gate_write_fail"); + 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"); + fs::create_dir_all(&out_path).expect("create directory at output path"); + + let args = vec![ + "--scope".to_string(), + "crate-write".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(), + "--require-branches".to_string(), + ]; + let err = report_gate(&args).expect_err("writing report to directory should fail"); + assert!(err.contains("failed to write")); + fs::remove_dir_all(root).expect("remove report gate write root"); + } + + #[test] fn report_gate_logs_branch_unavailable_path() { let root = temp_dir_path("report_gate_no_branches"); let summary_path = root.join("summary.json"); @@ -1501,6 +1627,29 @@ test_threads = 0 } #[test] + fn write_crate_names_output_covers_success_and_error_paths() { + let mut output = Vec::new(); + write_crate_names_output( + &mut output, + vec!["radroots-a".to_string(), "radroots-b".to_string()], + "required crates", + ) + .expect("write crate names"); + let rendered = String::from_utf8(output).expect("utf8"); + assert!(rendered.contains("radroots-a")); + assert!(rendered.contains("radroots-b")); + + let mut failing = FailingWriter; + let err = write_crate_names_output( + &mut failing, + vec!["radroots-a".to_string()], + "workspace crates", + ) + .expect_err("writer failure"); + assert!(err.contains("failed to write workspace crates output")); + } + + #[test] fn run_report_subcommand_dispatches_to_report_gate() { let root = temp_dir_path("run_dispatch_report"); let summary_path = root.join("summary.json");