@uniflowed/test 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  // @flow
2
2
  import type { BrowserTransport, ControlOptions } from "./cdp.js";
3
+ import { runnerFetch } from "./runner-fetch.js";
3
4
 
4
5
  export async function createTransport(options: ControlOptions): Promise<BrowserTransport> {
5
6
  if (options.executable != null)
@@ -12,7 +13,8 @@ export async function createTransport(options: ControlOptions): Promise<BrowserT
12
13
  method: string,
13
14
  args: $ReadOnlyArray<mixed> = [],
14
15
  ): Promise<$FlowFixMe> {
15
- const response = await fetch("/uf-test/browser", {
16
+ // The platform's `fetch`, not the global a test's request mock replaced.
17
+ const response = await runnerFetch("/uf-test/browser", {
16
18
  method: "POST",
17
19
  headers: { "content-type": "application/json", "uf-test-browser": token },
18
20
  body: JSON.stringify({ id, method, args }),
@@ -37,6 +37,7 @@
37
37
  import * as output from "../output.js";
38
38
  import { run } from "../run.js";
39
39
  import { installInSourceTests } from "../../in-source.js";
40
+ import { runnerFetch } from "./runner-fetch.js";
40
41
 
41
42
  /** One file, as the server hands it over. */
42
43
  type PageRequest = {
@@ -74,7 +75,9 @@ function flush(): Promise<void> {
74
75
  if (already != null) return already;
75
76
  if (queue.length === 0) return Promise.resolve();
76
77
  const batch = queue.splice(0, queue.length);
77
- const sent = fetch("/uf-test/events", {
78
+ // Not the global: a file that installed a request mock owns that one, and
79
+ // would answer the runner's report as an unhandled request.
80
+ const sent = runnerFetch("/uf-test/events", {
78
81
  method: "POST",
79
82
  headers: { "content-type": "application/json" },
80
83
  body: JSON.stringify(batch),
@@ -195,7 +198,7 @@ function page(): $FlowFixMe {
195
198
  async function serve(): Promise<void> {
196
199
  let request: PageRequest;
197
200
  try {
198
- request = await (await fetch("/uf-test/next")).json();
201
+ request = await (await runnerFetch("/uf-test/next")).json();
199
202
  } catch {
200
203
  // The driver has gone. Nothing to report it to.
201
204
  return;
@@ -0,0 +1,38 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/test`: the `fetch` the browser runner talks to `uf`
4
+ // with.
5
+ //
6
+ // A page reports its results over HTTP — `POST /uf-test/events`, and
7
+ // `POST /uf-test/browser` for `createBrowser` — and the file under test runs
8
+ // in that same page. A file that installs a request mock replaces
9
+ // `globalThis.fetch`, and `@uniflowed/mock`'s default policy rejects every
10
+ // request no handler claimed. Reached through the global, the runner's own
11
+ // requests would be answered by the test's mock: rejected, and the rejection
12
+ // swallowed by the reporter, so the results would never reach `uf test`
13
+ // (ubugeeei-prod/uf#1394).
14
+ //
15
+ // So the harness captures the platform's `fetch` in a classic script that runs
16
+ // before any module — before the runner, and so before any test file could
17
+ // replace it — and the runner's requests go through that capture. A constant
18
+ // captured in this module would depend on when it was first evaluated and on
19
+ // `page.js` and the import map reaching it by the same URL; the harness
20
+ // script depends on neither.
21
+
22
+ /** Where `server.js`'s harness leaves the platform's `fetch`. */
23
+ export const RUNNER_FETCH: symbol = Symbol.for("uf.test.browser.fetch");
24
+
25
+ /**
26
+ * `fetch`, as it was before any test file ran.
27
+ *
28
+ * Falls back to the global when nothing was captured, which is a page not
29
+ * served by `uf test --browser` — there is no test file ahead of it to have
30
+ * replaced anything.
31
+ */
32
+ export function runnerFetch(input: RequestInfo, init?: RequestOptions): Promise<Response> {
33
+ const captured = Reflect.get(globalThis, RUNNER_FETCH);
34
+ if (typeof captured === "function") {
35
+ return captured(input, init);
36
+ }
37
+ return fetch(input, init);
38
+ }
@@ -639,6 +639,10 @@ function harnessHtml(map: string, browserToken: string): string {
639
639
  '<meta charset="utf-8">',
640
640
  "<title>uf test</title>",
641
641
  `<script>globalThis[Symbol.for("uf.test.browser.token")]=${JSON.stringify(browserToken)}</script>`,
642
+ // Before any module, so before any test file can replace it: the runner
643
+ // reports through this `fetch`, never through a mocked global. See
644
+ // `./runner-fetch.js`.
645
+ '<script>globalThis[Symbol.for("uf.test.browser.fetch")]=globalThis.fetch.bind(globalThis)</script>',
642
646
  `<script type="importmap">${map}</script>`,
643
647
  `<script type="module" src="${page}"></script>`,
644
648
  "",
@@ -252,6 +252,14 @@ function sourceMapsForCallSites(): FindSourceMap | false {
252
252
  nodeSourceMaps = false;
253
253
  return false;
254
254
  }
255
+ // A `uf test` worker whose loader maps lazily holds the maps Node would
256
+ // have, and answers for them the way `findSourceMap` would; see
257
+ // `@uniflowed/host`'s `internal/lazy-source-maps.js`.
258
+ const lazy = host[Symbol.for("@uniflowed/host/source-maps")];
259
+ if (typeof lazy?.findSourceMap === "function") {
260
+ nodeSourceMaps = (file) => lazy.findSourceMap(file);
261
+ return nodeSourceMaps;
262
+ }
255
263
  const findSourceMap = process.getBuiltinModule("node:module")?.findSourceMap;
256
264
  nodeSourceMaps =
257
265
  typeof findSourceMap === "function"
package/internal/run.js CHANGED
@@ -75,6 +75,19 @@ export type RunOptions = {|
75
75
  * round. `uf test --bench` sets it.
76
76
  */
77
77
  readonly bench?: boolean,
78
+ /**
79
+ * Run only share `index` of `count` of the file's cases, and report nothing
80
+ * about the rest.
81
+ *
82
+ * `uf test` splits a file long enough to hold up a run across several
83
+ * workers, and each is handed one share: the cases are numbered in the
84
+ * order the file registered them, and share `index` is the contiguous run
85
+ * from `index * n / count` to `(index + 1) * n / count`. Every worker
86
+ * imports the whole file and walks the same tree, so the shares between
87
+ * them report every case once. A case outside the share is not reported at
88
+ * all — not as skipped — because another worker is reporting it.
89
+ */
90
+ readonly part?: {| readonly index: number, readonly count: number |} | null,
78
91
  |};
79
92
 
80
93
  /** Default budget for one case, matching what most runners use. */
@@ -381,7 +394,7 @@ async function runSuite(
381
394
  options: RunOptions,
382
395
  onlyMode: boolean,
383
396
  emit: (result: Result) => void,
384
- state: {| bail: boolean |},
397
+ state: {| bail: boolean, readonly owned: Set<Case> | null |},
385
398
  setUpAncestors: () => Promise<void>,
386
399
  ): Promise<boolean> {
387
400
  const skipped = context.skipped || node.modifier === "skip" || node.modifier === "todo";
@@ -430,6 +443,10 @@ async function runSuite(
430
443
  break;
431
444
  }
432
445
  if (child.kind !== "suite") {
446
+ // Another worker's share: it reports this case, and sets up for it.
447
+ if (state.owned != null && !state.owned.has(child)) {
448
+ continue;
449
+ }
433
450
  const willRun =
434
451
  !inner.skipped &&
435
452
  child.modifier !== "skip" &&
@@ -490,6 +507,7 @@ async function runSuite(
490
507
  export async function run(options: RunOptions, emit: (result: Result) => void): Promise<void> {
491
508
  const root = collected();
492
509
  const onlyMode = hasOnly(root, false);
510
+ const owned = share(root, options.part);
493
511
  const context: Context = {
494
512
  path: [],
495
513
  beforeEach: [],
@@ -497,5 +515,36 @@ export async function run(options: RunOptions, emit: (result: Result) => void):
497
515
  skipped: false,
498
516
  onlyPath: !onlyMode,
499
517
  };
500
- await runSuite(root, context, options, onlyMode, emit, { bail: false }, async () => {});
518
+ await runSuite(root, context, options, onlyMode, emit, { bail: false, owned }, async () => {});
519
+ }
520
+
521
+ /**
522
+ * The cases of `part`, or `null` for a run of every case.
523
+ *
524
+ * Numbered in the order a walk of the tree meets them, which is the order the
525
+ * file registered them and the order `runSuite` runs them in — the same in
526
+ * every worker that imported the file, which is what lets each work out its
527
+ * own share without being told the others'.
528
+ */
529
+ function share(
530
+ root: Suite,
531
+ part: ?{| readonly index: number, readonly count: number |},
532
+ ): Set<Case> | null {
533
+ if (part == null || part.count < 2) {
534
+ return null;
535
+ }
536
+ const cases: Array<Case> = [];
537
+ const walk = (node: Suite) => {
538
+ for (const child of node.children) {
539
+ if (child.kind === "suite") {
540
+ walk(child);
541
+ } else {
542
+ cases.push(child);
543
+ }
544
+ }
545
+ };
546
+ walk(root);
547
+ const from = Math.floor((part.index * cases.length) / part.count);
548
+ const to = Math.floor(((part.index + 1) * cases.length) / part.count);
549
+ return new Set(cases.slice(from, to));
501
550
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/test",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "!*.test.js"
39
39
  ],
40
40
  "dependencies": {
41
- "@uniflowed/host": "0.4.0",
41
+ "@uniflowed/host": "0.6.0",
42
42
  "pixelmatch": "^7.1.0",
43
43
  "pngjs": "^7.0.0"
44
44
  },
package/worker.js CHANGED
@@ -86,6 +86,8 @@ type Request = {|
86
86
  */
87
87
  readonly invalidate?: $ReadOnlyArray<string>,
88
88
  readonly filter?: string | null,
89
+ /** Run only this share of the file's cases; see `RunOptions.part`. */
90
+ readonly part?: {| readonly index: number, readonly count: number |} | null,
89
91
  readonly timeoutMs?: number,
90
92
  /**
91
93
  * Which request this is, counting from one within this worker.
@@ -259,6 +261,7 @@ async function runImportedFile(
259
261
  await run(
260
262
  {
261
263
  filter: request.filter ?? null,
264
+ part: request.part ?? null,
262
265
  timeoutMs: request.timeoutMs,
263
266
  file: absolute,
264
267
  bench: benching(),