@uniflowed/test 0.0.0-alpha.9 → 0.2.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.
package/worker.js CHANGED
@@ -49,14 +49,29 @@
49
49
 
50
50
  import * as output from "./internal/output.js";
51
51
  import { AsyncLocalStorage } from "node:async_hooks";
52
+ import process from "node:process";
52
53
  import { writeChangedSnapshots } from "./internal/snapshot.js";
53
54
  import { createInterface } from "node:readline";
54
55
  import { fileURLToPath, pathToFileURL } from "node:url";
55
56
 
56
- import { reset } from "./internal/registry.js";
57
- import { resetModuleState } from "./internal/modules.js";
57
+ import { installInSourceTests } from "./in-source.js";
58
+ import { restoreSharedState } from "./internal/isolation.js";
58
59
  import { run } from "./internal/run.js";
59
60
 
61
+ // `process` is imported rather than read off the global because this file is
62
+ // the process entry point, and an entry point that names what it depends on is
63
+ // one a reader does not have to trust.
64
+ //
65
+ // It used to be put *on* the global as well. Deno 1.x exposed the object as
66
+ // `node:process` and nothing as `globalThis.process`, and every module this
67
+ // worker reaches that wants the process reads the global: `internal/output.js`
68
+ // replaces `process.stdout.write`, `internal/namespace.js` reads
69
+ // `process.env` for `stubEnv`, and `internal/axe.js` reads `UF_AXE`. The Deno
70
+ // uf starts is 2.8 or newer — `@uniflowed/host/deno-preload` is built on that
71
+ // release's `registerHooks`, and `uf test` refuses anything older — and Deno 2
72
+ // has the global like Node and Bun. A shim for a host uf no longer starts would
73
+ // be a line claiming to be load-bearing while holding nothing up.
74
+
60
75
  /** What `uf` sends for one file. */
61
76
  type Request = {|
62
77
  readonly file: string,
@@ -137,18 +152,80 @@ function write(event: { readonly [string]: mixed }): void {
137
152
  * the `serving` store this runs inside — it is what every event written from
138
153
  * this file, or from anything this file leaves behind, is stamped with.
139
154
  */
155
+ /**
156
+ * Whether this is a run of the benchmarks, `uf test --bench`.
157
+ *
158
+ * Read from the environment, as snapshot updates are: it is a property of the
159
+ * run, set once on the worker by `uf`, and not something each request says.
160
+ */
161
+ function benching(): boolean {
162
+ const value = (globalThis: $FlowFixMe).process?.env?.UF_TEST_BENCH;
163
+ return value != null && value !== "" && value !== "0";
164
+ }
165
+
140
166
  async function runFile(request: Request, generation: number): Promise<void> {
141
167
  const started = performance.now();
142
- reset();
143
- // Every module this file stood in for goes back, before the next file can
144
- // import one of them and be handed the previous file's stand-in. A worker
145
- // serves many files out of one module registry, so this is the difference
146
- // between "one file at a time" and "one file's mocks at a time".
147
- resetModuleState();
168
+ // Everything the previous file changed and this package shares with it goes
169
+ // back: the registry, the stubbed environment and globals, the clock, the
170
+ // module stand-ins, and the document. What each of those is and why it is on
171
+ // the list is `internal/isolation.js`, which is one place rather than five
172
+ // calls here — a worker serves many files out of one process, `uf test` fans
173
+ // files across workers by size, and a leak therefore makes the *result* of a
174
+ // suite a function of the timings file rather than of the code under test.
175
+ // See ubugeeei-prod/uf#417, #581 and #607, which are that sentence three
176
+ // times over.
177
+ //
178
+ // Before the import rather than after the run, so a file that throws while
179
+ // loading still hands the next one a clean process.
180
+ restoreSharedState();
181
+ // Not a restore, and so not on that list: this is the output budget for the
182
+ // file about to run, rather than something the previous file left behind.
148
183
  output.startFile();
149
184
 
185
+ // In-source blocks in *this* file get uf's test API; the same blocks in
186
+ // every module this file imports get `undefined` and do not register. See
187
+ // `./in-source.js` for why the marker is a call rather than a constant.
188
+ const url = pathToFileURL(request.file).href;
189
+ const uninstallInSourceTests = installInSourceTests(url);
190
+ try {
191
+ await runImportedFile(request, generation, url, started);
192
+ } finally {
193
+ // For the whole file rather than only for its import. A block reads the
194
+ // marker at module scope, but a *case body* may read it too — `const
195
+ // { uft } = import.meta.uf.test` inside an `it` is an ordinary thing to
196
+ // write — and a marker that answered during the import and not during the
197
+ // run would be a value that changed under the file that read it.
198
+ //
199
+ // Taking it away earlier would not buy the isolation it looks like it
200
+ // buys: work a finished file left behind can register through a binding it
201
+ // captured just as easily as through this global, so the thing that keeps
202
+ // a straggler out of the next file is the registry reset at the top of
203
+ // this function and the generation stamped on every event, not the
204
+ // lifetime of one accessor.
205
+ uninstallInSourceTests();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * The half of [`runFile`] that has a file to run.
211
+ *
212
+ * Split out so the caller's `finally` covers the import *and* the run without
213
+ * either of the two `return`s below escaping it.
214
+ */
215
+ async function runImportedFile(
216
+ request: Request,
217
+ generation: number,
218
+ url: string,
219
+ started: number,
220
+ ): Promise<void> {
221
+ let closeNative: (() => Promise<void>) | null = null;
150
222
  try {
151
- await import(`${pathToFileURL(request.file).href}?uf-run=${generation}`);
223
+ if (process.env.UF_TEST_TARGET === "react-native") {
224
+ const { loadNativeFile } = await import("./internal/native-host.js");
225
+ closeNative = await loadNativeFile(request.file);
226
+ } else {
227
+ await import(`${url}?uf-run=${generation}`);
228
+ }
152
229
  } catch (thrown) {
153
230
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
154
231
  write({
@@ -164,7 +241,12 @@ async function runFile(request: Request, generation: number): Promise<void> {
164
241
  try {
165
242
  const absolute = fileURLToPath(pathToFileURL(request.file).href);
166
243
  await run(
167
- { filter: request.filter ?? null, timeoutMs: request.timeoutMs, file: absolute },
244
+ {
245
+ filter: request.filter ?? null,
246
+ timeoutMs: request.timeoutMs,
247
+ file: absolute,
248
+ bench: benching(),
249
+ },
168
250
  (result) => {
169
251
  write({
170
252
  event: "test",
@@ -180,12 +262,18 @@ async function runFile(request: Request, generation: number): Promise<void> {
180
262
  // with forty snapshots would otherwise rewrite its snapshot file forty
181
263
  // times, and a crash halfway through would leave a partial one.
182
264
  writeChangedSnapshots();
265
+ await closeNative?.();
183
266
  write({
184
267
  event: "file",
185
268
  status: "completed",
186
269
  durationMicros: Math.round((performance.now() - started) * 1000),
187
270
  });
188
271
  } catch (thrown) {
272
+ try {
273
+ await closeNative?.();
274
+ } catch {
275
+ // Preserve the original run or teardown failure.
276
+ }
189
277
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
190
278
  write({
191
279
  event: "file",
@@ -267,4 +355,21 @@ process.on("unhandledRejection", (reason: mixed) => {
267
355
  process.exit(1);
268
356
  });
269
357
 
358
+ // The same for an exception nothing caught — a server's `error` event with no
359
+ // listener, a throw from a timer callback. Node's default prints the stack to
360
+ // stderr and exits, and all `uf` could then say was that the worker had died:
361
+ // the file that did it went unnamed, and the message was somewhere above the
362
+ // report, unattributed. Reported the way a rejection is, it is the failure of
363
+ // the file whose work threw, with its message and stack in the file's result.
364
+ process.on("uncaughtException", (thrown: mixed) => {
365
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
366
+ write({
367
+ event: "file",
368
+ status: "run-failed",
369
+ message: `uncaught exception: ${error.message}`,
370
+ stack: error.stack ?? null,
371
+ });
372
+ process.exit(1);
373
+ });
374
+
270
375
  serve();