@systemfsoftware/vitest 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +203 -0
- package/README.md +236 -0
- package/dist/TestClock.d.ts +6 -0
- package/dist/TestClock.mjs +6 -0
- package/dist/compat.d.ts +11 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.mjs +1523 -0
- package/dist/refusals-BQtDszyU.mjs +32 -0
- package/dist/refusals-_6e3GzVK.d.ts +37 -0
- package/dist/refusals.d.ts +3 -0
- package/dist/refusals.mjs +3 -0
- package/dist/utils.d.ts +197 -0
- package/dist/utils.mjs +227 -0
- package/package.json +75 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
|
|
2
|
+
import { a as refuseBareEffect, c as refuseNoAssertion, i as refuseAsync, l as refusePositionalProp, n as narrowVacuous, r as presenceMessage, s as refuseHook, t as narrowNegated, u as refuseUnprovided } from "./refusals-BQtDszyU.mjs";
|
|
3
|
+
import * as V from "vitest";
|
|
4
|
+
import { inject } from "vitest";
|
|
5
|
+
import * as Function from "effect/Function";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as Fiber from "effect/Fiber";
|
|
8
|
+
import * as Equal from "effect/Equal";
|
|
9
|
+
import { Schema } from "effect";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Cause from "effect/Cause";
|
|
12
|
+
import * as Duration from "effect/Duration";
|
|
13
|
+
import * as Exit from "effect/Exit";
|
|
14
|
+
import * as Layer from "effect/Layer";
|
|
15
|
+
import * as Schedule from "effect/Schedule";
|
|
16
|
+
import * as Scheduler from "effect/Scheduler";
|
|
17
|
+
import * as Schema$1 from "effect/Schema";
|
|
18
|
+
import * as Scope from "effect/Scope";
|
|
19
|
+
import * as TestClock from "effect/testing/TestClock";
|
|
20
|
+
import * as TestConsole from "effect/testing/TestConsole";
|
|
21
|
+
import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary";
|
|
22
|
+
import * as Data from "effect/Data";
|
|
23
|
+
import * as Clock from "effect/Clock";
|
|
24
|
+
export * from "vitest";
|
|
25
|
+
//#region src/internal/binding.ts
|
|
26
|
+
/**
|
|
27
|
+
* The run a test's fibers execute in. Effect code reads it through
|
|
28
|
+
* `Fiber.getCurrent`, so no node-only store is needed (KTD4).
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
const Running = Context.Reference("effect-vitest/binding", { defaultValue: () => void 0 });
|
|
33
|
+
/** @internal */
|
|
34
|
+
const currentRun = () => Fiber.getCurrent()?.getRef(Running);
|
|
35
|
+
let syncRun = void 0;
|
|
36
|
+
/** @internal */
|
|
37
|
+
const withSyncRun = Function.dual(2, (run, body) => {
|
|
38
|
+
const previous = syncRun;
|
|
39
|
+
syncRun = run;
|
|
40
|
+
try {
|
|
41
|
+
return body();
|
|
42
|
+
} finally {
|
|
43
|
+
syncRun = previous;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* The run the current execution belongs to: the fiber's binding first, then the
|
|
48
|
+
* synchronous slot for code that runs outside a fiber.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const lookupRun = () => currentRun() ?? syncRun;
|
|
53
|
+
const NO_ERRORS = [];
|
|
54
|
+
/** @internal */
|
|
55
|
+
const errorCount = (ctx) => presentErrors(ctx.task.result?.errors).length;
|
|
56
|
+
const presentErrors = (errors) => errors === void 0 ? NO_ERRORS : errors;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/internal/equal.ts
|
|
59
|
+
let registered = false;
|
|
60
|
+
/** @internal */
|
|
61
|
+
const registerEqualTester = () => {
|
|
62
|
+
if (registered) return;
|
|
63
|
+
registered = true;
|
|
64
|
+
V.expect.addEqualityTesters([equalTester]);
|
|
65
|
+
};
|
|
66
|
+
const equalTester = (first, second) => equalTogether(first, second) ? true : void 0;
|
|
67
|
+
const equalTogether = (first, second) => bothEqualable(first, second) && Equal.equals(first, second);
|
|
68
|
+
const bothEqualable = (first, second) => Equal.isEqual(first) && Equal.isEqual(second);
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/internal/errors.schema.ts
|
|
71
|
+
/** @internal */
|
|
72
|
+
var NonBooleanVerdict = class extends Schema.TaggedError()("NonBooleanVerdict", { detail: Schema.String }) {
|
|
73
|
+
get message() {
|
|
74
|
+
return this.detail;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/** @internal */
|
|
78
|
+
var LeakedState = class extends Schema.TaggedError()("LeakedState", { detail: Schema.String }) {
|
|
79
|
+
get message() {
|
|
80
|
+
return this.detail;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** @internal */
|
|
84
|
+
var AfterFailedExpect = class extends Schema.TaggedError()("AfterFailedExpect", { detail: Schema.String }) {
|
|
85
|
+
get message() {
|
|
86
|
+
return this.detail;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/** @internal */
|
|
90
|
+
var Slop = class extends Schema.TaggedError()("Slop", { detail: Schema.String }) {
|
|
91
|
+
get message() {
|
|
92
|
+
return this.detail;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/** @internal */
|
|
96
|
+
var InvalidBudget = class extends Schema.TaggedError()("InvalidBudget", { detail: Schema.String }) {
|
|
97
|
+
get message() {
|
|
98
|
+
return this.detail;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/internal/owned.ts
|
|
103
|
+
/** @internal */
|
|
104
|
+
const OwnedAssertions = Context.Reference("effect-vitest/owned", { defaultValue: () => false });
|
|
105
|
+
/** @internal */
|
|
106
|
+
const isOwned = () => Fiber.getCurrent()?.getRef(OwnedAssertions) === true;
|
|
107
|
+
/** @internal */
|
|
108
|
+
const owned$1 = (self) => Effect.provideService(self, OwnedAssertions, true);
|
|
109
|
+
/**
|
|
110
|
+
* Counts an Effect-native check as an assertion of the run it happens in, so the run's own gate sees it.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
const recordAssertion$1 = () => {
|
|
115
|
+
const run = lookupRun();
|
|
116
|
+
if (run !== void 0) bump(run);
|
|
117
|
+
};
|
|
118
|
+
const bump = (run) => {
|
|
119
|
+
const { expect } = run.ctx;
|
|
120
|
+
expect.setState({ assertionCalls: expect.getState().assertionCalls + 1 });
|
|
121
|
+
};
|
|
122
|
+
const bindingOf = (running, owned) => ({ bind: (self) => self.pipe(Effect.provideService(Running, running), Effect.provideService(OwnedAssertions, owned)) });
|
|
123
|
+
/**
|
|
124
|
+
* The run binding of the test the caller is executing in.
|
|
125
|
+
*
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
const captureRunBinding$1 = Effect.withFiber((fiber) => Effect.succeed(bindingOf(fiber.getRef(Running), fiber.getRef(OwnedAssertions))));
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/internal/expect.ts
|
|
131
|
+
registerEqualTester();
|
|
132
|
+
const refuse = (detail) => {
|
|
133
|
+
throw new Slop({ detail });
|
|
134
|
+
};
|
|
135
|
+
const narrowedNames = (negated) => negated ? narrowNegated : narrowVacuous;
|
|
136
|
+
const narrowedName = (property, negated) => namedMatch(property, narrowedNames(negated));
|
|
137
|
+
const namedMatch = (property, names) => typeof property === "string" ? foundMatch(property, names) : void 0;
|
|
138
|
+
const foundMatch = (property, names) => names.find((name) => name === property);
|
|
139
|
+
const refuseNarrowed = (name) => () => refuse(presenceMessage(name));
|
|
140
|
+
const refusedMember = (property, negated) => {
|
|
141
|
+
const name = narrowedName(property, negated);
|
|
142
|
+
return name === void 0 ? void 0 : refuseNarrowed(name);
|
|
143
|
+
};
|
|
144
|
+
const memberAt = (target, property) => Reflect.get(target, property, target);
|
|
145
|
+
/**
|
|
146
|
+
* The narrowed names answer with the refusal, `not` re-enters the guard with the negation flipped, and every
|
|
147
|
+
* other member is Vitest's own, handed over unbound so Chai's property accessors keep working.
|
|
148
|
+
*/
|
|
149
|
+
const guardOf = (assertion, negated) => new Proxy(assertion, guardHandler(negated));
|
|
150
|
+
const guardHandler = (negated) => ({ get: (target, property) => guardedMember(target, negated, property) });
|
|
151
|
+
const guardedMember = (target, negated, property) => property === "not" ? guardOf(target.not, !negated) : forwardedMember(target, negated, property);
|
|
152
|
+
const forwardedMember = (target, negated, property) => refusedMember(property, negated) ?? memberAt(target, property);
|
|
153
|
+
const plainAssertion = (actual, message) => counted(guardOf(V.expect(actual, message), false));
|
|
154
|
+
const countsInThisRun = () => lookupRun()?.shadow !== true;
|
|
155
|
+
/**
|
|
156
|
+
* A check outside a soft run counts on the run it happens in: an `owned` region's checks throw, and a
|
|
157
|
+
* check outside every run has no gate to satisfy.
|
|
158
|
+
*/
|
|
159
|
+
const counted = (assertion) => {
|
|
160
|
+
if (countsInThisRun()) recordAssertion$1();
|
|
161
|
+
return assertion;
|
|
162
|
+
};
|
|
163
|
+
const softAssertion = (running, actual, message) => guardOf(running.ctx.expect.soft(actual, message), false);
|
|
164
|
+
const isSoftRun = (running) => running.shadow === false && isOwned() === false;
|
|
165
|
+
const liveAssertion = (running, actual, message) => isSoftRun(running) ? softAssertion(running, actual, message) : plainAssertion(actual, message);
|
|
166
|
+
/**
|
|
167
|
+
* A check inside a run is soft: it records the failure and the test stops at the next step. The second run
|
|
168
|
+
* and an `owned` region throw, so their own handler sees the failure.
|
|
169
|
+
*/
|
|
170
|
+
const assertionFor = (actual, message) => {
|
|
171
|
+
const running = lookupRun();
|
|
172
|
+
return running === void 0 ? plainAssertion(actual, message) : liveAssertion(running, actual, message);
|
|
173
|
+
};
|
|
174
|
+
Function.dual(2, assertionFor);
|
|
175
|
+
const messageOf = (value) => typeof value === "string" ? value : void 0;
|
|
176
|
+
const applyCheck = (args) => assertionFor(args[0], messageOf(args[1]));
|
|
177
|
+
/** The statics are Vitest's; only `soft` is the fork's, and soft is what a check already is inside a run. */
|
|
178
|
+
const makeExpect = () => new Proxy(V.expect, {
|
|
179
|
+
get: (target, property) => property === "soft" ? assertionFor : memberAt(target, property),
|
|
180
|
+
apply: (_target, _thisArg, args) => applyCheck(args)
|
|
181
|
+
});
|
|
182
|
+
/**
|
|
183
|
+
* The fork's `expect`: soft within one Effect step, refusing the slop forms.
|
|
184
|
+
*
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
const expect$1 = makeExpect();
|
|
188
|
+
const refuseHookNow = () => refuse(refuseHook);
|
|
189
|
+
const takesHook = (args) => typeof args[0] === "function";
|
|
190
|
+
const refuseHookPair = (fn, timeout) => {
|
|
191
|
+
return refuseHookNow();
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
const beforeEach$1 = Function.dual(takesHook, refuseHookPair);
|
|
197
|
+
/**
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
const afterEach$1 = Function.dual(takesHook, refuseHookPair);
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
const beforeAll$1 = Function.dual(takesHook, V.beforeAll);
|
|
205
|
+
/**
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
const afterAll$1 = Function.dual(takesHook, V.afterAll);
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/internal/property/defaults.ts
|
|
211
|
+
/** @internal The `test.provide` key the shared config fills with this run's tier's check options. */
|
|
212
|
+
const checkDefaultsKey = "@systemfsoftware/vitest:property-check";
|
|
213
|
+
/** @internal The configured property-check defaults, or `undefined` when the run provided none. */
|
|
214
|
+
const providedCheckDefaults = () => inject(checkDefaultsKey);
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/internal/property/coverage.ts
|
|
217
|
+
/**
|
|
218
|
+
* @internal The R14 coverage classes: labelled input predicates, each with a minimum share of runs.
|
|
219
|
+
*
|
|
220
|
+
* A class fails only when QuickCheck's sequential test (`stdConfidence`) is confident its share is below
|
|
221
|
+
* the minimum, and is satisfied once the test is confident the share is at least 0.9 of the minimum
|
|
222
|
+
* (`TOLERANCE` at a certainty of 10^9). The tolerance is what makes the test terminate: a class sitting
|
|
223
|
+
* exactly at its minimum would otherwise draw forever.
|
|
224
|
+
*/
|
|
225
|
+
const CERTAINTY = 10 ** 9;
|
|
226
|
+
const TOLERANCE = .9;
|
|
227
|
+
const FIRST_BATCH = 64;
|
|
228
|
+
const BATCH_CAP = 4096;
|
|
229
|
+
const DRAW_CAP = 1e5;
|
|
230
|
+
const FAILURE_HEAD = "coverage";
|
|
231
|
+
/** The certainty's two-sided tail probability: `alpha / 2` for `alpha = 1 / CERTAINTY`. */
|
|
232
|
+
const TAIL_PROBABILITY = 1 / (2 * CERTAINTY);
|
|
233
|
+
const C1 = -.007784894002430293;
|
|
234
|
+
const C2 = -.3223964580411365;
|
|
235
|
+
const C3 = -2.400758277161838;
|
|
236
|
+
const C4 = -2.549732539343734;
|
|
237
|
+
const C5 = 4.374664141464968;
|
|
238
|
+
const C6 = 2.938163982698783;
|
|
239
|
+
const D1 = .007784695709041462;
|
|
240
|
+
const D2 = .3224671290700398;
|
|
241
|
+
const D3 = 2.445134137142996;
|
|
242
|
+
const D4 = 3.754408661907416;
|
|
243
|
+
const TAIL_NUMERATOR = [
|
|
244
|
+
C1,
|
|
245
|
+
C2,
|
|
246
|
+
C3,
|
|
247
|
+
C4,
|
|
248
|
+
C5,
|
|
249
|
+
C6
|
|
250
|
+
];
|
|
251
|
+
const TAIL_DENOMINATOR = [
|
|
252
|
+
D1,
|
|
253
|
+
D2,
|
|
254
|
+
D3,
|
|
255
|
+
D4
|
|
256
|
+
];
|
|
257
|
+
/** Horner's rule over a coefficient list, highest power first. */
|
|
258
|
+
const poly = (coefficients, x) => coefficients.reduceRight((total, coefficient) => total * x + coefficient, 0);
|
|
259
|
+
const tailSpread = (p) => Math.sqrt(-2 * Math.log(p));
|
|
260
|
+
const tailInverse = (spread) => poly(TAIL_NUMERATOR, spread) / poly(TAIL_DENOMINATOR, spread);
|
|
261
|
+
/** The inverse standard-normal CDF at the tail probability the certainty asks for. */
|
|
262
|
+
const TAIL_Z = tailInverse(tailSpread(TAIL_PROBABILITY));
|
|
263
|
+
const wilson = (hits, runs, z) => {
|
|
264
|
+
const share = hits / runs;
|
|
265
|
+
const zz = z * z;
|
|
266
|
+
return (share + zz / (2 * runs) + z * Math.sqrt(share * (1 - share) / runs + zz / (4 * runs * runs))) / (1 + zz / runs);
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Thrown when a coverage class is confidently below its minimum share of runs.
|
|
270
|
+
*
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
var CoverageBelowMinimum = class extends Data.Error {};
|
|
274
|
+
const wilsonLower = (entry, runs) => wilson(entry.hits, runs, -TAIL_Z);
|
|
275
|
+
const wilsonUpper = (entry, runs) => wilson(entry.hits, runs, TAIL_Z);
|
|
276
|
+
const covered = (entry, runs) => wilsonLower(entry, runs) >= TOLERANCE * entry.minimum;
|
|
277
|
+
const uncovered = (entry, runs) => wilsonUpper(entry, runs) < entry.minimum;
|
|
278
|
+
const decided = (entry, runs) => covered(entry, runs) || uncovered(entry, runs);
|
|
279
|
+
const settled = (entry, runs) => runs === 0 ? false : decided(entry, runs);
|
|
280
|
+
const entriesOf = (classes) => Object.keys(classes).map((label) => entryAt(classes, label));
|
|
281
|
+
const entryAt = (classes, label) => [label, classOf(classes, label)];
|
|
282
|
+
const classOf = (classes, label) => classes[label] ?? {
|
|
283
|
+
hits: 0,
|
|
284
|
+
minimum: 0
|
|
285
|
+
};
|
|
286
|
+
const judgeOf = (classes, runs) => ({
|
|
287
|
+
classes: new Map(entriesOf(classes)),
|
|
288
|
+
runs
|
|
289
|
+
});
|
|
290
|
+
const openLabels = (judge) => [...judge.classes].filter(([, entry]) => !settled(entry, judge.runs)).map(([label]) => label);
|
|
291
|
+
const failedEntries = (judge) => [...judge.classes].filter(([, entry]) => uncovered(entry, judge.runs));
|
|
292
|
+
const describeShare = (hits, runs) => `${String(hits)}/${String(runs)}`;
|
|
293
|
+
const describeFailure = (label, entry, runs) => `${FAILURE_HEAD} ${label}: ${describeShare(entry.hits, runs)} of runs, below the required ${String(entry.minimum)}`;
|
|
294
|
+
const reportFailures = (judge) => failedEntries(judge).map(([label, entry]) => describeFailure(label, entry, judge.runs)).join("\n");
|
|
295
|
+
/** @internal */
|
|
296
|
+
const countHit = Function.dual(2, (classes, label) => {
|
|
297
|
+
const prior = classes.get(label);
|
|
298
|
+
if (prior !== void 0) classes.set(label, {
|
|
299
|
+
hits: prior.hits + 1,
|
|
300
|
+
minimum: prior.minimum
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
const countLabels = (classes, labels) => {
|
|
304
|
+
for (const label of labels) countHit(classes, label);
|
|
305
|
+
return classes;
|
|
306
|
+
};
|
|
307
|
+
const countDraws = (classes, drawn) => drawn.reduce(countLabels, new Map(classes));
|
|
308
|
+
const withDraws = (judge, drawn) => ({
|
|
309
|
+
classes: countDraws(judge.classes, drawn),
|
|
310
|
+
runs: judge.runs + drawn.length
|
|
311
|
+
});
|
|
312
|
+
const drawBatch = (judge, draw, batch) => withDraws(judge, draw.more(sizeOf(judge, batch)));
|
|
313
|
+
const sizeOf = (judge, batch) => Math.min(batch, DRAW_CAP - judge.runs);
|
|
314
|
+
const grown = (batch) => Math.min(batch * 2, BATCH_CAP);
|
|
315
|
+
const keepDrawing = (judge) => openLabels(judge).length > 0 && judge.runs < DRAW_CAP;
|
|
316
|
+
const toppedUp = (judge, draw, batch) => keepDrawing(judge) ? toppedUp(drawBatch(judge, draw, batch), draw, grown(batch)) : judge;
|
|
317
|
+
const reportOf$1 = (judge) => failedEntries(judge).length === 0 ? void 0 : reportFailures(judge);
|
|
318
|
+
/**
|
|
319
|
+
* Runs R14's sequential draw: tops up the undecided classes until every class is decided or the draw cap
|
|
320
|
+
* is reached, then reports the confidently under-covered classes, naming each label and its observed share.
|
|
321
|
+
*
|
|
322
|
+
* @internal
|
|
323
|
+
*/
|
|
324
|
+
const judgeCoverage = (options) => reportOf$1(toppedUp(judgeOf(options.classes, options.runs), options.draw, FIRST_BATCH));
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/internal/property/error.schema.ts
|
|
327
|
+
/**
|
|
328
|
+
* Thrown at file end when no property in the file refuted a subject's constant impostor (R12).
|
|
329
|
+
*
|
|
330
|
+
* @internal
|
|
331
|
+
*/
|
|
332
|
+
var VacuousProperty = class extends Schema$1.TaggedError()("VacuousProperty", { detail: Schema$1.String }) {
|
|
333
|
+
get message() {
|
|
334
|
+
return this.detail;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Thrown when a law kind is given a record `of`, which cannot be spread into its subject.
|
|
339
|
+
*
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
var NonTupleOf = class extends Schema$1.TaggedError()("NonTupleOf", { detail: Schema$1.String }) {
|
|
343
|
+
get message() {
|
|
344
|
+
return this.detail;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/internal/property/impostor.ts
|
|
349
|
+
const SUBJECT_KEY = "the subject";
|
|
350
|
+
const NO_VALUE = "";
|
|
351
|
+
const isCallable = (value) => typeof value === "function";
|
|
352
|
+
const isCallableSubject = (target) => typeof target === "function";
|
|
353
|
+
const describeValue = (value) => isCallable(value) ? "a function" : String(value);
|
|
354
|
+
const describeEntry = (key, value) => `${key} always returned ${describeValue(value)}`;
|
|
355
|
+
const emptyState = () => ({ outputs: /* @__PURE__ */ new Map() });
|
|
356
|
+
const frozenAt = (state, key, compute) => state.outputs.has(key) ? state.outputs.get(key) : remember(state, key, compute());
|
|
357
|
+
const remember = (state, key, value) => {
|
|
358
|
+
state.outputs.set(key, value);
|
|
359
|
+
return value;
|
|
360
|
+
};
|
|
361
|
+
const callOutput = (state, fn, thisArg, args) => frozenAt(state, SUBJECT_KEY, () => Reflect.apply(fn, thisArg, args));
|
|
362
|
+
const memberWrapper = (state, key, fn) => (...values) => frozenAt(state, key, () => Reflect.apply(fn, void 0, values));
|
|
363
|
+
const memberWrapperOf = (state, fn, key) => fn === void 0 ? void 0 : memberWrapper(state, key, fn);
|
|
364
|
+
const appliedImpostor = (state, target, thisArg, args) => isCallableSubject(target) ? callOutput(state, target, thisArg, args) : void 0;
|
|
365
|
+
const memberImpostor = (state, target, key) => memberWrapperOf(state, target[key], key);
|
|
366
|
+
const readImpostor = (state, target, property) => isCallableSubject(target) ? Reflect.get(target, property, target) : memberRead(state, target, property);
|
|
367
|
+
const memberRead = (state, target, property) => typeof property === "string" ? memberImpostor(state, target, property) : void 0;
|
|
368
|
+
const impostorHandler = (state) => ({
|
|
369
|
+
apply: (target, thisArg, args) => appliedImpostor(state, target, thisArg, args),
|
|
370
|
+
get: (target, property) => readImpostor(state, target, property)
|
|
371
|
+
});
|
|
372
|
+
const describeFrozen = (state) => [...state.outputs].map(([key, value]) => describeEntry(key, value)).join("; ");
|
|
373
|
+
/**
|
|
374
|
+
* A constant impostor of `subject`: a function returns its first output forever, and so does each member
|
|
375
|
+
* of a record.
|
|
376
|
+
*
|
|
377
|
+
* @internal
|
|
378
|
+
*/
|
|
379
|
+
const impostorOf = (subject) => {
|
|
380
|
+
const state = emptyState();
|
|
381
|
+
return {
|
|
382
|
+
impostor: new Proxy(subject, impostorHandler(state)),
|
|
383
|
+
frozen: () => describeFrozen(state)
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
/** @internal */
|
|
387
|
+
const subjectLabel = (subject) => isCallableSubject(subject) ? `a function of ${String(subject.length)} argument(s)` : `a record of ${Object.keys(subject).sort().join(", ")}`;
|
|
388
|
+
const judgeFor = (subject) => ({
|
|
389
|
+
label: subjectLabel(subject),
|
|
390
|
+
properties: [],
|
|
391
|
+
refuters: [],
|
|
392
|
+
frozen: NO_VALUE,
|
|
393
|
+
runs: 0
|
|
394
|
+
});
|
|
395
|
+
const trackProperty = (judge, property) => judge.properties.includes(property) ? judge : {
|
|
396
|
+
...judge,
|
|
397
|
+
properties: [...judge.properties, property]
|
|
398
|
+
};
|
|
399
|
+
const trackRefuter = (judge, property) => judge.refuters.includes(property) ? judge : {
|
|
400
|
+
...judge,
|
|
401
|
+
refuters: [...judge.refuters, property]
|
|
402
|
+
};
|
|
403
|
+
const trackVerdict = (judge, property, verdict) => {
|
|
404
|
+
const named = trackProperty(judge, property);
|
|
405
|
+
return verdict.refuted ? trackRefuter(named, property) : {
|
|
406
|
+
...named,
|
|
407
|
+
frozen: verdict.frozen,
|
|
408
|
+
runs: verdict.runs
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
const recordJudge = (state, subject, property, verdict) => {
|
|
412
|
+
const prior = state.judges.get(subject) ?? judgeFor(subject);
|
|
413
|
+
state.judges.set(subject, trackVerdict(prior, property, verdict));
|
|
414
|
+
};
|
|
415
|
+
const isVacuous = (judge) => judge.refuters.length === 0;
|
|
416
|
+
const describeVacuous = (judge) => `${judge.properties.join(", ")}: no property in this file refuted the constant impostor of this subject (${judge.label}), so nothing here pins it down. It held for ${judge.runs} run(s) against a fake that ${judge.frozen === NO_VALUE ? "was never called" : judge.frozen}, whatever the input. Laws that only relate outputs to each other (additivity, idempotence, commutativity, round trips through the subject) hold for such constants. Pin the output to the input: compare against an independent model (\`subject(x)\` equals a straightforward reimplementation), or conjoin a base case (\`subject([one])\` equals its known value). Also check that the body calls the \`subject\` it was given, not the imported implementation.`;
|
|
417
|
+
const finaliseJudges = (state, onProblems) => {
|
|
418
|
+
const vacuous = [...state.judges.values()].filter(isVacuous).map(describeVacuous);
|
|
419
|
+
if (vacuous.length === 0) return;
|
|
420
|
+
throw onProblems(vacuous.join("\n\n"));
|
|
421
|
+
};
|
|
422
|
+
const ledgerOf = (state, onProblems) => ({
|
|
423
|
+
record: (subject, property, verdict) => recordJudge(state, subject, property, verdict),
|
|
424
|
+
finalise: () => finaliseJudges(state, onProblems)
|
|
425
|
+
});
|
|
426
|
+
/**
|
|
427
|
+
* Collects the impostor verdicts of every property in the file and decides, at file end, which subjects
|
|
428
|
+
* nothing refuted.
|
|
429
|
+
*
|
|
430
|
+
* @internal
|
|
431
|
+
*/
|
|
432
|
+
const makeFileLedger = (onProblems) => ledgerOf({ judges: /* @__PURE__ */ new Map() }, onProblems);
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/internal/property/kinds.ts
|
|
435
|
+
/**
|
|
436
|
+
* @internal The R13 law kinds: `model`, `metamorphic`, `roundTrip` and `invariant` are sugar over the
|
|
437
|
+
* gated property (R11/R12); `idempotent` and `deterministic` are declared exempt — they skip the impostor
|
|
438
|
+
* gate, are reported as exempt, and are the only exemptions.
|
|
439
|
+
*
|
|
440
|
+
* Each kind owns the comparison, so there is nowhere to write `return true`: the author hands over
|
|
441
|
+
* functions, and the kind relates their outputs with Effect's structural `Equal`.
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* The generated values as the argument list a law subject is called with.
|
|
445
|
+
*
|
|
446
|
+
* @internal
|
|
447
|
+
*/
|
|
448
|
+
const spreadValues = (values) => Array.isArray(values) ? values : refuseRecordOf();
|
|
449
|
+
const refuseRecordOf = () => {
|
|
450
|
+
throw new NonTupleOf({ detail: "a law kind spreads the generated values into its subject, so `of` must be a tuple; a record `of` holds functions of the record, which only it.prop reaches" });
|
|
451
|
+
};
|
|
452
|
+
const call = (subject, values) => subject(...spreadValues(values));
|
|
453
|
+
const equalOutputs = (original, transformed) => Equal.equals(original, transformed);
|
|
454
|
+
/** @internal */
|
|
455
|
+
const modelHolds = (oracle) => (subject, values) => Equal.equals(call(subject, values), call(oracle, values));
|
|
456
|
+
/** @internal */
|
|
457
|
+
const metamorphicHolds = (relation) => (subject, values) => (relation.relate ?? equalOutputs)(call(subject, values), call(subject, relation.transform(...spreadValues(values))));
|
|
458
|
+
/** @internal */
|
|
459
|
+
const roundTripHolds = (decode) => (subject, values) => Equal.equals(decode(call(subject, values)), values);
|
|
460
|
+
/** @internal */
|
|
461
|
+
const invariantHolds = (holds) => (subject, values) => holds(call(subject, values), ...spreadValues(values));
|
|
462
|
+
const firstOf = (values) => values[0];
|
|
463
|
+
const sameTwice = (subject, value) => value === void 0 ? false : Equal.equals(subject(subject(value)), subject(value));
|
|
464
|
+
/** @internal */
|
|
465
|
+
const idempotentHolds = () => (subject, values) => sameTwice(subject, firstOf(spreadValues(values)));
|
|
466
|
+
/** @internal */
|
|
467
|
+
const deterministicHolds = () => (subject, values) => Equal.equals(call(subject, values), call(subject, values));
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/internal/property/engine.ts
|
|
470
|
+
/**
|
|
471
|
+
* @internal The lawful property engine (R11-R15): a property names the function under test, takes a budget
|
|
472
|
+
* that merges its own fields over the configured default over `runs: 100`, accepts only a literal boolean
|
|
473
|
+
* verdict (or an `Effect` of one), judges coverage classes with QuickCheck's sequential test, and defers the
|
|
474
|
+
* constant-impostor verdict to the end of the file.
|
|
475
|
+
*
|
|
476
|
+
* `of` accepts what upstream accepts — a tuple or record of Schemas or Arbitraries (KTD10) — and `holds`
|
|
477
|
+
* receives the generated values exactly as drawn, typed by the gens that produced them.
|
|
478
|
+
*/
|
|
479
|
+
const REWRITE = "it.prop(name, { of, subject, runs }, holds)";
|
|
480
|
+
const DEFAULT_RUNS = 100;
|
|
481
|
+
const isPositiveInteger = (runs) => Number.isInteger(runs) && runs > 0;
|
|
482
|
+
const describeRuns = (runs) => Number.isInteger(runs) ? `${String(runs)}` : "a non-integer";
|
|
483
|
+
const invalidPropertyBudget = (name, runs) => `${name}: pass a positive integer \`runs\`; received ${describeRuns(runs)}. Write ${REWRITE}.`;
|
|
484
|
+
const invalidProvidedBudget = (runs) => `the configured property budget (${checkDefaultsKey}) supplies \`runs\`: ${describeRuns(runs)}; it must be a positive integer.`;
|
|
485
|
+
const requirePositiveRuns = (message, runs) => {
|
|
486
|
+
if (isInvalidRuns(runs)) throw new InvalidBudget({ detail: message(runs) });
|
|
487
|
+
};
|
|
488
|
+
const isInvalidRuns = (runs) => runs !== void 0 && isPositiveInteger(runs) === false;
|
|
489
|
+
/** The property's own explicit fields, a property-level `runs` outranking the same field in `arbitrary`. */
|
|
490
|
+
const explicitOptions = (spec) => spec.runs === void 0 ? { ...spec.arbitrary } : {
|
|
491
|
+
...spec.arbitrary,
|
|
492
|
+
runs: spec.runs
|
|
493
|
+
};
|
|
494
|
+
const mergedOptions = (provided, explicit) => ({
|
|
495
|
+
runs: DEFAULT_RUNS,
|
|
496
|
+
...provided,
|
|
497
|
+
...explicit
|
|
498
|
+
});
|
|
499
|
+
const resolvedRuns = (options) => options.runs ?? DEFAULT_RUNS;
|
|
500
|
+
/**
|
|
501
|
+
* The effective check options: the property's own fields over the configured default over `runs: 100`,
|
|
502
|
+
* merged field by field so a property that sets only `runs` still inherits the configured size and caps.
|
|
503
|
+
*/
|
|
504
|
+
const resolveBudget = (name, spec, provided) => {
|
|
505
|
+
const explicit = explicitOptions(spec);
|
|
506
|
+
requirePositiveRuns((runs) => invalidPropertyBudget(name, runs), explicit.runs);
|
|
507
|
+
requirePositiveRuns(invalidProvidedBudget, provided?.runs);
|
|
508
|
+
const options = mergedOptions(provided, explicit);
|
|
509
|
+
return {
|
|
510
|
+
runs: resolvedRuns(options),
|
|
511
|
+
options
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
const toArbitrary = (input) => Schema$1.isSchema(input) ? Arbitrary.schema(input) : input;
|
|
515
|
+
const assignArbitrary = (built, of, key) => {
|
|
516
|
+
const input = of[key];
|
|
517
|
+
if (input !== void 0) built[key] = toArbitrary(input);
|
|
518
|
+
};
|
|
519
|
+
const recordArbitraries = (of) => {
|
|
520
|
+
const built = {};
|
|
521
|
+
for (const key of Object.keys(of)) assignArbitrary(built, of, key);
|
|
522
|
+
return built;
|
|
523
|
+
};
|
|
524
|
+
const isTupleOf = (of) => Array.isArray(of);
|
|
525
|
+
function arbitraryOf(of) {
|
|
526
|
+
return isTupleOf(of) ? Arbitrary.all(of.map(toArbitrary)) : Arbitrary.all(recordArbitraries(of));
|
|
527
|
+
}
|
|
528
|
+
const minimumOf = (entry) => entry[1];
|
|
529
|
+
const seedClasses = (entries) => {
|
|
530
|
+
const classes = /* @__PURE__ */ new Map();
|
|
531
|
+
for (const [label, entry] of entries) classes.set(label, {
|
|
532
|
+
hits: 0,
|
|
533
|
+
minimum: minimumOf(entry)
|
|
534
|
+
});
|
|
535
|
+
return classes;
|
|
536
|
+
};
|
|
537
|
+
const satisfiedLabels = (entries, values) => entries.filter(([, entry]) => entry[0](...spreadValues(values))).map(([label]) => label);
|
|
538
|
+
const observeRun = (classes, counter, entries, values) => {
|
|
539
|
+
counter.runs = counter.runs + 1;
|
|
540
|
+
for (const label of satisfiedLabels(entries, values)) countHit(classes, label);
|
|
541
|
+
};
|
|
542
|
+
const drawFor = (entries, arbitrary) => ({ more: (count) => Effect.runSync(Arbitrary.sampleEffect(arbitrary, { count }).pipe(Effect.orDie)).map((values) => satisfiedLabels(entries, values)) });
|
|
543
|
+
const noCoverage = () => ({
|
|
544
|
+
observe: () => void 0,
|
|
545
|
+
judge: () => void 0
|
|
546
|
+
});
|
|
547
|
+
const liveCoverage = (cover, arbitrary) => {
|
|
548
|
+
const entries = Object.entries(cover);
|
|
549
|
+
const classes = seedClasses(entries);
|
|
550
|
+
const counter = { runs: 0 };
|
|
551
|
+
return {
|
|
552
|
+
observe: (values) => observeRun(classes, counter, entries, values),
|
|
553
|
+
judge: () => judgeCoverage({
|
|
554
|
+
classes: Object.fromEntries(classes),
|
|
555
|
+
runs: counter.runs,
|
|
556
|
+
draw: drawFor(entries, arbitrary)
|
|
557
|
+
})
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
const makeCoverage = (cover, arbitrary) => cover === void 0 ? noCoverage() : liveCoverage(cover, arbitrary);
|
|
561
|
+
const describeVerdict = (verdict) => Effect.isEffect(verdict) ? "an Effect, which this lane never runs" : `a ${typeof verdict}`;
|
|
562
|
+
const nonBoolean = (verdict, violations) => {
|
|
563
|
+
violations.add(describeVerdict(verdict));
|
|
564
|
+
return true;
|
|
565
|
+
};
|
|
566
|
+
const literalVerdict = (verdict, violations) => typeof verdict === "boolean" ? verdict : nonBoolean(verdict, violations);
|
|
567
|
+
const effectVerdict = (lane, verdict, violations) => lane === "sync" ? Effect.succeed(nonBoolean(verdict, violations)) : Effect.map(verdict, (value) => literalVerdict(value, violations));
|
|
568
|
+
const verdictFor = (run, values, violations) => {
|
|
569
|
+
run.observe(values);
|
|
570
|
+
const verdict = run.holds(run.subject, values);
|
|
571
|
+
return Effect.isEffect(verdict) ? effectVerdict(run.lane, verdict, violations) : Effect.succeed(literalVerdict(verdict, violations));
|
|
572
|
+
};
|
|
573
|
+
const tolerateInterruption = (cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.fail(cause);
|
|
574
|
+
const guardedVerdict = (run, violations) => (values) => Effect.catchCause(Effect.suspend(() => verdictFor(run, values, violations)), tolerateInterruption);
|
|
575
|
+
const violationMessage = (name, violations) => `${name}: the property returned no boolean; it returned ${violations.join(", ")}. A property must return a literal boolean verdict — asserting with expect(), or returning an Option, Result or object, is not a verdict; write ${REWRITE}.`;
|
|
576
|
+
const runCheck = (run) => {
|
|
577
|
+
const violations = /* @__PURE__ */ new Set();
|
|
578
|
+
return Arbitrary.checkEffect(run.arbitrary, guardedVerdict(run, violations), run.options).pipe(Effect.map((result) => ({
|
|
579
|
+
violations,
|
|
580
|
+
result
|
|
581
|
+
})));
|
|
582
|
+
};
|
|
583
|
+
const reportOf = (checked) => Arbitrary.formatCheckFailure(checked.result);
|
|
584
|
+
const violationOf = (name, checked) => checked.violations.size === 0 ? void 0 : violationMessage(name, [...checked.violations]);
|
|
585
|
+
const isRefuted = (checked) => checked.violations.size > 0 || reportOf(checked) !== void 0;
|
|
586
|
+
const dieViolation = (detail) => Effect.die(new NonBooleanVerdict({ detail }));
|
|
587
|
+
const dieReported = (name, report) => Effect.die(/* @__PURE__ */ new Error(`${name}: the property was falsified. ${report}`));
|
|
588
|
+
const dieUncovered = (failure) => Effect.die(new CoverageBelowMinimum({ message: failure }));
|
|
589
|
+
const impostorHolds = (holds, impostor) => (_subject, values) => holds(impostor.impostor, values);
|
|
590
|
+
const recordImpostor = (run, budget, impostor, checked) => {
|
|
591
|
+
const verdict = {
|
|
592
|
+
refuted: isRefuted(checked),
|
|
593
|
+
frozen: impostor.frozen(),
|
|
594
|
+
runs: budget.runs
|
|
595
|
+
};
|
|
596
|
+
gatedLedger.record(run.subject, run.name, verdict);
|
|
597
|
+
};
|
|
598
|
+
const impostorRun = (run, budget) => {
|
|
599
|
+
const impostor = impostorOf(run.subject);
|
|
600
|
+
const retry = {
|
|
601
|
+
...run,
|
|
602
|
+
holds: impostorHolds(run.holds, impostor),
|
|
603
|
+
observe: noObserve
|
|
604
|
+
};
|
|
605
|
+
return runCheck(retry).pipe(Effect.map((checked) => recordImpostor(retry, budget, impostor, checked)));
|
|
606
|
+
};
|
|
607
|
+
const noObserve = () => void 0;
|
|
608
|
+
const gateRun = (run, budget, gate) => gate ? impostorRun(run, budget) : Effect.void;
|
|
609
|
+
const finishPassed = (run, budget, coverage, gate) => {
|
|
610
|
+
const failure = coverage.judge();
|
|
611
|
+
return failure === void 0 ? gateRun(run, budget, gate) : dieUncovered(`${run.name}: ${failure}`);
|
|
612
|
+
};
|
|
613
|
+
const settleReport = (run, budget, coverage, gate, report) => report === void 0 ? finishPassed(run, budget, coverage, gate) : dieReported(run.name, report);
|
|
614
|
+
const settle = (registration, run, budget, coverage, checked) => {
|
|
615
|
+
const violation = violationOf(registration.name, checked);
|
|
616
|
+
return violation === void 0 ? settleReport(run, budget, coverage, registration.gate, reportOf(checked)) : dieViolation(violation);
|
|
617
|
+
};
|
|
618
|
+
const checkOf = (registration, arbitrary, budget, observe) => ({
|
|
619
|
+
name: registration.name,
|
|
620
|
+
lane: registration.lane,
|
|
621
|
+
subject: registration.spec.subject,
|
|
622
|
+
holds: registration.holds,
|
|
623
|
+
arbitrary,
|
|
624
|
+
observe,
|
|
625
|
+
options: budget.options
|
|
626
|
+
});
|
|
627
|
+
const program = (registration) => Effect.gen(function* () {
|
|
628
|
+
const budget = resolveBudget(registration.name, registration.spec, providedCheckDefaults());
|
|
629
|
+
const arbitrary = arbitraryOf(registration.spec.of);
|
|
630
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary);
|
|
631
|
+
const run = checkOf(registration, arbitrary, budget, coverage.observe);
|
|
632
|
+
const checked = yield* runCheck(run);
|
|
633
|
+
yield* settle(registration, run, budget, coverage, checked);
|
|
634
|
+
});
|
|
635
|
+
const programOf = (registration) => () => registration.runtime.provide(program(registration));
|
|
636
|
+
const selfModelMessage = (name) => `${name}: the model is the subject itself; a model law must compare against an independent oracle.`;
|
|
637
|
+
const gatedLedger = makeFileLedger((message) => new VacuousProperty({ detail: message }));
|
|
638
|
+
V.afterAll(() => {
|
|
639
|
+
gatedLedger.finalise();
|
|
640
|
+
});
|
|
641
|
+
const registeredName = (name, kind, exempt) => exempt ? `${name} [exempt: ${kind}]` : name;
|
|
642
|
+
const isSelfModel = (subject, oracle) => Object.is(subject, oracle);
|
|
643
|
+
/**
|
|
644
|
+
* The property surface over one runtime: `prop`, `effectProp`, and the R13 law kinds routed through the gate.
|
|
645
|
+
*
|
|
646
|
+
* @internal
|
|
647
|
+
*/
|
|
648
|
+
const makeProperty = (runtime) => {
|
|
649
|
+
const body = (name, spec, holds, lane, gate) => {
|
|
650
|
+
const registration = {
|
|
651
|
+
runtime,
|
|
652
|
+
name,
|
|
653
|
+
spec,
|
|
654
|
+
holds,
|
|
655
|
+
lane,
|
|
656
|
+
gate
|
|
657
|
+
};
|
|
658
|
+
runtime.register(name, programOf(registration));
|
|
659
|
+
};
|
|
660
|
+
const refuseSelfModel = (name) => {
|
|
661
|
+
runtime.register(name, () => Effect.die(new VacuousProperty({ detail: selfModelMessage(name) })));
|
|
662
|
+
};
|
|
663
|
+
const gated = (name, spec, holds, kind, exempt) => body(registeredName(name, kind, exempt), spec, holds, "sync", exempt === false);
|
|
664
|
+
const model = (name, spec, oracle) => isSelfModel(spec.subject, oracle) ? refuseSelfModel(name) : gated(name, spec, modelHolds(oracle), "model", false);
|
|
665
|
+
const idempotent = (name, spec) => gated(name, spec, idempotentHolds(), "idempotent", true);
|
|
666
|
+
const deterministic = (name, spec) => gated(name, spec, deterministicHolds(), "deterministic", true);
|
|
667
|
+
return {
|
|
668
|
+
prop: (name, spec, holds) => body(name, spec, holds, "sync", true),
|
|
669
|
+
effectProp: (name, spec, holds) => body(name, spec, holds, "effect", true),
|
|
670
|
+
law: {
|
|
671
|
+
model,
|
|
672
|
+
metamorphic: (name, spec, relation) => gated(name, spec, metamorphicHolds(relation), "metamorphic", false),
|
|
673
|
+
roundTrip: (name, spec, decode) => gated(name, spec, roundTripHolds(decode), "roundTrip", false),
|
|
674
|
+
invariant: (name, spec, holds) => gated(name, spec, invariantHolds(holds), "invariant", false),
|
|
675
|
+
idempotent,
|
|
676
|
+
deterministic
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/internal/step-boundary.ts
|
|
682
|
+
/** @internal */
|
|
683
|
+
const makeStopState = (failed) => ({
|
|
684
|
+
stopped: /* @__PURE__ */ new WeakSet(),
|
|
685
|
+
failed
|
|
686
|
+
});
|
|
687
|
+
const alreadyStopped = (state, fiber) => state.stopped.has(fiber);
|
|
688
|
+
/** @internal */
|
|
689
|
+
const stopChecker = Function.dual(2, (state, fiber) => alreadyStopped(state, fiber) === false && state.failed());
|
|
690
|
+
const markStopped = (state, fiber) => {
|
|
691
|
+
state.stopped.add(fiber);
|
|
692
|
+
};
|
|
693
|
+
/** @internal */
|
|
694
|
+
const stopAfterFailedCheck = Function.dual(2, (scheduler, state) => ({
|
|
695
|
+
executionMode: scheduler.executionMode,
|
|
696
|
+
makeDispatcher: () => scheduler.makeDispatcher(),
|
|
697
|
+
shouldYield: (fiber) => checkFiber(state, scheduler, fiber)
|
|
698
|
+
}));
|
|
699
|
+
const checkFiber = (state, scheduler, fiber) => {
|
|
700
|
+
const steppable = fiber;
|
|
701
|
+
if (stopChecker(state, steppable) === false) return scheduler.shouldYield(fiber);
|
|
702
|
+
markStopped(state, steppable);
|
|
703
|
+
steppable.interruptUnsafe(steppable.id);
|
|
704
|
+
return true;
|
|
705
|
+
};
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/internal/test-context.ts
|
|
708
|
+
/** @internal */
|
|
709
|
+
const vitestTestContextKey$1 = "@systemfsoftware/vitest/TestContext";
|
|
710
|
+
/**
|
|
711
|
+
* The running Vitest test, or `null` outside a run; provided on every test the fork runs.
|
|
712
|
+
*
|
|
713
|
+
* @internal
|
|
714
|
+
*/
|
|
715
|
+
const VitestTestContext$1 = Context.Reference(vitestTestContextKey$1, { defaultValue: () => null });
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/internal/virtual-time.ts
|
|
718
|
+
const roundNanos = (millis) => BigInt(Math.round(millis * 1e6));
|
|
719
|
+
const byWakeOrder = (first, second) => {
|
|
720
|
+
const time = first.at - second.at;
|
|
721
|
+
if (time !== 0) return time;
|
|
722
|
+
return tieBreak(first, second);
|
|
723
|
+
};
|
|
724
|
+
const tieBreak = (first, second) => {
|
|
725
|
+
const role = Number(first.observer) - Number(second.observer);
|
|
726
|
+
if (role !== 0) return role;
|
|
727
|
+
return first.sequence - second.sequence;
|
|
728
|
+
};
|
|
729
|
+
const sortSleepers = (sleepers) => {
|
|
730
|
+
sleepers.sort(byWakeOrder);
|
|
731
|
+
};
|
|
732
|
+
const dueNow = (sleepers, now, observer) => sleepers.filter((sleeper) => sleeper.at <= now && sleeper.observer === observer);
|
|
733
|
+
const removeAll = (time, due) => {
|
|
734
|
+
due.forEach((sleeper) => time.removeSleeper(sleeper));
|
|
735
|
+
};
|
|
736
|
+
const wakeAll = (due) => {
|
|
737
|
+
due.forEach((sleeper) => sleeper.wake());
|
|
738
|
+
};
|
|
739
|
+
const runnable = (frozen, queued) => frozen === false && queued > 0;
|
|
740
|
+
const settledForAdvance = (frozen, pending, queued) => runnable(frozen, queued) && pending === 0;
|
|
741
|
+
const shouldStartAdvance = (watching, time) => watching.checking === false && runnable(watching.frozen, time.sleepers().length);
|
|
742
|
+
const removeEntry = (entries, entry) => {
|
|
743
|
+
const index = entries.indexOf(entry);
|
|
744
|
+
if (index !== -1) entries.splice(index, 1);
|
|
745
|
+
};
|
|
746
|
+
const makeVirtualTime = () => {
|
|
747
|
+
const sleepers = [];
|
|
748
|
+
const state = {
|
|
749
|
+
now: 0,
|
|
750
|
+
sequence: 0,
|
|
751
|
+
pending: 0
|
|
752
|
+
};
|
|
753
|
+
return {
|
|
754
|
+
now: () => state.now,
|
|
755
|
+
sleepers: () => sleepers,
|
|
756
|
+
addSleeper: (sleeper) => {
|
|
757
|
+
sleepers.push(sleeper);
|
|
758
|
+
},
|
|
759
|
+
removeSleeper: (sleeper) => removeEntry(sleepers, sleeper),
|
|
760
|
+
advanceTo: (at) => {
|
|
761
|
+
state.now = Math.max(state.now, at);
|
|
762
|
+
},
|
|
763
|
+
claimSequence: () => claimNext(state),
|
|
764
|
+
pendingTasks: () => state.pending,
|
|
765
|
+
beginTask: () => {
|
|
766
|
+
state.pending = state.pending + 1;
|
|
767
|
+
},
|
|
768
|
+
completeTask: () => {
|
|
769
|
+
state.pending = state.pending - 1;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
const claimNext = (state) => {
|
|
774
|
+
const claimed = state.sequence;
|
|
775
|
+
state.sequence = claimed + 1;
|
|
776
|
+
return claimed;
|
|
777
|
+
};
|
|
778
|
+
const runTracked = (time, task, advance) => {
|
|
779
|
+
try {
|
|
780
|
+
task();
|
|
781
|
+
} finally {
|
|
782
|
+
time.completeTask();
|
|
783
|
+
if (time.pendingTasks() === 0) advance();
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
const sleepUntil = (time, advance, observerOf, duration) => {
|
|
787
|
+
if (Duration.toMillis(duration) <= 0) return Effect.void;
|
|
788
|
+
return Effect.flatMap(Effect.fiberId, (fiberId) => Effect.callback((resume) => {
|
|
789
|
+
const sleeper = {
|
|
790
|
+
at: time.now() + Duration.toMillis(duration),
|
|
791
|
+
observer: fiberId === observerOf(),
|
|
792
|
+
sequence: time.claimSequence(),
|
|
793
|
+
wake: () => resume(Effect.void)
|
|
794
|
+
};
|
|
795
|
+
time.addSleeper(sleeper);
|
|
796
|
+
advance();
|
|
797
|
+
return Effect.sync(() => time.removeSleeper(sleeper));
|
|
798
|
+
}));
|
|
799
|
+
};
|
|
800
|
+
/** @internal */
|
|
801
|
+
const makeVirtualRuntime = () => {
|
|
802
|
+
const inner = new Scheduler.MixedScheduler();
|
|
803
|
+
const time = makeVirtualTime();
|
|
804
|
+
const watching = {
|
|
805
|
+
checking: false,
|
|
806
|
+
frozen: false,
|
|
807
|
+
observer: -1
|
|
808
|
+
};
|
|
809
|
+
const advanceWhenIdle = () => {
|
|
810
|
+
if (shouldStartAdvance(watching, time) === false) return;
|
|
811
|
+
watching.checking = true;
|
|
812
|
+
setImmediate(advanceFromIdle);
|
|
813
|
+
};
|
|
814
|
+
const advanceFromIdle = () => {
|
|
815
|
+
watching.checking = false;
|
|
816
|
+
if (settledForAdvance(watching.frozen, time.pendingTasks(), time.sleepers().length) === false) return;
|
|
817
|
+
wakeEarliest(time);
|
|
818
|
+
advanceWhenIdle();
|
|
819
|
+
};
|
|
820
|
+
const scheduler = {
|
|
821
|
+
executionMode: inner.executionMode,
|
|
822
|
+
shouldYield: (fiber) => inner.shouldYield(fiber),
|
|
823
|
+
makeDispatcher: () => {
|
|
824
|
+
const dispatcher = inner.makeDispatcher();
|
|
825
|
+
return {
|
|
826
|
+
scheduleTask: (task, priority) => {
|
|
827
|
+
time.beginTask();
|
|
828
|
+
dispatcher.scheduleTask(() => runTracked(time, task, advanceWhenIdle), priority);
|
|
829
|
+
},
|
|
830
|
+
flush: () => dispatcher.flush()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
return {
|
|
835
|
+
clock: {
|
|
836
|
+
currentTimeMillisUnsafe: () => time.now(),
|
|
837
|
+
currentTimeMillis: Effect.sync(() => time.now()),
|
|
838
|
+
currentTimeNanosUnsafe: () => roundNanos(time.now()),
|
|
839
|
+
currentTimeNanos: Effect.sync(() => roundNanos(time.now())),
|
|
840
|
+
monotonicTimeNanosUnsafe: () => roundNanos(time.now()),
|
|
841
|
+
monotonicTimeNanos: Effect.sync(() => roundNanos(time.now())),
|
|
842
|
+
sleep: (duration) => sleepUntil(time, advanceWhenIdle, () => watching.observer, duration)
|
|
843
|
+
},
|
|
844
|
+
scheduler,
|
|
845
|
+
observe: Effect.flatMap(Effect.fiberId, (id) => Effect.sync(() => {
|
|
846
|
+
watching.observer = id;
|
|
847
|
+
})),
|
|
848
|
+
end: Effect.sync(() => {
|
|
849
|
+
watching.frozen = true;
|
|
850
|
+
})
|
|
851
|
+
};
|
|
852
|
+
};
|
|
853
|
+
const wakeEarliest = (time) => {
|
|
854
|
+
const ordered = [...time.sleepers()];
|
|
855
|
+
sortSleepers(ordered);
|
|
856
|
+
const first = ordered[0];
|
|
857
|
+
if (first === void 0) return;
|
|
858
|
+
time.advanceTo(first.at);
|
|
859
|
+
const due = dueNow(time.sleepers(), time.now(), first.observer);
|
|
860
|
+
removeAll(time, due);
|
|
861
|
+
wakeAll(due);
|
|
862
|
+
};
|
|
863
|
+
/**
|
|
864
|
+
* The clock a virtual-time test runs on: a `Clock` that also answers the
|
|
865
|
+
* `TestClock` interface, so `TestClock.adjust` moves virtual time by letting
|
|
866
|
+
* that much time pass instead of waiting for a real clock (KTD6, R7).
|
|
867
|
+
*
|
|
868
|
+
* @internal
|
|
869
|
+
*/
|
|
870
|
+
const virtualClockLayer = (runtime) => Layer.succeed(Clock.Clock, virtualClockOf(runtime));
|
|
871
|
+
const virtualClockOf = (runtime) => {
|
|
872
|
+
const live = runtime.clock;
|
|
873
|
+
return {
|
|
874
|
+
currentTimeMillisUnsafe: () => live.currentTimeMillisUnsafe(),
|
|
875
|
+
currentTimeMillis: Effect.sync(() => live.currentTimeMillisUnsafe()),
|
|
876
|
+
currentTimeNanosUnsafe: () => live.currentTimeNanosUnsafe(),
|
|
877
|
+
currentTimeNanos: Effect.sync(() => live.currentTimeNanosUnsafe()),
|
|
878
|
+
monotonicTimeNanosUnsafe: () => live.monotonicTimeNanosUnsafe(),
|
|
879
|
+
monotonicTimeNanos: Effect.sync(() => live.monotonicTimeNanosUnsafe()),
|
|
880
|
+
sleep: (duration) => live.sleep(duration),
|
|
881
|
+
adjust: (duration) => Effect.sleep(duration),
|
|
882
|
+
setTime: (timestamp) => Effect.suspend(() => Effect.sleep(Duration.millis(Math.max(0, timestamp - live.currentTimeMillisUnsafe())))),
|
|
883
|
+
withLive: (effect) => Effect.provideService(effect, Clock.Clock, live)
|
|
884
|
+
};
|
|
885
|
+
};
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/internal/runner.ts
|
|
888
|
+
const getCurrentSuite = V.TestRunner.getCurrentSuite;
|
|
889
|
+
/** @internal */
|
|
890
|
+
const addEqualityTesters$1 = () => {
|
|
891
|
+
registerEqualTester();
|
|
892
|
+
};
|
|
893
|
+
const numbered = (timeout) => ({ timeout });
|
|
894
|
+
const givenOptions = (timeout) => timeout ?? {};
|
|
895
|
+
/** @internal */
|
|
896
|
+
const testOptions = (timeout) => typeof timeout === "number" ? numbered(timeout) : givenOptions(timeout);
|
|
897
|
+
/** @internal */
|
|
898
|
+
const hookTimeout = (timeout) => timeout === void 0 ? void 0 : Duration.toMillis(Duration.fromInputUnsafe(timeout));
|
|
899
|
+
/**
|
|
900
|
+
* The positional form is refused at run time with the R15 rewrite; the public type already refuses it.
|
|
901
|
+
*/
|
|
902
|
+
const refusePositional = (spec) => {
|
|
903
|
+
if (lacksInputs(spec)) throw new Slop({ detail: refusePositionalProp });
|
|
904
|
+
};
|
|
905
|
+
const lacksInputs = (spec) => isAbsent(spec) || hasNoOf(spec);
|
|
906
|
+
const isAbsent = (spec) => spec === null || spec === void 0;
|
|
907
|
+
const hasNoOf = (spec) => Object.hasOwn(spec, "of") === false;
|
|
908
|
+
const stopCheckOf = (ctx) => ctx === void 0 ? void 0 : failedAfter(ctx, errorCount(ctx));
|
|
909
|
+
const neverStop = () => void 0;
|
|
910
|
+
/** @internal */
|
|
911
|
+
const realTime = {
|
|
912
|
+
runtime: void 0,
|
|
913
|
+
owned: false,
|
|
914
|
+
stop: stopCheckOf
|
|
915
|
+
};
|
|
916
|
+
/** @internal */
|
|
917
|
+
const perRun = () => ({
|
|
918
|
+
runtime: makeVirtualRuntime(),
|
|
919
|
+
owned: true,
|
|
920
|
+
stop: stopCheckOf
|
|
921
|
+
});
|
|
922
|
+
const propertyEnv = {
|
|
923
|
+
runtime: void 0,
|
|
924
|
+
owned: false,
|
|
925
|
+
stop: neverStop
|
|
926
|
+
};
|
|
927
|
+
const logErrors = (cause) => Effect.forEach(Cause.prettyErrors(cause), (error) => Effect.logError(error), { discard: true });
|
|
928
|
+
const failedAfter = (ctx, before) => () => errorCount(ctx) > before;
|
|
929
|
+
const signalOf = (ctx) => ctx?.signal;
|
|
930
|
+
const signalOnly = (signal) => signal === void 0 ? {} : { signal };
|
|
931
|
+
const withScheduler = (signal, scheduler) => ({
|
|
932
|
+
...signalOnly(signal),
|
|
933
|
+
scheduler
|
|
934
|
+
});
|
|
935
|
+
const runOptions = (ctx, scheduler) => scheduler === void 0 ? signalOnly(signalOf(ctx)) : withScheduler(signalOf(ctx), scheduler);
|
|
936
|
+
const baseScheduler = (env) => env.runtime?.scheduler;
|
|
937
|
+
const stopScheduler = (base, failedCheck) => stopAfterFailedCheck(base ?? new Scheduler.MixedScheduler(), makeStopState(failedCheck));
|
|
938
|
+
const pickScheduler = (env, failedCheck) => failedCheck === void 0 ? baseScheduler(env) : stopScheduler(baseScheduler(env), failedCheck);
|
|
939
|
+
const runPromise = (effect, ctx, env = realTime) => {
|
|
940
|
+
const failedCheck = env.stop(ctx);
|
|
941
|
+
return runExit(effect, ctx, env, failedCheck).then(reportExit(failedCheck));
|
|
942
|
+
};
|
|
943
|
+
const runExit = (effect, ctx, env, failedCheck) => {
|
|
944
|
+
const program = wrapOwned(effect, ownedRuntime(env));
|
|
945
|
+
return Effect.runPromiseExit(program, runOptions(ctx, pickScheduler(env, failedCheck)));
|
|
946
|
+
};
|
|
947
|
+
const ownedRuntime = (env) => env.owned === false ? void 0 : env.runtime;
|
|
948
|
+
const observeOf = (runtime) => runtime === void 0 ? Effect.void : runtime.observe;
|
|
949
|
+
const endOf = (runtime) => runtime === void 0 ? Effect.void : runtime.end;
|
|
950
|
+
const wrapOwned = (effect, runtime) => observeOf(runtime).pipe(Effect.andThen(effect.pipe(Effect.ensuring(endOf(runtime)))));
|
|
951
|
+
const stoppedSettled = () => Promise.resolve(void 0);
|
|
952
|
+
const stoppedExit = (cause) => Cause.hasInterruptsOnly(cause) ? stoppedSettled() : failExit(cause);
|
|
953
|
+
const stoppedChecked = (cause, failed) => failed ? stoppedExit(cause) : failExit(cause);
|
|
954
|
+
const failedOrThrow = (cause, failedCheck) => failedCheck === void 0 ? failExit(cause) : stoppedChecked(cause, failedCheck());
|
|
955
|
+
const reportExit = (failedCheck) => (exit) => Exit.isSuccess(exit) ? Promise.resolve(exit.value) : failedOrThrow(exit.cause, failedCheck);
|
|
956
|
+
/**
|
|
957
|
+
* A body that needs a service nothing provides dies with Effect's own `Service not found`. That is a
|
|
958
|
+
* test-authoring error — the layers were never wired — so it is reported as the unprovided rewrite (R9)
|
|
959
|
+
* instead of as a raw defect. Failures an `owned` region is responsible for are recovered by their owner
|
|
960
|
+
* and never reach here.
|
|
961
|
+
*/
|
|
962
|
+
const isUnprovided = (error) => error instanceof Error && error.message.startsWith("Service not found");
|
|
963
|
+
const failExit = (cause) => logErrors(cause).pipe(Effect.runPromise).then(() => rethrowSquashed(cause));
|
|
964
|
+
const rethrowSquashed = (cause) => {
|
|
965
|
+
const error = Cause.squash(cause);
|
|
966
|
+
if (isUnprovided(error)) throw new Slop({ detail: refuseUnprovided });
|
|
967
|
+
throw error;
|
|
968
|
+
};
|
|
969
|
+
const runTest = (ctx, env = realTime) => (effect) => {
|
|
970
|
+
const promise = runPromise(effect, ctx, env);
|
|
971
|
+
if (ctx !== void 0) trackAbort(ctx, promise);
|
|
972
|
+
return promise;
|
|
973
|
+
};
|
|
974
|
+
const trackAbort = (ctx, promise) => {
|
|
975
|
+
const onAbort = () => {
|
|
976
|
+
ctx.onTestFinished(() => promise.then(noop, noop));
|
|
977
|
+
};
|
|
978
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
979
|
+
promise.then(() => detach(ctx, onAbort), () => detach(ctx, onAbort));
|
|
980
|
+
if (ctx.signal.aborted) onAbort();
|
|
981
|
+
};
|
|
982
|
+
const detach = (ctx, onAbort) => {
|
|
983
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
984
|
+
};
|
|
985
|
+
const noop = () => {};
|
|
986
|
+
const clockLayerOf = (env) => env.runtime === void 0 ? TestClock.layer() : virtualClockLayer(env.runtime);
|
|
987
|
+
/** @internal */
|
|
988
|
+
const testEnv = (env) => Layer.mergeAll(TestConsole.layer, clockLayerOf(env));
|
|
989
|
+
/**
|
|
990
|
+
* The run a test's fibers execute in: the test's own context as `Running`, and the same context under the
|
|
991
|
+
* fork's `VitestTestContext` for every library built on the fork.
|
|
992
|
+
*/
|
|
993
|
+
const bindRun = (effect, ctx, shadow) => effect.pipe(Effect.provideService(Running, {
|
|
994
|
+
ctx,
|
|
995
|
+
shadow
|
|
996
|
+
}), Effect.provideService(VitestTestContext$1, ctx));
|
|
997
|
+
const isObject = (output) => typeof output === "object" && output !== null;
|
|
998
|
+
const isThenable = (value) => "then" in value ? typeof value.then === "function" : false;
|
|
999
|
+
const isPromiseLike = (output) => isObject(output) && isThenable(output);
|
|
1000
|
+
const refusePromiseBody = () => {
|
|
1001
|
+
throw new Slop({ detail: refuseAsync });
|
|
1002
|
+
};
|
|
1003
|
+
const refuseAfter = (promise) => {
|
|
1004
|
+
Promise.resolve(promise).catch(noop);
|
|
1005
|
+
return refusePromiseBody();
|
|
1006
|
+
};
|
|
1007
|
+
const checkPromiseBody = (output) => {
|
|
1008
|
+
if (isPromiseLike(output)) refuseAfter(output);
|
|
1009
|
+
};
|
|
1010
|
+
const outputIsEffect = (output) => Effect.isEffect(output);
|
|
1011
|
+
const buildSingleRun = (body, mapOne, env, shadow) => (ctx) => {
|
|
1012
|
+
const output = withSyncRun({
|
|
1013
|
+
ctx,
|
|
1014
|
+
shadow
|
|
1015
|
+
}, () => body(ctx));
|
|
1016
|
+
checkPromiseBody(output);
|
|
1017
|
+
return outputIsEffect(output) ? runBuilt(ctx, env, mapOne, output, shadow) : Promise.resolve(void 0);
|
|
1018
|
+
};
|
|
1019
|
+
const runBuilt = (ctx, env, mapOne, output, shadow) => runTest(ctx, env)(mapOne(bindRun(Effect.suspend(() => output), ctx, shadow)));
|
|
1020
|
+
/**
|
|
1021
|
+
* Runs a property's program as the test that registered it, under the same binding the Effect lanes use, so a
|
|
1022
|
+
* check or an annotation inside `holds` belongs to the test that declared the property. The env is the bare
|
|
1023
|
+
* `propertyEnv`: no step boundary interrupts a property, so its `false` verdict still shrinks (R6, U4).
|
|
1024
|
+
*/
|
|
1025
|
+
const runProperty = (ctx, program) => buildSingleRun(() => program(), (effect) => effect, propertyEnv, false)(ctx);
|
|
1026
|
+
const property = makeProperty({
|
|
1027
|
+
register: (name, program) => {
|
|
1028
|
+
V.it(name, (ctx) => runProperty(ctx, program));
|
|
1029
|
+
},
|
|
1030
|
+
provide: (effect) => effect
|
|
1031
|
+
});
|
|
1032
|
+
const isName = (first) => typeof first === "string";
|
|
1033
|
+
const byName = (args) => isName(args[0]);
|
|
1034
|
+
const byLayer = (args) => Layer.isLayer(args[0]);
|
|
1035
|
+
const byEffect = (args) => Effect.isEffect(args[0]);
|
|
1036
|
+
const propImpl = (name, spec, holds) => {
|
|
1037
|
+
refusePositional(spec);
|
|
1038
|
+
property.prop(name, spec, holds);
|
|
1039
|
+
};
|
|
1040
|
+
/** @internal */
|
|
1041
|
+
const prop$1 = Function.dual(byName, propImpl);
|
|
1042
|
+
const outcomeOf = (promise) => promise.then(() => void 0, (error) => error);
|
|
1043
|
+
/**
|
|
1044
|
+
* The second run's failure: a diagnosis the fork itself made is rethrown as that diagnosis, because the
|
|
1045
|
+
* second run exists to catch leaked state, not to relabel a refusal as a leak (F7). Anything else is the leak.
|
|
1046
|
+
*/
|
|
1047
|
+
const throwSecondRun = (error) => {
|
|
1048
|
+
if (isRefusal(error)) throw error;
|
|
1049
|
+
return throwLeaked(error);
|
|
1050
|
+
};
|
|
1051
|
+
const leakedOrNothing = (error) => {
|
|
1052
|
+
if (error !== void 0) throwSecondRun(error);
|
|
1053
|
+
};
|
|
1054
|
+
const makeTesterWith = (mapEffect, it, envFor, rerunnable) => {
|
|
1055
|
+
const runOne = (ctx, args, self, shadow) => {
|
|
1056
|
+
const env = envFor();
|
|
1057
|
+
return buildSingleRun(() => self(...args), (effect) => mapEffect(effect, env), env, shadow)(ctx);
|
|
1058
|
+
};
|
|
1059
|
+
/**
|
|
1060
|
+
* The second run: the same layer build the first run used, built again, with the shadow flag set so its
|
|
1061
|
+
* checks throw instead of reporting softly (R5, KTD7).
|
|
1062
|
+
*/
|
|
1063
|
+
const rerun = (ctx, args, self) => outcomeOf(runOne(ctx, args, self, true)).then(leakedOrNothing);
|
|
1064
|
+
const run = (ctx, args, self) => outcomeOf(runOne(ctx, args, self, false)).then((error) => afterFirst(ctx, args, self, error));
|
|
1065
|
+
const afterFirst = (ctx, args, self, error) => {
|
|
1066
|
+
if (error !== void 0) throwAfterCheck(ctx, error);
|
|
1067
|
+
gateNoAssertion(ctx);
|
|
1068
|
+
return rerunIfClean(ctx, args, self);
|
|
1069
|
+
};
|
|
1070
|
+
const rerunIfClean = (ctx, args, self) => shouldRerun(ctx) ? rerun(ctx, args, self) : Promise.resolve();
|
|
1071
|
+
const shouldRerun = (ctx) => rerunnable && isClean(ctx);
|
|
1072
|
+
const registerOn = (registrar, name, self, timeout) => {
|
|
1073
|
+
registrar(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self));
|
|
1074
|
+
};
|
|
1075
|
+
const dualTest = (register) => Function.dual(byName, register);
|
|
1076
|
+
const f = dualTest((name, self, timeout) => registerOn(it, name, self, timeout));
|
|
1077
|
+
const skip = dualTest((name, self, timeout) => registerOn(it.skip, name, self, timeout));
|
|
1078
|
+
const skipIf = (condition) => dualTest((name, self, timeout) => registerOn(it.skipIf(condition), name, self, timeout));
|
|
1079
|
+
const runIf = (condition) => dualTest((name, self, timeout) => registerOn(it.runIf(condition), name, self, timeout));
|
|
1080
|
+
const only = dualTest((name, self, timeout) => registerOn(it.only, name, self, timeout));
|
|
1081
|
+
const each = (cases) => (name, self, timeout) => it.for(cases)(name, testOptions(timeout), (row, ctx) => run(ctx, [row], self));
|
|
1082
|
+
const fails = dualTest((name, self, timeout) => registerOn(V.it.fails, name, self, timeout));
|
|
1083
|
+
const effectProperty = makeProperty({
|
|
1084
|
+
register: (name, program) => {
|
|
1085
|
+
it(name, (ctx) => runProperty(ctx, program));
|
|
1086
|
+
},
|
|
1087
|
+
provide: (effect) => mapEffect(effect, envFor())
|
|
1088
|
+
});
|
|
1089
|
+
const testerProp = (name, spec, holds) => {
|
|
1090
|
+
refusePositional(spec);
|
|
1091
|
+
effectProperty.effectProp(name, spec, holds);
|
|
1092
|
+
};
|
|
1093
|
+
return Object.assign(f, {
|
|
1094
|
+
skip,
|
|
1095
|
+
skipIf,
|
|
1096
|
+
runIf,
|
|
1097
|
+
only,
|
|
1098
|
+
each,
|
|
1099
|
+
fails,
|
|
1100
|
+
prop: testerProp
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1103
|
+
const makeTester = (mapEffect, it = V.it) => makeTesterWith(mapEffect, it, perRun, true);
|
|
1104
|
+
const makeSharedTester = (mapEffect, it, envFor) => makeTesterWith(mapEffect, it, envFor, false);
|
|
1105
|
+
const isSlop = (error) => Schema$1.is(Slop)(error);
|
|
1106
|
+
/** Every failure the fork itself names, as `Schema.is`: the families a second run must not relabel. */
|
|
1107
|
+
const refusalChecks = [
|
|
1108
|
+
isSlop,
|
|
1109
|
+
Schema$1.is(AfterFailedExpect),
|
|
1110
|
+
Schema$1.is(InvalidBudget),
|
|
1111
|
+
Schema$1.is(NonBooleanVerdict),
|
|
1112
|
+
Schema$1.is(LeakedState)
|
|
1113
|
+
];
|
|
1114
|
+
const isRefusal = (error) => refusalChecks.some((matches) => matches(error));
|
|
1115
|
+
const throwAfterCheck = (ctx, error) => {
|
|
1116
|
+
if (isSlop(error)) throw error;
|
|
1117
|
+
return afterFailedCheck(ctx, error);
|
|
1118
|
+
};
|
|
1119
|
+
const afterFailedCheck = (ctx, error) => {
|
|
1120
|
+
if (errorCount(ctx) > 0) throwAfterFailed();
|
|
1121
|
+
throw error;
|
|
1122
|
+
};
|
|
1123
|
+
const throwAfterFailed = () => {
|
|
1124
|
+
throw new AfterFailedExpect({ detail: "thrown after a failed expect above, most likely caused by it" });
|
|
1125
|
+
};
|
|
1126
|
+
/** Whether a run left no failure behind: the second run is only worth doing for a clean first one. */
|
|
1127
|
+
const isClean = (ctx) => errorCount(ctx) === 0;
|
|
1128
|
+
const silentRun = (ctx) => errorCount(ctx) === 0 && assertionCalls(ctx) === 0;
|
|
1129
|
+
/**
|
|
1130
|
+
* The test's own assertion count. Every check the fork runs inside a test binds to that test's `expect`,
|
|
1131
|
+
* so a sibling test's checks are never visible here.
|
|
1132
|
+
*/
|
|
1133
|
+
const assertionCalls = (ctx) => ctx.expect.getState().assertionCalls;
|
|
1134
|
+
const gateNoAssertion = (ctx) => {
|
|
1135
|
+
if (silentRun(ctx)) throwNoAssertion();
|
|
1136
|
+
};
|
|
1137
|
+
const throwNoAssertion = () => {
|
|
1138
|
+
throw new Slop({ detail: refuseNoAssertion });
|
|
1139
|
+
};
|
|
1140
|
+
const clipped = (message) => message.length > 400 ? message.slice(0, 400) : message;
|
|
1141
|
+
const throwLeaked = (outcome) => {
|
|
1142
|
+
throw new LeakedState({ detail: "✗ this test passed, then failed when run again on a fresh build of its services: something keeps state outside its layer, so tests here see each other's changes. The second run failed with: " + clipped(outcome.message) });
|
|
1143
|
+
};
|
|
1144
|
+
const CONCURRENT_BLOCK = {
|
|
1145
|
+
concurrent: true,
|
|
1146
|
+
shuffle: true
|
|
1147
|
+
};
|
|
1148
|
+
const ALONE_BLOCK = {
|
|
1149
|
+
concurrent: false,
|
|
1150
|
+
shuffle: false
|
|
1151
|
+
};
|
|
1152
|
+
const withDefault = (given, fallback) => given ?? fallback;
|
|
1153
|
+
const concurrencyIn = (options) => options?.concurrent;
|
|
1154
|
+
const timeoutIn = (options) => options?.timeout;
|
|
1155
|
+
const excludedIn = (options) => options?.excludeTestServices;
|
|
1156
|
+
const sharedIn = (options) => options?.shared;
|
|
1157
|
+
const memoMapIn = (options) => options?.memoMap;
|
|
1158
|
+
const excludeTestServicesOf = (options) => withDefault(excludedIn(options), false);
|
|
1159
|
+
const blockEnvOf = (inherited) => withDefault(inherited, {
|
|
1160
|
+
runtime: makeVirtualRuntime(),
|
|
1161
|
+
owned: false,
|
|
1162
|
+
stop: stopCheckOf
|
|
1163
|
+
});
|
|
1164
|
+
const memoMapOf = (options) => withDefault(memoMapIn(options), Effect.runSync(Layer.makeMemoMap));
|
|
1165
|
+
const blockHookTimeout = (options) => hookTimeout(timeoutIn(options));
|
|
1166
|
+
const isSharedBlock = (options) => sharedIn(options) === true;
|
|
1167
|
+
/**
|
|
1168
|
+
* A named block's suite options: its tests run concurrently and in shuffled order (R4). A declared-shared
|
|
1169
|
+
* block is the stateful exception — its tests read each other's writes through one build, so it stays in
|
|
1170
|
+
* declaration order and runs alone.
|
|
1171
|
+
*/
|
|
1172
|
+
const describeOptions = (options, shared) => mergeBlockOptions(options, shared ? ALONE_BLOCK : CONCURRENT_BLOCK);
|
|
1173
|
+
const mergeBlockOptions = (options, fallback) => ({
|
|
1174
|
+
concurrent: withDefault(concurrencyIn(options), fallback.concurrent),
|
|
1175
|
+
shuffle: fallback.shuffle
|
|
1176
|
+
});
|
|
1177
|
+
/**
|
|
1178
|
+
* The block's layer with the test's own services merged in, unless the block excludes them. `Layer`'s output
|
|
1179
|
+
* is contravariant, so the merged layer is the *narrower* `Layer<R, E>` the callers provide to a run.
|
|
1180
|
+
*/
|
|
1181
|
+
const mergeTestEnv = (layer_, env, excludeTestServices) => excludeTestServices ? layer_ : Layer.provideMerge(layer_, env);
|
|
1182
|
+
const sharedContext = (shared, memoMap, scope) => Layer.buildWithMemoMap(shared, memoMap, scope).pipe(Effect.orDie, Effect.cached, Effect.runSync);
|
|
1183
|
+
const makeScopeCloser = (scope) => {
|
|
1184
|
+
let closed = false;
|
|
1185
|
+
return () => {
|
|
1186
|
+
if (closed) return Promise.resolve();
|
|
1187
|
+
closed = true;
|
|
1188
|
+
return runPromise(Scope.close(scope, Exit.void)).then(noop);
|
|
1189
|
+
};
|
|
1190
|
+
};
|
|
1191
|
+
/**
|
|
1192
|
+
* Opens a block from its arguments: an anonymous body, or a named one. The sole implementation behind both
|
|
1193
|
+
* arities, so `layer(L)(fn)` and `layer(L)("name", fn)` cannot drift apart.
|
|
1194
|
+
*/
|
|
1195
|
+
const blockOpener = (anonymous, named) => (first, second) => routeBlock(first, second, anonymous, named);
|
|
1196
|
+
const routeBlock = (first, second, anonymous, named) => {
|
|
1197
|
+
if (typeof first === "string") return openNamed(first, second, named);
|
|
1198
|
+
return anonymous(first);
|
|
1199
|
+
};
|
|
1200
|
+
const openNamed = (name, f, named) => {
|
|
1201
|
+
if (f !== void 0) named(name, f);
|
|
1202
|
+
};
|
|
1203
|
+
const layerFor = (layer_, options, inherited) => isSharedBlock(options) ? sharedBlock(layer_, options, inherited) : freshBlock(layer_, options);
|
|
1204
|
+
const freshBlock = (layer_, options) => {
|
|
1205
|
+
const excludeTestServices = excludeTestServicesOf(options);
|
|
1206
|
+
const withTestEnv = (env) => mergeTestEnv(layer_(env), testEnv(env), excludeTestServices);
|
|
1207
|
+
const nestedOpen = (nestedLayer, nestedOptions) => layerFor((env) => Layer.provideMerge(nestedLayer, withTestEnv(env)), {
|
|
1208
|
+
...nestedOptions,
|
|
1209
|
+
excludeTestServices
|
|
1210
|
+
}, void 0);
|
|
1211
|
+
const nestedLayer = Function.dual(byLayer, nestedOpen);
|
|
1212
|
+
const makeIt = (it) => makeItProxy(it, {
|
|
1213
|
+
effect: makeTester((effect, env) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, Layer.orDie(Layer.fresh(withTestEnv(env))))), it),
|
|
1214
|
+
prop: prop$1,
|
|
1215
|
+
law: property.law,
|
|
1216
|
+
flakyTest: flakyTest$1,
|
|
1217
|
+
layer: nestedLayer
|
|
1218
|
+
}, true);
|
|
1219
|
+
return blockOpener((f) => f(makeIt(V.it)), (name, f) => {
|
|
1220
|
+
V.describe(name, describeOptions(options, false), () => f(makeIt(V.it)));
|
|
1221
|
+
});
|
|
1222
|
+
};
|
|
1223
|
+
const nestedBlock = (nested, memoMap) => ({
|
|
1224
|
+
...nested,
|
|
1225
|
+
memoMap: Layer.forkMemoMapUnsafe(memoMap),
|
|
1226
|
+
shared: true
|
|
1227
|
+
});
|
|
1228
|
+
const sharedOverrides = (shared, contextEffect, memoMap, blockEnv) => {
|
|
1229
|
+
const nestedOpen = (nestedLayer, nestedOptions) => layerFor(() => Layer.provideMerge(nestedLayer, shared), nestedBlock(nestedOptions, memoMap), blockEnv);
|
|
1230
|
+
const nestedLayer = Function.dual(byLayer, nestedOpen);
|
|
1231
|
+
return {
|
|
1232
|
+
effect: makeSharedTester((effect) => Effect.flatMap(contextEffect, (context) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, context))), V.it, () => blockEnv),
|
|
1233
|
+
prop: prop$1,
|
|
1234
|
+
law: property.law,
|
|
1235
|
+
flakyTest: flakyTest$1,
|
|
1236
|
+
layer: nestedLayer
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1239
|
+
const sharedBlock = (layer_, options, inherited) => {
|
|
1240
|
+
const blockEnv = blockEnvOf(inherited);
|
|
1241
|
+
const memoMap = memoMapOf(options);
|
|
1242
|
+
const scope = Scope.makeUnsafe();
|
|
1243
|
+
const shared = mergeTestEnv(layer_(blockEnv), testEnv(blockEnv), excludeTestServicesOf(options));
|
|
1244
|
+
const contextEffect = sharedContext(shared, memoMap, scope);
|
|
1245
|
+
const makeIt = (it) => makeItProxy(it, sharedOverrides(shared, contextEffect, memoMap, blockEnv), false);
|
|
1246
|
+
const closeScope = makeScopeCloser(scope);
|
|
1247
|
+
return blockOpener((f) => sharedAnonymous(f, makeIt, options, blockTasksOf(), blockEnv, contextEffect, closeScope), (name, f) => describeShared(name, f, makeIt, options, contextEffect, closeScope, blockEnv));
|
|
1248
|
+
};
|
|
1249
|
+
const describeShared = (name, f, makeIt, options, contextEffect, closeScope, blockEnv) => {
|
|
1250
|
+
V.describe(name, describeOptions(options, true), () => {
|
|
1251
|
+
V.beforeAll(() => runPromise(Effect.asVoid(contextEffect), void 0, blockEnv), blockHookTimeout(options));
|
|
1252
|
+
V.afterAll(() => closeScope(), blockHookTimeout(options));
|
|
1253
|
+
return f(makeIt(V.it));
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
const blockTasksOf = () => suiteTasks(getCurrentSuite());
|
|
1257
|
+
const suiteTasks = (suite) => suite.tasks;
|
|
1258
|
+
const sharedAnonymous = (register, makeIt, options, blockTasks, blockEnv, contextEffect, closeScope) => {
|
|
1259
|
+
const previousTasks = new Set(blockTasks);
|
|
1260
|
+
register(makeIt(V.it));
|
|
1261
|
+
const added = collectTasks(blockTasksOf().filter((task) => previousTasks.has(task) === false));
|
|
1262
|
+
if (added.length === 0) {
|
|
1263
|
+
V.afterAll(() => closeScope(), blockHookTimeout(options));
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
openAddedTests(added, blockEnv, contextEffect, closeScope, options);
|
|
1267
|
+
};
|
|
1268
|
+
const openAddedTests = (added, blockEnv, contextEffect, closeScope, options) => {
|
|
1269
|
+
const blockTaskSet = new Set(added);
|
|
1270
|
+
V.beforeEach((ctx) => sharedBeforeEach(ctx, blockTaskSet, countDown(added.length, closeScope), contextEffect, blockEnv), blockHookTimeout(options));
|
|
1271
|
+
V.afterAll(() => closeScope(), blockHookTimeout(options));
|
|
1272
|
+
};
|
|
1273
|
+
const countDown = (remaining, closeScope) => {
|
|
1274
|
+
let left = remaining;
|
|
1275
|
+
return () => {
|
|
1276
|
+
left -= 1;
|
|
1277
|
+
return left === 0 ? closeScope() : void 0;
|
|
1278
|
+
};
|
|
1279
|
+
};
|
|
1280
|
+
const sharedBeforeEach = (ctx, blockTaskSet, finish, contextEffect, blockEnv) => {
|
|
1281
|
+
if (blockTaskSet.has(ctx.task) === false) return void 0;
|
|
1282
|
+
ctx.onTestFinished(() => finish());
|
|
1283
|
+
return runPromise(Effect.asVoid(contextEffect), ctx, blockEnv);
|
|
1284
|
+
};
|
|
1285
|
+
const collectTasks = (tasks) => {
|
|
1286
|
+
const acc = [];
|
|
1287
|
+
for (const task of tasks) collectOne(task, acc);
|
|
1288
|
+
return acc;
|
|
1289
|
+
};
|
|
1290
|
+
const collectOne = (task, acc) => {
|
|
1291
|
+
if (isRunnableTest(task)) acc.push(task);
|
|
1292
|
+
else collectNested(task, acc);
|
|
1293
|
+
};
|
|
1294
|
+
const collectNested = (task, acc) => {
|
|
1295
|
+
if (task.tasks !== void 0) collectTasks(task.tasks).forEach((nested) => acc.push(nested));
|
|
1296
|
+
};
|
|
1297
|
+
const notSkipped = (task) => task.mode !== "skip" && task.mode !== "todo";
|
|
1298
|
+
const isRunnableTest = (task) => task.type === "test" && notSkipped(task);
|
|
1299
|
+
const layerImpl = (layer_, options) => layerFor(() => layer_, options, void 0);
|
|
1300
|
+
/** @internal */
|
|
1301
|
+
const layer$1 = Function.dual(byLayer, layerImpl);
|
|
1302
|
+
const withinTimeout = (elapsed, timeout) => Duration.isLessThanOrEqualTo(Duration.fromInputUnsafe(elapsed), Duration.fromInputUnsafe(timeout));
|
|
1303
|
+
const retryWithin = (timeout) => Schedule.while(Schedule.recurs(10), (state) => Effect.succeed(withinTimeout(state.elapsed, timeout)));
|
|
1304
|
+
const flakyTestImpl = (self, timeout = Duration.seconds(30)) => self.pipe(Effect.scoped, Effect.sandbox, (sandboxed) => Effect.retry(sandboxed, retryWithin(timeout)), Effect.orDie);
|
|
1305
|
+
/** @internal */
|
|
1306
|
+
const flakyTest$1 = Function.dual(byEffect, flakyTestImpl);
|
|
1307
|
+
const isFunction = (arg) => typeof arg === "function";
|
|
1308
|
+
/**
|
|
1309
|
+
* The two returns the bare lane refuses: a promise, which runs outside the test runtime (R9), and an Effect,
|
|
1310
|
+
* which the bare lane never runs — it belongs on `it.effect`.
|
|
1311
|
+
*/
|
|
1312
|
+
const refuseBareOutput = (output) => {
|
|
1313
|
+
checkPromiseBody(output);
|
|
1314
|
+
if (outputIsEffect(output)) throw new Slop({ detail: refuseBareEffect });
|
|
1315
|
+
};
|
|
1316
|
+
/** One pass of the bare body: it runs bound to the run's context, and its return is refused here. */
|
|
1317
|
+
const bareRunOnce = (ctx, self, shadow) => {
|
|
1318
|
+
const returned = withSyncRun({
|
|
1319
|
+
ctx,
|
|
1320
|
+
shadow
|
|
1321
|
+
}, () => self(ctx));
|
|
1322
|
+
if (isObject(returned)) refuseBareOutput(returned);
|
|
1323
|
+
};
|
|
1324
|
+
/** The bare body's outcome: `undefined` when it passed, or the failure it threw or was refused with. */
|
|
1325
|
+
const bareOutcome = (ctx, self, shadow) => outcomeOf(Promise.resolve().then(() => bareRunOnce(ctx, self, shadow)));
|
|
1326
|
+
const shouldRerunBare = (ctx, rerunnable) => rerunnable && isClean(ctx);
|
|
1327
|
+
const afterBareGate = (ctx, self, rerunnable) => shouldRerunBare(ctx, rerunnable) ? bareOutcome(ctx, self, true).then(leakedOrNothing) : Promise.resolve();
|
|
1328
|
+
const afterBareFirst = (ctx, self, rerunnable, failure) => {
|
|
1329
|
+
if (failure !== void 0) throwAfterCheck(ctx, failure);
|
|
1330
|
+
gateNoAssertion(ctx);
|
|
1331
|
+
return afterBareGate(ctx, self, rerunnable);
|
|
1332
|
+
};
|
|
1333
|
+
/**
|
|
1334
|
+
* The bare `it(name, body)` lane: Vitest's own registration with the fork's defaults instead of an Effect
|
|
1335
|
+
* lane. The body is synchronous, so it is gated for a leftover assertion and run a second time on its own,
|
|
1336
|
+
* which is what catches a body that only passes while nothing else touched its module (R5, R9).
|
|
1337
|
+
*/
|
|
1338
|
+
const runDefault = (ctx, self, rerunnable) => bareOutcome(ctx, self, false).then((failure) => afterBareFirst(ctx, self, rerunnable, failure));
|
|
1339
|
+
const defaultLane = (self, rerunnable) => (ctx) => runDefault(ctx, self, rerunnable);
|
|
1340
|
+
/**
|
|
1341
|
+
* The fork's `it`: Vitest's test API with the fork's Effect lanes and lawful property lanes attached,
|
|
1342
|
+
* callable data-first (`it(name, body)`) or data-last (`it(body, options?)(name)`).
|
|
1343
|
+
*
|
|
1344
|
+
* @internal
|
|
1345
|
+
*/
|
|
1346
|
+
const makeMethods$1 = (it) => makeItProxy(it, {
|
|
1347
|
+
effect: makeTester((effect, env) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, testEnv(env))), it),
|
|
1348
|
+
live: makeTesterWith((effect) => Effect.scoped(effect), it, () => realTime, true),
|
|
1349
|
+
flakyTest: flakyTest$1,
|
|
1350
|
+
layer: layer$1,
|
|
1351
|
+
prop: prop$1,
|
|
1352
|
+
law: property.law
|
|
1353
|
+
}, true);
|
|
1354
|
+
/** @internal */
|
|
1355
|
+
const { effect: effect$1, live: live$1 } = makeMethods$1(V.it);
|
|
1356
|
+
function makeItProxy(it, overrides, rerunnable) {
|
|
1357
|
+
return new Proxy(it, {
|
|
1358
|
+
apply: (target, thisArg, args) => routeCall(target, thisArg, args, rerunnable),
|
|
1359
|
+
get: (target, property, receiver) => memberOrOverride(target, overrides, property, receiver)
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function routeCall(target, thisArg, args, rerunnable) {
|
|
1363
|
+
return isName(args[0]) ? applied(target, thisArg, args, rerunnable) : (name) => applied(target, thisArg, [
|
|
1364
|
+
name,
|
|
1365
|
+
args[0],
|
|
1366
|
+
args[1]
|
|
1367
|
+
], rerunnable);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* The route's registration. Vitest takes `[name, body]`, `[name, body, options]` or `[name, options, body]`,
|
|
1371
|
+
* so the body is the argument that is a function, and it is the one the fork's bare lane wraps. Vitest's own
|
|
1372
|
+
* helpers (`it.skip`, `it.each`, ...) come through the proxy's `get` trap and are handed over untouched.
|
|
1373
|
+
*/
|
|
1374
|
+
function applied(target, thisArg, args, rerunnable) {
|
|
1375
|
+
Reflect.apply(target, thisArg, args.map((arg) => isFunction(arg) ? defaultLane(arg, rerunnable) : arg));
|
|
1376
|
+
}
|
|
1377
|
+
function memberOrOverride(target, overrides, property, receiver) {
|
|
1378
|
+
return Object.hasOwn(overrides, property) ? memberOf(overrides, property, receiver) : memberOf(target, property, receiver);
|
|
1379
|
+
}
|
|
1380
|
+
function memberOf(target, property, receiver) {
|
|
1381
|
+
return Reflect.get(target, property, receiver);
|
|
1382
|
+
}
|
|
1383
|
+
const describeImpl = (name, f) => V.describe(name, {
|
|
1384
|
+
concurrent: true,
|
|
1385
|
+
shuffle: true
|
|
1386
|
+
}, (it) => f(makeMethods$1(it)));
|
|
1387
|
+
/** @internal */
|
|
1388
|
+
const describeWrapped$1 = Function.dual(byName, describeImpl);
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/mod.ts
|
|
1391
|
+
/**
|
|
1392
|
+
* @since 4.0.0
|
|
1393
|
+
*/
|
|
1394
|
+
const addEqualityTesters = addEqualityTesters$1;
|
|
1395
|
+
/**
|
|
1396
|
+
* @since 4.0.0
|
|
1397
|
+
*/
|
|
1398
|
+
const effect = effect$1;
|
|
1399
|
+
/**
|
|
1400
|
+
* @since 4.0.0
|
|
1401
|
+
*/
|
|
1402
|
+
const live = live$1;
|
|
1403
|
+
/**
|
|
1404
|
+
* Share a `Layer` between multiple tests, optionally wrapping
|
|
1405
|
+
* the tests in a `describe` block if a name is provided.
|
|
1406
|
+
*
|
|
1407
|
+
* Named layers accept `concurrent` to override inherited suite concurrency.
|
|
1408
|
+
* Anonymous layers always inherit the enclosing suite's concurrency.
|
|
1409
|
+
* Use `ctx.expect` in concurrent tests for test-local snapshots and assertion counts.
|
|
1410
|
+
*
|
|
1411
|
+
* @since 4.0.0
|
|
1412
|
+
*
|
|
1413
|
+
* ```ts
|
|
1414
|
+
* import { assert, layer } from "@systemfsoftware/vitest"
|
|
1415
|
+
* import { Effect, Layer, Context } from "effect"
|
|
1416
|
+
*
|
|
1417
|
+
* class Foo extends Context.Service<Foo, "foo">()("Foo") {
|
|
1418
|
+
* static layer = Layer.succeed(Foo, "foo")
|
|
1419
|
+
* }
|
|
1420
|
+
*
|
|
1421
|
+
* class Bar extends Context.Service<Bar, "bar">()("Bar") {
|
|
1422
|
+
* static layer = Layer.effect(
|
|
1423
|
+
* Bar,
|
|
1424
|
+
* Effect.map(Foo, () => "bar" as const)
|
|
1425
|
+
* )
|
|
1426
|
+
* }
|
|
1427
|
+
*
|
|
1428
|
+
* layer(Foo.layer)("layer", (it) => {
|
|
1429
|
+
* it.effect("adds context", () =>
|
|
1430
|
+
* Effect.gen(function*() {
|
|
1431
|
+
* const foo = yield* Foo
|
|
1432
|
+
* assert.strictEqual(foo, "foo")
|
|
1433
|
+
* }))
|
|
1434
|
+
*
|
|
1435
|
+
* it.layer(Bar.layer)("nested", (it) => {
|
|
1436
|
+
* it.effect("adds context", () =>
|
|
1437
|
+
* Effect.gen(function*() {
|
|
1438
|
+
* const foo = yield* Foo
|
|
1439
|
+
* const bar = yield* Bar
|
|
1440
|
+
* assert.strictEqual(foo, "foo")
|
|
1441
|
+
* assert.strictEqual(bar, "bar")
|
|
1442
|
+
* }))
|
|
1443
|
+
* })
|
|
1444
|
+
* })
|
|
1445
|
+
* ```
|
|
1446
|
+
*/
|
|
1447
|
+
const layer = layer$1;
|
|
1448
|
+
/**
|
|
1449
|
+
* @since 4.0.0
|
|
1450
|
+
*/
|
|
1451
|
+
const flakyTest = flakyTest$1;
|
|
1452
|
+
/**
|
|
1453
|
+
* @since 4.0.0
|
|
1454
|
+
*/
|
|
1455
|
+
const prop = prop$1;
|
|
1456
|
+
/**
|
|
1457
|
+
* @since 4.0.0
|
|
1458
|
+
*/
|
|
1459
|
+
const owned = owned$1;
|
|
1460
|
+
/**
|
|
1461
|
+
* @since 4.0.0
|
|
1462
|
+
*/
|
|
1463
|
+
const recordAssertion = recordAssertion$1;
|
|
1464
|
+
/**
|
|
1465
|
+
* The run binding of the test the effect is executing in. `bind` re-provides it
|
|
1466
|
+
* to an effect a library runs on a runtime of its own — its own scheduler, a
|
|
1467
|
+
* worker, a simulation kernel — so the checks inside it count as this test's
|
|
1468
|
+
* assertions, report softly, and see the same `owned` regions as the test.
|
|
1469
|
+
*
|
|
1470
|
+
* @since 4.0.0
|
|
1471
|
+
*/
|
|
1472
|
+
const captureRunBinding = captureRunBinding$1;
|
|
1473
|
+
/**
|
|
1474
|
+
* The key the running test's context is published under. A library that carries its own view of the task
|
|
1475
|
+
* context builds that view on this key, so the context a case lane sees and a property lane sees agree.
|
|
1476
|
+
*
|
|
1477
|
+
* @since 4.0.0
|
|
1478
|
+
*/
|
|
1479
|
+
const vitestTestContextKey = vitestTestContextKey$1;
|
|
1480
|
+
/**
|
|
1481
|
+
* The running Vitest `TestContext`, provided on every test the fork runs — the Effect lanes, both property
|
|
1482
|
+
* lanes, and each `it.each` row. It is `null` outside a run.
|
|
1483
|
+
*
|
|
1484
|
+
* @since 4.0.0
|
|
1485
|
+
*/
|
|
1486
|
+
const VitestTestContext = VitestTestContext$1;
|
|
1487
|
+
/**
|
|
1488
|
+
* @since 4.0.0
|
|
1489
|
+
*/
|
|
1490
|
+
const it = makeMethods$1(V.it);
|
|
1491
|
+
/**
|
|
1492
|
+
* @since 4.0.0
|
|
1493
|
+
*/
|
|
1494
|
+
const makeMethods = makeMethods$1;
|
|
1495
|
+
/**
|
|
1496
|
+
* @since 4.0.0
|
|
1497
|
+
*/
|
|
1498
|
+
const describeWrapped = describeWrapped$1;
|
|
1499
|
+
const describe = describeWrapped$1;
|
|
1500
|
+
/**
|
|
1501
|
+
* The fork's `expect`: soft within one Effect step, refusing the slop forms.
|
|
1502
|
+
*
|
|
1503
|
+
* @since 4.0.0
|
|
1504
|
+
*/
|
|
1505
|
+
const expect = expect$1;
|
|
1506
|
+
/**
|
|
1507
|
+
* @since 4.0.0
|
|
1508
|
+
*/
|
|
1509
|
+
const beforeEach = beforeEach$1;
|
|
1510
|
+
/**
|
|
1511
|
+
* @since 4.0.0
|
|
1512
|
+
*/
|
|
1513
|
+
const afterEach = afterEach$1;
|
|
1514
|
+
/**
|
|
1515
|
+
* @since 4.0.0
|
|
1516
|
+
*/
|
|
1517
|
+
const beforeAll = beforeAll$1;
|
|
1518
|
+
/**
|
|
1519
|
+
* @since 4.0.0
|
|
1520
|
+
*/
|
|
1521
|
+
const afterAll = afterAll$1;
|
|
1522
|
+
//#endregion
|
|
1523
|
+
export { VitestTestContext, addEqualityTesters, afterAll, afterEach, beforeAll, beforeEach, captureRunBinding, describe, describeWrapped, effect, expect, flakyTest, it, layer, live, makeMethods, owned, prop, recordAssertion, vitestTestContextKey };
|