fixture.js (11728B)
1 /** 2 * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts 3 **/import { assert, unreachable } from '../util/util.js'; 4 5 export class SkipTestCase extends Error {} 6 export class UnexpectedPassError extends Error {} 7 8 export { TestCaseRecorder } from '../internal/logging/test_case_recorder.js'; 9 10 /** The fully-general type for params passed to a test function invocation. */ 11 12 13 14 15 16 17 18 19 20 21 22 export class SubcaseBatchState { 23 constructor( 24 recorder, 25 /** The case parameters for this test fixture shared state. Subcase params are not included. */ 26 params) 27 {this.recorder = recorder;this.params = params;} 28 29 /** 30 * Runs before the `.before()` function. 31 * @internal MAINTENANCE_TODO: Make this not visible to test code? 32 */ 33 async init() {} 34 /** 35 * Runs between the `.before()` function and the subcases. 36 * @internal MAINTENANCE_TODO: Make this not visible to test code? 37 */ 38 async postInit() {} 39 /** 40 * Runs after all subcases finish. 41 * @internal MAINTENANCE_TODO: Make this not visible to test code? 42 */ 43 async finalize() {} 44 45 /** Throws an exception marking the subcase as skipped. */ 46 skip(msg) { 47 throw new SkipTestCase(msg); 48 } 49 50 /** Throws an exception making the subcase as skipped if condition is true */ 51 skipIf(cond, msg = '') { 52 if (cond) { 53 this.skip(typeof msg === 'function' ? msg() : msg); 54 } 55 } 56 } 57 58 /** 59 * A Fixture is a class used to instantiate each test sub/case at run time. 60 * A new instance of the Fixture is created for every single test subcase 61 * (i.e. every time the test function is run). 62 */ 63 export class Fixture { 64 65 66 /** 67 * Interface for recording logs and test status. 68 * 69 * @internal 70 */ 71 72 eventualExpectations = []; 73 numOutstandingAsyncExpectations = 0; 74 objectsToCleanUp = []; 75 76 static MakeSharedState(recorder, params) { 77 return new SubcaseBatchState(recorder, params); 78 } 79 80 /** @internal */ 81 constructor(sharedState, rec, params) { 82 this._sharedState = sharedState; 83 this.rec = rec; 84 this._params = params; 85 } 86 87 /** 88 * Returns the (case+subcase) parameters for this test function invocation. 89 */ 90 get params() { 91 return this._params; 92 } 93 94 /** 95 * Gets the test fixture's shared state. This object is shared between subcases 96 * within the same testcase. 97 */ 98 get sharedState() { 99 return this._sharedState; 100 } 101 102 /** 103 * Override this to do additional pre-test-function work in a derived fixture. 104 * This has to be a member function instead of an async `createFixture` function, because 105 * we need to be able to ergonomically override it in subclasses. 106 * 107 * @internal MAINTENANCE_TODO: Make this not visible to test code? 108 */ 109 async init() {} 110 111 /** 112 * Override this to do additional post-test-function work in a derived fixture. 113 * 114 * Called even if init was unsuccessful. 115 * 116 * @internal MAINTENANCE_TODO: Make this not visible to test code? 117 */ 118 async finalize() { 119 assert( 120 this.numOutstandingAsyncExpectations === 0, 121 'there were outstanding immediateAsyncExpectations (e.g. expectUncapturedError) at the end of the test' 122 ); 123 124 // Loop to exhaust the eventualExpectations in case they chain off each other. 125 while (this.eventualExpectations.length) { 126 const p = this.eventualExpectations.shift(); 127 try { 128 await p; 129 } catch (ex) { 130 this.rec.threw(ex); 131 } 132 } 133 134 // And clean up any objects now that they're done being used. 135 for (const o of this.objectsToCleanUp) { 136 if ('getExtension' in o) { 137 const WEBGL_lose_context = o.getExtension('WEBGL_lose_context'); 138 if (WEBGL_lose_context) WEBGL_lose_context.loseContext(); 139 } else if ('destroy' in o) { 140 o.destroy(); 141 } else if ('destroyAsync' in o) { 142 await o.destroyAsync(); 143 } else if ('close' in o) { 144 o.close(); 145 } else { 146 // HTMLVideoElement 147 o.src = ''; 148 o.srcObject = null; 149 } 150 } 151 } 152 153 /** 154 * Tracks an object to be cleaned up after the test finishes. 155 * 156 * Usually when creating buffers/textures/query sets, you can use the helpers in GPUTest instead. 157 */ 158 trackForCleanup(o) { 159 if (o instanceof Promise) { 160 this.eventualAsyncExpectation(() => 161 o.then( 162 (o) => this.trackForCleanup(o), 163 () => {} 164 ) 165 ); 166 return o; 167 } 168 169 if (o instanceof GPUDevice) { 170 this.objectsToCleanUp.push({ 171 async destroyAsync() { 172 o.destroy(); 173 await o.lost; 174 } 175 }); 176 } else { 177 this.objectsToCleanUp.push(o); 178 } 179 return o; 180 } 181 182 /** Tracks an object, if it's destroyable, to be cleaned up after the test finishes. */ 183 tryTrackForCleanup(o) { 184 if (typeof o === 'object' && o !== null) { 185 if ( 186 'destroy' in o || 187 'close' in o || 188 o instanceof WebGLRenderingContext || 189 o instanceof WebGL2RenderingContext) 190 { 191 this.objectsToCleanUp.push(o); 192 } 193 } 194 return o; 195 } 196 197 /** Call requestDevice() and track the device for cleanup. */ 198 requestDeviceTracked(adapter, desc = undefined) { 199 200 return this.trackForCleanup(adapter.requestDevice(desc)); 201 } 202 203 /** Log a debug message. */ 204 debug(msg) { 205 if (!this.rec.debugging) return; 206 if (typeof msg === 'function') { 207 msg = msg(); 208 } 209 this.rec.debug(new Error(msg)); 210 } 211 212 /** 213 * Log an info message. 214 * **Use sparingly. Use `debug()` instead if logs are only needed with debug logging enabled.** 215 */ 216 info(msg) { 217 this.rec.info(new Error(msg)); 218 } 219 220 /** Throws an exception marking the subcase as skipped. */ 221 skip(msg) { 222 throw new SkipTestCase(msg); 223 } 224 225 /** Throws an exception marking the subcase as skipped if condition is true */ 226 skipIf(cond, msg = '') { 227 if (cond) { 228 this.skip(typeof msg === 'function' ? msg() : msg); 229 } 230 } 231 232 /** Log a warning and increase the result status to "Warn". */ 233 warn(msg) { 234 this.rec.warn(new Error(msg)); 235 } 236 237 /** Log an error and increase the result status to "ExpectFailed". */ 238 fail(msg) { 239 this.rec.expectationFailed(new Error(msg)); 240 } 241 242 /** 243 * Wraps an async function. Tracks its status to fail if the test tries to report a test status 244 * before the async work has finished. 245 */ 246 async immediateAsyncExpectation(fn) { 247 this.numOutstandingAsyncExpectations++; 248 const ret = await fn(); 249 this.numOutstandingAsyncExpectations--; 250 return ret; 251 } 252 253 /** 254 * Wraps an async function, passing it an `Error` object recording the original stack trace. 255 * The async work will be implicitly waited upon before reporting a test status. 256 */ 257 eventualAsyncExpectation(fn) { 258 const promise = fn(new Error()); 259 this.eventualExpectations.push(promise); 260 } 261 262 expectErrorValue(expectedError, ex, niceStack) { 263 if (!(ex instanceof Error)) { 264 niceStack.message = `THREW non-error value, of type ${typeof ex}: ${ex}`; 265 this.rec.expectationFailed(niceStack); 266 return; 267 } 268 const actualName = ex.name; 269 if (expectedError !== true && actualName !== expectedError) { 270 niceStack.message = `THREW ${actualName}, instead of ${expectedError}: ${ex}`; 271 this.rec.expectationFailed(niceStack); 272 } else { 273 niceStack.message = `OK: threw ${actualName}: ${ex.message}`; 274 this.rec.debug(niceStack); 275 } 276 } 277 278 /** Expect that the provided promise resolves (fulfills). */ 279 shouldResolve(p, msg) { 280 this.eventualAsyncExpectation(async (niceStack) => { 281 const m = msg ? ': ' + msg : ''; 282 try { 283 await p; 284 niceStack.message = 'resolved as expected' + m; 285 } catch (ex) { 286 niceStack.message = `REJECTED${m}`; 287 if (ex instanceof Error) { 288 niceStack.message += '\n' + ex.message; 289 } 290 this.rec.expectationFailed(niceStack); 291 } 292 }); 293 } 294 295 /** Expect that the provided promise rejects, with the provided exception name. */ 296 shouldReject( 297 expectedName, 298 p, 299 { allowMissingStack = false, message } = {}) 300 { 301 this.eventualAsyncExpectation(async (niceStack) => { 302 const m = message ? ': ' + message : ''; 303 try { 304 await p; 305 niceStack.message = 'DID NOT REJECT' + m; 306 this.rec.expectationFailed(niceStack); 307 } catch (ex) { 308 this.expectErrorValue(expectedName, ex, niceStack); 309 if (!allowMissingStack) { 310 if (!(ex instanceof Error && typeof ex.stack === 'string')) { 311 const exMessage = ex instanceof Error ? ex.message : '?'; 312 niceStack.message = `rejected as expected, but missing stack (${exMessage})${m}`; 313 this.rec.expectationFailed(niceStack); 314 } 315 } 316 } 317 }); 318 } 319 320 /** 321 * Expect that the provided function throws (if `true` or `string`) or not (if `false`). 322 * If a string is provided, expect that the throw exception has that name. 323 * 324 * MAINTENANCE_TODO: Change to `string | false` so the exception name is always checked. 325 */ 326 shouldThrow( 327 expectedError, 328 fn, 329 { allowMissingStack = false, message } = {}) 330 { 331 const m = message ? ': ' + message : ''; 332 try { 333 fn(); 334 if (expectedError === false) { 335 this.rec.debug(new Error('did not throw, as expected' + m)); 336 } else { 337 this.rec.expectationFailed(new Error('unexpectedly did not throw' + m)); 338 } 339 } catch (ex) { 340 if (expectedError === false) { 341 this.rec.expectationFailed(new Error('threw unexpectedly' + m)); 342 } else { 343 this.expectErrorValue(expectedError, ex, new Error(m)); 344 if (!allowMissingStack) { 345 if (!(ex instanceof Error && typeof ex.stack === 'string')) { 346 this.rec.expectationFailed(new Error('threw as expected, but missing stack' + m)); 347 } 348 } 349 } 350 } 351 } 352 353 /** 354 * Expect that a condition is true. 355 * 356 * Note: You can pass a boolean condition, or a function that returns a boolean. 357 * The advantage to passing a function is that if it's short it is self documenting. 358 * 359 * t.expect(size >= maxSize); // prints Expect OK: 360 * t.expect(() => size >= maxSize) // prints Expect OK: () => size >= maxSize 361 */ 362 expect(cond, msg) { 363 if (typeof cond === 'function') { 364 if (msg === undefined) { 365 msg = cond.toString(); 366 } 367 cond = cond(); 368 } 369 if (cond) { 370 const m = msg ? ': ' + msg : ''; 371 this.rec.debug(new Error('expect OK' + m)); 372 } else { 373 this.rec.expectationFailed(new Error(msg)); 374 } 375 return cond; 376 } 377 378 /** 379 * If the argument is an `Error`, fail (or warn). If it's `undefined`, no-op. 380 * If the argument is an array, apply the above behavior on each of elements. 381 */ 382 expectOK( 383 error, 384 { mode = 'fail', niceStack } = {}) 385 { 386 const handleError = (error) => { 387 if (error instanceof Error) { 388 if (niceStack) { 389 error.stack = niceStack.stack; 390 } 391 if (mode === 'fail') { 392 this.rec.expectationFailed(error); 393 } else if (mode === 'warn') { 394 this.rec.warn(error); 395 } else { 396 unreachable(); 397 } 398 } 399 }; 400 401 if (Array.isArray(error)) { 402 for (const e of error) { 403 handleError(e); 404 } 405 } else { 406 handleError(error); 407 } 408 } 409 410 eventualExpectOK( 411 error, 412 { mode = 'fail' } = {}) 413 { 414 this.eventualAsyncExpectation(async (niceStack) => { 415 this.expectOK(await error, { mode, niceStack }); 416 }); 417 } 418 } 419 420 421 422 /** 423 * FixtureClass encapsulates a constructor for fixture and a corresponding 424 * shared state factory function. An interface version of the type is also 425 * defined for mixin declaration use ONLY. The interface version is necessary 426 * because mixin classes need a constructor with a single any[] rest 427 * parameter. 428 */