From 153c04aa9cdc1e8d1e498e018eae75aadf332a7b Mon Sep 17 00:00:00 2001 From: Mykhailo Zuiev Date: Tue, 20 Oct 2026 16:31:52 +0200 Subject: [PATCH 1/3] test: Exercise both compiled and fallback writers in schema binary tests Add `writeWithBothWriters` to typegpu-testing-utility. It encodes a value with `writeToArrayBuffer` once with the compiled writer and once with the fallback writer (eval blocked while a fresh TypeGPU instance loads), checks that each path was really used, and fails if the encoded bytes differ. Migrate the write/roundtrip tests in arrayBufferIO, struct, array and vector. The `*.noEval.test.ts` files are left as they are. --- .../src/bothWriters.ts | 211 ++++++++++++++++++ packages/typegpu-testing-utility/src/index.ts | 5 + packages/typegpu/tests/array.test.ts | 40 ++-- packages/typegpu/tests/arrayBufferIO.test.ts | 44 ++-- packages/typegpu/tests/struct.test.ts | 186 ++++++++------- packages/typegpu/tests/vector.test.ts | 85 +++---- 6 files changed, 407 insertions(+), 164 deletions(-) create mode 100644 packages/typegpu-testing-utility/src/bothWriters.ts diff --git a/packages/typegpu-testing-utility/src/bothWriters.ts b/packages/typegpu-testing-utility/src/bothWriters.ts new file mode 100644 index 0000000000..095035d8d9 --- /dev/null +++ b/packages/typegpu-testing-utility/src/bothWriters.ts @@ -0,0 +1,211 @@ +import { expect, vi } from 'vitest'; +import type * as TypeGpu from 'typegpu'; +import type { d } from 'typegpu'; + +type TypeGpuModule = typeof TypeGpu; + +// Captured once, so that restoring can never put back a proxy installed by another call. +const RealFunction = globalThis.Function; + +const TYPEGPU_GLOBAL_PREFIX = '__TYPEGPU_'; + +function snapshotTypegpuGlobals(): Map { + const saved = new Map(); + for (const key of Object.getOwnPropertyNames(globalThis)) { + if (key.startsWith(TYPEGPU_GLOBAL_PREFIX)) { + saved.set(key, (globalThis as Record)[key]); + } + } + return saved; +} + +function restoreTypegpuGlobals(saved: Map): void { + const globals = globalThis as Record; + for (const key of Object.getOwnPropertyNames(globalThis)) { + if (key.startsWith(TYPEGPU_GLOBAL_PREFIX) && !saved.has(key)) { + delete globals[key]; + } + } + for (const [key, value] of saved) { + globals[key] = value; + } +} + +/** + * Replaces the global `Function` constructor with a proxy that counts constructions + * and (optionally) throws, like an environment with a CSP that disallows eval. + */ +function interceptFunctionConstructor(block: boolean): { + count: () => number; + restore: () => void; +} { + let count = 0; + globalThis.Function = new Proxy(RealFunction, { + construct(target, args, newTarget) { + count++; + if (block) { + throw new EvalError('Code generation from strings disallowed (simulated)'); + } + return Reflect.construct(target, args, newTarget); + }, + apply(target, thisArg, args) { + count++; + if (block) { + throw new EvalError('Code generation from strings disallowed (simulated)'); + } + return Reflect.apply(target, thisArg, args); + }, + }); + return { + count: () => count, + restore: () => { + globalThis.Function = RealFunction; + }, + }; +} + +export type BothWritersCase = { + schema: T; + value: d.InferInput; + /** Size of the buffer to write into. Defaults to `sizeOf(schema)`. */ + byteLength?: number; +}; + +export type BothWritersResult = { + /** The encoded bytes, identical for the compiled and the fallback writer. */ + bytes: ArrayBuffer; + /** The value that was encoded. */ + value: d.InferInput; + /** The bytes read back with `readFromArrayBuffer`, by the same TypeGPU instance that created `value`. */ + decoded: d.Infer; + /** + * The TypeGPU instance that created `value` and `decoded`. Use it to create expected values, + * since objects from the statically imported `typegpu` are of different types. + */ + typegpu: TypeGpuModule; +}; + +// The logger prints `console.warn('⚠️ [] ', ...)` +const isFallbackWarning = (args: unknown[]) => String(args[0]).includes('[fallback]'); + +/** + * Collects `[fallback]` warnings instead of printing them. Other warnings are passed to + * whatever `console.warn` was before (possibly a spy of the calling test), and `restore` + * puts that exact function back. + */ +function captureFallbackWarnings(): { warnings: unknown[][]; restore: () => void } { + const previous = console.warn; + const warnings: unknown[][] = []; + console.warn = (...args: unknown[]) => { + if (isFallbackWarning(args)) { + warnings.push(args); + } else { + previous.apply(console, args); + } + }; + return { + warnings, + restore: () => { + console.warn = previous; + }, + }; +} + +async function encodeWithOnePath( + build: (typegpu: TypeGpuModule) => BothWritersCase, + path: 'compiled' | 'fallback', +): Promise> { + const savedGlobals = snapshotTypegpuGlobals(); + const fallbackWarningsCapture = captureFallbackWarnings(); + + try { + // A fresh TypeGPU instance. `EVAL_ALLOWED_IN_ENV` is decided when the instance is + // loaded, so the fallback instance is loaded while `new Function` is blocked. + // Removing the version marker lets the instance load without the "duplicate TypeGPU + // version" warning, which is expected here; `savedGlobals` is restored in `finally`. + delete (globalThis as Record).__TYPEGPU_VERSION__; + vi.resetModules(); + const loadGuard = interceptFunctionConstructor(path === 'fallback'); + let typegpu: TypeGpuModule; + try { + typegpu = await import('typegpu'); + } finally { + loadGuard.restore(); + } + const blockedAtLoad = loadGuard.count(); + + // The schema and the value must come from the same instance as the writer. + const { schema, value, byteLength } = build(typegpu); + const bytes = new ArrayBuffer(byteLength ?? typegpu.d.sizeOf(schema)); + + const writeGuard = interceptFunctionConstructor(false); + try { + typegpu.writeToArrayBuffer(bytes, schema, value); + } finally { + writeGuard.restore(); + } + const functionCallsDuringWrite = writeGuard.count(); + const fallbackWarnings = fallbackWarningsCapture.warnings.length; + + if (path === 'compiled') { + // The writer is generated with `new Function` and no warning says it was abandoned. + expect(functionCallsDuringWrite, 'compiled writer was not generated').toBeGreaterThan(0); + expect(fallbackWarnings, 'compiled writer reported a fallback').toBe(0); + } else { + expect(blockedAtLoad, 'eval was never probed while blocked').toBeGreaterThan(0); + expect(functionCallsDuringWrite, 'fallback run generated code').toBe(0); + expect(fallbackWarnings, 'fallback run did not report the fallback').toBe(1); + } + + const decoded = typegpu.readFromArrayBuffer(bytes, schema) as d.Infer; + return { bytes, value, decoded, typegpu }; + } finally { + fallbackWarningsCapture.restore(); + restoreTypegpuGlobals(savedGlobals); + } +} + +// Tail of the chain of calls, see `writeWithBothWriters`. +let queue: Promise = Promise.resolve(); + +/** + * Encodes a value with `writeToArrayBuffer` twice, with the compiled writer and with the + * fallback writer (used when eval is not allowed), fails if they produce different bytes, + * and returns the result of one of them. + * + * Each path runs against its own freshly loaded TypeGPU instance, and the schema and value + * can only be created inside `build`, from the instance passed to it. Objects created with + * the statically imported `typegpu` do not work with the fresh instances. `build` is called once + * per path, so it should not have side effects. + * + * Only for values that are written by the schema (not raw `ArrayBuffer` / typed array input, + * which bypass the writers). Reading has no compiled implementation, so `decoded` is + * the same for both paths. + * + * @example + * const { bytes } = await writeWithBothWriters(({ d }) => ({ + * schema: d.arrayOf(d.u32, 2), + * value: [1, 2], + * })); + * expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2]); + */ +export function writeWithBothWriters( + build: (typegpu: TypeGpuModule) => BothWritersCase, +): Promise> { + // The module registry and the globals are shared state, so calls never overlap. + const result = queue.then(async () => { + const compiled = await encodeWithOnePath(build, 'compiled'); + const fallback = await encodeWithOnePath(build, 'fallback'); + + expect( + new Uint8Array(fallback.bytes), + 'fallback bytes differ from compiled bytes', + ).toStrictEqual(new Uint8Array(compiled.bytes)); + return compiled; + }); + queue = result.then( + () => undefined, + () => undefined, + ); + return result; +} diff --git a/packages/typegpu-testing-utility/src/index.ts b/packages/typegpu-testing-utility/src/index.ts index 43117daa53..cf8a34aad1 100644 --- a/packages/typegpu-testing-utility/src/index.ts +++ b/packages/typegpu-testing-utility/src/index.ts @@ -39,4 +39,9 @@ export { type MockRenderBundleEncoder, type MockRenderPassEncoder, } from './extendedIt.ts'; +export { + writeWithBothWriters, + type BothWritersCase, + type BothWritersResult, +} from './bothWriters.ts'; export { CAPTURE, captureSnippets, simplifyType } from './capture.ts'; diff --git a/packages/typegpu/tests/array.test.ts b/packages/typegpu/tests/array.test.ts index 1695234df6..243f6793d1 100644 --- a/packages/typegpu/tests/array.test.ts +++ b/packages/typegpu/tests/array.test.ts @@ -1,9 +1,8 @@ import { attest } from '@ark/attest'; import { describe, expect, expectTypeOf } from 'vitest'; import { d, readFromArrayBuffer, tgpu, writeToArrayBuffer } from 'typegpu'; -import type { Infer } from 'typegpu/data'; import { arrayLength } from 'typegpu/std'; -import { it } from 'typegpu-testing-utility'; +import { it, writeWithBothWriters } from 'typegpu-testing-utility'; describe('array', () => { it('produces a visually pleasant type', () => { @@ -16,12 +15,13 @@ describe('array', () => { expect(d.sizeOf(TestArray)).toBe(48); }); - it('aligns array elements when writing', () => { - const TestArray = d.arrayOf(d.vec3u, 3); - const buffer = new ArrayBuffer(d.sizeOf(TestArray)); + it('aligns array elements when writing', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.arrayOf(d.vec3u, 3), + value: [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)], + })); - writeToArrayBuffer(buffer, TestArray, [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)]); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); }); it('aligns array elements when reading', () => { @@ -37,21 +37,19 @@ describe('array', () => { ]); }); - it('encodes and decodes arrays properly', () => { - const TestArray = d.arrayOf(d.vec3f, 5); - - const buffer = new ArrayBuffer(d.sizeOf(TestArray)); - - const value: Infer = [ - d.vec3f(1.5, 2, 3.5), - d.vec3f(), - d.vec3f(-1.5, 2, 3.5), - d.vec3f(1.5, -2, 3.5), - d.vec3f(1.5, 2, 15), - ]; + it('encodes and decodes arrays properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.arrayOf(d.vec3f, 5), + value: [ + d.vec3f(1.5, 2, 3.5), + d.vec3f(), + d.vec3f(-1.5, 2, 3.5), + d.vec3f(1.5, -2, 3.5), + d.vec3f(1.5, 2, 15), + ], + })); - writeToArrayBuffer(buffer, TestArray, value); - expect(readFromArrayBuffer(buffer, TestArray)).toStrictEqual(value); + expect(decoded).toStrictEqual(value); }); it('throws when trying to read/write a runtime-sized array', () => { diff --git a/packages/typegpu/tests/arrayBufferIO.test.ts b/packages/typegpu/tests/arrayBufferIO.test.ts index 5c1b8222e1..a2e7960397 100644 --- a/packages/typegpu/tests/arrayBufferIO.test.ts +++ b/packages/typegpu/tests/arrayBufferIO.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from 'vitest'; import { d, patchArrayBuffer, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu'; -import { it } from 'typegpu-testing-utility'; +import { it, writeWithBothWriters } from 'typegpu-testing-utility'; describe('arrayBufferIO', () => { describe('write', () => { @@ -48,25 +48,28 @@ describe('arrayBufferIO', () => { ]); }); - it('handles structs', () => { - const buffer = new ArrayBuffer(32); - const Boid = d.struct({ pos: d.vec2u, id: d.u32 }); - const Boids = d.arrayOf(Boid, 2); + it('handles structs', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => { + const Boid = d.struct({ pos: d.vec2u, id: d.u32 }); + const Boids = d.arrayOf(Boid, 2); - writeToArrayBuffer(buffer, Boids, [ - Boid({ pos: d.vec2u(1, 2), id: 3 }), - Boid({ pos: d.vec2u(4, 5), id: 6 }), - ]); + return { + schema: Boids, + value: [Boid({ pos: d.vec2u(1, 2), id: 3 }), Boid({ pos: d.vec2u(4, 5), id: 6 })], + }; + }); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0]); }); - it('handles vec3f', () => { - const buffer = new ArrayBuffer(16); + it('handles vec3f', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec3f, + value: [1, 2, 3] as const, + byteLength: 16, + })); - writeToArrayBuffer(buffer, d.vec3f, [1, 2, 3]); - - expect([...new Float32Array(buffer)]).toEqual([1, 2, 3, 0]); + expect([...new Float32Array(bytes)]).toEqual([1, 2, 3, 0]); }); it('does not override 4th element when handling vec3f', () => { @@ -78,12 +81,13 @@ describe('arrayBufferIO', () => { expect([...new Float32Array(buffer)]).toEqual([6, 7, 8, 4]); }); - it('handles mat3x3f', () => { - const buffer = new ArrayBuffer(48); + it('handles mat3x3f', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.mat3x3f, + value: [1, 2, 3, 4, 5, 6, 7, 8, 9] as const, + })); - writeToArrayBuffer(buffer, d.mat3x3f, [1, 2, 3, 4, 5, 6, 7, 8, 9]); - - expect([...new Float32Array(buffer)]).toEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); + expect([...new Float32Array(bytes)]).toEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); }); it('overrides all values when handling mat3x3f', () => { diff --git a/packages/typegpu/tests/struct.test.ts b/packages/typegpu/tests/struct.test.ts index f7e4a6d6c7..16a9965bfa 100644 --- a/packages/typegpu/tests/struct.test.ts +++ b/packages/typegpu/tests/struct.test.ts @@ -16,10 +16,9 @@ import { vec3h, vec3u, } from 'typegpu/data'; -import { readFromArrayBuffer, tgpu, writeToArrayBuffer, d } from 'typegpu'; -import type { Infer } from 'typegpu/data'; +import { readFromArrayBuffer, tgpu, d } from 'typegpu'; import { frexp } from 'typegpu/std'; -import { it } from 'typegpu-testing-utility'; +import { it, writeWithBothWriters } from 'typegpu-testing-utility'; describe('struct', () => { it('aligns struct properties when measuring', () => { @@ -30,16 +29,16 @@ describe('struct', () => { expect(sizeOf(TestStruct)).toBe(32); }); - it('aligns struct properties when writing', () => { - const TestStruct = struct({ - x: u32, - y: vec3u, - }); - - const buffer = new ArrayBuffer(sizeOf(TestStruct)); + it('aligns struct properties when writing', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + x: d.u32, + y: d.vec3u, + }), + value: { x: 1, y: d.vec3u(1, 2, 3) }, + })); - writeToArrayBuffer(buffer, TestStruct, { x: 1, y: vec3u(1, 2, 3) }); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 0, 0, 0, 1, 2, 3, 0]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 0, 0, 0, 1, 2, 3, 0]); }); it('aligns struct properties when reading', () => { @@ -58,39 +57,37 @@ describe('struct', () => { }); }); - it('encodes and decodes structures properly', () => { - const TestStruct = struct({ - a: u32, - b: vec3u, - c: vec3f, - d: f32, - e: i32, - f: struct({ - g: f32, - h: vec3u, - i: vec2u, - j: u32, + it('encodes and decodes structures properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + a: d.u32, + b: d.vec3u, + c: d.vec3f, + d: d.f32, + e: d.i32, + f: d.struct({ + g: d.f32, + h: d.vec3u, + i: d.vec2u, + j: d.u32, + }), }), - }); - - const buffer = new ArrayBuffer(sizeOf(TestStruct)); - - const value: Infer = { - a: 1, - b: vec3u(2, 3, 4), - c: vec3f(6.5, 7.5, 8.5), - d: 9.5, - e: -1, - f: { - g: -10.5, - h: vec3u(11, 12, 13), - i: vec2u(14, 15), - j: 16, + value: { + a: 1, + b: d.vec3u(2, 3, 4), + c: d.vec3f(6.5, 7.5, 8.5), + d: 9.5, + e: -1, + f: { + g: -10.5, + h: d.vec3u(11, 12, 13), + i: d.vec2u(14, 15), + j: 16, + }, }, - }; + })); - writeToArrayBuffer(buffer, TestStruct, value); - expect(readFromArrayBuffer(buffer, TestStruct)).toStrictEqual(value); + expect(decoded).toStrictEqual(value); }); it('allows for runtime sized arrays as last property', () => { @@ -148,18 +145,25 @@ describe('struct', () => { expect(sizeOf(TestStruct)).toBe(8); expect(alignmentOf(TestStruct)).toBe(2); + }); - const buffer = new ArrayBuffer(sizeOf(TestStruct)); - - const value: Infer = { - a: 1.0, - b: 2.0, - c: 3.0, - d: 4.0, - }; + it('encodes and decodes f16 structures properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + a: d.f16, + b: d.f16, + c: d.f16, + d: d.f16, + }), + value: { + a: 1.0, + b: 2.0, + c: 3.0, + d: 4.0, + }, + })); - writeToArrayBuffer(buffer, TestStruct, value); - expect(readFromArrayBuffer(buffer, TestStruct)).toStrictEqual(value); + expect(decoded).toStrictEqual(value); }); it('properly aligns with f16', () => { @@ -171,17 +175,23 @@ describe('struct', () => { expect(sizeOf(TestStruct)).toBe(12); expect(alignmentOf(TestStruct)).toBe(4); + }); - const buffer = new ArrayBuffer(sizeOf(TestStruct)); - - const value: Infer = { - a: 1, - b: 2.0, - c: 3, - }; + it('encodes and decodes structures aligned with f16 properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + a: d.u32, + b: d.f16, + c: d.u32, + }), + value: { + a: 1, + b: 2.0, + c: 3, + }, + })); - writeToArrayBuffer(buffer, TestStruct, value); - expect(readFromArrayBuffer(buffer, TestStruct)).toStrictEqual(value); + expect(decoded).toStrictEqual(value); }); it('supports and properly aligns with vectors of f16', () => { @@ -193,16 +203,6 @@ describe('struct', () => { expect(sizeOf(TestStruct)).toBe(8); expect(alignmentOf(TestStruct)).toBe(8); - const buffer = new ArrayBuffer(sizeOf(TestStruct)); - - const value: Infer = { - a: vec3h(1.0, 2.0, 3.0), - b: 4.0, - }; - - writeToArrayBuffer(buffer, TestStruct, value); - expect(readFromArrayBuffer(buffer, TestStruct)).toStrictEqual(value); - const TestStruct2 = struct({ a: vec2h, b: struct({ @@ -213,20 +213,42 @@ describe('struct', () => { }); expect(sizeOf(TestStruct2)).toBe(40); + }); - const buffer2 = new ArrayBuffer(sizeOf(TestStruct2)); - - const value2: Infer = { - a: vec2h(1.0, 2.0), - b: { - aa: [vec3h(1.0, 2.0, 3.0), vec3h(4.0, 5.0, 6.0)], - bb: 7.0, + it('encodes and decodes structures with vectors of f16 properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + a: d.vec3h, + b: d.f16, + }), + value: { + a: d.vec3h(1.0, 2.0, 3.0), + b: 4.0, }, - c: vec2h(8.0, 9.0), - }; + })); + + expect(decoded).toStrictEqual(value); + + const nested = await writeWithBothWriters(({ d }) => ({ + schema: d.struct({ + a: d.vec2h, + b: d.struct({ + aa: d.arrayOf(d.vec3h, 2), + bb: d.f16, + }), + c: d.vec2h, + }), + value: { + a: d.vec2h(1.0, 2.0), + b: { + aa: [d.vec3h(1.0, 2.0, 3.0), d.vec3h(4.0, 5.0, 6.0)], + bb: 7.0, + }, + c: d.vec2h(8.0, 9.0), + }, + })); - writeToArrayBuffer(buffer2, TestStruct2, value2); - expect(readFromArrayBuffer(buffer2, TestStruct2)).toStrictEqual(value2); + expect(nested.decoded).toStrictEqual(nested.value); }); it('can be called to create an object', () => { diff --git a/packages/typegpu/tests/vector.test.ts b/packages/typegpu/tests/vector.test.ts index 2878b3b133..f630b10e84 100644 --- a/packages/typegpu/tests/vector.test.ts +++ b/packages/typegpu/tests/vector.test.ts @@ -1,6 +1,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import { sizeOf } from 'typegpu/data'; import { tgpu, d, std, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu'; +import { writeWithBothWriters } from 'typegpu-testing-utility'; describe('constructors', () => { it('casts floats to signed integers', () => { @@ -58,13 +59,13 @@ describe('vec2f', () => { expect(vec.y).toBe(5); }); - it('should encode a 2d vector', () => { - const vec = d.vec2f(1, 2); - - const buffer = new ArrayBuffer(d.sizeOf(d.vec2f)); + it('should encode a 2d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec2f, + value: d.vec2f(1, 2), + })); - writeToArrayBuffer(buffer, d.vec2f, vec); - expect(readFromArrayBuffer(buffer, d.vec2f)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); it('differs in type from other vector schemas', () => { @@ -139,13 +140,13 @@ describe('vec2i', () => { expect(vec.y).toBe(5); }); - it('should encode a 2d vector', () => { - const vec = d.vec2i(1, 2); - - const buffer = new ArrayBuffer(d.sizeOf(d.vec2i)); + it('should encode a 2d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec2i, + value: d.vec2i(1, 2), + })); - writeToArrayBuffer(buffer, d.vec2i, vec); - expect(readFromArrayBuffer(buffer, d.vec2i)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); it('differs in type from other vector schemas', () => { @@ -321,13 +322,13 @@ describe('vec3f', () => { expect(vec.z).toBe(5); }); - it('should encode a 3d vector', () => { - const vec = d.vec3f(1, 2, 3); - - const buffer = new ArrayBuffer(d.sizeOf(d.vec3f)); + it('should encode a 3d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec3f, + value: d.vec3f(1, 2, 3), + })); - writeToArrayBuffer(buffer, d.vec3f, vec); - expect(readFromArrayBuffer(buffer, d.vec3f)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); it('differs in type from other vector schemas', () => { @@ -409,13 +410,13 @@ describe('vec3i', () => { expect(vec.z).toBe(5); }); - it('should encode a 3d vector', () => { - const vec = d.vec3i(1, 2, 3); - - const buffer = new ArrayBuffer(d.sizeOf(d.vec3i)); + it('should encode a 3d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec3i, + value: d.vec3i(1, 2, 3), + })); - writeToArrayBuffer(buffer, d.vec3i, vec); - expect(readFromArrayBuffer(buffer, d.vec3i)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); it('differs in type from other vector schemas', () => { @@ -608,13 +609,13 @@ describe('vec4f', () => { expect(vec.w).toBe(5); }); - it('should encode a 4d vector', () => { - const vec = d.vec4f(1, 2, 3, 4); + it('should encode a 4d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec4f, + value: d.vec4f(1, 2, 3, 4), + })); - const buffer = new ArrayBuffer(d.sizeOf(d.vec4f)); - - writeToArrayBuffer(buffer, d.vec4f, vec); - expect(readFromArrayBuffer(buffer, d.vec4f)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); it('differs in type from other vector schemas', () => { @@ -693,21 +694,23 @@ describe('vec2h', () => { expect(vec.y).toBe(5); }); - it('should encode a 2d vector', () => { - const vec = d.vec2h(1, 2050); - const buffer = new ArrayBuffer(sizeOf(d.vec2h)); + it('should encode a 2d vector', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec2h, + value: d.vec2h(1, 2050), + })); - writeToArrayBuffer(buffer, d.vec2h, vec); - expect(readFromArrayBuffer(buffer, d.vec2h)).toStrictEqual(vec); + expect(decoded).toStrictEqual(value); }); - it('should change unrepresentable values to the closest representable', () => { - const vec = d.vec2h(1, 4097); - - const buffer = new ArrayBuffer(sizeOf(d.vec2h)); + it('should change unrepresentable values to the closest representable', async () => { + const { decoded, typegpu } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec2h, + value: d.vec2h(1, 4097), + })); - writeToArrayBuffer(buffer, d.vec2h, vec); - expect(readFromArrayBuffer(buffer, d.vec2h)).toStrictEqual(d.vec2h(1, 4096)); + expect([...decoded]).toStrictEqual([1, 4096]); + expect(decoded).toStrictEqual(typegpu.d.vec2h(1, 4096)); }); it('differs in type from other vector schemas', () => { From 796d221e742eef2e5bca8b6f6a5aafc42eefc098 Mon Sep 17 00:00:00 2001 From: Mykhailo Zuiev Date: Wed, 21 Oct 2026 14:19:56 +0200 Subject: [PATCH 2/3] test: exercise both writers in more schema tests --- packages/typegpu/tests/arrayBufferIO.test.ts | 77 ++++++++---- packages/typegpu/tests/disarray.test.ts | 52 ++++----- packages/typegpu/tests/matrix.test.ts | 116 ++++++++++--------- packages/typegpu/tests/unstruct.test.ts | 91 +++++++-------- 4 files changed, 185 insertions(+), 151 deletions(-) diff --git a/packages/typegpu/tests/arrayBufferIO.test.ts b/packages/typegpu/tests/arrayBufferIO.test.ts index a2e7960397..1d19c8fec4 100644 --- a/packages/typegpu/tests/arrayBufferIO.test.ts +++ b/packages/typegpu/tests/arrayBufferIO.test.ts @@ -4,20 +4,22 @@ import { it, writeWithBothWriters } from 'typegpu-testing-utility'; describe('arrayBufferIO', () => { describe('write', () => { - it('handles d.vec input', () => { - const buffer = new ArrayBuffer(16); - - writeToArrayBuffer(buffer, d.vec4u, d.vec4u(1, 2, 3, 4)); + it('handles d.vec input', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec4u, + value: d.vec4u(1, 2, 3, 4), + })); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 4]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 4]); }); - it('handles plain array input', () => { - const buffer = new ArrayBuffer(16); - - writeToArrayBuffer(buffer, d.vec4u, [1, 2, 3, 4]); + it('handles plain array input', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec4u, + value: [1, 2, 3, 4] as const, + })); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 4]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 4]); }); it('handles typed array input', () => { @@ -72,6 +74,22 @@ describe('arrayBufferIO', () => { expect([...new Float32Array(bytes)]).toEqual([1, 2, 3, 0]); }); + it('handles structs with attributes', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => { + const Boid = d.struct({ pos: d.vec2u, id: d.align(16, d.u32) }); + const Boids = d.arrayOf(Boid, 2); + + return { + schema: Boids, + value: [Boid({ pos: d.vec2u(1, 2), id: 3 }), Boid({ pos: d.vec2u(4, 5), id: 6 })], + }; + }); + + expect([...new Uint32Array(bytes)]).toStrictEqual([ + 1, 2, 0, 0, 3, 0, 0, 0, 4, 5, 0, 0, 6, 0, 0, 0, + ]); + }); + it('does not override 4th element when handling vec3f', () => { const buffer = new ArrayBuffer(16); writeToArrayBuffer(buffer, d.vec4f, [1, 2, 3, 4]); @@ -90,6 +108,15 @@ describe('arrayBufferIO', () => { expect([...new Float32Array(bytes)]).toEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); }); + it('handles mat3x3f with matrix input', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.mat3x3f, + value: d.mat3x3f(1, 2, 3, 4, 5, 6, 7, 8, 9), + })); + + expect([...new Float32Array(bytes)]).toEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); + }); + it('overrides all values when handling mat3x3f', () => { const buffer = new ArrayBuffer(48); writeToArrayBuffer( @@ -115,25 +142,27 @@ describe('arrayBufferIO', () => { }); describe('read', () => { - it('handles vectors', () => { - const buffer = new ArrayBuffer(16); - writeToArrayBuffer(buffer, d.vec4u, d.vec4u(1, 2, 3, 4)); - - const result = readFromArrayBuffer(buffer, d.vec4u); + it('handles vectors', async () => { + const { decoded, typegpu } = await writeWithBothWriters(({ d }) => ({ + schema: d.vec4u, + value: d.vec4u(1, 2, 3, 4), + })); - expect(result).toStrictEqual(d.vec4u(1, 2, 3, 4)); + expect(decoded).toStrictEqual(typegpu.d.vec4u(1, 2, 3, 4)); }); - it('handles structs', () => { - const buffer = new ArrayBuffer(32); - const Boid = d.struct({ pos: d.vec2u, id: d.u32 }); - const Boids = d.arrayOf(Boid, 2); - const boids = [Boid({ pos: d.vec2u(1, 2), id: 3 }), Boid({ pos: d.vec2u(4, 5), id: 6 })]; - writeToArrayBuffer(buffer, Boids, boids); + it('handles structs', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const Boid = d.struct({ pos: d.vec2u, id: d.u32 }); + const Boids = d.arrayOf(Boid, 2); - const results = readFromArrayBuffer(buffer, Boids); + return { + schema: Boids, + value: [Boid({ pos: d.vec2u(1, 2), id: 3 }), Boid({ pos: d.vec2u(4, 5), id: 6 })], + }; + }); - expect(results).toStrictEqual(boids); + expect(decoded).toStrictEqual(value); }); }); diff --git a/packages/typegpu/tests/disarray.test.ts b/packages/typegpu/tests/disarray.test.ts index de7d785357..87bd2de8b5 100644 --- a/packages/typegpu/tests/disarray.test.ts +++ b/packages/typegpu/tests/disarray.test.ts @@ -1,7 +1,7 @@ import { describe, expect, expectTypeOf } from 'vitest'; -import { readFromArrayBuffer, writeToArrayBuffer } from 'typegpu'; +import { readFromArrayBuffer } from 'typegpu'; import * as d from 'typegpu/data'; -import { it } from 'typegpu-testing-utility'; +import { it, writeWithBothWriters } from 'typegpu-testing-utility'; describe('disarray', () => { it('does not take element alignment into account when measuring', () => { @@ -19,20 +19,22 @@ describe('disarray', () => { expect(d.sizeOf(TestArray)).toBe(108); }); - it('does not align array elements when writing', () => { - const TestArray = d.disarrayOf(d.vec3u, 3); - const buffer = new ArrayBuffer(d.sizeOf(TestArray)); + it('does not align array elements when writing', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.disarrayOf(d.vec3u, 3), + value: [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)], + })); - writeToArrayBuffer(buffer, TestArray, [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)]); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); - it('aligns array elements when writing with custom aligned elements', () => { - const TestArray = d.disarrayOf(d.align(16, d.vec3u), 3); - const buffer = new ArrayBuffer(d.sizeOf(TestArray)); + it('aligns array elements when writing with custom aligned elements', async () => { + const { bytes } = await writeWithBothWriters(({ d }) => ({ + schema: d.disarrayOf(d.align(16, d.vec3u), 3), + value: [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)], + })); - writeToArrayBuffer(buffer, TestArray, [d.vec3u(1, 2, 3), d.vec3u(4, 5, 6), d.vec3u(7, 8, 9)]); - expect([...new Uint32Array(buffer)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); + expect([...new Uint32Array(bytes)]).toStrictEqual([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0]); }); it('does not align array elements when reading', () => { @@ -74,21 +76,19 @@ describe('disarray', () => { ]); }); - it('encodes and decodes arrays properly', () => { - const TestArray = d.disarrayOf(d.vec3f, 5); - - const buffer = new ArrayBuffer(d.sizeOf(TestArray)); - - const value: d.Infer = [ - d.vec3f(1.5, 2, 3.5), - d.vec3f(), - d.vec3f(-1.5, 2, 3.5), - d.vec3f(1.5, -2, 3.5), - d.vec3f(1.5, 2, 15), - ]; + it('encodes and decodes arrays properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => ({ + schema: d.disarrayOf(d.vec3f, 5), + value: [ + d.vec3f(1.5, 2, 3.5), + d.vec3f(), + d.vec3f(-1.5, 2, 3.5), + d.vec3f(1.5, -2, 3.5), + d.vec3f(1.5, 2, 15), + ], + })); - writeToArrayBuffer(buffer, TestArray, value); - expect(readFromArrayBuffer(buffer, TestArray)).toStrictEqual(value); + expect(decoded).toStrictEqual(value); }); it('can be called to create a disarray', () => { diff --git a/packages/typegpu/tests/matrix.test.ts b/packages/typegpu/tests/matrix.test.ts index 5147363e8b..e79bc5af15 100644 --- a/packages/typegpu/tests/matrix.test.ts +++ b/packages/typegpu/tests/matrix.test.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import { tgpu, d } from 'typegpu'; -import { readFromArrayBuffer, writeToArrayBuffer } from 'typegpu'; +import { writeWithBothWriters } from 'typegpu-testing-utility'; import { isCloseTo } from 'typegpu/std'; describe('mat2x2f', () => { @@ -24,28 +24,30 @@ describe('mat2x2f', () => { expect(mat.columns[1]).toStrictEqual(v1); }); - it('encodes identity matrix properly', () => { - const identity = d.mat2x2f( - d.vec2f(1, 0), // column 0 - d.vec2f(0, 1), // column 1 - ); + it('encodes identity matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const identity = d.mat2x2f( + d.vec2f(1, 0), // column 0 + d.vec2f(0, 1), // column 1 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat2x2f)); + return { schema: d.mat2x2f, value: identity }; + }); - writeToArrayBuffer(buffer, d.mat2x2f, identity); - expect(readFromArrayBuffer(buffer, d.mat2x2f)).toStrictEqual(identity); + expect(decoded).toStrictEqual(value); }); - it('encodes a matrix properly', () => { - const mat = d.mat2x2f( - d.vec2f(0, 1), // column 0 - d.vec2f(2, 3), // column 1 - ); + it('encodes a matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const mat = d.mat2x2f( + d.vec2f(0, 1), // column 0 + d.vec2f(2, 3), // column 1 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat2x2f)); + return { schema: d.mat2x2f, value: mat }; + }); - writeToArrayBuffer(buffer, d.mat2x2f, mat); - expect(readFromArrayBuffer(buffer, d.mat2x2f)).toStrictEqual(mat); + expect(decoded).toStrictEqual(value); }); it('can be indexed into', () => { @@ -149,30 +151,32 @@ describe('mat3x3f', () => { expect(mat.columns[2]).toStrictEqual(v2); }); - it('encodes identity matrix properly', () => { - const identity = d.mat3x3f( - d.vec3f(1, 0, 0), // column 0 - d.vec3f(0, 1, 0), // column 1 - d.vec3f(0, 0, 1), // column 2 - ); + it('encodes identity matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const identity = d.mat3x3f( + d.vec3f(1, 0, 0), // column 0 + d.vec3f(0, 1, 0), // column 1 + d.vec3f(0, 0, 1), // column 2 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat3x3f)); + return { schema: d.mat3x3f, value: identity }; + }); - writeToArrayBuffer(buffer, d.mat3x3f, identity); - expect(readFromArrayBuffer(buffer, d.mat3x3f)).toStrictEqual(identity); + expect(decoded).toStrictEqual(value); }); - it('encodes a matrix properly', () => { - const mat = d.mat3x3f( - d.vec3f(0, 1, 2), // column 0 - d.vec3f(3, 4, 5), // column 1 - d.vec3f(6, 7, 8), // column 2 - ); + it('encodes a matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const mat = d.mat3x3f( + d.vec3f(0, 1, 2), // column 0 + d.vec3f(3, 4, 5), // column 1 + d.vec3f(6, 7, 8), // column 2 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat3x3f)); + return { schema: d.mat3x3f, value: mat }; + }); - writeToArrayBuffer(buffer, d.mat3x3f, mat); - expect(readFromArrayBuffer(buffer, d.mat3x3f)).toStrictEqual(mat); + expect(decoded).toStrictEqual(value); }); it('can be indexed into', () => { @@ -304,32 +308,34 @@ describe('mat4x4f', () => { expect(mat.columns[3]).toStrictEqual(v3); }); - it('encodes identity matrix properly', () => { - const identity = d.mat4x4f( - d.vec4f(1, 0, 0, 0), // column 0 - d.vec4f(0, 1, 0, 0), // column 1 - d.vec4f(0, 0, 1, 0), // column 2 - d.vec4f(0, 0, 0, 1), // column 3 - ); + it('encodes identity matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const identity = d.mat4x4f( + d.vec4f(1, 0, 0, 0), // column 0 + d.vec4f(0, 1, 0, 0), // column 1 + d.vec4f(0, 0, 1, 0), // column 2 + d.vec4f(0, 0, 0, 1), // column 3 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat4x4f)); + return { schema: d.mat4x4f, value: identity }; + }); - writeToArrayBuffer(buffer, d.mat4x4f, identity); - expect(readFromArrayBuffer(buffer, d.mat4x4f)).toStrictEqual(identity); + expect(decoded).toStrictEqual(value); }); - it('encodes a matrix properly', () => { - const mat = d.mat4x4f( - d.vec4f(0, 1, 2, 3), // column 0 - d.vec4f(4, 5, 6, 7), // column 1 - d.vec4f(8, 9, 10, 11), // column 2 - d.vec4f(12, 13, 14, 15), // column 3 - ); + it('encodes a matrix properly', async () => { + const { value, decoded } = await writeWithBothWriters(({ d }) => { + const mat = d.mat4x4f( + d.vec4f(0, 1, 2, 3), // column 0 + d.vec4f(4, 5, 6, 7), // column 1 + d.vec4f(8, 9, 10, 11), // column 2 + d.vec4f(12, 13, 14, 15), // column 3 + ); - const buffer = new ArrayBuffer(d.sizeOf(d.mat4x4f)); + return { schema: d.mat4x4f, value: mat }; + }); - writeToArrayBuffer(buffer, d.mat4x4f, mat); - expect(readFromArrayBuffer(buffer, d.mat4x4f)).toStrictEqual(mat); + expect(decoded).toStrictEqual(value); }); it('can be indexed into', () => { diff --git a/packages/typegpu/tests/unstruct.test.ts b/packages/typegpu/tests/unstruct.test.ts index 3bfb7c428c..774ff5f099 100644 --- a/packages/typegpu/tests/unstruct.test.ts +++ b/packages/typegpu/tests/unstruct.test.ts @@ -1,5 +1,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import { readFromArrayBuffer, writeToArrayBuffer, d } from 'typegpu'; +import { writeWithBothWriters } from 'typegpu-testing-utility'; describe('d.unstruct', () => { it('properly calculates size with only loose members', () => { @@ -103,22 +104,19 @@ describe('d.unstruct', () => { expect(d.sizeOf(s4)).toBe(68); }); - it('properly writes and reads data', () => { - const s = d.unstruct({ - a: d.unorm8x2, - b: d.align(16, d.snorm16x2), - c: d.float32x3, - }); - - const buffer = new ArrayBuffer(d.sizeOf(s)); - - writeToArrayBuffer(buffer, s, { - a: d.vec2f(0.5, 0.75), - b: d.vec2f(0.25, 0.5), - c: d.vec3f(1.0, 2.0, 3.0), - }); - - const data = readFromArrayBuffer(buffer, s); + it('properly writes and reads data', async () => { + const { decoded: data } = await writeWithBothWriters(({ d }) => ({ + schema: d.unstruct({ + a: d.unorm8x2, + b: d.align(16, d.snorm16x2), + c: d.float32x3, + }), + value: { + a: d.vec2f(0.5, 0.75), + b: d.vec2f(0.25, 0.5), + c: d.vec3f(1.0, 2.0, 3.0), + }, + })); expect(data.a.x).toBeCloseTo(0.5); expect(data.a.y).toBeCloseTo(0.75); @@ -129,28 +127,25 @@ describe('d.unstruct', () => { expect(data.c.z).toBeCloseTo(3.0); }); - it('properly writes and reads data with nested unstructs', () => { - const s = d.unstruct({ - a: d.unorm8x2, - b: d.align(16, d.snorm16x2), - c: d.unstruct({ - a: d.float32x3, - b: d.vec2i, + it('properly writes and reads data with nested unstructs', async () => { + const { decoded: data } = await writeWithBothWriters(({ d }) => ({ + schema: d.unstruct({ + a: d.unorm8x2, + b: d.align(16, d.snorm16x2), + c: d.unstruct({ + a: d.float32x3, + b: d.vec2i, + }), }), - }); - - const buffer = new ArrayBuffer(d.sizeOf(s)); - - writeToArrayBuffer(buffer, s, { - a: d.vec2f(0.5, 0.75), - b: d.vec2f(0.25, 0.5), - c: { - a: d.vec3f(1.0, 2.0, 3.0), - b: d.vec2i(4, 5), + value: { + a: d.vec2f(0.5, 0.75), + b: d.vec2f(0.25, 0.5), + c: { + a: d.vec3f(1.0, 2.0, 3.0), + b: d.vec2i(4, 5), + }, }, - }); - - const data = readFromArrayBuffer(buffer, s); + })); expect(data.a.x).toBeCloseTo(0.5); expect(data.a.y).toBeCloseTo(0.75); @@ -163,7 +158,7 @@ describe('d.unstruct', () => { expect(data.c.b.y).toBe(5); }); - it('can be custom aligned and behaves properly', () => { + it('can be custom aligned and behaves properly', async () => { const s = d.align( 16, d.unstruct({ @@ -178,18 +173,22 @@ describe('d.unstruct', () => { // since the unstruct is aligned to 16 bytes, the array stride should be 16 not 12 expect(d.sizeOf(a)).toBe(16 * 8); - const buffer = new ArrayBuffer(d.sizeOf(a)); - - writeToArrayBuffer( - buffer, - a, - Array.from({ length: 8 }, () => ({ + const { decoded: data } = await writeWithBothWriters(({ d }) => ({ + schema: d.disarrayOf( + d.align( + 16, + d.unstruct({ + a: d.unorm8x2, + b: d.align(8, d.snorm16x2), + }), + ), + 8, + ), + value: Array.from({ length: 8 }, () => ({ a: d.vec2f(0.5, 0.75), b: d.vec2f(-0.25, 0.25), })), - ); - - const data = readFromArrayBuffer(buffer, a); + })); data.forEach((item, _) => { expect(item.a.x).toBeCloseTo(0.5); From 609e8dc62e48082d9e6c71f06a171eb357571257 Mon Sep 17 00:00:00 2001 From: Mykhailo Zuiev Date: Wed, 21 Oct 2026 14:55:44 +0200 Subject: [PATCH 3/3] test: keep a caller's Function spy intact in writeWithBothWriters --- .../src/bothWriters.ts | 4 +- .../tests/internal/bothWriters.test.ts | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 packages/typegpu/tests/internal/bothWriters.test.ts diff --git a/packages/typegpu-testing-utility/src/bothWriters.ts b/packages/typegpu-testing-utility/src/bothWriters.ts index 095035d8d9..cb33917d27 100644 --- a/packages/typegpu-testing-utility/src/bothWriters.ts +++ b/packages/typegpu-testing-utility/src/bothWriters.ts @@ -4,7 +4,6 @@ import type { d } from 'typegpu'; type TypeGpuModule = typeof TypeGpu; -// Captured once, so that restoring can never put back a proxy installed by another call. const RealFunction = globalThis.Function; const TYPEGPU_GLOBAL_PREFIX = '__TYPEGPU_'; @@ -40,6 +39,7 @@ function interceptFunctionConstructor(block: boolean): { restore: () => void; } { let count = 0; + const previous = globalThis.Function; globalThis.Function = new Proxy(RealFunction, { construct(target, args, newTarget) { count++; @@ -59,7 +59,7 @@ function interceptFunctionConstructor(block: boolean): { return { count: () => count, restore: () => { - globalThis.Function = RealFunction; + globalThis.Function = previous; }, }; } diff --git a/packages/typegpu/tests/internal/bothWriters.test.ts b/packages/typegpu/tests/internal/bothWriters.test.ts new file mode 100644 index 0000000000..67c2658e54 --- /dev/null +++ b/packages/typegpu/tests/internal/bothWriters.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { writeWithBothWriters } from 'typegpu-testing-utility'; + +/** Runs `run` with a `Function` spy installed by the "calling test", then puts the original back. */ +async function withCallerFunctionSpy(run: (spy: FunctionConstructor) => Promise) { + const original = globalThis.Function; + const spy = new Proxy(original, {}); + globalThis.Function = spy; + try { + await run(spy); + } finally { + globalThis.Function = original; + } +} + +const writeU32 = () => + writeWithBothWriters(({ d }) => ({ + schema: d.u32, + value: 7, + })); + +// The write throws while the helper has `Function` intercepted. +const writeThatThrows = () => + writeWithBothWriters(({ d }) => ({ + schema: d.struct({ a: d.u32 }), + value: undefined as never, + })); + +describe('writeWithBothWriters', () => { + it('leaves a Function spy installed by the caller in place after a successful call', async () => { + await withCallerFunctionSpy(async (spy) => { + const { bytes } = await writeU32(); + + expect([...new Uint32Array(bytes)]).toStrictEqual([7]); + expect(globalThis.Function).toBe(spy); + }); + }); + + it('leaves a Function spy installed by the caller in place after a call that throws', async () => { + await withCallerFunctionSpy(async (spy) => { + await expect(writeThatThrows()).rejects.toThrow(); + + expect(globalThis.Function).toBe(spy); + }); + }); + + it('restores the original Function and keeps working after a call that throws', async () => { + const original = globalThis.Function; + + // Started together on purpose, the second call has to wait for the first one to fail. + const [failed, succeeded] = await Promise.allSettled([writeThatThrows(), writeU32()]); + + expect(failed.status).toBe('rejected'); + expect(succeeded.status).toBe('fulfilled'); + expect(globalThis.Function).toBe(original); + + const { bytes } = await writeU32(); + expect([...new Uint32Array(bytes)]).toStrictEqual([7]); + expect(globalThis.Function).toBe(original); + }); +});