@systemfsoftware/vitest 0.0.0 → 0.2.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 +24 -0
- package/README.md +113 -61
- package/dist/TestClock.d.ts +1 -1
- package/dist/checks-Cq9of-Lx.d.ts +336 -0
- package/dist/engine-YsoQ_9we.d.ts +158 -0
- package/dist/failure-BquJJPFZ.mjs +1217 -0
- package/dist/failure.d.ts +80 -0
- package/dist/failure.mjs +4 -0
- package/dist/guard-p_8ARgPm.mjs +250 -0
- package/dist/guard.d.ts +2 -0
- package/dist/guard.mjs +19 -0
- package/dist/index.d.ts +203 -440
- package/dist/index.mjs +376 -1055
- package/dist/integration.d.ts +37 -0
- package/dist/integration.mjs +38 -0
- package/dist/test-context-ChvjVyCo.mjs +328 -0
- package/package.json +17 -12
- package/dist/refusals-BQtDszyU.mjs +0 -32
- package/dist/refusals-_6e3GzVK.d.ts +0 -37
- package/dist/refusals.d.ts +0 -3
- package/dist/refusals.mjs +0 -3
- package/dist/utils.d.ts +0 -197
- package/dist/utils.mjs +0 -227
package/dist/index.mjs
CHANGED
|
@@ -1,59 +1,88 @@
|
|
|
1
1
|
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as replayOfFailure, c as isFailureRecordError, f as InvalidBudget, h as Slop, i as makeProperty, l as throwFailureRecord, m as NonBooleanVerdict, o as testIdentityOf, p as LeakedState, r as createSpanRecorder, s as providedRoot } from "./failure-BquJJPFZ.mjs";
|
|
3
|
+
import { a as isCheck, i as checksFor, n as vitestTestContextKey$1, o as makeLedger, r as Asserted, t as VitestTestContext$1 } from "./test-context-ChvjVyCo.mjs";
|
|
4
|
+
import { S as unprovidedText, _ as refuseScopedLiveLane, d as refuseAsyncBody, f as refuseEffectLane, g as refuseScopedLane, i as markTask, l as noCheckRefusal, o as isRefusal$1, p as refuseHook, u as refusalOf$1, v as refuseSyncBody, y as refuseUnyielded } from "./guard-p_8ARgPm.mjs";
|
|
3
5
|
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";
|
|
6
|
+
import { afterAll, assertType, beforeAll, expectTypeOf, inject, onTestFailed, onTestFinished, recordArtifact, vi, vitest } from "vitest";
|
|
11
7
|
import * as Cause from "effect/Cause";
|
|
12
8
|
import * as Duration from "effect/Duration";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
13
10
|
import * as Exit from "effect/Exit";
|
|
11
|
+
import * as Function from "effect/Function";
|
|
14
12
|
import * as Layer from "effect/Layer";
|
|
15
13
|
import * as Schedule from "effect/Schedule";
|
|
16
|
-
import * as
|
|
17
|
-
import * as Schema$1 from "effect/Schema";
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
18
15
|
import * as Scope from "effect/Scope";
|
|
19
16
|
import * as TestClock from "effect/testing/TestClock";
|
|
20
17
|
import * as TestConsole from "effect/testing/TestConsole";
|
|
21
|
-
import * as
|
|
22
|
-
import * as Data from "effect/Data";
|
|
18
|
+
import * as Equal from "effect/Equal";
|
|
23
19
|
import * as Clock from "effect/Clock";
|
|
24
|
-
|
|
25
|
-
//#region src/internal/
|
|
20
|
+
import * as Scheduler from "effect/Scheduler";
|
|
21
|
+
//#region src/internal/driver.ts
|
|
26
22
|
/**
|
|
27
|
-
* The
|
|
28
|
-
* `Fiber.getCurrent`, so no node-only store is needed (KTD4).
|
|
23
|
+
* The driver: one Effect that steps the body's generator in the fiber the test runs in.
|
|
29
24
|
*
|
|
30
|
-
*
|
|
25
|
+
* A `yield*` is a step, so the runner sees every one of them (KTD1):
|
|
26
|
+
* - a branded check is judged against this test's ledger, so it counts, and a failed judgement fails the check
|
|
27
|
+
* itself and stops the test right there (R8);
|
|
28
|
+
* - anything else opens a new observed state and then runs, which is what makes a second check on one state a
|
|
29
|
+
* second check in one window (R5), and a check inside a loop the same window as its siblings.
|
|
30
|
+
*
|
|
31
|
+
* The steps run in the same fiber as the test, so layers, virtual time, concurrency, the second run and scope
|
|
32
|
+
* finalizers keep working through the runner unchanged. A step that fails, and the test being interrupted, both
|
|
33
|
+
* return the generator: its `finally` blocks run and the steps after it never start (R8). When the body returns,
|
|
34
|
+
* the gate refuses what the body wrote and never judged, then a body that judged nothing at all (R4).
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* A value the body yielded, as the driver runs it. What a body needs of its environment belongs to the layer the
|
|
38
|
+
* runner provides around this whole Effect, so the driver erases the channels here, exactly as the runner erases
|
|
39
|
+
* a body's own (`outputIsEffect` in `runner.ts`): the layer block is where a requirement is checked, and a body
|
|
40
|
+
* that needs a service nothing provides is refused by the runner at run time (R9).
|
|
41
|
+
*/
|
|
42
|
+
const isRunnable = (value) => Effect.isEffect(value);
|
|
43
|
+
/** A body that yielded something the driver cannot run ran nothing, so nothing in the test could fail. */
|
|
44
|
+
const refuseUnsteppable = "✗ the body yielded a value that is not an Effect. Yield the Effect itself, or a check: yield* expect(actual).toEqual(expected).";
|
|
45
|
+
/**
|
|
46
|
+
* A yielded Effect: a branded check judges itself, and anything else opens a new observed state before it runs,
|
|
47
|
+
* which is what resets the ledger's one-check window (R5).
|
|
48
|
+
*/
|
|
49
|
+
const runEffect = (value, ledger) => isCheck(value) ? value : Effect.andThen(Effect.sync(() => ledger.asserted.step()), value);
|
|
50
|
+
/** One step: anything the driver cannot run is refused, because then no step of the test ran at all. */
|
|
51
|
+
const runStep = (value, ledger) => isRunnable(value) ? runEffect(value, ledger) : Effect.die(refusalOf$1(refuseUnsteppable));
|
|
52
|
+
/** Steps the iterator to its end, feeding each yielded Effect's result back into the body. */
|
|
53
|
+
const stepThrough = (steps, ledger, input) => Effect.suspend(() => {
|
|
54
|
+
const state = steps.next(input);
|
|
55
|
+
if (state.done === true) return Effect.void;
|
|
56
|
+
return Effect.flatMap(runStep(state.value, ledger), (value) => stepThrough(steps, ledger, value));
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* After the body returns: a check written and never yielded is refused first, then a body that judged none.
|
|
60
|
+
* Both carry the same text the refusal carries on its compile channel, branded by `refusalOf` so the runner's
|
|
61
|
+
* second run re-throws them as the refusals they are.
|
|
31
62
|
*/
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return body();
|
|
42
|
-
} finally {
|
|
43
|
-
syncRun = previous;
|
|
44
|
-
}
|
|
63
|
+
const gate = (ledger) => Effect.suspend(() => ledger.unyielded() > 0 ? refuse(refuseUnyielded) : judgedNone(ledger));
|
|
64
|
+
const judgedNone = (ledger) => ledger.judged() === 0 ? refuse(noCheckRefusal) : Effect.void;
|
|
65
|
+
const refuse = (text) => Effect.die(refusalOf$1(text));
|
|
66
|
+
/**
|
|
67
|
+
* A failure or an interruption returns the generator, so its `finally` blocks run and the step after the failed
|
|
68
|
+
* check never does (R8).
|
|
69
|
+
*/
|
|
70
|
+
const unwind = (steps) => Effect.sync(() => {
|
|
71
|
+
steps.return?.(void 0);
|
|
45
72
|
});
|
|
73
|
+
const driveBody = (body, args, ledger) => Effect.suspend(() => {
|
|
74
|
+
const steps = body(...args);
|
|
75
|
+
return Effect.onExit(Effect.flatMap(stepThrough(steps, ledger, void 0), () => gate(ledger)), (exit) => Exit.isSuccess(exit) ? Effect.void : unwind(steps));
|
|
76
|
+
});
|
|
77
|
+
/** The body's steps, with this test's ledger provided as the `Asserted` every check in it reaches for. */
|
|
78
|
+
const driven = (body, args, ledger) => Effect.provideService(driveBody(body, args, ledger), Asserted, ledger.asserted);
|
|
46
79
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
80
|
+
* Runs one test's body: `body` called with `args`, stepped against `ledger`, with the ledger's `asserted`
|
|
81
|
+
* provided as `Asserted` so every check in the body — including one yielded inside a forked fiber — reaches it.
|
|
49
82
|
*
|
|
50
83
|
* @internal
|
|
51
84
|
*/
|
|
52
|
-
const
|
|
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;
|
|
85
|
+
const drive = Function.dual(3, driven);
|
|
57
86
|
//#endregion
|
|
58
87
|
//#region src/internal/equal.ts
|
|
59
88
|
let registered = false;
|
|
@@ -67,653 +96,6 @@ const equalTester = (first, second) => equalTogether(first, second) ? true : voi
|
|
|
67
96
|
const equalTogether = (first, second) => bothEqualable(first, second) && Equal.equals(first, second);
|
|
68
97
|
const bothEqualable = (first, second) => Equal.isEqual(first) && Equal.isEqual(second);
|
|
69
98
|
//#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
99
|
//#region src/internal/virtual-time.ts
|
|
718
100
|
const roundNanos = (millis) => BigInt(Math.round(millis * 1e6));
|
|
719
101
|
const byWakeOrder = (first, second) => {
|
|
@@ -885,6 +267,18 @@ const virtualClockOf = (runtime) => {
|
|
|
885
267
|
};
|
|
886
268
|
//#endregion
|
|
887
269
|
//#region src/internal/runner.ts
|
|
270
|
+
/**
|
|
271
|
+
* The fork's runner: the test lanes, the layer blocks, the property lanes and the run pipeline.
|
|
272
|
+
*
|
|
273
|
+
* A test body is a generator the driver steps (KTD1), so `expect` is the body's own parameter and every check is
|
|
274
|
+
* yielded. Everything #512 promised holds unchanged: a fresh build of a test's layers, virtual time that advances
|
|
275
|
+
* when the fibers are idle, concurrent and shuffled blocks, a declared-shared block as the only sharing, a second
|
|
276
|
+
* run on a fresh build that reports leaked state, and Effect `Equal` in `toEqual`.
|
|
277
|
+
*
|
|
278
|
+
* What is a refusal here: a body that is not a generator, and the habit lanes `it.effect`, `it.scoped` and
|
|
279
|
+
* `it.scopedLive` (R2, R9). Registration marks every task this file adds (KTD8), so the guard in
|
|
280
|
+
* `@systemfsoftware/vitest/guard` can tell the fork's tests from a test registered by Vitest itself.
|
|
281
|
+
*/
|
|
888
282
|
const getCurrentSuite = V.TestRunner.getCurrentSuite;
|
|
889
283
|
/** @internal */
|
|
890
284
|
const addEqualityTesters$1 = () => {
|
|
@@ -896,80 +290,73 @@ const givenOptions = (timeout) => timeout ?? {};
|
|
|
896
290
|
const testOptions = (timeout) => typeof timeout === "number" ? numbered(timeout) : givenOptions(timeout);
|
|
897
291
|
/** @internal */
|
|
898
292
|
const hookTimeout = (timeout) => timeout === void 0 ? void 0 : Duration.toMillis(Duration.fromInputUnsafe(timeout));
|
|
293
|
+
const isName = (first) => typeof first === "string";
|
|
294
|
+
const byName = (args) => isName(args[0]);
|
|
295
|
+
const byLayer = (args) => Layer.isLayer(args[0]);
|
|
296
|
+
const byEffect = (args) => Effect.isEffect(args[0]);
|
|
899
297
|
/**
|
|
900
|
-
* The positional form is refused at run time with the R15 rewrite; the public type
|
|
298
|
+
* The positional property form is refused at run time with the R15 rewrite; the public type refuses it first
|
|
299
|
+
* because a `PropertySpec` is an object and the positional shape is not.
|
|
901
300
|
*/
|
|
301
|
+
const refusePositionalProp = "✗ it.prop(name, [arbitraries], predicate) is removed. Name the function under test and pass a budget: it.prop(name, { of, subject, runs }, holds).";
|
|
302
|
+
const isAbsent = (spec) => spec === null || spec === void 0;
|
|
303
|
+
const hasNoOf = (spec) => Object.hasOwn(spec, "of") === false;
|
|
304
|
+
const lacksInputs = (spec) => isAbsent(spec) || hasNoOf(spec);
|
|
902
305
|
const refusePositional = (spec) => {
|
|
903
306
|
if (lacksInputs(spec)) throw new Slop({ detail: refusePositionalProp });
|
|
904
307
|
};
|
|
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
308
|
/** @internal */
|
|
911
309
|
const realTime = {
|
|
912
310
|
runtime: void 0,
|
|
913
|
-
owned: false
|
|
914
|
-
stop: stopCheckOf
|
|
311
|
+
owned: false
|
|
915
312
|
};
|
|
916
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* A run with its own virtual clock: the clock only ever advances when the run's fibers are idle.
|
|
315
|
+
*
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
917
318
|
const perRun = () => ({
|
|
918
319
|
runtime: makeVirtualRuntime(),
|
|
919
|
-
owned: true
|
|
920
|
-
stop: stopCheckOf
|
|
320
|
+
owned: true
|
|
921
321
|
});
|
|
922
|
-
const propertyEnv =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
const failedAfter = (ctx, before) => () => errorCount(ctx) > before;
|
|
322
|
+
const propertyEnv = realTime;
|
|
323
|
+
const payloadOf = (reason) => Cause.isFailReason(reason) ? reason.error : reasonDefectOf(reason);
|
|
324
|
+
const reasonDefectOf = (reason) => Cause.isDieReason(reason) ? reason.defect : void 0;
|
|
325
|
+
const isLoggable = (thrown) => (reason) => !Cause.isInterruptReason(reason) && payloadOf(reason) !== thrown;
|
|
326
|
+
const unthrownOf = (cause) => cause.pipe(Cause.squash, isLoggable, (loggable) => Cause.fromReasons(cause.reasons.filter(loggable)));
|
|
327
|
+
const logUnthrownErrors = (cause) => cause.pipe(unthrownOf, Cause.prettyErrors, Effect.forEach((error) => Effect.logError(error), { discard: true }));
|
|
929
328
|
const signalOf = (ctx) => ctx?.signal;
|
|
930
329
|
const signalOnly = (signal) => signal === void 0 ? {} : { signal };
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
330
|
+
const schedulerOf = (env) => env.runtime === void 0 ? {} : { scheduler: env.runtime.scheduler };
|
|
331
|
+
const runOptions = (ctx, env) => ({
|
|
332
|
+
...signalOnly(signalOf(ctx)),
|
|
333
|
+
...schedulerOf(env)
|
|
934
334
|
});
|
|
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
335
|
const ownedRuntime = (env) => env.owned === false ? void 0 : env.runtime;
|
|
948
336
|
const observeOf = (runtime) => runtime === void 0 ? Effect.void : runtime.observe;
|
|
949
337
|
const endOf = (runtime) => runtime === void 0 ? Effect.void : runtime.end;
|
|
950
338
|
const wrapOwned = (effect, runtime) => observeOf(runtime).pipe(Effect.andThen(effect.pipe(Effect.ensuring(endOf(runtime)))));
|
|
951
|
-
const
|
|
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);
|
|
339
|
+
const runExit = (effect, ctx, env) => Effect.runPromiseExit(wrapOwned(effect, ownedRuntime(env)), runOptions(ctx, env));
|
|
956
340
|
/**
|
|
957
341
|
* 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
|
-
*
|
|
960
|
-
* and never reach here.
|
|
342
|
+
* test-authoring error — the layers were never wired — so it is reported as the unprovided rewrite (R9) instead
|
|
343
|
+
* of as a raw defect.
|
|
961
344
|
*/
|
|
962
345
|
const isUnprovided = (error) => error instanceof Error && error.message.startsWith("Service not found");
|
|
963
|
-
const
|
|
346
|
+
const refuseUnprovided = (failure) => {
|
|
347
|
+
if (isUnprovided(failure)) throw new Slop({ detail: unprovidedText });
|
|
348
|
+
};
|
|
964
349
|
const rethrowSquashed = (cause) => {
|
|
965
350
|
const error = Cause.squash(cause);
|
|
966
|
-
|
|
351
|
+
refuseUnprovided(error);
|
|
967
352
|
throw error;
|
|
968
353
|
};
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
354
|
+
const failExit = (cause) => logUnthrownErrors(cause).pipe(Effect.runPromise).then(() => rethrowSquashed(cause));
|
|
355
|
+
const reportExit = (exit) => Exit.isSuccess(exit) ? Promise.resolve(exit.value) : failExit(exit.cause);
|
|
356
|
+
const runPromise = (effect, ctx, env = realTime) => runExit(effect, ctx, env).then(reportExit);
|
|
357
|
+
const noop = () => {};
|
|
358
|
+
const detach = (ctx, onAbort) => {
|
|
359
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
973
360
|
};
|
|
974
361
|
const trackAbort = (ctx, promise) => {
|
|
975
362
|
const onAbort = () => {
|
|
@@ -979,168 +366,126 @@ const trackAbort = (ctx, promise) => {
|
|
|
979
366
|
promise.then(() => detach(ctx, onAbort), () => detach(ctx, onAbort));
|
|
980
367
|
if (ctx.signal.aborted) onAbort();
|
|
981
368
|
};
|
|
982
|
-
const
|
|
983
|
-
|
|
369
|
+
const rethrowRendered = (failure) => {
|
|
370
|
+
if (isFailureRecordError(failure)) throw failure;
|
|
371
|
+
};
|
|
372
|
+
const throwRecorded = (cause, recorder, ctx) => {
|
|
373
|
+
const failure = Cause.squash(cause);
|
|
374
|
+
rethrowRendered(failure);
|
|
375
|
+
refuseUnprovided(failure);
|
|
376
|
+
return throwFailureRecord({
|
|
377
|
+
failure,
|
|
378
|
+
spans: recorder.spans,
|
|
379
|
+
identity: testIdentityOf(ctx.task),
|
|
380
|
+
replay: replayOfFailure(failure),
|
|
381
|
+
root: providedRoot()
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
const failRecorded = (cause, recorder, ctx) => logUnthrownErrors(cause).pipe(Effect.runPromise).then(() => throwRecorded(cause, recorder, ctx));
|
|
385
|
+
const reportRecorded = (exit, recorder, ctx) => Exit.isSuccess(exit) ? Promise.resolve(exit.value) : failRecorded(exit.cause, recorder, ctx);
|
|
386
|
+
/**
|
|
387
|
+
* One test's run, recorded: its own in-memory tracer collects the spans the record is rendered from, and a failure
|
|
388
|
+
* leaves as the rendered record instead of a raw Cause (KTD2, R2, R7, R8).
|
|
389
|
+
*/
|
|
390
|
+
const runRecorded = (effect, ctx, env) => {
|
|
391
|
+
const recorder = createSpanRecorder();
|
|
392
|
+
const promise = runExit(effect.pipe(Effect.withTracer(recorder.tracer)), ctx, env).then((exit) => reportRecorded(exit, recorder, ctx));
|
|
393
|
+
trackAbort(ctx, promise);
|
|
394
|
+
return promise;
|
|
984
395
|
};
|
|
985
|
-
const noop = () => {};
|
|
986
396
|
const clockLayerOf = (env) => env.runtime === void 0 ? TestClock.layer() : virtualClockLayer(env.runtime);
|
|
987
397
|
/** @internal */
|
|
988
398
|
const testEnv = (env) => Layer.mergeAll(TestConsole.layer, clockLayerOf(env));
|
|
989
399
|
/**
|
|
990
|
-
* The run a test's fibers execute in: the test's own context
|
|
991
|
-
*
|
|
992
|
-
*/
|
|
993
|
-
const bindRun = (effect, ctx
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
400
|
+
* The run a test's fibers execute in: the test's own context under the fork's `VitestTestContext`, so a library
|
|
401
|
+
* built on the fork reads the same context the runner does.
|
|
402
|
+
*/
|
|
403
|
+
const bindRun = (effect, ctx) => effect.pipe(Effect.provideService(VitestTestContext$1, ctx));
|
|
404
|
+
const refuseBody = (text) => {
|
|
405
|
+
throw refusalOf$1(text);
|
|
406
|
+
};
|
|
407
|
+
/** The constructor names of the function kinds that hand back a promise instead of a generator. */
|
|
408
|
+
const AWAITED_KINDS = {
|
|
409
|
+
AsyncFunction: true,
|
|
410
|
+
AsyncGeneratorFunction: true
|
|
411
|
+
};
|
|
412
|
+
/** The constructor name of the one function kind the driver steps: a generator hands its steps over. */
|
|
413
|
+
const GENERATOR_KIND = "GeneratorFunction";
|
|
414
|
+
const kindOf = (body) => body.constructor.name;
|
|
415
|
+
const isGeneratorBody = (body) => kindOf(body) === GENERATOR_KIND;
|
|
416
|
+
const isSteps = (returned) => "next" in returned && typeof returned.next === "function";
|
|
417
|
+
/** The iterator a generator handed over; only a generator reaches here, so the guard is for the compiler alone. */
|
|
418
|
+
const asSteps = (returned) => isSteps(returned) ? returned : refuseBody(refuseSyncBody);
|
|
419
|
+
/**
|
|
420
|
+
* The refusal a body reaches before it runs (R2). An `async` body hands back a promise and runs outside the test
|
|
421
|
+
* runtime. A plain body can only be told from one by calling it, and nothing in a refused body may run, so it is
|
|
422
|
+
* refused as a sync body, whose rewrite is to pass a generator; the compile channel prints each shape's own text.
|
|
423
|
+
*/
|
|
424
|
+
const refusalOf = (body) => AWAITED_KINDS[kindOf(body)] === true ? refuseAsyncBody : refuseSyncBody;
|
|
425
|
+
/** The body's generator, or the refusal for a body the driver cannot step. */
|
|
426
|
+
const stepsOfBody = (body, checks) => isGeneratorBody(body) ? asSteps(body(checks)) : refuseBody(refusalOf(body));
|
|
427
|
+
/** The same for a row body: Vitest's `it.for` order is the row first, then the checks (R9). */
|
|
428
|
+
const stepsOfRow = (row, body, checks) => isGeneratorBody(body) ? asSteps(body(row, checks)) : refuseBody(refusalOf(body));
|
|
429
|
+
const bodyOf = (ledger, body) => () => asSteps(body(checksFor(ledger)));
|
|
430
|
+
/** The body a lane registered, closed over its checks: the generator itself, or the refusal for what it is not. */
|
|
431
|
+
const laneBodyOf = (body) => (checks) => typeof body === "string" ? refuseBody(refuseSyncBody) : stepsOfBody(body, checks);
|
|
432
|
+
/** The row body a lane registered: Vitest's `it.for` order is the row first, then the checks (R9). */
|
|
433
|
+
const rowBodyOf = (row, body) => (checks) => typeof body === "string" ? refuseBody(refuseSyncBody) : stepsOfRow(row, body, checks);
|
|
434
|
+
/** One test's run: a fresh ledger, and the lane's own services around the program the driver built. */
|
|
435
|
+
const runLaned = (ctx, mapEffect, env, body) => {
|
|
436
|
+
const ledger = makeLedger(ctx);
|
|
437
|
+
return runRecorded(mapEffect(bindRun(drive(bodyOf(ledger, body), [], ledger), ctx), env), ctx, env);
|
|
1002
438
|
};
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
439
|
+
const outcomeOf = (promise) => promise.then(() => void 0, (error) => error);
|
|
440
|
+
const isSlop = (error) => Schema.is(Slop)(error);
|
|
441
|
+
/** Every failure the fork itself names: the branded refusals, plus the Schema-tagged families. */
|
|
442
|
+
const otherRefusals = [
|
|
443
|
+
isSlop,
|
|
444
|
+
Schema.is(InvalidBudget),
|
|
445
|
+
Schema.is(NonBooleanVerdict),
|
|
446
|
+
Schema.is(LeakedState)
|
|
447
|
+
];
|
|
448
|
+
const isRefusal = (error) => isRefusal$1(error) || otherRefusals.some((matches) => matches(error));
|
|
449
|
+
const clipped = (message) => message.length > 400 ? message.slice(0, 400) : message;
|
|
450
|
+
const throwLeaked = (outcome) => {
|
|
451
|
+
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) });
|
|
1006
452
|
};
|
|
1007
|
-
|
|
1008
|
-
|
|
453
|
+
/**
|
|
454
|
+
* The second run's failure: a diagnosis the fork itself made is rethrown as that diagnosis, because the second
|
|
455
|
+
* run exists to catch leaked state, not to relabel a refusal as a leak. Anything else is the leak.
|
|
456
|
+
*/
|
|
457
|
+
const throwSecondRun = (error) => {
|
|
458
|
+
if (isRefusal(error)) throw error;
|
|
459
|
+
return throwLeaked(error);
|
|
1009
460
|
};
|
|
1010
|
-
const
|
|
1011
|
-
|
|
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);
|
|
461
|
+
const leakedOrNothing = (error) => {
|
|
462
|
+
if (error !== void 0) throwSecondRun(error);
|
|
1018
463
|
};
|
|
1019
|
-
const
|
|
464
|
+
const NO_ERRORS = [];
|
|
465
|
+
const presentErrors = (errors) => errors === void 0 ? NO_ERRORS : errors;
|
|
466
|
+
/** Whether a run left no failure behind: only a clean first run is worth running a second time. */
|
|
467
|
+
const isClean = (ctx) => presentErrors(ctx.task.result?.errors).length === 0;
|
|
1020
468
|
/**
|
|
1021
|
-
* Runs a property's program as the test that registered it, under the same
|
|
1022
|
-
*
|
|
1023
|
-
* `propertyEnv`: no step boundary interrupts a property, so its `false` verdict still shrinks (R6, U4).
|
|
469
|
+
* Runs a property's program as the test that registered it, under the same context the generator lanes use. The
|
|
470
|
+
* env is the bare `propertyEnv`: a `false` verdict is the property's own shrink path, so nothing interrupts it.
|
|
1024
471
|
*/
|
|
1025
|
-
const runProperty = (ctx, program) =>
|
|
472
|
+
const runProperty = (ctx, program) => runRecorded(bindRun(Effect.suspend(program), ctx), ctx, propertyEnv);
|
|
1026
473
|
const property = makeProperty({
|
|
1027
|
-
register: (name, program) =>
|
|
1028
|
-
V.it(name, (ctx) => runProperty(ctx, program));
|
|
1029
|
-
},
|
|
474
|
+
register: (name, program) => marked(() => V.it(name, (ctx) => runProperty(ctx, program))),
|
|
1030
475
|
provide: (effect) => effect
|
|
1031
476
|
});
|
|
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
477
|
const propImpl = (name, spec, holds) => {
|
|
1037
478
|
refusePositional(spec);
|
|
1038
479
|
property.prop(name, spec, holds);
|
|
1039
480
|
};
|
|
1040
481
|
/** @internal */
|
|
1041
482
|
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
483
|
/**
|
|
1130
|
-
* The
|
|
1131
|
-
*
|
|
484
|
+
* The law kinds over the sync property family: the top-level `it.law`.
|
|
485
|
+
*
|
|
486
|
+
* @internal
|
|
1132
487
|
*/
|
|
1133
|
-
const
|
|
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
|
-
};
|
|
488
|
+
const law$1 = property.law;
|
|
1144
489
|
const CONCURRENT_BLOCK = {
|
|
1145
490
|
concurrent: true,
|
|
1146
491
|
shuffle: true
|
|
@@ -1158,25 +503,24 @@ const memoMapIn = (options) => options?.memoMap;
|
|
|
1158
503
|
const excludeTestServicesOf = (options) => withDefault(excludedIn(options), false);
|
|
1159
504
|
const blockEnvOf = (inherited) => withDefault(inherited, {
|
|
1160
505
|
runtime: makeVirtualRuntime(),
|
|
1161
|
-
owned: false
|
|
1162
|
-
stop: stopCheckOf
|
|
506
|
+
owned: false
|
|
1163
507
|
});
|
|
1164
508
|
const memoMapOf = (options) => withDefault(memoMapIn(options), Effect.runSync(Layer.makeMemoMap));
|
|
1165
509
|
const blockHookTimeout = (options) => hookTimeout(timeoutIn(options));
|
|
1166
510
|
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
511
|
const mergeBlockOptions = (options, fallback) => ({
|
|
1174
512
|
concurrent: withDefault(concurrencyIn(options), fallback.concurrent),
|
|
1175
513
|
shuffle: fallback.shuffle
|
|
1176
514
|
});
|
|
1177
515
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
* is
|
|
516
|
+
* A named block's suite options: its tests run concurrently and in shuffled order (R4). A declared-shared block
|
|
517
|
+
* is the stateful exception — its tests read each other's writes through one build, so it stays in declaration
|
|
518
|
+
* order and runs alone.
|
|
519
|
+
*/
|
|
520
|
+
const describeOptions = (options, shared) => mergeBlockOptions(options, shared ? ALONE_BLOCK : CONCURRENT_BLOCK);
|
|
521
|
+
/**
|
|
522
|
+
* The block's layer with the test's own services merged in, unless the block excludes them. `Layer`'s output is
|
|
523
|
+
* contravariant, so the merged layer is the *narrower* `Layer<R, E>` the callers provide to a run.
|
|
1180
524
|
*/
|
|
1181
525
|
const mergeTestEnv = (layer_, env, excludeTestServices) => excludeTestServices ? layer_ : Layer.provideMerge(layer_, env);
|
|
1182
526
|
const sharedContext = (shared, memoMap, scope) => Layer.buildWithMemoMap(shared, memoMap, scope).pipe(Effect.orDie, Effect.cached, Effect.runSync);
|
|
@@ -1200,6 +544,31 @@ const routeBlock = (first, second, anonymous, named) => {
|
|
|
1200
544
|
const openNamed = (name, f, named) => {
|
|
1201
545
|
if (f !== void 0) named(name, f);
|
|
1202
546
|
};
|
|
547
|
+
/** Runs a registration and marks every test task it added, which is how the guard knows the fork registered it. */
|
|
548
|
+
const marked = (register) => {
|
|
549
|
+
const before = suiteTasks(getCurrentSuite()).length;
|
|
550
|
+
register();
|
|
551
|
+
markAdded(before);
|
|
552
|
+
};
|
|
553
|
+
const markAdded = (before) => {
|
|
554
|
+
for (const task of collectTasks(suiteTasks(getCurrentSuite()).slice(before))) markTask(task);
|
|
555
|
+
};
|
|
556
|
+
const blockTasksOf = () => suiteTasks(getCurrentSuite());
|
|
557
|
+
const suiteTasks = (suite) => suite.tasks;
|
|
558
|
+
const collectTasks = (tasks) => {
|
|
559
|
+
const acc = [];
|
|
560
|
+
for (const task of tasks) collectOne(task, acc);
|
|
561
|
+
return acc;
|
|
562
|
+
};
|
|
563
|
+
const collectNested = (task, acc) => {
|
|
564
|
+
if (task.tasks !== void 0) collectTasks(task.tasks).forEach((nested) => acc.push(nested));
|
|
565
|
+
};
|
|
566
|
+
const collectOne = (task, acc) => {
|
|
567
|
+
if (isRunnableTest(task)) acc.push(task);
|
|
568
|
+
else collectNested(task, acc);
|
|
569
|
+
};
|
|
570
|
+
const notSkipped = (task) => task.mode !== "skip" && task.mode !== "todo";
|
|
571
|
+
const isRunnableTest = (task) => task.type === "test" && notSkipped(task);
|
|
1203
572
|
const layerFor = (layer_, options, inherited) => isSharedBlock(options) ? sharedBlock(layer_, options, inherited) : freshBlock(layer_, options);
|
|
1204
573
|
const freshBlock = (layer_, options) => {
|
|
1205
574
|
const excludeTestServices = excludeTestServicesOf(options);
|
|
@@ -1209,13 +578,7 @@ const freshBlock = (layer_, options) => {
|
|
|
1209
578
|
excludeTestServices
|
|
1210
579
|
}, void 0);
|
|
1211
580
|
const nestedLayer = Function.dual(byLayer, nestedOpen);
|
|
1212
|
-
const makeIt = (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);
|
|
581
|
+
const makeIt = (it) => methodsFor((effect, env) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, Layer.orDie(Layer.fresh(withTestEnv(env))))), it, perRun, true, nestedLayer);
|
|
1219
582
|
return blockOpener((f) => f(makeIt(V.it)), (name, f) => {
|
|
1220
583
|
V.describe(name, describeOptions(options, false), () => f(makeIt(V.it)));
|
|
1221
584
|
});
|
|
@@ -1225,24 +588,15 @@ const nestedBlock = (nested, memoMap) => ({
|
|
|
1225
588
|
memoMap: Layer.forkMemoMapUnsafe(memoMap),
|
|
1226
589
|
shared: true
|
|
1227
590
|
});
|
|
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
591
|
const sharedBlock = (layer_, options, inherited) => {
|
|
1240
592
|
const blockEnv = blockEnvOf(inherited);
|
|
1241
593
|
const memoMap = memoMapOf(options);
|
|
1242
594
|
const scope = Scope.makeUnsafe();
|
|
1243
595
|
const shared = mergeTestEnv(layer_(blockEnv), testEnv(blockEnv), excludeTestServicesOf(options));
|
|
1244
596
|
const contextEffect = sharedContext(shared, memoMap, scope);
|
|
1245
|
-
const
|
|
597
|
+
const nestedOpen = (nestedLayer, nestedOptions) => layerFor(() => Layer.provideMerge(nestedLayer, shared), nestedBlock(nestedOptions, memoMap), blockEnv);
|
|
598
|
+
const nestedLayer = Function.dual(byLayer, nestedOpen);
|
|
599
|
+
const makeIt = (it) => methodsFor((effect) => Effect.flatMap(contextEffect, (context) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, context))), it, () => blockEnv, false, nestedLayer);
|
|
1246
600
|
const closeScope = makeScopeCloser(scope);
|
|
1247
601
|
return blockOpener((f) => sharedAnonymous(f, makeIt, options, blockTasksOf(), blockEnv, contextEffect, closeScope), (name, f) => describeShared(name, f, makeIt, options, contextEffect, closeScope, blockEnv));
|
|
1248
602
|
};
|
|
@@ -1253,8 +607,6 @@ const describeShared = (name, f, makeIt, options, contextEffect, closeScope, blo
|
|
|
1253
607
|
return f(makeIt(V.it));
|
|
1254
608
|
});
|
|
1255
609
|
};
|
|
1256
|
-
const blockTasksOf = () => suiteTasks(getCurrentSuite());
|
|
1257
|
-
const suiteTasks = (suite) => suite.tasks;
|
|
1258
610
|
const sharedAnonymous = (register, makeIt, options, blockTasks, blockEnv, contextEffect, closeScope) => {
|
|
1259
611
|
const previousTasks = new Set(blockTasks);
|
|
1260
612
|
register(makeIt(V.it));
|
|
@@ -1282,20 +634,6 @@ const sharedBeforeEach = (ctx, blockTaskSet, finish, contextEffect, blockEnv) =>
|
|
|
1282
634
|
ctx.onTestFinished(() => finish());
|
|
1283
635
|
return runPromise(Effect.asVoid(contextEffect), ctx, blockEnv);
|
|
1284
636
|
};
|
|
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
637
|
const layerImpl = (layer_, options) => layerFor(() => layer_, options, void 0);
|
|
1300
638
|
/** @internal */
|
|
1301
639
|
const layer$1 = Function.dual(byLayer, layerImpl);
|
|
@@ -1304,82 +642,101 @@ const retryWithin = (timeout) => Schedule.while(Schedule.recurs(10), (state) =>
|
|
|
1304
642
|
const flakyTestImpl = (self, timeout = Duration.seconds(30)) => self.pipe(Effect.scoped, Effect.sandbox, (sandboxed) => Effect.retry(sandboxed, retryWithin(timeout)), Effect.orDie);
|
|
1305
643
|
/** @internal */
|
|
1306
644
|
const flakyTest$1 = Function.dual(byEffect, flakyTestImpl);
|
|
1307
|
-
const isFunction = (arg) => typeof arg === "function";
|
|
1308
645
|
/**
|
|
1309
|
-
* The
|
|
1310
|
-
*
|
|
646
|
+
* The lanes a removed habit name reaches: the call is refused with its rewrite, on the `this` type and at run
|
|
647
|
+
* time, and the property the lane used to carry still runs (R9, KTD3).
|
|
1311
648
|
*/
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
649
|
+
const refuseHabitLane = (text) => {
|
|
650
|
+
throw refusalOf$1(text);
|
|
651
|
+
};
|
|
652
|
+
const effectLaneRefusal = (prop, text) => Object.assign(() => refuseHabitLane(text), { prop });
|
|
653
|
+
/**
|
|
654
|
+
* The lanes over one `mapEffect`: `it`, its modifiers and `it.each` all register the same generator body, and
|
|
655
|
+
* the Effect-lane property is bound to the same services the lane provides (R7, R9).
|
|
656
|
+
*/
|
|
657
|
+
const makeTesterWith = (mapEffect, it, envFor, rerunnable) => {
|
|
658
|
+
const runOnce = (ctx, body) => runLaned(ctx, mapEffect, envFor(), body);
|
|
659
|
+
const rerun = (ctx, body) => outcomeOf(runOnce(ctx, body)).then(leakedOrNothing);
|
|
660
|
+
const shouldRerun = (ctx) => rerunnable && isClean(ctx);
|
|
661
|
+
const rerunIfClean = (ctx, body) => shouldRerun(ctx) ? rerun(ctx, body) : Promise.resolve();
|
|
662
|
+
const run = (ctx, body) => outcomeOf(runOnce(ctx, body)).then((error) => {
|
|
663
|
+
if (error !== void 0) throw error;
|
|
664
|
+
return rerunIfClean(ctx, body);
|
|
665
|
+
});
|
|
666
|
+
const registering = (registrar) => (name, body, timeout) => {
|
|
667
|
+
const lane = laneBodyOf(body);
|
|
668
|
+
marked(() => registrar(name, testOptions(timeout), (ctx) => run(ctx, lane)));
|
|
669
|
+
};
|
|
670
|
+
const dualLane = (registrar) => Function.dual(byName, registering(registrar));
|
|
671
|
+
const base = dualLane(it);
|
|
672
|
+
const skip = dualLane(it.skip);
|
|
673
|
+
const skipIf = (condition) => dualLane(it.skipIf(condition));
|
|
674
|
+
const runIf = (condition) => dualLane(it.runIf(condition));
|
|
675
|
+
const only = dualLane(it.only);
|
|
676
|
+
const fails = dualLane(it.fails);
|
|
677
|
+
const each = (cases) => (name, body, timeout) => {
|
|
678
|
+
marked(() => it.for(cases)(name, testOptions(timeout), (row, ctx) => run(ctx, rowBodyOf(row, body))));
|
|
679
|
+
};
|
|
680
|
+
const effectProperty = makeProperty({
|
|
681
|
+
register: (name, program) => marked(() => it(name, (ctx) => runProperty(ctx, program))),
|
|
682
|
+
provide: (effect) => mapEffect(effect, envFor())
|
|
683
|
+
});
|
|
684
|
+
const prop = (name, spec, holds) => {
|
|
685
|
+
refusePositional(spec);
|
|
686
|
+
effectProperty.effectProp(name, spec, holds);
|
|
687
|
+
};
|
|
688
|
+
return Object.assign(base, {
|
|
689
|
+
skip,
|
|
690
|
+
skipIf,
|
|
691
|
+
runIf,
|
|
692
|
+
only,
|
|
693
|
+
fails,
|
|
694
|
+
each,
|
|
695
|
+
prop
|
|
696
|
+
});
|
|
1315
697
|
};
|
|
1316
|
-
/**
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
698
|
+
/** The block's methods: the generator lanes, the refusing habit lanes, and the block's own nested `layer`. */
|
|
699
|
+
const methodsFor = (mapEffect, it, envFor, rerunnable, layer) => {
|
|
700
|
+
const tester = makeTesterWith(mapEffect, it, envFor, rerunnable);
|
|
701
|
+
return Object.assign(tester, {
|
|
702
|
+
effect: effectLaneRefusal(tester.prop, refuseEffectLane),
|
|
703
|
+
scoped: effectLaneRefusal(tester.prop, refuseScopedLane),
|
|
704
|
+
scopedLive: effectLaneRefusal(tester.prop, refuseScopedLiveLane),
|
|
705
|
+
prop: prop$1,
|
|
706
|
+
law: property.law,
|
|
707
|
+
flakyTest: flakyTest$1,
|
|
708
|
+
layer
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
const refuseHookNow = () => {
|
|
712
|
+
throw refusalOf$1(refuseHook);
|
|
1323
713
|
};
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1326
|
-
|
|
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);
|
|
714
|
+
const takesHook = (args) => typeof args[0] === "function";
|
|
715
|
+
const refuseHookPair = (fn, timeout) => {
|
|
716
|
+
return refuseHookNow();
|
|
1332
717
|
};
|
|
1333
718
|
/**
|
|
1334
|
-
*
|
|
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).
|
|
719
|
+
* @internal
|
|
1337
720
|
*/
|
|
1338
|
-
const
|
|
1339
|
-
const defaultLane = (self, rerunnable) => (ctx) => runDefault(ctx, self, rerunnable);
|
|
721
|
+
const beforeEach$2 = Function.dual(takesHook, refuseHookPair);
|
|
1340
722
|
/**
|
|
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
723
|
* @internal
|
|
1345
724
|
*/
|
|
1346
|
-
const
|
|
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
|
-
}
|
|
725
|
+
const afterEach$1 = Function.dual(takesHook, refuseHookPair);
|
|
1369
726
|
/**
|
|
1370
|
-
* The
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
727
|
+
* The fork's `it`: the generator lanes on virtual time, the real-clock lane, the lawful property lanes and the
|
|
728
|
+
* layer blocks, callable data-first (`it(name, body)`) or data-last (`it(body, timeout?)(name)`).
|
|
729
|
+
*
|
|
730
|
+
* @internal
|
|
1373
731
|
*/
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
732
|
+
const makeMethods$1 = (it) => {
|
|
733
|
+
const base = methodsFor((effect, env) => effect.pipe(Effect.scoped, (scoped) => Effect.provide(scoped, testEnv(env))), it, perRun, true, layer$1);
|
|
734
|
+
const live = makeTesterWith((effect) => Effect.scoped(effect), it, () => realTime, true);
|
|
735
|
+
return Object.assign(base, {
|
|
736
|
+
live,
|
|
737
|
+
layer: layer$1
|
|
738
|
+
});
|
|
739
|
+
};
|
|
1383
740
|
const describeImpl = (name, f) => V.describe(name, {
|
|
1384
741
|
concurrent: true,
|
|
1385
742
|
shuffle: true
|
|
@@ -1389,60 +746,50 @@ const describeWrapped$1 = Function.dual(byName, describeImpl);
|
|
|
1389
746
|
//#endregion
|
|
1390
747
|
//#region src/mod.ts
|
|
1391
748
|
/**
|
|
749
|
+
* The fork's test API, with the generator lanes of R2 and R9.
|
|
750
|
+
*
|
|
1392
751
|
* @since 4.0.0
|
|
1393
752
|
*/
|
|
1394
|
-
const
|
|
753
|
+
const it = makeMethods$1(V.it);
|
|
1395
754
|
/**
|
|
755
|
+
* The same lanes under the other name upstream publishes.
|
|
756
|
+
*
|
|
1396
757
|
* @since 4.0.0
|
|
1397
758
|
*/
|
|
1398
|
-
const
|
|
759
|
+
const test = it;
|
|
1399
760
|
/**
|
|
761
|
+
* The fork's lawful collector: it forces the fork's concurrency and shuffle defaults and hands its body the
|
|
762
|
+
* fork's methods.
|
|
763
|
+
*
|
|
1400
764
|
* @since 4.0.0
|
|
1401
765
|
*/
|
|
1402
|
-
const
|
|
766
|
+
const describeWrapped = describeWrapped$1;
|
|
1403
767
|
/**
|
|
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
768
|
* @since 4.0.0
|
|
769
|
+
*/
|
|
770
|
+
const describe = describeWrapped$1;
|
|
771
|
+
/**
|
|
772
|
+
* Share a `Layer` between multiple tests, optionally wrapping the tests in a `describe` block if a name is
|
|
773
|
+
* provided. Every test in the block gets its own fresh build of the layer, unless the block declares
|
|
774
|
+
* `shared: true`.
|
|
1412
775
|
*
|
|
1413
776
|
* ```ts
|
|
1414
|
-
* import {
|
|
1415
|
-
* import { Effect, Layer
|
|
777
|
+
* import { it, layer } from "@systemfsoftware/vitest"
|
|
778
|
+
* import { Context, Effect, Layer } from "effect"
|
|
1416
779
|
*
|
|
1417
780
|
* class Foo extends Context.Service<Foo, "foo">()("Foo") {
|
|
1418
781
|
* static layer = Layer.succeed(Foo, "foo")
|
|
1419
782
|
* }
|
|
1420
783
|
*
|
|
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
784
|
* layer(Foo.layer)("layer", (it) => {
|
|
1429
|
-
* it
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
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
|
-
* }))
|
|
785
|
+
* it("reads the service", function* ({ expect }) {
|
|
786
|
+
* const foo = yield* Foo
|
|
787
|
+
* yield* expect(foo).toEqual("foo")
|
|
1443
788
|
* })
|
|
1444
789
|
* })
|
|
1445
790
|
* ```
|
|
791
|
+
*
|
|
792
|
+
* @since 4.0.0
|
|
1446
793
|
*/
|
|
1447
794
|
const layer = layer$1;
|
|
1448
795
|
/**
|
|
@@ -1456,68 +803,42 @@ const prop = prop$1;
|
|
|
1456
803
|
/**
|
|
1457
804
|
* @since 4.0.0
|
|
1458
805
|
*/
|
|
1459
|
-
const
|
|
1460
|
-
/**
|
|
1461
|
-
* @since 4.0.0
|
|
1462
|
-
*/
|
|
1463
|
-
const recordAssertion = recordAssertion$1;
|
|
806
|
+
const law = law$1;
|
|
1464
807
|
/**
|
|
1465
|
-
* The
|
|
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.
|
|
808
|
+
* The fork's test API over another Vitest `it`, so a library can register tests in its own suite.
|
|
1469
809
|
*
|
|
1470
810
|
* @since 4.0.0
|
|
1471
811
|
*/
|
|
1472
|
-
const
|
|
812
|
+
const makeMethods = makeMethods$1;
|
|
1473
813
|
/**
|
|
1474
|
-
*
|
|
1475
|
-
* context builds that view on this key, so the context a case lane sees and a property lane sees agree.
|
|
814
|
+
* Refused: hooks share state between tests, and services come fresh per test from `layer`.
|
|
1476
815
|
*
|
|
1477
816
|
* @since 4.0.0
|
|
1478
817
|
*/
|
|
1479
|
-
const
|
|
818
|
+
const beforeEach = beforeEach$2;
|
|
1480
819
|
/**
|
|
1481
|
-
*
|
|
1482
|
-
* lanes, and each `it.each` row. It is `null` outside a run.
|
|
820
|
+
* Refused: hooks share state between tests, and services come fresh per test from `layer`.
|
|
1483
821
|
*
|
|
1484
822
|
* @since 4.0.0
|
|
1485
823
|
*/
|
|
1486
|
-
const
|
|
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;
|
|
824
|
+
const afterEach = afterEach$1;
|
|
1495
825
|
/**
|
|
1496
826
|
* @since 4.0.0
|
|
1497
827
|
*/
|
|
1498
|
-
const
|
|
1499
|
-
const describe = describeWrapped$1;
|
|
828
|
+
const addEqualityTesters = addEqualityTesters$1;
|
|
1500
829
|
/**
|
|
1501
|
-
* The
|
|
830
|
+
* The key the running test's context is published under. A library that carries its own view of the task
|
|
831
|
+
* context builds that view on this key, so the context a case lane sees and a property lane sees agree.
|
|
1502
832
|
*
|
|
1503
833
|
* @since 4.0.0
|
|
1504
834
|
*/
|
|
1505
|
-
const
|
|
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;
|
|
835
|
+
const vitestTestContextKey = vitestTestContextKey$1;
|
|
1518
836
|
/**
|
|
837
|
+
* The running Vitest `TestContext`, provided on every test the fork runs — every generator lane, both property
|
|
838
|
+
* lanes, and each `it.each` row. It is `null` outside a run.
|
|
839
|
+
*
|
|
1519
840
|
* @since 4.0.0
|
|
1520
841
|
*/
|
|
1521
|
-
const
|
|
842
|
+
const VitestTestContext = VitestTestContext$1;
|
|
1522
843
|
//#endregion
|
|
1523
|
-
export { VitestTestContext, addEqualityTesters, afterAll, afterEach, beforeAll, beforeEach,
|
|
844
|
+
export { VitestTestContext, addEqualityTesters, afterAll, afterEach, assertType, beforeAll, beforeEach, describe, describeWrapped, expectTypeOf, flakyTest, inject, it, law, layer, makeMethods, onTestFailed, onTestFinished, prop, recordArtifact, test, vi, vitest, vitestTestContextKey };
|