diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index e33c449..8c686f3 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -21,7 +21,7 @@ jobs: uses: mymindstorm/setup-emsdk@v14 with: # Make sure to set a version number! - version: 3.1.68 + version: 4.0.23 # This is the name of the cache folder. # The cache folder will be placed in the build directory, # so make sure it doesn't conflict with anything! diff --git a/CMakeLists.txt b/CMakeLists.txt index 57f493e..1410794 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,13 @@ project(H5WASM LANGUAGES CXX C ) -set (BASE_URL "https://github.com/usnistgov/libhdf5-wasm/releases/download/v0.5.0_3.1.68" CACHE STRING "") +set (BASE_URL "https://github.com/usnistgov/libhdf5-wasm/releases/download/v0.6.0_4.0.23" CACHE STRING "") # set (BASE_URL "$ENV{HOME}/dev/libhdf5-wasm" CACHE STRING "") FetchContent_Declare( libhdf5-wasm - URL ${BASE_URL}/HDF5-1.14.6-Emscripten.tar.gz - URL_HASH SHA256=90c26256f3d1fd0b421b7e0defebe1894f179b2a50252aeb059aaa1fa6bdb7e9 + URL ${BASE_URL}/HDF5-2.0.0-Emscripten.tar.gz + URL_HASH SHA256=d4dc6719ef164679728d9a50d6a4d97c826f563e22a40c4dec63e485b29781be ) if (NOT libhdf5-wasm_POPULATED) FetchContent_MakeAvailable(libhdf5-wasm) @@ -22,7 +22,7 @@ set(HDF5_DIR ${libhdf5-wasm_SOURCE_DIR}/cmake) find_package(HDF5 REQUIRED CONFIG) add_executable(hdf5_util src/hdf5_util.cc) -target_link_libraries(hdf5_util hdf5_hl-static) +target_link_libraries(hdf5_util hdf5::hdf5_hl-static) set (EXPORTED_FUNCTIONS) list (APPEND EXPORTED_FUNCTIONS @@ -36,6 +36,7 @@ list (APPEND EXPORTED_FUNCTIONS pthread_create pthread_mutex_lock pthread_mutex_unlock pthread_atfork pthread_once pthread_cond_init pthread_barrier_init pthread_attr_init pthread_attr_setdetachstate H5Pget_chunk H5Tget_class H5Sget_simple_extent_dims + H5_libinit_g H5_libterm_g H5Pget_filter_by_id2 H5Pmodify_filter H5Pexist H5Tget_size H5Tget_native_type H5Tget_order H5Epush2 siprintf getTempRet0 __wasm_setjmp @@ -67,7 +68,7 @@ set_target_properties(hdf5_util PROPERTIES -s SINGLE_FILE \ -s EXPORT_ES6=1 \ -s FORCE_FILESYSTEM=1 \ - -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString']\" \ + -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString', 'HEAPU8']\" \ -s EXPORTED_FUNCTIONS=\"${EXPORTED_FUNCTIONS_STRING}\"" RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist/esm RUNTIME_OUTPUT_NAME hdf5_util @@ -75,7 +76,7 @@ set_target_properties(hdf5_util PROPERTIES ) add_executable(hdf5_util_node src/hdf5_util.cc) -target_link_libraries(hdf5_util_node hdf5_hl-static) +target_link_libraries(hdf5_util_node hdf5::hdf5_hl-static) set_target_properties(hdf5_util_node PROPERTIES LINK_FLAGS "-O2 --bind \ -s MAIN_MODULE=2 \ @@ -92,7 +93,7 @@ set_target_properties(hdf5_util_node PROPERTIES -s SINGLE_FILE \ -s EXPORT_ES6=1 \ -s ASSERTIONS=1 \ - -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString']\" \ + -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString', 'HEAPU8']\" \ -s EXPORTED_FUNCTIONS=\"${EXPORTED_FUNCTIONS_STRING}\"" RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist/node RUNTIME_OUTPUT_NAME hdf5_util diff --git a/README.md b/README.md index 1efcff4..362470d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ a zero-dependency WebAssembly-powered library for [reading](#reading) and [writi (built on the [HDF5 C API](http://portal.hdfgroup.org/pages/viewpage.action?pageId=50073943)) The built binaries (esm and node) will be attached to the [latest release](https://github.com/usnistgov/h5wasm/releases/latest/) as h5wasm-{version}.tgz - + The wasm-compiled libraries `libhdf5.a`, `libhdf5_cpp.a` ... and the related `include/` folder are retrieved from [libhdf5-wasm](https://github.com/usnistgov/libhdf5-wasm) during the build. Instead of importing a namespace "*", it is now possible to import the important h5wasm components in an object, from the default export: @@ -14,6 +14,8 @@ export const h5wasm = { File, Group, Dataset, + Datatype, + DatasetRegion, ready, ACCESS_MODES } @@ -21,9 +23,29 @@ export const h5wasm = { The Emscripten filesystem is important for operations, and it can be accessed after the WASM is loaded as below. +## Contents + +- [QuickStart](#quickstart) + - [Browser (no-build)](#browser-no-build) + - [Worker usage](#worker-usage) + - [Browser target (build system)](#browser-target-build-system) + - [Node.js](#nodejs) +- [User Guide](#user-guide) + - [Opening a file](#opening-a-file) + - [Reading](#reading) + - [Writing](#writing) + - [Editing](#editing) + - [SWMR (single writer multiple readers)](#swmr-single-writer-multiple-readers) + - [Links](#links) + - [Library version bounds (libver)](#library-version-bounds-libver) +- [Web Helpers](#web-helpers) +- [Persistent file store (web)](#persistent-file-store-web) + +# QuickStart + ## Browser (no-build) ```js -import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0latest/dist/esm/hdf5_hl.js"; +import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@latest/dist/esm/hdf5_hl.js"; // the WASM loads asychronously, and you can get the module like this: const Module = await h5wasm.ready; @@ -31,7 +53,7 @@ const Module = await h5wasm.ready; // then you can get the FileSystem object from the Module: const { FS } = Module; -// Or, you can directly get the FS if you don't care about the rest +// Or, you can directly get the FS if you don't care about the rest // of the module: // const { FS } = await h5wasm.ready; @@ -45,13 +67,13 @@ let f = new h5wasm.File("sans59510.nxs.ngv", "r"); // File {path: "/", file_id: 72057594037927936n, filename: "data.h5", mode: "r"} ``` -### Worker usage +## Worker usage Since ESM is not supported in all web worker contexts (e.g. Firefox), an additional ```./dist/iife/h5wasm.js``` is provided in the package for `h5wasm>=0.4.8`; it can be loaded in a worker and used as in the example below (which uses the WORKERFS file system for random access on local files): ```js // worker.js onmessage = async function(e) { const { FS } = await h5wasm.ready; - + // send in a file opened from an const f_in = e.data[0]; @@ -85,7 +107,7 @@ The host filesystem is made available through Emscripten "NODERAWFS=1". Enabling BigInt support may be required for nodejs < 16 ```bash npm i h5wasm -node --experimental-wasm-bigint +node ``` @@ -100,12 +122,37 @@ File { file_id: 72057594037927936n, filename: '/home/brian/Downloads/sans59510.nxs.ngv', mode: 'r' -} +} */ ``` -## Usage +# User Guide _(all examples are written in ESM - for Typescript some type casting is probably required, as `get` returns either Group or Dataset)_ + +## Opening a file + +```js +new h5wasm.File(filename, mode?, options?) +``` + +| Argument | Type | Default | Description | +|---|---|---|---| +| `filename` | `string` | — | Path to the HDF5 file | +| `mode` | `string` | `"r"` | Access mode (see table below) | +| `options.track_order` | `boolean` | `false` | Preserve insertion order of groups and attributes | +| `options.libver` | `string \| [string, string]` | — | Library version bound(s) for new objects (see [libver](#library-version-bounds-libver)) | + +Available modes: + +| Mode | Description | +|---|---| +| `"r"` | Read-only | +| `"a"` | Read/write (file must exist) | +| `"w"` | Create / truncate | +| `"x"` | Create, fail if exists | +| `"Sw"` | SWMR write | +| `"Sr"` | SWMR read | + ### Reading ```js let f = new h5wasm.File("sans59510.nxs.ngv", "r"); @@ -121,7 +168,7 @@ let data = f.get("entry/instrument/detector_MR/data") // Dataset {path: "/entry/instrument/detector_MR/data", file_id: 72057594037927936n} data.metadata -/* +/* { "signed": true, "vlen": false, @@ -187,19 +234,6 @@ data.to_array() */ ``` -### SWMR Read -(single writer multiple readers) -```js -const swmr_file = new h5wasm.File("swmr.h5", "Sr"); -let dset = swmr_file.get("data"); -dset.shape; -// 12 -// ...later -dset.refresh(); -dset.shape; -// 16 -``` - ### Writing ```js let new_file = new h5wasm.File("myfile.h5", "w"); @@ -223,7 +257,7 @@ new_file.get("entry/data").value //Float32Array(4) [3.0999999046325684, 4.099999904632568, 0, -1] // create a dataset with shape=[2,2] -// The dataset stored in the HDF5 file with the correct shape, +// The dataset stored in the HDF5 file with the correct shape, // but no attempt is made to make a 2x2 array out of it in javascript new_file.get("entry").create_dataset({name: "square_data", data: [3.1, 4.1, 0.0, -1.0], shape: [2,2], dtype: ' max_memory) { @@ -897,18 +934,61 @@ export class Group extends HasAttrs { } } +export interface FileOptions { + track_order?: boolean; + libver?: LIBVER_BOUNDS; +} + export class File extends Group { filename: string; mode: ACCESS_MODESTRING; - constructor(filename: string, mode: ACCESS_MODESTRING = "r", track_order: boolean = false) { + + constructor(filename: string, mode?: ACCESS_MODESTRING, options?: FileOptions); + /** @deprecated Use the `FileOptions` object overload instead: `new File(filename, mode, { track_order })` */ + constructor(filename: string, mode?: ACCESS_MODESTRING, track_order?: boolean); + constructor( + filename: string, + mode: ACCESS_MODESTRING = "r", + optionsOrTrackOrder: FileOptions | boolean = false + ) { + let track_order: boolean; + let libver: LIBVER_BOUNDS | undefined; + if (optionsOrTrackOrder !== null && typeof optionsOrTrackOrder === "object") { + track_order = optionsOrTrackOrder.track_order ?? false; + libver = optionsOrTrackOrder.libver; + } else { + track_order = (optionsOrTrackOrder as boolean) ?? false; + } const access_mode = ACCESS_MODES[mode]; const h5_mode = Module[access_mode]; - const file_id = Module.open(filename, h5_mode, track_order); + + // Parse libver into numeric bounds + let libver_low = -1; + let libver_high = -1; + + if (libver) { + if (Array.isArray(libver)) { + // Tuple: [low, high] + libver_low = parseLibverString(libver[0]); + libver_high = parseLibverString(libver[1]); + } else { + // Single string: both bounds set to same value + const ver = parseLibverString(libver); + libver_low = ver; + libver_high = ver; + } + } + const file_id = Module.open(filename, h5_mode, track_order, libver_low, libver_high); super(file_id, "/"); this.filename = filename; this.mode = mode; } + get libver(): [LIBVER_BOUND, LIBVER_BOUND] { + const bounds = Module.get_libver_bounds(this.file_id); + return [convertToLibverString(bounds.low), convertToLibverString(bounds.high)]; + } + flush() { Module.flush(this.file_id); } diff --git a/src/hdf5_util.cc b/src/hdf5_util.cc index 32f877e..6ec1d4f 100644 --- a/src/hdf5_util.cc +++ b/src/hdf5_util.cc @@ -28,11 +28,23 @@ EM_JS(void, throw_error, (const char *string_error), { // // pass // } -int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_RDONLY, bool track_order = false) +int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_RDONLY, bool track_order = false, int libver_low = -1, int libver_high = -1) { const char *filename = filename_string.c_str(); hid_t file_id; hid_t fcpl_id = H5Pcreate(H5P_FILE_CREATE); + hid_t fapl_id = H5Pcreate(H5P_FILE_ACCESS); + + // SWMR_WRITE requires RDWR, SWMR_READ works with RDONLY (which is 0) + if (h5_mode & H5F_ACC_SWMR_WRITE) + { + h5_mode |= H5F_ACC_RDWR; + } + + if (libver_low >= 0 && libver_high >= 0) + { + H5Pset_libver_bounds(fapl_id, (H5F_libver_t)libver_low, (H5F_libver_t)libver_high); + } if (track_order) { @@ -42,14 +54,15 @@ int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_ if (h5_mode == H5F_ACC_TRUNC || h5_mode == H5F_ACC_EXCL) { - file_id = H5Fcreate(filename, h5_mode, fcpl_id, H5P_DEFAULT); + file_id = H5Fcreate(filename, h5_mode, fcpl_id, fapl_id); } else { // then it is an existing file... - file_id = H5Fopen(filename, h5_mode, H5P_DEFAULT); + file_id = H5Fopen(filename, h5_mode, fapl_id); } herr_t status = H5Pclose(fcpl_id); + status = H5Pclose(fapl_id); return (int64_t)file_id; } @@ -59,6 +72,29 @@ int close_file(hid_t file_id) return (int)status; } +val get_libver_bounds(hid_t file_id) +{ + hid_t fapl_id = H5Fget_access_plist(file_id); + if (fapl_id < 0) { + throw_error("Failed to get file access property list"); + return val::null(); + } + + H5F_libver_t low, high; + herr_t status = H5Pget_libver_bounds(fapl_id, &low, &high); + H5Pclose(fapl_id); + + if (status < 0) { + throw_error("Failed to get libver bounds"); + return val::null(); + } + + val result = val::object(); + result.set("low", (int)low); + result.set("high", (int)high); + return result; +} + herr_t link_name_callback(hid_t loc_id, const char *name, const H5L_info_t *linfo, void *opdata) { std::vector *namelist = reinterpret_cast *>(opdata); @@ -648,7 +684,7 @@ int read_write_dataset_data(hid_t loc_id, const std::string& dataset_name_string else { status = H5Dread(ds_id, memtype, memspace, dspace, H5P_DEFAULT, rwdata); } - + H5Dclose(ds_id); H5Sclose(dspace); H5Sclose(memspace); @@ -949,7 +985,7 @@ int create_dataset(hid_t loc_id, std::string dset_name_string, uint64_t wdata_ui int create_vlen_str_dataset(hid_t loc_id, std::string dset_name_string, val data, val dims_in, val maxdims_in, val chunks_in, int dtype, int dsize, bool is_signed, bool is_vlstr, bool track_order=false) { uint64_t wdata_uint64; // ptr as uint64_t (webassembly will be 64-bit someday) - + std::vector data_string_vec = vecFromJSArray(data); std::vector data_char_vec; data_char_vec.reserve(data_string_vec.size()); @@ -1246,8 +1282,8 @@ val get_region_metadata(hid_t loc_id, const val ref_data_in) shape = val::array(); // elements of type hsize_t for (int d = 0; d < rank; d++) { - hsize_t blocksize = (block.at(d) == NULL) ? 1 : block.at(d); - shape.set(d, (double)(count.at(d) * blocksize)); + hsize_t blocksize = (block.at(d) == NULL) ? 1 : block.at(d); + shape.set(d, (double)(count.at(d) * blocksize)); } } metadata.set("shape", shape); @@ -1290,7 +1326,7 @@ int get_region_data(hid_t loc_id, val ref_data_in, uint64_t rdata_uint64) htri_t success = H5Sget_regular_hyperslab(dspace, nullptr, nullptr, count.data(), block.data()); for (int d = 0; d < rank; d++) { - int blocksize = (block.at(d) == NULL) ? 1 : block.at(d); + int blocksize = (block.at(d) == NULL) ? 1 : block.at(d); shape_out.at(d) = (count.at(d) * blocksize); } memspace = H5Screate_simple(shape_out.size(), &shape_out[0], nullptr); @@ -1343,6 +1379,7 @@ EMSCRIPTEN_BINDINGS(hdf5) { function("open", &open); function("close_file", &close_file); + function("get_libver_bounds", &get_libver_bounds); function("get_keys", &get_keys_vector); function("get_names", &get_child_names); function("get_types", &get_child_types); @@ -1424,9 +1461,17 @@ EMSCRIPTEN_BINDINGS(hdf5) constant("H5F_ACC_EXCL", H5F_ACC_EXCL); constant("H5F_ACC_CREAT", H5F_ACC_CREAT); constant("H5F_ACC_SWMR_WRITE", H5F_ACC_SWMR_WRITE); - constant("H5F_ACC_SWMR_READ", H5F_ACC_SWMR_READ); + // Library version bounds + constant("H5F_LIBVER_EARLIEST", (int)H5F_LIBVER_EARLIEST); + constant("H5F_LIBVER_V18", (int)H5F_LIBVER_V18); + constant("H5F_LIBVER_V110", (int)H5F_LIBVER_V110); + constant("H5F_LIBVER_V112", (int)H5F_LIBVER_V112); + constant("H5F_LIBVER_V114", (int)H5F_LIBVER_V114); + constant("H5F_LIBVER_V200", (int)H5F_LIBVER_V200); + constant("H5F_LIBVER_LATEST", (int)H5F_LIBVER_LATEST); + constant("H5G_GROUP", (int)H5G_GROUP); // 0 Object is a group. constant("H5G_DATASET", (int)H5G_DATASET); // 1 Object is a dataset. constant("H5G_TYPE", (int)H5G_TYPE); // 2 Object is a named datatype. diff --git a/src/hdf5_util_helpers.d.ts b/src/hdf5_util_helpers.d.ts index 5322238..3777a2e 100644 --- a/src/hdf5_util_helpers.d.ts +++ b/src/hdf5_util_helpers.d.ts @@ -59,8 +59,9 @@ export interface VirtualSource { } export interface H5Module extends EmscriptenModule { - open(filename: string, mode?: number, track_order?: boolean): bigint; + open(filename: string, mode?: number, track_order?: boolean, libver_low?: number, libver_high?: number): bigint; close_file(file_id: bigint): number; + get_libver_bounds(file_id: bigint): {low: number, high: number}; create_dataset(file_id: bigint, arg1: string, arg2: bigint, shape: bigint[], maxshape: (bigint | null)[], chunks: bigint[] | null, type: number, size: number, signed: boolean, vlen: boolean, compression_id: number, compression_opts: number[], track_order?: boolean): number; create_soft_link(file_id: bigint, link_target: string, link_name: string): number; create_hard_link(file_id: bigint, link_target: string, link_name: string): number; @@ -84,6 +85,13 @@ export interface H5Module extends EmscriptenModule { H5F_ACC_CREAT: 16; H5F_ACC_SWMR_WRITE: 32; H5F_ACC_SWMR_READ: 64; + H5F_LIBVER_EARLIEST: number; + H5F_LIBVER_V18: number; + H5F_LIBVER_V110: number; + H5F_LIBVER_V112: number; + H5F_LIBVER_V114: number; + H5F_LIBVER_V200: number; + H5F_LIBVER_LATEST: number; H5Z_FILTER_NONE: 0; H5Z_FILTER_DEFLATE: 1; H5Z_FILTER_SHUFFLE: 2; @@ -139,7 +147,7 @@ export interface H5Module extends EmscriptenModule { } export declare type Filter = { - id: number; + id: number; name: string; cd_values: number[]; } diff --git a/test/libver_test.mjs b/test/libver_test.mjs new file mode 100644 index 0000000..9f0c42a --- /dev/null +++ b/test/libver_test.mjs @@ -0,0 +1,303 @@ +#!/usr/bin/env node + +import { strict as assert } from 'assert'; +import { existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { h5wasm, convertToLibverString } from "h5wasm/node"; + +async function test_libver_v110() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_v110.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with libver="v110" + const f = new h5wasm.File(FILEPATH, "w", { libver: "v110" }); + f.create_dataset({name: "data", data: DATA}); + f.flush(); + f.close(); + + // Verify we can read it back + const f_read = new h5wasm.File(FILEPATH, "r"); + const dset = f_read.get("data"); + assert.deepEqual([...dset.value], [...DATA]); + f_read.close(); +} + +async function test_libver_latest() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_latest.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with libver="latest" + const f = new h5wasm.File(FILEPATH, "w", { libver: "latest" }); + f.create_dataset({name: "data", data: DATA}); + f.flush(); + f.close(); + + // Verify we can read it back + const f_read = new h5wasm.File(FILEPATH, "r"); + const dset = f_read.get("data"); + assert.deepEqual([...dset.value], [...DATA]); + f_read.close(); +} + +async function test_libver_v108() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_v18.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with libver="v108" + const f = new h5wasm.File(FILEPATH, "w", { libver: "v108" }); + f.create_dataset({name: "data", data: DATA}); + f.flush(); + f.close(); + + // Verify we can read it back + const f_read = new h5wasm.File(FILEPATH, "r"); + const dset = f_read.get("data"); + assert.deepEqual([...dset.value], [...DATA]); + f_read.close(); +} + +async function test_libver_asymmetric() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_asymmetric.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with asymmetric libver bounds + const f = new h5wasm.File(FILEPATH, "w", { libver: ["v110", "latest"] }); + f.create_dataset({name: "data", data: DATA}); + f.flush(); + f.close(); + + // Verify we can read it back + const f_read = new h5wasm.File(FILEPATH, "r"); + const dset = f_read.get("data"); + assert.deepEqual([...dset.value], [...DATA]); + f_read.close(); +} + +async function test_libver_swmr() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_swmr.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with superblock v3 (required for SWMR) + const f = new h5wasm.File(FILEPATH, "w", { libver: "v110" }); + // Create an extensible chunked dataset (required for SWMR) + f.create_dataset({ + name: "swmr_data", + data: DATA, + maxshape: [null], + chunks: [10] + }); + f.flush(); + f.close(); + + // Verify we can open in SWMR write mode + const f_swmr = new h5wasm.File(FILEPATH, "Sw"); + const dset = f_swmr.get("swmr_data"); + assert.deepEqual([...dset.value], [...DATA]); + f_swmr.close(); +} + +async function test_libver_with_track_order() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_track_order.h5"); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with track_order and explicit libver + const f = new h5wasm.File(FILEPATH, "w", { track_order: true, libver: "latest" }); + + // Create attributes in reverse alphabetical order + f.create_attribute("c", "first"); + f.create_attribute("b", "second"); + f.create_attribute("a", "third"); + + f.flush(); + f.close(); + + // Verify order is preserved + const f_read = new h5wasm.File(FILEPATH, "r"); + assert.deepEqual(Object.keys(f_read.attrs), ["c", "b", "a"]); + f_read.close(); +} + +async function test_libver_auto_with_track_order() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_auto.h5"); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with track_order but no explicit libver (should auto-set v18) + const f = new h5wasm.File(FILEPATH, "w", { track_order: true }); + + // Create attributes in reverse alphabetical order + f.create_attribute("c", "first"); + f.create_attribute("b", "second"); + f.create_attribute("a", "third"); + + f.flush(); + f.close(); + + // Verify order is preserved (auto-set libver should work) + const f_read = new h5wasm.File(FILEPATH, "r"); + assert.deepEqual(Object.keys(f_read.attrs), ["c", "b", "a"]); + f_read.close(); +} + +async function test_libver_case_insensitive() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_case.h5"); + const DATA = new Float32Array([1.0, 2.0, 3.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Test case-insensitive libver strings (uppercase/mixed case) + const f = new h5wasm.File(FILEPATH, "w", { libver: "LATEST" }); + f.create_dataset({name: "data", data: DATA}); + f.flush(); + f.close(); + + const f_read = new h5wasm.File(FILEPATH, "r"); + const dset = f_read.get("data"); + assert.deepEqual([...dset.value], [...DATA]); + f_read.close(); +} + +async function test_libver_constants() { + const Module = await h5wasm.ready; + + // Verify H5F_LIBVER constants are exported + assert.ok(typeof Module.H5F_LIBVER_EARLIEST === 'number'); + assert.ok(typeof Module.H5F_LIBVER_V18 === 'number'); + assert.ok(typeof Module.H5F_LIBVER_V110 === 'number'); + assert.ok(typeof Module.H5F_LIBVER_V112 === 'number'); + assert.ok(typeof Module.H5F_LIBVER_V114 === 'number'); + assert.ok(typeof Module.H5F_LIBVER_V200 === 'number'); + assert.ok(typeof Module.H5F_LIBVER_LATEST === 'number'); +} + +async function test_libver_getter() { + const Module = await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "libver_getter.h5"); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // look up latest as real version, e.g. "v200" + const latest = convertToLibverString(Module.H5F_LIBVER_LATEST); + + // Test single libver value + const f1 = new h5wasm.File(FILEPATH, "w", { libver: "v108" }); + assert.deepEqual(f1.libver, ["v108", "v108"]); + f1.close(); + + // Test asymmetric libver bounds + const f2 = new h5wasm.File(FILEPATH, "w", { libver: ["v110", "latest"] }); + assert.deepEqual(f2.libver, ["v110", latest]); + // On close, because we didn't use any features that require v110, + // it falls back to "v108" for the low bound + f2.close(); + + // Test reading libver from existing file.. + const f3 = new h5wasm.File(FILEPATH, "r"); + assert.deepEqual(f3.libver, ["v108", latest]); + f3.close(); + + // Open a file in SWMR read/write mode: + const f4 = new h5wasm.File(FILEPATH, "Sw"); + // If no libver is specified, hdf5 library will set lower bound + // to "v110" for SWMR compatibility + assert.deepEqual(f4.libver, ["v110", latest]); + f4.close(); + + // Open a file with track_order enabled: + const f5 = new h5wasm.File(FILEPATH, "w", { track_order: true }); + // If no libver is specified, and track_order is used, + // a minimum version "v108" is set by hdf5 library + assert.deepEqual(f5.libver, ["v108", latest]); + f5.close(); +} + +export const tests = [ + { + description: "Create file with libver='v110'", + test: test_libver_v110 + }, + { + description: "Create file with libver='latest'", + test: test_libver_latest + }, + { + description: "Create file with libver='v108'", + test: test_libver_v108 + }, + { + description: "Create file with asymmetric libver bounds", + test: test_libver_asymmetric + }, + { + description: "Create SWMR-compatible file and open in SWMR mode", + test: test_libver_swmr + }, + { + description: "Create file with track_order and explicit libver", + test: test_libver_with_track_order + }, + { + description: "Create file with track_order, auto-set libver", + test: test_libver_auto_with_track_order + }, + { + description: "Test case-insensitive libver parsing", + test: test_libver_case_insensitive + }, + { + description: "Verify H5F_LIBVER constants are exported", + test: test_libver_constants + }, + { + description: "Test libver property getter", + test: test_libver_getter + } +]; + +export default tests; diff --git a/test/swmr_test.mjs b/test/swmr_test.mjs new file mode 100644 index 0000000..82166e9 --- /dev/null +++ b/test/swmr_test.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +import { strict as assert } from 'assert'; +import { existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import h5wasm from "h5wasm/node"; + +async function test_swmr_write_read() { + await h5wasm.ready; + const PATH = join(".", "test", "tmp"); + const FILEPATH = join(PATH, "swmr_test.h5"); + const INITIAL_DATA = new Float32Array([1.0, 2.0, 3.0]); + const APPEND_DATA = new Float32Array([4.0, 5.0, 6.0]); + + if (!(existsSync(PATH))) { + mkdirSync(PATH); + } + + // Create file with SWMR-compatible format (v110 minimum) + const f_create = new h5wasm.File(FILEPATH, "w", { libver: "v110" }); + + // Create an extensible chunked dataset (required for SWMR) + f_create.create_dataset({ + name: "data", + data: INITIAL_DATA, + maxshape: [null], + chunks: [10] + }); + f_create.flush(); + f_create.close(); + + // Open for SWMR write + const f_write = new h5wasm.File(FILEPATH, "Sw"); + + // Open for SWMR read + const f_read = new h5wasm.File(FILEPATH, "Sr"); + + // Verify initial data in both handles + const dset_write = f_write.get("data"); + const dset_read = f_read.get("data"); + + assert.deepEqual([...dset_write.value], [...INITIAL_DATA]); + assert.deepEqual([...dset_read.value], [...INITIAL_DATA]); + + // Append data using write handle + const new_size = INITIAL_DATA.length + APPEND_DATA.length; + dset_write.resize([new_size]); + dset_write.write_slice([[INITIAL_DATA.length, new_size]], APPEND_DATA); + f_write.flush(); + + // Before refresh, read handle should still see old data + assert.equal(dset_read.shape[0], INITIAL_DATA.length); + + // Refresh the dataset in read handle + dset_read.refresh(); + + // After refresh, should see appended data + assert.equal(dset_read.shape[0], new_size); + const all_data = dset_read.value; + const expected = new Float32Array([...INITIAL_DATA, ...APPEND_DATA]); + assert.deepEqual([...all_data], [...expected]); + + // Clean up + f_write.close(); + f_read.close(); +} + +export const tests = [ + { + description: "SWMR: Write and read with refresh", + test: test_swmr_write_read + } +]; + +export default tests; diff --git a/test/test.mjs b/test/test.mjs index 4dcf9d0..52da982 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -20,6 +20,8 @@ import test_throwing_error_handler from './test_throwing_error_handler.mjs'; import test_empty from './empty_dataset_and_attrs.mjs'; import vlen_test from './vlen_test.mjs'; import track_order from './track_order.mjs'; +import libver_test from './libver_test.mjs'; +import swmr_test from './swmr_test.mjs'; let tests = []; const add_tests = (tests_in) => { /*global*/ tests = tests.concat(tests_in)} @@ -43,6 +45,8 @@ add_tests(test_throwing_error_handler); add_tests(test_empty); add_tests(vlen_test); add_tests(track_order); +add_tests(libver_test); +add_tests(swmr_test); let passed = true; async function run_test(test) {