//! Keeps the concurrent-read harnesses working: runs `concurrent_read`, the //! h5py script (threads and processes) and the comparison script end to end //! on tiny files. h5py reading the files also checks, element by element at //! spot positions, that both harnesses generate the same data and slabs. //! //! The h5py half is skipped when python3 with h5py is unavailable, unless //! `CLAWHDF5_REQUIRE_INTEROP=1`; `CLAWHDF5_PYTHON` picks the interpreter. use std::path::{Path, PathBuf}; use std::process::Command; fn python() -> String { std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string()) } fn interop_required() -> bool { std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1") } fn python_available() -> bool { Command::new(python()) .args(["-c", "import h5py, numpy"]) .output() .map(|o| o.status.success()) .unwrap_or(false) } fn scripts() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("scripts") } fn run(cmd: &mut Command) -> String { let out = cmd.output().expect("spawn"); assert!( out.status.success(), "{cmd:?} failed\nSTDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr) ); String::from_utf8_lossy(&out.stdout).into_owned() } const SMALL: [&str; 8] = [ "--threads", "1,2", "--slabs", "8", "--reps", "1", "--slab", "64", ]; fn results(path: &Path) -> serde_json::Value { serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap() } #[test] fn harnesses_run_end_to_end_on_tiny_files() { let dir = tempfile::TempDir::new().unwrap(); let data = dir.path().join("data"); let claw = dir.path().join("claw.json"); let bin = env!("CARGO_BIN_EXE_concurrent_read"); run(Command::new(bin) .arg("--dir") .arg(&data) .args(["--datasets", "3", "--mib", "1"]) .args(SMALL) .arg("--json") .arg(&claw)); // Second run reuses the files (and exercises --cold). let out = Command::new(bin) .arg("--dir") .arg(&data) .args(["--datasets", "3", "--mib", "1", "--cold"]) .args(SMALL) .output() .unwrap(); assert!(out.status.success()); assert!(String::from_utf8_lossy(&out.stderr).contains("reusing")); let doc = results(&claw); assert_eq!(doc["tool"], "clawhdf5"); // 2 layouts x 2 modes x 2 thread counts. assert_eq!(doc["results"].as_array().unwrap().len(), 8); for r in doc["results"].as_array().unwrap() { assert!(r["mb_s"].as_f64().unwrap() > 0.0, "{r}"); } if !python_available() { assert!( !interop_required(), "CLAWHDF5_REQUIRE_INTEROP=1 but {} has no h5py", python() ); eprintln!("skipping the h5py half: no h5py in {}", python()); return; } let mut jsons = vec![claw]; for executor in ["threads", "processes"] { let out = dir.path().join(format!("h5py-{executor}.json")); run(Command::new(python()) .arg(scripts().join("concurrent_read_h5py.py")) .arg("--dir") .arg(&data) .args(["--executor", executor]) .args(SMALL) .arg("--json") .arg(&out)); let doc = results(&out); assert_eq!(doc["tool"], format!("h5py-{executor}")); assert_eq!(doc["results"].as_array().unwrap().len(), 8); jsons.push(out); } let table = run(Command::new(python()) .arg(scripts().join("compare_concurrent_read.py")) .args(&jsons)); assert!(table.contains("| deflate | same | 2 |"), "{table}"); assert!(table.contains("clawhdf5 / h5py-processes"), "{table}"); // A different workload must not be compared. let other = dir.path().join("other.json"); run(Command::new(python()) .arg(scripts().join("concurrent_read_h5py.py")) .arg("--dir") .arg(&data) .args([ "--threads", "1", "--slabs", "4", "--reps", "1", "--slab", "64", ]) .arg("--json") .arg(&other)); let out = Command::new(python()) .arg(scripts().join("compare_concurrent_read.py")) .arg(&jsons[0]) .arg(&other) .output() .unwrap(); assert!(!out.status.success()); assert!(String::from_utf8_lossy(&out.stderr).contains("slabs")); }