From 386bd1d41e47e455279777143b26bd7151ebe30f Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 01:18:56 -0500 Subject: [PATCH] fix(tools): h5rs diff names its options as h5diff does -c meant "list at most N differences" in h5rs, but in h5diff -c is --compare (a flag) and the count is -n/--count=N, so a script moved over from h5diff behaved differently: `h5diff -r -c 2 A B` exits 2 (the 2 is taken as a file name) while h5rs exited 1. The count is now -n/--count, -c/--compare is accepted (h5rs always lists objects that are not comparable), and the --count=N, --delta=D, --relative=R forms are accepted; exit codes equal h5diff's on 7 cases. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 +-- crates/clawhdf5-tools/README.md | 5 +++ crates/clawhdf5-tools/src/cli.rs | 17 +++++++--- crates/clawhdf5-tools/src/diff.rs | 16 +++++++-- crates/clawhdf5-tools/tests/h5rs_interop.rs | 36 +++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 885da0f..ff031e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,8 +128,9 @@ - `h5rs stat FILE` reports h5stat's object, link, rank, layout, filter, attribute, raw-data and file-size figures (equal to h5stat's on the test files); metadata space is one figure, not broken down. - - `h5rs diff [-r] [-q] [-d D] [-p R] [--follow-symlinks] A B [OBJ1 [OBJ2]]` - compares objects, kinds, datatypes, shapes, attributes, values and link + - `h5rs diff [-r] [-q] [-n N] [-d D] [-p R] [--follow-symlinks] A B [OBJ1 + [OBJ2]]` (option names as h5diff's: `-c` is `--compare`, the count is + `-n`/`--count=N`) compares objects, kinds, datatypes, shapes, attributes, values and link targets; exit status 0/1/2 as h5diff's. Soft links are compared by target path, as h5diff's default, or with `--follow-symlinks` by the objects they lead to (external links are never followed). Every path is diff --git a/crates/clawhdf5-tools/README.md b/crates/clawhdf5-tools/README.md index 633da68..4f6460c 100644 --- a/crates/clawhdf5-tools/README.md +++ b/crates/clawhdf5-tools/README.md @@ -141,8 +141,13 @@ $ h5rs diff -r a.h5 b.h5 /d # list every differing element $ h5rs diff -d 0.001 a.h5 b.h5 # |a - b| > 0.001 is a difference $ h5rs diff -p 0.01 a.h5 b.h5 # |a - b| / |a| > 1% is a difference $ h5rs diff --follow-symlinks a.h5 b.h5 /lnk # what the soft link /lnk leads to +$ h5rs diff -r -n 10 a.h5 b.h5 # list at most 10 differing elements per object ``` +The options are named as h5diff's: `-n N`/`--count=N` limits the listed +elements, `-c`/`--compare` (list objects that are not comparable) is +accepted and always in effect, and `--delta=D`, `--relative=R` work too. + Integers are compared in integer arithmetic, with or without a tolerance, so 64-bit values beyond 2^53 lose no precision (`-d 0` tells 2^60 from 2^60 + 1). A relative tolerance below the f64 epsilon (2.2e-16) compares diff --git a/crates/clawhdf5-tools/src/cli.rs b/crates/clawhdf5-tools/src/cli.rs index e4ba795..a4f6cde 100644 --- a/crates/clawhdf5-tools/src/cli.rs +++ b/crates/clawhdf5-tools/src/cli.rs @@ -11,30 +11,37 @@ pub struct Out<'a> { /// The arguments after the subcommand name. pub struct Args { cmd: &'static str, - rest: std::vec::IntoIter, + rest: std::collections::VecDeque, } impl Args { pub fn new(cmd: &'static str, rest: Vec) -> Self { Self { cmd, - rest: rest.into_iter(), + rest: rest.into(), } } #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option { - self.rest.next() + self.rest.pop_front() + } + + /// Put an option and its value back, to be read next (for the + /// `--name=value` form). + pub fn push_front(&mut self, name: String, value: String) { + self.rest.push_front(value); + self.rest.push_front(name); } /// The value after an option such as `--max-bytes`. pub fn value(&mut self) -> Option { - self.rest.next() + self.rest.pop_front() } /// A numeric option value. pub fn number(&mut self) -> Option { - self.rest.next().and_then(|s| s.parse().ok()) + self.rest.pop_front().and_then(|s| s.parse().ok()) } /// Report a usage problem; exit status 2. diff --git a/crates/clawhdf5-tools/src/diff.rs b/crates/clawhdf5-tools/src/diff.rs index 8a52029..c7ad723 100644 --- a/crates/clawhdf5-tools/src/diff.rs +++ b/crates/clawhdf5-tools/src/diff.rs @@ -29,7 +29,9 @@ soft link is compared as a link, as h5diff does, unless --follow-symlinks). -d, --delta D numbers differ only when |a - b| > D -p, --relative R numbers differ only when |a - b| / |a| > R (an R below the f64 epsilon, 2.2e-16, compares exactly, as h5diff) - -c, --count N list at most N differing elements per object with -r + -n, --count N list at most N differing elements per object with -r + -c, --compare list objects that are not comparable (always done; + accepted for h5diff compatibility) --max-bytes N largest dataset read (default 1 GiB); a larger one is an error Two NaNs compare equal. Unlike h5diff, objects that cannot be compared @@ -139,7 +141,9 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result { } _ => return args.usage_error(out, "--relative needs a number >= 0", USAGE), }, - "-c" | "--count" => match args.number() { + // h5diff's -c: h5rs always lists them. + "-c" | "--compare" => {} + "-n" | "--count" => match args.number() { Some(n) => opts.count = n, None => return args.usage_error(out, "--count needs a number", USAGE), }, @@ -151,6 +155,14 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result { writeln!(out.o, "{USAGE}")?; return Ok(0); } + // h5diff's --count=N, --delta=D, --relative=R forms. + s if s.starts_with("--") && s.contains('=') => { + let (k, v) = s.split_once('=').unwrap_or((s, "")); + if !matches!(k, "--count" | "--delta" | "--relative" | "--max-bytes") { + return args.usage_error(out, &format!("unknown option {a}"), USAGE); + } + args.push_front(k.to_string(), v.to_string()); + } s if s.starts_with('-') && s.len() > 1 => { return args.usage_error(out, &format!("unknown option {a}"), USAGE); } diff --git a/crates/clawhdf5-tools/tests/h5rs_interop.rs b/crates/clawhdf5-tools/tests/h5rs_interop.rs index 0e00e0a..b82eb65 100644 --- a/crates/clawhdf5-tools/tests/h5rs_interop.rs +++ b/crates/clawhdf5-tools/tests/h5rs_interop.rs @@ -380,6 +380,42 @@ fn diff_exit_codes_match_h5diff() { assert!(s.contains("2 difference(s) found"), "{s}"); } +/// The option names are h5diff's: -c is --compare (a flag), the count is +/// -n/--count, and the --name=value forms are accepted. +#[test] +fn diff_options_are_named_like_h5diff() { + let Some(f) = generate() else { return }; + let (b, changed) = (f.p("base.h5"), f.p("changed.h5")); + let cases: Vec<(Vec<&str>, i32)> = vec![ + // "2" is then the first file name, which does not exist. + (vec!["-r", "-c", "2", &b, &changed], 2), + (vec!["-c", &b, &changed], 1), + (vec!["--compare", &b, &b], 0), + (vec!["-r", "-n", "1", &b, &changed], 1), + (vec!["-r", "--count=1", &b, &changed], 1), + (vec!["--delta=0.01", &b, &changed], 0), + (vec!["--relative=1e-9", &b, &changed], 1), + ]; + let have_h5diff = tool_available("h5diff"); + for (args, want) in cases { + if have_h5diff { + assert_eq!(code(&run("h5diff", &args)), want, "h5diff {args:?}"); + } + let o = h5rs(&[&["diff"], args.as_slice()].concat()); + assert_eq!(code(&o), want, "h5rs diff {args:?}: {}", stdout(&o)); + } + for count in [vec!["-n", "1"], vec!["--count=1"]] { + let o = h5rs(&[&["diff", "-r"], count.as_slice(), &[&b, &changed, "/d"]].concat()); + let s = stdout(&o); + assert!( + s.contains("[ 0 2 ]") && !s.contains("[ 2 3 ]"), + "{count:?}: {s}" + ); + assert!(s.contains("2 difference(s) found"), "{s}"); + } + assert_eq!(code(&h5rs(&["diff", "--bogus=1", &b, &b])), 2); +} + /// Tolerances on 64-bit integers one apart, far beyond f64's integer /// precision: compared exactly, as h5diff does. #[test]