diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 1365d04..5207bec 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -16,39 +16,32 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 with: + toolchain: 1.99.0 target: wasm32-unknown-unknown - - uses: taiki-e/install-action@v2 + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 with: - tool: wasm-pack + version: 6.0.12 - - name: Install Emscripten - run: | - sudo apt-get update - sudo apt-get install --yes emscripten - - - name: Build WASM - run: | - wasm-pack build crates/val-wasm --target web \ - --out-name val \ - --out-dir ../../www/packages/val-wasm \ - --locked - - - name: Check generated types - run: git diff --exit-code -- www/packages/val-wasm/val.d.ts + - name: Setup build tools + uses: taiki-e/install-action@v2 + with: + tool: just@1.58.0,wasm-pack@0.15.0 - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: latest + bun-version: 1.4.3 - name: Install dependencies working-directory: ./www @@ -58,9 +51,11 @@ jobs: working-directory: ./www run: bun run lint - - name: Test - working-directory: ./www - run: bun test --pass-with-no-tests + - name: Build and test WASM + run: just test-wasm + + - name: Check generated types + run: git diff --exit-code -- www/packages/val-wasm/val.d.ts - name: Build working-directory: ./www diff --git a/justfile b/justfile index a1e0f34..0974c60 100644 --- a/justfile +++ b/justfile @@ -19,9 +19,9 @@ build: [group: 'web'] build-wasm: - wasm-pack build crates/val-wasm --target web \ + wasm-pack build crates/val-wasm --target web --release \ --out-name val \ - --out-dir ../../www/packages/val-wasm + --out-dir ../../www/packages/val-wasm --locked [group: 'check'] check: @@ -82,6 +82,10 @@ serve-web: build-wasm test: cargo test +[group: 'test'] +test-wasm: build-wasm + cd www && bun test + [group: 'test'] test-release-workflow: -git tag -d test-release diff --git a/www/tests/wasm.test.ts b/www/tests/wasm.test.ts new file mode 100644 index 0000000..4f51f11 --- /dev/null +++ b/www/tests/wasm.test.ts @@ -0,0 +1,165 @@ +import { expect, test } from 'bun:test'; +import { readFile } from 'node:fs/promises'; +import init, { execute, parse } from 'val-wasm'; + +const wasm = await init({ + module_or_path: await readFile( + new URL('../packages/val-wasm/val_bg.wasm', import.meta.url) + ), +}); + +test.each([ + ['(2 ^ 128 + 1) - 2 ^ 128', '1'], + ['(1 / 3) * 3', '1'], + ['sqrt(4)', '2'], +])('executes arbitrary precision arithmetic: %s', (source, value) => { + expect(execute(source, '')).toEqual({ + errors: [], + exitCode: 0, + output: '', + value, + }); +}); + +test('executes input and output and resets the runtime', () => { + expect( + execute( + 'foo = input("foo"); print(foo); println(input()); input()', + 'bar\r\nbaz\nqux' + ) + ).toEqual({ + errors: [], + exitCode: 0, + output: 'foobarbaz\n', + value: 'qux', + }); + + expect(execute('input()', '')).toEqual({ + errors: [], + exitCode: 0, + output: '', + value: '', + }); +}); + +test.each([ + [ + '"foo', + [ + { + kind: 'Parser', + message: "found end of input expected something else, or '\"'", + range: { start: 4, end: 4 }, + }, + ], + ], + [ + '"😀"; foo', + [ + { + kind: 'Evaluator', + message: 'Undefined variable `foo`', + range: { start: 6, end: 9 }, + }, + ], + ], +])('returns errors with UTF-16 ranges: %s', (source, errors) => { + expect(execute(source, '')).toEqual({ + errors, + exitCode: 1, + output: '', + }); +}); + +test('stops execution at exit and preserves output', () => { + expect(execute('println("foo"); exit(7); println("bar")', '')).toEqual({ + errors: [], + exitCode: 7, + output: 'foo\n', + }); +}); + +test('parses through the generated bindings', () => { + expect(parse('"😀"')).toEqual({ + status: 'ok', + ast: { + kind: 'statements', + range: { start: 0, end: 4 }, + children: [ + { + kind: 'expression', + range: { start: 0, end: 4 }, + children: [ + { + kind: 'string', + range: { start: 0, end: 4 }, + children: [], + }, + ], + }, + ], + }, + }); +}); + +test.each([0, 1, 16, 17])('allocates and frees %i aligned bytes', (size) => { + wasm.free(0); + + const pointer = wasm.malloc(size); + + expect(pointer).not.toBe(0); + expect(pointer % 16).toBe(0); + + new Uint8Array(wasm.memory.buffer, pointer, size).fill(42); + wasm.free(pointer); +}); + +test('reallocates null, grows and shrinks while preserving bytes', () => { + const bytes = new TextEncoder().encode('foobar'); + const pointer = wasm.realloc(0, bytes.length); + + expect(pointer).not.toBe(0); + expect(pointer % 16).toBe(0); + new Uint8Array(wasm.memory.buffer, pointer, bytes.length).set(bytes); + + const grown = wasm.realloc(pointer, 128 * 1024); + + expect(grown).not.toBe(0); + expect(grown % 16).toBe(0); + expect(new Uint8Array(wasm.memory.buffer, grown, bytes.length)).toEqual( + bytes + ); + + const shrunk = wasm.realloc(grown, 3); + + expect(shrunk).not.toBe(0); + expect(shrunk % 16).toBe(0); + expect(new Uint8Array(wasm.memory.buffer, shrunk, 3)).toEqual( + bytes.subarray(0, 3) + ); + + const empty = wasm.realloc(shrunk, 0); + + expect(empty).not.toBe(0); + expect(empty % 16).toBe(0); + wasm.free(empty); +}); + +test.each([0x7fffffe1, 0x7fffffff, 0xfffffff0, 0xffffffff])( + 'rejects allocation size %i without changing existing memory', + (size) => { + const bytes = new TextEncoder().encode('foo'); + const pointer = wasm.malloc(bytes.length); + + expect(pointer).not.toBe(0); + new Uint8Array(wasm.memory.buffer, pointer, bytes.length).set(bytes); + + expect(wasm.malloc(size)).toBe(0); + expect(wasm.realloc(0, size)).toBe(0); + expect(wasm.realloc(pointer, size)).toBe(0); + expect(new Uint8Array(wasm.memory.buffer, pointer, bytes.length)).toEqual( + bytes + ); + wasm.free(pointer); + } +);