@alchemy.run/test 2.0.0-beta.85 → 2.0.0-beta.86
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/lib/Alchemy.d.ts +0 -1
- package/lib/Alchemy.d.ts.map +1 -1
- package/lib/Alchemy.js +7 -6
- package/lib/Alchemy.js.map +1 -1
- package/lib/Api.d.ts +2 -1
- package/lib/Api.d.ts.map +1 -1
- package/lib/Api.js.map +1 -1
- package/lib/Bun.d.ts +0 -1
- package/lib/Bun.d.ts.map +1 -1
- package/lib/Bun.js +0 -1
- package/lib/Bun.js.map +1 -1
- package/lib/Capture.d.ts.map +1 -1
- package/lib/Capture.js.map +1 -1
- package/lib/Core.d.ts +15 -1
- package/lib/Core.d.ts.map +1 -1
- package/lib/Core.js +17 -4
- package/lib/Core.js.map +1 -1
- package/lib/DevCli.d.ts.map +1 -1
- package/lib/DevCli.js.map +1 -1
- package/lib/Expect.d.ts.map +1 -1
- package/lib/Expect.js.map +1 -1
- package/lib/FileLog.d.ts.map +1 -1
- package/lib/FileLog.js.map +1 -1
- package/lib/Http.d.ts.map +1 -1
- package/lib/Http.js.map +1 -1
- package/lib/Model.d.ts +15 -2
- package/lib/Model.d.ts.map +1 -1
- package/lib/Model.js.map +1 -1
- package/lib/PlainReporter.d.ts +1 -2
- package/lib/PlainReporter.d.ts.map +1 -1
- package/lib/PlainReporter.js +19 -93
- package/lib/PlainReporter.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js.map +1 -1
- package/lib/Registry.d.ts.map +1 -1
- package/lib/Registry.js.map +1 -1
- package/lib/Reporter.d.ts.map +1 -1
- package/lib/Reporter.js.map +1 -1
- package/lib/Runner.d.ts.map +1 -1
- package/lib/Runner.js +66 -43
- package/lib/Runner.js.map +1 -1
- package/lib/StrayOutput.d.ts.map +1 -1
- package/lib/StrayOutput.js.map +1 -1
- package/lib/Tags.d.ts.map +1 -1
- package/lib/Tags.js.map +1 -1
- package/lib/TestState.d.ts.map +1 -1
- package/lib/TestState.js.map +1 -1
- package/lib/Tui.d.ts.map +1 -1
- package/lib/Tui.js.map +1 -1
- package/lib/Vitest.d.ts.map +1 -1
- package/lib/Vitest.js +0 -1
- package/lib/Vitest.js.map +1 -1
- package/lib/WorktreeStage.d.ts.map +1 -1
- package/lib/WorktreeStage.js.map +1 -1
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
- package/src/Alchemy.ts +9 -7
- package/src/Api.ts +2 -1
- package/src/Bun.ts +0 -2
- package/src/Core.ts +39 -5
- package/src/Model.ts +20 -2
- package/src/PlainReporter.ts +20 -108
- package/src/Runner.ts +62 -42
- package/src/Vitest.ts +0 -1
- package/src/index.ts +1 -0
package/src/PlainReporter.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Reports progress through the collection phase (importing a large suite's
|
|
5
5
|
* files takes tens of seconds before any test can run), then logs one line per
|
|
6
6
|
* test as it finishes, prefixed with a `[done/total]` counter (failures include
|
|
7
|
-
* their error and captured output inline);
|
|
8
|
-
* prints the list of currently-running tests; at the end every failure is
|
|
7
|
+
* their error and captured output inline); at the end every failure is
|
|
9
8
|
* repeated consolidated.
|
|
10
9
|
*/
|
|
11
10
|
import * as Effect from "effect/Effect";
|
|
@@ -53,25 +52,14 @@ const indent = (text: string, prefix = " "): string =>
|
|
|
53
52
|
interface ReporterState {
|
|
54
53
|
readonly hookLogs: Map<string, ReadonlyArray<LogEntry>>;
|
|
55
54
|
readonly failedFiles: Set<string>;
|
|
56
|
-
/**
|
|
57
|
-
* Currently-executing work items: tests AND file-level hooks. Hooks must
|
|
58
|
-
* be tracked too — a `beforeAll` deploy can legitimately run for many
|
|
59
|
-
* minutes with no test starting or finishing, and if the stall report
|
|
60
|
-
* only covered tests, such a run would print nothing at all and read as
|
|
61
|
-
* deadlocked.
|
|
62
|
-
*/
|
|
63
|
-
/** `queued`: started, but still waiting for its `exclusive` run lock. */
|
|
64
|
-
readonly running: Map<string, { label: string; startedAt: number; queued?: boolean }>;
|
|
65
55
|
/** Total tests in the run, known once RunStart arrives. */
|
|
66
56
|
total: number;
|
|
67
57
|
/** Tests that have reported a result so far, and how they went. */
|
|
68
58
|
completed: number;
|
|
69
59
|
passed: number;
|
|
70
60
|
failed: number;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
lastStallKey: string;
|
|
74
|
-
stallTimer: ReturnType<typeof setInterval> | undefined;
|
|
61
|
+
/** Repaints the collection line while files are importing. */
|
|
62
|
+
collectTimer: ReturnType<typeof setInterval> | undefined;
|
|
75
63
|
/** Files discovered for the collection phase. */
|
|
76
64
|
collectTotal: number;
|
|
77
65
|
collectDone: number;
|
|
@@ -128,6 +116,10 @@ const renderCollect = (state: ReporterState, force: boolean): void => {
|
|
|
128
116
|
/** Settle the progress line on the final count so the total cost stays visible. */
|
|
129
117
|
const finishCollect = (state: ReporterState): void => {
|
|
130
118
|
state.collecting = false;
|
|
119
|
+
if (state.collectTimer !== undefined) {
|
|
120
|
+
clearInterval(state.collectTimer);
|
|
121
|
+
state.collectTimer = undefined;
|
|
122
|
+
}
|
|
131
123
|
clearCollectLine(state);
|
|
132
124
|
state.collectCurrent = "";
|
|
133
125
|
renderCollect(state, true);
|
|
@@ -159,44 +151,6 @@ const progress = (state: ReporterState, status: TestResult["status"]): string =>
|
|
|
159
151
|
].join(" ");
|
|
160
152
|
};
|
|
161
153
|
|
|
162
|
-
const STALL_AFTER_MS = 10_000;
|
|
163
|
-
const STALL_LIST_MAX = 25;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Print the currently-running tests when nothing has finished for a while.
|
|
167
|
-
* Printed AT MOST ONCE per distinct running set: while the exact same tests
|
|
168
|
-
* stay stuck, nothing is re-printed — the timestamp on the report tells the
|
|
169
|
-
* reader how long ago it was taken.
|
|
170
|
-
*/
|
|
171
|
-
const printStalled = (state: ReporterState): void => {
|
|
172
|
-
if (state.running.size === 0) return;
|
|
173
|
-
if (Date.now() - state.lastEnd < STALL_AFTER_MS) return;
|
|
174
|
-
const key = [...state.running.entries()]
|
|
175
|
-
.map(([id, item]) => `${id}${item.queued ? " (queued)" : ""}`)
|
|
176
|
-
.sort()
|
|
177
|
-
.join("\n");
|
|
178
|
-
if (key === state.lastStallKey) return;
|
|
179
|
-
state.lastStallKey = key;
|
|
180
|
-
const now = Date.now();
|
|
181
|
-
const timestamp = new Date(now).toTimeString().slice(0, 8);
|
|
182
|
-
const items = [...state.running.values()].sort((a, b) => a.startedAt - b.startedAt);
|
|
183
|
-
const queued = items.filter((item) => item.queued).length;
|
|
184
|
-
const lines = [
|
|
185
|
-
dim(
|
|
186
|
-
`⧗ [${timestamp}] no tests finished in the last 10s — ${items.length - queued} still running` +
|
|
187
|
-
(queued > 0 ? `, ${queued} queued for an exclusive lock:` : ":"),
|
|
188
|
-
),
|
|
189
|
-
];
|
|
190
|
-
for (const item of items.slice(0, STALL_LIST_MAX)) {
|
|
191
|
-
const elapsed = formatDuration(now - item.startedAt);
|
|
192
|
-
lines.push(dim(` ${item.label} (${item.queued ? `queued ${elapsed}` : elapsed})`));
|
|
193
|
-
}
|
|
194
|
-
if (items.length > STALL_LIST_MAX) {
|
|
195
|
-
lines.push(dim(` … ${items.length - STALL_LIST_MAX} more`));
|
|
196
|
-
}
|
|
197
|
-
writeDirect(`${lines.join("\n")}\n`);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
154
|
const onEvent = (event: TestEvent, state: ReporterState): Effect.Effect<void> => {
|
|
201
155
|
switch (event._tag) {
|
|
202
156
|
case "PlanPreview":
|
|
@@ -209,6 +163,13 @@ const onEvent = (event: TestEvent, state: ReporterState): Effect.Effect<void> =>
|
|
|
209
163
|
state.collectTotal = event.files.length;
|
|
210
164
|
state.collectStartedAt = Date.now();
|
|
211
165
|
state.collecting = true;
|
|
166
|
+
if (state.collectTimer === undefined) {
|
|
167
|
+
state.collectTimer = setInterval(() => {
|
|
168
|
+
if (state.collecting) renderCollect(state, false);
|
|
169
|
+
}, 1000);
|
|
170
|
+
// Don't hold the process open once the run's fibers finish.
|
|
171
|
+
(state.collectTimer as unknown as { unref?: () => void }).unref?.();
|
|
172
|
+
}
|
|
212
173
|
return write(dim(`collecting ${event.files.length} test files...`));
|
|
213
174
|
case "FileCollected":
|
|
214
175
|
return Effect.sync(() => {
|
|
@@ -223,36 +184,7 @@ const onEvent = (event: TestEvent, state: ReporterState): Effect.Effect<void> =>
|
|
|
223
184
|
write(dim(`running ${event.tests.length} tests from ${event.files} files\n`)),
|
|
224
185
|
),
|
|
225
186
|
);
|
|
226
|
-
case "TestStart":
|
|
227
|
-
return Effect.sync(() => {
|
|
228
|
-
state.running.set(event.test.id, {
|
|
229
|
-
label: `${event.test.file} > ${event.test.titlePath.join(" > ")}`,
|
|
230
|
-
startedAt: Date.now(),
|
|
231
|
-
queued: true,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
case "TestLocked":
|
|
235
|
-
return Effect.sync(() => {
|
|
236
|
-
const item = state.running.get(event.test.id);
|
|
237
|
-
if (item !== undefined) {
|
|
238
|
-
state.running.set(event.test.id, { label: item.label, startedAt: Date.now() });
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
case "HookStart":
|
|
242
|
-
return Effect.sync(() => {
|
|
243
|
-
state.running.set(`${event.file} :: ${event.hook}`, {
|
|
244
|
-
label: `${event.file} > [${event.hook} hook]`,
|
|
245
|
-
startedAt: Date.now(),
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
case "HookEnd":
|
|
249
|
-
return Effect.sync(() => {
|
|
250
|
-
state.running.delete(`${event.file} :: ${event.hook}`);
|
|
251
|
-
state.lastEnd = Date.now();
|
|
252
|
-
});
|
|
253
187
|
case "TestEnd": {
|
|
254
|
-
state.running.delete(event.test.id);
|
|
255
|
-
state.lastEnd = Date.now();
|
|
256
188
|
const title = `${dim(event.test.file)} ${dim(">")} ${event.test.titlePath.join(` ${dim(">")} `)}`;
|
|
257
189
|
const duration = dim(`(${formatDuration(event.result.durationMs)})`);
|
|
258
190
|
const retries = event.result.retries > 0 ? yellow(` [retried x${event.result.retries}]`) : "";
|
|
@@ -306,6 +238,10 @@ const onEvent = (event: TestEvent, state: ReporterState): Effect.Effect<void> =>
|
|
|
306
238
|
return Effect.void;
|
|
307
239
|
}
|
|
308
240
|
case "RunEnd":
|
|
241
|
+
if (state.collectTimer !== undefined) {
|
|
242
|
+
clearInterval(state.collectTimer);
|
|
243
|
+
state.collectTimer = undefined;
|
|
244
|
+
}
|
|
309
245
|
return printSummary(event.summary, state.hookLogs);
|
|
310
246
|
default:
|
|
311
247
|
return Effect.void;
|
|
@@ -382,14 +318,11 @@ export const PlainReporterLive: Layer.Layer<Reporter> = Layer.sync(Reporter)(()
|
|
|
382
318
|
const state: ReporterState = {
|
|
383
319
|
hookLogs: new Map(),
|
|
384
320
|
failedFiles: new Set(),
|
|
385
|
-
running: new Map(),
|
|
386
321
|
total: 0,
|
|
387
322
|
completed: 0,
|
|
388
323
|
passed: 0,
|
|
389
324
|
failed: 0,
|
|
390
|
-
|
|
391
|
-
lastStallKey: "",
|
|
392
|
-
stallTimer: undefined,
|
|
325
|
+
collectTimer: undefined,
|
|
393
326
|
collectTotal: 0,
|
|
394
327
|
collectDone: 0,
|
|
395
328
|
collectStartedAt: Date.now(),
|
|
@@ -398,29 +331,8 @@ export const PlainReporterLive: Layer.Layer<Reporter> = Layer.sync(Reporter)(()
|
|
|
398
331
|
collectLinePending: false,
|
|
399
332
|
collecting: false,
|
|
400
333
|
};
|
|
401
|
-
// Ticks through both phases: repaints the collection line while files are
|
|
402
|
-
// importing, reports stalled tests once the run is executing.
|
|
403
|
-
const tick = (): void => {
|
|
404
|
-
if (state.collecting) renderCollect(state, false);
|
|
405
|
-
else printStalled(state);
|
|
406
|
-
};
|
|
407
334
|
return {
|
|
408
|
-
emit: (event) =>
|
|
409
|
-
Effect.suspend(() => {
|
|
410
|
-
if (
|
|
411
|
-
(event._tag === "CollectStart" || event._tag === "RunStart") &&
|
|
412
|
-
state.stallTimer === undefined
|
|
413
|
-
) {
|
|
414
|
-
state.stallTimer = setInterval(tick, 1000);
|
|
415
|
-
// Don't hold the process open once the run's fibers finish.
|
|
416
|
-
(state.stallTimer as unknown as { unref?: () => void }).unref?.();
|
|
417
|
-
}
|
|
418
|
-
if (event._tag === "RunEnd" && state.stallTimer !== undefined) {
|
|
419
|
-
clearInterval(state.stallTimer);
|
|
420
|
-
state.stallTimer = undefined;
|
|
421
|
-
}
|
|
422
|
-
return onEvent(event, state);
|
|
423
|
-
}),
|
|
335
|
+
emit: (event) => Effect.suspend(() => onEvent(event, state)),
|
|
424
336
|
waitForExit: () => Effect.void,
|
|
425
337
|
};
|
|
426
338
|
});
|
package/src/Runner.ts
CHANGED
|
@@ -24,7 +24,7 @@ import * as Path from "effect/Path";
|
|
|
24
24
|
import * as Semaphore from "effect/Semaphore";
|
|
25
25
|
import { makeCapture } from "./Capture.ts";
|
|
26
26
|
import { makeFileLog, type FileLog } from "./FileLog.ts";
|
|
27
|
-
import type { Exclusive, FileSuite, Hook, LogEntry, Suite, TestCase } from "./Model.ts";
|
|
27
|
+
import type { Exclusive, FileSuite, Hook, Limit, LogEntry, Suite, TestCase } from "./Model.ts";
|
|
28
28
|
import { containsOnly, forEachTest, makeFileSuite, titlePath } from "./Model.ts";
|
|
29
29
|
import type { TestPlan } from "./Plan.ts";
|
|
30
30
|
import * as Registry from "./Registry.ts";
|
|
@@ -309,18 +309,35 @@ const EXCLUSIVE_PERMITS = 100_000;
|
|
|
309
309
|
|
|
310
310
|
interface LockSpec {
|
|
311
311
|
readonly global: boolean;
|
|
312
|
-
/** Sorted, so holders of several labels acquire them in one order. */
|
|
313
|
-
readonly labels: ReadonlyArray<
|
|
312
|
+
/** Sorted by label, so holders of several labels acquire them in one order. */
|
|
313
|
+
readonly labels: ReadonlyArray<Limit>;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
const lockSpec = (values: ReadonlyArray<Exclusive | undefined>): LockSpec =>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
316
|
+
const lockSpec = (values: ReadonlyArray<Exclusive | undefined>): LockSpec => {
|
|
317
|
+
const flat = values.flatMap((value) => (Array.isArray(value) ? value : [value])) as Array<
|
|
318
|
+
boolean | string | Limit | undefined
|
|
319
|
+
>;
|
|
320
|
+
// A plain label is a one-permit lock; the tightest permit count wins.
|
|
321
|
+
const labels = new Map<string, number>();
|
|
322
|
+
for (const value of flat) {
|
|
323
|
+
const limit =
|
|
324
|
+
typeof value === "string"
|
|
325
|
+
? value === ""
|
|
326
|
+
? undefined
|
|
327
|
+
: { label: value, permits: 1 }
|
|
328
|
+
: typeof value === "object" && value !== null
|
|
329
|
+
? value
|
|
330
|
+
: undefined;
|
|
331
|
+
if (limit === undefined) continue;
|
|
332
|
+
labels.set(limit.label, Math.min(labels.get(limit.label) ?? Infinity, limit.permits));
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
global: flat.some((value) => value === true),
|
|
336
|
+
labels: [...labels]
|
|
337
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
338
|
+
.map(([label, permits]) => ({ label, permits })),
|
|
339
|
+
};
|
|
340
|
+
};
|
|
324
341
|
|
|
325
342
|
interface RunLock {
|
|
326
343
|
readonly withLock: (
|
|
@@ -342,10 +359,11 @@ interface RunLock {
|
|
|
342
359
|
const makeRunLock = Effect.gen(function* () {
|
|
343
360
|
const permits = yield* Semaphore.make(EXCLUSIVE_PERMITS);
|
|
344
361
|
const labels = new Map<string, Semaphore.Semaphore>();
|
|
345
|
-
|
|
362
|
+
// A label's permit count is fixed by its first holder.
|
|
363
|
+
const labelLock = ({ label, permits }: Limit): Semaphore.Semaphore => {
|
|
346
364
|
let lock = labels.get(label);
|
|
347
365
|
if (lock === undefined) {
|
|
348
|
-
lock = Semaphore.makeUnsafe(1);
|
|
366
|
+
lock = Semaphore.makeUnsafe(Math.max(1, Math.floor(permits)));
|
|
349
367
|
labels.set(label, lock);
|
|
350
368
|
}
|
|
351
369
|
return lock;
|
|
@@ -702,40 +720,19 @@ const runTest = Effect.fn(function* (test: TestCase, ctx: ExecContext) {
|
|
|
702
720
|
let exit = yield* runAttempt();
|
|
703
721
|
while (attemptNeedsRetry(exit, test.fails) && retries < (test.retry ?? ctx.options.retry)) {
|
|
704
722
|
retries++;
|
|
723
|
+
// The retry replaces this attempt's result; keep its error in the run
|
|
724
|
+
// log, or a test that passes on retry hides what failed (and leaked).
|
|
725
|
+
ctx.fileLog.appendTestLine(`${meta.file} > ${meta.titlePath.join(" > ")}`, {
|
|
726
|
+
level: "ERROR",
|
|
727
|
+
message: `attempt ${retries} failed, retrying: ${attemptOutcome(exit, test).error ?? "unknown"}`,
|
|
728
|
+
time: new Date(),
|
|
729
|
+
});
|
|
705
730
|
// Clear IN PLACE — TestStart handed this array's reference out.
|
|
706
731
|
ctx.capture.release(logs);
|
|
707
732
|
exit = yield* runAttempt();
|
|
708
733
|
}
|
|
709
734
|
|
|
710
|
-
|
|
711
|
-
let error: string | undefined;
|
|
712
|
-
if (wasInterrupted(exit)) {
|
|
713
|
-
status = "fail";
|
|
714
|
-
error = "killed (interrupted by user)";
|
|
715
|
-
} else if (Exit.isFailure(exit)) {
|
|
716
|
-
status = "fail";
|
|
717
|
-
error = prettyCause(exit.cause);
|
|
718
|
-
} else {
|
|
719
|
-
const failedHook = hookError(exit.value);
|
|
720
|
-
const bodyExit = exit.value.body;
|
|
721
|
-
if (failedHook !== undefined) {
|
|
722
|
-
status = "fail";
|
|
723
|
-
error = failedHook;
|
|
724
|
-
} else if (test.fails && bodyExit !== undefined) {
|
|
725
|
-
if (Exit.isFailure(bodyExit)) {
|
|
726
|
-
status = "pass";
|
|
727
|
-
} else {
|
|
728
|
-
status = "fail";
|
|
729
|
-
error = "expected test to fail, but it passed";
|
|
730
|
-
}
|
|
731
|
-
} else if (bodyExit !== undefined && Exit.isSuccess(bodyExit)) {
|
|
732
|
-
status = "pass";
|
|
733
|
-
} else {
|
|
734
|
-
status = "fail";
|
|
735
|
-
error = bodyExit === undefined ? "test body did not run" : prettyCause(bodyExit.cause);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
735
|
+
const { status, error } = attemptOutcome(exit, test);
|
|
739
736
|
if (ctx.interactive) ctx.completed.add(meta.id);
|
|
740
737
|
const result: TestResult = { status, durationMs, error, logs, retries };
|
|
741
738
|
ctx.results.set(meta.id, {
|
|
@@ -746,6 +743,29 @@ const runTest = Effect.fn(function* (test: TestCase, ctx: ExecContext) {
|
|
|
746
743
|
if (!ctx.interactive && status !== "fail") ctx.capture.release(logs);
|
|
747
744
|
});
|
|
748
745
|
|
|
746
|
+
/** The status and error a finished attempt reports. */
|
|
747
|
+
const attemptOutcome = (
|
|
748
|
+
exit: Exit.Exit<TestAttempt, unknown>,
|
|
749
|
+
test: TestCase,
|
|
750
|
+
): { status: TestResult["status"]; error: string | undefined } => {
|
|
751
|
+
if (wasInterrupted(exit)) return { status: "fail", error: "killed (interrupted by user)" };
|
|
752
|
+
if (Exit.isFailure(exit)) return { status: "fail", error: prettyCause(exit.cause) };
|
|
753
|
+
const failedHook = hookError(exit.value);
|
|
754
|
+
const bodyExit = exit.value.body;
|
|
755
|
+
if (failedHook !== undefined) return { status: "fail", error: failedHook };
|
|
756
|
+
if (test.fails && bodyExit !== undefined) {
|
|
757
|
+
return Exit.isFailure(bodyExit)
|
|
758
|
+
? { status: "pass", error: undefined }
|
|
759
|
+
: { status: "fail", error: "expected test to fail, but it passed" };
|
|
760
|
+
}
|
|
761
|
+
if (bodyExit !== undefined && Exit.isSuccess(bodyExit))
|
|
762
|
+
return { status: "pass", error: undefined };
|
|
763
|
+
return {
|
|
764
|
+
status: "fail",
|
|
765
|
+
error: bodyExit === undefined ? "test body did not run" : prettyCause(bodyExit.cause),
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
|
|
749
769
|
/** Fail every (non-skipped) test in a subtree without running it. */
|
|
750
770
|
const failSubtree = Effect.fn(function* (suite: Suite, ctx: ExecContext, error: string) {
|
|
751
771
|
const tests: Array<TestCase> = [];
|
package/src/Vitest.ts
CHANGED