Files
clawhdf5/scripts/ci-test.sh
T
osobhandClaude Opus 5.5 db2554dd81 clawhdf5-remote: block cache and HTTP range reads (open_url)
Range-read milestone M3, first half: a new crate with the block cache the
design makes mandatory for remote files and an HTTP backend, so
open_url("http://...") gives a clawhdf5::File over File::open_storage.

BlockCache wraps any Storage: aligned blocks (1 MiB by default, the size
docs/design/range-reads.md section 2 measured), LRU with a byte budget,
the missing blocks of one read_at/read_ranges fetched with one backend
read_ranges call as runs of consecutive blocks (a one-block gap filled to
merge runs, each request at most 8 MiB), and reads that miss more than
half the budget not kept. Thread-safe without holding the lock across a
fetch: a block being fetched is in flight, a second reader waits for it
instead of fetching it again, and a failed fetch fails its waiters and is
not cached. A backend holding the file in memory passes through.

HttpStorage (ureq, no TLS by default; `https` adds rustls with ring):
opening is one ranged GET of the first block, whose Content-Range gives
the length (the cache keeps the bytes). The file is pinned by a strong
ETag (If-Match), else Last-Modified (If-Unmodified-Since), and its length,
checked on every response: a change is RemoteError::FileChanged, never
mixed data. A server that ignores Range is refused without reading the
body unless a full download is allowed. Connection errors, timeouts,
408/429/5xx and short bodies are retried with exponential backoff;
Accept-Encoding: identity, and an encoded body is refused. read_ranges
fetches its ranges in parallel.

Tests (a std-only HTTP/1.1 server in tests/common/server.rs, also the
range_server example): every fixture read over HTTP gives File::open's
transcript (CLAWHDF5_REMOTE_CORPUS adds the conformance corpus), with
request counts per file with and without the cache; an h5py-written file
against libhdf5's values; a multi-block file fetched in whole blocks, each
once; a server ignoring Range; a file replaced mid-read (ETag,
Last-Modified, length only); truncated bodies and 503s (retried, then an
error, never cached); a slow server with 8 concurrent readers (no block
fetched twice); bad URLs, 404, encoded bodies, non-HDF5 data. The cache
has unit tests for coalescing, splitting, LRU order, large reads,
failures and concurrent in-flight dedup.

ci-test.sh: clawhdf5-remote joins the no-C default-build check, and its
https feature is linted.

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

309 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# CI test script — runs fmt, clippy (all targets + feature matrix), tests,
# Python interop suites, bench compilation, and no_std checks.
#
# Usage:
# ./scripts/ci-test.sh
#
# Environment:
# CLAWHDF5_REQUIRE_INTEROP=1 Fail (instead of skip) when python3 with
# h5py/netCDF4/xarray is missing, or without
# maturin/pytest for the Python package step.
# CI sets this.
# Unset locally, the interop steps are skipped
# if python3+h5py is not importable.
# CLAWHDF5_FUZZ_SECONDS=N Run each cargo-fuzz target for N seconds
# (needs nightly + cargo-fuzz). Default: skip.
#
# Exit codes:
# 0 — all checks passed
# 1 — one or more checks failed
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Interop suites drive a Python interpreter. On a PEP 668 "externally managed"
# system h5py can only live in a virtualenv, so pick one up here — before any
# test step, since the non-ignored interop suites read the same variable.
if [ -z "${CLAWHDF5_PYTHON:-}" ] && [ -x "$SCRIPT_DIR/../.venv/bin/python" ]; then
export CLAWHDF5_PYTHON="$SCRIPT_DIR/../.venv/bin/python"
fi
PASS=0
FAIL=0
STEPS=()
run_step() {
local name="$1"
shift
echo ""
echo "==> [$name]"
if "$@" 2>&1; then
echo " ✓ PASS: $name"
PASS=$((PASS + 1))
STEPS+=("PASS: $name")
else
echo " ✗ FAIL: $name"
FAIL=$((FAIL + 1))
STEPS+=("FAIL: $name")
fi
}
# All steps always run so one failure doesn't hide the others; the summary
# and the exit code at the end are the verdict.
# 1. Format check
run_step "cargo fmt --check" cargo fmt --check
# 2. Clippy over every target (lib, bins, tests, benches, examples). Without
# --all-targets, test and bench code is never linted. clawhdf5-py is
# excluded here: PyO3 needs a Python interpreter to build, so it is
# linted in the Python package step (5b) instead.
run_step "cargo clippy --all-targets" cargo clippy \
--workspace \
--exclude clawhdf5-py \
--all-targets \
-- -D warnings
# 3. Clippy over clawhdf5-format's optional features, which the default
# workspace build never compiles (szip is left out: it needs libaec).
# plugin-filters = bitshuffle, bzip2, blosc, blosc2, zfp (and the default-on lzf).
run_step "cargo clippy (format feature matrix)" cargo clippy \
-p clawhdf5-format \
--all-targets \
--features parallel,lz4,zstd,pcodec,fast-checksum,plugin-filters \
-- -D warnings
# Each plugin filter alone, so none of them leans on another's
# dependencies (bitshuffle and blosc share code).
plugin_filters_alone() {
local f
for f in bitshuffle bzip2 blosc blosc2 zfp; do
echo "--- $f"
cargo clippy -p clawhdf5-format --all-targets --features "$f" -- -D warnings || return 1
done
}
run_step "cargo clippy (each plugin filter alone)" plugin_filters_alone
# The facade's plugin-filter interop tests and its parallel decoding tests
# only build with those features on (parallel_integration.rs stopped
# compiling unnoticed while nothing built it).
run_step "cargo clippy (facade plugin filters + parallel)" cargo clippy \
-p clawhdf5 \
--all-targets \
--features plugin-filters,parallel \
-- -D warnings
# The HNSW index's parallel bulk build is feature-gated too.
run_step "cargo clippy (ann parallel)" cargo clippy \
-p clawhdf5-ann \
--all-targets \
--features parallel \
-- -D warnings
# zlib-ng is opt-in (`fast-deflate`; the default is pure-Rust zlib-rs), so
# nothing above builds it. Keep it compiling and passing.
run_step "cargo clippy (fast-deflate / zlib-ng)" cargo clippy \
-p clawhdf5-format -p clawhdf5-filters -p clawhdf5 \
--all-targets \
--features clawhdf5-format/fast-deflate,clawhdf5-filters/fast-deflate \
-- -D warnings
# clawhdf5-remote's HTTPS backend (rustls).
run_step "cargo clippy (remote, https)" cargo clippy \
-p clawhdf5-remote \
--all-targets \
--features https \
-- -D warnings
# The README promises that the core crates build no C by default. Hold it to
# that: fail if a crate that compiles C (a *-sys crate, cc or cmake) enters the
# default dependency tree of any of them. clawhdf5-migrate (bundled SQLite),
# clawhdf5-napi (Node) and clawhdf5-gpu (graphics drivers) are exempt.
# js-sys (clawhdf5-wasm's bindings to JavaScript) builds no C.
# clawhdf5-remote is checked by default (plain HTTP); its https feature
# (ring) builds C and is opt-in.
no_c_in_default_build() {
local entry crate features found=0
for entry in clawhdf5-format clawhdf5-io clawhdf5-filters clawhdf5 \
clawhdf5-agent clawhdf5-ann clawhdf5-accel clawhdf5-netcdf4 clawhdf5-cli \
clawhdf5-tools \
clawhdf5-wasm \
clawhdf5-remote; do
crate=${entry%%:*}
features=()
[ "$entry" != "$crate" ] && features=(--features "${entry#*:}")
local c_deps
c_deps=$(cargo tree -q -p "$crate" "${features[@]}" -e normal,build --prefix none \
| grep -E '^([a-z0-9_-]+-sys|cc|cmake) v' \
| grep -v '^js-sys v' | sort -u)
if [ -n "$c_deps" ]; then
echo "$entry pulls in C by default:"
echo "$c_deps" | sed 's/^/ /'
found=1
fi
done
return $found
}
run_step "no C in the default build (core crates)" no_c_in_default_build
# The reader in the browser: the facade's read path must build for
# wasm32-unknown-unknown (no mmap, no threads, no file system), and the
# wasm-bindgen crate must build and lint there. Needs
# `rustup target add wasm32-unknown-unknown`.
run_step "wasm32 build (clawhdf5, no default features)" cargo build \
-p clawhdf5 \
--target wasm32-unknown-unknown \
--no-default-features
run_step "wasm32 clippy (clawhdf5-wasm)" cargo clippy \
-p clawhdf5-wasm \
--target wasm32-unknown-unknown \
--all-targets \
-- -D warnings
# A 64-bit file address must not wrap on a 32-bit target.
run_step "check-32bit-casts.sh" "$SCRIPT_DIR/check-32bit-casts.sh"
# The built wasm package, run under Node against h5py/netCDF4-written files,
# and the viewer page in headless Chromium when one is found.
# Needs node and the wasm-bindgen CLI, which the CI container does not have;
# the same expectations are checked natively by clawhdf5-wasm's h5py_interop
# test in the cargo test step.
if command -v node >/dev/null && command -v wasm-bindgen >/dev/null; then
run_step "wasm package under Node (+ browser)" bash "$SCRIPT_DIR/../examples/wasm-viewer/test/run.sh"
else
echo ""
echo "==> [wasm package under Node] SKIPPED: needs node and wasm-bindgen"
STEPS+=("SKIP: wasm package under Node")
fi
# The workspace declares a minimum Rust version (rust-version in Cargo.toml);
# check that it really builds there, so the README badge and the manifests
# cannot drift from the truth. Separate target dir: a different toolchain
# would otherwise invalidate the main build.
msrv_check() {
local msrv
msrv=$(sed -n 's/^rust-version = "\(.*\)"/\1/p' "$SCRIPT_DIR/../Cargo.toml")
[ -n "$msrv" ] || { echo "no rust-version in Cargo.toml"; return 1; }
rustup toolchain install "$msrv" --profile minimal >/dev/null || return 1
echo "checking with Rust $msrv"
CARGO_TARGET_DIR="$SCRIPT_DIR/../target/msrv" cargo "+$msrv" check \
--workspace --exclude clawhdf5-py --all-targets
}
run_step "MSRV check" msrv_check
# 4. Tests (exclude clawhdf5-py)
run_step "cargo test" cargo test \
--workspace \
--exclude clawhdf5-py
run_step "cargo test (format feature matrix)" cargo test \
-p clawhdf5-format \
--features parallel,lz4,zstd,pcodec,fast-checksum,plugin-filters
run_step "cargo test (facade parallel)" cargo test \
-p clawhdf5 \
--features parallel
run_step "cargo test (ann parallel)" cargo test \
-p clawhdf5-ann \
--features parallel
run_step "cargo test (fast-deflate / zlib-ng)" cargo test \
-p clawhdf5-format -p clawhdf5-filters -p clawhdf5 \
--features clawhdf5-format/fast-deflate,clawhdf5-filters/fast-deflate
# 5. Python interop suites. The h5py writer tests are #[ignore]d so a plain
# `cargo test` stays hermetic; run them explicitly here.
# On a PEP 668 "externally managed" system h5py can only live in a
# virtualenv, so honour CLAWHDF5_PYTHON (and a local .venv) rather than
# skipping — the tests read the same variable.
PYTHON="${CLAWHDF5_PYTHON:-python3}"
if "$PYTHON" -c "import h5py" >/dev/null 2>&1 || [ "${CLAWHDF5_REQUIRE_INTEROP:-0}" = "1" ]; then
# lz4/zstd so the hdf5plugin round-trips (our LZ4 and Zstd output read by
# libhdf5's registered plugins) compile and run too.
run_step "h5py interop (format, ignored tests)" cargo test \
-p clawhdf5-format --features lz4,zstd --test writer_h5py_tests -- --include-ignored
# LZF, bitshuffle, bzip2 and Blosc both ways against h5py + hdf5plugin;
# Blosc2 and ZFP (read-only) against what h5py reads.
run_step "h5py interop (plugin filters)" cargo test \
-p clawhdf5 --features plugin-filters --test plugin_filters_interop --test zfp_interop
else
echo ""
echo "==> [h5py interop] SKIPPED: no h5py in $PYTHON"
echo " (set CLAWHDF5_PYTHON=/path/to/venv/bin/python, or create .venv;"
echo " CLAWHDF5_REQUIRE_INTEROP=1 makes this a failure instead)"
STEPS+=("SKIP: h5py interop (format, ignored tests)")
fi
# 5b. The Python package (crates/clawhdf5-py): lint it, build the wheel with
# maturin and run its pytest suite, which compares every read with h5py.
# The wheel is unpacked under target/ and put on PYTHONPATH, so the
# interpreter's environment is left as it was. Needs maturin and pytest
# in $PYTHON (CI installs both); skipped without them, and a failure
# instead when CLAWHDF5_REQUIRE_INTEROP=1.
python_package() {
local root="$SCRIPT_DIR/.." out
out="${CARGO_TARGET_DIR:-$root/target}/py-package"
rm -rf "$out" && mkdir -p "$out/wheel" "$out/site" || return 1
cargo clippy -p clawhdf5-py --all-targets -- -D warnings || return 1
"$PYTHON" -m maturin build \
-m "$root/crates/clawhdf5-py/Cargo.toml" \
-i "$PYTHON" \
--out "$out/wheel" || return 1
"$PYTHON" -m pip install --quiet --no-deps --target "$out/site" "$out"/wheel/*.whl || return 1
PYTHONPATH="$out/site" "$PYTHON" -m pytest -q -p no:cacheprovider \
"$root/crates/clawhdf5-py/tests"
}
if "$PYTHON" -m maturin --version >/dev/null 2>&1 && "$PYTHON" -c "import pytest" >/dev/null 2>&1; then
run_step "Python package (maturin build + pytest vs h5py)" python_package
elif [ "${CLAWHDF5_REQUIRE_INTEROP:-0}" = "1" ]; then
run_step "Python package (maturin build + pytest vs h5py)" \
bash -c "echo \"maturin and pytest are required in $PYTHON (CLAWHDF5_REQUIRE_INTEROP=1)\"; exit 1"
else
echo ""
echo "==> [Python package] SKIPPED: needs maturin and pytest in $PYTHON"
STEPS+=("SKIP: Python package (maturin build + pytest)")
fi
# 6. Benches must keep compiling (they are not run).
run_step "cargo bench --no-run" cargo bench \
--workspace \
--exclude clawhdf5-py \
--no-run
# 7. no_std check
run_step "check-nostd.sh" "$SCRIPT_DIR/check-nostd.sh"
# 8. Optional fuzz smoke run
if [ -n "${CLAWHDF5_FUZZ_SECONDS:-}" ]; then
fuzz_smoke() {
local crate target
for crate in clawhdf5-format clawhdf5-agent; do
cd "$SCRIPT_DIR/../crates/$crate" || return 1
for target in $(cargo +nightly fuzz list); do
echo "--- fuzz: $crate/$target"
cargo +nightly fuzz run "$target" -- \
-max_total_time="$CLAWHDF5_FUZZ_SECONDS" || return 1
done
done
}
run_step "fuzz smoke (${CLAWHDF5_FUZZ_SECONDS}s/target)" fuzz_smoke
fi
# Summary
echo ""
echo "========================================"
echo " CI Summary"
echo "========================================"
for s in "${STEPS[@]}"; do
echo " $s"
done
echo "----------------------------------------"
echo " $PASS passed, $FAIL failed"
echo "========================================"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0