use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use rust_hdf5::H5File;
fn unique_tmp(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_hard_links_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir.join(format!("{label}.h5"))
}
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 hard_link_to_dataset_shares_data() {
let path = unique_tmp("hl_dataset");
let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
{
let file = H5File::create(&path).unwrap();
let root = file.root_group();
let inst = root.create_group("instrument").unwrap();
let ds = inst
.new_dataset::<f32>()
.shape([12])
.create("detector")
.unwrap();
ds.write_raw(&data).unwrap();
let data_grp = root.create_group("data").unwrap();
data_grp.link("detector", "/instrument/detector").unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let original = file
.dataset("instrument/detector")
.unwrap()
.read_raw::<f32>()
.unwrap();
let aliased = file
.dataset("data/detector")
.unwrap()
.read_raw::<f32>()
.unwrap();
assert_eq!(original, data, "target reads back the written data");
assert_eq!(aliased, data, "hard link resolves to the same data");
}
cleanup(&path);
}
#[test]
fn hard_link_in_root_group() {
let path = unique_tmp("hl_root");
let data: Vec<i32> = vec![7, 8, 9];
{
let file = H5File::create(&path).unwrap();
let root = file.root_group();
let inst = root.create_group("instrument").unwrap();
let ds = inst
.new_dataset::<i32>()
.shape([3])
.create("counts")
.unwrap();
ds.write_raw(&data).unwrap();
root.link("counts_alias", "instrument/counts").unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let aliased = file
.dataset("counts_alias")
.unwrap()
.read_raw::<i32>()
.unwrap();
assert_eq!(aliased, data);
}
cleanup(&path);
}
#[test]
fn hard_link_rejects_unknown_target() {
let path = unique_tmp("hl_unknown");
let file = H5File::create(&path).unwrap();
let err = file
.root_group()
.link("alias", "/does/not/exist")
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("not found"), "unexpected error: {msg}");
drop(file);
cleanup(&path);
}
#[test]
fn hard_link_rejects_duplicate_name() {
let path = unique_tmp("hl_dup");
let file = H5File::create(&path).unwrap();
let root = file.root_group();
let inst = root.create_group("instrument").unwrap();
inst.new_dataset::<f32>()
.shape([4])
.create("detector")
.unwrap();
let err = inst.link("detector", "/instrument/detector").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("already exists"), "unexpected error: {msg}");
drop(file);
cleanup(&path);
}
#[test]
fn dataset_rejects_name_taken_by_hard_link() {
let path = unique_tmp("hl_reverse");
let file = H5File::create(&path).unwrap();
let root = file.root_group();
let inst = root.create_group("instrument").unwrap();
inst.new_dataset::<f32>()
.shape([4])
.create("detector")
.unwrap();
let data = root.create_group("data").unwrap();
data.link("detector", "/instrument/detector").unwrap();
let ds_result = data.new_dataset::<f32>().shape([4]).create("detector");
let err = ds_result
.err()
.expect("dataset creation should be rejected");
let msg = format!("{err}");
assert!(msg.contains("already exists"), "unexpected error: {msg}");
let grp_result = data.create_group("detector");
let err = grp_result.err().expect("group creation should be rejected");
let msg = format!("{err}");
assert!(msg.contains("already exists"), "unexpected error: {msg}");
drop(file);
cleanup(&path);
}
#[test]
fn swmr_writer_creates_hard_link() {
use rust_hdf5::swmr::{SwmrFileReader, SwmrFileWriter};
let path = unique_tmp("hl_swmr");
{
let mut w = SwmrFileWriter::create(&path).unwrap();
let ds = w.create_streaming_dataset::<u8>("frames", &[2, 2]).unwrap();
w.create_hard_link("/", "alias", "frames").unwrap();
w.start_swmr().unwrap();
w.append_frame(ds, &[1u8, 2, 3, 4]).unwrap();
w.close().unwrap();
}
let mut r = SwmrFileReader::open(&path).unwrap();
let names = r.dataset_names();
assert!(
names.iter().any(|n| n == "alias"),
"hard link 'alias' missing: {names:?}"
);
assert_eq!(r.read_dataset_raw("frames").unwrap(), vec![1u8, 2, 3, 4]);
assert_eq!(r.read_dataset_raw("alias").unwrap(), vec![1u8, 2, 3, 4]);
cleanup(&path);
}
#[test]
fn swmr_writer_builds_nexus_layout() {
use rust_hdf5::swmr::SwmrFileWriter;
let path = unique_tmp("swmr_nexus");
{
let mut w = SwmrFileWriter::create(&path).unwrap();
let ds = w
.create_streaming_dataset::<u16>("frames", &[2, 2])
.unwrap();
w.create_group("/", "entry").unwrap();
w.create_group("/entry", "data").unwrap();
w.set_group_attr_string("/entry", "NX_class", "NXentry")
.unwrap();
w.set_group_attr_string("/entry/data", "NX_class", "NXdata")
.unwrap();
w.create_hard_link("/entry/data", "data", "frames").unwrap();
w.start_swmr().unwrap();
w.append_frame(ds, &[1u8, 0, 2, 0, 3, 0, 4, 0]).unwrap();
w.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let root = file.root_group();
let entry = root.group("entry").unwrap();
assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
let data = entry.group("data").unwrap();
assert_eq!(data.attr_string("NX_class").unwrap(), "NXdata");
let aliased = file
.dataset("entry/data/data")
.unwrap()
.read_raw::<u16>()
.unwrap();
assert_eq!(aliased, vec![1u16, 2, 3, 4]);
cleanup(&path);
}
#[test]
fn hard_link_tolerates_trailing_slash() {
let path = unique_tmp("hl_trailing");
let data: Vec<i32> = vec![3, 1, 4, 1, 5];
{
let file = H5File::create(&path).unwrap();
let root = file.root_group();
let inst = root.create_group("instrument").unwrap();
let ds = inst
.new_dataset::<i32>()
.shape([5])
.create("counts")
.unwrap();
ds.write_raw(&data).unwrap();
root.link("alias", "/instrument/counts/").unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let aliased = file.dataset("alias").unwrap().read_raw::<i32>().unwrap();
assert_eq!(aliased, data);
}
cleanup(&path);
}