// Implements the standalone test runner (see also: /standalone/index.html). /* eslint no-console: "off" */ 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 { 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 { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } 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 = { ...kCTSOptionsInfo, runnow: { description: 'run immediately on load' }, }; const { queries: qs, options } = parseSearchParamLikeWithOptions( kStandaloneOptionsInfos, window.location.search || rootQuerySpec ); const { runnow, powerPreference, compatibility, forceFallbackAdapter } = options; globalTestConfig.enableDebugLogs = options.debug; globalTestConfig.unrollConstEvalLoops = options.unrollConstEvalLoops; globalTestConfig.compatibility = compatibility; globalTestConfig.logToWebSocket = options.logToWebSocket; const logger = new Logger(); setBaseResourcePath('../out/resources'); 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')!; 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 || 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(getResourcePath(`cache/${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; interface VisualizedSubtree { generateSubtreeHTML: GenerateSubtreeHTML; runSubtree: RunSubtree; } // DOM generation function memoize(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 = $('
').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 (testWorker) { await testWorker.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 = $('
').addClass('testcaselogs').hide(); const [casehead, setChecked] = makeTreeNodeHeaderHTML(t, runSubtree, 2, checked => { checked ? caselogs.show() : caselogs.hide(); }); const casetime = $('
').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(); // 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 => { $('
').addClass('testcaselogtext').text(l.toJSON()).appendTo(caselogs);
            });
          for (const l of caseResult.logs) {
            const caselog = $('
').addClass('testcaselog').appendTo(caselogs); $('