server.ts (9205B)
1 /* eslint-disable no-console, n/no-restricted-import */ 2 3 import * as fs from 'fs'; 4 import * as http from 'http'; 5 import { AddressInfo } from 'net'; 6 7 import { dataCache } from '../framework/data_cache.js'; 8 import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; 9 import { globalTestConfig } from '../framework/test_config.js'; 10 import { DefaultTestFileLoader } from '../internal/file_loader.js'; 11 import { prettyPrintLog } from '../internal/logging/log_message.js'; 12 import { Logger } from '../internal/logging/logger.js'; 13 import { LiveTestCaseResult, Status } from '../internal/logging/result.js'; 14 import { parseQuery } from '../internal/query/parseQuery.js'; 15 import { TestQueryWithExpectation } from '../internal/query/query.js'; 16 import { TestTreeLeaf } from '../internal/tree.js'; 17 import { Colors } from '../util/colors.js'; 18 import { setDefaultRequestAdapterOptions, setGPUProvider } from '../util/navigator_gpu.js'; 19 20 import sys from './helper/sys.js'; 21 22 function usage(rc: number): never { 23 console.log(`Usage: 24 tools/server [OPTIONS...] 25 Options: 26 --colors Enable ANSI colors in output. 27 --compat Run tests in compatibility mode. 28 --coverage Add coverage data to each result. 29 --verbose Print result/log of every test as it runs. 30 --debug Include debug messages in logging. 31 --gpu-provider Path to node module that provides the GPU implementation. 32 --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value> 33 --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests 34 --enforce-default-limits Enforce the default limits (note: powerPreference tests may fail) 35 --force-fallback-adapter Force a fallback adapter 36 --log-to-websocket Log to a websocket 37 --u Flag to set on the gpu-provider as <flag>=<value> 38 39 Provides an HTTP server used for running tests via an HTTP RPC interface 40 First, load some tree or subtree of tests: 41 http://localhost:port/load?unittests:basic:* 42 To run a single test case, perform an HTTP GET or POST at the URL: 43 http://localhost:port/run?unittests:basic:test,sync 44 To shutdown the server perform an HTTP GET or POST at the URL: 45 http://localhost:port/terminate 46 `); 47 return sys.exit(rc); 48 } 49 50 interface RunResult { 51 // The result of the test 52 status: Status; 53 // Any additional messages printed 54 message: string; 55 // The time it took to execute the test 56 durationMS: number; 57 // Code coverage data, if the server was started with `--coverage` 58 // This data is opaque (implementation defined). 59 coverageData?: string; 60 } 61 62 // The interface that exposes creation of the GPU, and optional interface to code coverage. 63 interface GPUProviderModule { 64 // @returns a GPU with the given flags 65 create(flags: string[]): GPU; 66 // An optional interface to a CodeCoverageProvider 67 coverage?: CodeCoverageProvider; 68 } 69 70 interface CodeCoverageProvider { 71 // Starts collecting code coverage 72 begin(): void; 73 // Ends collecting of code coverage, returning the coverage data. 74 // This data is opaque (implementation defined). 75 end(): string; 76 } 77 78 if (!sys.existsSync('src/common/runtime/cmdline.ts')) { 79 console.log('Must be run from repository root'); 80 usage(1); 81 } 82 setBaseResourcePath('out-node/resources'); 83 84 Colors.enabled = false; 85 86 let emitCoverage = false; 87 let verbose = false; 88 let gpuProviderModule: GPUProviderModule | undefined = undefined; 89 90 const gpuProviderFlags: string[] = []; 91 for (let i = 0; i < sys.args.length; ++i) { 92 const a = sys.args[i]; 93 if (a.startsWith('-')) { 94 if (a === '--colors') { 95 Colors.enabled = true; 96 } else if (a === '--compat') { 97 globalTestConfig.compatibility = true; 98 } else if (a === '--coverage') { 99 emitCoverage = true; 100 } else if (a === '--force-fallback-adapter') { 101 globalTestConfig.forceFallbackAdapter = true; 102 } else if (a === '--enforce-default-limits') { 103 globalTestConfig.enforceDefaultLimits = true; 104 } else if (a === '--block-all-features') { 105 globalTestConfig.blockAllFeatures = true; 106 } else if (a === '--subcases-between-attempting-gc') { 107 globalTestConfig.subcasesBetweenAttemptingGC = Number(sys.args[++i]); 108 } else if (a === '--cases-between-replacing-device') { 109 globalTestConfig.casesBetweenReplacingDevice = Number(sys.args[++i]); 110 } else if (a === '--log-to-websocket') { 111 globalTestConfig.logToWebSocket = true; 112 } else if (a === '--gpu-provider') { 113 const modulePath = sys.args[++i]; 114 gpuProviderModule = require(modulePath); 115 } else if (a === '--gpu-provider-flag') { 116 gpuProviderFlags.push(sys.args[++i]); 117 } else if (a === '--debug') { 118 globalTestConfig.enableDebugLogs = true; 119 } else if (a === '--unroll-const-eval-loops') { 120 globalTestConfig.unrollConstEvalLoops = true; 121 } else if (a === '--help') { 122 usage(1); 123 } else if (a === '--verbose') { 124 verbose = true; 125 } else { 126 console.log(`unrecognized flag: ${a}`); 127 } 128 } 129 } 130 131 let codeCoverage: CodeCoverageProvider | undefined = undefined; 132 133 if (globalTestConfig.compatibility || globalTestConfig.forceFallbackAdapter) { 134 setDefaultRequestAdapterOptions({ 135 featureLevel: globalTestConfig.compatibility ? 'compatibility' : 'core', 136 forceFallbackAdapter: globalTestConfig.forceFallbackAdapter, 137 }); 138 } 139 140 if (gpuProviderModule) { 141 setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags)); 142 143 if (emitCoverage) { 144 codeCoverage = gpuProviderModule.coverage; 145 if (codeCoverage === undefined) { 146 console.error( 147 `--coverage specified, but the GPUProviderModule does not support code coverage. 148 Did you remember to build with code coverage instrumentation enabled?` 149 ); 150 sys.exit(1); 151 } 152 } 153 } 154 155 dataCache.setStore({ 156 load: (path: string) => { 157 return new Promise<Uint8Array>((resolve, reject) => { 158 fs.readFile(getResourcePath(`cache/${path}`), (err, data) => { 159 if (err !== null) { 160 reject(err.message); 161 } else { 162 resolve(data); 163 } 164 }); 165 }); 166 }, 167 }); 168 169 if (verbose) { 170 dataCache.setDebugLogger(console.log); 171 } 172 173 // eslint-disable-next-line @typescript-eslint/require-await 174 (async () => { 175 const log = new Logger(); 176 const testcases = new Map<string, TestTreeLeaf>(); 177 178 async function runTestcase( 179 testcase: TestTreeLeaf, 180 expectations: TestQueryWithExpectation[] = [] 181 ): Promise<LiveTestCaseResult> { 182 const name = testcase.query.toString(); 183 const [rec, res] = log.record(name); 184 await testcase.run(rec, expectations); 185 return res; 186 } 187 188 const server = http.createServer( 189 async (request: http.IncomingMessage, response: http.ServerResponse) => { 190 if (request.url === undefined) { 191 response.end('invalid url'); 192 return; 193 } 194 195 const loadCasesPrefix = '/load?'; 196 const runPrefix = '/run?'; 197 const terminatePrefix = '/terminate'; 198 199 if (request.url.startsWith(loadCasesPrefix)) { 200 const query = request.url.substr(loadCasesPrefix.length); 201 try { 202 const webgpuQuery = parseQuery(query); 203 const loader = new DefaultTestFileLoader(); 204 for (const testcase of await loader.loadCases(webgpuQuery)) { 205 testcases.set(testcase.query.toString(), testcase); 206 } 207 response.statusCode = 200; 208 response.end(); 209 } catch (err) { 210 response.statusCode = 500; 211 response.end(`load failed with error: ${err}\n${(err as Error).stack}`); 212 } 213 } else if (request.url.startsWith(runPrefix)) { 214 const name = request.url.substr(runPrefix.length); 215 try { 216 const testcase = testcases.get(name); 217 if (testcase) { 218 if (codeCoverage !== undefined) { 219 codeCoverage.begin(); 220 } 221 const start = performance.now(); 222 const result = await runTestcase(testcase); 223 const durationMS = performance.now() - start; 224 const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined; 225 let message = ''; 226 if (result.logs !== undefined) { 227 message = result.logs.map(log => prettyPrintLog(log)).join('\n'); 228 } 229 const status = result.status; 230 const res: RunResult = { status, message, durationMS, coverageData }; 231 response.statusCode = 200; 232 response.end(JSON.stringify(res)); 233 } else { 234 response.statusCode = 404; 235 response.end(`test case '${name}' not found`); 236 } 237 } catch (err) { 238 response.statusCode = 500; 239 response.end(`run failed with error: ${err}`); 240 } 241 } else if (request.url.startsWith(terminatePrefix)) { 242 server.close(); 243 sys.exit(1); 244 } else { 245 response.statusCode = 404; 246 response.end('unhandled url request'); 247 } 248 } 249 ); 250 251 server.listen(0, () => { 252 const address = server.address() as AddressInfo; 253 console.log(`Server listening at [[${address.port}]]`); 254 }); 255 })().catch(ex => { 256 console.error(ex.stack ?? ex.toString()); 257 sys.exit(1); 258 });