diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common')
34 files changed, 1163 insertions, 355 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts index 77875e047d..616023e20c 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts @@ -15,7 +15,8 @@ export type TestParams = { type DestroyableObject = | { destroy(): void } | { close(): void } - | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context }; + | { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context } + | HTMLVideoElement; export class SubcaseBatchState { constructor( @@ -124,8 +125,12 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> { if (WEBGL_lose_context) WEBGL_lose_context.loseContext(); } else if ('destroy' in o) { o.destroy(); - } else { + } else if ('close' in o) { o.close(); + } else { + // HTMLVideoElement + o.src = ''; + o.srcObject = null; } } } @@ -161,6 +166,14 @@ export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> { this.rec.debug(new Error(msg)); } + /** + * Log an info message. + * **Use sparingly. Use `debug()` instead if logs are only needed with debug logging enabled.** + */ + info(msg: string): void { + this.rec.info(new Error(msg)); + } + /** Throws an exception marking the subcase as skipped. */ skip(msg: string): never { throw new SkipTestCase(msg); 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 index 2575418299..e6624ae120 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts @@ -1,4 +1,9 @@ export type TestConfig = { + /** + * Enable debug-level logs (normally logged via `Fixture.debug()`). + */ + enableDebugLogs: boolean; + maxSubcasesInFlight: number; testHeartbeatCallback: () => void; noRaceWithRejectOnTimeout: boolean; @@ -21,12 +26,25 @@ export type TestConfig = { * Whether or not we're running in compatibility mode. */ compatibility: boolean; + + /** + * Whether or not to request a fallback adapter. + */ + forceFallbackAdapter: boolean; + + /** + * Whether to enable the `logToWebSocket` function used for out-of-band test logging. + */ + logToWebSocket: boolean; }; export const globalTestConfig: TestConfig = { + enableDebugLogs: false, maxSubcasesInFlight: 500, testHeartbeatCallback: () => {}, noRaceWithRejectOnTimeout: false, unrollConstEvalLoops: false, compatibility: false, + forceFallbackAdapter: false, + logToWebSocket: false, }; diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts index b5e1b1a446..aae4b87995 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts @@ -73,8 +73,9 @@ export abstract class TestFileLoader extends EventTarget { query: TestQuery, { subqueriesToExpand = [], + fullyExpandSubtrees = [], maxChunkTime = Infinity, - }: { subqueriesToExpand?: string[]; maxChunkTime?: number } = {} + }: { subqueriesToExpand?: string[]; fullyExpandSubtrees?: string[]; maxChunkTime?: number } = {} ): Promise<TestTree> { const tree = await loadTreeForQuery(this, query, { subqueriesToExpand: subqueriesToExpand.map(s => { @@ -82,6 +83,7 @@ export abstract class TestFileLoader extends EventTarget { assert(q.level >= 2, () => `subqueriesToExpand entries should not be multi-file:\n ${q}`); return q; }), + fullyExpandSubtrees: fullyExpandSubtrees.map(s => parseQuery(s)), maxChunkTime, }); this.dispatchEvent(new MessageEvent<void>('finish')); diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts index ee006cdeb3..b01c08b56e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts @@ -1,19 +1,36 @@ import { ErrorWithExtra } from '../../util/util.js'; import { extractImportantStackTrace } from '../stack.js'; +import { LogMessageRawData } from './result.js'; + export class LogMessageWithStack extends Error { readonly extra: unknown; private stackHiddenMessage: string | undefined = undefined; - constructor(name: string, ex: Error | ErrorWithExtra) { - super(ex.message); + /** + * Wrap an Error (which was created to capture the stack at that point) into a + * LogMessageWithStack (which has extra stuff for good log messages). + * + * The original `ex.name` is ignored. Inclued it in the `name` parameter if it + * needs to be preserved. + */ + static wrapError(name: string, ex: Error | ErrorWithExtra) { + return new LogMessageWithStack({ + name, + message: ex.message, + stackHiddenMessage: undefined, + stack: ex.stack, + extra: 'extra' in ex ? ex.extra : undefined, + }); + } - this.name = name; - this.stack = ex.stack; - if ('extra' in ex) { - this.extra = ex.extra; - } + constructor(o: LogMessageRawData) { + super(o.message); + this.name = o.name; + this.stackHiddenMessage = o.stackHiddenMessage; + this.stack = o.stack; + this.extra = o.extra; } /** Set a flag so the stack is not printed in toJSON(). */ @@ -21,6 +38,11 @@ export class LogMessageWithStack extends Error { this.stackHiddenMessage ??= stackHiddenMessage; } + /** + * Print the message for display. + * + * Note: This is toJSON instead of toString to make it easy to save logs using JSON.stringify. + */ toJSON(): string { let m = this.name; if (this.message) m += ': ' + this.message; @@ -33,6 +55,21 @@ export class LogMessageWithStack extends Error { } return m; } + + /** + * Flatten the message for sending over a message channel. + * + * Note `extra` may get mangled by postMessage. + */ + toRawData(): LogMessageRawData { + return { + name: this.name, + message: this.message, + stackHiddenMessage: this.stackHiddenMessage, + stack: this.stack, + extra: this.extra, + }; + } } /** diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts index e4526cff54..6b95f48b74 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts @@ -1,3 +1,4 @@ +import { globalTestConfig } from '../../framework/test_config.js'; import { version } from '../version.js'; import { LiveTestCaseResult } from './result.js'; @@ -6,8 +7,6 @@ import { TestCaseRecorder } from './test_case_recorder.js'; export type LogResults = Map<string, LiveTestCaseResult>; export class Logger { - static globalDebugMode: boolean = false; - readonly overriddenDebugMode: boolean | undefined; readonly results: LogResults = new Map(); @@ -19,7 +18,7 @@ export class Logger { const result: LiveTestCaseResult = { status: 'running', timems: -1 }; this.results.set(name, result); return [ - new TestCaseRecorder(result, this.overriddenDebugMode ?? Logger.globalDebugMode), + new TestCaseRecorder(result, this.overriddenDebugMode ?? globalTestConfig.enableDebugLogs), result, ]; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts index 3318e8c937..9968f3d359 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts @@ -14,8 +14,24 @@ export interface LiveTestCaseResult extends TestCaseResult { logs?: LogMessageWithStack[]; } +/** + * Raw data for a test log message. + * + * This form is sendable over a message channel, except `extra` may get mangled. + */ +export interface LogMessageRawData { + name: string; + message: string; + stackHiddenMessage: string | undefined; + stack: string | undefined; + extra: unknown; +} + +/** + * Test case results in a form sendable over a message channel. + * + * Note `extra` may get mangled by postMessage. + */ export interface TransferredTestCaseResult extends TestCaseResult { - // When transferred from a worker, a LogMessageWithStack turns into a generic Error - // (its prototype gets lost and replaced with Error). - logs?: Error[]; + logs?: LogMessageRawData[]; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts index f5c3252b5c..78f625269e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts @@ -45,8 +45,6 @@ export class TestCaseRecorder { private logs: LogMessageWithStack[] = []; private logLinesAtCurrentSeverity = 0; private debugging = false; - /** Used to dedup log messages which have identical stacks. */ - private messagesForPreviouslySeenStacks = new Map<string, LogMessageWithStack>(); constructor(result: LiveTestCaseResult, debugging: boolean) { this.result = result; @@ -143,13 +141,15 @@ export class TestCaseRecorder { this.skipped(ex); return; } - this.logImpl(LogSeverity.ThrewException, 'EXCEPTION', ex); + // logImpl will discard the original error's ex.name. Preserve it here. + const name = ex instanceof Error ? `EXCEPTION: ${ex.name}` : 'EXCEPTION'; + this.logImpl(LogSeverity.ThrewException, name, ex); } private logImpl(level: LogSeverity, name: string, baseException: unknown): void { assert(baseException instanceof Error, 'test threw a non-Error object'); globalTestConfig.testHeartbeatCallback(); - const logMessage = new LogMessageWithStack(name, baseException); + const logMessage = LogMessageWithStack.wrapError(name, baseException); // Final case status should be the "worst" of all log entries. if (this.inSubCase) { diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts index a9419b87c1..f49833f5a2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts @@ -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) { diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts index 996835b0ec..0a9b355804 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts @@ -17,12 +17,49 @@ import { import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js'; import { validQueryPart } from './validQueryPart.js'; -export function parseQuery(s: string): TestQuery { +/** + * converts foo/bar/src/webgpu/this/that/file.spec.ts to webgpu:this,that,file,* + */ +function convertPathToQuery(path: string) { + // removes .spec.ts and splits by directory separators. + const parts = path.substring(0, path.length - 8).split(/\/|\\/g); + // Gets parts only after the last `src`. Example: returns ['webgpu', 'foo', 'bar', 'test'] + // for ['Users', 'me', 'src', 'cts', 'src', 'webgpu', 'foo', 'bar', 'test'] + const partsAfterSrc = parts.slice(parts.lastIndexOf('src') + 1); + const suite = partsAfterSrc.shift(); + return `${suite}:${partsAfterSrc.join(',')},*`; +} + +/** + * If a query looks like a path (ends in .spec.ts and has directory separators) + * then convert try to convert it to a query. + */ +function convertPathLikeToQuery(queryOrPath: string) { + return queryOrPath.endsWith('.spec.ts') && + (queryOrPath.includes('/') || queryOrPath.includes('\\')) + ? convertPathToQuery(queryOrPath) + : queryOrPath; +} + +/** + * Convert long suite names (the part before the first colon) to the + * shortest last word + * foo.bar.moo:test,subtest,foo -> moo:test,subtest,foo + */ +function shortenSuiteName(query: string) { + const parts = query.split(':'); + // converts foo.bar.moo to moo + const suite = parts.shift()?.replace(/.*\.(\w+)$/, '$1'); + return [suite, ...parts].join(':'); +} + +export function parseQuery(queryLike: string): TestQuery { try { - return parseQueryImpl(s); + const query = shortenSuiteName(convertPathLikeToQuery(queryLike)); + return parseQueryImpl(query); } catch (ex) { if (ex instanceof Error) { - ex.message += '\n on: ' + s; + ex.message += `\n on: ${queryLike}`; } throw ex; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts index 7c72a62f88..676ac46d38 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts @@ -1,5 +1,5 @@ import { TestParams } from '../../framework/fixture.js'; -import { optionEnabled } from '../../runtime/helper/options.js'; +import { optionWorkerMode } from '../../runtime/helper/options.js'; import { assert, unreachable } from '../../util/util.js'; import { Expectation } from '../logging/result.js'; @@ -188,12 +188,12 @@ export function parseExpectationsForTestQuery( assert( expectationURL.pathname === wptURL.pathname, `Invalid expectation path ${expectationURL.pathname} -Expectation should be of the form path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;... +Expectation should be of the form path/to/cts.https.html?debug=0&q=suite:test_path:test_name:foo=1;bar=2;... ` ); const params = expectationURL.searchParams; - if (optionEnabled('worker', params) !== optionEnabled('worker', wptURL.searchParams)) { + if (optionWorkerMode('worker', params) !== optionWorkerMode('worker', wptURL.searchParams)) { continue; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts index 632a822ef1..e1d0cde12d 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts @@ -34,7 +34,7 @@ import { validQueryPart } from '../internal/query/validQueryPart.js'; import { DeepReadonly } from '../util/types.js'; import { assert, unreachable } from '../util/util.js'; -import { logToWebsocket } from './websocket_logger.js'; +import { logToWebSocket } from './websocket_logger.js'; export type RunFn = ( rec: TestCaseRecorder, @@ -294,9 +294,11 @@ class TestBuilder<S extends SubcaseBatchState, F extends Fixture> { (this.description ? this.description + '\n\n' : '') + 'TODO: .unimplemented()'; this.isUnimplemented = true; - this.testFn = () => { + // Use the beforeFn to skip the test, so we don't have to iterate the subcases. + this.beforeFn = () => { throw new SkipTestCase('test unimplemented'); }; + this.testFn = () => {}; } /** Perform various validation/"lint" chenks. */ @@ -350,7 +352,7 @@ class TestBuilder<S extends SubcaseBatchState, F extends Fixture> { const testcaseStringUnique = stringifyPublicParamsUniquely(params); assert( !seen.has(testcaseStringUnique), - `Duplicate public test case+subcase params for test ${testPathString}: ${testcaseString}` + `Duplicate public test case+subcase params for test ${testPathString}: ${testcaseString} (${caseQuery})` ); seen.add(testcaseStringUnique); } @@ -737,7 +739,7 @@ class RunCaseSpecific implements RunCase { timems: rec.result.timems, nonskippedSubcaseCount: rec.nonskippedSubcaseCount, }; - logToWebsocket(JSON.stringify(msg)); + logToWebSocket(JSON.stringify(msg)); } } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts index 2d2b555366..c5a0e11448 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts @@ -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; diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts index 594837059c..f2fad59037 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts @@ -286,8 +286,9 @@ export async function loadTreeForQuery( queryToLoad: TestQuery, { subqueriesToExpand, + fullyExpandSubtrees = [], maxChunkTime = Infinity, - }: { subqueriesToExpand: TestQuery[]; maxChunkTime?: number } + }: { subqueriesToExpand: TestQuery[]; fullyExpandSubtrees?: TestQuery[]; maxChunkTime?: number } ): Promise<TestTree> { const suite = queryToLoad.suite; const specs = await loader.listing(suite); @@ -303,6 +304,10 @@ export async function loadTreeForQuery( // If toExpand == subquery, no expansion is needed (but it's still "seen"). if (ordering === Ordering.Equal) seenSubqueriesToExpand[i] = true; return ordering !== Ordering.StrictSubset; + }) && + fullyExpandSubtrees.every(toExpand => { + const ordering = compareQueries(toExpand, subquery); + return ordering === Ordering.Unordered; }); // L0 = suite-level, e.g. suite:* diff --git a/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts b/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts index 30246df843..373378e7c2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts @@ -1,3 +1,5 @@ +import { globalTestConfig } from '../framework/test_config.js'; + /** * - 'uninitialized' means we haven't tried to connect yet * - Promise means it's pending @@ -8,12 +10,15 @@ let connection: Promise<WebSocket | 'failed'> | WebSocket | 'failed' | 'uninitia 'uninitialized'; /** - * Log a string to a websocket at `localhost:59497`. See `tools/websocket-logger`. + * If the logToWebSocket option is enabled (?log_to_web_socket=1 in browser, + * --log-to-web-socket on command line, or enable it by default in options.ts), + * log a string to a websocket at `localhost:59497`. See `tools/websocket-logger`. * - * This does nothing if a connection couldn't be established on the first call. + * This does nothing if a logToWebSocket is not enabled, or if a connection + * couldn't be established on the first call. */ -export function logToWebsocket(msg: string) { - if (connection === 'failed') { +export function logToWebSocket(msg: string) { + if (!globalTestConfig.logToWebSocket || connection === 'failed') { return; } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts index 44a73fb38b..2a00640f0e 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { dataCache } from '../framework/data_cache.js'; +import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; import { globalTestConfig } from '../framework/test_config.js'; import { DefaultTestFileLoader } from '../internal/file_loader.js'; import { prettyPrintLog } from '../internal/logging/log_message.js'; @@ -37,6 +38,12 @@ Options: return sys.exit(rc); } +if (!sys.existsSync('src/common/runtime/cmdline.ts')) { + console.log('Must be run from repository root'); + usage(1); +} +setBaseResourcePath('out-node/resources'); + // The interface that exposes creation of the GPU, and optional interface to code coverage. interface GPUProviderModule { // @returns a GPU with the given flags @@ -60,12 +67,10 @@ Colors.enabled = false; let verbose = false; let emitCoverage = false; let listMode: listModes = 'none'; -let debug = false; let printJSON = false; let quiet = false; let loadWebGPUExpectations: Promise<unknown> | undefined = undefined; let gpuProviderModule: GPUProviderModule | undefined = undefined; -let dataPath: string | undefined = undefined; const queries: string[] = []; const gpuProviderFlags: string[] = []; @@ -83,9 +88,7 @@ for (let i = 0; i < sys.args.length; ++i) { } else if (a === '--list-unimplemented') { listMode = 'unimplemented'; } else if (a === '--debug') { - debug = true; - } else if (a === '--data') { - dataPath = sys.args[++i]; + globalTestConfig.enableDebugLogs = true; } else if (a === '--print-json') { printJSON = true; } else if (a === '--expectations') { @@ -102,6 +105,10 @@ for (let i = 0; i < sys.args.length; ++i) { globalTestConfig.unrollConstEvalLoops = true; } else if (a === '--compat') { globalTestConfig.compatibility = true; + } else if (a === '--force-fallback-adapter') { + globalTestConfig.forceFallbackAdapter = true; + } else if (a === '--log-to-websocket') { + globalTestConfig.logToWebSocket = true; } else { console.log('unrecognized flag: ', a); usage(1); @@ -113,9 +120,12 @@ for (let i = 0; i < sys.args.length; ++i) { let codeCoverage: CodeCoverageProvider | undefined = undefined; -if (globalTestConfig.compatibility) { +if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) { // MAINTENANCE_TODO: remove the cast once compatibilityMode is officially added - setDefaultRequestAdapterOptions({ compatibilityMode: true } as GPURequestAdapterOptions); + setDefaultRequestAdapterOptions({ + compatibilityMode: globalTestConfig.compatibility, + forceFallbackAdapter: globalTestConfig.forceFallbackAdapter, + } as GPURequestAdapterOptions); } if (gpuProviderModule) { @@ -132,21 +142,20 @@ Did you remember to build with code coverage instrumentation enabled?` } } -if (dataPath !== undefined) { - dataCache.setStore({ - load: (path: string) => { - return new Promise<Uint8Array>((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, (err, data) => { - if (err !== null) { - reject(err.message); - } else { - resolve(data); - } - }); +dataCache.setStore({ + load: (path: string) => { + return new Promise<Uint8Array>((resolve, reject) => { + fs.readFile(getResourcePath(`cache/${path}`), (err, data) => { + if (err !== null) { + reject(err.message); + } else { + resolve(data); + } }); - }, - }); -} + }); + }, +}); + if (verbose) { dataCache.setDebugLogger(console.log); } @@ -166,7 +175,6 @@ if (queries.length === 0) { filterQuery ); - Logger.globalDebugMode = debug; const log = new Logger(); const failed: Array<[string, LiveTestCaseResult]> = []; diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts index 38974b803f..4a82c7d292 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts @@ -1,3 +1,5 @@ +import { unreachable } from '../../util/util.js'; + let windowURL: URL | undefined = undefined; function getWindowURL() { if (windowURL === undefined) { @@ -6,6 +8,7 @@ function getWindowURL() { return windowURL; } +/** Parse a runner option that is always boolean-typed. False if missing or '0'. */ export function optionEnabled( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams @@ -14,30 +17,55 @@ export function optionEnabled( return val !== null && val !== '0'; } +/** Parse a runner option that is string-typed. If the option is missing, returns `null`. */ export function optionString( opt: string, searchParams: URLSearchParams = getWindowURL().searchParams -): string { - return searchParams.get(opt) || ''; +): string | null { + return searchParams.get(opt); +} + +/** Runtime modes for running tests in different types of workers. */ +export type WorkerMode = 'dedicated' | 'service' | 'shared'; +/** Parse a runner option for different worker modes (as in `?worker=shared`). Null if no worker. */ +export function optionWorkerMode( + opt: string, + searchParams: URLSearchParams = getWindowURL().searchParams +): WorkerMode | null { + const value = searchParams.get(opt); + if (value === null || value === '0') { + return null; + } else if (value === 'service') { + return 'service'; + } else if (value === 'shared') { + return 'shared'; + } else if (value === '' || value === '1' || value === 'dedicated') { + return 'dedicated'; + } + unreachable('invalid worker= option value'); } /** * The possible options for the tests. */ export interface CTSOptions { - worker: boolean; + worker: WorkerMode | null; debug: boolean; compatibility: boolean; + forceFallbackAdapter: boolean; unrollConstEvalLoops: boolean; - powerPreference?: GPUPowerPreference | ''; + powerPreference: GPUPowerPreference | null; + logToWebSocket: boolean; } export const kDefaultCTSOptions: CTSOptions = { - worker: false, + worker: null, debug: true, compatibility: false, + forceFallbackAdapter: false, unrollConstEvalLoops: false, - powerPreference: '', + powerPreference: null, + logToWebSocket: false, }; /** @@ -45,8 +73,8 @@ export const kDefaultCTSOptions: CTSOptions = { */ export interface OptionInfo { description: string; - parser?: (key: string, searchParams?: URLSearchParams) => boolean | string; - selectValueDescriptions?: { value: string; description: string }[]; + parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null; + selectValueDescriptions?: { value: string | null; description: string }[]; } /** @@ -59,19 +87,30 @@ export type OptionsInfos<Type> = Record<keyof Type, OptionInfo>; * Options to the CTS. */ export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = { - worker: { description: 'run in a worker' }, + worker: { + description: 'run in a worker', + parser: optionWorkerMode, + selectValueDescriptions: [ + { value: null, description: 'no worker' }, + { value: 'dedicated', description: 'dedicated worker' }, + { value: 'shared', description: 'shared worker' }, + { value: 'service', description: 'service worker' }, + ], + }, debug: { description: 'show more info' }, compatibility: { description: 'run in compatibility mode' }, + forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' }, unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' }, powerPreference: { description: 'set default powerPreference for some tests', parser: optionString, selectValueDescriptions: [ - { value: '', description: 'default' }, + { value: null, description: 'default' }, { value: 'low-power', description: 'low-power' }, { value: 'high-performance', description: 'high-performance' }, ], }, + logToWebSocket: { description: 'send some logs to ws://localhost:59497/' }, }; /** @@ -95,7 +134,7 @@ function getOptionsInfoFromSearchString<Type extends CTSOptions>( searchString: string ): Type { const searchParams = new URLSearchParams(searchString); - const optionValues: Record<string, boolean | string> = {}; + const optionValues: Record<string, boolean | string | null> = {}; for (const [optionName, info] of Object.entries(optionsInfos)) { const parser = info.parser || optionEnabled; optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams); diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts index e8d187ea7e..ebc206c3b2 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts @@ -1,15 +1,11 @@ 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 DedicatedWorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". +// Should be WorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom". /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ declare const self: any; @@ -17,25 +13,10 @@ const loader = new DefaultTestFileLoader(); setBaseResourcePath('../../../resources'); -self.onmessage = async (ev: MessageEvent) => { - const query: string = ev.data.query; - const expectations: TestQueryWithExpectation[] = ev.data.expectations; - const ctsOptions: CTSOptions = ev.data.ctsOptions; +async function reportTestResults(this: MessagePort | Worker, ev: MessageEvent) { + 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'); @@ -44,5 +25,23 @@ self.onmessage = async (ev: MessageEvent) => { const [rec, result] = log.record(testcase.query.toString()); await testcase.run(rec, expectations); - self.postMessage({ query, result }); + this.postMessage({ + query, + result: { + ...result, + logs: result.logs?.map(l => l.toRawData()), + }, + }); +} + +self.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(ev.source || self, ev); +}; + +self.onconnect = (event: MessageEvent) => { + const port = event.ports[0]; + + port.onmessage = (ev: MessageEvent) => { + void reportTestResults.call(port, ev); + }; }; diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts index 9bbcab0946..f9a44bb7bc 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts @@ -2,48 +2,190 @@ 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 { CTSOptions, WorkerMode, kDefaultCTSOptions } from './options.js'; +import { WorkerTestRunRequest } from './utils_worker.js'; -export class TestWorker { - private readonly ctsOptions: CTSOptions; - private readonly worker: Worker; - private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>(); +/** 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(); + } + }); +} - constructor(ctsOptions?: CTSOptions) { - this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker: true } }; - 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); +// 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(); +}); + +abstract class TestBaseWorker { + protected readonly ctsOptions: CTSOptions; + protected readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>(); + + constructor(worker: WorkerMode, ctsOptions?: CTSOptions) { + this.ctsOptions = { ...(ctsOptions || kDefaultCTSOptions), ...{ worker } }; + } - // 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). + onmessage(ev: MessageEvent) { + const query: string = ev.data.query; + const transferredResult: TransferredTestCaseResult = ev.data.result; + + const result: LiveTestCaseResult = { + status: transferredResult.status, + timems: transferredResult.timems, + logs: transferredResult.logs?.map(l => new LogMessageWithStack(l)), }; + + this.resolvers.get(query)!(result); + 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 run( - rec: TestCaseRecorder, + makeRequestAndRecordResult( + target: MessagePort | Worker | ServiceWorker, query: string, - expectations: TestQueryWithExpectation[] = [] - ): Promise<void> { - this.worker.postMessage({ + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult> { + const request: WorkerTestRunRequest = { query, expectations, ctsOptions: this.ctsOptions, - }); - const workerResult = await new Promise<LiveTestCaseResult>(resolve => { + }; + target.postMessage(request); + + return new Promise<LiveTestCaseResult>(resolve => { + assert(!this.resolvers.has(query), "can't request same query twice simultaneously"); this.resolvers.set(query, resolve); }); - rec.injectResult(workerResult); + } + + async run( + rec: TestCaseRecorder, + query: string, + expectations: TestQueryWithExpectation[] = [] + ): Promise<void> { + try { + rec.injectResult(await this.runImpl(query, expectations)); + } catch (ex) { + rec.start(); + rec.threw(ex); + rec.finish(); + } + } + + protected abstract runImpl( + query: string, + expectations: TestQueryWithExpectation[] + ): Promise<LiveTestCaseResult>; +} + +export class TestDedicatedWorker extends TestBaseWorker { + private readonly worker: Worker | Error; + + constructor(ctsOptions?: CTSOptions) { + super('dedicated', ctsOptions); + try { + if (typeof Worker === 'undefined') { + throw new Error('Dedicated Workers not available'); + } + + 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 => this.onmessage(ev); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.worker = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.worker instanceof Worker) { + return this.makeRequestAndRecordResult(this.worker, query, expectations); + } else { + throw this.worker; + } + } +} + +/** @deprecated Use TestDedicatedWorker instead. */ +export class TestWorker extends TestDedicatedWorker {} + +export class TestSharedWorker extends TestBaseWorker { + /** MessagePort to the SharedWorker, or an Error if it couldn't be initialized. */ + private readonly port: MessagePort | Error; + + constructor(ctsOptions?: CTSOptions) { + super('shared', ctsOptions); + try { + if (typeof SharedWorker === 'undefined') { + throw new Error('Shared Workers not available'); + } + + 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 => this.onmessage(ev); + } catch (ex) { + assert(ex instanceof Error); + // Save the exception to re-throw in runImpl(). + this.port = ex; + } + } + + override runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (this.port instanceof MessagePort) { + return this.makeRequestAndRecordResult(this.port, query, expectations); + } else { + throw this.port; + } + } +} + +export class TestServiceWorker extends TestBaseWorker { + constructor(ctsOptions?: CTSOptions) { + super('service', ctsOptions); + } + + override async runImpl(query: string, expectations: TestQueryWithExpectation[] = []) { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Workers not available'); + } + const [suite, name] = query.split(':', 2); + const fileName = name.split(',').join('/'); + + const selfPath = import.meta.url; + const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/')); + // Construct the path to the worker file, then use URL to resolve the `../` components. + const serviceWorkerURL = new URL( + `${selfPathDir}/../../../${suite}/webworker/${fileName}.worker.js` + ).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); + return this.makeRequestAndRecordResult(serviceWorker, query, expectations); } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts new file mode 100644 index 0000000000..13880635bc --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts @@ -0,0 +1,35 @@ +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 { powerPreference, compatibility } = ctsOptions; + globalTestConfig.enableDebugLogs = ctsOptions.debug; + globalTestConfig.unrollConstEvalLoops = ctsOptions.unrollConstEvalLoops; + globalTestConfig.compatibility = compatibility; + globalTestConfig.logToWebSocket = ctsOptions.logToWebSocket; + + 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; +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts new file mode 100644 index 0000000000..5f600fe89d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts @@ -0,0 +1,54 @@ +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'; + +/** + * Sets up the currently running Web Worker to wrap the TestGroup object `g`. + * `g` is the `g` exported from a `.spec.ts` file: a TestGroupBuilder<F> interface, + * which underneath is actually a TestGroup<F> object. + * + * This is used in the generated `.worker.js` files that are generated to use as service workers. + */ +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: [LogMessageWithStack.wrapError('INTERNAL', ex)], + }, + }); + } + }; +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts index 8310784e3a..3999b285ba 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts @@ -5,6 +5,7 @@ import * as http from 'http'; import { AddressInfo } from 'net'; import { dataCache } from '../framework/data_cache.js'; +import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; import { globalTestConfig } from '../framework/test_config.js'; import { DefaultTestFileLoader } from '../internal/file_loader.js'; import { prettyPrintLog } from '../internal/logging/log_message.js'; @@ -20,21 +21,23 @@ import sys from './helper/sys.js'; function usage(rc: number): never { console.log(`Usage: - tools/run_${sys.type} [OPTIONS...] + tools/server [OPTIONS...] Options: --colors Enable ANSI colors in output. --compat Run tests in compatibility mode. --coverage Add coverage data to each result. - --data Path to the data cache directory. --verbose Print result/log of every test as it runs. + --debug Include debug messages in logging. --gpu-provider Path to node module that provides the GPU implementation. --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value> --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests --u Flag to set on the gpu-provider as <flag>=<value> Provides an HTTP server used for running tests via an HTTP RPC interface -To run a test, perform an HTTP GET or POST at the URL: - http://localhost:port/run?<test-name> +First, load some tree or subtree of tests: + http://localhost:port/load?unittests:basic:* +To run a single test case, perform an HTTP GET or POST at the URL: + http://localhost:port/run?unittests:basic:test,sync To shutdown the server perform an HTTP GET or POST at the URL: http://localhost:port/terminate `); @@ -46,6 +49,8 @@ interface RunResult { status: Status; // Any additional messages printed message: string; + // The time it took to execute the test + durationMS: number; // Code coverage data, if the server was started with `--coverage` // This data is opaque (implementation defined). coverageData?: string; @@ -71,13 +76,13 @@ if (!sys.existsSync('src/common/runtime/cmdline.ts')) { console.log('Must be run from repository root'); usage(1); } +setBaseResourcePath('out-node/resources'); Colors.enabled = false; let emitCoverage = false; let verbose = false; let gpuProviderModule: GPUProviderModule | undefined = undefined; -let dataPath: string | undefined = undefined; const gpuProviderFlags: string[] = []; for (let i = 0; i < sys.args.length; ++i) { @@ -89,13 +94,17 @@ for (let i = 0; i < sys.args.length; ++i) { globalTestConfig.compatibility = true; } else if (a === '--coverage') { emitCoverage = true; - } else if (a === '--data') { - dataPath = sys.args[++i]; + } else if (a === '--force-fallback-adapter') { + globalTestConfig.forceFallbackAdapter = true; + } else if (a === '--log-to-websocket') { + globalTestConfig.logToWebSocket = true; } else if (a === '--gpu-provider') { const modulePath = sys.args[++i]; gpuProviderModule = require(modulePath); } else if (a === '--gpu-provider-flag') { gpuProviderFlags.push(sys.args[++i]); + } else if (a === '--debug') { + globalTestConfig.enableDebugLogs = true; } else if (a === '--unroll-const-eval-loops') { globalTestConfig.unrollConstEvalLoops = true; } else if (a === '--help') { @@ -110,9 +119,12 @@ for (let i = 0; i < sys.args.length; ++i) { let codeCoverage: CodeCoverageProvider | undefined = undefined; -if (globalTestConfig.compatibility) { +if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) { // MAINTENANCE_TODO: remove the cast once compatibilityMode is officially added - setDefaultRequestAdapterOptions({ compatibilityMode: true } as GPURequestAdapterOptions); + setDefaultRequestAdapterOptions({ + compatibilityMode: globalTestConfig.compatibility, + forceFallbackAdapter: globalTestConfig.forceFallbackAdapter, + } as GPURequestAdapterOptions); } if (gpuProviderModule) { @@ -130,28 +142,26 @@ Did you remember to build with code coverage instrumentation enabled?` } } -if (dataPath !== undefined) { - dataCache.setStore({ - load: (path: string) => { - return new Promise<Uint8Array>((resolve, reject) => { - fs.readFile(`${dataPath}/${path}`, (err, data) => { - if (err !== null) { - reject(err.message); - } else { - resolve(data); - } - }); +dataCache.setStore({ + load: (path: string) => { + return new Promise<Uint8Array>((resolve, reject) => { + fs.readFile(getResourcePath(`cache/${path}`), (err, data) => { + if (err !== null) { + reject(err.message); + } else { + resolve(data); + } }); - }, - }); -} + }); + }, +}); + if (verbose) { dataCache.setDebugLogger(console.log); } // eslint-disable-next-line @typescript-eslint/require-await (async () => { - Logger.globalDebugMode = verbose; const log = new Logger(); const testcases = new Map<string, TestTreeLeaf>(); @@ -198,14 +208,16 @@ if (verbose) { if (codeCoverage !== undefined) { codeCoverage.begin(); } + const start = performance.now(); const result = await runTestcase(testcase); + const durationMS = performance.now() - start; const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined; let message = ''; if (result.logs !== undefined) { message = result.logs.map(log => prettyPrintLog(log)).join('\n'); } const status = result.status; - const res: RunResult = { status, message, coverageData }; + const res: RunResult = { status, message, durationMS, coverageData }; response.statusCode = 200; response.end(JSON.stringify(res)); } else { diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts index 0376f92dda..dc75e6fd01 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts @@ -2,7 +2,7 @@ /* eslint no-console: "off" */ import { dataCache } from '../framework/data_cache.js'; -import { setBaseResourcePath } from '../framework/resources.js'; +import { getResourcePath, 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'; @@ -21,7 +21,7 @@ import { OptionsInfos, camelCaseToSnakeCase, } from './helper/options.js'; -import { TestWorker } from './helper/test_worker.js'; +import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from './helper/test_worker.js'; const rootQuerySpec = 'webgpu:*'; let promptBeforeReload = false; @@ -47,16 +47,26 @@ const { queries: qs, options } = parseSearchParamLikeWithOptions( kStandaloneOptionsInfos, window.location.search || rootQuerySpec ); -const { runnow, debug, unrollConstEvalLoops, powerPreference, compatibility } = options; -globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops; +const { runnow, powerPreference, compatibility, forceFallbackAdapter } = options; +globalTestConfig.enableDebugLogs = options.debug; +globalTestConfig.unrollConstEvalLoops = options.unrollConstEvalLoops; globalTestConfig.compatibility = compatibility; +globalTestConfig.logToWebSocket = options.logToWebSocket; -Logger.globalDebugMode = debug; const logger = new Logger(); setBaseResourcePath('../out/resources'); -const worker = options.worker ? new TestWorker(options) : undefined; +const testWorker = + options.worker === null + ? null + : options.worker === 'dedicated' + ? new TestDedicatedWorker(options) + : options.worker === 'shared' + ? new TestSharedWorker(options) + : options.worker === 'service' + ? new TestServiceWorker(options) + : unreachable(); const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement; const resultsVis = document.getElementById('resultsVis')!; @@ -70,17 +80,18 @@ stopButtonElem.addEventListener('click', () => { stopRequested = true; }); -if (powerPreference || compatibility) { +if (powerPreference || compatibility || forceFallbackAdapter) { setDefaultRequestAdapterOptions({ ...(powerPreference && { powerPreference }), // MAINTENANCE_TODO: Change this to whatever the option ends up being ...(compatibility && { compatibilityMode: true }), + ...(forceFallbackAdapter && { forceFallbackAdapter: true }), }); } dataCache.setStore({ load: async (path: string) => { - const response = await fetch(`data/${path}`); + const response = await fetch(getResourcePath(`cache/${path}`)); if (!response.ok) { return Promise.reject(response.statusText); } @@ -168,8 +179,8 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree { const [rec, res] = logger.record(name); caseResult = res; - if (worker) { - await worker.run(rec, name); + if (testWorker) { + await testWorker.run(rec, name); } else { await t.run(rec); } @@ -223,6 +234,12 @@ function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree { if (caseResult.logs) { caselogs.empty(); + // Show exceptions at the top since they are often unexpected can point out an error in the test itself vs the WebGPU implementation. + caseResult.logs + .filter(l => l.name === 'EXCEPTION') + .forEach(l => { + $('<pre>').addClass('testcaselogtext').text(l.toJSON()).appendTo(caselogs); + }); for (const l of caseResult.logs) { const caselog = $('<div>').addClass('testcaselog').appendTo(caselogs); $('<button>') @@ -500,13 +517,11 @@ function makeTreeNodeHeaderHTML( // Collapse s:f:t:* or s:f:t:c by default. let lastQueryLevelToExpand: TestQueryLevel = 2; -type ParamValue = string | undefined | null | boolean | string[]; - /** * Takes an array of string, ParamValue and returns an array of pairs * of [key, value] where value is a string. Converts boolean to '0' or '1'. */ -function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] { +function keyValueToPairs([k, v]: [string, boolean | string | null]): [string, string][] { const key = camelCaseToSnakeCase(k); if (typeof v === 'boolean') { return [[key, v ? '1' : '0']]; @@ -527,9 +542,9 @@ function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] { * @param params Some object with key value pairs. * @returns a search string. */ -function prepareParams(params: Record<string, ParamValue>): string { +function prepareParams(params: Record<string, boolean | string | null>): string { const pairsArrays = Object.entries(params) - .filter(([, v]) => !!v) + .filter(([, v]) => !(v === false || v === null || v === '0')) .map(keyValueToPairs); const pairs = pairsArrays.flat(); return new URLSearchParams(pairs).toString(); @@ -537,7 +552,7 @@ function prepareParams(params: Record<string, ParamValue>): string { // This is just a cast in one place. export function optionsToRecord(options: CTSOptions) { - return options as unknown as Record<string, boolean | string>; + return options as unknown as Record<string, boolean | string | null>; } /** @@ -597,15 +612,15 @@ void (async () => { }; const createSelect = (optionName: string, info: OptionInfo) => { - const select = $('<select>').on('change', function () { - optionValues[optionName] = (this as HTMLInputElement).value; + const select = $('<select>').on('change', function (this: HTMLSelectElement) { + optionValues[optionName] = JSON.parse(this.value); updateURLsWithCurrentOptions(); }); const currentValue = optionValues[optionName]; for (const { value, description } of info.selectValueDescriptions!) { $('<option>') .text(description) - .val(value) + .val(JSON.stringify(value)) .prop('selected', value === currentValue) .appendTo(select); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts index d4a4008154..79ed1b5924 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts @@ -8,8 +8,8 @@ import { parseQuery } from '../internal/query/parseQuery.js'; import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/query/query.js'; import { assert } from '../util/util.js'; -import { optionEnabled } from './helper/options.js'; -import { TestWorker } from './helper/test_worker.js'; +import { optionEnabled, optionWorkerMode } from './helper/options.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 { @@ -31,8 +31,10 @@ setup({ }); void (async () => { - const workerEnabled = optionEnabled('worker'); - const worker = workerEnabled ? new TestWorker() : undefined; + const workerString = optionWorkerMode('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'); @@ -63,8 +65,12 @@ void (async () => { const wpt_fn = async () => { const [rec, res] = log.record(name); - if (worker) { - await worker.run(rec, name, expectations); + if (dedicatedWorker) { + 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); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts index 50340dd68b..21a335b11c 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts @@ -45,13 +45,16 @@ async function crawlFilesRecursively(dir: string): Promise<string[]> { ); } -export async function crawl(suiteDir: string, validate: boolean): Promise<TestSuiteListingEntry[]> { +export async function crawl( + suiteDir: string, + opts: { validate: boolean; printMetadataWarnings: boolean } | null = null +): Promise<TestSuiteListingEntry[]> { if (!fs.existsSync(suiteDir)) { throw new Error(`Could not find suite: ${suiteDir}`); } let validateTimingsEntries; - if (validate) { + if (opts?.validate) { const metadata = loadMetadataForSuite(suiteDir); if (metadata) { validateTimingsEntries = { @@ -75,7 +78,7 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu const suite = path.basename(suiteDir); - if (validate) { + if (opts?.validate) { const filename = `../../${suite}/${filepathWithoutExtension}.spec.js`; assert(!process.env.STANDALONE_DEV_SERVER); @@ -109,8 +112,6 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu } if (validateTimingsEntries) { - let failed = false; - const zeroEntries = []; const staleEntries = []; for (const [metadataKey, metadataValue] of Object.entries(validateTimingsEntries.metadata)) { @@ -125,36 +126,39 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu staleEntries.push(metadataKey); } } - if (zeroEntries.length) { - console.warn('WARNING: subcaseMS≤0 found in listing_meta.json (allowed, but try to avoid):'); + if (zeroEntries.length && opts?.printMetadataWarnings) { + console.warn( + 'WARNING: subcaseMS ≤ 0 found in listing_meta.json (see docs/adding_timing_metadata.md):' + ); for (const metadataKey of zeroEntries) { console.warn(` ${metadataKey}`); } } - if (staleEntries.length) { - console.error('ERROR: Non-existent tests found in listing_meta.json:'); - for (const metadataKey of staleEntries) { - console.error(` ${metadataKey}`); - } - failed = true; - } - const missingEntries = []; - for (const metadataKey of validateTimingsEntries.testsFoundInFiles) { - if (!(metadataKey in validateTimingsEntries.metadata)) { - missingEntries.push(metadataKey); + if (opts?.printMetadataWarnings) { + const missingEntries = []; + for (const metadataKey of validateTimingsEntries.testsFoundInFiles) { + if (!(metadataKey in validateTimingsEntries.metadata)) { + missingEntries.push(metadataKey); + } + } + if (missingEntries.length) { + console.error( + 'WARNING: Tests missing from listing_meta.json (see docs/adding_timing_metadata.md):' + ); + for (const metadataKey of missingEntries) { + console.error(` ${metadataKey}`); + } } } - if (missingEntries.length) { - console.error( - 'ERROR: Tests missing from listing_meta.json. Please add the new tests (See docs/adding_timing_metadata.md):' - ); - for (const metadataKey of missingEntries) { + + if (staleEntries.length) { + console.error('ERROR: Non-existent tests found in listing_meta.json. Please update:'); + for (const metadataKey of staleEntries) { console.error(` ${metadataKey}`); - failed = true; } + unreachable(); } - assert(!failed); } return entries; @@ -163,5 +167,5 @@ export async function crawl(suiteDir: string, validate: boolean): Promise<TestSu export function makeListing(filename: string): Promise<TestSuiteListing> { // Don't validate. This path is only used for the dev server and running tests with Node. // Validation is done for listing generation and presubmit. - return crawl(path.dirname(filename), false); + return crawl(path.dirname(filename)); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts index 57cb6a7ea4..8e0e3bdbe6 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts @@ -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); diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts index ce0854aa20..d8309ebcb1 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts @@ -3,32 +3,41 @@ import * as path from 'path'; import * as process from 'process'; import { Cacheable, dataCache, setIsBuildingDataCache } from '../framework/data_cache.js'; +import { crc32, toHexString } from '../util/crc32.js'; +import { parseImports } from '../util/parse_imports.js'; function usage(rc: number): void { - console.error(`Usage: tools/gen_cache [options] [OUT_DIR] [SUITE_DIRS...] + console.error(`Usage: tools/gen_cache [options] [SUITE_DIRS...] For each suite in SUITE_DIRS, pre-compute data that is expensive to generate -at runtime and store it under OUT_DIR. If the data file is found then the -DataCache will load this instead of building the expensive data at CTS runtime. +at runtime and store it under 'src/resources/cache'. If the data file is found +then the DataCache will load this instead of building the expensive data at CTS +runtime. +Note: Due to differences in gzip compression, different versions of node can +produce radically different binary cache files. gen_cache uses the hashes of the +source files to determine whether a cache file is 'up to date'. This is faster +and does not depend on the compressed output. Options: --help Print this message and exit. --list Print the list of output files without writing them. - --nth i/n Only process every file where (file_index % n == i) - --validate Check that cache should build (Tests for collisions). + --force Rebuild cache even if they're up to date + --validate Check the cache is up to date --verbose Print each action taken. `); process.exit(rc); } +// Where the cache is generated +const outDir = 'src/resources/cache'; + +let forceRebuild = false; let mode: 'emit' | 'list' | 'validate' = 'emit'; -let nth = { i: 0, n: 1 }; let verbose = false; const nonFlagsArgs: string[] = []; -for (let i = 0; i < process.argv.length; i++) { - const arg = process.argv[i]; +for (const arg of process.argv) { if (arg.startsWith('-')) { switch (arg) { case '--list': { @@ -39,6 +48,10 @@ for (let i = 0; i < process.argv.length; i++) { usage(0); break; } + case '--force': { + forceRebuild = true; + break; + } case '--verbose': { verbose = true; break; @@ -47,28 +60,6 @@ for (let i = 0; i < process.argv.length; i++) { mode = 'validate'; break; } - case '--nth': { - const err = () => { - console.error( - `--nth requires a value of the form 'i/n', where i and n are positive integers and i < n` - ); - process.exit(1); - }; - i++; - if (i >= process.argv.length) { - err(); - } - const value = process.argv[i]; - const parts = value.split('/'); - if (parts.length !== 2) { - err(); - } - nth = { i: parseInt(parts[0]), n: parseInt(parts[1]) }; - if (nth.i < 0 || nth.n < 1 || nth.i > nth.n) { - err(); - } - break; - } default: { console.log('unrecognized flag: ', arg); usage(1); @@ -79,12 +70,10 @@ for (let i = 0; i < process.argv.length; i++) { } } -if (nonFlagsArgs.length < 4) { +if (nonFlagsArgs.length < 3) { usage(0); } -const outRootDir = nonFlagsArgs[2]; - dataCache.setStore({ load: (path: string) => { return new Promise<Uint8Array>((resolve, reject) => { @@ -100,57 +89,133 @@ dataCache.setStore({ }); setIsBuildingDataCache(); +const cacheFileSuffix = __filename.endsWith('.ts') ? '.cache.ts' : '.cache.js'; + +/** + * @returns a list of all the files under 'dir' that has the given extension + * @param dir the directory to search + * @param ext the extension of the files to find + */ +function glob(dir: string, ext: string) { + const files: string[] = []; + for (const file of fs.readdirSync(dir)) { + const path = `${dir}/${file}`; + if (fs.statSync(path).isDirectory()) { + for (const child of glob(path, ext)) { + files.push(`${file}/${child}`); + } + } + + if (path.endsWith(ext) && fs.statSync(path).isFile()) { + files.push(file); + } + } + return files; +} + +/** + * Exception type thrown by SourceHasher.hashFile() when a file annotated with + * MUST_NOT_BE_IMPORTED_BY_DATA_CACHE is transitively imported by a .cache.ts file. + */ +class InvalidImportException { + constructor(path: string) { + this.stack = [path]; + } + toString(): string { + return `invalid transitive import for cache:\n ${this.stack.join('\n ')}`; + } + readonly stack: string[]; +} +/** + * SourceHasher is a utility for producing a hash of a source .ts file and its imported source files. + */ +class SourceHasher { + /** + * @param path the source file path + * @returns a hash of the source file and all of its imported dependencies. + */ + public hashOf(path: string) { + this.u32Array[0] = this.hashFile(path); + return this.u32Array[0].toString(16); + } + + hashFile(path: string): number { + if (!fs.existsSync(path) && path.endsWith('.js')) { + path = path.substring(0, path.length - 2) + 'ts'; + } + + const cached = this.hashes.get(path); + if (cached !== undefined) { + return cached; + } + + this.hashes.set(path, 0); // Store a zero hash to handle cyclic imports + + const content = fs.readFileSync(path, { encoding: 'utf-8' }); + const normalized = content.replace('\r\n', '\n'); + let hash = crc32(normalized); + for (const importPath of parseImports(path, normalized)) { + try { + const importHash = this.hashFile(importPath); + hash = this.hashCombine(hash, importHash); + } catch (ex) { + if (ex instanceof InvalidImportException) { + ex.stack.push(path); + throw ex; + } + } + } + + if (content.includes('MUST_NOT_BE_IMPORTED_BY_DATA_CACHE')) { + throw new InvalidImportException(path); + } + + this.hashes.set(path, hash); + return hash; + } + + /** Simple non-cryptographic hash combiner */ + hashCombine(a: number, b: number): number { + return crc32(`${toHexString(a)} ${toHexString(b)}`); + } + + private hashes = new Map<string, number>(); + private u32Array = new Uint32Array(1); +} + void (async () => { - for (const suiteDir of nonFlagsArgs.slice(3)) { + const suiteDirs = nonFlagsArgs.slice(2); // skip <exe> <js> + for (const suiteDir of suiteDirs) { await build(suiteDir); } })(); -const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js'; - -async function crawlFilesRecursively(dir: string): Promise<string[]> { - const subpathInfo = await Promise.all( - (await fs.promises.readdir(dir)).map(async d => { - const p = path.join(dir, d); - const stats = await fs.promises.stat(p); - return { - path: p, - isDirectory: stats.isDirectory(), - isFile: stats.isFile(), - }; - }) - ); - - const files = subpathInfo - .filter(i => i.isFile && i.path.endsWith(specFileSuffix)) - .map(i => i.path); - - return files.concat( - await subpathInfo - .filter(i => i.isDirectory) - .map(i => crawlFilesRecursively(i.path)) - .reduce(async (a, b) => (await a).concat(await b), Promise.resolve([])) - ); -} - async function build(suiteDir: string) { if (!fs.existsSync(suiteDir)) { console.error(`Could not find ${suiteDir}`); process.exit(1); } - // Crawl files and convert paths to be POSIX-style, relative to suiteDir. - let filesToEnumerate = (await crawlFilesRecursively(suiteDir)).sort(); + // Load hashes.json + const fileHashJsonPath = `${outDir}/hashes.json`; + let fileHashes: Record<string, string> = {}; + if (fs.existsSync(fileHashJsonPath)) { + const json = fs.readFileSync(fileHashJsonPath, { encoding: 'utf8' }); + fileHashes = JSON.parse(json); + } - // Filter out non-spec files - filesToEnumerate = filesToEnumerate.filter(f => f.endsWith(specFileSuffix)); + // Crawl files and convert paths to be POSIX-style, relative to suiteDir. + const filesToEnumerate = glob(suiteDir, cacheFileSuffix) + .map(p => `${suiteDir}/${p}`) + .sort(); + const fileHasher = new SourceHasher(); const cacheablePathToTS = new Map<string, string>(); + const errors: Array<string> = []; - let fileIndex = 0; for (const file of filesToEnumerate) { - const pathWithoutExtension = file.substring(0, file.length - specFileSuffix.length); - const mod = await import(`../../../${pathWithoutExtension}.spec.js`); + const pathWithoutExtension = file.substring(0, file.length - 3); + const mod = await import(`../../../${pathWithoutExtension}.js`); if (mod.d?.serialize !== undefined) { const cacheable = mod.d as Cacheable<unknown>; @@ -158,41 +223,78 @@ async function build(suiteDir: string) { // Check for collisions const existing = cacheablePathToTS.get(cacheable.path); if (existing !== undefined) { - console.error( - `error: Cacheable '${cacheable.path}' is emitted by both: + errors.push( + `'${cacheable.path}' is emitted by both: '${existing}' and '${file}'` ); - process.exit(1); } cacheablePathToTS.set(cacheable.path, file); } - const outPath = `${outRootDir}/data/${cacheable.path}`; + const outPath = `${outDir}/${cacheable.path}`; + const fileHash = fileHasher.hashOf(file); - if (fileIndex++ % nth.n === nth.i) { - switch (mode) { - case 'emit': { + switch (mode) { + case 'emit': { + if (!forceRebuild && fileHashes[cacheable.path] === fileHash) { if (verbose) { - console.log(`building '${outPath}'`); + console.log(`'${outPath}' is up to date`); } - const data = await cacheable.build(); - const serialized = cacheable.serialize(data); - fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, serialized, 'binary'); - break; + continue; } - case 'list': { - console.log(outPath); - break; - } - case 'validate': { - // Only check currently performed is the collision detection above - break; + console.log(`building '${outPath}'`); + const data = await cacheable.build(); + const serialized = cacheable.serialize(data); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, serialized, 'binary'); + fileHashes[cacheable.path] = fileHash; + break; + } + case 'list': { + console.log(outPath); + break; + } + case 'validate': { + if (fileHashes[cacheable.path] !== fileHash) { + errors.push( + `'${outPath}' needs rebuilding. Generate with 'npx grunt run:generate-cache'` + ); + } else if (verbose) { + console.log(`'${outPath}' is up to date`); } } } } } + + // Check that there aren't stale files in the cache directory + for (const file of glob(outDir, '.bin')) { + if (cacheablePathToTS.get(file) === undefined) { + switch (mode) { + case 'emit': + fs.rmSync(file); + break; + case 'validate': + errors.push( + `cache file '${outDir}/${file}' is no longer generated. Remove with 'npx grunt run:generate-cache'` + ); + break; + } + } + } + + // Update hashes.json + if (mode === 'emit') { + const json = JSON.stringify(fileHashes, undefined, ' '); + fs.writeFileSync(fileHashJsonPath, json, { encoding: 'utf8' }); + } + + if (errors.length > 0) { + for (const error of errors) { + console.error(error); + } + process.exit(1); + } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts index fc5e1f3cde..7cc8cb78f3 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts @@ -9,7 +9,7 @@ function usage(rc: number): void { For each suite in SUITE_DIRS, generate listings and write each listing.js into OUT_DIR/{suite}/listing.js. Example: - tools/gen_listings out/ src/unittests/ src/webgpu/ + tools/gen_listings gen/ src/unittests/ src/webgpu/ Options: --help Print this message and exit. @@ -40,7 +40,7 @@ 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 => { + void crawl(suiteDir).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 }); @@ -52,12 +52,5 @@ for (const suiteDir of argv.slice(3)) { export const listing = ${JSON.stringify(listing, undefined, 2)}; ` ); - - // If there was a sourcemap for the file we just replaced, delete it. - try { - fs.unlinkSync(outFile + '.map'); - } catch (ex) { - // ignore if file didn't exist - } }); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts new file mode 100644 index 0000000000..04ce669de3 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts @@ -0,0 +1,89 @@ +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).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}. + +import { g } from '${relPathToSuiteRoot}/${entry.file.join('/')}.spec.js'; +import { wrapTestGroupForWorker } from '${relPathToSuiteRoot}/../common/runtime/helper/wrap_for_worker.js'; + +wrapTestGroupForWorker(g); +` + ); + } + }); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts index e8161304e9..46c2ae4354 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts @@ -23,6 +23,7 @@ gen_wpt_cts_html.ts. Example: { "suite": "webgpu", "out": "path/to/output/cts.https.html", + "outJSON": "path/to/output/webgpu_variant_list.json", "template": "path/to/template/cts.https.html", "maxChunkTimeMS": 2000 } @@ -35,15 +36,15 @@ where arguments.txt is a file containing a list of arguments prefixes to both ge in the expectations. The entire variant list generation runs *once per prefix*, so this multiplies the size of the variant list. - ?worker=0&q= - ?worker=1&q= + ?debug=0&q= + ?debug=1&q= and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g.: - path/to/cts.https.html?worker=0&q=webgpu:a/foo:bar={"x":1} - path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":1} + path/to/cts.https.html?debug=0&q=webgpu:a/foo:bar={"x":1} + path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":1} - path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":3} + path/to/cts.https.html?debug=1&q=webgpu:a/foo:bar={"x":3} `); process.exit(rc); } @@ -51,9 +52,11 @@ and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g interface ConfigJSON { /** Test suite to generate from. */ suite: string; - /** Output filename, relative to JSON file. */ + /** Output path for HTML file, relative to config file. */ out: string; - /** Input template filename, relative to JSON file. */ + /** Output path for JSON file containing the "variant" list, relative to config file. */ + outVariantList?: string; + /** Input template filename, relative to config file. */ template: string; /** * Maximum time for a single WPT "variant" chunk, in milliseconds. Defaults to infinity. @@ -71,18 +74,31 @@ interface ConfigJSON { /** The prefix to trim from every line of the expectations_file. */ prefix: string; }; + /** Expend all subtrees for provided queries */ + fullyExpandSubtrees?: { + file: string; + prefix: string; + }; + /*No long path assert */ + noLongPathAssert?: boolean; } interface Config { suite: string; out: string; + outVariantList?: string; template: string; maxChunkTimeMS: number; argumentsPrefixes: string[]; + noLongPathAssert: boolean; expectations?: { file: string; prefix: string; }; + fullyExpandSubtrees?: { + file: string; + prefix: string; + }; } let config: Config; @@ -101,13 +117,23 @@ let config: Config; template: path.resolve(jsonFileDir, configJSON.template), maxChunkTimeMS: configJSON.maxChunkTimeMS ?? Infinity, argumentsPrefixes: configJSON.argumentsPrefixes ?? ['?q='], + noLongPathAssert: configJSON.noLongPathAssert ?? false, }; + if (configJSON.outVariantList) { + config.outVariantList = path.resolve(jsonFileDir, configJSON.outVariantList); + } if (configJSON.expectations) { config.expectations = { file: path.resolve(jsonFileDir, configJSON.expectations.file), prefix: configJSON.expectations.prefix, }; } + if (configJSON.fullyExpandSubtrees) { + config.fullyExpandSubtrees = { + file: path.resolve(jsonFileDir, configJSON.fullyExpandSubtrees.file), + prefix: configJSON.fullyExpandSubtrees.prefix, + }; + } break; } case 4: @@ -130,6 +156,7 @@ let config: Config; suite, maxChunkTimeMS: Infinity, argumentsPrefixes: ['?q='], + noLongPathAssert: false, }; if (process.argv.length >= 7) { config.argumentsPrefixes = (await fs.readFile(argsPrefixesFile, 'utf8')) @@ -153,29 +180,16 @@ let config: Config; config.argumentsPrefixes.sort((a, b) => b.length - a.length); // Load expectations (if any) - let expectationLines = new Set<string>(); - if (config.expectations) { - expectationLines = new Set( - (await fs.readFile(config.expectations.file, 'utf8')).split(/\r?\n/).filter(l => l.length) - ); - } + const expectations: Map<string, string[]> = await loadQueryFile( + config.argumentsPrefixes, + config.expectations + ); - const expectations: Map<string, string[]> = new Map(); - for (const prefix of config.argumentsPrefixes) { - expectations.set(prefix, []); - } - - expLoop: for (const exp of expectationLines) { - // Take each expectation for the longest prefix it matches. - for (const argsPrefix of config.argumentsPrefixes) { - const prefix = config.expectations!.prefix + argsPrefix; - if (exp.startsWith(prefix)) { - expectations.get(argsPrefix)!.push(exp.substring(prefix.length)); - continue expLoop; - } - } - console.log('note: ignored expectation: ' + exp); - } + // Load fullyExpandSubtrees queries (if any) + const fullyExpand: Map<string, string[]> = await loadQueryFile( + config.argumentsPrefixes, + config.fullyExpandSubtrees + ); const loader = new DefaultTestFileLoader(); const lines = []; @@ -183,6 +197,7 @@ let config: Config; const rootQuery = new TestQueryMultiFile(config.suite, []); const tree = await loader.loadTree(rootQuery, { subqueriesToExpand: expectations.get(prefix), + fullyExpandSubtrees: fullyExpand.get(prefix), maxChunkTime: config.maxChunkTimeMS, }); @@ -199,22 +214,24 @@ let config: Config; alwaysExpandThroughLevel, })) { assert(query instanceof TestQueryMultiCase); - const queryString = query.toString(); - // Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole - // path must be <= 259. Leave room for e.g.: - // 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt' - assert( - queryString.length < 185, - `Generated test variant would produce too-long -actual.txt filename. Possible solutions: + if (!config.noLongPathAssert) { + const queryString = query.toString(); + // Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole + // path must be <= 259. Leave room for e.g.: + // 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt' + assert( + queryString.length < 185, + `Generated test variant would produce too-long -actual.txt filename. Possible solutions: - Reduce the length of the parts of the test query - Reduce the parameterization of the test - Make the test function faster and regenerate the listing_meta entry - Reduce the specificity of test expectations (if you're using them) ${queryString}` - ); + ); + } lines.push({ - urlQueryString: prefix + query.toString(), // "?worker=0&q=..." + urlQueryString: prefix + query.toString(), // "?debug=0&q=..." comment: useChunking ? `estimated: ${subtreeCounts?.totalTimeMS.toFixed(3)} ms` : undefined, }); @@ -232,6 +249,39 @@ ${queryString}` process.exit(1); }); +async function loadQueryFile( + argumentsPrefixes: string[], + queryFile?: { + file: string; + prefix: string; + } +): Promise<Map<string, string[]>> { + let lines = new Set<string>(); + if (queryFile) { + lines = new Set( + (await fs.readFile(queryFile.file, 'utf8')).split(/\r?\n/).filter(l => l.length) + ); + } + + const result: Map<string, string[]> = new Map(); + for (const prefix of argumentsPrefixes) { + result.set(prefix, []); + } + + expLoop: for (const exp of lines) { + // Take each expectation for the longest prefix it matches. + for (const argsPrefix of argumentsPrefixes) { + const prefix = queryFile!.prefix + argsPrefix; + if (exp.startsWith(prefix)) { + result.get(argsPrefix)!.push(exp.substring(prefix.length)); + continue expLoop; + } + } + console.log('note: ignored expectation: ' + exp); + } + return result; +} + async function generateFile( lines: Array<{ urlQueryString?: string; comment?: string } | undefined> ): Promise<void> { @@ -240,13 +290,20 @@ async function generateFile( result += await fs.readFile(config.template, 'utf8'); + const variantList = []; for (const line of lines) { if (line !== undefined) { - if (line.urlQueryString) result += `<meta name=variant content='${line.urlQueryString}'>`; + if (line.urlQueryString) { + result += `<meta name=variant content='${line.urlQueryString}'>`; + variantList.push(line.urlQueryString); + } if (line.comment) result += `<!-- ${line.comment} -->`; } result += '\n'; } await fs.writeFile(config.out, result); + if (config.outVariantList) { + await fs.writeFile(config.outVariantList, JSON.stringify(variantList, undefined, 2)); + } } diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts index fb33ae20fb..a8bef354cc 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts @@ -36,21 +36,13 @@ In more detail: - For each suite seen, loads its listing_meta.json, takes the max of the old and new data, and writes it back out. -How to generate TIMING_LOG_FILES files: - -- Launch the 'websocket-logger' tool (see its README.md), which listens for - log messages on localhost:59497. -- Run the tests you want to capture data for, on the same system. Since - logging is done through the websocket side-channel, you can run the tests - under any runtime (standalone, WPT, etc.) as long as WebSocket support is - available (always true in browsers). -- Run \`tools/merge_listing_times webgpu -- tools/websocket-logger/wslog-*.txt\` +See 'docs/adding_timing_metadata.md' for how to generate TIMING_LOG_FILES files. `); process.exit(rc); } const kHeader = `{ - "_comment": "SEMI AUTO-GENERATED: Please read docs/adding_timing_metadata.md.", + "_comment": "SEMI AUTO-GENERATED. This list is NOT exhaustive. Please read docs/adding_timing_metadata.md.", `; const kFooter = `\ "_end": "" diff --git a/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts b/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts index 164ee3259a..47aa9782a8 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts @@ -2,7 +2,7 @@ import * as process from 'process'; import { crawl } from './crawl.js'; -function usage(rc: number): void { +function usage(rc: number): never { console.error(`Usage: tools/validate [options] [SUITE_DIRS...] For each suite in SUITE_DIRS, validate some properties about the file: @@ -14,23 +14,40 @@ For each suite in SUITE_DIRS, validate some properties about the file: - That each case query is not too long Example: - tools/validate src/unittests/ src/webgpu/ + tools/validate src/unittests src/webgpu Options: - --help Print this message and exit. + --help Print this message and exit. + --print-metadata-warnings Print non-fatal warnings about listing_meta.json files. `); process.exit(rc); } const args = process.argv.slice(2); +if (args.length < 1) { + usage(0); +} if (args.indexOf('--help') !== -1) { usage(0); } -if (args.length < 1) { +let printMetadataWarnings = false; +const suiteDirs = []; +for (const arg of args) { + if (arg === '--print-metadata-warnings') { + printMetadataWarnings = true; + } else { + suiteDirs.push(arg); + } +} + +if (suiteDirs.length === 0) { usage(0); } -for (const suiteDir of args) { - void crawl(suiteDir, true); +for (const suiteDir of suiteDirs) { + void crawl(suiteDir, { + validate: true, + printMetadataWarnings, + }); } diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts b/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts new file mode 100644 index 0000000000..5f74b4662e --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts @@ -0,0 +1,57 @@ +/// CRC32 immutable lookup table data. +const kCRC32LUT = [ + 0, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, + 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, + 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, + 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, + 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, + 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, + 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, + 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, + 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, + 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, + 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, + 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, + 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, + 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, + 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, + 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, + 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, + 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, + 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, + 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, + 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, + 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, + 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, + 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, + 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, + 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, + 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, + 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, +]; + +/** + * @param str the input string + * @returns the CRC32 of the input string + * @see https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm + */ +export function crc32(str: string): number { + const utf8 = new TextEncoder().encode(str); + const u32 = new Uint32Array(1); + + u32[0] = 0xffffffff; + for (const c of utf8) { + u32[0] = (u32[0] >>> 8) ^ kCRC32LUT[(u32[0] & 0xff) ^ c]; + } + u32[0] = u32[0] ^ 0xffffffff; + return u32[0]; +} + +/** @returns the input number has a 8-character hex string */ +export function toHexString(number: number): string { + return ('00000000' + number.toString(16)).slice(-8); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts b/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts new file mode 100644 index 0000000000..4b5604b897 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts @@ -0,0 +1,36 @@ +/** + * Parses all the paths of the typescript `import` statements from content + * @param path the current path of the file + * @param content the file content + * @returns the list of import paths + */ +export function parseImports(path: string, content: string): string[] { + const out: string[] = []; + const importRE = /^import\s[^'"]*(['"])([./\w]*)(\1);/gm; + let importMatch: RegExpMatchArray | null; + while ((importMatch = importRE.exec(content))) { + const importPath = importMatch[2].replace(`'`, '').replace(`"`, ''); + out.push(joinPath(path, importPath)); + } + return out; +} + +function joinPath(a: string, b: string): string { + const aParts = a.split('/'); + const bParts = b.split('/'); + aParts.pop(); // remove file + let bStart = 0; + while (aParts.length > 0) { + switch (bParts[bStart]) { + case '.': + bStart++; + continue; + case '..': + aParts.pop(); + bStart++; + continue; + } + break; + } + return [...aParts, ...bParts.slice(bStart)].join('/'); +} diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts index 9433aaddb0..4092b997a3 100644 --- a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts +++ b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts @@ -1,7 +1,6 @@ import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import { SkipTestCase } from '../framework/fixture.js'; import { globalTestConfig } from '../framework/test_config.js'; -import { Logger } from '../internal/logging/logger.js'; import { keysOf } from './data_tables.js'; import { timeout } from './timeout.js'; @@ -24,7 +23,7 @@ export class ErrorWithExtra extends Error { super(message); const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {}; - this.extra = Logger.globalDebugMode + this.extra = globalTestConfig.enableDebugLogs ? { ...oldExtras, ...newExtra() } : { omitted: 'pass ?debug=1' }; } @@ -304,6 +303,8 @@ const TypedArrayBufferViewInstances = [ new Float16Array(), new Float32Array(), new Float64Array(), + new BigInt64Array(), + new BigUint64Array(), ] as const; export type TypedArrayBufferView = (typeof TypedArrayBufferViewInstances)[number]; |