- Datatype::parse returns Datatype::Complex for class 11 (also inside
compounds, arrays and VL types) instead of the {r, i} compound view.
- Facade: DType::Complex(Box<DType>); read_complex_f32/f64 accept it.
- h5rs dump/ls/diff print native complex as h5dump/h5ls/h5diff 2.2.0 do
(checked against a fixture written by h5py 3.16 / libhdf5 2.0.0);
dump --json keeps the {r, i} compound (hdf5-json has no complex class).
- clawhdf5-wasm reads native complex datasets as [re, im] pairs.
- Python: clawhdf5.File(path, 'w', libver=...) with h5py's values,
mapped to FileBuilder::libver_bounds; 'v108' output opens in HDF5 1.8.23.
- Docs: known-issues entry moved to Fixed (history), CHANGELOG, READMEs.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
133 lines
5.9 KiB
Bash
133 lines
5.9 KiB
Bash
#!/usr/bin/env bash
|
||
# Load the viewer page in headless Chromium and check what it renders.
|
||
#
|
||
# browser.sh FIXTURE_DIR
|
||
#
|
||
# FIXTURE_DIR holds fixture.h5 from make_fixture.py (and big.h5 when it was
|
||
# run with WASM_BIG_MB); ../pkg must be built. test/serve.py serves the
|
||
# page and, under /fix, the fixtures, with HTTP range requests (no copies,
|
||
# no links). The page is opened with ?file=fix/<name>&path=<object>, which
|
||
# opens the file with openUrl (range requests), builds the tree down to
|
||
# <object> and shows it; the rendered DOM is dumped and checked for the
|
||
# values libhdf5 reads and for the request counter.
|
||
#
|
||
# Browser: $CHROME, else chromium/google-chrome on PATH, else a Playwright
|
||
# download under ~/.cache/ms-playwright. Exit 3 when none is found.
|
||
# BROWSER_DEBUG=/some/prefix saves each rendered page as prefix.<path>.html.
|
||
set -euo pipefail
|
||
|
||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||
FIX="$(cd "$1" && pwd)"
|
||
PY="${CLAWHDF5_PYTHON:-python3}"
|
||
|
||
chrome="${CHROME:-}"
|
||
if [ -z "$chrome" ]; then
|
||
for c in chromium chromium-browser google-chrome chrome-headless-shell; do
|
||
if command -v "$c" >/dev/null; then chrome="$(command -v "$c")"; break; fi
|
||
done
|
||
fi
|
||
if [ -z "$chrome" ]; then
|
||
chrome="$(ls -d "$HOME"/.cache/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell 2>/dev/null | tail -1 || true)"
|
||
fi
|
||
if [ -z "$chrome" ] || [ ! -x "$chrome" ]; then
|
||
echo "no Chromium found (set CHROME)" >&2
|
||
exit 3
|
||
fi
|
||
|
||
profiles="$(mktemp -d)"
|
||
server=""
|
||
cleanup() {
|
||
[ -n "$server" ] && kill "$server" 2>/dev/null || true
|
||
rm -rf "$profiles"
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
"$PY" "$HERE/serve.py" --root "fix=$FIX" --root "=$HERE/.." > "$profiles/port" &
|
||
server=$!
|
||
for _ in $(seq 50); do
|
||
[ -s "$profiles/port" ] && break
|
||
sleep 0.1
|
||
done
|
||
port="$(head -1 "$profiles/port")"
|
||
|
||
fails=0
|
||
# A fresh profile per page: a second instance on the same profile fails.
|
||
render() {
|
||
local profile
|
||
profile="$(mktemp -d "$profiles/profile.XXXXXX")"
|
||
"$chrome" --headless --no-sandbox --disable-gpu --user-data-dir="$profile" \
|
||
--virtual-time-budget=20000 \
|
||
--dump-dom "http://127.0.0.1:$port/index.html?file=$1&path=$2" 2>/dev/null
|
||
}
|
||
# expect FILE PATH TEXT...: every TEXT appears in the page rendered for PATH
|
||
# of FILE (a URL relative to the page); "re:TEXT" is an extended regex.
|
||
expect() {
|
||
local file="$1" path="$2" dom
|
||
shift 2
|
||
dom="$(render "$file" "$path")"
|
||
[ -n "${BROWSER_DEBUG:-}" ] && printf "%s\n" "$dom" > "$BROWSER_DEBUG.$(echo "$file$path" | tr / _).html"
|
||
for text in "$@"; do
|
||
local flag=-qF
|
||
if [ "${text#re:}" != "$text" ]; then flag=-qE; text="${text#re:}"; fi
|
||
if ! grep $flag -- "$text" <<<"$dom"; then
|
||
echo "FAIL: page for $file $path lacks: $text" >&2
|
||
fails=$((fails + 1))
|
||
fi
|
||
done
|
||
echo "rendered $file $path"
|
||
}
|
||
|
||
H5=fix/fixture.h5
|
||
# Tree (root expanded; the group row carries its path) and root attributes.
|
||
expect $H5 "/" 'data-path="/sensors"' 'data-path="/grid"' '<td>title</td><td>"wasm fixture"</td>' \
|
||
'<td>big</td><td>9223372036854775813</td>' '(compound{x: f64, n: i32})'
|
||
# A chunked, deflated 2-D dataset: type, shape, the first window of values;
|
||
# the request counter.
|
||
expect $H5 "/grid" '<dd>f64</dd>' '<dd>(6, 10)</dd>' '<th>9</th>' '<td>0.25</td>' '<td>14.75</td>' \
|
||
'showing rows 0–5, columns 0–9 of 60 values' 'request' 'fetched (100.0%)'
|
||
# Nested path revealed through the tree; big-endian float32.
|
||
expect $H5 "/sensors/temp" 'data-path="/sensors/temp"' '<dd>f32</dd>' '<td>21.5</td>' '<td>22.25</td>'
|
||
# 64-bit integers stay exact; strings; array datatype cells.
|
||
expect $H5 "/u64" '<td>18446744073709551615</td>'
|
||
expect $H5 "/vlen_str" '<td>"двa"</td>' '<dd>vlen string</dd>'
|
||
expect $H5 "/pairs" '<td>[2, 3]</td>' '<dd>array[2]<i32></dd>'
|
||
# 3-D: leading dimension held at 0, window over the last two.
|
||
expect $H5 "/cube" '<dd>(2, 5, 6)</dd>' '<td>29</td>' 'dim 0'
|
||
# HDF5 2.0 native complex (written when h5py's libhdf5 is 2.0 or later):
|
||
# each cell is the [re, im] pair.
|
||
if "$PY" -c 'import sys, h5py; sys.exit(not getattr(h5py.get_config(), "has_native_complex", False))'; then
|
||
expect $H5 "/native_c128" '<dd>complex<f64></dd>' '<dd>(3, 2)</dd>' '<td>[1, -1.5]</td>' \
|
||
'<td>[5, -7.5]</td>'
|
||
fi
|
||
# Unsupported type: an error, not values.
|
||
expect $H5 "/table" 'class="error"' 'reading compound{x: f64, n: i32} datasets is not supported'
|
||
# Cross-origin: the page is on 127.0.0.1, the file on localhost. CORS that
|
||
# exposes Content-Range, and CORS that exposes nothing (/unexposed/: the
|
||
# browser hides Content-Range and ETag, so the page learns the length with
|
||
# a HEAD request, which the server's log must show).
|
||
expect "http://localhost:$port/$H5" "/grid" '<td>14.75</td>' 'request'
|
||
get() { "$PY" -c 'import sys, urllib.request; print(urllib.request.urlopen(sys.argv[1]).read().decode())' "$1"; }
|
||
get "http://127.0.0.1:$port/__reset" >/dev/null
|
||
expect "http://localhost:$port/unexposed/$H5" "/grid" '<td>0.25</td>' '<td>14.75</td>' 'request'
|
||
heads="$(get "http://127.0.0.1:$port/__stats" | "$PY" -c \
|
||
'import json, sys; print(sum(1 for l in json.load(sys.stdin)["log"] if l[4] == "HEAD" and l[0].endswith("fixture.h5")))')"
|
||
if [ "$heads" -lt 1 ]; then
|
||
echo "FAIL: no HEAD request for the length with unexposed headers" >&2
|
||
fails=$((fails + 1))
|
||
fi
|
||
# A server without range support: the file is downloaded whole, and says so.
|
||
expect norange/$H5 "/grid" '<td>14.75</td>' 'downloaded whole'
|
||
# A large file: a small dataset, and a window of the large one, fetch a few
|
||
# blocks (the counter shows a small percentage: "(1.5%)", not "(100.0%)").
|
||
if [ -f "$FIX/big.h5" ]; then
|
||
expect fix/big.h5 "/small" '<td>1.5</td>' '<td>3.25</td>' 're:MiB of 191 MiB fetched \([0-9]\.[0-9]%\)'
|
||
expect fix/big.h5 "/big" '<dd>(25000000)</dd>' '<td>0</td>' '<td>24.5</td>' \
|
||
're:MiB of 191 MiB fetched \([0-9]\.[0-9]%\)'
|
||
fi
|
||
|
||
if [ "$fails" -gt 0 ]; then
|
||
echo "browser: $fails checks failed" >&2
|
||
exit 1
|
||
fi
|
||
echo "browser: all checks passed ($chrome)"
|