use rust_hdf5::{H5Dataset, H5File};
fn tmp(name: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"rust_hdf5_chunk_slice_{}_{}_{}.h5",
name,
std::process::id(),
n
))
}
fn round_trip(
name: &str,
build: impl Fn(&H5File) -> H5Dataset,
patch: impl Fn(&H5Dataset),
) -> Vec<i32> {
let path = tmp(name);
{
let file = H5File::create(&path).unwrap();
let ds = build(&file);
let seed: Vec<i32> = (0..24).collect();
ds.write_slice(&[0, 0], &[4, 6], &seed).unwrap();
patch(&ds);
file.close().unwrap();
}
let out = {
let file = H5File::open(&path).unwrap();
file.dataset("d").unwrap().read_raw::<i32>().unwrap()
};
std::fs::remove_file(&path).ok();
out
}
fn fixed_array(file: &H5File) -> H5Dataset {
file.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.create("d")
.unwrap()
}
fn extensible_array(file: &H5File) -> H5Dataset {
file.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.max_shape(&[None, Some(6)])
.create("d")
.unwrap()
}
fn btree_v2(file: &H5File) -> H5Dataset {
file.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.max_shape(&[None, None])
.create("d")
.unwrap()
}
#[cfg(feature = "deflate")]
fn deflated(file: &H5File) -> H5Dataset {
file.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.max_shape(&[None, Some(6)])
.deflate(6)
.create("d")
.unwrap()
}
#[cfg(feature = "deflate")]
fn deflated_btree_v2(file: &H5File) -> H5Dataset {
file.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.max_shape(&[None, None])
.deflate(6)
.create("d")
.unwrap()
}
type Layout = (&'static str, fn(&H5File) -> H5Dataset);
fn layouts() -> Vec<Layout> {
let mut cases: Vec<Layout> = vec![
("fa", fixed_array),
("ea", extensible_array),
("bt2", btree_v2),
];
cases.extend(filtered_layouts());
cases
}
#[cfg(feature = "deflate")]
fn filtered_layouts() -> Vec<Layout> {
vec![("deflate", deflated), ("deflate-bt2", deflated_btree_v2)]
}
#[cfg(not(feature = "deflate"))]
fn filtered_layouts() -> Vec<Layout> {
Vec::new()
}
fn expected(starts: [usize; 2], counts: [usize; 2], values: &[i32]) -> Vec<i32> {
let mut want: Vec<i32> = (0..24).collect();
for r in 0..counts[0] {
for c in 0..counts[1] {
want[(starts[0] + r) * 6 + starts[1] + c] = values[r * counts[1] + c];
}
}
want
}
#[test]
fn whole_chunk_selection_replaces_that_chunk_only() {
let values: Vec<i32> = vec![-1, -2, -3, -4, -5, -6];
for (label, build) in layouts() {
let got = round_trip(&format!("whole_{label}"), build, |ds| {
ds.write_slice(&[2, 3], &[2, 3], &values).unwrap()
});
assert_eq!(got, expected([2, 3], [2, 3], &values), "index {label}");
}
}
#[test]
fn partial_chunk_selection_preserves_the_rest_of_the_chunk() {
let values: Vec<i32> = vec![-7, -8];
for (label, build) in layouts() {
let got = round_trip(&format!("partial_{label}"), build, |ds| {
ds.write_slice(&[1, 1], &[1, 2], &values).unwrap()
});
assert_eq!(got, expected([1, 1], [1, 2], &values), "index {label}");
}
}
#[test]
fn row_update_spans_the_chunk_row_and_leaves_the_others() {
let values: Vec<i32> = vec![100, 101, 102, 103, 104, 105];
for (label, build) in layouts() {
let got = round_trip(&format!("row_{label}"), build, |ds| {
ds.write_slice(&[2, 0], &[1, 6], &values).unwrap()
});
assert_eq!(got, expected([2, 0], [1, 6], &values), "index {label}");
}
}
#[test]
fn selection_straddling_every_chunk_boundary() {
let values: Vec<i32> = vec![70, 71, 72, 73, 74, 75, 76, 77];
for (label, build) in layouts() {
let got = round_trip(&format!("straddle_{label}"), build, |ds| {
ds.write_slice(&[1, 2], &[2, 4], &values).unwrap()
});
assert_eq!(got, expected([1, 2], [2, 4], &values), "index {label}");
}
}
#[test]
fn untouched_elements_of_a_new_chunk_hold_the_fill_value() {
let path = tmp("fill");
{
let file = H5File::create(&path).unwrap();
let ds = file
.new_dataset::<i32>()
.shape([4, 6])
.chunk(&[2, 3])
.fill_value(-99)
.create("d")
.unwrap();
ds.write_slice(&[1, 1], &[1, 1], &[42i32]).unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let got = file.dataset("d").unwrap().read_raw::<i32>().unwrap();
let mut want = vec![-99i32; 24];
want[6 + 1] = 42; assert_eq!(got, want);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn edge_chunks_that_hang_past_the_extent() {
let path = tmp("edge");
{
let file = H5File::create(&path).unwrap();
let ds = file
.new_dataset::<i32>()
.shape([5, 5])
.chunk(&[2, 2])
.fill_value(0)
.create("d")
.unwrap();
let seed: Vec<i32> = (0..25).collect();
ds.write_slice(&[0, 0], &[5, 5], &seed).unwrap();
ds.write_slice(&[4, 4], &[1, 1], &[999i32]).unwrap();
ds.write_slice(&[4, 0], &[1, 4], &[900i32, 901, 902, 903])
.unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let ds = file.dataset("d").unwrap();
assert_eq!(ds.shape(), vec![5, 5]);
let mut want: Vec<i32> = (0..25).collect();
want[24] = 999;
want[20..24].copy_from_slice(&[900, 901, 902, 903]);
assert_eq!(ds.read_raw::<i32>().unwrap(), want);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn three_dimensional_selection() {
let path = tmp("3d");
{
let file = H5File::create(&path).unwrap();
let ds = file
.new_dataset::<i32>()
.shape([2, 4, 3])
.chunk(&[1, 2, 3])
.create("d")
.unwrap();
let seed: Vec<i32> = (0..24).collect();
ds.write_slice(&[0, 0, 0], &[2, 4, 3], &seed).unwrap();
ds.write_slice(&[1, 1, 0], &[1, 2, 2], &[-1i32, -2, -3, -4])
.unwrap();
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
let got = file.dataset("d").unwrap().read_raw::<i32>().unwrap();
let mut want: Vec<i32> = (0..24).collect();
want[15] = -1;
want[16] = -2;
want[18] = -3;
want[19] = -4;
assert_eq!(got, want);
}
std::fs::remove_file(&path).ok();
}
#[test]
fn repeated_partial_writes_are_idempotent() {
for (label, build) in layouts() {
let got = round_trip(&format!("idem_{label}"), build, |ds| {
for _ in 0..5 {
ds.write_slice(&[1, 2], &[2, 2], &[-1i32, -2, -3, -4])
.unwrap();
}
});
assert_eq!(
got,
expected([1, 2], [2, 2], &[-1, -2, -3, -4]),
"index {label}"
);
}
}
#[test]
fn a_row_update_allocates_only_the_chunks_it_touches() {
let path = tmp("cost");
let full_extent_bytes = 64 * 16 * 4;
{
let file = H5File::create(&path).unwrap();
let ds = file
.new_dataset::<i32>()
.shape([64, 16])
.chunk(&[1, 16])
.max_shape(&[None, Some(16)])
.create("d")
.unwrap();
ds.write_slice(&[7, 0], &[1, 16], &[5i32; 16]).unwrap();
file.close().unwrap();
}
let size = std::fs::metadata(&path).unwrap().len();
assert!(
size < full_extent_bytes / 4,
"one-row update wrote {size} bytes for a {full_extent_bytes}-byte extent"
);
{
let file = H5File::open(&path).unwrap();
let got = file.dataset("d").unwrap().read_raw::<i32>().unwrap();
assert_eq!(&got[7 * 16..8 * 16], &[5i32; 16]);
assert!(got[..7 * 16].iter().all(|&v| v == 0));
assert!(got[8 * 16..].iter().all(|&v| v == 0));
}
std::fs::remove_file(&path).ok();
}
#[test]
fn out_of_bounds_selection_is_rejected() {
let path = tmp("oob");
let file = H5File::create(&path).unwrap();
let ds = fixed_array(&file);
assert!(ds.write_slice(&[3, 0], &[2, 6], &[0i32; 12]).is_err());
assert!(ds.write_slice(&[0, 4], &[4, 4], &[0i32; 16]).is_err());
assert!(ds.write_slice(&[0, 0], &[2, 3], &[0i32; 5]).is_err());
assert!(ds.write_slice(&[2, 3], &[2, 3], &[1i32; 6]).is_ok());
std::fs::remove_file(&path).ok();
}