@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,362 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: a clock a test controls.
|
|
4
|
+
//
|
|
5
|
+
// A test about "after five minutes the session expires" should not take five
|
|
6
|
+
// minutes, and a test that waits a real 50ms to see a debounce is a test that
|
|
7
|
+
// fails on a loaded machine. Fake timers replace the scheduling globals with a
|
|
8
|
+
// queue the test advances by hand, so elapsed time becomes an input rather than
|
|
9
|
+
// something to wait for.
|
|
10
|
+
//
|
|
11
|
+
// # What is faked
|
|
12
|
+
//
|
|
13
|
+
// `setTimeout`, `setInterval`, `setImmediate` and their `clear` partners, plus
|
|
14
|
+
// `Date.now` and `new Date()` with no arguments. Not `queueMicrotask` and not
|
|
15
|
+
// promises: a microtask is not scheduled *in time*, it runs at the end of the
|
|
16
|
+
// current turn, and pretending otherwise would let a test claim a promise
|
|
17
|
+
// resolved "after 100ms" when the two have nothing to do with each other. The
|
|
18
|
+
// async advance methods are the honest way to let microtasks run in between.
|
|
19
|
+
//
|
|
20
|
+
// # Why the ids are numbers
|
|
21
|
+
//
|
|
22
|
+
// Node's `setTimeout` returns a `Timeout` object and a browser's returns a
|
|
23
|
+
// number, and code written for either passes what it got straight to
|
|
24
|
+
// `clearTimeout`. A number works in both places — `clearTimeout` only ever
|
|
25
|
+
// looks the value up — and a test that stores one in a `Map` keyed by number
|
|
26
|
+
// keeps working.
|
|
27
|
+
|
|
28
|
+
/** One scheduled callback. */
|
|
29
|
+
type Task = {
|
|
30
|
+
readonly id: number,
|
|
31
|
+
/** When it is due, on the fake clock. */
|
|
32
|
+
due: number,
|
|
33
|
+
/** How often it repeats, or `null` for a one-shot. */
|
|
34
|
+
readonly every: number | null,
|
|
35
|
+
readonly body: (...args: $ReadOnlyArray<mixed>) => mixed,
|
|
36
|
+
readonly args: $ReadOnlyArray<mixed>,
|
|
37
|
+
/** Ordering among tasks due at the same instant: first scheduled, first run. */
|
|
38
|
+
readonly sequence: number,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The globals a clock replaces, so they can be put back exactly. */
|
|
42
|
+
type Saved = {
|
|
43
|
+
readonly setTimeout: mixed,
|
|
44
|
+
readonly clearTimeout: mixed,
|
|
45
|
+
readonly setInterval: mixed,
|
|
46
|
+
readonly clearInterval: mixed,
|
|
47
|
+
readonly setImmediate: mixed,
|
|
48
|
+
readonly clearImmediate: mixed,
|
|
49
|
+
readonly Date: mixed,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How many times a run loop will fire a timer before deciding it will not stop.
|
|
54
|
+
*
|
|
55
|
+
* An interval reschedules itself, so `runAllTimers` on one would never finish.
|
|
56
|
+
* A bound turns an unbounded hang into a failure that names the problem, which
|
|
57
|
+
* is the difference between a test suite that stops and one that has to be
|
|
58
|
+
* killed.
|
|
59
|
+
*/
|
|
60
|
+
const RUNAWAY_LIMIT = 10_000;
|
|
61
|
+
|
|
62
|
+
let installed: Saved | null = null;
|
|
63
|
+
let now = 0;
|
|
64
|
+
let nextId = 1;
|
|
65
|
+
let sequence = 0;
|
|
66
|
+
let tasks: Array<Task> = [];
|
|
67
|
+
|
|
68
|
+
/** Whether a clock is currently installed. */
|
|
69
|
+
export function isFaked(): boolean {
|
|
70
|
+
return installed != null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The globals to patch, in one place so install and restore cannot diverge. */
|
|
74
|
+
function host(): $FlowFixMe {
|
|
75
|
+
return globalThis;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Replace the scheduling globals with a clock this module controls.
|
|
80
|
+
*
|
|
81
|
+
* `now` starts at the real current time rather than at zero, so a test that
|
|
82
|
+
* formats a date sees a plausible one — and `setSystemTime` is how a test that
|
|
83
|
+
* cares says which.
|
|
84
|
+
*/
|
|
85
|
+
export function useFakeTimers(): void {
|
|
86
|
+
if (installed != null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const global = host();
|
|
90
|
+
installed = {
|
|
91
|
+
setTimeout: global.setTimeout,
|
|
92
|
+
clearTimeout: global.clearTimeout,
|
|
93
|
+
setInterval: global.setInterval,
|
|
94
|
+
clearInterval: global.clearInterval,
|
|
95
|
+
setImmediate: global.setImmediate,
|
|
96
|
+
clearImmediate: global.clearImmediate,
|
|
97
|
+
Date: global.Date,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
now = Date.now();
|
|
101
|
+
tasks = [];
|
|
102
|
+
nextId = 1;
|
|
103
|
+
sequence = 0;
|
|
104
|
+
|
|
105
|
+
global.setTimeout = (body: $FlowFixMe, delay?: number, ...args: $ReadOnlyArray<mixed>) =>
|
|
106
|
+
schedule(body, delay ?? 0, null, args);
|
|
107
|
+
global.setInterval = (body: $FlowFixMe, delay?: number, ...args: $ReadOnlyArray<mixed>) =>
|
|
108
|
+
// A zero-delay interval would be scheduled at the same instant forever, so
|
|
109
|
+
// it advances by one tick — which is what every runtime does with it.
|
|
110
|
+
schedule(body, delay ?? 0, Math.max(delay ?? 0, 1), args);
|
|
111
|
+
// `setImmediate` is "before any timer, after this turn", which on a fake
|
|
112
|
+
// clock is a zero-delay timer that sorts ahead by having been scheduled at
|
|
113
|
+
// the current instant.
|
|
114
|
+
global.setImmediate = (body: $FlowFixMe, ...args: $ReadOnlyArray<mixed>) =>
|
|
115
|
+
schedule(body, 0, null, args);
|
|
116
|
+
global.clearTimeout = cancel;
|
|
117
|
+
global.clearInterval = cancel;
|
|
118
|
+
global.clearImmediate = cancel;
|
|
119
|
+
global.Date = fakeDate(installed.Date as $FlowFixMe);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Put the real scheduling globals back. */
|
|
123
|
+
export function useRealTimers(): void {
|
|
124
|
+
if (installed == null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const global = host();
|
|
128
|
+
global.setTimeout = installed.setTimeout;
|
|
129
|
+
global.clearTimeout = installed.clearTimeout;
|
|
130
|
+
global.setInterval = installed.setInterval;
|
|
131
|
+
global.clearInterval = installed.clearInterval;
|
|
132
|
+
global.setImmediate = installed.setImmediate;
|
|
133
|
+
global.clearImmediate = installed.clearImmediate;
|
|
134
|
+
global.Date = installed.Date;
|
|
135
|
+
installed = null;
|
|
136
|
+
tasks = [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A `Date` whose "now" is the fake clock's.
|
|
141
|
+
*
|
|
142
|
+
* A proxy over the real constructor rather than a subclass of it, and the
|
|
143
|
+
* difference is three bugs rather than a preference:
|
|
144
|
+
*
|
|
145
|
+
* * `Date()` — called as a function, with no `new` — returns a string in every
|
|
146
|
+
* runtime. A `class` cannot be called that way at all, so a subclass turned
|
|
147
|
+
* every such call into a `TypeError`, under fake timers only.
|
|
148
|
+
* * `before instanceof Date`, for a date built *before* the clock was faked,
|
|
149
|
+
* was false: the object's prototype chain runs through the real `Date` and a
|
|
150
|
+
* subclass's prototype is not in it. Anything branching on that — including
|
|
151
|
+
* `setSystemTime` below, once — silently took the wrong branch. A proxy has
|
|
152
|
+
* no prototype of its own, so the check is the real one.
|
|
153
|
+
* * `Date.name` was `"FakeDate"`, and a subclass's own `toString` reads as
|
|
154
|
+
* class source rather than native code.
|
|
155
|
+
*
|
|
156
|
+
* Only `now` is replaced. `parse`, `UTC`, and every prototype method are the
|
|
157
|
+
* real ones, reached through the proxy.
|
|
158
|
+
*/
|
|
159
|
+
function fakeDate(Real: $FlowFixMe): $FlowFixMe {
|
|
160
|
+
return new Proxy(Real, {
|
|
161
|
+
// `Date(anything)` ignores its arguments and returns the current time as a
|
|
162
|
+
// string, which on this clock is the fake one.
|
|
163
|
+
apply(): string {
|
|
164
|
+
return new Real(now).toString();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
construct(target: $FlowFixMe, args: $ReadOnlyArray<mixed>, newTarget: $FlowFixMe) {
|
|
168
|
+
// Only the no-argument form reads the clock; every other form is
|
|
169
|
+
// constructing a specific date and has nothing to do with "now".
|
|
170
|
+
const actual = args.length === 0 ? [now] : args;
|
|
171
|
+
// `newTarget` rather than `Real`, so a subclass of the faked `Date` gets
|
|
172
|
+
// its own prototype instead of the real one's.
|
|
173
|
+
return Reflect.construct(Real, actual, newTarget === undefined ? Real : newTarget);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
get(target: $FlowFixMe, property: $FlowFixMe, receiver: $FlowFixMe): $FlowFixMe {
|
|
177
|
+
if (property === "now") {
|
|
178
|
+
return fakeNow;
|
|
179
|
+
}
|
|
180
|
+
return Reflect.get(target, property, receiver);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `Date.now`, as its own function so the proxy hands back a stable identity. */
|
|
186
|
+
function fakeNow(): number {
|
|
187
|
+
return now;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Put a task on the queue and hand back its id. */
|
|
191
|
+
function schedule(
|
|
192
|
+
body: $FlowFixMe,
|
|
193
|
+
delay: number,
|
|
194
|
+
every: number | null,
|
|
195
|
+
args: $ReadOnlyArray<mixed>,
|
|
196
|
+
): number {
|
|
197
|
+
const id = nextId;
|
|
198
|
+
nextId += 1;
|
|
199
|
+
sequence += 1;
|
|
200
|
+
tasks.push({
|
|
201
|
+
id,
|
|
202
|
+
due: now + Math.max(delay, 0),
|
|
203
|
+
every,
|
|
204
|
+
body,
|
|
205
|
+
args,
|
|
206
|
+
sequence,
|
|
207
|
+
});
|
|
208
|
+
return id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Take a task off the queue. Unknown ids are ignored, as the real ones are. */
|
|
212
|
+
function cancel(id: mixed): void {
|
|
213
|
+
tasks = tasks.filter((task) => task.id !== id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** How many timers are waiting. */
|
|
217
|
+
export function getTimerCount(): number {
|
|
218
|
+
return tasks.length;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The next task due, or `null`. Ties break by scheduling order. */
|
|
222
|
+
function nextTask(before: number): Task | null {
|
|
223
|
+
let best: Task | null = null;
|
|
224
|
+
for (const task of tasks) {
|
|
225
|
+
if (task.due > before) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
best == null ||
|
|
230
|
+
task.due < best.due ||
|
|
231
|
+
(task.due === best.due && task.sequence < best.sequence)
|
|
232
|
+
) {
|
|
233
|
+
best = task;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Run one task, rescheduling it when it repeats. */
|
|
240
|
+
function fire(task: Task): void {
|
|
241
|
+
now = task.due;
|
|
242
|
+
if (task.every == null) {
|
|
243
|
+
cancel(task.id);
|
|
244
|
+
} else {
|
|
245
|
+
task.due = now + task.every;
|
|
246
|
+
}
|
|
247
|
+
task.body(...task.args);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Raised when a run loop will not terminate. */
|
|
251
|
+
export class RunawayTimersError extends Error {
|
|
252
|
+
constructor(method: string) {
|
|
253
|
+
super(
|
|
254
|
+
`uf.${method}: still firing after ${RUNAWAY_LIMIT} timers. ` +
|
|
255
|
+
"A timer that reschedules itself never drains — advance the clock by a " +
|
|
256
|
+
"fixed amount instead.",
|
|
257
|
+
);
|
|
258
|
+
this.name = "RunawayTimersError";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Move the clock forward, firing everything that comes due. */
|
|
263
|
+
export function advanceTimersByTime(millis: number): void {
|
|
264
|
+
const target = now + Math.max(millis, 0);
|
|
265
|
+
let fired = 0;
|
|
266
|
+
for (;;) {
|
|
267
|
+
const task = nextTask(target);
|
|
268
|
+
if (task == null) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
fired += 1;
|
|
272
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
273
|
+
throw new RunawayTimersError("advanceTimersByTime");
|
|
274
|
+
}
|
|
275
|
+
fire(task);
|
|
276
|
+
}
|
|
277
|
+
// Land on the requested instant even when nothing was due there, so two
|
|
278
|
+
// advances of 50ms are the same as one of 100ms.
|
|
279
|
+
now = target;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The same, yielding to the microtask queue between timers. */
|
|
283
|
+
export async function advanceTimersByTimeAsync(millis: number): Promise<void> {
|
|
284
|
+
const target = now + Math.max(millis, 0);
|
|
285
|
+
let fired = 0;
|
|
286
|
+
for (;;) {
|
|
287
|
+
const task = nextTask(target);
|
|
288
|
+
if (task == null) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
fired += 1;
|
|
292
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
293
|
+
throw new RunawayTimersError("advanceTimersByTimeAsync");
|
|
294
|
+
}
|
|
295
|
+
fire(task);
|
|
296
|
+
// The point of the async form: a callback that awaited something gets to
|
|
297
|
+
// continue before the next timer fires.
|
|
298
|
+
await Promise.resolve();
|
|
299
|
+
}
|
|
300
|
+
now = target;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Fire the next timer due, whenever it is due. */
|
|
304
|
+
export function advanceTimersToNextTimer(): void {
|
|
305
|
+
const task = nextTask(Number.POSITIVE_INFINITY);
|
|
306
|
+
if (task != null) {
|
|
307
|
+
fire(task);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Fire everything until the queue is empty. */
|
|
312
|
+
export function runAllTimers(): void {
|
|
313
|
+
let fired = 0;
|
|
314
|
+
for (;;) {
|
|
315
|
+
const task = nextTask(Number.POSITIVE_INFINITY);
|
|
316
|
+
if (task == null) {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
fired += 1;
|
|
320
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
321
|
+
throw new RunawayTimersError("runAllTimers");
|
|
322
|
+
}
|
|
323
|
+
fire(task);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Fire only what is already queued, not what those callbacks schedule.
|
|
329
|
+
*
|
|
330
|
+
* The way to drain an interval once: `runAllTimers` on one never terminates,
|
|
331
|
+
* and this fires each waiting task exactly once.
|
|
332
|
+
*/
|
|
333
|
+
export function runOnlyPendingTimers(): void {
|
|
334
|
+
const pending = [...tasks].sort((a, b) => a.due - b.due || a.sequence - b.sequence);
|
|
335
|
+
for (const task of pending) {
|
|
336
|
+
// A callback may have cancelled a later one, so check it is still queued.
|
|
337
|
+
if (tasks.some((queued) => queued.id === task.id)) {
|
|
338
|
+
fire(task);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Move the wall clock without firing anything.
|
|
345
|
+
*
|
|
346
|
+
* Duck-typed rather than `instanceof Date`, and deliberately: `Date` here is
|
|
347
|
+
* whatever is installed, so the check would be asking about the *fake* one
|
|
348
|
+
* while the caller may be holding a date made before the clock was faked, or
|
|
349
|
+
* one from another realm. Either answered `false`, fell through to the number
|
|
350
|
+
* branch, and set the clock to an object — after which every comparison
|
|
351
|
+
* against it was `false` and no timer ever came due.
|
|
352
|
+
*
|
|
353
|
+
* Anything that can say what instant it is, is an instant.
|
|
354
|
+
*/
|
|
355
|
+
export function setSystemTime(time: number | Date): void {
|
|
356
|
+
now = typeof time === "number" ? time : Number((time as $FlowFixMe).getTime());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** What the fake clock currently reads. */
|
|
360
|
+
export function getMockedSystemTime(): Date | null {
|
|
361
|
+
return installed == null ? null : new (installed.Date as $FlowFixMe)(now);
|
|
362
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.4",
|
|
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",
|
|
@@ -19,5 +19,8 @@
|
|
|
19
19
|
"index.js",
|
|
20
20
|
"worker.js",
|
|
21
21
|
"internal"
|
|
22
|
-
]
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@uniflowed/host": "0.0.0-alpha.4"
|
|
25
|
+
}
|
|
23
26
|
}
|
package/worker.js
CHANGED
|
@@ -9,32 +9,59 @@
|
|
|
9
9
|
//
|
|
10
10
|
// → {"file": "src/math.test.js", "filter": "adds", "timeoutMs": 5000}
|
|
11
11
|
// ← {"event": "test", "name": "math > adds", "status": "passed", …}
|
|
12
|
+
// ← {"event": "output", "stream": "stdout", "test": "math > adds", "text": "hi\n"}
|
|
12
13
|
// ← {"event": "file", "status": "completed", "durationMicros": 1234}
|
|
13
14
|
//
|
|
14
|
-
//
|
|
15
|
-
// batched at the end, so `uf test` can draw progress and `--bail` can stop
|
|
16
|
-
// long run early.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Three decisions worth stating. Results are streamed as they happen rather
|
|
16
|
+
// than batched at the end, so `uf test` can draw progress and `--bail` can stop
|
|
17
|
+
// a long run early. A file that throws while being *imported* is a file result,
|
|
18
|
+
// not a test result: there were no tests to fail, and saying "0 tests" for a
|
|
19
|
+
// module that could not load would be a lie. And the protocol does not share
|
|
20
|
+
// its stream with the tests: a test's own printing becomes an `output` event
|
|
21
|
+
// (`internal/output.js`), so a `console.log` cannot land in the middle of a
|
|
22
|
+
// line `uf` is parsing.
|
|
19
23
|
//
|
|
20
24
|
// This module runs on import by design — it is a process entry point, the way
|
|
21
25
|
// `@uniflowed/vite`'s loaders are.
|
|
22
26
|
|
|
27
|
+
import * as output from "./internal/output.js";
|
|
28
|
+
import { writeChangedSnapshots } from "./internal/snapshot.js";
|
|
23
29
|
import { createInterface } from "node:readline";
|
|
24
|
-
import { pathToFileURL } from "node:url";
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
31
|
|
|
26
32
|
import { reset } from "./internal/registry.js";
|
|
27
33
|
import { run } from "./internal/run.js";
|
|
28
34
|
|
|
29
35
|
/** What `uf` sends for one file. */
|
|
30
36
|
type Request = {|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
readonly file: string,
|
|
38
|
+
readonly filter?: string | null,
|
|
39
|
+
readonly timeoutMs?: number,
|
|
34
40
|
|};
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The protocol's own stdout, and the capture that gave it up.
|
|
44
|
+
*
|
|
45
|
+
* A test can reach `console.log` and `process.stdout.write`, and both used to
|
|
46
|
+
* land in the middle of a line `uf` was parsing. `internal/output.js` takes
|
|
47
|
+
* those over and hands back the real write it replaced, so the protocol has a
|
|
48
|
+
* stream nothing else can reach and a test's printing is still reported —
|
|
49
|
+
* as an `output` event, escaped, whatever it says.
|
|
50
|
+
*
|
|
51
|
+
* At module scope, before a single test file can be imported, so output
|
|
52
|
+
* written while a file is still loading is reported rather than lost.
|
|
53
|
+
*/
|
|
54
|
+
const emit: (chunk: string) => void = output.install((chunk) => {
|
|
55
|
+
write({
|
|
56
|
+
event: "output",
|
|
57
|
+
stream: chunk.stream,
|
|
58
|
+
test: chunk.test,
|
|
59
|
+
text: chunk.text,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function write(event: { readonly [string]: mixed }): void {
|
|
64
|
+
emit(`${JSON.stringify(event)}\n`);
|
|
38
65
|
}
|
|
39
66
|
|
|
40
67
|
/**
|
|
@@ -47,6 +74,7 @@ function write(event: { +[string]: mixed }): void {
|
|
|
47
74
|
async function runFile(request: Request, generation: number): Promise<void> {
|
|
48
75
|
const started = performance.now();
|
|
49
76
|
reset();
|
|
77
|
+
output.startFile();
|
|
50
78
|
|
|
51
79
|
try {
|
|
52
80
|
await import(`${pathToFileURL(request.file).href}?uf-run=${generation}`);
|
|
@@ -63,9 +91,24 @@ async function runFile(request: Request, generation: number): Promise<void> {
|
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
const absolute = fileURLToPath(pathToFileURL(request.file).href);
|
|
95
|
+
await run(
|
|
96
|
+
{ filter: request.filter ?? null, timeoutMs: request.timeoutMs, file: absolute },
|
|
97
|
+
(result) => {
|
|
98
|
+
write({
|
|
99
|
+
event: "test",
|
|
100
|
+
...result.outcome,
|
|
101
|
+
name: result.name,
|
|
102
|
+
line: result.line,
|
|
103
|
+
column: result.column,
|
|
104
|
+
durationMicros: result.durationMicros,
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
// Once, at the end of the file, rather than after each snapshot: a file
|
|
109
|
+
// with forty snapshots would otherwise rewrite its snapshot file forty
|
|
110
|
+
// times, and a crash halfway through would leave a partial one.
|
|
111
|
+
writeChangedSnapshots();
|
|
69
112
|
write({
|
|
70
113
|
event: "file",
|
|
71
114
|
status: "completed",
|
|
@@ -103,7 +146,11 @@ function serve(): void {
|
|
|
103
146
|
try {
|
|
104
147
|
request = JSON.parse(line);
|
|
105
148
|
} catch (error) {
|
|
106
|
-
write({
|
|
149
|
+
write({
|
|
150
|
+
event: "file",
|
|
151
|
+
status: "run-failed",
|
|
152
|
+
message: `malformed request: ${String(error)}`,
|
|
153
|
+
});
|
|
107
154
|
return;
|
|
108
155
|
}
|
|
109
156
|
generation += 1;
|