The test compared h5py against its own expected table, so it passed with the fix reverted. Found by the adversarial review. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
126 lines
4.1 KiB
Rust
126 lines
4.1 KiB
Rust
//! Version-1 shared messages (HDF5 1.6 era). A dataset that uses a committed
|
|
//! datatype stores a *shared* datatype message pointing at the type's object
|
|
//! header. In version 1 that pointer is a 1.6 "symbol table entry": after six
|
|
//! reserved bytes comes a length-sized heap offset, *then* the address.
|
|
//!
|
|
//! Fixture: `tcompound.h5` from libhdf5's own tool tests
|
|
//! (`tools/test/testfiles/tcompound.h5`, HDF5 source tree, BSD-style
|
|
//! licence), 8 KiB. Its datasets use committed compound types through v1
|
|
//! shared messages. The expected types are what h5dump 1.14.6 and h5py 3.16
|
|
//! (HDF5 2.0) report; the h5py cross-check runs when python3 with h5py is
|
|
//! available (required with `CLAWHDF5_REQUIRE_INTEROP=1`).
|
|
|
|
use std::process::Command;
|
|
|
|
use clawhdf5::{DType, File};
|
|
|
|
const FIXTURE: &[u8] = include_bytes!("../../clawhdf5-format/tests/fixtures/tcompound.h5");
|
|
|
|
fn python() -> String {
|
|
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
|
|
}
|
|
|
|
fn interop_required() -> bool {
|
|
std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1")
|
|
}
|
|
|
|
fn python_available() -> bool {
|
|
Command::new(python())
|
|
.args(["-c", "import h5py, numpy"])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn compound(fields: &[(&str, DType)]) -> DType {
|
|
DType::Compound(
|
|
fields
|
|
.iter()
|
|
.map(|(n, t)| (n.to_string(), t.clone()))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn expected() -> Vec<(&'static str, DType)> {
|
|
let int_float = |i: &str, f: &str| compound(&[(i, DType::I32), (f, DType::F32)]);
|
|
vec![
|
|
("group1/dset2", int_float("int_name", "float_name")),
|
|
(
|
|
"group1/dset3",
|
|
compound(&[
|
|
("int_array", DType::Array(Box::new(DType::I32), vec![4])),
|
|
(
|
|
"float_array",
|
|
DType::Array(Box::new(DType::F32), vec![5, 6]),
|
|
),
|
|
]),
|
|
),
|
|
("group1/dset4", int_float("int", "float")),
|
|
("group2/dset5", int_float("int", "float")),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn v1_shared_datatype_resolves_to_the_committed_type() {
|
|
// Reading the heap offset as the address used to land on the superblock
|
|
// and fail with InvalidObjectHeaderVersion.
|
|
let file = File::from_bytes(FIXTURE.to_vec()).unwrap();
|
|
for (path, dtype) in expected() {
|
|
assert_eq!(
|
|
file.dataset(path).unwrap().dtype().unwrap(),
|
|
dtype,
|
|
"{path}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn v1_shared_datatype_field_names_match_h5py() {
|
|
if !python_available() {
|
|
assert!(
|
|
!interop_required(),
|
|
"CLAWHDF5_REQUIRE_INTEROP=1 but python3 with h5py is not available"
|
|
);
|
|
eprintln!("SKIP: python3 with h5py not available");
|
|
return;
|
|
}
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("tcompound.h5");
|
|
std::fs::write(&path, FIXTURE).unwrap();
|
|
let script = format!(
|
|
r#"
|
|
import h5py
|
|
with h5py.File("{path}", "r") as f:
|
|
for p in ("group1/dset2", "group1/dset3", "group1/dset4", "group2/dset5"):
|
|
print(p, *f[p].dtype.names)
|
|
"#,
|
|
path = path.display()
|
|
);
|
|
let out = Command::new(python())
|
|
.args(["-c", &script])
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
let theirs: Vec<&str> = stdout.lines().collect();
|
|
// Read the types through clawhdf5, not from `expected()`, so this checks
|
|
// our reader against libhdf5 rather than the table against h5py.
|
|
let file = File::from_bytes(FIXTURE.to_vec()).unwrap();
|
|
let ours: Vec<String> = expected()
|
|
.into_iter()
|
|
.map(|(p, _)| (p, file.dataset(p).unwrap().dtype().unwrap()))
|
|
.map(|(p, t)| match t {
|
|
DType::Compound(fields) => {
|
|
let names: Vec<String> = fields.into_iter().map(|(n, _)| n).collect();
|
|
format!("{p} {}", names.join(" "))
|
|
}
|
|
other => panic!("{other:?}"),
|
|
})
|
|
.collect();
|
|
assert_eq!(theirs, ours);
|
|
}
|