cmdline.ts (9744B)
1 /* eslint-disable no-console, n/no-restricted-import */ 2 3 import { dataCache } from '../framework/data_cache.js'; 4 import { getResourcePath, setBaseResourcePath } from '../framework/resources.js'; 5 import { globalTestConfig } from '../framework/test_config.js'; 6 import { DefaultTestFileLoader } from '../internal/file_loader.js'; 7 import { prettyPrintLog } from '../internal/logging/log_message.js'; 8 import { Logger } from '../internal/logging/logger.js'; 9 import { LiveTestCaseResult } from '../internal/logging/result.js'; 10 import { parseQuery } from '../internal/query/parseQuery.js'; 11 import { parseExpectationsForTestQuery } from '../internal/query/query.js'; 12 import { Colors } from '../util/colors.js'; 13 import { setDefaultRequestAdapterOptions, setGPUProvider } from '../util/navigator_gpu.js'; 14 import { assert, unreachable } from '../util/util.js'; 15 16 import sys from './helper/sys.js'; 17 18 function usage(rc: number): never { 19 console.log(`Usage: 20 tools/run_${sys.type} [OPTIONS...] QUERIES... 21 tools/run_${sys.type} 'unittests:*' 'webgpu:buffers,*' 22 Options: 23 --colors Enable ANSI colors in output. 24 --compat Runs tests in compatibility mode. 25 --coverage Emit coverage data. 26 --verbose Print result/log of every test as it runs. 27 --list Print all testcase names that match the given query and exit. 28 --list-unimplemented Print all unimplemented tests 29 --debug Include debug messages in logging. 30 --print-json Print the complete result JSON in the output. 31 --expectations Path to expectations file. 32 --gpu-provider Path to node module that provides the GPU implementation. 33 --gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value> 34 --unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests 35 --enforce-default-limits Enforce the default limits (note: powerPreference tests may fail) 36 --force-fallback-adapter Force a fallback adapter 37 --log-to-websocket Log to a websocket 38 --quiet Suppress summary information in output 39 `); 40 return sys.exit(rc); 41 } 42 43 if (!sys.existsSync('src/common/runtime/cmdline.ts')) { 44 console.log('Must be run from repository root'); 45 usage(1); 46 } 47 setBaseResourcePath('out-node/resources'); 48 49 // The interface that exposes creation of the GPU, and optional interface to code coverage. 50 interface GPUProviderModule { 51 // @returns a GPU with the given flags 52 create(flags: string[]): GPU; 53 // An optional interface to a CodeCoverageProvider 54 coverage?: CodeCoverageProvider; 55 } 56 57 interface CodeCoverageProvider { 58 // Starts collecting code coverage 59 begin(): void; 60 // Ends collecting of code coverage, returning the coverage data. 61 // This data is opaque (implementation defined). 62 end(): string; 63 } 64 65 type listModes = 'none' | 'cases' | 'unimplemented'; 66 67 Colors.enabled = false; 68 69 let verbose = false; 70 let emitCoverage = false; 71 let listMode: listModes = 'none'; 72 let printJSON = false; 73 let quiet = false; 74 let loadWebGPUExpectations: Promise<unknown> | undefined = undefined; 75 let gpuProviderModule: GPUProviderModule | undefined = undefined; 76 77 const queries: string[] = []; 78 const gpuProviderFlags: string[] = []; 79 for (let i = 0; i < sys.args.length; ++i) { 80 const a = sys.args[i]; 81 if (a.startsWith('-')) { 82 if (a === '--colors') { 83 Colors.enabled = true; 84 } else if (a === '--coverage') { 85 emitCoverage = true; 86 } else if (a === '--verbose') { 87 verbose = true; 88 } else if (a === '--list') { 89 listMode = 'cases'; 90 } else if (a === '--list-unimplemented') { 91 listMode = 'unimplemented'; 92 } else if (a === '--debug') { 93 globalTestConfig.enableDebugLogs = true; 94 } else if (a === '--print-json') { 95 printJSON = true; 96 } else if (a === '--expectations') { 97 const expectationsFile = new URL(sys.args[++i], `file://${sys.cwd()}`).pathname; 98 loadWebGPUExpectations = import(expectationsFile).then(m => m.expectations); 99 } else if (a === '--gpu-provider') { 100 const modulePath = sys.args[++i]; 101 gpuProviderModule = require(modulePath); 102 } else if (a === '--gpu-provider-flag') { 103 gpuProviderFlags.push(sys.args[++i]); 104 } else if (a === '--quiet') { 105 quiet = true; 106 } else if (a === '--unroll-const-eval-loops') { 107 globalTestConfig.unrollConstEvalLoops = true; 108 } else if (a === '--compat') { 109 globalTestConfig.compatibility = true; 110 } else if (a === '--force-fallback-adapter') { 111 globalTestConfig.forceFallbackAdapter = true; 112 } else if (a === '--enforce-default-limits') { 113 globalTestConfig.enforceDefaultLimits = true; 114 } else if (a === '--block-all-features') { 115 globalTestConfig.blockAllFeatures = true; 116 } else if (a === '--subcases-between-attempting-gc') { 117 globalTestConfig.subcasesBetweenAttemptingGC = Number(sys.args[++i]); 118 } else if (a === '--cases-between-replacing-device') { 119 globalTestConfig.casesBetweenReplacingDevice = Number(sys.args[++i]); 120 } else if (a === '--log-to-websocket') { 121 globalTestConfig.logToWebSocket = true; 122 } else { 123 console.log('unrecognized flag: ', a); 124 usage(1); 125 } 126 } else { 127 queries.push(a); 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 if (emitCoverage) { 143 codeCoverage = gpuProviderModule.coverage; 144 if (codeCoverage === undefined) { 145 console.error( 146 `--coverage specified, but the GPUProviderModule does not support code coverage. 147 Did you remember to build with code coverage instrumentation enabled?` 148 ); 149 sys.exit(1); 150 } 151 } 152 } 153 154 dataCache.setStore({ 155 load: (path: string) => { 156 return new Promise<Uint8Array>((resolve, reject) => { 157 sys.readFile( 158 getResourcePath(`cache/${path}`), 159 (err: { message: string }, data: Uint8Array) => { 160 if (err !== null) { 161 reject(err.message); 162 } else { 163 resolve(data); 164 } 165 } 166 ); 167 }); 168 }, 169 }); 170 171 if (verbose) { 172 dataCache.setDebugLogger(console.log); 173 } 174 175 if (queries.length === 0) { 176 console.log('no queries specified'); 177 usage(0); 178 } 179 180 (async () => { 181 const loader = new DefaultTestFileLoader(); 182 assert(queries.length === 1, 'currently, there must be exactly one query on the cmd line'); 183 const filterQuery = parseQuery(queries[0]); 184 const testcases = await loader.loadCases(filterQuery); 185 const expectations = parseExpectationsForTestQuery( 186 await (loadWebGPUExpectations ?? []), 187 filterQuery 188 ); 189 190 const log = new Logger(); 191 192 const failed: Array<[string, LiveTestCaseResult]> = []; 193 const warned: Array<[string, LiveTestCaseResult]> = []; 194 const skipped: Array<[string, LiveTestCaseResult]> = []; 195 196 let total = 0; 197 198 if (codeCoverage !== undefined) { 199 codeCoverage.begin(); 200 } 201 202 for (const testcase of testcases) { 203 const name = testcase.query.toString(); 204 switch (listMode) { 205 case 'cases': 206 console.log(name); 207 continue; 208 case 'unimplemented': 209 if (testcase.isUnimplemented) { 210 console.log(name); 211 } 212 continue; 213 default: 214 break; 215 } 216 217 const [rec, res] = log.record(name); 218 await testcase.run(rec, expectations); 219 220 if (verbose) { 221 printResults([[name, res]]); 222 } 223 224 total++; 225 switch (res.status) { 226 case 'pass': 227 break; 228 case 'fail': 229 failed.push([name, res]); 230 break; 231 case 'warn': 232 warned.push([name, res]); 233 break; 234 case 'skip': 235 skipped.push([name, res]); 236 break; 237 default: 238 unreachable('unrecognized status'); 239 } 240 } 241 242 if (codeCoverage !== undefined) { 243 const coverage = codeCoverage.end(); 244 console.log(`Code-coverage: [[${coverage}]]`); 245 } 246 247 if (listMode !== 'none') { 248 return; 249 } 250 251 assert(total > 0, 'found no tests!'); 252 253 // MAINTENANCE_TODO: write results out somewhere (a file?) 254 if (printJSON) { 255 console.log(log.asJSON(2)); 256 } 257 258 if (!quiet) { 259 if (skipped.length) { 260 console.log(''); 261 console.log('** Skipped **'); 262 printResults(skipped); 263 } 264 if (warned.length) { 265 console.log(''); 266 console.log('** Warnings **'); 267 printResults(warned); 268 } 269 if (failed.length) { 270 console.log(''); 271 console.log('** Failures **'); 272 printResults(failed); 273 } 274 275 const passed = total - warned.length - failed.length - skipped.length; 276 const pct = (x: number) => ((100 * x) / total).toFixed(2); 277 const rpt = (x: number) => { 278 const xs = x.toString().padStart(1 + Math.log10(total), ' '); 279 return `${xs} / ${total} = ${pct(x).padStart(6, ' ')}%`; 280 }; 281 console.log(''); 282 console.log(`** Summary ** 283 Passed w/o warnings = ${rpt(passed)} 284 Passed with warnings = ${rpt(warned.length)} 285 Skipped = ${rpt(skipped.length)} 286 Failed = ${rpt(failed.length)}`); 287 } 288 289 if (failed.length || warned.length) { 290 sys.exit(1); 291 } 292 sys.exit(0); 293 })().catch(ex => { 294 console.log(ex.stack ?? ex.toString()); 295 sys.exit(1); 296 }); 297 298 function printResults(results: Array<[string, LiveTestCaseResult]>): void { 299 for (const [name, r] of results) { 300 console.log(`[${r.status}] ${name} (${r.timems}ms). Log:`); 301 if (r.logs) { 302 for (const l of r.logs) { 303 console.log(prettyPrintLog(l)); 304 } 305 } 306 } 307 }