@uniflowed/test 0.0.0-alpha.4 → 0.0.0-alpha.40
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/browser-worker.js +339 -0
- package/bun/index.js +305 -0
- package/in-source.js +169 -0
- package/index.js +13 -1
- package/internal/axe.js +362 -0
- package/internal/browser/node.js +375 -0
- package/internal/browser/page.js +258 -0
- package/internal/browser/server.js +841 -0
- package/internal/expect.js +435 -54
- package/internal/frames.js +87 -1
- package/internal/isolation.js +231 -0
- package/internal/modules.js +430 -0
- package/internal/namespace.js +98 -50
- package/internal/output.js +105 -38
- package/internal/registry.js +54 -3
- package/internal/run.js +188 -40
- package/internal/timers.js +2 -2
- package/internal/unsupported.js +30 -0
- package/package.json +29 -4
- package/worker.js +187 -20
package/internal/output.js
CHANGED
|
@@ -21,25 +21,58 @@
|
|
|
21
21
|
// # Why this is its own module
|
|
22
22
|
//
|
|
23
23
|
// Two callers need it and neither owns it: `worker.js` installs the capture at
|
|
24
|
-
// start-up, and `run.js`
|
|
25
|
-
// "Who printed this" is also state with a lifetime of its
|
|
26
|
-
// case's
|
|
27
|
-
//
|
|
28
|
-
//
|
|
24
|
+
// start-up, and `run.js` runs each case inside the ownership kept here, so a
|
|
25
|
+
// chunk can be named. "Who printed this" is also state with a lifetime of its
|
|
26
|
+
// own — one case's hooks and body — exactly like the snapshot key in
|
|
27
|
+
// `snapshot.js`, and for the same reason it lives beside the thing it describes
|
|
28
|
+
// rather than inside either caller.
|
|
29
29
|
//
|
|
30
30
|
// # What "the test that printed it" means
|
|
31
31
|
//
|
|
32
|
-
// The
|
|
33
|
-
//
|
|
34
|
-
// `setTimeout` a test leaves behind prints while the next case is running and
|
|
35
|
-
// is filed under that one; a chunk from no case at all is filed under the
|
|
36
|
-
// file.
|
|
32
|
+
// The case whose *asynchronous context* the write happened in, which is the
|
|
33
|
+
// case whose code produced it.
|
|
37
34
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
35
|
+
// The obvious answer was a module-level variable the runner set before a case
|
|
36
|
+
// and cleared after it, and it was wrong in one shape that matters: the name a
|
|
37
|
+
// chunk carried was whatever the worker happened to be running when the chunk
|
|
38
|
+
// arrived. A `setTimeout` a test left behind fires while the *next* case is
|
|
39
|
+
// running, so the line it printed was reported under that next case — a test
|
|
40
|
+
// accused of printing something it never printed, which is worse than not
|
|
41
|
+
// naming it at all, because a reader chasing the message finds it under code
|
|
42
|
+
// that does not contain it. See ubugeeei-prod/uf#207.
|
|
43
|
+
//
|
|
44
|
+
// So the owner is an `AsyncLocalStorage`, and what `run.js` calls is
|
|
45
|
+
// `runInTest` rather than an `enterTest` / `exitTest` pair: the store is only
|
|
46
|
+
// carried by work started *inside* the case, so the case's setup, body and
|
|
47
|
+
// teardown have to run within it. Everything they schedule inherits it,
|
|
48
|
+
// whenever it eventually runs.
|
|
49
|
+
//
|
|
50
|
+
// # When there is no owner
|
|
51
|
+
//
|
|
52
|
+
// `getStore()` answers nothing outside a case, and a chunk with no owner is
|
|
53
|
+
// filed under the file — which is right for an import, a `beforeAll`, or a
|
|
54
|
+
// straggler from a case that is long gone.
|
|
55
|
+
//
|
|
56
|
+
// It is also the answer on a host whose storage does not reach the callback.
|
|
57
|
+
// Deno 1.31 has `AsyncLocalStorage` and propagates it across `await`, but not
|
|
58
|
+
// through `setTimeout`, so a detached callback there is filed under the file
|
|
59
|
+
// rather than under the case that scheduled it. That degradation is the point:
|
|
60
|
+
// of the two ways to be less than exact, naming the file says less, and naming
|
|
61
|
+
// the next case says something false.
|
|
62
|
+
//
|
|
63
|
+
// `node:async_hooks` itself is not guarded for, because a guard could not run.
|
|
64
|
+
// Node, Deno and Bun all provide it under the `node:` specifier, and a host
|
|
65
|
+
// that had no `node:` builtins could not start this worker at all — `node:util`
|
|
66
|
+
// is imported below, `node:readline` and `node:url` by `worker.js`. A `typeof`
|
|
67
|
+
// check around the constructor would only ever execute on a host where this
|
|
68
|
+
// module had already linked.
|
|
69
|
+
//
|
|
70
|
+
// The one thing this does not answer is a straggler that outlives its *file*:
|
|
71
|
+
// the worker runs the next file in the same process, and a chunk still carrying
|
|
72
|
+
// a name from the file before is a name the next file's report has no test for.
|
|
73
|
+
// The host files it under the file it arrived in, which is honest but not
|
|
74
|
+
// exact, and closing it properly needs the file's generation in the protocol.
|
|
75
|
+
// That is ubugeeei-prod/uf#203, and it is not this module's to fix.
|
|
43
76
|
|
|
44
77
|
// # Bounds
|
|
45
78
|
//
|
|
@@ -49,6 +82,7 @@
|
|
|
49
82
|
// nothing after it is kept. The budget starts over for each file, so a chatty
|
|
50
83
|
// file does not silence the next one in the same worker.
|
|
51
84
|
|
|
85
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
52
86
|
import { format, inspect } from "node:util";
|
|
53
87
|
|
|
54
88
|
import { userFrames } from "./frames.js";
|
|
@@ -95,9 +129,16 @@ const DECODER = new TextDecoder();
|
|
|
95
129
|
/** What a stream's `write` calls when it has taken the chunk. */
|
|
96
130
|
type WriteCallback = () => mixed;
|
|
97
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The case a write belongs to, kept in the asynchronous context it ran in.
|
|
134
|
+
*
|
|
135
|
+
* Full names rather than a record, because that is the whole of what a chunk
|
|
136
|
+
* needs to say and the protocol carries it as a string either way.
|
|
137
|
+
*/
|
|
138
|
+
const owner: AsyncLocalStorage<string> = new AsyncLocalStorage();
|
|
139
|
+
|
|
98
140
|
let sink: OutputSink | null = null;
|
|
99
141
|
let raw: ((chunk: string) => void) | null = null;
|
|
100
|
-
let current: string | null = null;
|
|
101
142
|
let captured = 0;
|
|
102
143
|
let stopped = false;
|
|
103
144
|
|
|
@@ -159,7 +200,7 @@ function capture(stream: OutputStream, text: string): void {
|
|
|
159
200
|
stopped = true;
|
|
160
201
|
}
|
|
161
202
|
captured += kept.length;
|
|
162
|
-
to({ stream, test:
|
|
203
|
+
to({ stream, test: owner.getStore() ?? null, text: kept });
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
/** A stand-in for `process.stdout.write` / `process.stderr.write`. */
|
|
@@ -200,6 +241,19 @@ function writer(
|
|
|
200
241
|
* Installed once, for the life of the worker: a worker runs many files, and
|
|
201
242
|
* restoring the real methods between them would leave a window in which a
|
|
202
243
|
* straggling `setTimeout` from the previous file writes into the protocol.
|
|
244
|
+
*
|
|
245
|
+
* # A page has no stream to take
|
|
246
|
+
*
|
|
247
|
+
* `uf test --browser` runs this same capture inside a page
|
|
248
|
+
* (`./browser/page.js`), and there the whole premise of the returned value is
|
|
249
|
+
* absent: there is no `process.stdout`, so there is nothing for a test to
|
|
250
|
+
* write into by accident, and the protocol's channel is a separate HTTP
|
|
251
|
+
* request rather than a stream anything else can reach. So the two `write`
|
|
252
|
+
* methods are only replaced when there are two `write` methods, and the
|
|
253
|
+
* "raw stream" handed back is a no-op nobody has a use for.
|
|
254
|
+
*
|
|
255
|
+
* Deliberately not a `typeof process` check at each use: the question is asked
|
|
256
|
+
* once, here, because the answer cannot change under a running host.
|
|
203
257
|
*/
|
|
204
258
|
export function install(to: OutputSink): (chunk: string) => void {
|
|
205
259
|
const global = host();
|
|
@@ -207,11 +261,14 @@ export function install(to: OutputSink): (chunk: string) => void {
|
|
|
207
261
|
if (already != null) {
|
|
208
262
|
return already;
|
|
209
263
|
}
|
|
210
|
-
const stdout = global.process
|
|
211
|
-
const real = stdout
|
|
212
|
-
const protocol =
|
|
213
|
-
real
|
|
214
|
-
|
|
264
|
+
const stdout = global.process?.stdout;
|
|
265
|
+
const real = stdout?.write;
|
|
266
|
+
const protocol =
|
|
267
|
+
real == null
|
|
268
|
+
? (_chunk: string) => {}
|
|
269
|
+
: (chunk: string) => {
|
|
270
|
+
real.call(stdout, chunk);
|
|
271
|
+
};
|
|
215
272
|
raw = protocol;
|
|
216
273
|
sink = to;
|
|
217
274
|
for (const method of Object.keys(CONSOLE_STREAMS)) {
|
|
@@ -230,31 +287,41 @@ export function install(to: OutputSink): (chunk: string) => void {
|
|
|
230
287
|
capture("stderr", `${userFrames(error.stack) ?? `Trace: ${error.message}`}\n`);
|
|
231
288
|
};
|
|
232
289
|
|
|
233
|
-
global.process
|
|
234
|
-
|
|
290
|
+
if (global.process?.stdout != null) {
|
|
291
|
+
global.process.stdout.write = writer("stdout");
|
|
292
|
+
global.process.stderr.write = writer("stderr");
|
|
293
|
+
}
|
|
235
294
|
return protocol;
|
|
236
295
|
}
|
|
237
296
|
|
|
238
297
|
/**
|
|
239
|
-
*
|
|
298
|
+
* Run `body` as `name`, so what it prints — and what it leaves behind to print
|
|
299
|
+
* later — is filed under that case.
|
|
300
|
+
*
|
|
301
|
+
* The runner wraps one case's `beforeEach`, body and `afterEach` in a single
|
|
302
|
+
* call, because those are the one case's work. Whatever `body` returns is
|
|
303
|
+
* returned unchanged, so an `await` on this is an `await` on the case.
|
|
240
304
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
305
|
+
* Nothing here needs an "and now nothing is running" counterpart. Output from
|
|
306
|
+
* an import, a `beforeAll` or a case that has already been reported was never
|
|
307
|
+
* inside this call, so it has no owner and is the file's — which is the
|
|
308
|
+
* property the previous module-level variable had to be reset to keep, and
|
|
309
|
+
* kept only for as long as nothing straggled.
|
|
245
310
|
*/
|
|
246
|
-
export function
|
|
247
|
-
|
|
311
|
+
export function runInTest<T>(name: string, body: () => T): T {
|
|
312
|
+
return owner.run(name, body);
|
|
248
313
|
}
|
|
249
314
|
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Start one file's output budget over.
|
|
317
|
+
*
|
|
318
|
+
* Only the budget: there is no current case to clear, because a case's
|
|
319
|
+
* ownership lives in the callbacks it started rather than in this module. A
|
|
320
|
+
* straggler from the file before still carries the name it was written under,
|
|
321
|
+
* which the host cannot match to a test of the new file and files under that
|
|
322
|
+
* file instead. See ubugeeei-prod/uf#203.
|
|
323
|
+
*/
|
|
256
324
|
export function startFile(): void {
|
|
257
325
|
captured = 0;
|
|
258
326
|
stopped = false;
|
|
259
|
-
current = null;
|
|
260
327
|
}
|
package/internal/registry.js
CHANGED
|
@@ -30,13 +30,29 @@ export type Body = () => mixed | Promise<mixed>;
|
|
|
30
30
|
/** The suffix written on a registration call. */
|
|
31
31
|
export type Modifier = "none" | "only" | "skip" | "todo";
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* How a benchmark runs under `uf test --bench`.
|
|
35
|
+
*
|
|
36
|
+
* `warmup` calls are made and their times thrown away, then `iterations` calls
|
|
37
|
+
* are each timed. `timeout` is the budget for one call, in milliseconds, and
|
|
38
|
+
* the benchmark as a whole is held to that budget once per call.
|
|
39
|
+
*/
|
|
40
|
+
export type BenchOptions = {|
|
|
41
|
+
readonly warmup?: number,
|
|
42
|
+
readonly iterations?: number,
|
|
43
|
+
readonly timeout?: number,
|
|
44
|
+
|};
|
|
45
|
+
|
|
46
|
+
/** One registered test case, or one benchmark. */
|
|
34
47
|
export type Case = {|
|
|
35
|
-
readonly kind: "test",
|
|
48
|
+
readonly kind: "test" | "bench",
|
|
36
49
|
readonly name: string,
|
|
37
50
|
readonly body: Body | null,
|
|
38
51
|
readonly modifier: Modifier,
|
|
52
|
+
readonly skipReason: string | null,
|
|
39
53
|
readonly timeoutMs: number | null,
|
|
54
|
+
/** How to run it, for a benchmark; `null` for a test. */
|
|
55
|
+
readonly bench: BenchOptions | null,
|
|
40
56
|
readonly line: number,
|
|
41
57
|
readonly column: number,
|
|
42
58
|
|};
|
|
@@ -123,14 +139,18 @@ function addCase(
|
|
|
123
139
|
body: Body | null,
|
|
124
140
|
modifier: Modifier,
|
|
125
141
|
timeoutMs: number | null,
|
|
142
|
+
skipReason: string | null = null,
|
|
143
|
+
bench: BenchOptions | null = null,
|
|
126
144
|
): void {
|
|
127
145
|
const position = callSite();
|
|
128
146
|
current.children.push({
|
|
129
|
-
kind: "test",
|
|
147
|
+
kind: bench == null ? "test" : "bench",
|
|
130
148
|
name,
|
|
131
149
|
body,
|
|
132
150
|
modifier,
|
|
151
|
+
skipReason,
|
|
133
152
|
timeoutMs,
|
|
153
|
+
bench,
|
|
134
154
|
line: position.line,
|
|
135
155
|
column: position.column,
|
|
136
156
|
});
|
|
@@ -179,6 +199,9 @@ function caseApi(): $FlowFixMe {
|
|
|
179
199
|
api.skip = (name: string, body?: Body) => {
|
|
180
200
|
addCase(name, body ?? null, "skip", null);
|
|
181
201
|
};
|
|
202
|
+
api.skipBecause = (name: string, reason: string, body?: Body) => {
|
|
203
|
+
addCase(name, body ?? null, "skip", null, reason);
|
|
204
|
+
};
|
|
182
205
|
api.todo = (name: string, body?: Body) => {
|
|
183
206
|
addCase(name, body ?? null, "todo", null);
|
|
184
207
|
};
|
|
@@ -211,6 +234,34 @@ export const it: $FlowFixMe = caseApi();
|
|
|
211
234
|
/** `test` is `it`, for people who write it that way. */
|
|
212
235
|
export const test: $FlowFixMe = it;
|
|
213
236
|
|
|
237
|
+
/** The `bench` API, and its modifiers. See [`suiteApi`] for the shape. */
|
|
238
|
+
function benchApi(): $FlowFixMe {
|
|
239
|
+
const api: $FlowFixMe = (name: string, body: Body, options?: BenchOptions) => {
|
|
240
|
+
addCase(name, body, "none", options?.timeout ?? null, null, options ?? {});
|
|
241
|
+
};
|
|
242
|
+
api.only = (name: string, body: Body, options?: BenchOptions) => {
|
|
243
|
+
addCase(name, body, "only", options?.timeout ?? null, null, options ?? {});
|
|
244
|
+
};
|
|
245
|
+
api.skip = (name: string, body?: Body) => {
|
|
246
|
+
addCase(name, body ?? null, "skip", null, null, {});
|
|
247
|
+
};
|
|
248
|
+
api.todo = (name: string) => {
|
|
249
|
+
addCase(name, null, "todo", null, null, {});
|
|
250
|
+
};
|
|
251
|
+
return api;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Register one benchmark.
|
|
256
|
+
*
|
|
257
|
+
* `uf test` reports a benchmark as skipped, so a suite does not pay for timing
|
|
258
|
+
* one, and `uf test --bench` runs the benchmarks in place of the tests and
|
|
259
|
+
* reports how long each call took. `bench.only`, `bench.skip` and `bench.todo`
|
|
260
|
+
* do what `it`'s do. See [`BenchOptions`] for `warmup`, `iterations` and
|
|
261
|
+
* `timeout`.
|
|
262
|
+
*/
|
|
263
|
+
export const bench: $FlowFixMe = benchApi();
|
|
264
|
+
|
|
214
265
|
/**
|
|
215
266
|
* Substitute a row into a name, the way every runner spells it: `%s` for the
|
|
216
267
|
* value, `%j` for its JSON.
|
package/internal/run.js
CHANGED
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
import * as output from "./output.js";
|
|
13
13
|
import * as snapshot from "./snapshot.js";
|
|
14
14
|
import { AssertionError } from "./expect.js";
|
|
15
|
-
import { firstUserSite, userFrames } from "./frames.js";
|
|
16
|
-
import { type Body, type Case, type Suite, collected } from "./registry.js";
|
|
15
|
+
import { type Site, firstUserSite, siteInFile, userFrames } from "./frames.js";
|
|
16
|
+
import { type BenchOptions, type Body, type Case, type Suite, collected } from "./registry.js";
|
|
17
17
|
|
|
18
18
|
/** How one case ended. */
|
|
19
19
|
export type Outcome =
|
|
20
|
-
| {|
|
|
20
|
+
| {|
|
|
21
|
+
readonly status: "passed",
|
|
22
|
+
/**
|
|
23
|
+
* One timing per measured call, in whole microseconds, for a benchmark
|
|
24
|
+
* run under `uf test --bench`.
|
|
25
|
+
*/
|
|
26
|
+
readonly samples?: $ReadOnlyArray<number>,
|
|
27
|
+
|}
|
|
21
28
|
| {|
|
|
22
29
|
readonly status: "failed",
|
|
23
30
|
readonly message: string,
|
|
@@ -27,7 +34,11 @@ export type Outcome =
|
|
|
27
34
|
/** Where the failing assertion was written, when the stack says. */
|
|
28
35
|
readonly site: {| readonly line: number, readonly column: number |} | null,
|
|
29
36
|
|}
|
|
30
|
-
| {|
|
|
37
|
+
| {|
|
|
38
|
+
readonly status: "skipped",
|
|
39
|
+
readonly reason: "explicit" | "not-only" | "filtered" | "bench" | "not-bench",
|
|
40
|
+
readonly message?: string | null,
|
|
41
|
+
|}
|
|
31
42
|
| {| readonly status: "todo" |};
|
|
32
43
|
|
|
33
44
|
/** One finished case, as the runner reports it. */
|
|
@@ -52,11 +63,31 @@ export type RunOptions = {|
|
|
|
52
63
|
* which file that is — a test's name alone does not locate it.
|
|
53
64
|
*/
|
|
54
65
|
readonly file?: string,
|
|
66
|
+
/**
|
|
67
|
+
* Run the benchmarks and report the tests skipped, rather than the other way
|
|
68
|
+
* round. `uf test --bench` sets it.
|
|
69
|
+
*/
|
|
70
|
+
readonly bench?: boolean,
|
|
55
71
|
|};
|
|
56
72
|
|
|
57
73
|
/** Default budget for one case, matching what most runners use. */
|
|
58
74
|
export const DEFAULT_TIMEOUT_MS: number = 5000;
|
|
59
75
|
|
|
76
|
+
/** Calls a benchmark makes and throws away before it times any, unless it says. */
|
|
77
|
+
export const DEFAULT_BENCH_WARMUP: number = 5;
|
|
78
|
+
|
|
79
|
+
/** Timed calls a benchmark makes, unless it says. */
|
|
80
|
+
export const DEFAULT_BENCH_ITERATIONS: number = 50;
|
|
81
|
+
|
|
82
|
+
/** Most timed calls one benchmark may ask for, and most samples `uf` keeps. */
|
|
83
|
+
export const MAX_BENCH_ITERATIONS: number = 100000;
|
|
84
|
+
|
|
85
|
+
/** Most untimed calls one benchmark may ask for first. */
|
|
86
|
+
export const MAX_BENCH_WARMUP: number = 10000;
|
|
87
|
+
|
|
88
|
+
/** The longest delay `setTimeout` honours; past it, the timer fires at once. */
|
|
89
|
+
const MAX_TIMER_MS = 2147483647;
|
|
90
|
+
|
|
60
91
|
/** The separator between a suite's name and its child's. */
|
|
61
92
|
export const NAME_SEPARATOR: string = " > ";
|
|
62
93
|
|
|
@@ -72,7 +103,9 @@ function fullName(path: $ReadOnlyArray<string>): string {
|
|
|
72
103
|
*/
|
|
73
104
|
function hasOnly(node: Suite | Case, inherited: boolean): boolean {
|
|
74
105
|
const marked = inherited || node.modifier === "only";
|
|
75
|
-
|
|
106
|
+
// A benchmark is a case like a test: `bench.only` restricts a file as
|
|
107
|
+
// `it.only` does.
|
|
108
|
+
if (node.kind !== "suite") {
|
|
76
109
|
return marked;
|
|
77
110
|
}
|
|
78
111
|
return node.children.some((child) => hasOnly(child, marked));
|
|
@@ -102,7 +135,22 @@ async function withTimeout(body: Body, timeoutMs: number): Promise<void> {
|
|
|
102
135
|
}
|
|
103
136
|
}
|
|
104
137
|
|
|
105
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Where to say a failure happened.
|
|
140
|
+
*
|
|
141
|
+
* The reported position is printed under the path of the file being run, so
|
|
142
|
+
* when that file is known the line has to come from it: `firstUserSite` will
|
|
143
|
+
* hand back the first frame of whatever library raised, and a library's line
|
|
144
|
+
* number wearing the test file's path sends the reader to the wrong place
|
|
145
|
+
* (ubugeeei-prod/uf#319). The fallback is for a caller that did not say which
|
|
146
|
+
* file it is running — `run` is driven directly by this repository's own
|
|
147
|
+
* tests as well as by the worker — and is what every failure used before.
|
|
148
|
+
*/
|
|
149
|
+
function siteOf(stack: string | null, file: string | null): Site | null {
|
|
150
|
+
return file == null || file === "" ? firstUserSite(stack, false) : siteInFile(stack, file);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function failure(thrown: mixed, file: string | null): Outcome {
|
|
106
154
|
if (thrown instanceof AssertionError) {
|
|
107
155
|
const stack = userFrames(thrown.stack);
|
|
108
156
|
return {
|
|
@@ -111,7 +159,7 @@ function failure(thrown: mixed): Outcome {
|
|
|
111
159
|
stack,
|
|
112
160
|
expected: thrown.expected,
|
|
113
161
|
received: thrown.received,
|
|
114
|
-
site:
|
|
162
|
+
site: siteOf(stack, file),
|
|
115
163
|
};
|
|
116
164
|
}
|
|
117
165
|
if (thrown instanceof Error) {
|
|
@@ -122,7 +170,7 @@ function failure(thrown: mixed): Outcome {
|
|
|
122
170
|
stack,
|
|
123
171
|
expected: null,
|
|
124
172
|
received: null,
|
|
125
|
-
site:
|
|
173
|
+
site: siteOf(stack, file),
|
|
126
174
|
};
|
|
127
175
|
}
|
|
128
176
|
return {
|
|
@@ -173,12 +221,17 @@ async function runCase(
|
|
|
173
221
|
});
|
|
174
222
|
};
|
|
175
223
|
|
|
176
|
-
if (test.modifier === "todo"
|
|
224
|
+
if (test.modifier === "todo") {
|
|
177
225
|
report({ status: "todo" });
|
|
178
226
|
return true;
|
|
179
227
|
}
|
|
180
228
|
if (context.skipped || test.modifier === "skip") {
|
|
181
|
-
report({ status: "skipped", reason: "explicit" });
|
|
229
|
+
report({ status: "skipped", reason: "explicit", message: test.skipReason });
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const body = test.body;
|
|
233
|
+
if (body == null) {
|
|
234
|
+
report({ status: "todo" });
|
|
182
235
|
return true;
|
|
183
236
|
}
|
|
184
237
|
if (!context.onlyPath) {
|
|
@@ -190,42 +243,126 @@ async function runCase(
|
|
|
190
243
|
report({ status: "skipped", reason: "filtered" });
|
|
191
244
|
return true;
|
|
192
245
|
}
|
|
246
|
+
// A run is the tests or the benchmarks, never both. A benchmark timed inside
|
|
247
|
+
// an ordinary run would slow every suite that has one, beside workers busy
|
|
248
|
+
// with other files; a test inside a run of benchmarks would be timed with
|
|
249
|
+
// them.
|
|
250
|
+
const benchmark = test.kind === "bench";
|
|
251
|
+
if (benchmark !== (options.bench === true)) {
|
|
252
|
+
report({ status: "skipped", reason: benchmark ? "bench" : "not-bench" });
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
193
255
|
|
|
194
256
|
const timeoutMs = test.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
195
257
|
let outcome: Outcome = { status: "passed" };
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
outcome = failure(thrown);
|
|
208
|
-
}
|
|
209
|
-
// Teardown runs whatever happened above, and only reports its own failure
|
|
210
|
-
// when the body had not already failed.
|
|
211
|
-
for (const hook of context.afterEach) {
|
|
258
|
+
let samples: $ReadOnlyArray<number> | null = null;
|
|
259
|
+
// Setup, body and teardown run *inside* the case's output context, and that
|
|
260
|
+
// nesting is the whole of the fix for #207. What names a printed line is no
|
|
261
|
+
// longer where the runner had got to when the line arrived — which named the
|
|
262
|
+
// next case for anything a `setTimeout` left behind — but which case's work
|
|
263
|
+
// the write descends from. A callback scheduled here keeps this name however
|
|
264
|
+
// late it fires.
|
|
265
|
+
//
|
|
266
|
+
// The cases above never reach this: nothing runs for a `todo`, a `skip` or a
|
|
267
|
+
// filtered-out case, so nothing of theirs can print.
|
|
268
|
+
await output.runInTest(name, async () => {
|
|
212
269
|
try {
|
|
213
|
-
|
|
270
|
+
for (const hook of context.beforeEach) {
|
|
271
|
+
await withTimeout(hook, timeoutMs);
|
|
272
|
+
}
|
|
273
|
+
if (benchmark) {
|
|
274
|
+
samples = await measure(body, test.bench, timeoutMs);
|
|
275
|
+
} else {
|
|
276
|
+
await withTimeout(body, timeoutMs);
|
|
277
|
+
}
|
|
214
278
|
} catch (thrown) {
|
|
215
|
-
|
|
216
|
-
|
|
279
|
+
outcome = failure(thrown, options.file ?? null);
|
|
280
|
+
}
|
|
281
|
+
// Teardown runs whatever happened above, and only reports its own failure
|
|
282
|
+
// when the body had not already failed.
|
|
283
|
+
for (const hook of context.afterEach) {
|
|
284
|
+
try {
|
|
285
|
+
await withTimeout(hook, timeoutMs);
|
|
286
|
+
} catch (thrown) {
|
|
287
|
+
if (outcome.status === "passed") {
|
|
288
|
+
outcome = failure(thrown, options.file ?? null);
|
|
289
|
+
}
|
|
217
290
|
}
|
|
218
291
|
}
|
|
219
|
-
}
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
292
|
+
});
|
|
293
|
+
// The `await` above resumes outside the context it entered, so this line and
|
|
294
|
+
// everything after it belong to no case again — a line printed between cases
|
|
295
|
+
// is the file's, and a snapshot taken outside one fails with something better
|
|
296
|
+
// than a key belonging to whichever test happened to run last.
|
|
224
297
|
snapshot.exitTest();
|
|
225
|
-
report(outcome);
|
|
298
|
+
report(outcome.status === "passed" && samples != null ? { status: "passed", samples } : outcome);
|
|
226
299
|
return outcome.status !== "failed";
|
|
227
300
|
}
|
|
228
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Time a benchmark's body: `warmup` calls thrown away, then `iterations` timed.
|
|
304
|
+
*
|
|
305
|
+
* The body is called directly and awaited, with nothing else between the two
|
|
306
|
+
* readings of the clock: a timer set around every call would be timed with
|
|
307
|
+
* it, and on a body that takes microseconds that is most of the number. The
|
|
308
|
+
* budget is held around the whole loop instead, at one call's budget per
|
|
309
|
+
* call, so a benchmark that hangs still fails rather than holding the worker.
|
|
310
|
+
*/
|
|
311
|
+
async function measure(
|
|
312
|
+
body: Body,
|
|
313
|
+
options: BenchOptions | null,
|
|
314
|
+
timeoutMs: number,
|
|
315
|
+
): Promise<$ReadOnlyArray<number>> {
|
|
316
|
+
const warmup = count("warmup", options?.warmup, DEFAULT_BENCH_WARMUP, 0, MAX_BENCH_WARMUP);
|
|
317
|
+
const iterations = count(
|
|
318
|
+
"iterations",
|
|
319
|
+
options?.iterations,
|
|
320
|
+
DEFAULT_BENCH_ITERATIONS,
|
|
321
|
+
1,
|
|
322
|
+
MAX_BENCH_ITERATIONS,
|
|
323
|
+
);
|
|
324
|
+
const rounds = warmup + iterations;
|
|
325
|
+
const samples: Array<number> = [];
|
|
326
|
+
await withTimeout(
|
|
327
|
+
async () => {
|
|
328
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
329
|
+
const started = performance.now();
|
|
330
|
+
await body();
|
|
331
|
+
if (round >= warmup) {
|
|
332
|
+
samples.push(Math.round((performance.now() - started) * 1000));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
Math.min(timeoutMs * rounds, MAX_TIMER_MS),
|
|
337
|
+
);
|
|
338
|
+
return samples;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A benchmark option that has to be a whole number from `least` to `most`.
|
|
343
|
+
*
|
|
344
|
+
* Refused rather than clamped: `iterations: 0` or `warmup: 1e9` is a mistake,
|
|
345
|
+
* and a benchmark quietly run some other number of times reports numbers
|
|
346
|
+
* about a run nobody asked for.
|
|
347
|
+
*/
|
|
348
|
+
function count(
|
|
349
|
+
option: string,
|
|
350
|
+
value: ?number,
|
|
351
|
+
fallback: number,
|
|
352
|
+
least: number,
|
|
353
|
+
most: number,
|
|
354
|
+
): number {
|
|
355
|
+
if (value == null) {
|
|
356
|
+
return fallback;
|
|
357
|
+
}
|
|
358
|
+
if (!Number.isInteger(value) || value < least || value > most) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`bench \`${option}\` has to be a whole number from ${least} to ${most}, and was ${String(value)}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
|
|
229
366
|
/**
|
|
230
367
|
* Walk one suite, running what it contains.
|
|
231
368
|
*
|
|
@@ -238,6 +375,7 @@ async function runSuite(
|
|
|
238
375
|
onlyMode: boolean,
|
|
239
376
|
emit: (result: Result) => void,
|
|
240
377
|
state: {| bail: boolean |},
|
|
378
|
+
setUpAncestors: () => Promise<void>,
|
|
241
379
|
): Promise<boolean> {
|
|
242
380
|
const skipped = context.skipped || node.modifier === "skip" || node.modifier === "todo";
|
|
243
381
|
const onlyPath = !onlyMode || context.onlyPath || node.modifier === "only";
|
|
@@ -252,11 +390,18 @@ async function runSuite(
|
|
|
252
390
|
|
|
253
391
|
// `beforeAll` is deferred until a case in this suite actually runs, so a
|
|
254
392
|
// fully skipped suite never sets anything up. `afterAll` mirrors it.
|
|
393
|
+
//
|
|
394
|
+
// "In this suite" means anywhere under it. A suite whose children are all
|
|
395
|
+
// suites has no case of its own, and setting up only for a direct child left
|
|
396
|
+
// every `beforeAll` in an ordinary file — one where the tests live inside a
|
|
397
|
+
// `describe` — never running at all, silently. The chain is walked outermost
|
|
398
|
+
// first, so an inner suite's setup sees what the outer one did.
|
|
255
399
|
let setUp = false;
|
|
256
400
|
const setUpOnce = async () => {
|
|
257
401
|
if (setUp) {
|
|
258
402
|
return;
|
|
259
403
|
}
|
|
404
|
+
await setUpAncestors();
|
|
260
405
|
setUp = true;
|
|
261
406
|
for (const hook of node.beforeAll) {
|
|
262
407
|
await withTimeout(hook, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
@@ -268,13 +413,16 @@ async function runSuite(
|
|
|
268
413
|
if (state.bail) {
|
|
269
414
|
break;
|
|
270
415
|
}
|
|
271
|
-
if (child.kind
|
|
416
|
+
if (child.kind !== "suite") {
|
|
272
417
|
const willRun =
|
|
273
418
|
!inner.skipped &&
|
|
274
419
|
child.modifier !== "skip" &&
|
|
275
420
|
child.modifier !== "todo" &&
|
|
276
421
|
child.body != null &&
|
|
277
|
-
(!onlyMode || inner.onlyPath || child.modifier === "only")
|
|
422
|
+
(!onlyMode || inner.onlyPath || child.modifier === "only") &&
|
|
423
|
+
// A benchmark in a run of the tests, or a test in a run of the
|
|
424
|
+
// benchmarks, is reported skipped and sets nothing up.
|
|
425
|
+
(child.kind === "bench") === (options.bench === true);
|
|
278
426
|
if (willRun) {
|
|
279
427
|
try {
|
|
280
428
|
await setUpOnce();
|
|
@@ -286,7 +434,7 @@ async function runSuite(
|
|
|
286
434
|
line: child.line,
|
|
287
435
|
column: child.column,
|
|
288
436
|
durationMicros: 0,
|
|
289
|
-
outcome: failure(thrown),
|
|
437
|
+
outcome: failure(thrown, options.file ?? null),
|
|
290
438
|
});
|
|
291
439
|
passed = false;
|
|
292
440
|
continue;
|
|
@@ -299,7 +447,7 @@ async function runSuite(
|
|
|
299
447
|
const ok = await runCase(child, childContext, options, emit);
|
|
300
448
|
passed = passed && ok;
|
|
301
449
|
} else {
|
|
302
|
-
const ok = await runSuite(child, inner, options, onlyMode, emit, state);
|
|
450
|
+
const ok = await runSuite(child, inner, options, onlyMode, emit, state, setUpOnce);
|
|
303
451
|
passed = passed && ok;
|
|
304
452
|
}
|
|
305
453
|
}
|
|
@@ -333,5 +481,5 @@ export async function run(options: RunOptions, emit: (result: Result) => void):
|
|
|
333
481
|
skipped: false,
|
|
334
482
|
onlyPath: !onlyMode,
|
|
335
483
|
};
|
|
336
|
-
await runSuite(root, context, options, onlyMode, emit, { bail: false });
|
|
484
|
+
await runSuite(root, context, options, onlyMode, emit, { bail: false }, async () => {});
|
|
337
485
|
}
|