use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use rust_hdf5::swmr::{SwmrFileReader, SwmrFileWriter};
use rust_hdf5::{FileLocking, H5File};
fn enabled() -> rust_hdf5::H5FileOptions {
H5File::options().locking(FileLocking::Enabled)
}
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_lock_{}_{}_{}",
label,
std::process::id(),
n
));
std::fs::create_dir_all(&dir).unwrap();
dir.join(format!("{label}.h5"))
}
#[test]
fn second_writer_open_is_blocked() {
let path = unique_tmp("two_writers");
let _file1 = enabled().create(&path).unwrap();
let err = enabled().open_rw(&path);
assert!(
err.is_err(),
"expected second writer open to fail with lock conflict"
);
}
#[test]
fn reader_blocks_writer() {
let path = unique_tmp("reader_blocks_writer");
enabled().create(&path).unwrap().close().unwrap();
let _reader = enabled().open(&path).unwrap();
let err = enabled().open_rw(&path);
assert!(
err.is_err(),
"expected writer open to be blocked by reader's shared lock"
);
}
#[test]
fn writer_blocks_reader() {
let path = unique_tmp("writer_blocks_reader");
let _writer = enabled().create(&path).unwrap();
let err = enabled().open(&path);
assert!(
err.is_err(),
"expected reader open to be blocked by writer's exclusive lock"
);
}
#[test]
fn multiple_readers_coexist() {
let path = unique_tmp("multi_readers");
enabled().create(&path).unwrap().close().unwrap();
let _r1 = enabled().open(&path).unwrap();
let _r2 = enabled().open(&path).unwrap();
let _r3 = enabled().open(&path).unwrap();
}
#[test]
fn disabled_locking_bypasses_conflict() {
let path = unique_tmp("disabled");
enabled().create(&path).unwrap().close().unwrap();
let _w1 = H5File::options().no_locking().open_rw(&path).unwrap();
let w2 = H5File::options().no_locking().open_rw(&path);
assert!(
w2.is_ok(),
"expected disabled-locking second open to succeed: {:?}",
w2.err()
);
}
#[test]
fn best_effort_does_not_error_on_conflict() {
let path = unique_tmp("best_effort");
enabled().create(&path).unwrap().close().unwrap();
let _w1 = enabled().open_rw(&path).unwrap();
let w2 = H5File::options().best_effort_locking().open_rw(&path);
assert!(
w2.is_ok(),
"expected best-effort open to succeed despite conflict: {:?}",
w2.err()
);
}
#[test]
fn lock_releases_on_drop() {
let path = unique_tmp("release_on_drop");
enabled().create(&path).unwrap().close().unwrap();
{
let _w1 = enabled().open_rw(&path).unwrap();
} let w2 = enabled().open_rw(&path);
assert!(
w2.is_ok(),
"expected reopen after drop to succeed: {:?}",
w2.err()
);
}
#[test]
fn create_with_lock_conflict_does_not_truncate_existing_file() {
let path = unique_tmp("preserve_on_conflict");
{
let f = enabled().create(&path).unwrap();
let ds = f.new_dataset::<u8>().shape([4]).create("data").unwrap();
ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
f.close().unwrap();
}
let original_size = std::fs::metadata(&path).unwrap().len();
assert!(original_size > 0, "fixture file should be non-empty");
let _holder = enabled().open_rw(&path).unwrap();
let conflict = enabled().create(&path);
assert!(conflict.is_err(), "second create should fail under lock");
let size_after = std::fs::metadata(&path).unwrap().len();
assert_eq!(
size_after, original_size,
"file must not be truncated when create() loses the lock race"
);
}
#[test]
fn lock_releases_on_close() {
let path = unique_tmp("release_on_close");
let w1 = enabled().create(&path).unwrap();
w1.close().unwrap();
let w2 = enabled().open_rw(&path);
assert!(
w2.is_ok(),
"expected reopen after close to succeed: {:?}",
w2.err()
);
}
#[test]
fn options_locking_overrides_env() {
let path = unique_tmp("options_override");
enabled().create(&path).unwrap().close().unwrap();
let _w1 = enabled().open_rw(&path).unwrap();
let conflict = enabled().open_rw(&path);
assert!(conflict.is_err(), "Enabled policy should block second open");
let bypass = H5File::options()
.locking(FileLocking::Disabled)
.open_rw(&path);
assert!(bypass.is_ok(), "Disabled policy should bypass the lock");
}
#[test]
fn swmr_reader_attaches_after_start_swmr() {
let path = unique_tmp("swmr_attach");
let mut writer = SwmrFileWriter::create_with_locking(&path, FileLocking::Enabled).unwrap();
let _ds = writer
.create_streaming_dataset::<f32>("frames", &[4, 4])
.unwrap();
let too_early = SwmrFileReader::open_with_locking(&path, FileLocking::Enabled);
assert!(
too_early.is_err(),
"SWMR reader should be blocked before start_swmr"
);
writer.start_swmr().unwrap();
let reader = SwmrFileReader::open_with_locking(&path, FileLocking::Enabled);
assert!(
reader.is_ok(),
"SWMR reader should attach after start_swmr: {:?}",
reader.err()
);
let other_writer = enabled().open_rw(&path);
assert!(
other_writer.is_err(),
"second writer should still be blocked while SWMR writer holds shared lock"
);
drop(reader);
writer.close().unwrap();
}
#[test]
fn swmr_disabled_locking_allows_concurrent_writer() {
let path = unique_tmp("swmr_no_lock");
let mut writer = SwmrFileWriter::create_with_locking(&path, FileLocking::Disabled).unwrap();
let _ds = writer
.create_streaming_dataset::<f32>("frames", &[2, 2])
.unwrap();
writer.start_swmr().unwrap();
let _r = SwmrFileReader::open_with_locking(&path, FileLocking::Disabled).unwrap();
let other_writer = H5File::options()
.no_locking()
.open_rw(&path);
assert!(
other_writer.is_ok(),
"disabled-locking second writer should succeed: {:?}",
other_writer.err()
);
}