//! `h5rs dump` and `h5rs ls` of HDF5 2.0 native complex numbers (datatype //! class 11), against the output of h5dump 2.2.0 and h5ls 2.2.0 (the Debian //! h5dump CI installs, 1.14.x, predates the type). The h5dump output is //! stored next to the fixture; see //! `crates/clawhdf5/tests/fixtures/gen_native_complex.py`. use std::path::PathBuf; use std::process::Command; fn fixture(name: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../clawhdf5/tests/fixtures") .join(name) } fn h5rs(args: &[&str]) -> String { let out = Command::new(env!("CARGO_BIN_EXE_h5rs")) .args(args) .output() .unwrap(); assert!(out.status.success(), "h5rs {args:?}: {out:?}"); String::from_utf8(out.stdout).unwrap() } /// Split a dump into its lines, with the data lines (`(i): ...`) replaced /// by their index, and the data values in order. fn split(ddl: &str) -> (Vec, Vec) { let (mut frame, mut data) = (Vec::new(), Vec::new()); for line in ddl.lines() { match line.split_once("):") { Some((idx, body)) if idx.trim_start().starts_with('(') => { frame.push(format!("{idx}):")); // Values are split at `, `; array elements come bracketed. data.extend( body.split(", ") .map(|s| s.trim().trim_matches(|c| c == '[' || c == ']' || c == ',')) .map(str::trim) .filter(|s| !s.is_empty() && *s != "{") .map(String::from), ); } _ => { let t = line.trim().trim_end_matches(','); if t.ends_with('i') || t.parse::().is_ok() { // A compound member's value on its own line. data.push(t.to_string()); } else { frame.push(line.to_string()); } } } } (frame, data) } /// `a+bi`, `a-bi` or `a+-bi` as its two parts (a real number as one). fn parts(v: &str) -> Vec { let Some(z) = v.strip_suffix('i') else { return vec![v.parse().unwrap()]; }; let b = z.as_bytes(); let at = (1..b.len()) .find(|&k| (b[k] == b'+' || b[k] == b'-') && !matches!(b[k - 1], b'e' | b'E' | b'+')) .unwrap_or_else(|| panic!("not a complex value: {v}")); let (re, im) = z.split_at(at); let im = im.strip_prefix('+').unwrap_or(im); vec![re.parse().unwrap(), im.parse().unwrap()] } #[test] fn dump_matches_h5dump_2_2() { let path = fixture("native_complex_hdf5_2.h5"); let ours = h5rs(&["dump", path.to_str().unwrap()]); let reference = std::fs::read_to_string(fixture("native_complex_hdf5_2.ddl")).unwrap(); let (mut our_frame, our_data) = split(&ours); let (mut ref_frame, ref_data) = split(&reference); // The first line names the file as it was given. our_frame.remove(0); ref_frame.remove(0); // Everything but the values is h5dump's byte for byte: the types print // as H5T_COMPLEX_IEEE_F64LE, H5T_ARRAY { [2] H5T_COMPLEX_IEEE_F32LE }, ... assert_eq!(our_frame, ref_frame); assert_eq!(our_data.len(), ref_data.len(), "{our_data:?}\n{ref_data:?}"); assert_eq!(our_data.len(), 36); // Values: h5dump prints `%g` (6 digits), inf/nan, and the binary16 // parts it has no C type for as `+i` (`1+-2i`); h5rs prints // the shortest round-trip string and Inf/NaN, joined the same way. for (i, (o, r)) in our_data.iter().zip(&ref_data).enumerate() { assert_eq!(o.contains("+-"), r.contains("+-"), "[{i}] {o} vs {r}"); let (op, rp) = (parts(o), parts(r)); assert_eq!(op.len(), rp.len(), "[{i}] {o} vs {r}"); for (x, y) in op.iter().zip(&rp) { let same = if y.is_nan() || y.is_infinite() { x.is_nan() == y.is_nan() && (y.is_nan() || x == y) } else { *x as f32 == *y as f32 || (x - y).abs() <= 5e-6 * y.abs() }; assert!(same, "[{i}] h5rs {o}, h5dump {r}"); assert_eq!( x.is_sign_negative(), y.is_sign_negative(), "[{i}] {o} vs {r}" ); } } } #[test] fn ls_describes_complex_like_h5ls_2_2() { let path = fixture("native_complex_hdf5_2.h5"); // h5ls 2.2.0 -v, `Type:` of the types it has no native C name for (it // prints `native double _Complex` for the others, as it prints `native // double` where h5rs prints `IEEE 64-bit little-endian float`). for (name, long) in [ ( "c128be", "complex number of\n IEEE 64-bit big-endian float", ), ( "c128", "complex number of\n IEEE 64-bit little-endian float", ), ( "c32", "complex number of\n IEEE 16-bit little-endian float", ), ( "array", "[2] complex number of\n IEEE 32-bit little-endian float", ), ] { let target = format!("{}/{name}", path.display()); let verbose = h5rs(&["ls", "-v", &target]); assert!( verbose.contains(&format!(" Type: {long}\n")), "{name}:\n{verbose}" ); } let listing = h5rs(&["ls", path.to_str().unwrap()]); for (name, short) in [ ("c64", "complex64"), ("c128", "complex128"), ("c128be", "complex128-be"), ("c32", "complex32"), ("scalar", "complex128"), ("array", "array[2]"), ("compound", "compound{z: complex128, k: int64}"), ] { assert!( listing .lines() .any(|l| l.starts_with(&format!("{name} ")) && l.ends_with(&format!(" {short}"))), "{name}:\n{listing}" ); } } #[test] fn json_dump_uses_the_r_i_compound() { // hdf5-json (h5json 2.0.0) has no complex class: the type is written as // the `{r, i}` compound h5py uses, the values as `[re, im]` pairs. let path = fixture("native_complex_hdf5_2.h5"); let j: serde_json::Value = serde_json::from_str(&h5rs(&["dump", "--json", path.to_str().unwrap()])).unwrap(); let ds = j["datasets"] .as_object() .unwrap() .values() .find(|d| d["alias"][0] == "/scalar") .unwrap(); assert_eq!(ds["type"]["class"], "H5T_COMPOUND"); assert_eq!(ds["type"]["fields"][0]["name"], "r"); assert_eq!(ds["type"]["fields"][1]["type"]["base"], "H5T_IEEE_F64LE"); assert_eq!(ds["value"], serde_json::json!([2.5, -0.5])); } #[test] fn diff_compares_complex_values() { let path = fixture("native_complex_hdf5_2.h5"); let p = path.to_str().unwrap(); // Same values, different byte order: no differences. let out = Command::new(env!("CARGO_BIN_EXE_h5rs")) .args(["diff", p, p, "/c128", "/c128be"]) .output() .unwrap(); assert!(out.status.success(), "{out:?}"); // One element differs in its imaginary part: one difference, exit // status 1, as h5diff 2.2.0 reports it. let out = Command::new(env!("CARGO_BIN_EXE_h5rs")) .args(["diff", "-r", p, p, "/c128", "/c128x"]) .output() .unwrap(); assert_eq!(out.status.code(), Some(1), "{out:?}"); let text = String::from_utf8_lossy(&out.stdout); assert!(text.contains("1 difference(s) found"), "{text}"); // h5diff 2.2.0: `[ 1 1 ] 0+1i 1+1i 1+0i`. let row = text.lines().find(|l| l.starts_with("[ 1 1 ]")).unwrap(); assert_eq!( row.split_whitespace().collect::>(), ["[", "1", "1", "]", "0+1i", "1+1i", "1+0i"] ); }