test_group.ts (24876B)
1 import { 2 Fixture, 3 SubcaseBatchState, 4 SkipTestCase, 5 TestParams, 6 UnexpectedPassError, 7 SubcaseBatchStateFromFixture, 8 FixtureClass, 9 } from '../framework/fixture.js'; 10 import { 11 CaseParamsBuilder, 12 builderIterateCasesWithSubcases, 13 kUnitCaseParamsBuilder, 14 ParamsBuilderBase, 15 SubcaseParamsBuilder, 16 } from '../framework/params_builder.js'; 17 import { globalTestConfig } from '../framework/test_config.js'; 18 import { Expectation } from '../internal/logging/result.js'; 19 import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js'; 20 import { extractPublicParams, Merged, mergeParams } from '../internal/params_utils.js'; 21 import { compareQueries, Ordering } from '../internal/query/compare.js'; 22 import { 23 TestQueryMultiFile, 24 TestQueryMultiTest, 25 TestQuerySingleCase, 26 TestQueryWithExpectation, 27 } from '../internal/query/query.js'; 28 import { kPathSeparator } from '../internal/query/separators.js'; 29 import { 30 stringifyPublicParams, 31 stringifyPublicParamsUniquely, 32 } from '../internal/query/stringify_params.js'; 33 import { validQueryPart } from '../internal/query/validQueryPart.js'; 34 import { attemptGarbageCollection } from '../util/collect_garbage.js'; 35 import { DeepReadonly } from '../util/types.js'; 36 import { assert, unreachable } from '../util/util.js'; 37 38 import { logToWebSocket } from './websocket_logger.js'; 39 40 export type RunFn = ( 41 rec: TestCaseRecorder, 42 expectations?: TestQueryWithExpectation[] 43 ) => Promise<void>; 44 45 export interface TestCaseID { 46 readonly test: readonly string[]; 47 readonly params: TestParams; 48 } 49 50 export interface RunCase { 51 readonly id: TestCaseID; 52 readonly isUnimplemented: boolean; 53 computeSubcaseCount(): number; 54 run( 55 rec: TestCaseRecorder, 56 selfQuery: TestQuerySingleCase, 57 expectations: TestQueryWithExpectation[] 58 ): Promise<void>; 59 } 60 61 // Interface for defining tests 62 export interface TestGroupBuilder<F extends Fixture> { 63 test(name: string): TestBuilderWithName<F>; 64 } 65 export function makeTestGroup<F extends Fixture>(fixture: FixtureClass<F>): TestGroupBuilder<F> { 66 return new TestGroup(fixture as unknown as FixtureClass); 67 } 68 69 // Interfaces for running tests 70 export interface IterableTestGroup { 71 iterate(): Iterable<IterableTest>; 72 validate(fileQuery: TestQueryMultiFile): void; 73 /** Returns the file-relative test paths of tests which have >0 cases. */ 74 collectNonEmptyTests(): { testPath: string[] }[]; 75 } 76 export interface IterableTest { 77 testPath: string[]; 78 description: string | undefined; 79 readonly testCreationStack: Error; 80 iterate(caseFilter: TestParams | null): Iterable<RunCase>; 81 } 82 83 export function makeTestGroupForUnitTesting<F extends Fixture>( 84 fixture: FixtureClass<F> 85 ): TestGroup<F> { 86 return new TestGroup(fixture); 87 } 88 89 /** The maximum allowed length of a test query string. Checked by tools/validate. */ 90 export const kQueryMaxLength = 375; 91 92 /** Parameter name for batch number (see also TestBuilder.batch). */ 93 const kBatchParamName = 'batch__'; 94 95 type TestFn<F extends Fixture, P extends {}> = ( 96 t: F & { params: DeepReadonly<P> } 97 ) => Promise<void> | void; 98 type BeforeAllSubcasesFn<S extends SubcaseBatchState, P extends {}> = ( 99 s: S & { params: DeepReadonly<P> } 100 ) => Promise<void> | void; 101 102 export class TestGroup<F extends Fixture> implements TestGroupBuilder<F> { 103 private fixture: FixtureClass; 104 private seen: Set<string> = new Set(); 105 private tests: Array<TestBuilder<SubcaseBatchStateFromFixture<F>, F>> = []; 106 107 constructor(fixture: FixtureClass) { 108 this.fixture = fixture; 109 } 110 111 iterate(): Iterable<IterableTest> { 112 return this.tests; 113 } 114 115 private checkName(name: string): void { 116 assert( 117 // Shouldn't happen due to the rule above. Just makes sure that treating 118 // unencoded strings as encoded strings is OK. 119 name === decodeURIComponent(name), 120 `Not decodeURIComponent-idempotent: ${name} !== ${decodeURIComponent(name)}` 121 ); 122 assert(!this.seen.has(name), `Duplicate test name: ${name}`); 123 124 this.seen.add(name); 125 } 126 127 test(name: string): TestBuilderWithName<F> { 128 const testCreationStack = new Error(`Test created: ${name}`); 129 130 this.checkName(name); 131 132 const parts = name.split(kPathSeparator); 133 for (const p of parts) { 134 assert(validQueryPart.test(p), `Invalid test name part ${p}; must match ${validQueryPart}`); 135 } 136 137 const test = new TestBuilder(parts, this.fixture, testCreationStack); 138 this.tests.push(test); 139 return test as unknown as TestBuilderWithName<F>; 140 } 141 142 validate(fileQuery: TestQueryMultiFile): void { 143 for (const test of this.tests) { 144 const testQuery = new TestQueryMultiTest( 145 fileQuery.suite, 146 fileQuery.filePathParts, 147 test.testPath 148 ); 149 test.validate(testQuery); 150 } 151 } 152 153 collectNonEmptyTests(): { testPath: string[] }[] { 154 const testPaths = []; 155 for (const test of this.tests) { 156 if (test.computeCaseCount() > 0) { 157 testPaths.push({ testPath: test.testPath }); 158 } 159 } 160 return testPaths; 161 } 162 } 163 164 interface TestBuilderWithName<F extends Fixture> extends TestBuilderWithParams<F, {}, {}> { 165 desc(description: string): this; 166 /** 167 * A noop function to associate a test with the relevant part of the specification. 168 * 169 * @param url a link to the spec where test is extracted from. 170 */ 171 specURL(url: string): this; 172 /** 173 * Parameterize the test, generating multiple cases, each possibly having subcases. 174 * 175 * The `unit` value passed to the `cases` callback is an immutable constant 176 * `CaseParamsBuilder<{}>` representing the "unit" builder `[ {} ]`, 177 * provided for convenience. The non-callback overload can be used if `unit` is not needed. 178 */ 179 params<CaseP extends {}, SubcaseP extends {}>( 180 cases: (unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<CaseP, SubcaseP> 181 ): TestBuilderWithParams<F, CaseP, SubcaseP>; 182 /** 183 * Parameterize the test, generating multiple cases, each possibly having subcases. 184 * 185 * Use the callback overload of this method if a "unit" builder is needed. 186 */ 187 params<CaseP extends {}, SubcaseP extends {}>( 188 cases: ParamsBuilderBase<CaseP, SubcaseP> 189 ): TestBuilderWithParams<F, CaseP, SubcaseP>; 190 191 /** 192 * Parameterize the test, generating multiple cases, without subcases. 193 */ 194 paramsSimple<P extends {}>(cases: Iterable<P>): TestBuilderWithParams<F, P, {}>; 195 196 /** 197 * Parameterize the test, generating one case with multiple subcases. 198 */ 199 paramsSubcasesOnly<P extends {}>(subcases: Iterable<P>): TestBuilderWithParams<F, {}, P>; 200 /** 201 * Parameterize the test, generating one case with multiple subcases. 202 * 203 * The `unit` value passed to the `subcases` callback is an immutable constant 204 * `SubcaseParamsBuilder<{}>`, with one empty case `{}` and one empty subcase `{}`. 205 */ 206 paramsSubcasesOnly<P extends {}>( 207 subcases: (unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, P> 208 ): TestBuilderWithParams<F, {}, P>; 209 } 210 211 interface TestBuilderWithParams<F extends Fixture, CaseP extends {}, SubcaseP extends {}> { 212 /** 213 * Limit subcases to a maximum number of per testcase. 214 * @param b the maximum number of subcases per testcase. 215 * 216 * If the number of subcases exceeds `b`, add an internal 217 * numeric, incrementing `batch__` param to split subcases 218 * into groups of at most `b` subcases. 219 */ 220 batch(b: number): this; 221 /** 222 * Run a function on shared subcase batch state before each 223 * batch of subcases. 224 * @param fn the function to run. It is called with the test 225 * fixture's shared subcase batch state. 226 * 227 * Generally, this function should be careful to avoid mutating 228 * any state on the shared subcase batch state which could result 229 * in unexpected order-dependent test behavior. 230 */ 231 beforeAllSubcases(fn: BeforeAllSubcasesFn<SubcaseBatchStateFromFixture<F>, CaseP>): this; 232 /** 233 * Set the test function. 234 * @param fn the test function. 235 */ 236 fn(fn: TestFn<F, Merged<CaseP, SubcaseP>>): void; 237 /** 238 * Mark the test as unimplemented. 239 */ 240 unimplemented(): void; 241 } 242 243 class TestBuilder<S extends SubcaseBatchState, F extends Fixture> { 244 readonly testPath: string[]; 245 isUnimplemented: boolean; 246 description: string | undefined; 247 readonly testCreationStack: Error; 248 249 private readonly fixture: FixtureClass; 250 private testFn: TestFn<Fixture, {}> | undefined; 251 private beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined; 252 private testCases?: ParamsBuilderBase<{}, {}> = undefined; 253 private batchSize: number = 0; 254 255 constructor(testPath: string[], fixture: FixtureClass, testCreationStack: Error) { 256 this.testPath = testPath; 257 this.isUnimplemented = false; 258 this.fixture = fixture; 259 this.testCreationStack = testCreationStack; 260 } 261 262 desc(description: string): this { 263 this.description = description.trim(); 264 return this; 265 } 266 267 specURL(_url: string): this { 268 return this; 269 } 270 271 beforeAllSubcases(fn: BeforeAllSubcasesFn<SubcaseBatchState, {}>): this { 272 assert(this.beforeFn === undefined); 273 this.beforeFn = fn; 274 return this; 275 } 276 277 fn(fn: TestFn<Fixture, {}>): void { 278 // eslint-disable-next-line no-warning-comments 279 // MAINTENANCE_TODO: add "TODO" if there's no description? (and make sure it only ends up on 280 // actual tests, not on test parents in the tree, which is what happens if you do it here, not 281 // sure why) 282 assert(this.testFn === undefined); 283 this.testFn = fn; 284 } 285 286 batch(b: number): this { 287 this.batchSize = b; 288 return this; 289 } 290 291 unimplemented(): void { 292 assert(this.testFn === undefined); 293 294 this.description = 295 (this.description ? this.description + '\n\n' : '') + 'TODO: .unimplemented()'; 296 this.isUnimplemented = true; 297 298 // Use the beforeFn to skip the test, so we don't have to iterate the subcases. 299 this.beforeFn = () => { 300 throw new SkipTestCase('test unimplemented'); 301 }; 302 this.testFn = () => {}; 303 } 304 305 /** Perform various validation/"lint" chenks. */ 306 validate(testQuery: TestQueryMultiTest): void { 307 const testPathString = this.testPath.join(kPathSeparator); 308 assert(this.testFn !== undefined, () => { 309 let s = `Test is missing .fn(): ${testPathString}`; 310 if (this.testCreationStack.stack) { 311 s += `\n-> test created at:\n${this.testCreationStack.stack}`; 312 } 313 return s; 314 }); 315 316 assert( 317 testQuery.toString().length <= kQueryMaxLength, 318 () => 319 `Test query ${testQuery} is too long. Max length is ${kQueryMaxLength} characters. Please shorten names or reduce parameters.` 320 ); 321 322 if (this.testCases === undefined) { 323 return; 324 } 325 326 const seen = new Set<string>(); 327 for (const [caseParams, subcases] of builderIterateCasesWithSubcases(this.testCases, null)) { 328 const caseQuery = new TestQuerySingleCase( 329 testQuery.suite, 330 testQuery.filePathParts, 331 testQuery.testPathParts, 332 caseParams 333 ).toString(); 334 assert( 335 caseQuery.length <= kQueryMaxLength, 336 () => 337 `Case query ${caseQuery} is too long. Max length is ${kQueryMaxLength} characters. Please shorten names or reduce parameters.` 338 ); 339 340 for (const subcaseParams of subcases ?? [{}]) { 341 const params = mergeParams(caseParams, subcaseParams); 342 assert(this.batchSize === 0 || !(kBatchParamName in params)); 343 344 // stringifyPublicParams also checks for invalid params values 345 let testcaseString; 346 try { 347 testcaseString = stringifyPublicParams(params); 348 } catch (e) { 349 throw new Error(`${e}: ${testPathString}`); 350 } 351 352 // A (hopefully) unique representation of a params value. 353 const testcaseStringUnique = stringifyPublicParamsUniquely(params); 354 assert( 355 !seen.has(testcaseStringUnique), 356 `Duplicate public test case+subcase params for test ${testPathString}: ${testcaseString} (${caseQuery})` 357 ); 358 seen.add(testcaseStringUnique); 359 } 360 } 361 } 362 363 computeCaseCount(): number { 364 if (this.testCases === undefined) { 365 return 1; 366 } 367 368 let caseCount = 0; 369 for (const [_caseParams, _subcases] of builderIterateCasesWithSubcases(this.testCases, null)) { 370 caseCount++; 371 } 372 return caseCount; 373 } 374 375 params( 376 cases: ((unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<{}, {}>) | ParamsBuilderBase<{}, {}> 377 ): TestBuilder<S, F> { 378 assert(this.testCases === undefined, 'test case is already parameterized'); 379 if (cases instanceof Function) { 380 this.testCases = cases(kUnitCaseParamsBuilder); 381 } else { 382 this.testCases = cases; 383 } 384 return this; 385 } 386 387 paramsSimple(cases: Iterable<{}>): TestBuilder<S, F> { 388 assert(this.testCases === undefined, 'test case is already parameterized'); 389 this.testCases = kUnitCaseParamsBuilder.combineWithParams(cases); 390 return this; 391 } 392 393 paramsSubcasesOnly( 394 subcases: Iterable<{}> | ((unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, {}>) 395 ): TestBuilder<S, F> { 396 if (subcases instanceof Function) { 397 return this.params(subcases(kUnitCaseParamsBuilder.beginSubcases())); 398 } else { 399 return this.params(kUnitCaseParamsBuilder.beginSubcases().combineWithParams(subcases)); 400 } 401 } 402 403 private makeCaseSpecific(params: {}, subcases: Iterable<{}> | undefined) { 404 assert(this.testFn !== undefined, 'No test function (.fn()) for test'); 405 return new RunCaseSpecific( 406 this.testPath, 407 params, 408 this.isUnimplemented, 409 subcases, 410 this.fixture, 411 this.testFn, 412 this.beforeFn, 413 this.testCreationStack 414 ); 415 } 416 417 *iterate(caseFilter: TestParams | null): IterableIterator<RunCase> { 418 this.testCases ??= kUnitCaseParamsBuilder; 419 420 // Remove the batch__ from the caseFilter because the params builder doesn't 421 // know about it (we don't add it until later in this function). 422 let filterToBatch: number | undefined; 423 const caseFilterWithoutBatch = caseFilter ? { ...caseFilter } : null; 424 if (caseFilterWithoutBatch && kBatchParamName in caseFilterWithoutBatch) { 425 const batchParam = caseFilterWithoutBatch[kBatchParamName]; 426 assert(typeof batchParam === 'number'); 427 filterToBatch = batchParam; 428 delete caseFilterWithoutBatch[kBatchParamName]; 429 } 430 431 for (const [caseParams, subcases] of builderIterateCasesWithSubcases( 432 this.testCases, 433 caseFilterWithoutBatch 434 )) { 435 // If batches are not used, yield just one case. 436 if (this.batchSize === 0 || subcases === undefined) { 437 yield this.makeCaseSpecific(caseParams, subcases); 438 continue; 439 } 440 441 // Same if there ends up being only one batch. 442 const subcaseArray = Array.from(subcases); 443 if (subcaseArray.length <= this.batchSize) { 444 yield this.makeCaseSpecific(caseParams, subcaseArray); 445 continue; 446 } 447 448 // There are multiple batches. Helper function for this case: 449 const makeCaseForBatch = (batch: number) => { 450 const sliceStart = batch * this.batchSize; 451 return this.makeCaseSpecific( 452 { ...caseParams, [kBatchParamName]: batch }, 453 subcaseArray.slice(sliceStart, Math.min(subcaseArray.length, sliceStart + this.batchSize)) 454 ); 455 }; 456 457 // If we filter to just one batch, yield it. 458 if (filterToBatch !== undefined) { 459 yield makeCaseForBatch(filterToBatch); 460 continue; 461 } 462 463 // Finally, if not, yield all of the batches. 464 for (let batch = 0; batch * this.batchSize < subcaseArray.length; ++batch) { 465 yield makeCaseForBatch(batch); 466 } 467 } 468 } 469 } 470 471 class RunCaseSpecific implements RunCase { 472 readonly id: TestCaseID; 473 readonly isUnimplemented: boolean; 474 475 private readonly params: {}; 476 private readonly subcases: Iterable<{}> | undefined; 477 private readonly fixture: FixtureClass; 478 private readonly fn: TestFn<Fixture, {}>; 479 private readonly beforeFn?: BeforeAllSubcasesFn<SubcaseBatchState, {}>; 480 private readonly testCreationStack: Error; 481 482 constructor( 483 testPath: string[], 484 params: {}, 485 isUnimplemented: boolean, 486 subcases: Iterable<{}> | undefined, 487 fixture: FixtureClass, 488 fn: TestFn<Fixture, {}>, 489 beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined, 490 testCreationStack: Error 491 ) { 492 this.id = { test: testPath, params: extractPublicParams(params) }; 493 this.isUnimplemented = isUnimplemented; 494 this.params = params; 495 this.subcases = subcases; 496 this.fixture = fixture; 497 this.fn = fn; 498 this.beforeFn = beforeFn; 499 this.testCreationStack = testCreationStack; 500 } 501 502 computeSubcaseCount(): number { 503 if (this.subcases) { 504 let count = 0; 505 for (const _subcase of this.subcases) { 506 count++; 507 } 508 return count; 509 } else { 510 return 1; 511 } 512 } 513 514 async runTest( 515 rec: TestCaseRecorder, 516 sharedState: SubcaseBatchState, 517 params: TestParams, 518 throwSkip: boolean, 519 expectedStatus: Expectation 520 ): Promise<void> { 521 try { 522 rec.beginSubCase(); 523 if (expectedStatus === 'skip') { 524 throw new SkipTestCase('Skipped by expectations'); 525 } 526 527 const inst = new this.fixture(sharedState, rec, params); 528 try { 529 await inst.init(); 530 await this.fn(inst as Fixture & { params: {} }); 531 rec.passed(); 532 } finally { 533 // Runs as long as constructor succeeded, even if initialization or the test failed. 534 await inst.finalize(); 535 } 536 } catch (ex) { 537 // There was an exception from constructor, init, test, or finalize. 538 // An error from init or test may have been a SkipTestCase. 539 // An error from finalize may have been an eventualAsyncExpectation failure 540 // or unexpected validation/OOM error from the GPUDevice. 541 rec.threw(ex); 542 if (throwSkip && ex instanceof SkipTestCase) { 543 throw ex; 544 } 545 } finally { 546 try { 547 rec.endSubCase(expectedStatus); 548 } catch (ex) { 549 assert(ex instanceof UnexpectedPassError); 550 ex.message = `Testcase passed unexpectedly.`; 551 ex.stack = this.testCreationStack.stack; 552 rec.warn(ex); 553 } 554 } 555 } 556 557 async run( 558 rec: TestCaseRecorder, 559 selfQuery: TestQuerySingleCase, 560 expectations: TestQueryWithExpectation[] 561 ): Promise<void> { 562 const getExpectedStatus = (selfQueryWithSubParams: TestQuerySingleCase) => { 563 let didSeeFail = false; 564 for (const exp of expectations) { 565 const ordering = compareQueries(exp.query, selfQueryWithSubParams); 566 if (ordering === Ordering.Unordered || ordering === Ordering.StrictSubset) { 567 continue; 568 } 569 570 switch (exp.expectation) { 571 // Skip takes precedence. If there is any expectation indicating a skip, 572 // signal it immediately. 573 case 'skip': 574 return 'skip'; 575 case 'fail': 576 // Otherwise, indicate that we might expect a failure. 577 didSeeFail = true; 578 break; 579 default: 580 unreachable(); 581 } 582 } 583 return didSeeFail ? 'fail' : 'pass'; 584 }; 585 586 const { testHeartbeatCallback, maxSubcasesInFlight } = globalTestConfig; 587 try { 588 rec.start(); 589 const sharedState = this.fixture.MakeSharedState(rec, this.params); 590 try { 591 await sharedState.init(); 592 if (this.beforeFn) { 593 await this.beforeFn(sharedState); 594 } 595 await sharedState.postInit(); 596 testHeartbeatCallback(); 597 598 let allPreviousSubcasesFinalizedPromise: Promise<void> = Promise.resolve(); 599 if (this.subcases) { 600 let totalCount = 0; 601 let skipCount = 0; 602 603 // If there are too many subcases in flight, starting the next subcase will register 604 // `resolvePromiseBlockingSubcase` and wait until `subcaseFinishedCallback` is called. 605 let subcasesInFlight = 0; 606 let resolvePromiseBlockingSubcase: (() => void) | undefined = undefined; 607 const subcaseFinishedCallback = () => { 608 subcasesInFlight -= 1; 609 // If there is any subcase waiting on a previous subcase to finish, 610 // unblock it now, and clear the resolve callback. 611 if (resolvePromiseBlockingSubcase) { 612 resolvePromiseBlockingSubcase(); 613 resolvePromiseBlockingSubcase = undefined; 614 } 615 }; 616 617 for (const subParams of this.subcases) { 618 // Defer subcase logs so that they appear in the correct order. 619 const subRec = rec.makeDeferredSubRecorder( 620 `(in subcase: ${stringifyPublicParams(subParams)}) `, 621 allPreviousSubcasesFinalizedPromise 622 ); 623 624 const params = mergeParams(this.params, subParams); 625 const subcaseQuery = new TestQuerySingleCase( 626 selfQuery.suite, 627 selfQuery.filePathParts, 628 selfQuery.testPathParts, 629 params 630 ); 631 632 // Limit the maximum number of subcases in flight. 633 if (subcasesInFlight >= maxSubcasesInFlight) { 634 await new Promise<void>(resolve => { 635 // There should only be one subcase waiting at a time. 636 assert(resolvePromiseBlockingSubcase === undefined); 637 resolvePromiseBlockingSubcase = resolve; 638 }); 639 } 640 641 subcasesInFlight += 1; 642 // Runs async without waiting so that subsequent subcases can start. 643 // All finalization steps will be waited on at the end of the testcase. 644 const finalizePromise = this.runTest( 645 subRec, 646 sharedState, 647 params, 648 /* throwSkip */ true, 649 getExpectedStatus(subcaseQuery) 650 ) 651 .then(() => { 652 subRec.info(new Error('subcase ran')); 653 }) 654 .catch(ex => { 655 if (ex instanceof SkipTestCase) { 656 // Convert SkipTestCase to an info message so it won't skip the whole test 657 ex.message = 'subcase skipped: ' + ex.message; 658 subRec.info(new Error('subcase skipped')); 659 ++skipCount; 660 } else { 661 // We are catching all other errors inside runTest(), so this should never happen 662 subRec.threw(ex); 663 } 664 }) 665 .finally(attemptGarbageCollectionIfDue) 666 .finally(subcaseFinishedCallback); 667 668 allPreviousSubcasesFinalizedPromise = allPreviousSubcasesFinalizedPromise.then( 669 () => finalizePromise 670 ); 671 ++totalCount; 672 } 673 674 // Wait for all subcases to finalize and report their results. 675 await allPreviousSubcasesFinalizedPromise; 676 677 if (skipCount === totalCount) { 678 rec.skipped(new SkipTestCase('all subcases were skipped')); 679 } 680 } else { 681 try { 682 await this.runTest( 683 rec, 684 sharedState, 685 this.params, 686 /* throwSkip */ false, 687 getExpectedStatus(selfQuery) 688 ); 689 } finally { 690 await attemptGarbageCollectionIfDue(); 691 } 692 } 693 } finally { 694 testHeartbeatCallback(); 695 // Runs as long as the shared state constructor succeeded, even if initialization or a test failed. 696 await sharedState.finalize(); 697 testHeartbeatCallback(); 698 } 699 } catch (ex) { 700 // There was an exception from sharedState/fixture constructor, init, beforeFn, or test. 701 // An error from beforeFn may have been SkipTestCase. 702 // An error from finalize may have been an eventualAsyncExpectation failure 703 // or unexpected validation/OOM error from the GPUDevice. 704 rec.threw(ex); 705 } finally { 706 rec.finish(); 707 708 const msg: CaseTimingLogLine = { 709 q: selfQuery.toString(), 710 timems: rec.result.timems, 711 nonskippedSubcaseCount: rec.nonskippedSubcaseCount, 712 }; 713 logToWebSocket(JSON.stringify(msg)); 714 } 715 } 716 } 717 718 export type CaseTimingLogLine = { 719 q: string; 720 /** Total time it took to execute the case. */ 721 timems: number; 722 /** 723 * Number of subcases that ran in the case (excluding skipped subcases, so 724 * they don't dilute the average per-subcase time. 725 */ 726 nonskippedSubcaseCount: number; 727 }; 728 729 /** Every `subcasesBetweenAttemptingGC` calls to this function will `attemptGarbageCollection()`. */ 730 const attemptGarbageCollectionIfDue: () => Promise<void> = (() => { 731 // This state is global because garbage is global. 732 let subcasesSinceLastGC = 0; 733 734 return async function attemptGarbageCollectionIfDue() { 735 subcasesSinceLastGC++; 736 if (subcasesSinceLastGC >= globalTestConfig.subcasesBetweenAttemptingGC) { 737 subcasesSinceLastGC = 0; 738 return attemptGarbageCollection(); 739 } 740 }; 741 })();