@uniflowed/test 0.0.0-alpha.1 → 0.0.0-alpha.4
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/index.js +19 -2
- package/internal/asymmetric.js +184 -0
- package/internal/equality.js +79 -19
- package/internal/expect.js +294 -81
- package/internal/frames.js +9 -3
- package/internal/namespace.js +251 -0
- package/internal/output.js +260 -0
- package/internal/registry.js +30 -27
- package/internal/run.js +44 -21
- package/internal/snapshot.js +306 -0
- package/internal/spy.js +236 -0
- package/internal/timers.js +362 -0
- package/package.json +5 -2
- package/worker.js +62 -15
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: the `uft` namespace.
|
|
4
|
+
//
|
|
5
|
+
// The operations Vitest groups under `vi`, under uf's own name. The *shape* is
|
|
6
|
+
// what a migration needs — `fn`, `spyOn`, `stubEnv`, `useFakeTimers` doing what
|
|
7
|
+
// they do elsewhere — and borrowing another tool's brand for it would be
|
|
8
|
+
// claiming something uf has not earned.
|
|
9
|
+
//
|
|
10
|
+
// A namespace rather than loose named exports, because several of these names
|
|
11
|
+
// are generic enough to collide in a test file: `@uniflowed/testing` re-exports
|
|
12
|
+
// both this package and `@uniflowed/react-testing`, and both have a `waitFor`.
|
|
13
|
+
//
|
|
14
|
+
// `uft` rather than `uf`, and rather than `uf.test`. A bare `uf` is the command
|
|
15
|
+
// and the project, and a test file would be using the name for something much
|
|
16
|
+
// smaller than everything else in the toolchain answers to. `uf.test` reads as
|
|
17
|
+
// the `test` function this package also exports, which is `it` under another
|
|
18
|
+
// name — two very different things one dot apart. `uft` is three characters,
|
|
19
|
+
// belongs to nothing else, and is what a reader types a hundred times a file.
|
|
20
|
+
//
|
|
21
|
+
// What is *not* here is as deliberate. `uft.mock` intercepts a module before it
|
|
22
|
+
// is imported, which needs the loader rather than the runner, and uf's loader is
|
|
23
|
+
// `@uniflowed/host` — so it is a real piece of work rather than a wrapper, and
|
|
24
|
+
// it is not pretended at here. A missing binding throws with what it would take;
|
|
25
|
+
// a binding that silently did nothing would be worse than not having it.
|
|
26
|
+
|
|
27
|
+
import { clearAllMocks, fn, resetAllMocks, restoreAllMocks, spyOn } from "./spy.js";
|
|
28
|
+
import * as timers from "./timers.js";
|
|
29
|
+
|
|
30
|
+
/** Environment variables `stubEnv` replaced, and what they were. */
|
|
31
|
+
const stubbedEnv: Map<string, string | void> = new Map();
|
|
32
|
+
|
|
33
|
+
/** Globals `stubGlobal` replaced, and what they were. */
|
|
34
|
+
const stubbedGlobals: Map<string, { readonly owned: boolean, readonly value: mixed }> = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Raised for a `uft` namespace binding that is not implemented.
|
|
38
|
+
*
|
|
39
|
+
* Names what the binding needs rather than only that it is missing: `uft.mock`
|
|
40
|
+
* is absent because module interception belongs to the loader, and a reader who
|
|
41
|
+
* knows that can decide whether to wait or to restructure the test.
|
|
42
|
+
*/
|
|
43
|
+
export class UnsupportedError extends Error {
|
|
44
|
+
/** The binding that was called. */
|
|
45
|
+
binding: string;
|
|
46
|
+
|
|
47
|
+
constructor(binding: string, reason: string) {
|
|
48
|
+
super(`uft.${binding} is not implemented yet: ${reason}`);
|
|
49
|
+
this.name = "UnsupportedError";
|
|
50
|
+
this.binding = binding;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read the process environment, whichever host this is.
|
|
56
|
+
*
|
|
57
|
+
* Node, Deno and Bun all expose `process.env`; Deno also has `Deno.env`, and
|
|
58
|
+
* reaching for `process` first keeps one code path across the three.
|
|
59
|
+
*/
|
|
60
|
+
function environment(): { [string]: string } | null {
|
|
61
|
+
const host = globalThis as $FlowFixMe;
|
|
62
|
+
return host.process?.env ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Replace an environment variable for the rest of the test.
|
|
67
|
+
*
|
|
68
|
+
* Undone by `unstubAllEnvs`, which the runner calls between files — a stub that
|
|
69
|
+
* outlived its test would be a test that passes alone and fails in a suite.
|
|
70
|
+
*/
|
|
71
|
+
export function stubEnv(name: string, value: string | void): void {
|
|
72
|
+
const env = environment();
|
|
73
|
+
if (env == null) {
|
|
74
|
+
throw new UnsupportedError("stubEnv", "this host exposes no process environment");
|
|
75
|
+
}
|
|
76
|
+
if (!stubbedEnv.has(name)) {
|
|
77
|
+
stubbedEnv.set(name, Object.hasOwn(env, name) ? env[name] : undefined);
|
|
78
|
+
}
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
delete env[name];
|
|
81
|
+
} else {
|
|
82
|
+
env[name] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Put every environment variable `stubEnv` replaced back. */
|
|
87
|
+
export function unstubAllEnvs(): void {
|
|
88
|
+
const env = environment();
|
|
89
|
+
if (env == null) {
|
|
90
|
+
stubbedEnv.clear();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const [name, previous] of stubbedEnv) {
|
|
94
|
+
if (previous === undefined) {
|
|
95
|
+
delete env[name];
|
|
96
|
+
} else {
|
|
97
|
+
env[name] = previous;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
stubbedEnv.clear();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Replace a global for the rest of the test.
|
|
105
|
+
*
|
|
106
|
+
* Whether the global was the object's own property is recorded, because putting
|
|
107
|
+
* back an inherited one by assignment would leave a copy that shadows whatever
|
|
108
|
+
* it was inherited from.
|
|
109
|
+
*/
|
|
110
|
+
export function stubGlobal(name: string, value: mixed): void {
|
|
111
|
+
const host = globalThis as $FlowFixMe;
|
|
112
|
+
if (!stubbedGlobals.has(name)) {
|
|
113
|
+
stubbedGlobals.set(name, {
|
|
114
|
+
owned: Object.hasOwn(host, name),
|
|
115
|
+
value: host[name],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
host[name] = value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Put every global `stubGlobal` replaced back. */
|
|
122
|
+
export function unstubAllGlobals(): void {
|
|
123
|
+
const host = globalThis as $FlowFixMe;
|
|
124
|
+
for (const [name, previous] of stubbedGlobals) {
|
|
125
|
+
if (previous.owned) {
|
|
126
|
+
host[name] = previous.value;
|
|
127
|
+
} else {
|
|
128
|
+
delete host[name];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
stubbedGlobals.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** How often `waitFor` re-runs its body while it is failing. */
|
|
135
|
+
const WAIT_INTERVAL_MS = 20;
|
|
136
|
+
|
|
137
|
+
/** How long `waitFor` keeps trying before giving up. */
|
|
138
|
+
const WAIT_TIMEOUT_MS = 1_000;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run `body` until it stops throwing, or the timeout passes.
|
|
142
|
+
*
|
|
143
|
+
* The last failure is what is raised, not a timeout — "expected 2, got 1" says
|
|
144
|
+
* what went wrong, and "timed out" says only that something did.
|
|
145
|
+
*/
|
|
146
|
+
export async function waitFor<T>(
|
|
147
|
+
body: () => T | Promise<T>,
|
|
148
|
+
options?: { readonly timeout?: number, readonly interval?: number },
|
|
149
|
+
): Promise<T> {
|
|
150
|
+
const timeout = options?.timeout ?? WAIT_TIMEOUT_MS;
|
|
151
|
+
const interval = options?.interval ?? WAIT_INTERVAL_MS;
|
|
152
|
+
const deadline = Date.now() + timeout;
|
|
153
|
+
let last: mixed = null;
|
|
154
|
+
|
|
155
|
+
for (;;) {
|
|
156
|
+
try {
|
|
157
|
+
return await body();
|
|
158
|
+
} catch (thrown) {
|
|
159
|
+
last = thrown;
|
|
160
|
+
}
|
|
161
|
+
if (Date.now() >= deadline) {
|
|
162
|
+
throw last ?? new Error(`uft.waitFor: gave up after ${timeout}ms`);
|
|
163
|
+
}
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Run `body` until it returns something truthy, or the timeout passes. */
|
|
169
|
+
export async function waitUntil(
|
|
170
|
+
body: () => mixed | Promise<mixed>,
|
|
171
|
+
options?: { readonly timeout?: number, readonly interval?: number },
|
|
172
|
+
): Promise<mixed> {
|
|
173
|
+
return waitFor(async () => {
|
|
174
|
+
const value = await body();
|
|
175
|
+
if (value == null || value === false) {
|
|
176
|
+
throw new Error("uft.waitUntil: the condition is not true yet");
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}, options);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Hand a value back with its mock methods visible to the type checker.
|
|
184
|
+
*
|
|
185
|
+
* Purely a type-level convenience, exactly as in Vitest: at runtime it is the
|
|
186
|
+
* identity function, and its whole job is letting a test write
|
|
187
|
+
* `uft.mocked(client.send).mockReturnValue(…)` without a cast.
|
|
188
|
+
*/
|
|
189
|
+
export function mocked<T>(value: T): $FlowFixMe {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Not implemented, and specific about what it would take. */
|
|
194
|
+
function unsupported(binding: string, reason: string): () => empty {
|
|
195
|
+
return () => {
|
|
196
|
+
throw new UnsupportedError(binding, reason);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The reason every module-interception binding is absent. */
|
|
201
|
+
const NEEDS_LOADER =
|
|
202
|
+
"intercepting a module before it is imported belongs to the loader " +
|
|
203
|
+
"(`@uniflowed/host`), not to the runner, and uf has not wired it yet";
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The `uft` namespace.
|
|
207
|
+
*
|
|
208
|
+
* A frozen object rather than a class: it is a namespace, nothing about it is
|
|
209
|
+
* per-instance, and freezing it means a test cannot leave a monkey-patch behind
|
|
210
|
+
* for the next one.
|
|
211
|
+
*/
|
|
212
|
+
export const uft: $FlowFixMe = Object.freeze({
|
|
213
|
+
fn,
|
|
214
|
+
spyOn,
|
|
215
|
+
mocked,
|
|
216
|
+
|
|
217
|
+
clearAllMocks,
|
|
218
|
+
resetAllMocks,
|
|
219
|
+
restoreAllMocks,
|
|
220
|
+
|
|
221
|
+
stubEnv,
|
|
222
|
+
unstubAllEnvs,
|
|
223
|
+
stubGlobal,
|
|
224
|
+
unstubAllGlobals,
|
|
225
|
+
|
|
226
|
+
waitFor,
|
|
227
|
+
waitUntil,
|
|
228
|
+
|
|
229
|
+
// The clock a test controls. A test about "after five minutes the session
|
|
230
|
+
// expires" should not take five minutes.
|
|
231
|
+
useFakeTimers: timers.useFakeTimers,
|
|
232
|
+
useRealTimers: timers.useRealTimers,
|
|
233
|
+
isFakeTimers: timers.isFaked,
|
|
234
|
+
advanceTimersByTime: timers.advanceTimersByTime,
|
|
235
|
+
advanceTimersByTimeAsync: timers.advanceTimersByTimeAsync,
|
|
236
|
+
advanceTimersToNextTimer: timers.advanceTimersToNextTimer,
|
|
237
|
+
runAllTimers: timers.runAllTimers,
|
|
238
|
+
runOnlyPendingTimers: timers.runOnlyPendingTimers,
|
|
239
|
+
getTimerCount: timers.getTimerCount,
|
|
240
|
+
setSystemTime: timers.setSystemTime,
|
|
241
|
+
getMockedSystemTime: timers.getMockedSystemTime,
|
|
242
|
+
|
|
243
|
+
// Module interception. Absent rather than faked; see `NEEDS_LOADER`.
|
|
244
|
+
mock: unsupported("mock", NEEDS_LOADER),
|
|
245
|
+
doMock: unsupported("doMock", NEEDS_LOADER),
|
|
246
|
+
unmock: unsupported("unmock", NEEDS_LOADER),
|
|
247
|
+
doUnmock: unsupported("doUnmock", NEEDS_LOADER),
|
|
248
|
+
importActual: unsupported("importActual", NEEDS_LOADER),
|
|
249
|
+
importMock: unsupported("importMock", NEEDS_LOADER),
|
|
250
|
+
resetModules: unsupported("resetModules", NEEDS_LOADER),
|
|
251
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: what a test printed, attributed to the test
|
|
4
|
+
// that printed it.
|
|
5
|
+
//
|
|
6
|
+
// The worker and the tests it runs share one stdout, and the worker's protocol
|
|
7
|
+
// is one JSON object per line on it. A `console.log` in a test therefore wrote
|
|
8
|
+
// a line into the middle of that protocol, `uf` read it as an event it could
|
|
9
|
+
// not parse, and the whole file died — a green suite killed by a debugging
|
|
10
|
+
// statement somebody left in.
|
|
11
|
+
//
|
|
12
|
+
// So the stream stops being shared. This module takes over `console` and the
|
|
13
|
+
// two `write` methods a test can reach directly, turns everything written
|
|
14
|
+
// through them into an ordinary protocol event, and hands the caller the real
|
|
15
|
+
// `process.stdout.write` it took — the protocol keeps the raw stream, and it
|
|
16
|
+
// is the only thing that has it. A test that prints something shaped exactly
|
|
17
|
+
// like a protocol line is then a string inside a JSON field, which is the
|
|
18
|
+
// point: the escaping is what makes the channel robust, not a filter that
|
|
19
|
+
// tries to recognise the imposter.
|
|
20
|
+
//
|
|
21
|
+
// # Why this is its own module
|
|
22
|
+
//
|
|
23
|
+
// Two callers need it and neither owns it: `worker.js` installs the capture at
|
|
24
|
+
// start-up, and `run.js` says which case is running so a chunk can be named.
|
|
25
|
+
// "Who printed this" is also state with a lifetime of its own — set around a
|
|
26
|
+
// case's body and hooks, cleared between them — exactly like the snapshot key
|
|
27
|
+
// in `snapshot.js`, and for the same reason it lives beside the thing it
|
|
28
|
+
// describes rather than inside either caller.
|
|
29
|
+
//
|
|
30
|
+
// # What "the test that printed it" means
|
|
31
|
+
//
|
|
32
|
+
// The name a chunk carries is the case the *worker* is running when the chunk
|
|
33
|
+
// arrives, which is not the same as the case whose code produced it. A
|
|
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.
|
|
37
|
+
//
|
|
38
|
+
// Getting this exactly right needs the printing to be tied to the asynchronous
|
|
39
|
+
// context the case ran in — `AsyncLocalStorage` and everything under it — and
|
|
40
|
+
// that is a bigger change than this module, because it has to reach the
|
|
41
|
+
// scheduler that runs the cases. It is written down here rather than left to
|
|
42
|
+
// be discovered from a confusing report. See ubugeeei-prod/uf#207.
|
|
43
|
+
|
|
44
|
+
// # Bounds
|
|
45
|
+
//
|
|
46
|
+
// A test that prints in a loop must not be able to fill the pipe, the worker's
|
|
47
|
+
// memory or the report, so one file's captured output is bounded and the
|
|
48
|
+
// bound is announced rather than hidden: the chunk that reaches it says so and
|
|
49
|
+
// nothing after it is kept. The budget starts over for each file, so a chatty
|
|
50
|
+
// file does not silence the next one in the same worker.
|
|
51
|
+
|
|
52
|
+
import { format, inspect } from "node:util";
|
|
53
|
+
|
|
54
|
+
import { userFrames } from "./frames.js";
|
|
55
|
+
|
|
56
|
+
/** Which of the process's two streams a chunk was written to. */
|
|
57
|
+
export type OutputStream = "stdout" | "stderr";
|
|
58
|
+
|
|
59
|
+
/** One thing a test — or the file around it — printed. */
|
|
60
|
+
export type OutputChunk = {|
|
|
61
|
+
readonly stream: OutputStream,
|
|
62
|
+
/** Full name of the case that was running, or `null` when none was. */
|
|
63
|
+
readonly test: string | null,
|
|
64
|
+
/** The text as it would have reached the terminal, newline included. */
|
|
65
|
+
readonly text: string,
|
|
66
|
+
|};
|
|
67
|
+
|
|
68
|
+
/** Where captured output goes. */
|
|
69
|
+
export type OutputSink = (chunk: OutputChunk) => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Longest single write kept, in UTF-16 code units.
|
|
73
|
+
*
|
|
74
|
+
* A test that prints a megabyte-long serialised fixture meant to print
|
|
75
|
+
* something; the first few kilobytes of it are what says what happened, and
|
|
76
|
+
* the rest is not a report's to carry.
|
|
77
|
+
*/
|
|
78
|
+
export const MAX_CHUNK_LENGTH: number = 8 * 1024;
|
|
79
|
+
|
|
80
|
+
/** Most output kept from one file, in UTF-16 code units. */
|
|
81
|
+
export const MAX_FILE_LENGTH: number = 128 * 1024;
|
|
82
|
+
|
|
83
|
+
/** Which stream each replaced `console` method writes to, as Node routes them. */
|
|
84
|
+
const CONSOLE_STREAMS: { readonly [string]: OutputStream } = {
|
|
85
|
+
log: "stdout",
|
|
86
|
+
info: "stdout",
|
|
87
|
+
debug: "stdout",
|
|
88
|
+
warn: "stderr",
|
|
89
|
+
error: "stderr",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Decoder for a `write` that was handed bytes rather than a string. */
|
|
93
|
+
const DECODER = new TextDecoder();
|
|
94
|
+
|
|
95
|
+
/** What a stream's `write` calls when it has taken the chunk. */
|
|
96
|
+
type WriteCallback = () => mixed;
|
|
97
|
+
|
|
98
|
+
let sink: OutputSink | null = null;
|
|
99
|
+
let raw: ((chunk: string) => void) | null = null;
|
|
100
|
+
let current: string | null = null;
|
|
101
|
+
let captured = 0;
|
|
102
|
+
let stopped = false;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The globals to patch, in one place.
|
|
106
|
+
*
|
|
107
|
+
* The one untyped expression in this module, and it is the trust boundary
|
|
108
|
+
* itself: `console` and `process.stdout` are the host's, their libdef types
|
|
109
|
+
* are read-only, and replacing them is exactly what this module is for.
|
|
110
|
+
*/
|
|
111
|
+
function host(): $FlowFixMe {
|
|
112
|
+
return globalThis;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `args` rendered the way `console.log` renders them.
|
|
117
|
+
*
|
|
118
|
+
* Node's console hands its arguments to `util.format`, and this is that in two
|
|
119
|
+
* cases rather than one, because `format`'s first parameter is the template:
|
|
120
|
+
* with a leading string it substitutes `%s`, `%d`, `%o` and the rest and
|
|
121
|
+
* inspects whatever is left over, and with anything else there is no template
|
|
122
|
+
* to substitute into, so every argument is inspected and the results joined
|
|
123
|
+
* with a space. Both are what Node prints.
|
|
124
|
+
*/
|
|
125
|
+
function formatArguments(args: $ReadOnlyArray<mixed>): string {
|
|
126
|
+
const [first, ...rest] = args;
|
|
127
|
+
if (typeof first === "string") {
|
|
128
|
+
return format(first, ...rest);
|
|
129
|
+
}
|
|
130
|
+
return args.map((value) => (typeof value === "string" ? value : inspect(value))).join(" ");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The text of one `write` argument, whether it arrived as bytes or a string. */
|
|
134
|
+
function textOf(chunk: mixed): string {
|
|
135
|
+
if (typeof chunk === "string") {
|
|
136
|
+
return chunk;
|
|
137
|
+
}
|
|
138
|
+
if (chunk instanceof Uint8Array) {
|
|
139
|
+
return DECODER.decode(chunk);
|
|
140
|
+
}
|
|
141
|
+
return String(chunk);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Record one piece of output, within the file's budget.
|
|
146
|
+
*
|
|
147
|
+
* Silently doing nothing when no sink is installed is deliberate: a test that
|
|
148
|
+
* prints must never fail because of how it was run.
|
|
149
|
+
*/
|
|
150
|
+
function capture(stream: OutputStream, text: string): void {
|
|
151
|
+
const to = sink;
|
|
152
|
+
if (to == null || stopped || text === "") {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let kept = text.length > MAX_CHUNK_LENGTH ? `${text.slice(0, MAX_CHUNK_LENGTH)}…\n` : text;
|
|
156
|
+
const room = MAX_FILE_LENGTH - captured;
|
|
157
|
+
if (kept.length >= room) {
|
|
158
|
+
kept = `${kept.slice(0, Math.max(room, 0))}\n[uf] output stopped after ${MAX_FILE_LENGTH} characters\n`;
|
|
159
|
+
stopped = true;
|
|
160
|
+
}
|
|
161
|
+
captured += kept.length;
|
|
162
|
+
to({ stream, test: current, text: kept });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A stand-in for `process.stdout.write` / `process.stderr.write`. */
|
|
166
|
+
function writer(
|
|
167
|
+
stream: OutputStream,
|
|
168
|
+
): (chunk: mixed, encoding?: string | WriteCallback, callback?: WriteCallback) => boolean {
|
|
169
|
+
return (chunk, encoding, callback) => {
|
|
170
|
+
capture(stream, textOf(chunk));
|
|
171
|
+
// `write(chunk, callback)` and `write(chunk, encoding, callback)` are both
|
|
172
|
+
// real calls, and a caller that passed a callback is waiting for it.
|
|
173
|
+
//
|
|
174
|
+
// Deferred, because the real `Writable.write` never calls back before it
|
|
175
|
+
// returns: a caller that writes and then does something on the next line
|
|
176
|
+
// has that line run first, and one whose callback ran inline would see the
|
|
177
|
+
// two in the other order. `queueMicrotask` rather than `process.nextTick`
|
|
178
|
+
// so this holds on every host uf supports.
|
|
179
|
+
const done = typeof encoding === "function" ? encoding : callback;
|
|
180
|
+
if (done != null) {
|
|
181
|
+
queueMicrotask(done);
|
|
182
|
+
}
|
|
183
|
+
// The real method returns whether the stream has room for more. Nothing is
|
|
184
|
+
// buffered here, so it always has — and a caller told otherwise would wait
|
|
185
|
+
// for a `drain` that never comes.
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Route everything a test prints to `to` instead of to the process's streams,
|
|
192
|
+
* and return the real `process.stdout.write` that was replaced.
|
|
193
|
+
*
|
|
194
|
+
* The caller gets the raw stream because the caller is the protocol, and a
|
|
195
|
+
* protocol sharing its stream with the code it reports on is the bug this
|
|
196
|
+
* module exists for. Handing it back from here rather than letting the caller
|
|
197
|
+
* read it first is what makes "taken before anything could have replaced it"
|
|
198
|
+
* true by construction.
|
|
199
|
+
*
|
|
200
|
+
* Installed once, for the life of the worker: a worker runs many files, and
|
|
201
|
+
* restoring the real methods between them would leave a window in which a
|
|
202
|
+
* straggling `setTimeout` from the previous file writes into the protocol.
|
|
203
|
+
*/
|
|
204
|
+
export function install(to: OutputSink): (chunk: string) => void {
|
|
205
|
+
const global = host();
|
|
206
|
+
const already = raw;
|
|
207
|
+
if (already != null) {
|
|
208
|
+
return already;
|
|
209
|
+
}
|
|
210
|
+
const stdout = global.process.stdout;
|
|
211
|
+
const real = stdout.write;
|
|
212
|
+
const protocol = (chunk: string) => {
|
|
213
|
+
real.call(stdout, chunk);
|
|
214
|
+
};
|
|
215
|
+
raw = protocol;
|
|
216
|
+
sink = to;
|
|
217
|
+
for (const method of Object.keys(CONSOLE_STREAMS)) {
|
|
218
|
+
const stream = CONSOLE_STREAMS[method];
|
|
219
|
+
global.console[method] = (...args: $ReadOnlyArray<mixed>) => {
|
|
220
|
+
capture(stream, `${formatArguments(args)}\n`);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
global.console.trace = (...args: $ReadOnlyArray<mixed>) => {
|
|
224
|
+
// `console.trace` is a message *and* the stack under it, which is the
|
|
225
|
+
// whole reason to call it rather than `console.error`. `Error.stack`
|
|
226
|
+
// opens with `Trace: <message>`, so the trace Node prints is that string
|
|
227
|
+
// with this module's own frames taken off it.
|
|
228
|
+
const error = new Error(formatArguments(args));
|
|
229
|
+
error.name = "Trace";
|
|
230
|
+
capture("stderr", `${userFrames(error.stack) ?? `Trace: ${error.message}`}\n`);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
global.process.stdout.write = writer("stdout");
|
|
234
|
+
global.process.stderr.write = writer("stderr");
|
|
235
|
+
return protocol;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Say which case is running, so what it prints can be named.
|
|
240
|
+
*
|
|
241
|
+
* The runner calls this around a case's body and hooks and clears it between
|
|
242
|
+
* them: output written while the module is being imported, from a `beforeAll`,
|
|
243
|
+
* or after the last case finished belongs to the file, not to whichever case
|
|
244
|
+
* happened to run last.
|
|
245
|
+
*/
|
|
246
|
+
export function enterTest(name: string): void {
|
|
247
|
+
current = name;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Say that no case is running. */
|
|
251
|
+
export function exitTest(): void {
|
|
252
|
+
current = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Start one file's output budget over, with no case running. */
|
|
256
|
+
export function startFile(): void {
|
|
257
|
+
captured = 0;
|
|
258
|
+
stopped = false;
|
|
259
|
+
current = null;
|
|
260
|
+
}
|
package/internal/registry.js
CHANGED
|
@@ -32,27 +32,27 @@ export type Modifier = "none" | "only" | "skip" | "todo";
|
|
|
32
32
|
|
|
33
33
|
/** One registered test case. */
|
|
34
34
|
export type Case = {|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
readonly kind: "test",
|
|
36
|
+
readonly name: string,
|
|
37
|
+
readonly body: Body | null,
|
|
38
|
+
readonly modifier: Modifier,
|
|
39
|
+
readonly timeoutMs: number | null,
|
|
40
|
+
readonly line: number,
|
|
41
|
+
readonly column: number,
|
|
42
42
|
|};
|
|
43
43
|
|
|
44
44
|
/** One `describe` and everything inside it. */
|
|
45
45
|
export type Suite = {|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
readonly kind: "suite",
|
|
47
|
+
readonly name: string,
|
|
48
|
+
readonly modifier: Modifier,
|
|
49
|
+
readonly children: Array<Suite | Case>,
|
|
50
|
+
readonly beforeAll: Array<Body>,
|
|
51
|
+
readonly afterAll: Array<Body>,
|
|
52
|
+
readonly beforeEach: Array<Body>,
|
|
53
|
+
readonly afterEach: Array<Body>,
|
|
54
|
+
readonly line: number,
|
|
55
|
+
readonly column: number,
|
|
56
56
|
|};
|
|
57
57
|
|
|
58
58
|
function suite(name: string, modifier: Modifier, line: number, column: number): Suite {
|
|
@@ -101,7 +101,7 @@ export function collected(): Suite {
|
|
|
101
101
|
* not in a shape we understand, the position is `0`, which every consumer
|
|
102
102
|
* treats as "unknown" rather than as line one.
|
|
103
103
|
*/
|
|
104
|
-
function callSite(): {|
|
|
104
|
+
function callSite(): {| readonly line: number, readonly column: number |} {
|
|
105
105
|
return firstUserSite(new Error("position").stack) ?? { line: 0, column: 0 };
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -118,7 +118,12 @@ function addSuite(name: string, body: Body, modifier: Modifier): void {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function addCase(
|
|
121
|
+
function addCase(
|
|
122
|
+
name: string,
|
|
123
|
+
body: Body | null,
|
|
124
|
+
modifier: Modifier,
|
|
125
|
+
timeoutMs: number | null,
|
|
126
|
+
): void {
|
|
122
127
|
const position = callSite();
|
|
123
128
|
current.children.push({
|
|
124
129
|
kind: "test",
|
|
@@ -132,7 +137,7 @@ function addCase(name: string, body: Body | null, modifier: Modifier, timeoutMs:
|
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/** Options a single test may carry. */
|
|
135
|
-
export type TestOptions = {|
|
|
140
|
+
export type TestOptions = {| readonly timeout?: number |};
|
|
136
141
|
|
|
137
142
|
/**
|
|
138
143
|
* The `describe` API, and its modifiers.
|
|
@@ -155,13 +160,11 @@ function suiteApi(): $FlowFixMe {
|
|
|
155
160
|
api.todo = (name: string, body?: Body) => {
|
|
156
161
|
addSuite(name, body ?? (() => {}), "todo");
|
|
157
162
|
};
|
|
158
|
-
api.each =
|
|
159
|
-
(table
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
};
|
|
163
|
+
api.each = (table: $ReadOnlyArray<mixed>) => (name: string, body: (row: mixed) => mixed) => {
|
|
164
|
+
for (const row of table) {
|
|
165
|
+
addSuite(formatRow(name, row), () => body(row), "none");
|
|
166
|
+
}
|
|
167
|
+
};
|
|
165
168
|
return api;
|
|
166
169
|
}
|
|
167
170
|
|