test_group.js (17700B)
1 /** 2 * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts 3 **/import { 4 SkipTestCase, 5 6 UnexpectedPassError } from 7 8 9 '../framework/fixture.js'; 10 import { 11 12 builderIterateCasesWithSubcases, 13 kUnitCaseParamsBuilder } from 14 15 16 '../framework/params_builder.js'; 17 import { globalTestConfig } from '../framework/test_config.js'; 18 19 20 import { extractPublicParams, mergeParams } from '../internal/params_utils.js'; 21 import { compareQueries, Ordering } from '../internal/query/compare.js'; 22 import { 23 24 TestQueryMultiTest, 25 TestQuerySingleCase } from 26 27 '../internal/query/query.js'; 28 import { kPathSeparator } from '../internal/query/separators.js'; 29 import { 30 stringifyPublicParams, 31 stringifyPublicParamsUniquely } from 32 '../internal/query/stringify_params.js'; 33 import { validQueryPart } from '../internal/query/validQueryPart.js'; 34 import { attemptGarbageCollection } from '../util/collect_garbage.js'; 35 36 import { assert, unreachable } from '../util/util.js'; 37 38 import { logToWebSocket } from './websocket_logger.js'; 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // Interface for defining tests 62 63 64 65 export function makeTestGroup(fixture) { 66 return new TestGroup(fixture); 67 } 68 69 // Interfaces for running tests 70 71 72 73 74 75 76 77 78 79 80 81 82 83 export function makeTestGroupForUnitTesting( 84 fixture) 85 { 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 96 97 98 99 100 101 102 export class TestGroup { 103 104 seen = new Set(); 105 tests = []; 106 107 constructor(fixture) { 108 this.fixture = fixture; 109 } 110 111 iterate() { 112 return this.tests; 113 } 114 115 checkName(name) { 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) { 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; 140 } 141 142 validate(fileQuery) { 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() { 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 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 class TestBuilder { 244 245 246 247 248 249 250 251 252 testCases = undefined; 253 batchSize = 0; 254 255 constructor(testPath, fixture, testCreationStack) { 256 this.testPath = testPath; 257 this.isUnimplemented = false; 258 this.fixture = fixture; 259 this.testCreationStack = testCreationStack; 260 } 261 262 desc(description) { 263 this.description = description.trim(); 264 return this; 265 } 266 267 specURL(_url) { 268 return this; 269 } 270 271 beforeAllSubcases(fn) { 272 assert(this.beforeFn === undefined); 273 this.beforeFn = fn; 274 return this; 275 } 276 277 fn(fn) { 278 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) { 287 this.batchSize = b; 288 return this; 289 } 290 291 unimplemented() { 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) { 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(); 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() { 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) 377 { 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) { 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) 395 { 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 makeCaseSpecific(params, subcases) { 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) { 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; 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) => { 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 { 472 473 474 475 476 477 478 479 480 481 482 constructor( 483 testPath, 484 params, 485 isUnimplemented, 486 subcases, 487 fixture, 488 fn, 489 beforeFn, 490 testCreationStack) 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() { 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, 516 sharedState, 517 params, 518 throwSkip, 519 expectedStatus) 520 { 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); 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, 559 selfQuery, 560 expectations) 561 { 562 const getExpectedStatus = (selfQueryWithSubParams) => { 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.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 = 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((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 = { 709 q: selfQuery.toString(), 710 timems: rec.result.timems, 711 nonskippedSubcaseCount: rec.nonskippedSubcaseCount 712 }; 713 logToWebSocket(JSON.stringify(msg)); 714 } 715 } 716 } 717 718 719 720 721 722 723 724 725 726 727 728 729 /** Every `subcasesBetweenAttemptingGC` calls to this function will `attemptGarbageCollection()`. */ 730 const attemptGarbageCollectionIfDue = (() => { 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 })();