Add service worker support (#3419)

* Generate Web Worker script for every test file

This will be needed to launch Service Workers, which cannot be generated
at runtime and cannot use dynamic import().

* Add service worker support

* Update wpt.ts

* Run tests

* Address kainino0x feedback

* Address feedback | part 2

* Address feedback | part 3

* Address feedback | part 4

* Address feedback | part 5

* Address feedback | part 6

* Address feedback | part 7

* Address feedback | part 8

* Apply suggestions from code review

* use WorkerTestRunRequest in the postMessage/onmessage interface

* Clean up resolvers map

* Use express routing for .worker.js

* Skip worker tests when run in a worker that can't support them

DedicatedWorker can be created from DedicatedWorker, but none of the
other nested worker pairs are allowed.

* Clean up all service workers on startup and shutdown

* Avoid reinitializing service workers for every single case

* lint fixes

* Catch errors in wrapTestGroupForWorker onMessage

* Make sure the service worker has the correct URL

---------

Co-authored-by: Kai Ninomiya <kainino@chromium.org>
This commit is contained in:
François Beaufort 2024-03-11 20:54:26 +01:00 committed by GitHub
parent f9f6c90b80
commit 5ad758908a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 514 additions and 281 deletions

View file

@ -24,9 +24,9 @@ module.exports = function (grunt) {
cmd: 'node',
args: ['tools/gen_version'],
},
'generate-listings': {
'generate-listings-and-webworkers': {
cmd: 'node',
args: ['tools/gen_listings', 'gen/', ...kAllSuites.map(s => 'src/' + s)],
args: ['tools/gen_listings_and_webworkers', 'gen/', ...kAllSuites.map(s => 'src/' + s)],
},
validate: {
cmd: 'node',
@ -159,14 +159,14 @@ module.exports = function (grunt) {
// Must run after generate-common and run:build-out.
files: [
{ expand: true, dest: 'out/', cwd: 'gen', src: 'common/internal/version.js' },
{ expand: true, dest: 'out/', cwd: 'gen', src: '*/listing.js' },
{ expand: true, dest: 'out/', cwd: 'gen', src: '*/**/*.js' },
],
},
'gen-to-out-wpt': {
// Must run after generate-common and run:build-out-wpt.
files: [
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'common/internal/version.js' },
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'webgpu/listing.js' },
{ expand: true, dest: 'out-wpt/', cwd: 'gen', src: 'webgpu/**/*.js' },
],
},
'htmlfiles-to-out': {
@ -243,7 +243,7 @@ module.exports = function (grunt) {
grunt.registerTask('generate-common', 'Generate files into gen/ and src/', [
'run:generate-version',
'run:generate-listings',
'run:generate-listings-and-webworkers',
'run:generate-cache',
]);
grunt.registerTask('build-standalone', 'Build out/ (no checks; run after generate-common)', [

View file

@ -58,6 +58,7 @@ The following url parameters change how the harness runs:
- `debug=1` enables verbose debug logging from tests.
- `worker=dedicated` runs the tests on a dedicated worker instead of the main thread.
- `worker=shared` runs the tests on a shared worker instead of the main thread.
- `worker=service` runs the tests on a service worker instead of the main thread.
- `power_preference=low-power` runs most tests passing `powerPreference: low-power` to `requestAdapter`
- `power_preference=high-performance` runs most tests passing `powerPreference: high-performance` to `requestAdapter`

View file

@ -111,7 +111,7 @@ Each Suite has one Listing File (`suite/listing.[tj]s`), containing a list of th
in the suite.
In `src/suite/listing.ts`, this is computed dynamically.
In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings`).
In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings_and_webworkers`).
**Type:** Once `import`ed, `ListingFile`

View file

@ -58,7 +58,10 @@ function compareOneLevel(ordering: Ordering, aIsBig: boolean, bIsBig: boolean):
return Ordering.Unordered;
}
function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
/**
* Compare two file paths, or file-local test paths, returning an Ordering between the two.
*/
export function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
const shorter = Math.min(a.length, b.length);
for (let i = 0; i < shorter; ++i) {

View file

@ -1,6 +1,6 @@
// A listing of all specs within a single suite. This is the (awaited) type of
// `groups` in '{cts,unittests}/listing.ts' and `listing` in the auto-generated
// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings).
// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings_and_webworkers).
export type TestSuiteListing = TestSuiteListingEntry[];
export type TestSuiteListingEntry = TestSuiteListingEntrySpec | TestSuiteListingEntryReadme;

View file

@ -25,7 +25,7 @@ export function optionString(
* The possible options for the tests.
*/
export interface CTSOptions {
worker?: 'dedicated' | 'shared' | '';
worker?: 'dedicated' | 'shared' | 'service' | '';
debug: boolean;
compatibility: boolean;
forceFallbackAdapter: boolean;
@ -68,6 +68,7 @@ export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = {
{ value: '', description: 'no worker' },
{ value: 'dedicated', description: 'dedicated worker' },
{ value: 'shared', description: 'shared worker' },
{ value: 'service', description: 'service worker' },
],
},
debug: { description: 'show more info' },

View file

@ -1,13 +1,9 @@
import { setBaseResourcePath } from '../../framework/resources.js';
import { globalTestConfig } from '../../framework/test_config.js';
import { DefaultTestFileLoader } from '../../internal/file_loader.js';
import { Logger } from '../../internal/logging/logger.js';
import { parseQuery } from '../../internal/query/parseQuery.js';
import { TestQueryWithExpectation } from '../../internal/query/query.js';
import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js';
import { assert } from '../../util/util.js';
import { CTSOptions } from './options.js';
import { setupWorkerEnvironment, WorkerTestRunRequest } from './utils_worker.js';
// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@ -18,24 +14,9 @@ const loader = new DefaultTestFileLoader();
setBaseResourcePath('../../../resources');
async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
const query: string = ev.data.query;
const expectations: TestQueryWithExpectation[] = ev.data.expectations;
const ctsOptions: CTSOptions = ev.data.ctsOptions;
const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest;
const { debug, unrollConstEvalLoops, powerPreference, compatibility } = ctsOptions;
globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;
Logger.globalDebugMode = debug;
const log = new Logger();
if (powerPreference || compatibility) {
setDefaultRequestAdapterOptions({
...(powerPreference && { powerPreference }),
// MAINTENANCE_TODO: Change this to whatever the option ends up being
...(compatibility && { compatibilityMode: true }),
});
}
const log = setupWorkerEnvironment(ctsOptions);
const testcases = Array.from(await loader.loadCases(parseQuery(query)));
assert(testcases.length === 1, 'worker query resulted in != 1 cases');
@ -48,7 +29,7 @@ async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
}
self.onmessage = (ev: MessageEvent) => {
void reportTestResults.call(self, ev);
void reportTestResults.call(ev.source || self, ev);
};
self.onconnect = (event: MessageEvent) => {

View file

@ -2,33 +2,82 @@ import { LogMessageWithStack } from '../../internal/logging/log_message.js';
import { TransferredTestCaseResult, LiveTestCaseResult } from '../../internal/logging/result.js';
import { TestCaseRecorder } from '../../internal/logging/test_case_recorder.js';
import { TestQueryWithExpectation } from '../../internal/query/query.js';
import { timeout } from '../../util/timeout.js';
import { assert } from '../../util/util.js';
import { CTSOptions, kDefaultCTSOptions } from './options.js';
import { WorkerTestRunRequest } from './utils_worker.js';
export class TestDedicatedWorker {
private readonly ctsOptions: CTSOptions;
/** Query all currently-registered service workers, and unregister them. */
function unregisterAllServiceWorkers() {
void navigator.serviceWorker.getRegistrations().then(registrations => {
for (const registration of registrations) {
void registration.unregister();
}
});
}
// NOTE: This code runs on startup for any runtime with worker support. Here, we use that chance to
// delete any leaked service workers, and register to clean up after ourselves at shutdown.
unregisterAllServiceWorkers();
window.addEventListener('beforeunload', () => {
unregisterAllServiceWorkers();
});
class TestBaseWorker {
protected readonly ctsOptions: CTSOptions;
protected readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();
constructor(worker: CTSOptions['worker'], ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker } };
}
onmessage(ev: MessageEvent) {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);
this.resolvers.delete(query);
// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
}
async makeRequestAndRecordResult(
target: MessagePort | Worker | ServiceWorker,
rec: TestCaseRecorder,
query: string,
expectations: TestQueryWithExpectation[]
) {
const request: WorkerTestRunRequest = {
query,
expectations,
ctsOptions: this.ctsOptions,
};
target.postMessage(request);
const workerResult = await new Promise<LiveTestCaseResult>(resolve => {
assert(!this.resolvers.has(query), "can't request same query twice simultaneously");
this.resolvers.set(query, resolve);
});
rec.injectResult(workerResult);
}
}
export class TestDedicatedWorker extends TestBaseWorker {
private readonly worker: Worker;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();
constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'dedicated' } };
super('dedicated', ctsOptions);
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/test_worker-worker.js';
this.worker = new Worker(workerPath, { type: 'module' });
this.worker.onmessage = ev => {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);
// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
};
this.worker.onmessage = ev => this.onmessage(ev);
}
async run(
@ -36,46 +85,24 @@ export class TestDedicatedWorker {
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
this.worker.postMessage({
query,
expectations,
ctsOptions: this.ctsOptions,
});
const workerResult = await new Promise<LiveTestCaseResult>(resolve => {
this.resolvers.set(query, resolve);
});
rec.injectResult(workerResult);
await this.makeRequestAndRecordResult(this.worker, rec, query, expectations);
}
}
export class TestWorker extends TestDedicatedWorker {}
export class TestSharedWorker {
private readonly ctsOptions: CTSOptions;
export class TestSharedWorker extends TestBaseWorker {
private readonly port: MessagePort;
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();
constructor(ctsOptions?: CTSOptions) {
this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: 'shared' } };
super('shared', ctsOptions);
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/test_worker-worker.js';
const worker = new SharedWorker(workerPath, { type: 'module' });
this.port = worker.port;
this.port.start();
this.port.onmessage = ev => {
const query: string = ev.data.query;
const result: TransferredTestCaseResult = ev.data.result;
if (result.logs) {
for (const l of result.logs) {
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
}
}
this.resolvers.get(query)!(result as LiveTestCaseResult);
// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
// update the entire results JSON somehow at some point).
};
this.port.onmessage = ev => this.onmessage(ev);
}
async run(
@ -83,14 +110,39 @@ export class TestSharedWorker {
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
this.port.postMessage({
query,
expectations,
ctsOptions: this.ctsOptions,
});
const workerResult = await new Promise<LiveTestCaseResult>(resolve => {
this.resolvers.set(query, resolve);
});
rec.injectResult(workerResult);
await this.makeRequestAndRecordResult(this.port, rec, query, expectations);
}
}
export class TestServiceWorker extends TestBaseWorker {
constructor(ctsOptions?: CTSOptions) {
super('service', ctsOptions);
}
async run(
rec: TestCaseRecorder,
query: string,
expectations: TestQueryWithExpectation[] = []
): Promise<void> {
const [suite, name] = query.split(':', 2);
const fileName = name.split(',').join('/');
const serviceWorkerURL = new URL(
`/out/${suite}/webworker/${fileName}.worker.js`,
window.location.href
).toString();
// If a registration already exists for this path, it will be ignored.
const registration = await navigator.serviceWorker.register(serviceWorkerURL, {
type: 'module',
});
// Make sure the registration we just requested is active. (We don't worry about it being
// outdated from a previous page load, because we wipe all service workers on shutdown/startup.)
while (!registration.active || registration.active.scriptURL !== serviceWorkerURL) {
await new Promise(resolve => timeout(resolve, 0));
}
const serviceWorker = registration.active;
navigator.serviceWorker.onmessage = ev => this.onmessage(ev);
await this.makeRequestAndRecordResult(serviceWorker, rec, query, expectations);
}
}

View file

@ -0,0 +1,34 @@
import { globalTestConfig } from '../../framework/test_config.js';
import { Logger } from '../../internal/logging/logger.js';
import { TestQueryWithExpectation } from '../../internal/query/query.js';
import { setDefaultRequestAdapterOptions } from '../../util/navigator_gpu.js';
import { CTSOptions } from './options.js';
export interface WorkerTestRunRequest {
query: string;
expectations: TestQueryWithExpectation[];
ctsOptions: CTSOptions;
}
/**
* Set config environment for workers with ctsOptions and return a Logger.
*/
export function setupWorkerEnvironment(ctsOptions: CTSOptions): Logger {
const { debug, unrollConstEvalLoops, powerPreference, compatibility } = ctsOptions;
globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
globalTestConfig.compatibility = compatibility;
Logger.globalDebugMode = debug;
const log = new Logger();
if (powerPreference || compatibility) {
setDefaultRequestAdapterOptions({
...(powerPreference && { powerPreference }),
// MAINTENANCE_TODO: Change this to whatever the option ends up being
...(compatibility && { compatibilityMode: true }),
});
}
return log;
}

View file

@ -0,0 +1,43 @@
import { Fixture } from '../../framework/fixture';
import { LogMessageWithStack } from '../../internal/logging/log_message.js';
import { comparePaths, comparePublicParamsPaths, Ordering } from '../../internal/query/compare.js';
import { parseQuery } from '../../internal/query/parseQuery.js';
import { TestQuerySingleCase } from '../../internal/query/query.js';
import { TestGroup } from '../../internal/test_group.js';
import { assert } from '../../util/util.js';
import { setupWorkerEnvironment, WorkerTestRunRequest } from './utils_worker.js';
export function wrapTestGroupForWorker(g: TestGroup<Fixture>) {
self.onmessage = async (ev: MessageEvent) => {
const { query, expectations, ctsOptions } = ev.data as WorkerTestRunRequest;
try {
const log = setupWorkerEnvironment(ctsOptions);
const testQuery = parseQuery(query);
assert(testQuery instanceof TestQuerySingleCase);
let testcase = null;
for (const t of g.iterate()) {
if (comparePaths(t.testPath, testQuery.testPathParts) !== Ordering.Equal) {
continue;
}
for (const c of t.iterate(testQuery.params)) {
if (comparePublicParamsPaths(c.id.params, testQuery.params) === Ordering.Equal) {
testcase = c;
}
}
}
assert(!!testcase, 'testcase not found');
const [rec, result] = log.record(query);
await testcase.run(rec, testQuery, expectations);
ev.source?.postMessage({ query, result });
} catch (thrown) {
const ex = thrown instanceof Error ? thrown : new Error(`${thrown}`);
ev.source?.postMessage({
query,
result: { status: 'fail', timems: 0, logs: [new LogMessageWithStack('INTERNAL', ex)] },
});
}
};
}

View file

@ -21,7 +21,7 @@ import {
OptionsInfos,
camelCaseToSnakeCase,
} from './helper/options.js';
import { TestDedicatedWorker, TestSharedWorker } from './helper/test_worker.js';
import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from './helper/test_worker.js';
const rootQuerySpec = 'webgpu:*';
let promptBeforeReload = false;
@ -66,6 +66,7 @@ setBaseResourcePath('../out/resources');
const dedicatedWorker =
options.worker === 'dedicated' ? new TestDedicatedWorker(options) : undefined;
const sharedWorker = options.worker === 'shared' ? new TestSharedWorker(options) : undefined;
const serviceWorker = options.worker === 'service' ? new TestServiceWorker(options) : undefined;
const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement;
const resultsVis = document.getElementById('resultsVis')!;
@ -182,6 +183,8 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
await dedicatedWorker.run(rec, name);
} else if (sharedWorker) {
await sharedWorker.run(rec, name);
} else if (serviceWorker) {
await serviceWorker.run(rec, name);
} else {
await t.run(rec);
}

View file

@ -9,7 +9,7 @@ import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/
import { assert } from '../util/util.js';
import { optionEnabled, optionString } from './helper/options.js';
import { TestDedicatedWorker, TestSharedWorker } from './helper/test_worker.js';
import { TestDedicatedWorker, TestServiceWorker, TestSharedWorker } from './helper/test_worker.js';
// testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html)
declare interface WptTestObject {
@ -34,6 +34,7 @@ void (async () => {
const workerString = optionString('worker');
const dedicatedWorker = workerString === 'dedicated' ? new TestDedicatedWorker() : undefined;
const sharedWorker = workerString === 'shared' ? new TestSharedWorker() : undefined;
const serviceWorker = workerString === 'service' ? new TestServiceWorker() : undefined;
globalTestConfig.unrollConstEvalLoops = optionEnabled('unroll_const_eval_loops');
@ -68,6 +69,8 @@ void (async () => {
await dedicatedWorker.run(rec, name, expectations);
} else if (sharedWorker) {
await sharedWorker.run(rec, name, expectations);
} else if (serviceWorker) {
await serviceWorker.run(rec, name, expectations);
} else {
await testcase.run(rec, expectations);
}

View file

@ -144,6 +144,19 @@ app.get('/out/:suite([a-zA-Z0-9_-]+)/listing.js', async (req, res, next) => {
}
});
// Serve .worker.js files by generating the necessary wrapper.
app.get('/out/:suite([a-zA-Z0-9_-]+)/webworker/:filepath(*).worker.js', (req, res, next) => {
const { suite, filepath } = req.params;
const result = `\
import { g } from '/out/${suite}/${filepath}.spec.js';
import { wrapTestGroupForWorker } from '/out/common/runtime/helper/wrap_for_worker.js';
wrapTestGroupForWorker(g);
`;
res.setHeader('Content-Type', 'application/javascript');
res.send(result);
});
// Serve all other .js files by fetching the source .ts file and compiling it.
app.get('/out/**/*.js', async (req, res, next) => {
const jsUrl = path.relative('/out', req.url);

View file

@ -1,56 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';
import { crawl } from './crawl.js';
function usage(rc: number): void {
console.error(`Usage: tools/gen_listings [options] [OUT_DIR] [SUITE_DIRS...]
For each suite in SUITE_DIRS, generate listings and write each listing.js
into OUT_DIR/{suite}/listing.js. Example:
tools/gen_listings gen/ src/unittests/ src/webgpu/
Options:
--help Print this message and exit.
`);
process.exit(rc);
}
const argv = process.argv;
if (argv.indexOf('--help') !== -1) {
usage(0);
}
{
// Ignore old argument that is now the default
const i = argv.indexOf('--no-validate');
if (i !== -1) {
argv.splice(i, 1);
}
}
if (argv.length < 4) {
usage(0);
}
const myself = 'src/common/tools/gen_listings.ts';
const outDir = argv[2];
for (const suiteDir of argv.slice(3)) {
// Run concurrently for each suite (might be a tiny bit more efficient)
void crawl(suiteDir, false).then(listing => {
const suite = path.basename(suiteDir);
const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`));
fs.mkdirSync(path.join(outDir, suite), { recursive: true });
fs.writeFileSync(
outFile,
`\
// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
export const listing = ${JSON.stringify(listing, undefined, 2)};
`
);
});
}

View file

@ -0,0 +1,90 @@
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';
import { crawl } from './crawl.js';
function usage(rc: number): void {
console.error(`Usage: tools/gen_listings_and_webworkers [options] [OUT_DIR] [SUITE_DIRS...]
For each suite in SUITE_DIRS, generate listings into OUT_DIR/{suite}/listing.js,
and generate Web Worker proxies in OUT_DIR/{suite}/webworker/**/*.worker.js for
every .spec.js file. (Note {suite}/webworker/ is reserved for this purpose.)
Example:
tools/gen_listings_and_webworkers gen/ src/unittests/ src/webgpu/
Options:
--help Print this message and exit.
`);
process.exit(rc);
}
const argv = process.argv;
if (argv.indexOf('--help') !== -1) {
usage(0);
}
{
// Ignore old argument that is now the default
const i = argv.indexOf('--no-validate');
if (i !== -1) {
argv.splice(i, 1);
}
}
if (argv.length < 4) {
usage(0);
}
const myself = 'src/common/tools/gen_listings_and_webworkers.ts';
const outDir = argv[2];
for (const suiteDir of argv.slice(3)) {
// Run concurrently for each suite (might be a tiny bit more efficient)
void crawl(suiteDir, false).then(listing => {
const suite = path.basename(suiteDir);
// Write listing.js
const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`));
fs.mkdirSync(path.join(outDir, suite), { recursive: true });
fs.writeFileSync(
outFile,
`\
// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
export const listing = ${JSON.stringify(listing, undefined, 2)};
`
);
// Write suite/webworker/**/*.worker.js
for (const entry of listing) {
if ('readme' in entry) continue;
const outFileDir = path.join(
outDir,
suite,
'webworker',
...entry.file.slice(0, entry.file.length - 1)
);
const outFile = path.join(outDir, suite, 'webworker', ...entry.file) + '.worker.js';
const relPathToSuiteRoot = Array<string>(entry.file.length).fill('..').join('/');
fs.mkdirSync(outFileDir, { recursive: true });
fs.writeFileSync(
outFile,
`\
// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
// g is a TestGroup<Fixture> object (defined in common/internal/test_group.ts).
import { g } from '${relPathToSuiteRoot}/${entry.file.join('/')}.spec.js';
import { wrapTestGroupForWorker } from '${relPathToSuiteRoot}/../common/runtime/helper/wrap_for_worker.js';
wrapTestGroupForWorker(g);
`
);
}
});
}

2
src/demo/webworker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers.
*

2
src/manual/webworker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers.
*

View file

@ -1,110 +1,110 @@
{
"webgpu/shader/execution/binary/af_addition.bin": "b6797474",
"webgpu/shader/execution/binary/af_logical.bin": "ff27f50f",
"webgpu/shader/execution/binary/af_division.bin": "97ba2e6d",
"webgpu/shader/execution/binary/af_matrix_addition.bin": "842a508f",
"webgpu/shader/execution/binary/af_matrix_subtraction.bin": "f086bb00",
"webgpu/shader/execution/binary/af_multiplication.bin": "fb6d6475",
"webgpu/shader/execution/binary/af_remainder.bin": "aa462ca7",
"webgpu/shader/execution/binary/af_subtraction.bin": "f04fbb32",
"webgpu/shader/execution/binary/f16_addition.bin": "3f136f5a",
"webgpu/shader/execution/binary/f16_logical.bin": "8556deb1",
"webgpu/shader/execution/binary/f16_division.bin": "693df765",
"webgpu/shader/execution/binary/f16_matrix_addition.bin": "296a11b6",
"webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "cf1ca531",
"webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "d3a317bd",
"webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "a7a1b148",
"webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "8f540eee",
"webgpu/shader/execution/binary/f16_multiplication.bin": "1a7bca1c",
"webgpu/shader/execution/binary/f16_remainder.bin": "62b7bff8",
"webgpu/shader/execution/binary/f16_subtraction.bin": "c921e09d",
"webgpu/shader/execution/binary/f32_addition.bin": "7d7629bd",
"webgpu/shader/execution/binary/f32_logical.bin": "3e903bfb",
"webgpu/shader/execution/binary/f32_division.bin": "35b49326",
"webgpu/shader/execution/binary/f32_matrix_addition.bin": "1c2aff7c",
"webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "966b264e",
"webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "4ab99310",
"webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "77f37d5a",
"webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "50dfea9a",
"webgpu/shader/execution/binary/f32_multiplication.bin": "2524db25",
"webgpu/shader/execution/binary/f32_remainder.bin": "b47ad113",
"webgpu/shader/execution/binary/f32_subtraction.bin": "e98584ae",
"webgpu/shader/execution/binary/i32_arithmetic.bin": "88d15602",
"webgpu/shader/execution/binary/i32_comparison.bin": "ddd815a3",
"webgpu/shader/execution/binary/u32_arithmetic.bin": "d882b7f6",
"webgpu/shader/execution/binary/u32_comparison.bin": "e3dfeec7",
"webgpu/shader/execution/abs.bin": "b6f6d4ec",
"webgpu/shader/execution/acos.bin": "d681fe45",
"webgpu/shader/execution/acosh.bin": "a58482ca",
"webgpu/shader/execution/asin.bin": "3856670",
"webgpu/shader/execution/asinh.bin": "7567d4b7",
"webgpu/shader/execution/atan.bin": "80fc2c53",
"webgpu/shader/execution/atan2.bin": "2fc91f76",
"webgpu/shader/execution/atanh.bin": "b68b4e98",
"webgpu/shader/execution/bitcast.bin": "142a34e",
"webgpu/shader/execution/ceil.bin": "65c551a0",
"webgpu/shader/execution/clamp.bin": "369a3396",
"webgpu/shader/execution/cos.bin": "894e3d79",
"webgpu/shader/execution/cosh.bin": "3bf810f1",
"webgpu/shader/execution/cross.bin": "582e87e8",
"webgpu/shader/execution/degrees.bin": "762053b5",
"webgpu/shader/execution/determinant.bin": "a60ac4f9",
"webgpu/shader/execution/distance.bin": "602fc718",
"webgpu/shader/execution/dot.bin": "c33454b1",
"webgpu/shader/execution/exp.bin": "ad6670cf",
"webgpu/shader/execution/exp2.bin": "6f8a0b39",
"webgpu/shader/execution/faceForward.bin": "759ef028",
"webgpu/shader/execution/floor.bin": "d8f91dbe",
"webgpu/shader/execution/fma.bin": "5019731d",
"webgpu/shader/execution/fract.bin": "3a9c858d",
"webgpu/shader/execution/frexp.bin": "33ffb585",
"webgpu/shader/execution/inverseSqrt.bin": "3684c8a",
"webgpu/shader/execution/ldexp.bin": "30881102",
"webgpu/shader/execution/length.bin": "fed952",
"webgpu/shader/execution/log.bin": "b55f7cd0",
"webgpu/shader/execution/log2.bin": "e4fddf41",
"webgpu/shader/execution/max.bin": "ca77aba5",
"webgpu/shader/execution/min.bin": "6b57ac82",
"webgpu/shader/execution/mix.bin": "89f42f5a",
"webgpu/shader/execution/modf.bin": "77eb4ff5",
"webgpu/shader/execution/normalize.bin": "36214034",
"webgpu/shader/execution/pack2x16float.bin": "8bee7573",
"webgpu/shader/execution/pow.bin": "8bcaf788",
"webgpu/shader/execution/quantizeToF16.bin": "8c1bc956",
"webgpu/shader/execution/radians.bin": "c35eecd0",
"webgpu/shader/execution/reflect.bin": "3591dbf7",
"webgpu/shader/execution/refract.bin": "8b2a061d",
"webgpu/shader/execution/round.bin": "58787d04",
"webgpu/shader/execution/saturate.bin": "b5c39be6",
"webgpu/shader/execution/sign.bin": "3b6e8c88",
"webgpu/shader/execution/sin.bin": "1e39b6f9",
"webgpu/shader/execution/sinh.bin": "296767d4",
"webgpu/shader/execution/smoothstep.bin": "2771eb6a",
"webgpu/shader/execution/sqrt.bin": "78c4534c",
"webgpu/shader/execution/step.bin": "b8817ec0",
"webgpu/shader/execution/tan.bin": "f2537ac4",
"webgpu/shader/execution/tanh.bin": "c611a910",
"webgpu/shader/execution/transpose.bin": "7fdac41d",
"webgpu/shader/execution/trunc.bin": "da4434eb",
"webgpu/shader/execution/unpack2x16float.bin": "a5db67bb",
"webgpu/shader/execution/unpack2x16snorm.bin": "b0e6f0f4",
"webgpu/shader/execution/unpack2x16unorm.bin": "ddf30c4f",
"webgpu/shader/execution/unpack4x8snorm.bin": "599b3fb9",
"webgpu/shader/execution/unpack4x8unorm.bin": "d47c58be",
"webgpu/shader/execution/unary/af_arithmetic.bin": "790d7c95",
"webgpu/shader/execution/unary/af_assignment.bin": "679cfa9a",
"webgpu/shader/execution/unary/bool_conversion.bin": "d10f1dbb",
"webgpu/shader/execution/unary/f16_arithmetic.bin": "1b44c7f",
"webgpu/shader/execution/unary/f16_conversion.bin": "436a5636",
"webgpu/shader/execution/unary/f32_arithmetic.bin": "8f8fcd42",
"webgpu/shader/execution/unary/f32_conversion.bin": "97d0e21a",
"webgpu/shader/execution/unary/i32_arithmetic.bin": "41460aa",
"webgpu/shader/execution/unary/i32_conversion.bin": "d5a46ee6",
"webgpu/shader/execution/unary/u32_conversion.bin": "f03edff7",
"webgpu/shader/execution/unary/ai_assignment.bin": "5d612a40",
"webgpu/shader/execution/binary/ai_arithmetic.bin": "c0dd04f9",
"webgpu/shader/execution/unary/ai_arithmetic.bin": "3ea222b8",
"webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "3d9cf9b7",
"webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "a84ecc73",
"webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "6fe3e741"
"webgpu/shader/execution/binary/af_addition.bin": "1043cbb2",
"webgpu/shader/execution/binary/af_logical.bin": "9ab41597",
"webgpu/shader/execution/binary/af_division.bin": "9ec22a4e",
"webgpu/shader/execution/binary/af_matrix_addition.bin": "1da2ca22",
"webgpu/shader/execution/binary/af_matrix_subtraction.bin": "25303b8e",
"webgpu/shader/execution/binary/af_multiplication.bin": "70d8f01a",
"webgpu/shader/execution/binary/af_remainder.bin": "101c7835",
"webgpu/shader/execution/binary/af_subtraction.bin": "26791651",
"webgpu/shader/execution/binary/f16_addition.bin": "2d67db8f",
"webgpu/shader/execution/binary/f16_logical.bin": "80f9e754",
"webgpu/shader/execution/binary/f16_division.bin": "53e3bdaa",
"webgpu/shader/execution/binary/f16_matrix_addition.bin": "911d18f7",
"webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "a306da0a",
"webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "be77024a",
"webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "ea813df5",
"webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "1692bec1",
"webgpu/shader/execution/binary/f16_multiplication.bin": "7c618e51",
"webgpu/shader/execution/binary/f16_remainder.bin": "9de2be15",
"webgpu/shader/execution/binary/f16_subtraction.bin": "c77bff8a",
"webgpu/shader/execution/binary/f32_addition.bin": "d4282b93",
"webgpu/shader/execution/binary/f32_logical.bin": "47467130",
"webgpu/shader/execution/binary/f32_division.bin": "4d79a5f6",
"webgpu/shader/execution/binary/f32_matrix_addition.bin": "5972ccf0",
"webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "8bc3f04a",
"webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "7376da03",
"webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "c4785f3a",
"webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "20d4b1",
"webgpu/shader/execution/binary/f32_multiplication.bin": "7586ed6d",
"webgpu/shader/execution/binary/f32_remainder.bin": "59f46306",
"webgpu/shader/execution/binary/f32_subtraction.bin": "4311ba10",
"webgpu/shader/execution/binary/i32_arithmetic.bin": "d345f9c7",
"webgpu/shader/execution/binary/i32_comparison.bin": "6d4ae7e0",
"webgpu/shader/execution/binary/u32_arithmetic.bin": "163f8a38",
"webgpu/shader/execution/binary/u32_comparison.bin": "f6c1497b",
"webgpu/shader/execution/abs.bin": "63fa988e",
"webgpu/shader/execution/acos.bin": "47521b3e",
"webgpu/shader/execution/acosh.bin": "59762b6f",
"webgpu/shader/execution/asin.bin": "db0c6641",
"webgpu/shader/execution/asinh.bin": "f0329d7f",
"webgpu/shader/execution/atan.bin": "f6fea79e",
"webgpu/shader/execution/atan2.bin": "1c390d0f",
"webgpu/shader/execution/atanh.bin": "4c430905",
"webgpu/shader/execution/bitcast.bin": "2dfc8f9",
"webgpu/shader/execution/ceil.bin": "3319384e",
"webgpu/shader/execution/clamp.bin": "4873dd79",
"webgpu/shader/execution/cos.bin": "c16d181a",
"webgpu/shader/execution/cosh.bin": "137f4997",
"webgpu/shader/execution/cross.bin": "aebda306",
"webgpu/shader/execution/degrees.bin": "5001b340",
"webgpu/shader/execution/determinant.bin": "c461c715",
"webgpu/shader/execution/distance.bin": "d05e2f8d",
"webgpu/shader/execution/dot.bin": "7f8b7e13",
"webgpu/shader/execution/exp.bin": "d448dc91",
"webgpu/shader/execution/exp2.bin": "1d232c0e",
"webgpu/shader/execution/faceForward.bin": "87c0576c",
"webgpu/shader/execution/floor.bin": "c3a60f36",
"webgpu/shader/execution/fma.bin": "6b94384d",
"webgpu/shader/execution/fract.bin": "d49c3d50",
"webgpu/shader/execution/frexp.bin": "252e0615",
"webgpu/shader/execution/inverseSqrt.bin": "1deb1185",
"webgpu/shader/execution/ldexp.bin": "617e4cc4",
"webgpu/shader/execution/length.bin": "f7b51c96",
"webgpu/shader/execution/log.bin": "ae124706",
"webgpu/shader/execution/log2.bin": "c428e9f2",
"webgpu/shader/execution/max.bin": "a45914cd",
"webgpu/shader/execution/min.bin": "1017e86d",
"webgpu/shader/execution/mix.bin": "3f5ebb6e",
"webgpu/shader/execution/modf.bin": "fedc0c50",
"webgpu/shader/execution/normalize.bin": "60173a1e",
"webgpu/shader/execution/pack2x16float.bin": "e472a927",
"webgpu/shader/execution/pow.bin": "c5a2a9c1",
"webgpu/shader/execution/quantizeToF16.bin": "8d1cb7a6",
"webgpu/shader/execution/radians.bin": "94cbdf6c",
"webgpu/shader/execution/reflect.bin": "39a479a0",
"webgpu/shader/execution/refract.bin": "63e5b11a",
"webgpu/shader/execution/round.bin": "13bf324",
"webgpu/shader/execution/saturate.bin": "c368b1a7",
"webgpu/shader/execution/sign.bin": "5cd05b5c",
"webgpu/shader/execution/sin.bin": "85679e",
"webgpu/shader/execution/sinh.bin": "eaa61750",
"webgpu/shader/execution/smoothstep.bin": "bd06d7cc",
"webgpu/shader/execution/sqrt.bin": "997f5572",
"webgpu/shader/execution/step.bin": "87505049",
"webgpu/shader/execution/tan.bin": "9d7ca121",
"webgpu/shader/execution/tanh.bin": "8bfa913c",
"webgpu/shader/execution/transpose.bin": "7f8fa4c6",
"webgpu/shader/execution/trunc.bin": "9a054523",
"webgpu/shader/execution/unpack2x16float.bin": "ed71c71d",
"webgpu/shader/execution/unpack2x16snorm.bin": "89b4cd2d",
"webgpu/shader/execution/unpack2x16unorm.bin": "df3b3851",
"webgpu/shader/execution/unpack4x8snorm.bin": "aa10073",
"webgpu/shader/execution/unpack4x8unorm.bin": "2fe14b98",
"webgpu/shader/execution/unary/af_arithmetic.bin": "accfc1bd",
"webgpu/shader/execution/unary/af_assignment.bin": "cb3e120e",
"webgpu/shader/execution/unary/bool_conversion.bin": "d0c1e5a3",
"webgpu/shader/execution/unary/f16_arithmetic.bin": "310a05dd",
"webgpu/shader/execution/unary/f16_conversion.bin": "9d94aa72",
"webgpu/shader/execution/unary/f32_arithmetic.bin": "fe955d67",
"webgpu/shader/execution/unary/f32_conversion.bin": "918f6cb3",
"webgpu/shader/execution/unary/i32_arithmetic.bin": "a8649cbb",
"webgpu/shader/execution/unary/i32_conversion.bin": "e5157a69",
"webgpu/shader/execution/unary/u32_conversion.bin": "d07d0c20",
"webgpu/shader/execution/unary/ai_assignment.bin": "f62c765c",
"webgpu/shader/execution/binary/ai_arithmetic.bin": "43501242",
"webgpu/shader/execution/unary/ai_arithmetic.bin": "8e448c53",
"webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "7ea0df71",
"webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "4919c460",
"webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "33c10dad"
}

2
src/stress/webworker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers.
*

2
src/unittests/webworker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers.
*

View file

@ -2219,6 +2219,7 @@
"webgpu:web_platform,external_texture,video:importExternalTexture,sampleWithVideoFrameWithVisibleRectParam:*": { "subcaseMS": 29.160 },
"webgpu:web_platform,external_texture,video:importExternalTexture,sample_non_YUV_video_frame:*": { "subcaseMS": 36.270 },
"webgpu:web_platform,worker,worker:dedicated_worker:*": { "subcaseMS": 245.901 },
"webgpu:web_platform,worker,worker:service_worker:*": { "subcaseMS": 102.800 },
"webgpu:web_platform,worker,worker:shared_worker:*": { "subcaseMS": 26.801 },
"_end": ""
}

View file

@ -4,6 +4,9 @@ Tests WebGPU is available in a dedicated worker and a shared worker.
Note: The CTS test can be run respectively in a dedicated worker and a shared worker by
passing in worker=dedicated and worker=shared as a query parameter. These tests
are specifically to check that WebGPU is available in a dedicated worker and a shared worker.
TODO[2]: Figure out how to make these tests run in service workers (not actually
important unless service workers gain the ability to launch other workers).
`;
import { Fixture } from '../../../common/framework/fixture.js';
@ -12,24 +15,26 @@ import { assert } from '../../../common/util/util.js';
export const g = makeTestGroup(Fixture);
function isNode(): boolean {
return typeof process !== 'undefined' && process?.versions?.node !== undefined;
}
const isNode = typeof process !== 'undefined' && process?.versions?.node !== undefined;
// [1]: we load worker_launcher dynamically because ts-node support
// is using commonjs which doesn't support import.meta. Further,
// we need to put the url in a string and pass the string to import
// otherwise typescript tries to parse the file which again, fails.
// worker_launcher.js is excluded in node.tsconfig.json.
// [2]: That hack does not work in Service Workers.
const isServiceWorker = globalThis.constructor.name === 'ServiceWorkerGlobalScope';
g.test('dedicated_worker')
.desc(`test WebGPU is available in dedicated workers and check for basic functionality`)
.fn(async t => {
if (isNode()) {
t.skip('node does not support 100% compatible workers');
return;
}
// Note: we load worker_launcher dynamically because ts-node support
// is using commonjs which doesn't support import.meta. Further,
// we need to put the url in a string add pass the string to import
// otherwise typescript tries to parse the file which again, fails.
// worker_launcher.js is excluded in node.tsconfig.json.
t.skipIf(isNode, 'node does not support 100% compatible workers');
t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2]
const url = './worker_launcher.js';
const { launchDedicatedWorker } = await import(url);
const { launchDedicatedWorker } = await import(url); // [1]
const result = await launchDedicatedWorker();
assert(result.error === undefined, `should be no error from worker but was: ${result.error}`);
});
@ -37,17 +42,25 @@ g.test('dedicated_worker')
g.test('shared_worker')
.desc(`test WebGPU is available in shared workers and check for basic functionality`)
.fn(async t => {
if (isNode()) {
t.skip('node does not support 100% compatible workers');
return;
}
// Note: we load worker_launcher dynamically because ts-node support
// is using commonjs which doesn't support import.meta. Further,
// we need to put the url in a string add pass the string to import
// otherwise typescript tries to parse the file which again, fails.
// worker_launcher.js is excluded in node.tsconfig.json.
t.skipIf(isNode, 'node does not support 100% compatible workers');
t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2]
const url = './worker_launcher.js';
const { launchSharedWorker } = await import(url);
const { launchSharedWorker } = await import(url); // [1]
const result = await launchSharedWorker();
assert(result.error === undefined, `should be no error from worker but was: ${result.error}`);
});
g.test('service_worker')
.desc(`test WebGPU is available in service workers and check for basic functionality`)
.fn(async t => {
t.skipIf(isNode, 'node does not support 100% compatible workers');
t.skipIf(isServiceWorker, 'Service workers do not support this import() hack'); // [2]
const url = './worker_launcher.js';
const { launchServiceWorker } = await import(url); // [1]
const result = await launchServiceWorker();
assert(result.error === undefined, `should be no error from worker but was: ${result.error}`);
});

View file

@ -87,7 +87,7 @@ async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) {
}
self.onmessage = (ev: MessageEvent) => {
void reportTestResults.call(self, ev);
void reportTestResults.call(ev.source || self, ev);
};
self.onconnect = (event: MessageEvent) => {

View file

@ -1,3 +1,4 @@
import { SkipTestCase } from '../../../common/framework/fixture.js';
import { getDefaultRequestAdapterOptions } from '../../../common/util/navigator_gpu.js';
export type TestResult = {
@ -5,6 +6,10 @@ export type TestResult = {
};
export async function launchDedicatedWorker() {
if (typeof Worker === 'undefined') {
throw new SkipTestCase(`Worker undefined in context ${globalThis.constructor.name}`);
}
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/worker.js';
@ -18,6 +23,10 @@ export async function launchDedicatedWorker() {
}
export async function launchSharedWorker() {
if (typeof SharedWorker === 'undefined') {
throw new SkipTestCase(`SharedWorker undefined in context ${globalThis.constructor.name}`);
}
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const workerPath = selfPathDir + '/worker.js';
@ -33,3 +42,34 @@ export async function launchSharedWorker() {
});
return await promise;
}
export async function launchServiceWorker() {
if (typeof navigator === 'undefined' || typeof navigator.serviceWorker === 'undefined') {
throw new SkipTestCase(
`navigator.serviceWorker undefined in context ${globalThis.constructor.name}`
);
}
const selfPath = import.meta.url;
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
const serviceWorkerPath = selfPathDir + '/worker.js';
const registration = await navigator.serviceWorker.register(serviceWorkerPath, {
type: 'module',
});
await registration.update();
const promise = new Promise<TestResult>(resolve => {
navigator.serviceWorker.addEventListener(
'message',
ev => {
resolve(ev.data as TestResult);
void registration.unregister();
},
{ once: true }
);
});
registration.active?.postMessage({
defaultRequestAdapterOptions: getDefaultRequestAdapterOptions(),
});
return await promise;
}

2
src/webgpu/webworker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# DIRECTORY RESERVED FOR GENERATED FILES. See gen_listings_and_webworkers.
*

View file

@ -1,7 +0,0 @@
#!/usr/bin/env node
// Crawl a suite directory (e.g. src/webgpu/) to generate a listing.js containing
// the listing of test files in the suite.
require('../src/common/tools/setup-ts-in-node.js');
require('../src/common/tools/gen_listings.ts');

View file

@ -0,0 +1,8 @@
#!/usr/bin/env node
// Crawl a suite directory (e.g. src/webgpu/) to generate a listing.js containing
// the listing of test files in the suite, and some generated test files
// (like suite/serviceworker/**/*.spec.js).
require('../src/common/tools/setup-ts-in-node.js');
require('../src/common/tools/gen_listings_and_webworkers.ts');