fix(format): page Fixed Array data blocks past 1024 chunks
The Fixed Array writer always packed every element into one data block behind one checksum. Past 2^10 elements libhdf5 (and our reader) expect a paged block: a page-init bitmap after the prefix, then one checksummed page per 1024 elements. Any dataset with more than 1024 chunks and no unlimited dimension failed with "incorrect metadata checksum" in h5py, h5dump and our own reader. build_fixed_array_at now takes one Option<WrittenChunk> per array slot so later fixes can leave unallocated slots. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use clawhdf5::File;
|
||||
use clawhdf5::{File, FileBuilder};
|
||||
|
||||
fn python() -> String {
|
||||
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
|
||||
@@ -254,3 +254,139 @@ fn h5py_fixed_array_partial_extent_reads_correctly() {
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Files we write, read back by libhdf5 (h5py and h5dump) and by us
|
||||
// ===========================================================================
|
||||
|
||||
/// One `i4` dataset we write, filled with `arange` over `shape`.
|
||||
struct WriteCase {
|
||||
name: String,
|
||||
shape: Vec<u64>,
|
||||
chunks: Vec<u64>,
|
||||
maxshape: Option<Vec<u64>>,
|
||||
deflate: bool,
|
||||
}
|
||||
|
||||
fn wcase(name: &str, shape: &[u64], chunks: &[u64], maxshape: Option<&[u64]>) -> WriteCase {
|
||||
WriteCase {
|
||||
name: name.to_string(),
|
||||
shape: shape.to_vec(),
|
||||
chunks: chunks.to_vec(),
|
||||
maxshape: maxshape.map(<[u64]>::to_vec),
|
||||
deflate: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn h5dump_available() -> bool {
|
||||
Command::new("h5dump")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Write every case into one file with our writer, then check that our own
|
||||
/// reader, h5py and h5dump (when installed) all return every value. Only the
|
||||
/// libhdf5 half is skipped without h5py.
|
||||
fn check_we_write(cases: &[WriteCase]) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("ours_chunk_index.h5");
|
||||
let path_str = path.display().to_string();
|
||||
|
||||
let mut b = FileBuilder::new();
|
||||
for c in cases {
|
||||
let n: u64 = c.shape.iter().product();
|
||||
let data: Vec<i32> = (0..n as i32).collect();
|
||||
let ds = b.create_dataset(&c.name);
|
||||
ds.with_i32_data(&data)
|
||||
.with_shape(&c.shape)
|
||||
.with_chunks(&c.chunks);
|
||||
if let Some(ms) = &c.maxshape {
|
||||
ds.with_maxshape(ms);
|
||||
}
|
||||
if c.deflate {
|
||||
ds.with_deflate(4);
|
||||
}
|
||||
}
|
||||
b.write(&path).unwrap();
|
||||
|
||||
// Our reader.
|
||||
let file = File::open(&path).unwrap();
|
||||
for c in cases {
|
||||
let got = file.dataset(&c.name).unwrap().read_i32().unwrap();
|
||||
let n: u64 = c.shape.iter().product();
|
||||
let bad = got
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, &v)| v != i as i32)
|
||||
.count();
|
||||
assert!(
|
||||
got.len() == n as usize && bad == 0,
|
||||
"{}: our reader: {bad} of {n} values wrong",
|
||||
c.name
|
||||
);
|
||||
}
|
||||
|
||||
// libhdf5 via h5py.
|
||||
skip_if_no_python!();
|
||||
let mut script =
|
||||
format!("import h5py, numpy as np\nbad = []\nf = h5py.File(r'{path_str}', 'r')\n");
|
||||
for c in cases {
|
||||
let shape: Vec<String> = c.shape.iter().map(u64::to_string).collect();
|
||||
let maxshape: Vec<String> = c
|
||||
.maxshape
|
||||
.as_ref()
|
||||
.unwrap_or(&c.shape)
|
||||
.iter()
|
||||
.map(|&d| {
|
||||
if d == u64::MAX {
|
||||
"None".to_string()
|
||||
} else {
|
||||
d.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
script += &format!(
|
||||
"d = f['{name}']\n\
|
||||
want = np.arange({n}, dtype='i4').reshape(({shape},))\n\
|
||||
got = d[()]\n\
|
||||
if d.maxshape != ({maxshape},): bad.append(('{name}', 'maxshape', d.maxshape))\n\
|
||||
elif not np.array_equal(got, want): \
|
||||
bad.append(('{name}', int((got != want).sum()), 'of', got.size))\n",
|
||||
name = c.name,
|
||||
n = c.shape.iter().product::<u64>(),
|
||||
shape = shape.join(","),
|
||||
maxshape = maxshape.join(","),
|
||||
);
|
||||
}
|
||||
script += "print(bad if bad else 'OK')\n";
|
||||
let out = run_python(&script);
|
||||
assert_eq!(out, "OK", "h5py disagrees");
|
||||
|
||||
// libhdf5's own tool, when installed.
|
||||
if h5dump_available() {
|
||||
let o = Command::new("h5dump").arg(&path).output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&o.stderr);
|
||||
assert!(
|
||||
o.status.success() && !stderr.to_lowercase().contains("error"),
|
||||
"h5dump failed: {stderr}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Fixed Array with more than 1024 elements must be paged, or libhdf5
|
||||
/// rejects the data block's checksum.
|
||||
#[test]
|
||||
fn we_write_paged_fixed_array() {
|
||||
let mut cases: Vec<WriteCase> = [1023u64, 1024, 1025, 2048, 5000]
|
||||
.iter()
|
||||
.map(|&n| wcase(&format!("fa_{n}"), &[n * 4], &[4], None))
|
||||
.collect();
|
||||
// Filtered elements are wider; a 2-D grid pages the same way.
|
||||
let mut filtered = wcase("fa_1500_deflate", &[1500 * 4], &[4], None);
|
||||
filtered.deflate = true;
|
||||
cases.push(filtered);
|
||||
cases.push(wcase("fa_2d_1100", &[110, 40], &[1, 4], None));
|
||||
check_we_write(&cases);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user