From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../checkout/src/common/framework/data_cache.ts | 197 ++++++ .../cts/checkout/src/common/framework/fixture.ts | 370 ++++++++++ .../cts/checkout/src/common/framework/metadata.ts | 28 + .../src/common/framework/params_builder.ts | 389 +++++++++++ .../cts/checkout/src/common/framework/resources.ts | 110 +++ .../checkout/src/common/framework/test_config.ts | 32 + .../checkout/src/common/framework/test_group.ts | 1 + .../checkout/src/common/internal/file_loader.ts | 105 +++ .../src/common/internal/logging/log_message.ts | 44 ++ .../checkout/src/common/internal/logging/logger.ts | 30 + .../checkout/src/common/internal/logging/result.ts | 21 + .../common/internal/logging/test_case_recorder.ts | 184 +++++ .../checkout/src/common/internal/params_utils.ts | 138 ++++ .../checkout/src/common/internal/query/compare.ts | 95 +++ .../common/internal/query/encode_selectively.ts | 23 + .../src/common/internal/query/json_param_value.ts | 114 ++++ .../src/common/internal/query/parseQuery.ts | 155 +++++ .../checkout/src/common/internal/query/query.ts | 262 +++++++ .../src/common/internal/query/separators.ts | 14 + .../src/common/internal/query/stringify_params.ts | 44 ++ .../src/common/internal/query/validQueryPart.ts | 2 + .../cts/checkout/src/common/internal/stack.ts | 82 +++ .../cts/checkout/src/common/internal/test_group.ts | 754 +++++++++++++++++++++ .../src/common/internal/test_suite_listing.ts | 15 + .../tests/cts/checkout/src/common/internal/tree.ts | 671 ++++++++++++++++++ .../tests/cts/checkout/src/common/internal/util.ts | 10 + .../cts/checkout/src/common/internal/version.ts | 1 + .../src/common/internal/websocket_logger.ts | 52 ++ .../cts/checkout/src/common/runtime/cmdline.ts | 286 ++++++++ .../checkout/src/common/runtime/helper/options.ts | 129 ++++ .../cts/checkout/src/common/runtime/helper/sys.ts | 46 ++ .../common/runtime/helper/test_worker-worker.ts | 48 ++ .../src/common/runtime/helper/test_worker.ts | 49 ++ .../cts/checkout/src/common/runtime/server.ts | 236 +++++++ .../cts/checkout/src/common/runtime/standalone.ts | 679 +++++++++++++++++++ .../tests/cts/checkout/src/common/runtime/wpt.ts | 83 +++ .../checkout/src/common/templates/cts.https.html | 32 + .../cts/checkout/src/common/tools/.eslintrc.json | 11 + .../cts/checkout/src/common/tools/checklist.ts | 136 ++++ .../tests/cts/checkout/src/common/tools/crawl.ts | 167 +++++ .../cts/checkout/src/common/tools/dev_server.ts | 214 ++++++ .../cts/checkout/src/common/tools/gen_cache.ts | 198 ++++++ .../cts/checkout/src/common/tools/gen_listings.ts | 63 ++ .../checkout/src/common/tools/gen_wpt_cts_html.ts | 252 +++++++ .../cts/checkout/src/common/tools/image_utils.ts | 58 ++ .../src/common/tools/merge_listing_times.ts | 177 +++++ .../checkout/src/common/tools/run_wpt_ref_tests.ts | 446 ++++++++++++ .../checkout/src/common/tools/setup-ts-in-node.js | 51 ++ .../cts/checkout/src/common/tools/validate.ts | 36 + .../tests/cts/checkout/src/common/tools/version.ts | 4 + .../checkout/src/common/util/collect_garbage.ts | 58 ++ .../tests/cts/checkout/src/common/util/colors.ts | 127 ++++ .../cts/checkout/src/common/util/data_tables.ts | 129 ++++ .../cts/checkout/src/common/util/navigator_gpu.ts | 86 +++ .../cts/checkout/src/common/util/preprocessor.ts | 149 ++++ .../tests/cts/checkout/src/common/util/timeout.ts | 7 + .../tests/cts/checkout/src/common/util/types.ts | 97 +++ .../tests/cts/checkout/src/common/util/util.ts | 476 +++++++++++++ .../checkout/src/common/util/wpt_reftest_wait.ts | 24 + 59 files changed, 8497 insertions(+) create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/metadata.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/resources.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/framework/test_group.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/params_utils.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/encode_selectively.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/json_param_value.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/separators.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/stringify_params.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/query/validQueryPart.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/stack.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/util.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/version.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/helper/sys.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/templates/cts.https.html create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/.eslintrc.json create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/checklist.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/image_utils.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/run_wpt_ref_tests.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/setup-ts-in-node.js create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/tools/version.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/colors.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/types.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/util.ts create mode 100644 dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts (limited to 'dom/webgpu/tests/cts/checkout/src/common') 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..c1e3a889be --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/data_cache.ts @@ -0,0 +1,197 @@ +/** + * 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). + */ + +import { assert } from '../util/util.js'; + +interface DataStore { + load(path: string): Promise; +} + +/** Logger is a basic debug logger function */ +export type Logger = (s: string) => void; + +/** + * DataCacheNode represents a single cache entry in the LRU DataCache. + * DataCacheNode is a doubly linked list, so that least-recently-used entries can be removed, and + * cache hits can move the node to the front of the list. + */ +class DataCacheNode { + public constructor(path: string, data: unknown) { + this.path = path; + this.data = data; + } + + /** insertAfter() re-inserts this node in the doubly-linked list after `prev` */ + public insertAfter(prev: DataCacheNode) { + this.unlink(); + this.next = prev.next; + this.prev = prev; + prev.next = this; + if (this.next) { + this.next.prev = this; + } + } + + /** unlink() removes this node from the doubly-linked list */ + public unlink() { + const prev = this.prev; + const next = this.next; + if (prev) { + prev.next = next; + } + if (next) { + next.prev = prev; + } + this.prev = null; + this.next = null; + } + + public readonly path: string; // The file path this node represents + public readonly data: unknown; // The deserialized data for this node + public prev: DataCacheNode | null = null; // The previous node in the doubly-linked list + public next: DataCacheNode | null = null; // The next node in the doubly-linked list +} + +/** DataCache is an interface to a LRU-cached data store used to hold data cached by path */ +export class DataCache { + public constructor() { + this.lruHeadNode.next = this.lruTailNode; + this.lruTailNode.prev = this.lruHeadNode; + } + + /** 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(cacheable: Cacheable): Promise { + { + // First check the in-memory cache + const node = this.cache.get(cacheable.path); + if (node !== undefined) { + this.log('in-memory cache hit'); + node.insertAfter(this.lruHeadNode); + return Promise.resolve(node.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: Uint8Array | 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`); + const data = cacheable.deserialize(serialized); + this.addToCache(cacheable.path, data); + return data; + } + } + // Not found anywhere. Build the data, and cache for future lookup. + this.log(`cache: building (${cacheable.path})`); + const data = await cacheable.build(); + this.addToCache(cacheable.path, data); + return data; + } + + /** + * addToCache() creates a new node for `path` and `data`, inserting the new node at the front of + * the doubly-linked list. If the number of entries in the cache exceeds this.maxCount, then the + * least recently used entry is evicted + * @param path the file path for the data + * @param data the deserialized data + */ + private addToCache(path: string, data: unknown) { + if (this.cache.size >= this.maxCount) { + const toEvict = this.lruTailNode.prev; + assert(toEvict !== null); + toEvict.unlink(); + this.cache.delete(toEvict.path); + this.log(`evicting ${toEvict.path}`); + } + const node = new DataCacheNode(path, data); + node.insertAfter(this.lruHeadNode); + this.cache.set(path, node); + this.log(`added ${path}. new count: ${this.cache.size}`); + } + + private log(msg: string) { + if (this.debugLogger !== null) { + this.debugLogger(`DataCache: ${msg}`); + } + } + + // Max number of entries in the cache before LRU entries are evicted. + private readonly maxCount = 4; + + private cache = new Map(); + private lruHeadNode = new DataCacheNode('', null); // placeholder node (no path or data) + private lruTailNode = new DataCacheNode('', null); // placeholder node (no path or data) + private unavailableFiles = new Set(); + 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 { + /** 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; + + /** + * serialize() encodes `data` to a binary representation so that it can be stored in a cache file. + */ + serialize(data: Data): Uint8Array; + + /** + * deserialize() is the inverse of serialize(), decoding the binary representation back to a Data + * object. + */ + deserialize(binary: Uint8Array): 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..77875e047d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts @@ -0,0 +1,370 @@ +import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js'; +import { JSONWithUndefined } from '../internal/params_utils.js'; +import { assert, ExceptionCheckOptions, 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 { + constructor( + protected readonly recorder: TestCaseRecorder, + /** The case parameters for this test fixture shared state. Subcase params are not included. */ + public readonly params: TestParams + ) {} + + /** + * 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 { + private _params: unknown; + private _sharedState: S; + /** + * Interface for recording logs and test status. + * + * @internal + */ + readonly rec: TestCaseRecorder; + private eventualExpectations: Array> = []; + private numOutstandingAsyncExpectations = 0; + private objectsToCleanUp: DestroyableObject[] = []; + + public static MakeSharedState(recorder: TestCaseRecorder, params: TestParams): SubcaseBatchState { + return new SubcaseBatchState(recorder, 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 {} + + /** + * 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 { + 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(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(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); + } + + /** Throws an exception marking the subcase as skipped if condition is true */ + skipIf(cond: boolean, msg: string | (() => string) = '') { + if (cond) { + this.skip(typeof msg === 'function' ? msg() : 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(fn: () => Promise): Promise { + 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(fn: (niceStack: Error) => Promise): 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, 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, + { allowMissingStack = false, message }: ExceptionCheckOptions = {} + ): void { + this.eventualAsyncExpectation(async niceStack => { + const m = message ? ': ' + message : ''; + try { + await p; + niceStack.message = 'DID NOT REJECT' + m; + this.rec.expectationFailed(niceStack); + } catch (ex) { + this.expectErrorValue(expectedName, ex, niceStack); + if (!allowMissingStack) { + if (!(ex instanceof Error && typeof ex.stack === 'string')) { + const exMessage = ex instanceof Error ? ex.message : '?'; + niceStack.message = `rejected as expected, but missing stack (${exMessage})${m}`; + this.rec.expectationFailed(niceStack); + } + } + } + }); + } + + /** + * Expect that the provided function throws (if `true` or `string`) or not (if `false`). + * If a string is provided, expect that the throw exception has that name. + * + * MAINTENANCE_TODO: Change to `string | false` so the exception name is always checked. + */ + shouldThrow( + expectedError: string | boolean, + fn: () => void, + { allowMissingStack = false, message }: ExceptionCheckOptions = {} + ) { + const m = message ? ': ' + message : ''; + 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)); + if (!allowMissingStack) { + if (!(ex instanceof Error && typeof ex.stack === 'string')) { + this.rec.expectationFailed(new Error('threw as expected, but missing stack' + 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, + { mode = 'fail' }: { mode?: 'fail' | 'warn' } = {} + ) { + this.eventualAsyncExpectation(async niceStack => { + this.expectOK(await error, { mode, niceStack }); + }); + } +} + +export type SubcaseBatchStateFromFixture = F extends Fixture ? S : never; + +/** + * FixtureClass encapsulates a constructor for fixture and a corresponding + * shared state factory function. An interface version of the type is also + * defined for mixin declaration use ONLY. The interface version is necessary + * because mixin classes need a constructor with a single any[] rest + * parameter. + */ +export type FixtureClass = { + new (sharedState: SubcaseBatchStateFromFixture, log: TestCaseRecorder, params: TestParams): F; + MakeSharedState(recorder: TestCaseRecorder, params: TestParams): SubcaseBatchStateFromFixture; +}; +export type FixtureClassInterface = { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + new (...args: any[]): F; + MakeSharedState(recorder: TestCaseRecorder, params: TestParams): SubcaseBatchStateFromFixture; +}; +export type FixtureClassWithMixin = FC extends FixtureClass + ? FixtureClass + : never; diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/metadata.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/metadata.ts new file mode 100644 index 0000000000..2c2a1ef794 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/metadata.ts @@ -0,0 +1,28 @@ +import { assert } from '../util/util.js'; + +/** Metadata about tests (that can't be derived at runtime). */ +export type TestMetadata = { + /** + * Estimated average time-per-subcase, in milliseconds. + * This is used to determine chunking granularity when exporting to WPT with + * chunking enabled (like out-wpt/cts-chunked2sec.https.html). + */ + subcaseMS: number; +}; + +export type TestMetadataListing = { + [testQuery: string]: TestMetadata; +}; + +export function loadMetadataForSuite(suiteDir: string): TestMetadataListing | null { + assert(typeof require !== 'undefined', 'loadMetadataForSuite is only implemented on Node'); + const fs = require('fs'); + + const metadataFile = `${suiteDir}/listing_meta.json`; + if (!fs.existsSync(metadataFile)) { + return null; + } + + const metadata: TestMetadataListing = JSON.parse(fs.readFileSync(metadataFile, 'utf8')); + return metadata; +} 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..09a7d9c320 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts @@ -0,0 +1,389 @@ +import { Merged, mergeParams, mergeParamsChecked } from '../internal/params_utils.js'; +import { comparePublicParamsPaths, Ordering } from '../internal/query/compare.js'; +import { stringifyPublicParams } from '../internal/query/stringify_params.js'; +import { DeepReadonly } from '../util/types.js'; +import { assert, mapLazy, objectEquals } from '../util/util.js'; + +import { TestParams } from './fixture.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; + + /** + * 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; + + /** + * 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 + ? Merged + : T extends CaseParamsBuilder + ? CaseP + : never; + +// ================================================================ +// Implementation +// ================================================================ + +/** + * Iterable over pairs of either: + * - `[case params, Iterable]` if there are subcases. + * - `[case params, undefined]` if not. + */ +export type CaseSubcaseIterable = Iterable< + readonly [DeepReadonly, Iterable> | undefined] +>; + +/** + * Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`. + */ +export abstract class ParamsBuilderBase { + protected readonly cases: (caseFilter: TestParams | null) => Generator; + + constructor(cases: (caseFilter: TestParams | null) => Generator) { + this.cases = cases; + } + + /** + * Hidden from test files. Use `builderIterateCasesWithSubcases` to access this. + */ + protected abstract iterateCasesWithSubcases( + caseFilter: TestParams | null + ): CaseSubcaseIterable; +} + +/** + * Calls the (normally hidden) `iterateCasesWithSubcases()` method. + */ +export function builderIterateCasesWithSubcases( + builder: ParamsBuilderBase<{}, {}>, + caseFilter: TestParams | null +) { + interface IterableParamsBuilder { + iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<{}, {}>; + } + + return (builder as unknown as IterableParamsBuilder).iterateCasesWithSubcases(caseFilter); +} + +/** + * 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 + extends ParamsBuilderBase + implements Iterable>, ParamsBuilder +{ + *iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable { + for (const caseP of this.cases(caseFilter)) { + if (caseFilter) { + // this.cases() only filters out cases which conflict with caseFilter. Now that we have + // the final caseP, filter out cases which are missing keys that caseFilter requires. + const ordering = comparePublicParamsPaths(caseP, caseFilter); + if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) { + continue; + } + } + + yield [caseP as DeepReadonly, undefined]; + } + } + + [Symbol.iterator](): Iterator> { + return this.cases(null) as Iterator>; + } + + /** @inheritDoc */ + expandWithParams( + expander: (_: CaseP) => Iterable + ): CaseParamsBuilder> { + const baseGenerator = this.cases; + return new CaseParamsBuilder(function* (caseFilter) { + for (const a of baseGenerator(caseFilter)) { + for (const b of expander(a)) { + if (caseFilter) { + // If the expander generated any key-value pair that conflicts with caseFilter, skip. + const kvPairs = Object.entries(b); + if (kvPairs.some(([k, v]) => k in caseFilter && !objectEquals(caseFilter[k], v))) { + continue; + } + } + + yield mergeParamsChecked(a, b); + } + } + }); + } + + /** @inheritDoc */ + expand( + key: NewPKey, + expander: (_: CaseP) => Iterable + ): CaseParamsBuilder> { + const baseGenerator = this.cases; + return new CaseParamsBuilder(function* (caseFilter) { + for (const a of baseGenerator(caseFilter)) { + assert(!(key in a), `New key '${key}' already exists in ${JSON.stringify(a)}`); + + for (const v of expander(a)) { + // If the expander generated a value for this key that conflicts with caseFilter, skip. + if (caseFilter && key in caseFilter) { + if (!objectEquals(caseFilter[key], v)) { + continue; + } + } + yield { ...a, [key]: v } as Merged; + } + } + }); + } + + /** @inheritDoc */ + combineWithParams( + newParams: Iterable + ): CaseParamsBuilder> { + assertNotGenerator(newParams); + const seenValues = new Set(); + 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( + key: NewPKey, + values: Iterable + ): CaseParamsBuilder> { + assertNotGenerator(values); + const mapped = mapLazy(values, v => ({ [key]: v }) as { [name in NewPKey]: NewPValue }); + return this.combineWithParams(mapped); + } + + /** @inheritDoc */ + filter(pred: (_: CaseP) => boolean): CaseParamsBuilder { + const baseGenerator = this.cases; + return new CaseParamsBuilder(function* (caseFilter) { + for (const a of baseGenerator(caseFilter)) { + if (pred(a)) yield a; + } + }); + } + + /** @inheritDoc */ + unless(pred: (_: CaseP) => boolean): CaseParamsBuilder { + 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 { + 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 + extends ParamsBuilderBase + implements ParamsBuilder +{ + protected readonly subcases: (_: CaseP) => Generator; + + constructor( + cases: (caseFilter: TestParams | null) => Generator, + generator: (_: CaseP) => Generator + ) { + super(cases); + this.subcases = generator; + } + + *iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable { + for (const caseP of this.cases(caseFilter)) { + if (caseFilter) { + // this.cases() only filters out cases which conflict with caseFilter. Now that we have + // the final caseP, filter out cases which are missing keys that caseFilter requires. + const ordering = comparePublicParamsPaths(caseP, caseFilter); + if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) { + continue; + } + } + + const subcases = Array.from(this.subcases(caseP)); + if (subcases.length) { + yield [ + caseP as DeepReadonly, + subcases as DeepReadonly<(typeof subcases)[number]>[], + ]; + } + } + } + + /** @inheritDoc */ + expandWithParams( + expander: (_: Merged) => Iterable + ): SubcaseParamsBuilder> { + const baseGenerator = this.subcases; + return new SubcaseParamsBuilder(this.cases, function* (base) { + for (const a of baseGenerator(base)) { + for (const b of expander(mergeParams(base, a))) { + yield mergeParamsChecked(a, b); + } + } + }); + } + + /** @inheritDoc */ + expand( + key: NewPKey, + expander: (_: Merged) => Iterable + ): SubcaseParamsBuilder> { + const baseGenerator = this.subcases; + return new SubcaseParamsBuilder(this.cases, function* (base) { + for (const a of baseGenerator(base)) { + const before = mergeParams(base, a); + assert(!(key in before), () => `Key '${key}' already exists in ${JSON.stringify(before)}`); + + for (const v of expander(before)) { + yield { ...a, [key]: v } as Merged; + } + } + }); + } + + /** @inheritDoc */ + combineWithParams( + newParams: Iterable + ): SubcaseParamsBuilder> { + assertNotGenerator(newParams); + return this.expandWithParams(() => newParams); + } + + /** @inheritDoc */ + combine( + key: NewPKey, + values: Iterable + ): SubcaseParamsBuilder> { + assertNotGenerator(values); + return this.expand(key, () => values); + } + + /** @inheritDoc */ + filter(pred: (_: Merged) => boolean): SubcaseParamsBuilder { + const baseGenerator = this.subcases; + return new SubcaseParamsBuilder(this.cases, function* (base) { + for (const a of baseGenerator(base)) { + if (pred(mergeParams(base, a))) yield a; + } + }); + } + + /** @inheritDoc */ + unless(pred: (_: Merged) => boolean): SubcaseParamsBuilder { + return this.filter(x => !pred(x)); + } +} + +/** 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()`, ``, `