summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common/framework
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/webgpu/tests/cts/checkout/src/common/framework
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common/framework')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts120
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts328
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts337
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts110
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts20
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts1
6 files changed, 916 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts
new file mode 100644
index 0000000000..6f6e80288a
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts
@@ -0,0 +1,120 @@
+/**
+ * Utilities to improve the performance of the CTS, by caching data that is
+ * expensive to build using a two-level cache (in-memory, pre-computed file).
+ */
+
+interface DataStore {
+ load(path: string): Promise<string>;
+}
+
+/** Logger is a basic debug logger function */
+export type Logger = (s: string) => void;
+
+/** DataCache is an interface to a data store used to hold cached data */
+export class DataCache {
+ /** setDataStore() sets the backing data store used by the data cache */
+ public setStore(dataStore: DataStore) {
+ this.dataStore = dataStore;
+ }
+
+ /** setDebugLogger() sets the verbose logger */
+ public setDebugLogger(logger: Logger) {
+ this.debugLogger = logger;
+ }
+
+ /**
+ * fetch() retrieves cacheable data from the data cache, first checking the
+ * in-memory cache, then the data store (if specified), then resorting to
+ * building the data and storing it in the cache.
+ */
+ public async fetch<Data>(cacheable: Cacheable<Data>): Promise<Data> {
+ // First check the in-memory cache
+ let data = this.cache.get(cacheable.path);
+ if (data !== undefined) {
+ this.log('in-memory cache hit');
+ return Promise.resolve(data as Data);
+ }
+ this.log('in-memory cache miss');
+ // In in-memory cache miss.
+ // Next, try the data store.
+ if (this.dataStore !== null && !this.unavailableFiles.has(cacheable.path)) {
+ let serialized: string | undefined;
+ try {
+ serialized = await this.dataStore.load(cacheable.path);
+ this.log('loaded serialized');
+ } catch (err) {
+ // not found in data store
+ this.log(`failed to load (${cacheable.path}): ${err}`);
+ this.unavailableFiles.add(cacheable.path);
+ }
+ if (serialized !== undefined) {
+ this.log(`deserializing`);
+ data = cacheable.deserialize(serialized);
+ this.cache.set(cacheable.path, data);
+ return data as Data;
+ }
+ }
+ // Not found anywhere. Build the data, and cache for future lookup.
+ this.log(`cache: building (${cacheable.path})`);
+ data = await cacheable.build();
+ this.cache.set(cacheable.path, data);
+ return data as Data;
+ }
+
+ private log(msg: string) {
+ if (this.debugLogger !== null) {
+ this.debugLogger(`DataCache: ${msg}`);
+ }
+ }
+
+ private cache = new Map<string, unknown>();
+ private unavailableFiles = new Set<string>();
+ private dataStore: DataStore | null = null;
+ private debugLogger: Logger | null = null;
+}
+
+/** The data cache */
+export const dataCache = new DataCache();
+
+/** true if the current process is building the cache */
+let isBuildingDataCache = false;
+
+/** @returns true if the data cache is currently being built */
+export function getIsBuildingDataCache() {
+ return isBuildingDataCache;
+}
+
+/** Sets whether the data cache is currently being built */
+export function setIsBuildingDataCache(value = true) {
+ isBuildingDataCache = value;
+}
+
+/**
+ * Cacheable is the interface to something that can be stored into the
+ * DataCache.
+ * The 'npm run gen_cache' tool will look for module-scope variables of this
+ * interface, with the name `d`.
+ */
+export interface Cacheable<Data> {
+ /** the globally unique path for the cacheable data */
+ readonly path: string;
+
+ /**
+ * build() builds the cacheable data.
+ * This is assumed to be an expensive operation and will only happen if the
+ * cache does not already contain the built data.
+ */
+ build(): Promise<Data>;
+
+ /**
+ * serialize() transforms `data` to a string (usually JSON encoded) so that it
+ * can be stored in a text cache file.
+ */
+ serialize(data: Data): string;
+
+ /**
+ * deserialize() is the inverse of serialize(), transforming the string back
+ * to the Data object.
+ */
+ deserialize(serialized: string): Data;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts
new file mode 100644
index 0000000000..1368a3f96e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts
@@ -0,0 +1,328 @@
+import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
+import { JSONWithUndefined } from '../internal/params_utils.js';
+import { assert, unreachable } from '../util/util.js';
+
+export class SkipTestCase extends Error {}
+export class UnexpectedPassError extends Error {}
+
+export { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
+
+/** The fully-general type for params passed to a test function invocation. */
+export type TestParams = {
+ readonly [k: string]: JSONWithUndefined;
+};
+
+type DestroyableObject =
+ | { destroy(): void }
+ | { close(): void }
+ | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context };
+
+export class SubcaseBatchState {
+ private _params: TestParams;
+
+ constructor(params: TestParams) {
+ this._params = params;
+ }
+
+ /**
+ * Returns the case parameters for this test fixture shared state. Subcase params
+ * are not included.
+ */
+ get params(): TestParams {
+ return this._params;
+ }
+
+ /**
+ * Runs before the `.before()` function.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async init() {}
+ /**
+ * Runs between the `.before()` function and the subcases.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async postInit() {}
+ /**
+ * Runs after all subcases finish.
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async finalize() {}
+}
+
+/**
+ * A Fixture is a class used to instantiate each test sub/case at run time.
+ * A new instance of the Fixture is created for every single test subcase
+ * (i.e. every time the test function is run).
+ */
+export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
+ private _params: unknown;
+ private _sharedState: S;
+ /**
+ * Interface for recording logs and test status.
+ *
+ * @internal
+ */
+ protected rec: TestCaseRecorder;
+ private eventualExpectations: Array<Promise<unknown>> = [];
+ private numOutstandingAsyncExpectations = 0;
+ private objectsToCleanUp: DestroyableObject[] = [];
+
+ public static MakeSharedState(params: TestParams): SubcaseBatchState {
+ return new SubcaseBatchState(params);
+ }
+
+ /** @internal */
+ constructor(sharedState: S, rec: TestCaseRecorder, params: TestParams) {
+ this._sharedState = sharedState;
+ this.rec = rec;
+ this._params = params;
+ }
+
+ /**
+ * Returns the (case+subcase) parameters for this test function invocation.
+ */
+ get params(): unknown {
+ return this._params;
+ }
+
+ /**
+ * Gets the test fixture's shared state. This object is shared between subcases
+ * within the same testcase.
+ */
+ get sharedState(): S {
+ return this._sharedState;
+ }
+
+ /**
+ * Override this to do additional pre-test-function work in a derived fixture.
+ * This has to be a member function instead of an async `createFixture` function, because
+ * we need to be able to ergonomically override it in subclasses.
+ *
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async init(): Promise<void> {}
+
+ /**
+ * Override this to do additional post-test-function work in a derived fixture.
+ *
+ * Called even if init was unsuccessful.
+ *
+ * @internal MAINTENANCE_TODO: Make this not visible to test code?
+ */
+ async finalize(): Promise<void> {
+ assert(
+ this.numOutstandingAsyncExpectations === 0,
+ 'there were outstanding immediateAsyncExpectations (e.g. expectUncapturedError) at the end of the test'
+ );
+
+ // Loop to exhaust the eventualExpectations in case they chain off each other.
+ while (this.eventualExpectations.length) {
+ const p = this.eventualExpectations.shift()!;
+ try {
+ await p;
+ } catch (ex) {
+ this.rec.threw(ex);
+ }
+ }
+
+ // And clean up any objects now that they're done being used.
+ for (const o of this.objectsToCleanUp) {
+ if ('getExtension' in o) {
+ const WEBGL_lose_context = o.getExtension('WEBGL_lose_context');
+ if (WEBGL_lose_context) WEBGL_lose_context.loseContext();
+ } else if ('destroy' in o) {
+ o.destroy();
+ } else {
+ o.close();
+ }
+ }
+ }
+
+ /**
+ * Tracks an object to be cleaned up after the test finishes.
+ *
+ * MAINTENANCE_TODO: Use this in more places. (Will be easier once .destroy() is allowed on
+ * invalid objects.)
+ */
+ trackForCleanup<T extends DestroyableObject>(o: T): T {
+ this.objectsToCleanUp.push(o);
+ return o;
+ }
+
+ /** Tracks an object, if it's destroyable, to be cleaned up after the test finishes. */
+ tryTrackForCleanup<T>(o: T): T {
+ if (typeof o === 'object' && o !== null) {
+ if (
+ 'destroy' in o ||
+ 'close' in o ||
+ o instanceof WebGLRenderingContext ||
+ o instanceof WebGL2RenderingContext
+ ) {
+ this.objectsToCleanUp.push((o as unknown) as DestroyableObject);
+ }
+ }
+ return o;
+ }
+
+ /** Log a debug message. */
+ debug(msg: string): void {
+ this.rec.debug(new Error(msg));
+ }
+
+ /** Throws an exception marking the subcase as skipped. */
+ skip(msg: string): never {
+ throw new SkipTestCase(msg);
+ }
+
+ /** Log a warning and increase the result status to "Warn". */
+ warn(msg?: string): void {
+ this.rec.warn(new Error(msg));
+ }
+
+ /** Log an error and increase the result status to "ExpectFailed". */
+ fail(msg?: string): void {
+ this.rec.expectationFailed(new Error(msg));
+ }
+
+ /**
+ * Wraps an async function. Tracks its status to fail if the test tries to report a test status
+ * before the async work has finished.
+ */
+ protected async immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> {
+ this.numOutstandingAsyncExpectations++;
+ const ret = await fn();
+ this.numOutstandingAsyncExpectations--;
+ return ret;
+ }
+
+ /**
+ * Wraps an async function, passing it an `Error` object recording the original stack trace.
+ * The async work will be implicitly waited upon before reporting a test status.
+ */
+ protected eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void {
+ const promise = fn(new Error());
+ this.eventualExpectations.push(promise);
+ }
+
+ private expectErrorValue(expectedError: string | true, ex: unknown, niceStack: Error): void {
+ if (!(ex instanceof Error)) {
+ niceStack.message = `THREW non-error value, of type ${typeof ex}: ${ex}`;
+ this.rec.expectationFailed(niceStack);
+ return;
+ }
+ const actualName = ex.name;
+ if (expectedError !== true && actualName !== expectedError) {
+ niceStack.message = `THREW ${actualName}, instead of ${expectedError}: ${ex}`;
+ this.rec.expectationFailed(niceStack);
+ } else {
+ niceStack.message = `OK: threw ${actualName}: ${ex.message}`;
+ this.rec.debug(niceStack);
+ }
+ }
+
+ /** Expect that the provided promise resolves (fulfills). */
+ shouldResolve(p: Promise<unknown>, msg?: string): void {
+ this.eventualAsyncExpectation(async niceStack => {
+ const m = msg ? ': ' + msg : '';
+ try {
+ await p;
+ niceStack.message = 'resolved as expected' + m;
+ } catch (ex) {
+ niceStack.message = `REJECTED${m}`;
+ if (ex instanceof Error) {
+ niceStack.message += '\n' + ex.message;
+ }
+ this.rec.expectationFailed(niceStack);
+ }
+ });
+ }
+
+ /** Expect that the provided promise rejects, with the provided exception name. */
+ shouldReject(expectedName: string, p: Promise<unknown>, msg?: string): void {
+ this.eventualAsyncExpectation(async niceStack => {
+ const m = msg ? ': ' + msg : '';
+ try {
+ await p;
+ niceStack.message = 'DID NOT REJECT' + m;
+ this.rec.expectationFailed(niceStack);
+ } catch (ex) {
+ niceStack.message = 'rejected as expected' + m;
+ this.expectErrorValue(expectedName, ex, niceStack);
+ }
+ });
+ }
+
+ /**
+ * Expect that the provided function throws.
+ * If an `expectedName` is provided, expect that the throw exception has that name.
+ */
+ shouldThrow(expectedError: string | boolean, fn: () => void, msg?: string): void {
+ const m = msg ? ': ' + msg : '';
+ try {
+ fn();
+ if (expectedError === false) {
+ this.rec.debug(new Error('did not throw, as expected' + m));
+ } else {
+ this.rec.expectationFailed(new Error('unexpectedly did not throw' + m));
+ }
+ } catch (ex) {
+ if (expectedError === false) {
+ this.rec.expectationFailed(new Error('threw unexpectedly' + m));
+ } else {
+ this.expectErrorValue(expectedError, ex, new Error(m));
+ }
+ }
+ }
+
+ /** Expect that a condition is true. */
+ expect(cond: boolean, msg?: string): boolean {
+ if (cond) {
+ const m = msg ? ': ' + msg : '';
+ this.rec.debug(new Error('expect OK' + m));
+ } else {
+ this.rec.expectationFailed(new Error(msg));
+ }
+ return cond;
+ }
+
+ /**
+ * If the argument is an `Error`, fail (or warn). If it's `undefined`, no-op.
+ * If the argument is an array, apply the above behavior on each of elements.
+ */
+ expectOK(
+ error: Error | undefined | (Error | undefined)[],
+ { mode = 'fail', niceStack }: { mode?: 'fail' | 'warn'; niceStack?: Error } = {}
+ ): void {
+ const handleError = (error: Error | undefined) => {
+ if (error instanceof Error) {
+ if (niceStack) {
+ error.stack = niceStack.stack;
+ }
+ if (mode === 'fail') {
+ this.rec.expectationFailed(error);
+ } else if (mode === 'warn') {
+ this.rec.warn(error);
+ } else {
+ unreachable();
+ }
+ }
+ };
+
+ if (Array.isArray(error)) {
+ for (const e of error) {
+ handleError(e);
+ }
+ } else {
+ handleError(error);
+ }
+ }
+
+ eventualExpectOK(
+ error: Promise<Error | undefined | (Error | undefined)[]>,
+ { mode = 'fail' }: { mode?: 'fail' | 'warn' } = {}
+ ) {
+ this.eventualAsyncExpectation(async niceStack => {
+ this.expectOK(await error, { mode, niceStack });
+ });
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts
new file mode 100644
index 0000000000..d22444a9b6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts
@@ -0,0 +1,337 @@
+import { Merged, mergeParams } from '../internal/params_utils.js';
+import { stringifyPublicParams } from '../internal/query/stringify_params.js';
+import { assert, mapLazy } from '../util/util.js';
+
+// ================================================================
+// "Public" ParamsBuilder API / Documentation
+// ================================================================
+
+/**
+ * Provides doc comments for the methods of CaseParamsBuilder and SubcaseParamsBuilder.
+ * (Also enforces rough interface match between them.)
+ */
+export interface ParamsBuilder {
+ /**
+ * Expands each item in `this` into zero or more items.
+ * Each item has its parameters expanded with those returned by the `expander`.
+ *
+ * **Note:** When only a single key is being added, use the simpler `expand` for readability.
+ *
+ * ```text
+ * this = [ a , b , c ]
+ * this.map(expander) = [ f(a) f(b) f(c) ]
+ * = [[a1, a2, a3] , [ b1 ] , [] ]
+ * merge and flatten = [ merge(a, a1), merge(a, a2), merge(a, a3), merge(b, b1) ]
+ * ```
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ expandWithParams(expander: (_: any) => any): any;
+
+ /**
+ * Expands each item in `this` into zero or more items. Each item has its parameters expanded
+ * with one new key, `key`, and the values returned by `expander`.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ expand(key: string, expander: (_: any) => any): any;
+
+ /**
+ * Expands each item in `this` to multiple items, one for each item in `newParams`.
+ *
+ * In other words, takes the cartesian product of [ the items in `this` ] and `newParams`.
+ *
+ * **Note:** When only a single key is being added, use the simpler `combine` for readability.
+ *
+ * ```text
+ * this = [ {a:1}, {b:2} ]
+ * newParams = [ {x:1}, {y:2} ]
+ * this.combineP(newParams) = [ {a:1,x:1}, {a:1,y:2}, {b:2,x:1}, {b:2,y:2} ]
+ * ```
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ combineWithParams(newParams: Iterable<any>): any;
+
+ /**
+ * Expands each item in `this` to multiple items with `{ [name]: value }` for each value.
+ *
+ * In other words, takes the cartesian product of [ the items in `this` ]
+ * and `[ {[name]: value} for each value in values ]`
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ combine(key: string, newParams: Iterable<any>): any;
+
+ /**
+ * Filters `this` to only items for which `pred` returns true.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ filter(pred: (_: any) => boolean): any;
+
+ /**
+ * Filters `this` to only items for which `pred` returns false.
+ */
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ unless(pred: (_: any) => boolean): any;
+}
+
+/**
+ * Determines the resulting parameter object type which would be generated by an object of
+ * the given ParamsBuilder type.
+ */
+export type ParamTypeOf<
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ T extends ParamsBuilder
+> = T extends SubcaseParamsBuilder<infer CaseP, infer SubcaseP>
+ ? Merged<CaseP, SubcaseP>
+ : T extends CaseParamsBuilder<infer CaseP>
+ ? CaseP
+ : never;
+
+// ================================================================
+// Implementation
+// ================================================================
+
+/**
+ * Iterable over pairs of either:
+ * - `[case params, Iterable<subcase params>]` if there are subcases.
+ * - `[case params, undefined]` if not.
+ */
+export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
+ readonly [CaseP, Iterable<SubcaseP> | undefined]
+>;
+
+/**
+ * Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
+ */
+export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
+ protected readonly cases: () => Generator<CaseP>;
+
+ constructor(cases: () => Generator<CaseP>) {
+ this.cases = cases;
+ }
+
+ /**
+ * Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
+ */
+ protected abstract iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP>;
+}
+
+/**
+ * Calls the (normally hidden) `iterateCasesWithSubcases()` method.
+ */
+export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {}>) {
+ interface IterableParamsBuilder {
+ iterateCasesWithSubcases(): CaseSubcaseIterable<{}, {}>;
+ }
+
+ return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases();
+}
+
+/**
+ * Builder for combinatorial test **case** parameters.
+ *
+ * CaseParamsBuilder is immutable. Each method call returns a new, immutable object,
+ * modifying the list of cases according to the method called.
+ *
+ * This means, for example, that the `unit` passed into `TestBuilder.params()` can be reused.
+ */
+export class CaseParamsBuilder<CaseP extends {}>
+ extends ParamsBuilderBase<CaseP, {}>
+ implements Iterable<CaseP>, ParamsBuilder {
+ *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, {}> {
+ for (const a of this.cases()) {
+ yield [a, undefined];
+ }
+ }
+
+ [Symbol.iterator](): Iterator<CaseP> {
+ return this.cases();
+ }
+
+ /** @inheritDoc */
+ expandWithParams<NewP extends {}>(
+ expander: (_: Merged<{}, CaseP>) => Iterable<NewP>
+ ): CaseParamsBuilder<Merged<CaseP, NewP>> {
+ const newGenerator = expanderGenerator(this.cases, expander);
+ return new CaseParamsBuilder(() => newGenerator({}));
+ }
+
+ /** @inheritDoc */
+ expand<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ expander: (_: Merged<{}, CaseP>) => Iterable<NewPValue>
+ ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
+ return this.expandWithParams(function* (p) {
+ for (const value of expander(p)) {
+ yield { [key]: value } as { readonly [name in NewPKey]: NewPValue };
+ }
+ });
+ }
+
+ /** @inheritDoc */
+ combineWithParams<NewP extends {}>(
+ newParams: Iterable<NewP>
+ ): CaseParamsBuilder<Merged<CaseP, NewP>> {
+ assertNotGenerator(newParams);
+ const seenValues = new Set<string>();
+ for (const params of newParams) {
+ const paramsStr = stringifyPublicParams(params);
+ assert(!seenValues.has(paramsStr), `Duplicate entry in combine[WithParams]: ${paramsStr}`);
+ seenValues.add(paramsStr);
+ }
+
+ return this.expandWithParams(() => newParams);
+ }
+
+ /** @inheritDoc */
+ combine<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ values: Iterable<NewPValue>
+ ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
+ assertNotGenerator(values);
+ const mapped = mapLazy(values, v => ({ [key]: v } as { [name in NewPKey]: NewPValue }));
+ return this.combineWithParams(mapped);
+ }
+
+ /** @inheritDoc */
+ filter(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
+ const newGenerator = filterGenerator(this.cases, pred);
+ return new CaseParamsBuilder(() => newGenerator({}));
+ }
+
+ /** @inheritDoc */
+ unless(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
+ return this.filter(x => !pred(x));
+ }
+
+ /**
+ * "Finalize" the list of cases and begin defining subcases.
+ * Returns a new SubcaseParamsBuilder. Methods called on SubcaseParamsBuilder
+ * generate new subcases instead of new cases.
+ */
+ beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
+ return new SubcaseParamsBuilder(
+ () => this.cases(),
+ function* () {
+ yield {};
+ }
+ );
+ }
+}
+
+/**
+ * The unit CaseParamsBuilder, representing a single case with no params: `[ {} ]`.
+ *
+ * `punit` is passed to every `.params()`/`.paramsSubcasesOnly()` call, so `kUnitCaseParamsBuilder`
+ * is only explicitly needed if constructing a ParamsBuilder outside of a test builder.
+ */
+export const kUnitCaseParamsBuilder = new CaseParamsBuilder(function* () {
+ yield {};
+});
+
+/**
+ * Builder for combinatorial test _subcase_ parameters.
+ *
+ * SubcaseParamsBuilder is immutable. Each method call returns a new, immutable object,
+ * modifying the list of subcases according to the method called.
+ */
+export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
+ extends ParamsBuilderBase<CaseP, SubcaseP>
+ implements ParamsBuilder {
+ protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;
+
+ constructor(cases: () => Generator<CaseP>, generator: (_: CaseP) => Generator<SubcaseP>) {
+ super(cases);
+ this.subcases = generator;
+ }
+
+ *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP> {
+ for (const caseP of this.cases()) {
+ const subcases = Array.from(this.subcases(caseP));
+ if (subcases.length) {
+ yield [caseP, subcases];
+ }
+ }
+ }
+
+ /** @inheritDoc */
+ expandWithParams<NewP extends {}>(
+ expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
+ return new SubcaseParamsBuilder(this.cases, expanderGenerator(this.subcases, expander));
+ }
+
+ /** @inheritDoc */
+ expand<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
+ return this.expandWithParams(function* (p) {
+ for (const value of expander(p)) {
+ // TypeScript doesn't know here that NewPKey is always a single literal string type.
+ yield { [key]: value } as { [name in NewPKey]: NewPValue };
+ }
+ });
+ }
+
+ /** @inheritDoc */
+ combineWithParams<NewP extends {}>(
+ newParams: Iterable<NewP>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
+ assertNotGenerator(newParams);
+ return this.expandWithParams(() => newParams);
+ }
+
+ /** @inheritDoc */
+ combine<NewPKey extends string, NewPValue>(
+ key: NewPKey,
+ values: Iterable<NewPValue>
+ ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
+ assertNotGenerator(values);
+ return this.expand(key, () => values);
+ }
+
+ /** @inheritDoc */
+ filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
+ return new SubcaseParamsBuilder(this.cases, filterGenerator(this.subcases, pred));
+ }
+
+ /** @inheritDoc */
+ unless(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
+ return this.filter(x => !pred(x));
+ }
+}
+
+function expanderGenerator<Base, A, B>(
+ baseGenerator: (_: Base) => Generator<A>,
+ expander: (_: Merged<Base, A>) => Iterable<B>
+): (_: Base) => Generator<Merged<A, B>> {
+ return function* (base: Base) {
+ for (const a of baseGenerator(base)) {
+ for (const b of expander(mergeParams(base, a))) {
+ yield mergeParams(a, b);
+ }
+ }
+ };
+}
+
+function filterGenerator<Base, A>(
+ baseGenerator: (_: Base) => Generator<A>,
+ pred: (_: Merged<Base, A>) => boolean
+): (_: Base) => Generator<A> {
+ return function* (base: Base) {
+ for (const a of baseGenerator(base)) {
+ if (pred(mergeParams(base, a))) {
+ yield a;
+ }
+ }
+ };
+}
+
+/** Assert an object is not a Generator (a thing returned from a generator function). */
+function assertNotGenerator(x: object) {
+ if ('constructor' in x) {
+ assert(
+ x.constructor !== (function* () {})().constructor,
+ 'Argument must not be a generator, as generators are not reusable'
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts
new file mode 100644
index 0000000000..05451304b6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts
@@ -0,0 +1,110 @@
+/**
+ * Base path for resources. The default value is correct for non-worker WPT, but standalone and
+ * workers must access resources using a different base path, so this is overridden in
+ * `test_worker-worker.ts` and `standalone.ts`.
+ */
+let baseResourcePath = './resources';
+let crossOriginHost = '';
+
+function getAbsoluteBaseResourcePath(path: string) {
+ // Path is already an absolute one.
+ if (path[0] === '/') {
+ return path;
+ }
+
+ // Path is relative
+ const relparts = window.location.pathname.split('/');
+ relparts.pop();
+ const pathparts = path.split('/');
+
+ let i;
+ for (i = 0; i < pathparts.length; ++i) {
+ switch (pathparts[i]) {
+ case '':
+ break;
+ case '.':
+ break;
+ case '..':
+ relparts.pop();
+ break;
+ default:
+ relparts.push(pathparts[i]);
+ break;
+ }
+ }
+
+ return relparts.join('/');
+}
+
+function runningOnLocalHost(): boolean {
+ const hostname = window.location.hostname;
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
+}
+
+/**
+ * Get a path to a resource in the `resources` directory relative to the current execution context
+ * (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc but from cross origin host.
+ * Provide onlineUrl if the case running online.
+ * @internal MAINTENANCE_TODO: Cases may run in the LAN environment (not localhost but no internet
+ * access). We temporarily use `crossOriginHost` to configure the cross origin host name in that situation.
+ * But opening to auto-detect mechanism or other solutions.
+ */
+export function getCrossOriginResourcePath(pathRelativeToResourcesDir: string, onlineUrl = '') {
+ // A cross origin host has been configured. Use this to load resource.
+ if (crossOriginHost !== '') {
+ return (
+ crossOriginHost +
+ getAbsoluteBaseResourcePath(baseResourcePath) +
+ '/' +
+ pathRelativeToResourcesDir
+ );
+ }
+
+ // Using 'localhost' and '127.0.0.1' trick to load cross origin resource. Set cross origin host name
+ // to 'localhost' if case is not running in 'localhost' domain. Otherwise, use '127.0.0.1'.
+ // host name to locahost unless the server running in
+ if (runningOnLocalHost()) {
+ let crossOriginHostName = '';
+ if (location.hostname === 'localhost') {
+ crossOriginHostName = 'http://127.0.0.1';
+ } else {
+ crossOriginHostName = 'http://localhost';
+ }
+
+ return (
+ crossOriginHostName +
+ ':' +
+ location.port +
+ getAbsoluteBaseResourcePath(baseResourcePath) +
+ '/' +
+ pathRelativeToResourcesDir
+ );
+ }
+
+ return onlineUrl;
+}
+
+/**
+ * Get a path to a resource in the `resources` directory, relative to the current execution context
+ * (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc. Pass the cross origin host
+ * name if wants to load resoruce from cross origin host.
+ */
+export function getResourcePath(pathRelativeToResourcesDir: string) {
+ return baseResourcePath + '/' + pathRelativeToResourcesDir;
+}
+
+/**
+ * Set the base resource path (path to the `resources` directory relative to the current
+ * execution context).
+ */
+export function setBaseResourcePath(path: string) {
+ baseResourcePath = path;
+}
+
+/**
+ * Set the cross origin host and cases related to cross origin
+ * will load resource from the given host.
+ */
+export function setCrossOriginHost(host: string) {
+ crossOriginHost = host;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts
new file mode 100644
index 0000000000..bec74e20c5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts
@@ -0,0 +1,20 @@
+export type TestConfig = {
+ maxSubcasesInFlight: number;
+ testHeartbeatCallback: () => void;
+ noRaceWithRejectOnTimeout: boolean;
+
+ /**
+ * Controls the emission of loops in constant-evaluation shaders under
+ * 'webgpu:shader,execution,expression,*'
+ * FXC is extremely slow to compile shaders with loops unrolled, where as the
+ * MSL compiler is extremely slow to compile with loops rolled.
+ */
+ unrollConstEvalLoops: boolean;
+};
+
+export const globalTestConfig: TestConfig = {
+ maxSubcasesInFlight: 500,
+ testHeartbeatCallback: () => {},
+ noRaceWithRejectOnTimeout: false,
+ unrollConstEvalLoops: false,
+};
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts
new file mode 100644
index 0000000000..5b761db9db
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts
@@ -0,0 +1 @@
+export { makeTestGroup } from '../internal/test_group.js';