tor-browser

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

params_builder.ts (13708B)


      1 import { Merged, mergeParams, mergeParamsChecked } from '../internal/params_utils.js';
      2 import { comparePublicParamsPaths, Ordering } from '../internal/query/compare.js';
      3 import { stringifyPublicParams } from '../internal/query/stringify_params.js';
      4 import { DeepReadonly } from '../util/types.js';
      5 import { assert, mapLazy, objectEquals } from '../util/util.js';
      6 
      7 import { TestParams } from './fixture.js';
      8 
      9 // ================================================================
     10 // "Public" ParamsBuilder API / Documentation
     11 // ================================================================
     12 
     13 /**
     14 * Provides doc comments for the methods of CaseParamsBuilder and SubcaseParamsBuilder.
     15 * (Also enforces rough interface match between them.)
     16 */
     17 export interface ParamsBuilder {
     18  /**
     19   * Expands each item in `this` into zero or more items.
     20   * Each item has its parameters expanded with those returned by the `expander`.
     21   *
     22   * **Note:** When only a single key is being added, use the simpler `expand` for readability.
     23   *
     24   * ```text
     25   *               this = [     a       ,      b     ,       c       ]
     26   * this.map(expander) = [   f(a)           f(b)          f(c)      ]
     27   *                    = [[a1, a2, a3] ,    [ b1 ]  ,       []      ]
     28   *  merge and flatten = [ merge(a, a1), merge(a, a2), merge(a, a3), merge(b, b1) ]
     29   * ```
     30   */
     31  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     32  expandWithParams(expander: (_: any) => any): any;
     33 
     34  /**
     35   * Expands each item in `this` into zero or more items. Each item has its parameters expanded
     36   * with one new key, `key`, and the values returned by `expander`.
     37   */
     38  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     39  expand(key: string, expander: (_: any) => any): any;
     40 
     41  /**
     42   * Expands each item in `this` to multiple items, one for each item in `newParams`.
     43   *
     44   * In other words, takes the cartesian product of [ the items in `this` ] and `newParams`.
     45   *
     46   * **Note:** When only a single key is being added, use the simpler `combine` for readability.
     47   *
     48   * ```text
     49   *                     this = [ {a:1}, {b:2} ]
     50   *                newParams = [ {x:1}, {y:2} ]
     51   * this.combineP(newParams) = [ {a:1,x:1}, {a:1,y:2}, {b:2,x:1}, {b:2,y:2} ]
     52   * ```
     53   */
     54  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     55  combineWithParams(newParams: Iterable<any>): any;
     56 
     57  /**
     58   * Expands each item in `this` to multiple items with `{ [name]: value }` for each value.
     59   *
     60   * In other words, takes the cartesian product of [ the items in `this` ]
     61   * and `[ {[name]: value} for each value in values ]`
     62   */
     63  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     64  combine(key: string, newParams: Iterable<any>): any;
     65 
     66  /**
     67   * Filters `this` to only items for which `pred` returns true.
     68   */
     69  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     70  filter(pred: (_: any) => boolean): any;
     71 
     72  /**
     73   * Filters `this` to only items for which `pred` returns false.
     74   */
     75  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     76  unless(pred: (_: any) => boolean): any;
     77 }
     78 
     79 /**
     80 * Determines the resulting parameter object type which would be generated by an object of
     81 * the given ParamsBuilder type.
     82 */
     83 export type ParamTypeOf<
     84  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
     85  T extends ParamsBuilder,
     86 > = T extends SubcaseParamsBuilder<infer CaseP, infer SubcaseP>
     87  ? Merged<CaseP, SubcaseP>
     88  : T extends CaseParamsBuilder<infer CaseP>
     89  ? CaseP
     90  : never;
     91 
     92 // ================================================================
     93 // Implementation
     94 // ================================================================
     95 
     96 /**
     97 * Iterable over pairs of either:
     98 * - `[case params, Iterable<subcase params>]` if there are subcases.
     99 * - `[case params, undefined]` if not.
    100 */
    101 export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
    102  readonly [DeepReadonly<CaseP>, Iterable<DeepReadonly<SubcaseP>> | undefined]
    103 >;
    104 
    105 /**
    106 * Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
    107 */
    108 export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
    109  protected readonly cases: (caseFilter: TestParams | null) => Generator<CaseP>;
    110 
    111  constructor(cases: (caseFilter: TestParams | null) => Generator<CaseP>) {
    112    this.cases = cases;
    113  }
    114 
    115  /**
    116   * Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
    117   */
    118  protected abstract iterateCasesWithSubcases(
    119    caseFilter: TestParams | null
    120  ): CaseSubcaseIterable<CaseP, SubcaseP>;
    121 }
    122 
    123 /**
    124 * Calls the (normally hidden) `iterateCasesWithSubcases()` method.
    125 */
    126 export function builderIterateCasesWithSubcases(
    127  builder: ParamsBuilderBase<{}, {}>,
    128  caseFilter: TestParams | null
    129 ) {
    130  interface IterableParamsBuilder {
    131    iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<{}, {}>;
    132  }
    133 
    134  return (builder as unknown as IterableParamsBuilder).iterateCasesWithSubcases(caseFilter);
    135 }
    136 
    137 /**
    138 * Builder for combinatorial test **case** parameters.
    139 *
    140 * CaseParamsBuilder is immutable. Each method call returns a new, immutable object,
    141 * modifying the list of cases according to the method called.
    142 *
    143 * This means, for example, that the `unit` passed into `TestBuilder.params()` can be reused.
    144 */
    145 export class CaseParamsBuilder<CaseP extends {}>
    146  extends ParamsBuilderBase<CaseP, {}>
    147  implements Iterable<DeepReadonly<CaseP>>, ParamsBuilder
    148 {
    149  *iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<CaseP, {}> {
    150    for (const caseP of this.cases(caseFilter)) {
    151      if (caseFilter) {
    152        // this.cases() only filters out cases which conflict with caseFilter. Now that we have
    153        // the final caseP, filter out cases which are missing keys that caseFilter requires.
    154        const ordering = comparePublicParamsPaths(caseP, caseFilter);
    155        if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) {
    156          continue;
    157        }
    158      }
    159 
    160      yield [caseP as DeepReadonly<typeof caseP>, undefined];
    161    }
    162  }
    163 
    164  [Symbol.iterator](): Iterator<DeepReadonly<CaseP>> {
    165    return this.cases(null) as Iterator<DeepReadonly<CaseP>>;
    166  }
    167 
    168  /** @inheritDoc */
    169  expandWithParams<NewP extends {}>(
    170    expander: (_: CaseP) => Iterable<NewP>
    171  ): CaseParamsBuilder<Merged<CaseP, NewP>> {
    172    const baseGenerator = this.cases;
    173    return new CaseParamsBuilder(function* (caseFilter) {
    174      for (const a of baseGenerator(caseFilter)) {
    175        for (const b of expander(a)) {
    176          if (caseFilter) {
    177            // If the expander generated any key-value pair that conflicts with caseFilter, skip.
    178            const kvPairs = Object.entries(b);
    179            if (kvPairs.some(([k, v]) => k in caseFilter && !objectEquals(caseFilter[k], v))) {
    180              continue;
    181            }
    182          }
    183 
    184          yield mergeParamsChecked(a, b);
    185        }
    186      }
    187    });
    188  }
    189 
    190  /** @inheritDoc */
    191  expand<NewPKey extends string, NewPValue>(
    192    key: NewPKey,
    193    expander: (_: CaseP) => Iterable<NewPValue>
    194  ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
    195    const baseGenerator = this.cases;
    196    return new CaseParamsBuilder(function* (caseFilter) {
    197      for (const a of baseGenerator(caseFilter)) {
    198        assert(!(key in a), `New key '${key}' already exists in ${JSON.stringify(a)}`);
    199 
    200        for (const v of expander(a)) {
    201          // If the expander generated a value for this key that conflicts with caseFilter, skip.
    202          if (caseFilter && key in caseFilter) {
    203            if (!objectEquals(caseFilter[key], v)) {
    204              continue;
    205            }
    206          }
    207          yield { ...a, [key]: v } as Merged<CaseP, { [name in NewPKey]: NewPValue }>;
    208        }
    209      }
    210    });
    211  }
    212 
    213  /** @inheritDoc */
    214  combineWithParams<NewP extends {}>(
    215    newParams: Iterable<NewP>
    216  ): CaseParamsBuilder<Merged<CaseP, NewP>> {
    217    assertNotGenerator(newParams);
    218    const seenValues = new Set<string>();
    219    for (const params of newParams) {
    220      const paramsStr = stringifyPublicParams(params);
    221      assert(!seenValues.has(paramsStr), `Duplicate entry in combine[WithParams]: ${paramsStr}`);
    222      seenValues.add(paramsStr);
    223    }
    224 
    225    return this.expandWithParams(() => newParams);
    226  }
    227 
    228  /** @inheritDoc */
    229  combine<NewPKey extends string, NewPValue>(
    230    key: NewPKey,
    231    values: Iterable<NewPValue>
    232  ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
    233    assertNotGenerator(values);
    234    const mapped = mapLazy(values, v => ({ [key]: v }) as { [name in NewPKey]: NewPValue });
    235    return this.combineWithParams(mapped);
    236  }
    237 
    238  /** @inheritDoc */
    239  filter(pred: (_: CaseP) => boolean): CaseParamsBuilder<CaseP> {
    240    const baseGenerator = this.cases;
    241    return new CaseParamsBuilder(function* (caseFilter) {
    242      for (const a of baseGenerator(caseFilter)) {
    243        if (pred(a)) yield a;
    244      }
    245    });
    246  }
    247 
    248  /** @inheritDoc */
    249  unless(pred: (_: CaseP) => boolean): CaseParamsBuilder<CaseP> {
    250    return this.filter(x => !pred(x));
    251  }
    252 
    253  /**
    254   * "Finalize" the list of cases and begin defining subcases.
    255   * Returns a new SubcaseParamsBuilder. Methods called on SubcaseParamsBuilder
    256   * generate new subcases instead of new cases.
    257   */
    258  beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
    259    return new SubcaseParamsBuilder(this.cases, function* () {
    260      yield {};
    261    });
    262  }
    263 }
    264 
    265 /**
    266 * The unit CaseParamsBuilder, representing a single case with no params: `[ {} ]`.
    267 *
    268 * `punit` is passed to every `.params()`/`.paramsSubcasesOnly()` call, so `kUnitCaseParamsBuilder`
    269 * is only explicitly needed if constructing a ParamsBuilder outside of a test builder.
    270 */
    271 export const kUnitCaseParamsBuilder = new CaseParamsBuilder(function* () {
    272  yield {};
    273 });
    274 
    275 /**
    276 * Builder for combinatorial test _subcase_ parameters.
    277 *
    278 * SubcaseParamsBuilder is immutable. Each method call returns a new, immutable object,
    279 * modifying the list of subcases according to the method called.
    280 */
    281 export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
    282  extends ParamsBuilderBase<CaseP, SubcaseP>
    283  implements ParamsBuilder
    284 {
    285  protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;
    286 
    287  constructor(
    288    cases: (caseFilter: TestParams | null) => Generator<CaseP>,
    289    generator: (_: CaseP) => Generator<SubcaseP>
    290  ) {
    291    super(cases);
    292    this.subcases = generator;
    293  }
    294 
    295  *iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<CaseP, SubcaseP> {
    296    for (const caseP of this.cases(caseFilter)) {
    297      if (caseFilter) {
    298        // this.cases() only filters out cases which conflict with caseFilter. Now that we have
    299        // the final caseP, filter out cases which are missing keys that caseFilter requires.
    300        const ordering = comparePublicParamsPaths(caseP, caseFilter);
    301        if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) {
    302          continue;
    303        }
    304      }
    305 
    306      const subcases = Array.from(this.subcases(caseP));
    307      if (subcases.length) {
    308        yield [
    309          caseP as DeepReadonly<typeof caseP>,
    310          subcases as DeepReadonly<(typeof subcases)[number]>[],
    311        ];
    312      }
    313    }
    314  }
    315 
    316  /** @inheritDoc */
    317  expandWithParams<NewP extends {}>(
    318    expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
    319  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
    320    const baseGenerator = this.subcases;
    321    return new SubcaseParamsBuilder(this.cases, function* (base) {
    322      for (const a of baseGenerator(base)) {
    323        for (const b of expander(mergeParams(base, a))) {
    324          yield mergeParamsChecked(a, b);
    325        }
    326      }
    327    });
    328  }
    329 
    330  /** @inheritDoc */
    331  expand<NewPKey extends string, NewPValue>(
    332    key: NewPKey,
    333    expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
    334  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
    335    const baseGenerator = this.subcases;
    336    return new SubcaseParamsBuilder(this.cases, function* (base) {
    337      for (const a of baseGenerator(base)) {
    338        const before = mergeParams(base, a);
    339        assert(!(key in before), () => `Key '${key}' already exists in ${JSON.stringify(before)}`);
    340 
    341        for (const v of expander(before)) {
    342          yield { ...a, [key]: v } as Merged<SubcaseP, { [k in NewPKey]: NewPValue }>;
    343        }
    344      }
    345    });
    346  }
    347 
    348  /** @inheritDoc */
    349  combineWithParams<NewP extends {}>(
    350    newParams: Iterable<NewP>
    351  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
    352    assertNotGenerator(newParams);
    353    return this.expandWithParams(() => newParams);
    354  }
    355 
    356  /** @inheritDoc */
    357  combine<NewPKey extends string, NewPValue>(
    358    key: NewPKey,
    359    values: Iterable<NewPValue>
    360  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
    361    assertNotGenerator(values);
    362    return this.expand(key, () => values);
    363  }
    364 
    365  /** @inheritDoc */
    366  filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
    367    const baseGenerator = this.subcases;
    368    return new SubcaseParamsBuilder(this.cases, function* (base) {
    369      for (const a of baseGenerator(base)) {
    370        if (pred(mergeParams(base, a))) yield a;
    371      }
    372    });
    373  }
    374 
    375  /** @inheritDoc */
    376  unless(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
    377    return this.filter(x => !pred(x));
    378  }
    379 }
    380 
    381 /** Assert an object is not a Generator (a thing returned from a generator function). */
    382 function assertNotGenerator(x: object) {
    383  if ('constructor' in x) {
    384    assert(
    385      x.constructor !== (function* () {})().constructor,
    386      'Argument must not be a generator, as generators are not reusable'
    387    );
    388  }
    389 }