summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common/framework/params_builder.ts
blob: d22444a9b66df48705b9dbf1045917206d7b95b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import { Merged, mergeParams } from '../internal/params_utils.js';
import { stringifyPublicParams } from '../internal/query/stringify_params.js';
import { assert, mapLazy } from '../util/util.js';

// ================================================================
// "Public" ParamsBuilder API / Documentation
// ================================================================

/**
 * Provides doc comments for the methods of CaseParamsBuilder and SubcaseParamsBuilder.
 * (Also enforces rough interface match between them.)
 */
export interface ParamsBuilder {
  /**
   * Expands each item in `this` into zero or more items.
   * Each item has its parameters expanded with those returned by the `expander`.
   *
   * **Note:** When only a single key is being added, use the simpler `expand` for readability.
   *
   * ```text
   *               this = [     a       ,      b     ,       c       ]
   * this.map(expander) = [   f(a)           f(b)          f(c)      ]
   *                    = [[a1, a2, a3] ,    [ b1 ]  ,       []      ]
   *  merge and flatten = [ merge(a, a1), merge(a, a2), merge(a, a3), merge(b, b1) ]
   * ```
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  expandWithParams(expander: (_: any) => any): any;

  /**
   * Expands each item in `this` into zero or more items. Each item has its parameters expanded
   * with one new key, `key`, and the values returned by `expander`.
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  expand(key: string, expander: (_: any) => any): any;

  /**
   * Expands each item in `this` to multiple items, one for each item in `newParams`.
   *
   * In other words, takes the cartesian product of [ the items in `this` ] and `newParams`.
   *
   * **Note:** When only a single key is being added, use the simpler `combine` for readability.
   *
   * ```text
   *                     this = [ {a:1}, {b:2} ]
   *                newParams = [ {x:1}, {y:2} ]
   * this.combineP(newParams) = [ {a:1,x:1}, {a:1,y:2}, {b:2,x:1}, {b:2,y:2} ]
   * ```
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  combineWithParams(newParams: Iterable<any>): any;

  /**
   * Expands each item in `this` to multiple items with `{ [name]: value }` for each value.
   *
   * In other words, takes the cartesian product of [ the items in `this` ]
   * and `[ {[name]: value} for each value in values ]`
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  combine(key: string, newParams: Iterable<any>): any;

  /**
   * Filters `this` to only items for which `pred` returns true.
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  filter(pred: (_: any) => boolean): any;

  /**
   * Filters `this` to only items for which `pred` returns false.
   */
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  unless(pred: (_: any) => boolean): any;
}

/**
 * Determines the resulting parameter object type which would be generated by an object of
 * the given ParamsBuilder type.
 */
export type ParamTypeOf<
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  T extends ParamsBuilder
> = T extends SubcaseParamsBuilder<infer CaseP, infer SubcaseP>
  ? Merged<CaseP, SubcaseP>
  : T extends CaseParamsBuilder<infer CaseP>
  ? CaseP
  : never;

// ================================================================
// Implementation
// ================================================================

/**
 * Iterable over pairs of either:
 * - `[case params, Iterable<subcase params>]` if there are subcases.
 * - `[case params, undefined]` if not.
 */
export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
  readonly [CaseP, Iterable<SubcaseP> | undefined]
>;

/**
 * Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
 */
export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
  protected readonly cases: () => Generator<CaseP>;

  constructor(cases: () => Generator<CaseP>) {
    this.cases = cases;
  }

  /**
   * Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
   */
  protected abstract iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP>;
}

/**
 * Calls the (normally hidden) `iterateCasesWithSubcases()` method.
 */
export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {}>) {
  interface IterableParamsBuilder {
    iterateCasesWithSubcases(): CaseSubcaseIterable<{}, {}>;
  }

  return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases();
}

/**
 * Builder for combinatorial test **case** parameters.
 *
 * CaseParamsBuilder is immutable. Each method call returns a new, immutable object,
 * modifying the list of cases according to the method called.
 *
 * This means, for example, that the `unit` passed into `TestBuilder.params()` can be reused.
 */
export class CaseParamsBuilder<CaseP extends {}>
  extends ParamsBuilderBase<CaseP, {}>
  implements Iterable<CaseP>, ParamsBuilder {
  *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, {}> {
    for (const a of this.cases()) {
      yield [a, undefined];
    }
  }

  [Symbol.iterator](): Iterator<CaseP> {
    return this.cases();
  }

  /** @inheritDoc */
  expandWithParams<NewP extends {}>(
    expander: (_: Merged<{}, CaseP>) => Iterable<NewP>
  ): CaseParamsBuilder<Merged<CaseP, NewP>> {
    const newGenerator = expanderGenerator(this.cases, expander);
    return new CaseParamsBuilder(() => newGenerator({}));
  }

  /** @inheritDoc */
  expand<NewPKey extends string, NewPValue>(
    key: NewPKey,
    expander: (_: Merged<{}, CaseP>) => Iterable<NewPValue>
  ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
    return this.expandWithParams(function* (p) {
      for (const value of expander(p)) {
        yield { [key]: value } as { readonly [name in NewPKey]: NewPValue };
      }
    });
  }

  /** @inheritDoc */
  combineWithParams<NewP extends {}>(
    newParams: Iterable<NewP>
  ): CaseParamsBuilder<Merged<CaseP, NewP>> {
    assertNotGenerator(newParams);
    const seenValues = new Set<string>();
    for (const params of newParams) {
      const paramsStr = stringifyPublicParams(params);
      assert(!seenValues.has(paramsStr), `Duplicate entry in combine[WithParams]: ${paramsStr}`);
      seenValues.add(paramsStr);
    }

    return this.expandWithParams(() => newParams);
  }

  /** @inheritDoc */
  combine<NewPKey extends string, NewPValue>(
    key: NewPKey,
    values: Iterable<NewPValue>
  ): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
    assertNotGenerator(values);
    const mapped = mapLazy(values, v => ({ [key]: v } as { [name in NewPKey]: NewPValue }));
    return this.combineWithParams(mapped);
  }

  /** @inheritDoc */
  filter(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
    const newGenerator = filterGenerator(this.cases, pred);
    return new CaseParamsBuilder(() => newGenerator({}));
  }

  /** @inheritDoc */
  unless(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
    return this.filter(x => !pred(x));
  }

  /**
   * "Finalize" the list of cases and begin defining subcases.
   * Returns a new SubcaseParamsBuilder. Methods called on SubcaseParamsBuilder
   * generate new subcases instead of new cases.
   */
  beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
    return new SubcaseParamsBuilder(
      () => this.cases(),
      function* () {
        yield {};
      }
    );
  }
}

/**
 * The unit CaseParamsBuilder, representing a single case with no params: `[ {} ]`.
 *
 * `punit` is passed to every `.params()`/`.paramsSubcasesOnly()` call, so `kUnitCaseParamsBuilder`
 * is only explicitly needed if constructing a ParamsBuilder outside of a test builder.
 */
export const kUnitCaseParamsBuilder = new CaseParamsBuilder(function* () {
  yield {};
});

/**
 * Builder for combinatorial test _subcase_ parameters.
 *
 * SubcaseParamsBuilder is immutable. Each method call returns a new, immutable object,
 * modifying the list of subcases according to the method called.
 */
export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
  extends ParamsBuilderBase<CaseP, SubcaseP>
  implements ParamsBuilder {
  protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;

  constructor(cases: () => Generator<CaseP>, generator: (_: CaseP) => Generator<SubcaseP>) {
    super(cases);
    this.subcases = generator;
  }

  *iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP> {
    for (const caseP of this.cases()) {
      const subcases = Array.from(this.subcases(caseP));
      if (subcases.length) {
        yield [caseP, subcases];
      }
    }
  }

  /** @inheritDoc */
  expandWithParams<NewP extends {}>(
    expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
    return new SubcaseParamsBuilder(this.cases, expanderGenerator(this.subcases, expander));
  }

  /** @inheritDoc */
  expand<NewPKey extends string, NewPValue>(
    key: NewPKey,
    expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
    return this.expandWithParams(function* (p) {
      for (const value of expander(p)) {
        // TypeScript doesn't know here that NewPKey is always a single literal string type.
        yield { [key]: value } as { [name in NewPKey]: NewPValue };
      }
    });
  }

  /** @inheritDoc */
  combineWithParams<NewP extends {}>(
    newParams: Iterable<NewP>
  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
    assertNotGenerator(newParams);
    return this.expandWithParams(() => newParams);
  }

  /** @inheritDoc */
  combine<NewPKey extends string, NewPValue>(
    key: NewPKey,
    values: Iterable<NewPValue>
  ): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
    assertNotGenerator(values);
    return this.expand(key, () => values);
  }

  /** @inheritDoc */
  filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
    return new SubcaseParamsBuilder(this.cases, filterGenerator(this.subcases, pred));
  }

  /** @inheritDoc */
  unless(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
    return this.filter(x => !pred(x));
  }
}

function expanderGenerator<Base, A, B>(
  baseGenerator: (_: Base) => Generator<A>,
  expander: (_: Merged<Base, A>) => Iterable<B>
): (_: Base) => Generator<Merged<A, B>> {
  return function* (base: Base) {
    for (const a of baseGenerator(base)) {
      for (const b of expander(mergeParams(base, a))) {
        yield mergeParams(a, b);
      }
    }
  };
}

function filterGenerator<Base, A>(
  baseGenerator: (_: Base) => Generator<A>,
  pred: (_: Merged<Base, A>) => boolean
): (_: Base) => Generator<A> {
  return function* (base: Base) {
    for (const a of baseGenerator(base)) {
      if (pred(mergeParams(base, a))) {
        yield a;
      }
    }
  };
}

/** Assert an object is not a Generator (a thing returned from a generator function). */
function assertNotGenerator(x: object) {
  if ('constructor' in x) {
    assert(
      x.constructor !== (function* () {})().constructor,
      'Argument must not be a generator, as generators are not reusable'
    );
  }
}