// 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`); 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`);