use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use rust_hdf5::{
H5File, Hyperslab, LibverBound, PointSelection, Reference, RegularHyperslab, Selection,
};
const REVISED_REFS: &[u8] = include_bytes!("fixtures/revised_refs.h5");
fn write_temp(label: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rust_hdf5_revised_refs_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(format!("{label}.h5"));
std::fs::write(&path, REVISED_REFS).unwrap();
path
}
fn write_path(label: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rust_hdf5_revised_write_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir.join(format!("{label}.h5"))
}
const TEST_PYTHONS: [&str; 2] = [
"/Users/stevek/mamba/envs/bs2026.1/bin/python",
"/home/stevek/micromamba/envs/tomo/bin/python",
];
fn h5dump() -> Option<PathBuf> {
let candidates: Vec<String> = match std::env::var("RUST_HDF5_TEST_PYTHON") {
Ok(p) => vec![p],
Err(_) => TEST_PYTHONS.iter().map(|p| p.to_string()).collect(),
};
let found = candidates
.iter()
.map(|c| PathBuf::from(c).parent().unwrap().join("h5dump"))
.find(|t| t.exists());
if found.is_none() {
eprintln!("skipping the h5dump cross-check: none of {candidates:?} ships one");
}
found
}
fn cleanup(path: &PathBuf) {
let _ = std::fs::remove_file(path);
if let Some(dir) = path.parent() {
let _ = std::fs::remove_dir_all(dir);
}
}
#[test]
fn object2_references_resolve_to_paths() {
let path = write_temp("object2");
let file = H5File::open(&path).unwrap();
let refs = file.dataset("objrefs").unwrap().read_references().unwrap();
let paths: Vec<Option<&str>> = refs.iter().map(Reference::path).collect();
assert_eq!(paths, vec![Some("/matrix"), Some("/grp")]);
assert!(refs.iter().all(|r| matches!(r, Reference::Object { .. })));
drop(file);
cleanup(&path);
}
#[test]
fn region2_references_report_their_selections() {
let path = write_temp("region2");
let file = H5File::open(&path).unwrap();
let refs = file.dataset("regrefs").unwrap().read_references().unwrap();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].path(), Some("/matrix"));
assert_eq!(refs[0].bounds(), Some((vec![1, 2], vec![2, 4])));
assert!(matches!(
refs[0].selection(),
Some(Selection::Hyperslab { .. })
));
assert_eq!(refs[1].path(), Some("/matrix"));
assert_eq!(refs[1].bounds(), Some((vec![0, 1], vec![3, 5])));
assert_eq!(
refs[1].selection(),
Some(&Selection::Points(PointSelection {
rank: 2,
points: vec![vec![0, 1], vec![3, 5]],
}))
);
drop(file);
cleanup(&path);
}
#[test]
fn object2_references_written_here_resolve_in_libhdf5() {
let path = write_path("object2");
let file = H5File::options()
.libver(LibverBound::V112)
.create(&path)
.unwrap();
let matrix = file
.new_dataset::<i32>()
.shape([4])
.create("matrix")
.unwrap();
matrix.write_raw(&[10i32, 20, 30, 40]).unwrap();
file.create_group("grp").unwrap();
let refs = file
.new_dataset::<u64>()
.std_object_references()
.shape([2])
.create("objrefs")
.unwrap();
refs.write_object_references(&["/matrix", "/grp"]).unwrap();
file.close().unwrap();
let file = H5File::open(&path).unwrap();
let read = file.dataset("objrefs").unwrap().read_references().unwrap();
let paths: Vec<Option<&str>> = read.iter().map(Reference::path).collect();
assert_eq!(paths, vec![Some("/matrix"), Some("/grp")]);
assert!(read.iter().all(|r| matches!(r, Reference::Object { .. })));
drop(file);
if let Some(h5dump) = h5dump() {
let out = std::process::Command::new(&h5dump)
.args(["-d", "/objrefs", path.to_str().unwrap()])
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "h5dump failed:\n{text}");
assert!(text.contains("H5T_REFERENCE { H5T_STD_REF }"), "{text}");
assert!(text.contains("DATASET \""), "{text}");
assert!(text.contains("10, 20, 30, 40"), "{text}");
assert!(text.contains("GROUP \""), "{text}");
}
cleanup(&path);
}
#[test]
fn attribute_references_name_the_attribute() {
let path = write_temp("attr");
let file = H5File::open(&path).unwrap();
let refs = file.dataset("attrrefs").unwrap().read_references().unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].path(), Some("/matrix"));
assert_eq!(refs[0].attribute_name(), Some("note"));
let target = file
.dataset(refs[0].path().unwrap().trim_start_matches('/'))
.unwrap();
let note = target.attr(refs[0].attribute_name().unwrap()).unwrap();
assert_eq!(note.read_numeric_as::<i32>().unwrap(), vec![7, 8, 9]);
drop(file);
cleanup(&path);
}
#[test]
fn region2_references_written_here_resolve_in_libhdf5() {
let path = write_path("region2");
let file = H5File::options()
.libver(LibverBound::V112)
.create(&path)
.unwrap();
let matrix = file
.new_dataset::<i32>()
.shape([4, 6])
.create("matrix")
.unwrap();
matrix.write_raw(&(0..24).collect::<Vec<i32>>()).unwrap();
let refs = file
.new_dataset::<u64>()
.std_region_references()
.shape([3])
.create("regrefs")
.unwrap();
let rows = Selection::Hyperslab {
rank: 2,
form: Hyperslab::Regular(RegularHyperslab {
start: vec![1, 2],
stride: vec![1, 1],
count: vec![2, 3],
block: vec![1, 1],
}),
};
let points = Selection::Points(PointSelection {
rank: 2,
points: vec![vec![0, 1], vec![3, 5]],
});
let whole = Selection::All;
refs.write_std_region_references(&[("/matrix", rows), ("/matrix", points), ("/matrix", whole)])
.unwrap();
file.close().unwrap();
let file = H5File::open(&path).unwrap();
let read = file.dataset("regrefs").unwrap().read_references().unwrap();
assert_eq!(read.len(), 3);
assert_eq!(read[0].path(), Some("/matrix"));
assert_eq!(read[0].bounds(), Some((vec![1, 2], vec![2, 4])));
assert_eq!(read[1].path(), Some("/matrix"));
assert_eq!(read[1].bounds(), Some((vec![0, 1], vec![3, 5])));
assert_eq!(read[2].path(), Some("/matrix"));
assert_eq!(read[2].selection(), Some(&Selection::All));
drop(file);
if let Some(h5dump) = h5dump() {
let out = std::process::Command::new(&h5dump)
.args(["-d", "/regrefs", path.to_str().unwrap()])
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "h5dump failed:\n{text}");
assert!(text.contains("H5T_REFERENCE { H5T_STD_REF }"), "{text}");
assert!(text.contains("REGION_TYPE BLOCK"), "{text}");
assert!(text.contains("(1,2)-(2,4)"), "{text}");
assert!(text.contains("REGION_TYPE POINT"), "{text}");
assert!(text.contains("(0,1)"), "{text}");
assert!(text.contains("(3,5)"), "{text}");
assert_eq!(text.matches("/matrix\"").count(), 3, "{text}");
assert_eq!(text.matches("REGION_TYPE").count(), 2, "{text}");
}
cleanup(&path);
}
#[test]
fn attribute_references_written_here_resolve_in_libhdf5() {
let path = write_path("attr");
let file = H5File::options()
.libver(LibverBound::V112)
.create(&path)
.unwrap();
let matrix = file
.new_dataset::<i32>()
.shape([4])
.create("matrix")
.unwrap();
matrix.write_raw(&[10i32, 20, 30, 40]).unwrap();
matrix
.new_attr::<i32>()
.shape([3])
.create("note")
.unwrap()
.write_array(&[7i32, 8, 9])
.unwrap();
let grp = file.create_group("grp").unwrap();
grp.set_attr_array_numeric("tag", &[1i32, 2]).unwrap();
let refs = file
.new_dataset::<u64>()
.attribute_references()
.shape([2])
.create("attrrefs")
.unwrap();
refs.write_attribute_references(&[("/matrix", "note"), ("/grp", "tag")])
.unwrap();
file.close().unwrap();
let file = H5File::open(&path).unwrap();
let read = file.dataset("attrrefs").unwrap().read_references().unwrap();
assert_eq!(read.len(), 2);
assert_eq!(read[0].path(), Some("/matrix"));
assert_eq!(read[0].attribute_name(), Some("note"));
assert_eq!(read[1].path(), Some("/grp"));
assert_eq!(read[1].attribute_name(), Some("tag"));
let note = file.dataset("matrix").unwrap().attr("note").unwrap();
assert_eq!(note.read_numeric_as::<i32>().unwrap(), vec![7, 8, 9]);
drop(file);
if let Some(h5dump) = h5dump() {
let out = std::process::Command::new(&h5dump)
.args(["-d", "/attrrefs", path.to_str().unwrap()])
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "h5dump failed:\n{text}");
assert!(text.contains("H5T_REFERENCE { H5T_STD_REF }"), "{text}");
assert!(text.contains("/matrix/note\""), "{text}");
assert!(text.contains("/grp/tag\""), "{text}");
assert_eq!(text.matches("ATTRIBUTE \"").count(), 2, "{text}");
assert!(text.contains("SIMPLE { ( 3 ) / ( 3 ) }"), "{text}");
assert!(text.contains("SIMPLE { ( 2 ) / ( 2 ) }"), "{text}");
}
cleanup(&path);
}
#[test]
fn revised_reference_writes_refuse_what_h5r_refuses() {
let path = write_path("refused");
let file = H5File::options()
.libver(LibverBound::V112)
.create(&path)
.unwrap();
file.new_dataset::<i32>().shape([4]).create("m").unwrap();
file.create_group("g").unwrap();
let whole = Selection::Hyperslab {
rank: 1,
form: Hyperslab::Regular(RegularHyperslab {
start: vec![0],
stride: vec![1],
count: vec![4],
block: vec![1],
}),
};
let legacy = file
.new_dataset::<u64>()
.region_references()
.shape([1])
.create("legacy")
.unwrap();
assert!(legacy
.write_std_region_references(&[("/m", whole.clone())])
.is_err());
let refs = file
.new_dataset::<u64>()
.std_region_references()
.shape([2])
.create("refs")
.unwrap();
assert!(refs
.write_std_region_references(&[("/g", whole.clone())])
.is_err());
let past_end = Selection::Hyperslab {
rank: 1,
form: Hyperslab::Regular(RegularHyperslab {
start: vec![2],
stride: vec![1],
count: vec![4],
block: vec![1],
}),
};
assert!(refs
.write_std_region_references(&[("/m", past_end)])
.is_err());
assert!(refs
.write_attribute_references(&[("/m", "missing")])
.is_err());
assert!(refs
.write_attribute_references(&[("/nope", "note")])
.is_err());
refs.write_std_region_references(&[("/m", whole)]).unwrap();
file.close().unwrap();
let file = H5File::open(&path).unwrap();
let read = file.dataset("refs").unwrap().read_references().unwrap();
assert_eq!(read[0].bounds(), Some((vec![0], vec![3])));
assert_eq!(
read[1],
Reference::Null,
"the second element was never written"
);
drop(file);
cleanup(&path);
}
const EXT_TARGET: &str = "tests/fixtures/ext_ref_target.h5";
const EXT_HOLDER: &str = "tests/fixtures/ext_refs.h5";
#[test]
fn external_references_name_the_file_they_cross_into() {
let file = H5File::open(EXT_HOLDER).unwrap();
let objs = file
.dataset("extobjrefs")
.unwrap()
.read_references()
.unwrap();
assert_eq!(
objs.iter().map(Reference::file).collect::<Vec<_>>(),
vec![Some(EXT_TARGET), Some(EXT_TARGET)]
);
assert_eq!(
objs.iter().map(Reference::path).collect::<Vec<_>>(),
vec![Some("/matrix"), Some("/grp")]
);
let regs = file
.dataset("extregrefs")
.unwrap()
.read_references()
.unwrap();
assert_eq!(regs[0].file(), Some(EXT_TARGET));
assert_eq!(regs[0].path(), Some("/matrix"));
assert_eq!(regs[0].bounds(), Some((vec![1, 2], vec![2, 4])));
let attrs = file
.dataset("extattrrefs")
.unwrap()
.read_references()
.unwrap();
assert_eq!(attrs[0].file(), Some(EXT_TARGET));
assert_eq!(attrs[0].path(), Some("/matrix"));
assert_eq!(attrs[0].attribute_name(), Some("note"));
let target = H5File::open(attrs[0].file().unwrap()).unwrap();
let matrix = target
.dataset(attrs[0].path().unwrap().trim_start_matches('/'))
.unwrap();
let note = matrix.attr(attrs[0].attribute_name().unwrap()).unwrap();
assert_eq!(note.read_numeric_as::<i32>().unwrap(), vec![7, 8, 9]);
}
#[test]
fn an_external_reference_to_an_absent_file_still_names_it() {
let (was, now) = (b"ext_ref_target.h5", b"ext_ref_absent.h5");
let mut bytes = std::fs::read(EXT_HOLDER).unwrap();
let mut hits = 0;
for at in 0..bytes.len() - was.len() {
if &bytes[at..at + was.len()] == was {
bytes[at..at + now.len()].copy_from_slice(now);
hits += 1;
}
}
assert_eq!(hits, 4, "one name per reference in the fixture");
let path = write_path("ext_absent");
std::fs::write(&path, &bytes).unwrap();
let file = H5File::open(&path).unwrap();
let refs = file
.dataset("extobjrefs")
.unwrap()
.read_references()
.unwrap();
assert_eq!(refs[0].file(), Some("tests/fixtures/ext_ref_absent.h5"));
assert_eq!(refs[0].path(), None, "nothing is under that name");
assert_eq!(refs[0].address(), Some(0x320));
drop(file);
cleanup(&path);
}