cargo test --workspace unifies clawhdf5-format/zstd on (another member enables it), so the native interop test read the Zstd dataset that the wasm build refuses. The fixture now records its values plus the error the wasm build must give; the native test accepts either, the Node test of the real wasm package still requires the error. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
144 lines
6.4 KiB
JavaScript
144 lines
6.4 KiB
JavaScript
// Node test of the built wasm package (the exact pkg/ the viewer page loads)
|
|
// and the viewer's DOM-free helpers. Run by test/run.sh:
|
|
// node test.mjs PKG_DIR FIXTURE_DIR
|
|
// FIXTURE_DIR holds fixture.h5, fixture.nc and expected.json from
|
|
// make_fixture.py (values as libhdf5 reads them back).
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const [pkgDir, fixDir] = process.argv.slice(2);
|
|
const pkg = await import(pathToFileURL(join(pkgDir, "clawhdf5_wasm.js")));
|
|
pkg.initSync({ module: readFileSync(join(pkgDir, "clawhdf5_wasm_bg.wasm")) });
|
|
const lib = await import(pathToFileURL(join(import.meta.dirname, "..", "viewer-lib.js")));
|
|
|
|
let checks = 0;
|
|
const eq = (a, b, msg) => { assert.deepEqual(a, b, msg); checks++; };
|
|
|
|
const ARRAY_TYPES = {
|
|
f32: Float32Array, f64: Float64Array, i8: Int8Array, i16: Int16Array, i32: Int32Array,
|
|
i64: BigInt64Array, u8: Uint8Array, u16: Uint16Array, u32: Uint32Array, u64: BigUint64Array,
|
|
strings: Array,
|
|
};
|
|
|
|
function values(kind, data) {
|
|
const arr = Array.from(data);
|
|
if (kind === "f32" || kind === "f64" || kind === "strings") return arr;
|
|
return arr.map(String);
|
|
}
|
|
|
|
function checkAttr(ctx, a, want) {
|
|
const v = a.value;
|
|
if ("raw" in want) {
|
|
eq(v, null, ctx);
|
|
assert.ok(a.dtype.includes(want.raw), `${ctx}: ${a.dtype}`);
|
|
return;
|
|
}
|
|
eq(a.dtype, null, `${ctx} dtype`);
|
|
if ("string" in want) return eq(v, want.string, ctx);
|
|
if ("strings" in want) return eq(v, want.strings, ctx);
|
|
if ("int" in want) {
|
|
if (want.scalar) {
|
|
assert.ok(typeof v === "number" || typeof v === "bigint", ctx);
|
|
return eq([String(v)], want.int, ctx);
|
|
}
|
|
assert.ok(v instanceof BigInt64Array || v instanceof BigUint64Array, ctx);
|
|
return eq(Array.from(v, String), want.int, ctx);
|
|
}
|
|
if ("float" in want) {
|
|
if (want.scalar) return eq([v], want.float, ctx);
|
|
assert.ok(v instanceof Float64Array, ctx);
|
|
return eq(Array.from(v), want.float, ctx);
|
|
}
|
|
assert.fail(`${ctx}: unknown expectation ${JSON.stringify(want)}`);
|
|
}
|
|
|
|
const expected = JSON.parse(readFileSync(join(fixDir, "expected.json"), "utf8"));
|
|
for (const [name, exp] of Object.entries(expected)) {
|
|
const file = pkg.open(new Uint8Array(readFileSync(join(fixDir, name))));
|
|
|
|
for (const [path, want] of Object.entries(exp.lists)) {
|
|
eq(file.kind(path), "group", `${name}:${path} kind`);
|
|
const list = file.list(path);
|
|
for (const [kind, key] of [["group", "groups"], ["dataset", "datasets"]]) {
|
|
eq(list.filter((c) => c.kind === kind).map((c) => c.name).sort(), want[key], `${name}:${path} ${key}`);
|
|
}
|
|
}
|
|
|
|
for (const [path, want] of Object.entries(exp.datasets)) {
|
|
const ctx = `${name}:${path}`;
|
|
eq(file.kind(path), "dataset", `${ctx} kind`);
|
|
if (want.unavailable) {
|
|
// The wasm build has no zstd (it links C): a clear error, no data.
|
|
assert.throws(() => file.read(path), (e) => e.message.includes(want.unavailable), ctx);
|
|
checks++;
|
|
continue;
|
|
}
|
|
const info = file.info(path);
|
|
eq([...info.shape, ...info.elementShape], want.shape, `${ctx} info shape`);
|
|
const r = file.read(path);
|
|
eq(r.shape, want.shape, `${ctx} shape`);
|
|
eq(r.dtype, info.dtype, `${ctx} dtype`);
|
|
assert.ok(r.data instanceof ARRAY_TYPES[want.kind], `${ctx}: ${r.data.constructor.name} for ${want.kind}`);
|
|
eq(values(want.kind, r.data), want.values, ctx);
|
|
if (want.slab) {
|
|
const s = want.slab;
|
|
const part = file.readHyperslab(path, s.start, s.count, s.stride);
|
|
eq(part.shape, s.shape, `${ctx} slab shape`);
|
|
eq(values(want.kind, part.data), s.values, `${ctx} slab`);
|
|
}
|
|
}
|
|
|
|
for (const [path, what] of Object.entries(exp.errors)) {
|
|
assert.throws(() => file.read(path), (e) => e instanceof Error && e.message.includes(what), `${name}:${path}`);
|
|
checks++;
|
|
}
|
|
|
|
for (const [path, want] of Object.entries(exp.attrs)) {
|
|
const attrs = file.attrs(path);
|
|
eq(file.attrErrors(path), [], `${name}:${path} attr errors`);
|
|
const seen = attrs.filter((a) => !a.name.startsWith("_") && !exp.skip_attrs.includes(a.name));
|
|
eq(seen.map((a) => a.name).sort(), Object.keys(want).sort(), `${name}:${path} attr names`);
|
|
for (const a of seen) checkAttr(`${name}:${path}@${a.name}`, a, want[a.name]);
|
|
}
|
|
file.free();
|
|
}
|
|
|
|
// Errors reach JavaScript as thrown Errors, never as data.
|
|
const h5 = pkg.open(new Uint8Array(readFileSync(join(fixDir, "fixture.h5"))));
|
|
const throwsMsg = (fn, re) => { assert.throws(fn, (e) => e instanceof Error && re.test(e.message)); checks++; };
|
|
throwsMsg(() => pkg.open(new Uint8Array(64)), /./);
|
|
throwsMsg(() => h5.read("/nope"), /./);
|
|
throwsMsg(() => h5.list("/grid"), /not a group/);
|
|
throwsMsg(() => h5.readHyperslab("/grid", [0], [1]), /dimensions/);
|
|
throwsMsg(() => h5.readHyperslab("/grid", [5, 0], [2, 1]), /exceeds/);
|
|
throwsMsg(() => h5.readHyperslab("/grid", [-1, 0], [1, 1]), /non-negative integers/);
|
|
throwsMsg(() => h5.readHyperslab("/grid", [0.5, 0], [1, 1]), /non-negative integers/);
|
|
// Info for a dataset with an unlimited dimension (netCDF "time").
|
|
const nc = pkg.open(new Uint8Array(readFileSync(join(fixDir, "fixture.nc"))));
|
|
eq(nc.info("/time").maxshape, [null], "unlimited dimension is null");
|
|
// Big integers stay exact.
|
|
eq(h5.read("/u64").data[0], 18446744073709551615n, "u64 max");
|
|
eq(typeof pkg.version(), "string", "version");
|
|
|
|
// Viewer helpers.
|
|
eq(lib.joinPath("/", "a"), "/a", "joinPath root");
|
|
eq(lib.joinPath("/a", "b"), "/a/b", "joinPath nested");
|
|
eq(lib.viewWindow([], {}), null, "scalar window");
|
|
eq(lib.viewWindow([7], { row: 5, rows: 50 }), { start: [5], count: [2], rows: 2, cols: 1, row: 5, col: 0 }, "1-D window");
|
|
const w = lib.viewWindow([2, 5, 6], { row: 1, col: 4, rows: 3, cols: 5, fixed: [1] });
|
|
eq(w, { start: [1, 1, 4], count: [1, 3, 2], rows: 3, cols: 2, row: 1, col: 4 }, "3-D window");
|
|
// The window the page would request reads the same values as a direct slab.
|
|
const cube = h5.readHyperslab("/cube", w.start, w.count);
|
|
eq(lib.toRows(cube.data, w.rows, w.cols), [["40", "41"], ["46", "47"], ["52", "53"]], "cube window cells");
|
|
const pairs = h5.readHyperslab("/pairs", [1], [2]);
|
|
eq(lib.toRows(pairs.data, 2, 1, lib.perElement([2])), [["[2, 3]"], ["[4, 5]"]], "array-type cells");
|
|
eq(lib.formatValue(0.1 + 0.2), "0.3", "float formatting");
|
|
eq(lib.formatValue(2n ** 64n - 1n), "18446744073709551615", "bigint formatting");
|
|
eq(lib.formatValue("x"), '"x"', "string formatting");
|
|
h5.free();
|
|
nc.free();
|
|
|
|
console.log(`wasm package: ${checks} checks passed`);
|