diff --git a/src/cli.rs b/src/cli.rs index 0a577c5..2c2332f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,7 @@ use std::path::Path; use clap::{Parser, Subcommand}; use crate::srd::fetch::{self, FetchOutcome, SrdSources}; +use crate::srd::verify; #[derive(Parser)] #[command( @@ -27,10 +28,13 @@ enum Command { enum SrdCommand { /// Download and verify the SRD PDF. Fetch, + /// Check the SRD corpus against its vendored source and schema. + Verify, } -/// Runs the parsed command, using `srd_sources` for `storied srd fetch`. -pub fn run(cli: Cli, srd_sources: &SrdSources) -> Result<(), String> { +/// Runs the parsed command, using `srd_sources` for `storied srd fetch` and +/// `layer_root` for `storied srd verify`. +pub fn run(cli: Cli, srd_sources: &SrdSources, layer_root: &Path) -> Result<(), String> { match cli.command { None => { println!("{}", crate::greeting()); @@ -39,6 +43,9 @@ pub fn run(cli: Cli, srd_sources: &SrdSources) -> Result<(), String> { Some(Command::Srd { command: SrdCommand::Fetch, }) => run_srd_fetch(srd_sources), + Some(Command::Srd { + command: SrdCommand::Verify, + }) => run_srd_verify(layer_root), } } @@ -50,6 +57,26 @@ fn run_srd_fetch(sources: &SrdSources) -> Result<(), String> { Ok(()) } +/// Runs every check against the corpus layer at `layer_root`, printing one +/// failure per line and a summary line on success. +fn run_srd_verify(layer_root: &Path) -> Result<(), String> { + render_verify_report(&verify::verify(layer_root)) +} + +/// Prints `report`'s failures one per line, then either a summary line (no +/// failures) or a terse error naming how many there were. +fn render_verify_report(report: &verify::Report) -> Result<(), String> { + for failure in &report.failures { + println!("{failure}"); + } + if report.failures.is_empty() { + println!("{}", report.summary()); + Ok(()) + } else { + Err(format!("{} failure(s)", report.failures.len())) + } +} + fn report_fetch(destination: &Path, outcome: FetchOutcome) { match outcome { FetchOutcome::AlreadyPresent => { @@ -106,6 +133,14 @@ mod tests { } } + fn verify_cli() -> Cli { + Cli { + command: Some(Command::Srd { + command: SrdCommand::Verify, + }), + } + } + fn sha256_hex(bytes: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(bytes); @@ -208,7 +243,7 @@ mod tests { meta: fixture_meta(), }; - let result = run(Cli { command: None }, &sources); + let result = run(Cli { command: None }, &sources, &unique_temp_dir()); assert_eq!(result, Ok(())); } @@ -243,7 +278,7 @@ mod tests { meta: fixture_meta(), }; - let result = run(fetch_cli(), &sources); + let result = run(fetch_cli(), &sources, &unique_temp_dir()); assert_eq!(result, Ok(())); } @@ -272,7 +307,7 @@ mod tests { meta: fixture_meta(), }; - let result = run(fetch_cli(), &sources); + let result = run(fetch_cli(), &sources, &unique_temp_dir()); assert_eq!(result, Ok(())); pdf_server.join().unwrap(); @@ -294,11 +329,55 @@ mod tests { meta: fixture_meta(), }; - let result = run(fetch_cli(), &sources); + let result = run(fetch_cli(), &sources, &unique_temp_dir()); assert!(result.is_err()); } + #[test] + fn run_srd_verify_reports_failures_as_an_error() { + let sources = SrdSources { + pdf: FetchConfig { + url: "http://127.0.0.1:0".to_string(), + destination: PathBuf::from("unused"), + expected_sha256: "0".repeat(64), + }, + text: never_dialed_text_config(), + meta: fixture_meta(), + }; + + let result = run(verify_cli(), &sources, &unique_temp_dir()); + + assert!(result.is_err()); + } + + #[test] + fn render_verify_report_succeeds_with_a_summary_when_there_are_no_failures() { + let report = verify::Report { + failures: vec![], + counts: std::collections::HashMap::new(), + }; + + let result = render_verify_report(&report); + + assert_eq!(result, Ok(())); + } + + #[test] + fn render_verify_report_reports_the_failure_count_as_an_error() { + let report = verify::Report { + failures: vec![ + verify::Failure::new("spells/fireball.md", "missing name"), + verify::Failure::new("monsters/goblin.md", "missing ac"), + ], + counts: std::collections::HashMap::new(), + }; + + let result = render_verify_report(&report); + + assert_eq!(result, Err("2 failure(s)".to_string())); + } + #[test] fn exit_code_is_zero_on_success() { assert_eq!(exit_code(Ok(())), 0); diff --git a/src/main.rs b/src/main.rs index dac46c5..0f97a29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +use std::path::Path; + use clap::Parser; fn main() -> std::process::ExitCode { let arguments = storied::cli::Cli::parse(); let srd_sources = storied::srd::fetch::SrdSources::srd_5_2_1(); - let result = storied::cli::run(arguments, &srd_sources); + let layer_root = Path::new(storied::srd::SRD_LAYER_ROOT); + let result = storied::cli::run(arguments, &srd_sources, layer_root); std::process::ExitCode::from(storied::cli::exit_code(result)) } diff --git a/src/srd/mod.rs b/src/srd/mod.rs index f366cae..df05f8b 100644 --- a/src/srd/mod.rs +++ b/src/srd/mod.rs @@ -1,2 +1,5 @@ pub mod fetch; pub mod verify; + +/// Where the SRD 5.2 corpus layer lives, relative to the repository root. +pub const SRD_LAYER_ROOT: &str = "rules/srd-5.2";