diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 543a4d9cdf..b80fa381dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Test run: pnpm test + - name: Build docs run: cd apps/typegpu-docs && pnpm build diff --git a/apps/typegpu-docs/vitest.config.mts b/apps/typegpu-docs/vitest.config.mts index 36c0c4b982..631620a9d3 100644 --- a/apps/typegpu-docs/vitest.config.mts +++ b/apps/typegpu-docs/vitest.config.mts @@ -3,12 +3,16 @@ import type TypeGPUPlugin from 'unplugin-typegpu/vite'; import { imagetools } from 'vite-imagetools'; import { defineConfig, type Plugin } from 'vitest/config'; import { preview } from '@vitest/browser-preview'; +import { typegpuBuiltAliases } from 'typegpu-testing-utility/config'; const jiti = createJiti(import.meta.url); const typegpu = await jiti.import('unplugin-typegpu/vite', { default: true }); export default defineConfig({ plugins: [typegpu({ include: [/\.m?[jt]sx?/] }), imagetools()] as Plugin[], + resolve: { + alias: typegpuBuiltAliases(), + }, server: { proxy: { '/TypeGPU': { diff --git a/package.json b/package.json index a8099e6609..e496420339 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "dev": "DEV=true pnpm run --filter typegpu-docs dev", "dev:host": "DEV=true pnpm run --filter typegpu-docs dev --host --mode https", "fix": "oxlint -c oxlint.config.ts --fix && oxfmt", - "test": "pnpm run test:types && pnpm run test:style && pnpm run test:unit-and-attest && pnpm run test:circular-deps", + "test": "pnpm run test:types && pnpm run test:style && pnpm run test:unit-and-attest && pnpm run test:built-unit-and-attest && pnpm run test:circular-deps", "test:circular-deps": "pnpm dpdm -T --exit-code circular:1 packages/**/src/index.ts packages/**/src/index.js !packages/**/node_modules", "test:types": "pnpm run --filter typegpu-docs transform-overloads && pnpm run -r --parallel test:types", "test:style": "oxlint -c oxlint.config.ts --max-warnings=0 --type-aware --ignore-pattern 'packages/typegpu-cli/templates/template-expo-bare/**' --report-unused-disable-directives && oxlint -c oxlint.config.ts --max-warnings=0 packages/typegpu-cli/templates/template-expo-bare --report-unused-disable-directives && oxfmt --check", "test:unit-and-attest": "ENABLE_ATTEST=1 vitest run --project=!browser", "test:unit": "vitest run --project=!browser", + "test:built-unit": "pnpm run --filter './packages/*' build && TEST_BUILT=1 vitest run --project=!browser", + "test:built-unit-and-attest": "pnpm run --filter './packages/*' build && TEST_BUILT=1 ENABLE_ATTEST=1 vitest run --project=!browser", "test:unit:watch": "vitest --project=!browser", "test:browser": "vitest run --browser.enabled --project browser", "test:browser:watch": "vitest --browser.enabled --project browser", @@ -51,6 +53,7 @@ "oxlint": "^1.57.0", "oxlint-tsgolint": "^0.17.4", "pkg-pr-new": "^0.0.66", + "typegpu-testing-utility": "workspace:*", "typescript": "catalog:types", "unplugin-typegpu": "workspace:*", "vite-imagetools": "catalog:frontend", diff --git a/packages/typegpu-gl/vitest.config.mts b/packages/typegpu-gl/vitest.config.mts index df56d64cc7..db70d708b5 100644 --- a/packages/typegpu-gl/vitest.config.mts +++ b/packages/typegpu-gl/vitest.config.mts @@ -1,10 +1,14 @@ import { createJiti } from 'jiti'; import type TypeGPUPlugin from 'unplugin-typegpu/vite'; import { defineConfig } from 'vitest/config'; +import { typegpuBuiltAliases } from 'typegpu-testing-utility/config'; const jiti = createJiti(import.meta.url); const typegpu = await jiti.import('unplugin-typegpu/vite', { default: true }); export default defineConfig({ plugins: [typegpu({ forceTgpuAlias: 'tgpu', earlyPruning: false })], + resolve: { + alias: typegpuBuiltAliases(), + }, }); diff --git a/packages/typegpu-react/tests/root-context.test.tsx b/packages/typegpu-react/tests/root-context.test.tsx index 5373233c37..d5db80a6d4 100644 --- a/packages/typegpu-react/tests/root-context.test.tsx +++ b/packages/typegpu-react/tests/root-context.test.tsx @@ -1,6 +1,5 @@ import { act, render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, vi } from 'vitest'; -import tgpu from 'typegpu'; import type { TgpuRoot } from 'typegpu'; import { Root, useRootWithStatus } from '@typegpu/react'; import { useEffect } from 'react'; @@ -56,15 +55,9 @@ describe('Root unmount cleanup', () => { }); it('should destroy root when init promise resolves after unmount', async ({ - root: fixtureRoot, + stallDeviceRequest, }) => { - let resolveInit!: (root: TgpuRoot) => void; - const initPromise = new Promise((resolve) => { - resolveInit = resolve; - }); - - using _initSpy = vi.spyOn(tgpu, 'init').mockReturnValue(initPromise); - const destroySpy = vi.spyOn(fixtureRoot, 'destroy'); + const resume = stallDeviceRequest(); function TestConsumer() { useRootWithStatus(); @@ -82,7 +75,8 @@ describe('Root unmount cleanup', () => { vi.runAllTimers(); // Resolve init after context has been destroyed - resolveInit(fixtureRoot); + const device = await resume(); + const destroySpy = vi.spyOn(device, 'destroy'); // Flush microtasks so the .then() callback runs await act(async () => { diff --git a/packages/typegpu-react/vitest.config.mts b/packages/typegpu-react/vitest.config.mts index bdd55872ae..e4f7be17c4 100644 --- a/packages/typegpu-react/vitest.config.mts +++ b/packages/typegpu-react/vitest.config.mts @@ -1,12 +1,16 @@ import { createJiti } from 'jiti'; import type TypeGPUPlugin from 'unplugin-typegpu/vite'; import { defineConfig } from 'vitest/config'; +import { typegpuBuiltAliases } from 'typegpu-testing-utility/config'; const jiti = createJiti(import.meta.url); const typegpu = await jiti.import('unplugin-typegpu/vite', { default: true }); export default defineConfig({ plugins: [typegpu({ forceTgpuAlias: 'tgpu', earlyPruning: false })], + resolve: { + alias: typegpuBuiltAliases(), + }, esbuild: { jsx: 'automatic', jsxImportSource: 'react', diff --git a/packages/typegpu-testing-utility/package.json b/packages/typegpu-testing-utility/package.json index 895567d67d..664b8d78a2 100644 --- a/packages/typegpu-testing-utility/package.json +++ b/packages/typegpu-testing-utility/package.json @@ -4,7 +4,10 @@ "private": true, "license": "MIT", "type": "module", - "exports": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./config": "./src/config.ts" + }, "scripts": { "test:types": "pnpm tsc --p ./tsconfig.json --noEmit" }, diff --git a/packages/typegpu-testing-utility/src/config.ts b/packages/typegpu-testing-utility/src/config.ts new file mode 100644 index 0000000000..3f10976020 --- /dev/null +++ b/packages/typegpu-testing-utility/src/config.ts @@ -0,0 +1,25 @@ +type Alias = { + find: RegExp; + replacement: string; +}; + +export function isTestingBuiltTypegpu() { + return process.env.TEST_BUILT === '1' || process.env.TEST_BUILT === 'true'; +} + +export function typegpuBuiltAliases(): Alias[] { + if (!isTestingBuiltTypegpu()) { + return []; + } + + return [ + { + find: /^typegpu$/, + replacement: 'typegpu/$built$', + }, + { + find: /^typegpu\/(?!package\.json$)(?!.*\/\$built\$$)(.+)$/, + replacement: 'typegpu/$1/$built$', + }, + ]; +} diff --git a/packages/typegpu-testing-utility/src/extendedIt.ts b/packages/typegpu-testing-utility/src/extendedIt.ts index 5765c19a91..ddc7767e62 100644 --- a/packages/typegpu-testing-utility/src/extendedIt.ts +++ b/packages/typegpu-testing-utility/src/extendedIt.ts @@ -138,10 +138,39 @@ export const it = base return mockDevice as unknown as GPUDevice & { mock: typeof mockDevice }; }) - .extend('adapter', ({ device }) => { + .extend('_stallDeviceRequest', ({ device }) => { + let stallResolve: () => void; + let stallPromise: Promise | undefined; + let devicePromise: Promise | undefined; + + const result = { + enabled: false, + get stallPromise() { + return (stallPromise ??= new Promise((r) => { + stallResolve = r; + })); + }, + get devicePromise(): Promise { + return (devicePromise ??= this.stallPromise.then(() => device)); + }, + get stallResolve() { + void this.stallPromise; // ensuring the promise is initialized + return stallResolve; + }, + }; + + return result; + }) + .extend('adapter', ({ device, _stallDeviceRequest }) => { const adapterMock = { features: new Set(['timestamp-query']), - requestDevice: vi.fn((_descriptor) => Promise.resolve(device)), + requestDevice: vi.fn((_descriptor) => { + if (_stallDeviceRequest.enabled) { + return _stallDeviceRequest.devicePromise; + } + + return Promise.resolve(device); + }), limits: { maxStorageBufferBindingSize: 64 * 1024 * 1024, maxBufferSize: 64 * 1024 * 1024, @@ -171,6 +200,33 @@ export const it = base vi.unstubAllGlobals(); }); }) + /** + * Used to introduce an artificial delay between requesting a device and getting it. + * @example + * ```ts + * it('foo', async ({ stallDeviceRequest }) => { + * const resume = stallDeviceRequest(); + * + * // do something asynchronous that requests a device + * + * const device = await resume(); // causes the device to resolve + * }); + * ``` + */ + .extend('stallDeviceRequest', ({ _stallDeviceRequest }) => { + return () => { + if (_stallDeviceRequest.enabled) { + throw new Error('Cannot stall .requestDevice() more than once at a time'); + } + _stallDeviceRequest.enabled = true; + + return async () => { + _stallDeviceRequest.stallResolve(); + _stallDeviceRequest.enabled = false; + return await _stallDeviceRequest.devicePromise; + }; + }; + }) .extend('root', async ({}, { onCleanup }) => { const root = await tgpu.init(); diff --git a/packages/typegpu/setupVitest.ts b/packages/typegpu/setupVitest.ts index b4f12cf76f..15a7025a66 100644 --- a/packages/typegpu/setupVitest.ts +++ b/packages/typegpu/setupVitest.ts @@ -1,7 +1,9 @@ import { setup } from '@ark/attest'; import { type } from 'arktype'; -const truthyString = type('"0"|"1"').pipe.try((value) => Boolean(Number.parseInt(value))); +const truthyString = type('"0"|"1"|"true"|"false"').pipe.try( + (value) => value === '1' || value === 'true', +); const ProcessEnvType = type({ 'ENABLE_ATTEST?': type.or(truthyString, 'undefined'), @@ -9,9 +11,10 @@ const ProcessEnvType = type({ const env = ProcessEnvType.assert(process.env); -export default () => - setup({ +export default () => { + return setup({ formatCmd: 'pnpm fix', // Skipping type tests by default skipTypes: !env.ENABLE_ATTEST, }); +}; diff --git a/packages/typegpu/vitest.config.mts b/packages/typegpu/vitest.config.mts index 18da22bbb0..67a95a2d23 100644 --- a/packages/typegpu/vitest.config.mts +++ b/packages/typegpu/vitest.config.mts @@ -1,13 +1,21 @@ import { createJiti } from 'jiti'; import type TypeGPUPlugin from 'unplugin-typegpu/vite'; -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; +import { isTestingBuiltTypegpu, typegpuBuiltAliases } from 'typegpu-testing-utility/config'; const jiti = createJiti(import.meta.url); const typegpu = await jiti.import('unplugin-typegpu/vite', { default: true }); +const testBuilt = isTestingBuiltTypegpu(); export default defineConfig({ plugins: [typegpu({ forceTgpuAlias: 'tgpu', earlyPruning: false })], + resolve: { + alias: typegpuBuiltAliases(), + }, test: { + exclude: testBuilt + ? [...configDefaults.exclude, 'tests/internal/**/*.{test,spec}.?(c|m)[jt]s?(x)'] + : configDefaults.exclude, globalSetup: ['setupVitest.ts'], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b83dd481d8..a32d56edc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,9 @@ importers: typescript: specifier: npm:tsover@^6.0.2 version: tsover@6.0.2 + typegpu-testing-utility: + specifier: workspace:* + version: link:packages/typegpu-testing-utility unplugin-typegpu: specifier: workspace:* version: link:packages/unplugin-typegpu diff --git a/vitest.config.mts b/vitest.config.mts index 2372abc9ac..c2e91eea79 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,6 +1,10 @@ import { defineConfig } from 'vitest/config'; +import { typegpuBuiltAliases } from 'typegpu-testing-utility/config'; export default defineConfig({ + resolve: { + alias: typegpuBuiltAliases(), + }, test: { projects: ['packages/*', 'apps/*'], },