summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/fixture.ts17
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/framework/test_config.ts18
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/file_loader.ts4
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/log_message.ts51
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/logger.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/result.ts22
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/logging/test_case_recorder.ts8
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/compare.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/parseQuery.ts43
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/query/query.ts6
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/test_group.ts10
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/test_suite_listing.ts2
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/tree.ts7
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/internal/websocket_logger.ts13
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/cmdline.ts52
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/options.ts61
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker-worker.ts49
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/test_worker.ts202
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/utils_worker.ts35
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/helper/wrap_for_worker.ts54
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/server.ts62
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts53
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/wpt.ts18
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/crawl.ts56
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts13
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_cache.ts278
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings.ts11
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_listings_and_webworkers.ts89
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/gen_wpt_cts_html.ts135
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/merge_listing_times.ts12
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/tools/validate.ts29
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/crc32.ts57
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/parse_imports.ts36
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/util.ts5
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];