tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 })();