summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts679
1 files changed, 679 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts
new file mode 100644
index 0000000000..0376f92dda
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/runtime/standalone.ts
@@ -0,0 +1,679 @@
+// Implements the standalone test runner (see also: /standalone/index.html).
+/* eslint no-console: "off" */
+
+import { dataCache } from '../framework/data_cache.js';
+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 { LiveTestCaseResult } from '../internal/logging/result.js';
+import { parseQuery } from '../internal/query/parseQuery.js';
+import { TestQueryLevel } from '../internal/query/query.js';
+import { TestTreeNode, TestSubtree, TestTreeLeaf, TestTree } from '../internal/tree.js';
+import { setDefaultRequestAdapterOptions } from '../util/navigator_gpu.js';
+import { ErrorWithExtra, unreachable } from '../util/util.js';
+
+import {
+ kCTSOptionsInfo,
+ parseSearchParamLikeWithOptions,
+ CTSOptions,
+ OptionInfo,
+ OptionsInfos,
+ camelCaseToSnakeCase,
+} from './helper/options.js';
+import { TestWorker } from './helper/test_worker.js';
+
+const rootQuerySpec = 'webgpu:*';
+let promptBeforeReload = false;
+let isFullCTS = false;
+
+globalTestConfig.frameworkDebugLog = console.log;
+
+window.onbeforeunload = () => {
+ // Prompt user before reloading if there are any results
+ return promptBeforeReload ? false : undefined;
+};
+
+const kOpenTestLinkAltText = 'Open';
+
+type StandaloneOptions = CTSOptions & { runnow: OptionInfo };
+
+const kStandaloneOptionsInfos: OptionsInfos<StandaloneOptions> = {
+ ...kCTSOptionsInfo,
+ runnow: { description: 'run immediately on load' },
+};
+
+const { queries: qs, options } = parseSearchParamLikeWithOptions(
+ kStandaloneOptionsInfos,
+ window.location.search || rootQuerySpec
+);
+const { runnow, debug, unrollConstEvalLoops, powerPreference, compatibility } = options;
+globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
+globalTestConfig.compatibility = compatibility;
+
+Logger.globalDebugMode = debug;
+const logger = new Logger();
+
+setBaseResourcePath('../out/resources');
+
+const worker = options.worker ? new TestWorker(options) : undefined;
+
+const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement;
+const resultsVis = document.getElementById('resultsVis')!;
+const progressElem = document.getElementById('progress')!;
+const progressTestNameElem = progressElem.querySelector('.progress-test-name')!;
+const stopButtonElem = progressElem.querySelector('button')!;
+let runDepth = 0;
+let stopRequested = false;
+
+stopButtonElem.addEventListener('click', () => {
+ stopRequested = true;
+});
+
+if (powerPreference || compatibility) {
+ setDefaultRequestAdapterOptions({
+ ...(powerPreference && { powerPreference }),
+ // MAINTENANCE_TODO: Change this to whatever the option ends up being
+ ...(compatibility && { compatibilityMode: true }),
+ });
+}
+
+dataCache.setStore({
+ load: async (path: string) => {
+ const response = await fetch(`data/${path}`);
+ if (!response.ok) {
+ return Promise.reject(response.statusText);
+ }
+ return new Uint8Array(await response.arrayBuffer());
+ },
+});
+
+interface SubtreeResult {
+ pass: number;
+ fail: number;
+ warn: number;
+ skip: number;
+ total: number;
+ timems: number;
+}
+
+function emptySubtreeResult() {
+ return { pass: 0, fail: 0, warn: 0, skip: 0, total: 0, timems: 0 };
+}
+
+function mergeSubtreeResults(...results: SubtreeResult[]) {
+ const target = emptySubtreeResult();
+ for (const result of results) {
+ target.pass += result.pass;
+ target.fail += result.fail;
+ target.warn += result.warn;
+ target.skip += result.skip;
+ target.total += result.total;
+ target.timems += result.timems;
+ }
+ return target;
+}
+
+type SetCheckedRecursively = () => void;
+type GenerateSubtreeHTML = (parent: HTMLElement) => SetCheckedRecursively;
+type RunSubtree = () => Promise<SubtreeResult>;
+
+interface VisualizedSubtree {
+ generateSubtreeHTML: GenerateSubtreeHTML;
+ runSubtree: RunSubtree;
+}
+
+// DOM generation
+
+function memoize<T>(fn: () => T): () => T {
+ let value: T | undefined;
+ return () => {
+ if (value === undefined) {
+ value = fn();
+ }
+ return value;
+ };
+}
+
+function makeTreeNodeHTML(tree: TestTreeNode, parentLevel: TestQueryLevel): VisualizedSubtree {
+ let subtree: VisualizedSubtree;
+
+ if ('children' in tree) {
+ subtree = makeSubtreeHTML(tree, parentLevel);
+ } else {
+ subtree = makeCaseHTML(tree);
+ }
+
+ const generateMyHTML = (parentElement: HTMLElement) => {
+ const div = $('<div>').appendTo(parentElement)[0];
+ return subtree.generateSubtreeHTML(div);
+ };
+ return { runSubtree: subtree.runSubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
+ // Becomes set once the case has been run once.
+ let caseResult: LiveTestCaseResult | undefined;
+
+ // Becomes set once the DOM for this case exists.
+ let clearRenderedResult: (() => void) | undefined;
+ let updateRenderedResult: (() => void) | undefined;
+
+ const name = t.query.toString();
+ const runSubtree = async () => {
+ if (clearRenderedResult) clearRenderedResult();
+
+ const result: SubtreeResult = emptySubtreeResult();
+ progressTestNameElem.textContent = name;
+
+ const [rec, res] = logger.record(name);
+ caseResult = res;
+ if (worker) {
+ await worker.run(rec, name);
+ } else {
+ await t.run(rec);
+ }
+
+ result.total++;
+ result.timems += caseResult.timems;
+ switch (caseResult.status) {
+ case 'pass':
+ result.pass++;
+ break;
+ case 'fail':
+ result.fail++;
+ break;
+ case 'skip':
+ result.skip++;
+ break;
+ case 'warn':
+ result.warn++;
+ break;
+ default:
+ unreachable();
+ }
+
+ if (updateRenderedResult) updateRenderedResult();
+
+ return result;
+ };
+
+ const generateSubtreeHTML = (div: HTMLElement) => {
+ div.classList.add('testcase');
+
+ const caselogs = $('<div>').addClass('testcaselogs').hide();
+ const [casehead, setChecked] = makeTreeNodeHeaderHTML(t, runSubtree, 2, checked => {
+ checked ? caselogs.show() : caselogs.hide();
+ });
+ const casetime = $('<div>').addClass('testcasetime').html('ms').appendTo(casehead);
+ div.appendChild(casehead);
+ div.appendChild(caselogs[0]);
+
+ clearRenderedResult = () => {
+ div.removeAttribute('data-status');
+ casetime.text('ms');
+ caselogs.empty();
+ };
+
+ updateRenderedResult = () => {
+ if (caseResult) {
+ div.setAttribute('data-status', caseResult.status);
+
+ casetime.text(caseResult.timems.toFixed(4) + ' ms');
+
+ if (caseResult.logs) {
+ caselogs.empty();
+ for (const l of caseResult.logs) {
+ const caselog = $('<div>').addClass('testcaselog').appendTo(caselogs);
+ $('<button>')
+ .addClass('testcaselogbtn')
+ .attr('alt', 'Log stack to console')
+ .attr('title', 'Log stack to console')
+ .appendTo(caselog)
+ .on('click', () => {
+ consoleLogError(l);
+ });
+ $('<pre>').addClass('testcaselogtext').appendTo(caselog).text(l.toJSON());
+ }
+ }
+ }
+ };
+
+ updateRenderedResult();
+
+ return setChecked;
+ };
+
+ return { runSubtree, generateSubtreeHTML };
+}
+
+function makeSubtreeHTML(n: TestSubtree, parentLevel: TestQueryLevel): VisualizedSubtree {
+ let subtreeResult: SubtreeResult = emptySubtreeResult();
+ // Becomes set once the DOM for this case exists.
+ let clearRenderedResult: (() => void) | undefined;
+ let updateRenderedResult: (() => void) | undefined;
+
+ const { runSubtree, generateSubtreeHTML } = makeSubtreeChildrenHTML(
+ n.children.values(),
+ n.query.level
+ );
+
+ const runMySubtree = async () => {
+ if (runDepth === 0) {
+ stopRequested = false;
+ progressElem.style.display = '';
+ // only prompt if this is the full CTS and we started from the root.
+ if (isFullCTS && n.query.filePathParts.length === 0) {
+ promptBeforeReload = true;
+ }
+ }
+ if (stopRequested) {
+ const result = emptySubtreeResult();
+ result.skip = 1;
+ result.total = 1;
+ return result;
+ }
+
+ ++runDepth;
+
+ if (clearRenderedResult) clearRenderedResult();
+ subtreeResult = await runSubtree();
+ if (updateRenderedResult) updateRenderedResult();
+
+ --runDepth;
+ if (runDepth === 0) {
+ progressElem.style.display = 'none';
+ }
+
+ return subtreeResult;
+ };
+
+ const generateMyHTML = (div: HTMLElement) => {
+ const subtreeHTML = $('<div>').addClass('subtreechildren');
+ const generateSubtree = memoize(() => generateSubtreeHTML(subtreeHTML[0]));
+
+ // Hide subtree - it's not generated yet.
+ subtreeHTML.hide();
+ const [header, setChecked] = makeTreeNodeHeaderHTML(n, runMySubtree, parentLevel, checked => {
+ if (checked) {
+ // Make sure the subtree is generated and then show it.
+ generateSubtree();
+ subtreeHTML.show();
+ } else {
+ subtreeHTML.hide();
+ }
+ });
+
+ div.classList.add('subtree');
+ div.classList.add(['', 'multifile', 'multitest', 'multicase'][n.query.level]);
+ div.appendChild(header);
+ div.appendChild(subtreeHTML[0]);
+
+ clearRenderedResult = () => {
+ div.removeAttribute('data-status');
+ };
+
+ updateRenderedResult = () => {
+ let status = '';
+ if (subtreeResult.pass > 0) {
+ status += 'pass';
+ }
+ if (subtreeResult.fail > 0) {
+ status += 'fail';
+ }
+ if (subtreeResult.skip === subtreeResult.total && subtreeResult.total > 0) {
+ status += 'skip';
+ }
+ div.setAttribute('data-status', status);
+ if (autoCloseOnPass.checked && status === 'pass') {
+ div.firstElementChild!.removeAttribute('open');
+ }
+ };
+
+ updateRenderedResult();
+
+ return () => {
+ setChecked();
+ const setChildrenChecked = generateSubtree();
+ setChildrenChecked();
+ };
+ };
+
+ return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function makeSubtreeChildrenHTML(
+ children: Iterable<TestTreeNode>,
+ parentLevel: TestQueryLevel
+): VisualizedSubtree {
+ const childFns = Array.from(children, subtree => makeTreeNodeHTML(subtree, parentLevel));
+
+ const runMySubtree = async () => {
+ const results: SubtreeResult[] = [];
+ for (const { runSubtree } of childFns) {
+ results.push(await runSubtree());
+ }
+ return mergeSubtreeResults(...results);
+ };
+ const generateMyHTML = (div: HTMLElement) => {
+ const setChildrenChecked = Array.from(childFns, ({ generateSubtreeHTML }) =>
+ generateSubtreeHTML(div)
+ );
+
+ return () => {
+ for (const setChildChecked of setChildrenChecked) {
+ setChildChecked();
+ }
+ };
+ };
+
+ return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
+}
+
+function consoleLogError(e: Error | ErrorWithExtra | undefined) {
+ if (e === undefined) return;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ (globalThis as any)._stack = e;
+ console.log('_stack =', e);
+ if ('extra' in e && e.extra !== undefined) {
+ console.log('_stack.extra =', e.extra);
+ }
+}
+
+function makeTreeNodeHeaderHTML(
+ n: TestTreeNode,
+ runSubtree: RunSubtree,
+ parentLevel: TestQueryLevel,
+ onChange: (checked: boolean) => void
+): [HTMLElement, SetCheckedRecursively] {
+ const isLeaf = 'run' in n;
+ const div = $('<details>').addClass('nodeheader');
+ const header = $('<summary>').appendTo(div);
+
+ // prevent toggling if user is selecting text from an input element
+ {
+ let lastNodeName = '';
+ div.on('pointerdown', event => {
+ lastNodeName = event.target.nodeName;
+ });
+ div.on('click', event => {
+ if (lastNodeName === 'INPUT') {
+ event.preventDefault();
+ }
+ });
+ }
+
+ const setChecked = () => {
+ div.prop('open', true); // (does not fire onChange)
+ onChange(true);
+ };
+
+ const href = createSearchQuery([n.query.toString()]);
+ if (onChange) {
+ div.on('toggle', function (this) {
+ onChange((this as HTMLDetailsElement).open);
+ });
+
+ // Expand the shallower parts of the tree at load.
+ // Also expand completely within subtrees that are at the same query level
+ // (e.g. s:f:t,* and s:f:t,t,*).
+ if (n.query.level <= lastQueryLevelToExpand || n.query.level === parentLevel) {
+ setChecked();
+ }
+ }
+ const runtext = isLeaf ? 'Run case' : 'Run subtree';
+ $('<button>')
+ .addClass(isLeaf ? 'leafrun' : 'subtreerun')
+ .attr('alt', runtext)
+ .attr('title', runtext)
+ .on('click', async () => {
+ if (runDepth > 0) {
+ showInfo('tests are already running');
+ return;
+ }
+ showInfo('');
+ console.log(`Starting run for ${n.query}`);
+ // turn off all run buttons
+ $('#resultsVis').addClass('disable-run');
+ const startTime = performance.now();
+ await runSubtree();
+ const dt = performance.now() - startTime;
+ const dtMinutes = dt / 1000 / 60;
+ // turn on all run buttons
+ $('#resultsVis').removeClass('disable-run');
+ console.log(`Finished run: ${dt.toFixed(1)} ms = ${dtMinutes.toFixed(1)} min`);
+ })
+ .appendTo(header);
+ $('<a>')
+ .addClass('nodelink')
+ .attr('href', href)
+ .attr('alt', kOpenTestLinkAltText)
+ .attr('title', kOpenTestLinkAltText)
+ .appendTo(header);
+ $('<button>')
+ .addClass('copybtn')
+ .attr('alt', 'copy query')
+ .attr('title', 'copy query')
+ .on('click', () => {
+ void navigator.clipboard.writeText(n.query.toString());
+ })
+ .appendTo(header);
+ if ('testCreationStack' in n && n.testCreationStack) {
+ $('<button>')
+ .addClass('testcaselogbtn')
+ .attr('alt', 'Log test creation stack to console')
+ .attr('title', 'Log test creation stack to console')
+ .appendTo(header)
+ .on('click', () => {
+ consoleLogError(n.testCreationStack);
+ });
+ }
+ const nodetitle = $('<div>').addClass('nodetitle').appendTo(header);
+ const nodecolumns = $('<span>').addClass('nodecolumns').appendTo(nodetitle);
+ {
+ $('<input>')
+ .attr('type', 'text')
+ .prop('readonly', true)
+ .addClass('nodequery')
+ .on('click', event => {
+ (event.target as HTMLInputElement).select();
+ })
+ .val(n.query.toString())
+ .appendTo(nodecolumns);
+ if (n.subtreeCounts) {
+ $('<span>')
+ .attr('title', '(Nodes with TODOs) / (Total test count)')
+ .text(TestTree.countsToString(n))
+ .appendTo(nodecolumns);
+ }
+ }
+ if ('description' in n && n.description) {
+ nodetitle.append('&nbsp;');
+ $('<pre>') //
+ .addClass('nodedescription')
+ .text(n.description)
+ .appendTo(header);
+ }
+ return [div[0], setChecked];
+}
+
+// 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][] {
+ const key = camelCaseToSnakeCase(k);
+ if (typeof v === 'boolean') {
+ return [[key, v ? '1' : '0']];
+ } else if (Array.isArray(v)) {
+ return v.map(v => [key, v]);
+ } else {
+ return [[key, v!.toString()]];
+ }
+}
+
+/**
+ * Converts key value pairs to a search string.
+ * Keys will appear in order in the search string.
+ * Values can be undefined, null, boolean, string, or string[]
+ * If the value is falsy the key will not appear in the search string.
+ * If the value is an array the key will appear multiple times.
+ *
+ * @param params Some object with key value pairs.
+ * @returns a search string.
+ */
+function prepareParams(params: Record<string, ParamValue>): string {
+ const pairsArrays = Object.entries(params)
+ .filter(([, v]) => !!v)
+ .map(keyValueToPairs);
+ const pairs = pairsArrays.flat();
+ return new URLSearchParams(pairs).toString();
+}
+
+// This is just a cast in one place.
+export function optionsToRecord(options: CTSOptions) {
+ return options as unknown as Record<string, boolean | string>;
+}
+
+/**
+ * Given a search query, generates a search parameter string
+ * @param queries array of queries
+ * @param params an optional existing search
+ * @returns a search query string
+ */
+function createSearchQuery(queries: string[], params?: string) {
+ params = params === undefined ? prepareParams(optionsToRecord(options)) : params;
+ // Add in q separately to avoid escaping punctuation marks.
+ return `?${params}${params ? '&' : ''}${queries.map(q => 'q=' + q).join('&')}`;
+}
+
+/**
+ * Show an info message on the page.
+ * @param msg Message to show
+ */
+function showInfo(msg: string) {
+ $('#info')[0].textContent = msg;
+}
+
+void (async () => {
+ const loader = new DefaultTestFileLoader();
+
+ // MAINTENANCE_TODO: start populating page before waiting for everything to load?
+ isFullCTS = qs.length === 1 && qs[0] === rootQuerySpec;
+
+ // Update the URL bar to match the exact current options.
+ const updateURLsWithCurrentOptions = () => {
+ const params = prepareParams(optionsToRecord(options));
+ let url = `${window.location.origin}${window.location.pathname}`;
+ url += createSearchQuery(qs, params);
+ window.history.replaceState(null, '', url.toString());
+ document.querySelectorAll(`a[alt=${kOpenTestLinkAltText}]`).forEach(elem => {
+ const a = elem as HTMLAnchorElement;
+ const qs = new URLSearchParams(a.search).getAll('q');
+ a.search = createSearchQuery(qs, params);
+ });
+ };
+
+ const addOptionsToPage = (
+ options: StandaloneOptions,
+ optionsInfos: typeof kStandaloneOptionsInfos
+ ) => {
+ const optionsElem = $('table#options>tbody')[0];
+ const optionValues = optionsToRecord(options);
+
+ const createCheckbox = (optionName: string) => {
+ return $(`<input>`)
+ .attr('type', 'checkbox')
+ .prop('checked', optionValues[optionName] as boolean)
+ .on('change', function () {
+ optionValues[optionName] = (this as HTMLInputElement).checked;
+ updateURLsWithCurrentOptions();
+ });
+ };
+
+ const createSelect = (optionName: string, info: OptionInfo) => {
+ const select = $('<select>').on('change', function () {
+ optionValues[optionName] = (this as HTMLInputElement).value;
+ updateURLsWithCurrentOptions();
+ });
+ const currentValue = optionValues[optionName];
+ for (const { value, description } of info.selectValueDescriptions!) {
+ $('<option>')
+ .text(description)
+ .val(value)
+ .prop('selected', value === currentValue)
+ .appendTo(select);
+ }
+ return select;
+ };
+
+ for (const [optionName, info] of Object.entries(optionsInfos)) {
+ const input =
+ typeof optionValues[optionName] === 'boolean'
+ ? createCheckbox(optionName)
+ : createSelect(optionName, info);
+ $('<tr>')
+ .append($('<td>').append(input))
+ .append($('<td>').text(camelCaseToSnakeCase(optionName)))
+ .append($('<td>').text(info.description))
+ .appendTo(optionsElem);
+ }
+ };
+ addOptionsToPage(options, kStandaloneOptionsInfos);
+
+ if (qs.length !== 1) {
+ showInfo('currently, there must be exactly one ?q=');
+ return;
+ }
+
+ let rootQuery;
+ try {
+ rootQuery = parseQuery(qs[0]);
+ } catch (e) {
+ showInfo((e as Error).toString());
+ return;
+ }
+
+ if (rootQuery.level > lastQueryLevelToExpand) {
+ lastQueryLevelToExpand = rootQuery.level;
+ }
+ loader.addEventListener('import', ev => {
+ showInfo(`loading: ${ev.data.url}`);
+ });
+ loader.addEventListener('imported', ev => {
+ showInfo(`imported: ${ev.data.url}`);
+ });
+ loader.addEventListener('finish', () => {
+ showInfo('');
+ });
+
+ let tree;
+ try {
+ tree = await loader.loadTree(rootQuery);
+ } catch (err) {
+ showInfo((err as Error).toString());
+ return;
+ }
+
+ tree.dissolveSingleChildTrees();
+
+ const { runSubtree, generateSubtreeHTML } = makeSubtreeHTML(tree.root, 1);
+ const setTreeCheckedRecursively = generateSubtreeHTML(resultsVis);
+
+ document.getElementById('expandall')!.addEventListener('click', () => {
+ setTreeCheckedRecursively();
+ });
+
+ document.getElementById('copyResultsJSON')!.addEventListener('click', () => {
+ void navigator.clipboard.writeText(logger.asJSON(2));
+ });
+
+ if (runnow) {
+ void runSubtree();
+ }
+})();