Files
clawhdf5/crates/clawhdf5-tools/tests/remote.rs
T
osobhandClaude Opus 5.5 8df5b209a7 clawhdf5-remote, h5rs: never allocate a length the server only claims
h5rs check URL read the whole file with one read_at(0, len), len being
whatever Content-Range said. BlockCache listed every block index of the
span and preallocated len bytes: a server claiming 2^62 bytes for a 10 KB
file made h5rs abort (memory allocation of 35184372088832 bytes failed).

- BlockCache: a read spanning more than the budget (or eight max_requests)
  is fetched piece by piece and not kept, its output growing only as
  data arrives; read_ranges falls back to that per range; prefetch is
  clamped to the budget.
- New clawhdf5_remote::download(storage, max_bytes): refuses a claimed
  length above the limit (RemoteError::TooLarge) before any request, then
  reads in 64 MiB steps. New RemoteError::Backend for read errors.
- h5rs check downloads through it, with --max-download N (default 1 GiB).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 18:27:14 -05:00

122 lines
4.4 KiB
Rust

//! `h5rs` on URLs (feature `remote`): every subcommand prints for
//! `http://…/file.h5` what it prints for the local file (the name aside).
//! The files are served by the range-request test server of
//! clawhdf5-remote on 127.0.0.1.
#![cfg(feature = "remote")]
#[path = "../../clawhdf5-remote/tests/common/server.rs"]
mod server;
use std::path::{Path, PathBuf};
use std::process::Command;
fn h5rs(args: &[&str]) -> (String, i32) {
let out = Command::new(env!("CARGO_BIN_EXE_h5rs"))
.args(args)
.output()
.expect("run h5rs");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(text, out.status.code().unwrap_or(-1))
}
fn fixtures() -> Vec<PathBuf> {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
[
"../clawhdf5/tests/fixtures/tall.h5",
"../clawhdf5/tests/fixtures/written_by_v2_7_0.h5",
"../clawhdf5/tests/fixtures/written_by_v2_7_0_paged.h5",
"../clawhdf5/tests/fixtures/h5clear_mdc_image.h5",
"../clawhdf5-format/tests/fixtures/fractal_heap_multiblock.h5",
"../clawhdf5-format/tests/fixtures/legacy/tcompound.h5",
"../clawhdf5-format/tests/fixtures/legacy/h5ex_g_iterate.h5",
]
.iter()
.map(|p| root.join(p))
.collect()
}
#[test]
fn every_subcommand_reads_a_url_like_the_local_file() {
let files = fixtures();
let served: Vec<(String, Vec<u8>)> = files
.iter()
.enumerate()
.map(|(i, p)| {
let name = p.file_name().unwrap().to_str().unwrap();
(format!("/{i}/{name}"), std::fs::read(p).unwrap())
})
.collect();
let server = server::Server::start(served.clone());
for (p, (url_path, _)) in files.iter().zip(&served) {
let url = server.url(url_path);
let local = p.to_str().unwrap();
for cmd in [
&["ls", "-r", "-v"][..],
&["dump"],
&["dump", "--json"],
&["stat"],
&["check", "--data"],
] {
fn args<'a>(cmd: &[&'a str], f: &'a str) -> Vec<&'a str> {
cmd.iter().copied().chain([f]).collect()
}
let (want, want_rc) = h5rs(&args(cmd, local));
let (got, got_rc) = h5rs(&args(cmd, &url));
assert_eq!(
got.replace(&url, local),
want,
"h5rs {} {url}",
cmd.join(" ")
);
assert_eq!(got_rc, want_rc, "h5rs {} {url}", cmd.join(" "));
}
let (a, rc) = h5rs(&["diff", local, &url]);
assert_eq!(rc, 0, "h5rs diff {local} {url}: {a}");
}
}
#[test]
fn url_errors_are_clean() {
let server = server::Server::start(vec![("/x.h5".into(), vec![1u8; 100])]);
let (out, rc) = h5rs(&["ls", &server.url("/missing.h5")]);
assert_eq!(rc, 2, "{out}");
assert!(out.contains("404"), "{out}");
let (out, rc) = h5rs(&["ls", &server.url("/x.h5")]);
assert_eq!(rc, 2, "{out}");
assert!(out.contains("not an HDF5 file"), "{out}");
let (out, rc) = h5rs(&["check", &server.url("/missing.h5")]);
assert_eq!(rc, 2, "{out}");
#[cfg(not(feature = "remote-https"))]
{
let (out, rc) = h5rs(&["ls", "https://example.com/a.h5"]);
assert_eq!(rc, 2, "{out}");
assert!(out.contains("`https` feature"), "{out}");
}
}
/// `check` downloads a remote file whole, but never trusts the length the
/// server claims: a server claiming 2^62 bytes for a small file is refused
/// before anything is allocated (it used to abort the process), and
/// `--max-download` caps real files too.
#[test]
fn check_refuses_a_remote_file_beyond_the_download_limit() {
use std::sync::atomic::Ordering;
let tall = Path::new(env!("CARGO_MANIFEST_DIR")).join("../clawhdf5/tests/fixtures/tall.h5");
let server = server::Server::start(vec![("/t.h5".into(), std::fs::read(&tall).unwrap())]);
let url = server.url("/t.h5");
let (out, rc) = h5rs(&["check", &url]);
assert_eq!(rc, 0, "{out}");
let (out, rc) = h5rs(&["check", "--max-download", "1000", &url]);
assert_eq!(rc, 2, "{out}");
assert!(out.contains("download limit of 1000 bytes"), "{out}");
server.shared.fake_total.store(1 << 62, Ordering::SeqCst);
let (out, rc) = h5rs(&["check", &url]);
assert_eq!(rc, 2, "{out}");
assert!(out.contains("more than the download limit"), "{out}");
}