#![cfg(feature = "threadsafe")]
use rust_hdf5::types::VarLenUnicode;
use rust_hdf5::H5File;
const TEST_PYTHON: &str = "/Users/stevek/mamba/envs/bs2026.1/bin/python";
fn python() -> Option<&'static str> {
static PY: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
PY.get_or_init(|| {
let candidate =
std::env::var("RUST_HDF5_TEST_PYTHON").unwrap_or_else(|_| TEST_PYTHON.to_string());
if std::path::Path::new(&candidate).exists() {
Some(candidate)
} else {
eprintln!("skipping h5py cross-check: {candidate} not present");
None
}
})
.as_deref()
}
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_cc_{}_{}_{}.h5",
name,
std::process::id(),
n
))
}
fn value(k: usize, i: usize) -> i32 {
(k * 1_000_000 + i) as i32
}
#[test]
fn concurrent_create_append_metadata_distinct_datasets() {
const N: usize = 8; const BATCHES: usize = 8; const BATCH: usize = 32; const LEN: usize = BATCHES * BATCH; const CHUNK: usize = 16; const ITERS: usize = 16;
for iter in 0..ITERS {
let path = tmp(&format!("iter{iter}"));
{
let file = H5File::create(&path).unwrap();
std::thread::scope(|s| {
for k in 0..N {
let file = &file;
s.spawn(move || {
let ds = file
.new_dataset::<i32>()
.shape([0])
.chunk(&[CHUNK])
.max_shape(&[None])
.create(&format!("ds{k}"))
.unwrap_or_else(|e| panic!("create ds{k}: {e}"));
for b in 0..BATCHES {
let batch: Vec<i32> =
(0..BATCH).map(|i| value(k, b * BATCH + i)).collect();
ds.append(&batch)
.unwrap_or_else(|e| panic!("append ds{k} batch {b}: {e}"));
}
let attr = ds
.new_attr::<VarLenUnicode>()
.shape(())
.create("origin")
.unwrap_or_else(|e| panic!("create attr ds{k}: {e}"));
attr.write_string(&format!("thread-{k}"))
.unwrap_or_else(|e| panic!("write attr ds{k}: {e}"));
});
}
});
file.close().unwrap();
}
{
let file = H5File::open(&path).unwrap();
for k in 0..N {
let ds = file.dataset(&format!("ds{k}")).unwrap();
let got = ds.read_raw::<i32>().unwrap();
let want: Vec<i32> = (0..LEN).map(|i| value(k, i)).collect();
assert_eq!(
got.len(),
LEN,
"dataset ds{k} length mismatch on iter {iter}"
);
assert_eq!(got, want, "dataset ds{k} content mismatch on iter {iter}");
let origin = ds
.attr("origin")
.and_then(|a| a.read_string())
.unwrap_or_else(|e| panic!("read attr ds{k} iter {iter}: {e}"));
assert_eq!(
origin,
format!("thread-{k}"),
"dataset ds{k} attribute mismatch on iter {iter}"
);
}
}
if iter == 0 {
if let Some(py) = python() {
let script = format!(
"import h5py, numpy as np\n\
f = h5py.File(r'{}', 'r')\n\
for k in range({N}):\n\
\x20 d = f['ds%d' % k]\n\
\x20 v = d[...]\n\
\x20 exp = np.arange({LEN}, dtype='i8') + k * 1000000\n\
\x20 assert v.shape == ({LEN},), (k, v.shape)\n\
\x20 assert (v.astype('i8') == exp).all(), (k, v[:8], exp[:8])\n\
\x20 assert d.attrs['origin'] == ('thread-%d' % k), (k, d.attrs['origin'])\n\
f.close()\n",
path.display()
);
let status = std::process::Command::new(py)
.arg("-c")
.arg(&script)
.status()
.expect("failed to spawn python");
assert!(status.success(), "h5py cross-check failed for {path:?}");
}
}
std::fs::remove_file(&path).ok();
}
}
#[test]
fn concurrent_create_same_name_yields_one_dataset() {
use std::sync::atomic::{AtomicUsize, Ordering};
const N: usize = 8; const ITERS: usize = 64; const PATTERN: [i32; 4] = [10, 20, 30, 40];
for iter in 0..ITERS {
let path = tmp(&format!("dup{iter}"));
let successes = AtomicUsize::new(0);
{
let file = H5File::create(&path).unwrap();
std::thread::scope(|s| {
for _ in 0..N {
let file = &file;
let successes = &successes;
s.spawn(move || {
if let Ok(ds) = file
.new_dataset::<i32>()
.shape([0])
.chunk(&[4])
.max_shape(&[None])
.create("dup")
{
successes.fetch_add(1, Ordering::Relaxed);
ds.append(&PATTERN).expect("winner append");
}
});
}
});
file.close().unwrap();
}
let n_ok = successes.load(Ordering::Relaxed);
assert_eq!(
n_ok, 1,
"exactly one create must win on iter {iter}, got {n_ok}"
);
let file = H5File::open(&path).unwrap();
let dup_links = file
.dataset_names()
.into_iter()
.filter(|n| n.trim_start_matches('/') == "dup")
.count();
assert_eq!(
dup_links, 1,
"file must contain exactly one 'dup' link on iter {iter}, got {dup_links}"
);
let got = file.dataset("dup").unwrap().read_raw::<i32>().unwrap();
assert_eq!(got, PATTERN.to_vec(), "winner's data on iter {iter}");
drop(file);
std::fs::remove_file(&path).ok();
}
}