@systemfsoftware/vitest 1.0.0 → 4.1.1
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 +4 -48
- package/README.md +82 -4
- package/dist/{checks-DiuzHhQM.d.ts → checks-BHOX6fGC.d.ts} +2 -2
- package/dist/engine-CYGdHvr8.d.ts +464 -0
- package/dist/failure-C67X1OAe.mjs +2294 -0
- package/dist/failure.d.ts +29 -116
- package/dist/failure.mjs +3 -3
- package/dist/{guard-BocOhXt2.mjs → guard-Ci0TyBYv.mjs} +18 -6
- package/dist/guard.mjs +2 -2
- package/dist/index.d.ts +20 -77
- package/dist/index.mjs +41 -79
- package/dist/integration.d.ts +1 -1
- package/dist/integration.mjs +1 -1
- package/dist/property-defaults.d.ts +11 -0
- package/dist/property-defaults.mjs +15 -0
- package/dist/{test-context-D_-acRSc.mjs → test-context-DelFkLBL.mjs} +10 -6
- package/dist/tiers-W0BWq1uh.mjs +36 -0
- package/package.json +18 -19
- package/dist/engine-CMvYG5V0.d.ts +0 -166
- package/dist/failure-D8mPXwf4.mjs +0 -1293
|
@@ -0,0 +1,2294 @@
|
|
|
1
|
+
|
|
2
|
+
import { n as providedBudgetOf } from "./tiers-W0BWq1uh.mjs";
|
|
3
|
+
import { a as isCheck, d as isUserFrame, f as siteOfFrame, i as checksFor, o as makeLedger, p as withRaisingFrame, r as Asserted, s as callFrame, t as VitestTestContext, u as framesOf$1 } from "./test-context-DelFkLBL.mjs";
|
|
4
|
+
import { l as noCheckRefusal, u as refusalOf, y as refuseUnyielded } from "./guard-Ci0TyBYv.mjs";
|
|
5
|
+
import { TestRunner, inject } from "vitest";
|
|
6
|
+
import * as Cause from "effect/Cause";
|
|
7
|
+
import * as Effect$1 from "effect/Effect";
|
|
8
|
+
import * as Equal from "effect/Equal";
|
|
9
|
+
import * as Exit from "effect/Exit";
|
|
10
|
+
import * as Function from "effect/Function";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Schema$1 from "effect/Schema";
|
|
13
|
+
import * as Option$1 from "effect/Option";
|
|
14
|
+
import * as Match$1 from "effect/Match";
|
|
15
|
+
import * as Tracer from "effect/Tracer";
|
|
16
|
+
import { Effect, Match, Option, Schema, SchemaGetter, SchemaIssue } from "effect";
|
|
17
|
+
import * as Predicate from "effect/Predicate";
|
|
18
|
+
import * as Arbitrary from "effect/Arbitrary";
|
|
19
|
+
import * as Config from "effect/Config";
|
|
20
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
21
|
+
import * as Random from "effect/Random";
|
|
22
|
+
import * as Ref from "effect/Ref";
|
|
23
|
+
import * as Result from "effect/Result";
|
|
24
|
+
import * as FileSystem from "effect/FileSystem";
|
|
25
|
+
import * as Path from "effect/Path";
|
|
26
|
+
//#region src/internal/property/defaults.ts
|
|
27
|
+
/** @internal The `test.provide` key the shared config fills with this run's tier's check options. */
|
|
28
|
+
const checkDefaultsKey = "@systemfsoftware/vitest:property-check";
|
|
29
|
+
const processEnvironment = () => ({ ...typeof process === "undefined" ? void 0 : process.env });
|
|
30
|
+
/**
|
|
31
|
+
* @internal The property-check defaults a run provided through `test.provide`, else the tier's budget for this
|
|
32
|
+
* process's environment, read from the same table the setup entry uses.
|
|
33
|
+
*/
|
|
34
|
+
const providedCheckDefaults = () => inject("@systemfsoftware/vitest:property-check") ?? providedBudgetOf(processEnvironment());
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/replay.schema.ts
|
|
37
|
+
/**
|
|
38
|
+
* The one replay grammar (KTD10, KTD5, KD8, R6, R9-R12): the `CONFORMANCE_REPLAY` text a failure record's rerun
|
|
39
|
+
* line carries and the engine reads back to re-run the same verdict. Two forms share it:
|
|
40
|
+
*
|
|
41
|
+
* - the kernel's schedule, `seed=<whole number>;path=<comma-separated whole numbers>` — the path is empty when the
|
|
42
|
+
* run took no decision (`seed=1;path=`);
|
|
43
|
+
* - a property entry, `property=<identity hash>;seed=<whole number>;runs=<whole number>` for a run that was not
|
|
44
|
+
* refuted, and the same entry plus `;attempt=<n>;size=<n>;path=<comma-separated decisions>;failure=<tag>` for a
|
|
45
|
+
* refuted run, so the engine rebuilds the generator's replay token. Several entries are joined by `|`, which no
|
|
46
|
+
* entry can contain: every key is lowercase letters and every value is digits, commas or a failure tag.
|
|
47
|
+
*
|
|
48
|
+
* A text that names neither form is not a replay. The seed and every decision are branded whole numbers, and
|
|
49
|
+
* the text is a codec rooted at `Schema.String`, so one declaration serves both the writer and the
|
|
50
|
+
* reader instead of a pattern each.
|
|
51
|
+
*
|
|
52
|
+
* @since 4.0.0
|
|
53
|
+
*/
|
|
54
|
+
/** The seed a replay names: the whole number a generator chose, never negative (CONST-D1). */
|
|
55
|
+
const ReplaySeed = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ReplaySeed"));
|
|
56
|
+
/** One decision along a replay path: the index the scheduler picked at that step. */
|
|
57
|
+
const ReplayStep = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ReplayStep"));
|
|
58
|
+
/** The replay a generator chose: its seed and the decisions it took, in order (R6). */
|
|
59
|
+
const Replay = Schema.Struct({
|
|
60
|
+
seed: ReplaySeed,
|
|
61
|
+
path: Schema.Array(ReplayStep)
|
|
62
|
+
});
|
|
63
|
+
/** A property's identity hash: the unsigned whole number `identityHash` derives from its identity (KTD4). */
|
|
64
|
+
const PropertyHash = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyHash"));
|
|
65
|
+
/** The count of runs a property entry names, never negative (CONST-D1). */
|
|
66
|
+
const ReplayRunCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ReplayRunCount"));
|
|
67
|
+
/** The failure kind effect's falsification runner records in its replay token. */
|
|
68
|
+
const ReplayFailureTag = Schema.Literals(["ReturnedFalse", "PropertyError"]);
|
|
69
|
+
/**
|
|
70
|
+
* A property entry for a run that was not refuted: the property's identity hash, the seed it ran with and its
|
|
71
|
+
* run count (R9, R11).
|
|
72
|
+
*
|
|
73
|
+
* @since 4.0.0
|
|
74
|
+
*/
|
|
75
|
+
const PlainPropertyReplay = Schema.TaggedStruct("Plain", {
|
|
76
|
+
property: PropertyHash,
|
|
77
|
+
seed: ReplaySeed,
|
|
78
|
+
runs: ReplayRunCount
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* A property entry for a refuted run: the plain fields, plus the generator replay token's parts the engine needs
|
|
82
|
+
* to rebuild it — the attempt, the size, the shrink path and the failure tag (R9, R12, KTD5).
|
|
83
|
+
*
|
|
84
|
+
* @since 4.0.0
|
|
85
|
+
*/
|
|
86
|
+
const RefutedPropertyReplay = Schema.TaggedStruct("Refuted", {
|
|
87
|
+
property: PropertyHash,
|
|
88
|
+
seed: ReplaySeed,
|
|
89
|
+
runs: ReplayRunCount,
|
|
90
|
+
attempt: ReplayRunCount,
|
|
91
|
+
size: ReplayRunCount,
|
|
92
|
+
path: Schema.Array(ReplayStep),
|
|
93
|
+
failure: ReplayFailureTag
|
|
94
|
+
});
|
|
95
|
+
/** One property entry: a plain run, or a refuted one (R9). */
|
|
96
|
+
const PropertyReplay = Schema.Union([RefutedPropertyReplay, PlainPropertyReplay]);
|
|
97
|
+
/** What a `CONFORMANCE_REPLAY` text names: the kernel's replay, or one or more property entries (KTD5). */
|
|
98
|
+
const ReplayChannel = Schema.Union([Replay, Schema.Array(PropertyReplay)]);
|
|
99
|
+
const SEED_PREFIX = "seed=";
|
|
100
|
+
const PATH_PREFIX = "path=";
|
|
101
|
+
const PROPERTY_PREFIX = "property=";
|
|
102
|
+
const RUNS_PREFIX = "runs=";
|
|
103
|
+
const ATTEMPT_PREFIX = "attempt=";
|
|
104
|
+
const SIZE_PREFIX = "size=";
|
|
105
|
+
const FAILURE_PREFIX = "failure=";
|
|
106
|
+
const PAIR_SEPARATOR = ";";
|
|
107
|
+
const LIST_SEPARATOR = ",";
|
|
108
|
+
const ENTRY_SEPARATOR = "|";
|
|
109
|
+
const WHOLE_NUMBER = /^\d+$/u;
|
|
110
|
+
const ASSIGNMENT = /^([a-z]+)=([\s\S]*)$/u;
|
|
111
|
+
const NOT_A_REPLAY = "a replay reads \"seed=<whole number>;path=<comma-separated whole numbers>\"";
|
|
112
|
+
const NOT_A_CHANNEL = "a replay reads \"seed=<whole number>;path=<whole numbers>\", or one or more \"property=<hash>;seed=<n>;runs=<n>\" entries joined by \"|\"";
|
|
113
|
+
const wholeNumberOf = (text) => WHOLE_NUMBER.test(text) ? Number(text) : void 0;
|
|
114
|
+
const seedDigitsOf = (text) => text.startsWith(SEED_PREFIX) ? text.slice(5) : void 0;
|
|
115
|
+
const pathDigitsOf = (text) => text.startsWith(PATH_PREFIX) ? text.slice(5) : void 0;
|
|
116
|
+
const entriesOf$1 = (text) => text.length === 0 ? [] : text.split(LIST_SEPARATOR);
|
|
117
|
+
const wholeNumbersOf = (entries) => {
|
|
118
|
+
const numbers = entries.map(wholeNumberOf);
|
|
119
|
+
return numbers.every((value) => value !== void 0) ? numbers : void 0;
|
|
120
|
+
};
|
|
121
|
+
const seedIn = (part) => Option.flatMap(Option.fromUndefinedOr(seedDigitsOf(part)), (digits) => Option.fromUndefinedOr(wholeNumberOf(digits)));
|
|
122
|
+
const decisionsIn = (part) => Option.flatMap(Option.fromUndefinedOr(pathDigitsOf(part)), (entries) => Option.fromUndefinedOr(wholeNumbersOf(entriesOf$1(entries))));
|
|
123
|
+
const exactlyTwoParts = (parts) => parts.length === 2;
|
|
124
|
+
const pairPartsOf = (text) => {
|
|
125
|
+
const parts = text.split(PAIR_SEPARATOR);
|
|
126
|
+
return exactlyTwoParts(parts) ? Option.all([Option.fromUndefinedOr(parts[0]), Option.fromUndefinedOr(parts[1])]) : Option.none();
|
|
127
|
+
};
|
|
128
|
+
/** The seed and decisions a pair names, or nothing when the pair is not `seed=…;path=…`. */
|
|
129
|
+
const namedPairOf = (text) => Option.flatMap(pairPartsOf(text), ([seedPart, pathPart]) => Option.all([seedIn(seedPart), decisionsIn(pathPart)]));
|
|
130
|
+
/** The replay a seed and decision path name, or nothing when either is outside the grammar's domain. */
|
|
131
|
+
const replayOfParts = (parts) => Schema.decodeOption(Replay)(parts);
|
|
132
|
+
const replayOfName = (text) => Option.flatMap(namedPairOf(text), ([seed, path]) => replayOfParts({
|
|
133
|
+
seed,
|
|
134
|
+
path
|
|
135
|
+
}));
|
|
136
|
+
const textOfEncodedReplay = (replay) => `${SEED_PREFIX}${replay.seed}${PAIR_SEPARATOR}${PATH_PREFIX}${replay.path.join(LIST_SEPARATOR)}`;
|
|
137
|
+
const replayRefusal = (text) => /* @__PURE__ */ new Error(`CONFORMANCE_REPLAY names neither a seed nor a decision path: ${text}`);
|
|
138
|
+
/**
|
|
139
|
+
* The `CONFORMANCE_REPLAY` text as the replay it names. A text a seed and a path cannot be read out of is refused
|
|
140
|
+
* with the same prose the spec runtime has always thrown, so the grammar and its refusal share one home.
|
|
141
|
+
*
|
|
142
|
+
* @since 4.0.0
|
|
143
|
+
*/
|
|
144
|
+
const replayOfText = (text) => Option.match(replayOfName(text), {
|
|
145
|
+
onNone: () => {
|
|
146
|
+
throw replayRefusal(text);
|
|
147
|
+
},
|
|
148
|
+
onSome: (replay) => replay
|
|
149
|
+
});
|
|
150
|
+
/** The text a replay is written as: the `CONFORMANCE_REPLAY` value a rerun line carries (R6).
|
|
151
|
+
*
|
|
152
|
+
* @since 4.0.0
|
|
153
|
+
*/
|
|
154
|
+
const replayTextOf = (replay) => textOfEncodedReplay(replay);
|
|
155
|
+
const fieldOfPart = (part) => Option.flatMap(Option.fromNullishOr(ASSIGNMENT.exec(part)), (matched) => Option.map(Option.all([Option.fromUndefinedOr(matched[1]), Option.fromUndefinedOr(matched[2])]), ([key, value]) => [key, value]));
|
|
156
|
+
const fieldsOf$1 = (text) => Option.all(text.split(PAIR_SEPARATOR).map(fieldOfPart));
|
|
157
|
+
const textIn = (fields, key) => Option.map(Option.fromNullishOr(fields.find(([name]) => name === key)), (field) => field[1]);
|
|
158
|
+
/** The entry's keys name exactly `keys`, in order — so a reordered or repeated field is refused (KD8). */
|
|
159
|
+
const keysAre = (fields, keys) => fields.length === keys.length && fields.every(([name], at) => name === keys[at]);
|
|
160
|
+
const naturalIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Option.fromUndefinedOr(wholeNumberOf(text)));
|
|
161
|
+
const pathIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Option.fromUndefinedOr(wholeNumbersOf(entriesOf$1(text))));
|
|
162
|
+
const failureIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Schema.decodeUnknownOption(ReplayFailureTag)(text));
|
|
163
|
+
const PLAIN_KEYS = [
|
|
164
|
+
"property",
|
|
165
|
+
"seed",
|
|
166
|
+
"runs"
|
|
167
|
+
];
|
|
168
|
+
const REFUTED_KEYS = [
|
|
169
|
+
"property",
|
|
170
|
+
"seed",
|
|
171
|
+
"runs",
|
|
172
|
+
"attempt",
|
|
173
|
+
"size",
|
|
174
|
+
"path",
|
|
175
|
+
"failure"
|
|
176
|
+
];
|
|
177
|
+
const plainEncodedOf = (fields) => Option.map(Option.all([
|
|
178
|
+
naturalIn(fields, "property"),
|
|
179
|
+
naturalIn(fields, "seed"),
|
|
180
|
+
naturalIn(fields, "runs")
|
|
181
|
+
]), ([property, seed, runs]) => ({
|
|
182
|
+
_tag: "Plain",
|
|
183
|
+
property,
|
|
184
|
+
seed,
|
|
185
|
+
runs
|
|
186
|
+
}));
|
|
187
|
+
const refutedEncodedOf = (fields) => Option.map(Option.all([
|
|
188
|
+
naturalIn(fields, "property"),
|
|
189
|
+
naturalIn(fields, "seed"),
|
|
190
|
+
naturalIn(fields, "runs"),
|
|
191
|
+
naturalIn(fields, "attempt"),
|
|
192
|
+
naturalIn(fields, "size"),
|
|
193
|
+
pathIn(fields, "path"),
|
|
194
|
+
failureIn(fields, "failure")
|
|
195
|
+
]), ([property, seed, runs, attempt, size, path, failure]) => ({
|
|
196
|
+
_tag: "Refuted",
|
|
197
|
+
property,
|
|
198
|
+
seed,
|
|
199
|
+
runs,
|
|
200
|
+
attempt,
|
|
201
|
+
size,
|
|
202
|
+
path,
|
|
203
|
+
failure
|
|
204
|
+
}));
|
|
205
|
+
const plainEntryOf = (fields) => Option.flatMap(plainEncodedOf(fields), (encoded) => Schema.decodeOption(PlainPropertyReplay)(encoded));
|
|
206
|
+
const refutedEntryOf$1 = (fields) => Option.flatMap(refutedEncodedOf(fields), (encoded) => Schema.decodeOption(RefutedPropertyReplay)(encoded));
|
|
207
|
+
const plainWhenKeysOf = (fields) => keysAre(fields, PLAIN_KEYS) ? plainEntryOf(fields) : Option.none();
|
|
208
|
+
const refutedWhenKeysOf = (fields) => keysAre(fields, REFUTED_KEYS) ? refutedEntryOf$1(fields) : Option.none();
|
|
209
|
+
const propertyEntryOf = (text) => Option.flatMap(fieldsOf$1(text), (fields) => Option.orElse(plainWhenKeysOf(fields), () => refutedWhenKeysOf(fields)));
|
|
210
|
+
const propertyChannelOf = (text) => text.length === 0 ? Option.none() : Option.all(text.split(ENTRY_SEPARATOR).map(propertyEntryOf));
|
|
211
|
+
const channelOfName = (text) => Option.match(replayOfName(text), {
|
|
212
|
+
onNone: () => propertyChannelOf(text),
|
|
213
|
+
onSome: (replay) => Option.some(replay)
|
|
214
|
+
});
|
|
215
|
+
const isEntries$1 = (channel) => Array.isArray(channel);
|
|
216
|
+
/** The text a plain property entry is written as (R9, R11). */
|
|
217
|
+
const plainReplayText = (fields) => `${PROPERTY_PREFIX}${fields.property}${PAIR_SEPARATOR}${SEED_PREFIX}${fields.seed}${PAIR_SEPARATOR}${RUNS_PREFIX}${fields.runs}`;
|
|
218
|
+
/** The text several plain entries are written as, joined by `|` (R11). */
|
|
219
|
+
const plainReplayTexts = (entries) => entries.map(plainReplayText).join(ENTRY_SEPARATOR);
|
|
220
|
+
/** The text a refuted property entry is written as (R9, R12). */
|
|
221
|
+
const refutedReplayText$1 = (fields) => `${plainReplayText(fields)}${PAIR_SEPARATOR}${ATTEMPT_PREFIX}${fields.attempt}${PAIR_SEPARATOR}${SIZE_PREFIX}${fields.size}${PAIR_SEPARATOR}${PATH_PREFIX}${fields.path.join(LIST_SEPARATOR)}${PAIR_SEPARATOR}${FAILURE_PREFIX}${fields.failure}`;
|
|
222
|
+
const entryTextOf = (entry) => Match.value(entry).pipe(Match.tag("Refuted", (refuted) => refutedReplayText$1(refuted)), Match.tag("Plain", (plain) => plainReplayText(plain)), Match.exhaustive);
|
|
223
|
+
/** The text a channel is written as: the kernel's replay, or the entries joined by `|` (KTD5). */
|
|
224
|
+
const replayChannelTextOf = (channel) => isEntries$1(channel) ? channel.map(entryTextOf).join(ENTRY_SEPARATOR) : textOfEncodedReplay(channel);
|
|
225
|
+
/**
|
|
226
|
+
* The kernel's replay codec: `Schema.String` in, a replay out. A text that names no seed and path fails the decode,
|
|
227
|
+
* and every replay encodes back to the text it was read from.
|
|
228
|
+
*
|
|
229
|
+
* @since 4.0.0
|
|
230
|
+
*/
|
|
231
|
+
const ReplayFromText = Schema.String.pipe(Schema.decodeTo(Replay, {
|
|
232
|
+
decode: SchemaGetter.transformEffect((text, options) => Effect.fromOption(replayOfName(text), () => new SchemaIssue.InvalidValue({ message: NOT_A_REPLAY }, text, options))),
|
|
233
|
+
encode: SchemaGetter.transform(textOfEncodedReplay)
|
|
234
|
+
}));
|
|
235
|
+
/**
|
|
236
|
+
* The channel codec: `Schema.String` in, the kernel's replay or the property entries out. A text that names neither
|
|
237
|
+
* form fails the decode, and every channel encodes back to the text it was read from.
|
|
238
|
+
*
|
|
239
|
+
* @since 4.0.0
|
|
240
|
+
*/
|
|
241
|
+
const ReplayChannelFromText = Schema.String.pipe(Schema.decodeTo(ReplayChannel, {
|
|
242
|
+
decode: SchemaGetter.transformEffect((text, options) => Effect.fromOption(channelOfName(text), () => new SchemaIssue.InvalidValue({ message: NOT_A_CHANNEL }, text, options))),
|
|
243
|
+
encode: SchemaGetter.transform((channel) => replayChannelTextOf(channel))
|
|
244
|
+
}));
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/internal/errors.schema.ts
|
|
247
|
+
/**
|
|
248
|
+
* The contract rules a rendered failure record can break (R10, KTD7).
|
|
249
|
+
*
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
const Breach = Schema.Literals([
|
|
253
|
+
"R1",
|
|
254
|
+
"R2",
|
|
255
|
+
"R6"
|
|
256
|
+
]);
|
|
257
|
+
/** A record's breaches, in the order the renderer found them.
|
|
258
|
+
*
|
|
259
|
+
* @internal
|
|
260
|
+
*/
|
|
261
|
+
const Breaches = Schema.Array(Breach);
|
|
262
|
+
/** The fixed prose each breach is refused with; nothing ever reads a refusal back, so it cannot recurse. */
|
|
263
|
+
const REFUSED_BREACH_TEXT = {
|
|
264
|
+
R1: "the headline is empty",
|
|
265
|
+
R2: "the record names no source location",
|
|
266
|
+
R6: "a replay value names a run no generator chose, or the rerun line is missing"
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* The one detail a record refusal carries: every breach the renderer found, in the prose above (R10, KTD7). The
|
|
270
|
+
* breach letter is not printed: a reader acts on what is missing, not on which requirement numbered it.
|
|
271
|
+
*
|
|
272
|
+
* @internal
|
|
273
|
+
*/
|
|
274
|
+
const refusedRecordDetail = (breaches) => `✗ refused a failure record: ${breaches.map((breach) => REFUSED_BREACH_TEXT[breach]).join(", and ")}`;
|
|
275
|
+
/** @internal */
|
|
276
|
+
var LeakedState = class extends Schema.TaggedError()("LeakedState", { detail: Schema.String }) {
|
|
277
|
+
get message() {
|
|
278
|
+
return this.detail;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
/** @internal */
|
|
282
|
+
var Slop = class extends Schema.TaggedError()("Slop", { detail: Schema.String }) {
|
|
283
|
+
get message() {
|
|
284
|
+
return this.detail;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** @internal */
|
|
288
|
+
var InvalidBudget = class extends Schema.TaggedError()("InvalidBudget", { detail: Schema.String }) {
|
|
289
|
+
get message() {
|
|
290
|
+
return this.detail;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
/** @internal */
|
|
294
|
+
var FailureRecordRefused = class extends Schema.TaggedError()("FailureRecordRefused", {
|
|
295
|
+
breaches: Breaches,
|
|
296
|
+
cause: Schema.optional(Schema.Unknown)
|
|
297
|
+
}) {
|
|
298
|
+
get message() {
|
|
299
|
+
return refusedRecordDetail(this.breaches);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/internal/provided.schema.ts
|
|
304
|
+
/**
|
|
305
|
+
* The npm name of the package under test, as its own `package.json` spells it.
|
|
306
|
+
*
|
|
307
|
+
* @internal
|
|
308
|
+
*/
|
|
309
|
+
const PackageName = Schema.NonEmptyString.pipe(Schema.brand("PackageName"));
|
|
310
|
+
/**
|
|
311
|
+
* The workspace root every rendered path is relativized against.
|
|
312
|
+
*
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
const WorkspaceRoot = Schema.NonEmptyString.pipe(Schema.brand("WorkspaceRoot"));
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/internal/provided.ts
|
|
318
|
+
/**
|
|
319
|
+
* The values the shared vitest config provides for the fork: the package under test's npm name and the workspace
|
|
320
|
+
* root every rendered path is relativized against. Both are read out of the run's provided context (Vitest's
|
|
321
|
+
* `inject`), so no production source reads a Node built-in, and an absent value is simply `undefined` — a rerun
|
|
322
|
+
* line then drops its package filter, and paths print absolute (R6, KTD5).
|
|
323
|
+
*
|
|
324
|
+
* The values are branded (`provided.schema.ts`): a package name and a workspace root are not interchangeable
|
|
325
|
+
* text.
|
|
326
|
+
*
|
|
327
|
+
* @since 4.0.0
|
|
328
|
+
*/
|
|
329
|
+
/** @internal */
|
|
330
|
+
const packageKey = "@systemfsoftware/vitest:package";
|
|
331
|
+
/** @internal */
|
|
332
|
+
const workspaceRootKey = "@systemfsoftware/vitest:workspace-root";
|
|
333
|
+
const isText$1 = (value) => typeof value === "string";
|
|
334
|
+
const nonEmpty = (value) => value.length === 0 ? void 0 : value;
|
|
335
|
+
const nonEmptyText$1 = (value) => isText$1(value) ? nonEmpty(value) : void 0;
|
|
336
|
+
const read = (key) => {
|
|
337
|
+
try {
|
|
338
|
+
return nonEmptyText$1(inject(key));
|
|
339
|
+
} catch {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const readNamed = (key, schema) => Option.getOrUndefined(Option.flatMap(Option.fromUndefinedOr(read(key)), (text) => Schema.decodeOption(schema)(text)));
|
|
344
|
+
/** @internal */
|
|
345
|
+
const providedPackage = () => readNamed(packageKey, PackageName);
|
|
346
|
+
/** @internal */
|
|
347
|
+
const providedRoot = () => readNamed(workspaceRootKey, WorkspaceRoot);
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/internal/failure-record.ts
|
|
350
|
+
/**
|
|
351
|
+
* The one renderer for a spec failure: the failure and the spans its run recorded become the record Vitest
|
|
352
|
+
* prints, together with the contract rules that record breaks (KTD1, KTD7).
|
|
353
|
+
*
|
|
354
|
+
* The record is ordered by R2-R6: the raising frame leads, the failing step and its spec line follow, then every
|
|
355
|
+
* cause layer, then the step trail with the cell decisions each step caused, then the rerun command. The
|
|
356
|
+
* renderer is pure and never throws: a value it cannot read renders as text, never as an exception.
|
|
357
|
+
*
|
|
358
|
+
* @since 4.0.0
|
|
359
|
+
*/
|
|
360
|
+
const STEP_SPAN = "gherkin.step";
|
|
361
|
+
const STEP_KEYWORD = "gherkin.keyword";
|
|
362
|
+
const STEP_TEXT = "gherkin.text";
|
|
363
|
+
const CODE_SITE = "code.site";
|
|
364
|
+
const CELL_NAME = "cell.name";
|
|
365
|
+
const CELL_STACKTRACE = "cell.stacktrace";
|
|
366
|
+
const CELL_DECIDE_STACKTRACE = "cell.decide_stacktrace";
|
|
367
|
+
const CELL_COMMAND_TAG = "cell.command_tag";
|
|
368
|
+
const CELL_MEMBER_TAGS = "cell.member_tags";
|
|
369
|
+
const CELL_DECLARED = "cell.declared";
|
|
370
|
+
const CELL_OUTCOME = "cell.outcome";
|
|
371
|
+
const TAG_FIELD = "_tag";
|
|
372
|
+
const MESSAGE_FIELD = "message";
|
|
373
|
+
const NAME_FIELD = "name";
|
|
374
|
+
const STACK_FIELD = "stack";
|
|
375
|
+
const CAUSE_FIELD = "cause";
|
|
376
|
+
const FILE_PROTOCOL = "file://";
|
|
377
|
+
const UNKNOWN_SITE = "?";
|
|
378
|
+
const DEFAULT_FAILURE_NAME = "Error";
|
|
379
|
+
const NEVER_FINISHED = " (never finished)";
|
|
380
|
+
const CAUSE_DEPTH = 6;
|
|
381
|
+
/** The fields a layer's own values exclude: the chain renders the cause, and the raise site names the stack. */
|
|
382
|
+
const SKIPPED_FIELDS = {
|
|
383
|
+
[TAG_FIELD]: true,
|
|
384
|
+
[MESSAGE_FIELD]: true,
|
|
385
|
+
[NAME_FIELD]: true,
|
|
386
|
+
[STACK_FIELD]: true,
|
|
387
|
+
[CAUSE_FIELD]: true
|
|
388
|
+
};
|
|
389
|
+
/** The contract's "first location": any `<path>:<line>` the record names. */
|
|
390
|
+
const LOCATION = /(?:[\w.@-]+\/)*[\w.@-]+\.[cm]?[jt]sx?:\d+/u;
|
|
391
|
+
const COLUMN_SUFFIX = /:\d+$/u;
|
|
392
|
+
const MARK = {
|
|
393
|
+
passed: "✓",
|
|
394
|
+
failed: "✗",
|
|
395
|
+
unfinished: "✗"
|
|
396
|
+
};
|
|
397
|
+
const not$1 = (value) => !value;
|
|
398
|
+
const and = (left, right) => left && right;
|
|
399
|
+
const or = (left, right) => left || right;
|
|
400
|
+
const firstPresent = (values) => values.find((value) => value !== void 0);
|
|
401
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
402
|
+
const isText = (value) => typeof value === "string";
|
|
403
|
+
const isArray = (value) => Array.isArray(value);
|
|
404
|
+
const isNumber = (value) => typeof value === "number";
|
|
405
|
+
const isBoolean = (value) => typeof value === "boolean";
|
|
406
|
+
const isBigInt = (value) => typeof value === "bigint";
|
|
407
|
+
const isNull = (value) => value === null;
|
|
408
|
+
const isPrimitive = (value) => or(or(isNumber(value), isBoolean(value)), isBigInt(value));
|
|
409
|
+
const objectOrUndefined = (value) => isObject(value) ? value : void 0;
|
|
410
|
+
const isFieldName = (name) => SKIPPED_FIELDS[name] !== true;
|
|
411
|
+
const presentOrUndefined = (text) => text.length === 0 ? void 0 : text;
|
|
412
|
+
const nonEmptyText = (text) => text === void 0 ? void 0 : presentOrUndefined(text);
|
|
413
|
+
const fieldOf = (value, key) => Reflect.get(value, key);
|
|
414
|
+
const textFieldOf = (value, key) => {
|
|
415
|
+
const field = fieldOf(value, key);
|
|
416
|
+
return isText(field) ? field : void 0;
|
|
417
|
+
};
|
|
418
|
+
const nonEmptyFieldOf = (value, key) => nonEmptyText(textFieldOf(value, key));
|
|
419
|
+
const tagFieldOf = (value) => nonEmptyFieldOf(value, TAG_FIELD);
|
|
420
|
+
const messageFieldOf = (value) => nonEmptyFieldOf(value, MESSAGE_FIELD);
|
|
421
|
+
const pairOf = (name, text) => ({
|
|
422
|
+
name,
|
|
423
|
+
text
|
|
424
|
+
});
|
|
425
|
+
const pairTextOf = (pair) => `${JSON.stringify(pair.name)}:${pair.text}`;
|
|
426
|
+
const pairTextsOf = (pairs) => pairs.map(pairTextOf);
|
|
427
|
+
const hasPair = (pairs, name) => pairs.some((pair) => pair.name === name);
|
|
428
|
+
const pairsOf = (value) => isObject(value) ? Object.keys(value).map((key) => pairOf(key, renderValue(fieldOf(value, key)))) : [];
|
|
429
|
+
const fieldsOf = (value) => pairsOf(value).filter((pair) => isFieldName(pair.name));
|
|
430
|
+
const renderValue = (value) => witnessOf(value).rendered;
|
|
431
|
+
const renderScalar = (value) => isText(value) ? JSON.stringify(value) : renderPrimitive(value);
|
|
432
|
+
const renderPrimitive = (value) => isBigInt(value) ? `${value}n` : renderPlain(value);
|
|
433
|
+
const renderPlain = (value) => isPrimitive(value) ? `${value}` : renderOther(value);
|
|
434
|
+
const renderOther = (value) => isNull(value) ? "null" : renderUndefined(value);
|
|
435
|
+
const renderUndefined = (value) => value === void 0 ? "undefined" : renderContainer(value);
|
|
436
|
+
const renderContainer = (value) => isArray(value) ? renderList(value) : typeof value;
|
|
437
|
+
const listTextOf = (children) => `[${children.join(",")}]`;
|
|
438
|
+
const recordTextOf = (pairs) => `{${pairTextsOf(pairs).join(",")}}`;
|
|
439
|
+
const renderList = (value) => listTextOf(value.map(renderValue));
|
|
440
|
+
const renderRecord$1 = (value) => recordTextOf(pairsOf(value));
|
|
441
|
+
const KIND_FIELD = "_kind";
|
|
442
|
+
const VALUE_FIELD = "value";
|
|
443
|
+
const DESCRIPTION_FIELD = "description";
|
|
444
|
+
const UNDEFINED_JSON = { [KIND_FIELD]: "undefined" };
|
|
445
|
+
const CIRCULAR_WITNESS = {
|
|
446
|
+
rendered: "[Circular]",
|
|
447
|
+
value: { [KIND_FIELD]: "circular" }
|
|
448
|
+
};
|
|
449
|
+
const markOf = (kind, field, text) => ({
|
|
450
|
+
[KIND_FIELD]: kind,
|
|
451
|
+
[field]: text
|
|
452
|
+
});
|
|
453
|
+
const isSymbol = (value) => typeof value === "symbol";
|
|
454
|
+
const isCallable = (value) => typeof value === "function";
|
|
455
|
+
const callableNameOf = (value) => {
|
|
456
|
+
const name = fieldOf(value, NAME_FIELD);
|
|
457
|
+
return isText(name) ? name : "";
|
|
458
|
+
};
|
|
459
|
+
const numberJsonOf = (value) => Number.isFinite(value) ? value : markOf("number", VALUE_FIELD, `${value}`);
|
|
460
|
+
const bigintJsonOf = (value) => markOf("bigint", VALUE_FIELD, `${value}`);
|
|
461
|
+
const functionJsonOf = (value) => markOf("function", NAME_FIELD, callableNameOf(value));
|
|
462
|
+
const symbolJsonOf = (value) => markOf("symbol", DESCRIPTION_FIELD, value.description ?? "");
|
|
463
|
+
const scalarJsonOf = (value) => isText(value) ? value : nonTextJsonOf(value);
|
|
464
|
+
const nonTextJsonOf = (value) => isBoolean(value) ? value : nullOrOtherJsonOf(value);
|
|
465
|
+
const nullOrOtherJsonOf = (value) => isNull(value) ? value : numberOrOtherJsonOf(value);
|
|
466
|
+
const numberOrOtherJsonOf = (value) => isNumber(value) ? numberJsonOf(value) : bigintOrOtherJsonOf(value);
|
|
467
|
+
const bigintOrOtherJsonOf = (value) => isBigInt(value) ? bigintJsonOf(value) : callableOrUndefinedJsonOf(value);
|
|
468
|
+
const callableOrUndefinedJsonOf = (value) => isCallable(value) ? functionJsonOf(value) : symbolOrUndefinedJsonOf(value);
|
|
469
|
+
const symbolOrUndefinedJsonOf = (value) => isSymbol(value) ? symbolJsonOf(value) : UNDEFINED_JSON;
|
|
470
|
+
/**
|
|
471
|
+
* The one traversal a value's rendered text and its JSON projection come from (R3, KD7): `rendered` is exactly
|
|
472
|
+
* `renderValue`'s text, `value` is the same walk with every value JSON cannot carry marked by kind, and a cycle
|
|
473
|
+
* marks `circular` rather than recursing forever.
|
|
474
|
+
*
|
|
475
|
+
* @internal
|
|
476
|
+
*/
|
|
477
|
+
const witnessOf = (value) => witnessAt(value, []);
|
|
478
|
+
const witnessAt = (value, seen) => isObject(value) ? witnessedStructure(value, seen) : witnessedScalar(value);
|
|
479
|
+
const witnessedStructure = (value, seen) => seen.includes(value) ? CIRCULAR_WITNESS : witnessedObject(value, seen);
|
|
480
|
+
const witnessedObject = (value, seen) => isArray(value) ? witnessedList(value, seen) : witnessedRecord(value, seen);
|
|
481
|
+
const witnessedScalar = (value) => ({
|
|
482
|
+
rendered: renderScalar(value),
|
|
483
|
+
value: scalarJsonOf(value)
|
|
484
|
+
});
|
|
485
|
+
const witnessedList = (value, seen) => {
|
|
486
|
+
const children = value.map((item) => witnessAt(item, seenWith(seen, value)));
|
|
487
|
+
return {
|
|
488
|
+
rendered: listTextOf(children.map(renderedOf)),
|
|
489
|
+
value: children.map(witnessValueOf)
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
const witnessedRecord = (value, seen) => {
|
|
493
|
+
const entries = entriesAt(value, seenWith(seen, value));
|
|
494
|
+
return {
|
|
495
|
+
rendered: tagFieldOf(value) ?? recordTextOf(entryPairsOf(entries)),
|
|
496
|
+
value: entriesJsonOf(entries)
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
const seenWith = (seen, value) => [...seen, value];
|
|
500
|
+
const entriesAt = (value, seen) => Object.keys(value).map((key) => witnessEntryOf(key, witnessAt(fieldOf(value, key), seen)));
|
|
501
|
+
const witnessEntryOf = (name, witness) => ({
|
|
502
|
+
name,
|
|
503
|
+
witness
|
|
504
|
+
});
|
|
505
|
+
const entryPairsOf = (entries) => entries.map((entry) => pairOf(entry.name, entry.witness.rendered));
|
|
506
|
+
const entriesJsonOf = (entries) => Object.fromEntries(entries.map((entry) => [entry.name, entry.witness.value]));
|
|
507
|
+
const renderedOf = (witness) => witness.rendered;
|
|
508
|
+
const witnessValueOf = (witness) => witness.value;
|
|
509
|
+
const bracesOf = (pairs) => ` {${pairTextsOf(pairs).join(",")}}`;
|
|
510
|
+
const renderFields = (value) => bracesOf(fieldsOf(value));
|
|
511
|
+
const textSummaryOf = (text) => text.length === 0 ? text : JSON.stringify(text);
|
|
512
|
+
const scalarSummaryOf = (value) => isText(value) ? textSummaryOf(value) : renderScalar(value);
|
|
513
|
+
/** @internal */
|
|
514
|
+
const summaryOf = (value) => isObject(value) ? objectSummaryOf(value) : scalarSummaryOf(value);
|
|
515
|
+
const objectSummaryOf = (value) => {
|
|
516
|
+
const tag = tagFieldOf(value);
|
|
517
|
+
return tag === void 0 ? untaggedSummaryOf(value) : taggedSummaryOf(tag, value);
|
|
518
|
+
};
|
|
519
|
+
const untaggedSummaryOf = (value) => isErrorValue(value) ? errorSummaryOf(value) : valueRecordSummaryOf(value);
|
|
520
|
+
const valueRecordSummaryOf = (value) => isArray(value) ? renderList(value) : renderRecord$1(value);
|
|
521
|
+
const isErrorValue = (value) => value instanceof Error;
|
|
522
|
+
const firstLine = (text) => text.split("\n")[0] ?? text;
|
|
523
|
+
const restLines = (text) => text.split("\n").slice(1);
|
|
524
|
+
const taggedSummaryOf = (tag, value) => {
|
|
525
|
+
const message = messageFieldOf(value);
|
|
526
|
+
return message === void 0 ? `${tag}${renderFields(value)}` : `${tag}: ${firstLine(message)}`;
|
|
527
|
+
};
|
|
528
|
+
const errorSummaryOf = (value) => {
|
|
529
|
+
const name = objectNameOf(value);
|
|
530
|
+
const message = messageFieldOf(value);
|
|
531
|
+
return message === void 0 ? `${name}${renderFields(value)}` : `${name}: ${firstLine(message)}`;
|
|
532
|
+
};
|
|
533
|
+
const objectNameOf = (value) => firstPresent([tagFieldOf(value), nonEmptyFieldOf(value, NAME_FIELD)]) ?? DEFAULT_FAILURE_NAME;
|
|
534
|
+
const headlineOf = (layers) => layers.length === 0 ? "" : summaryOf(layers[0]);
|
|
535
|
+
const nameOf = (layers) => {
|
|
536
|
+
const entry = layers[0];
|
|
537
|
+
return isObject(entry) ? objectNameOf(entry) : DEFAULT_FAILURE_NAME;
|
|
538
|
+
};
|
|
539
|
+
const stripProtocol = (path) => path.startsWith(FILE_PROTOCOL) ? path.slice(7) : path;
|
|
540
|
+
const stripColumn = (site) => site.replace(COLUMN_SUFFIX, "");
|
|
541
|
+
const knownSiteOf = (site) => isText(site) ? stripColumn(stripProtocol(site)) : void 0;
|
|
542
|
+
const siteTextOf = (site) => knownSiteOf(site) ?? UNKNOWN_SITE;
|
|
543
|
+
const siteOfFrames = (frames) => Option$1.getOrUndefined(Option$1.map(Option$1.fromUndefinedOr(frames.find(isUserFrame)), siteOfFrame));
|
|
544
|
+
/** The author's site in a raw captured stack, the form a cell span records instead of a pre-resolved site. */
|
|
545
|
+
const cellStackSiteOf = (stack) => isText(stack) ? siteOfFrames(framesOf$1(stack)) : void 0;
|
|
546
|
+
const stackSiteTextOf = (stack) => knownSiteOf(cellStackSiteOf(stack)) ?? UNKNOWN_SITE;
|
|
547
|
+
const stackIn = (value) => textFieldOf(value, STACK_FIELD) ?? "";
|
|
548
|
+
const stackOf = (value) => isObject(value) ? stackIn(value) : "";
|
|
549
|
+
const framesOf = (value) => framesOf$1(stackOf(value));
|
|
550
|
+
const isFrame = (frame) => frame !== void 0;
|
|
551
|
+
const usableFrameOf = (value) => framesOf(value).find(isUserFrame);
|
|
552
|
+
const frameTextOf = (frame) => `${frame.path}:${frame.line}${frameNameSuffix(frame.fn)}`;
|
|
553
|
+
const isAuthorName = (fn) => fn !== void 0 && !fn.includes("~effect/");
|
|
554
|
+
const frameNameSuffix = (fn) => isAuthorName(fn) ? ` (${fn})` : "";
|
|
555
|
+
const isSameSite = (frame, other) => and(frame.path === other.path, frame.line === other.line);
|
|
556
|
+
const isSameFrame = (frame, other) => other === void 0 ? false : isSameSite(frame, other);
|
|
557
|
+
const chosenFrameOf = (layers) => {
|
|
558
|
+
const inner = layers.slice(1).map(usableFrameOf).filter(isFrame).at(-1);
|
|
559
|
+
return inner === void 0 ? usableFrameOf(layers[0]) : inner;
|
|
560
|
+
};
|
|
561
|
+
const attrOf = (span, key) => span.attributes.get(key);
|
|
562
|
+
const textAttrOf = (span, key) => {
|
|
563
|
+
const value = attrOf(span, key);
|
|
564
|
+
return isText(value) ? value : void 0;
|
|
565
|
+
};
|
|
566
|
+
const ancestorsIn = (span) => span instanceof Tracer.NativeSpan ? [span, ...ancestorsOf(span.parent)] : [];
|
|
567
|
+
const ancestorsOf = (parent) => Option$1.isNone(parent) ? [] : ancestorsIn(parent.value);
|
|
568
|
+
const stepAncestorOf = (span) => ancestorsOf(span.parent).find((ancestor) => ancestor.name === STEP_SPAN);
|
|
569
|
+
const stateOf = (span) => Match$1.value(span.status).pipe(Match$1.when({ _tag: "Started" }, () => "unfinished"), Match$1.when({ _tag: "Ended" }, (ended) => endedStateOf(ended.exit)), Match$1.exhaustive);
|
|
570
|
+
const endedStateOf = (exit) => Exit.isSuccess(exit) ? "passed" : "failed";
|
|
571
|
+
const stepSpansOf = (spans) => spans.filter(isTopStepSpan);
|
|
572
|
+
const isTopStepSpan = (span) => and(span.name === STEP_SPAN, stepAncestorOf(span) === void 0);
|
|
573
|
+
const isCellSpan = (span) => isText(attrOf(span, CELL_STACKTRACE));
|
|
574
|
+
const cellSpansOf = (spans) => spans.filter(isCellSpan);
|
|
575
|
+
const cellNameOf = (cell) => textAttrOf(cell, CELL_NAME) ?? cell.name;
|
|
576
|
+
const causeFieldOf = (value) => isObject(value) ? objectOrUndefined(fieldOf(value, CAUSE_FIELD)) : void 0;
|
|
577
|
+
/** One layer with whatever its own `cause` adds, depth-limited. */
|
|
578
|
+
const layerWithCauseOf = (value, depth) => [value, ...nestedCauseOf(value, depth - 1)];
|
|
579
|
+
/** The layers the reasons of an Effect `Cause` add, each rendered as any other layer. */
|
|
580
|
+
const reasonLayersOf = (cause, depth) => cause.reasons.flatMap((reason) => layerWithCauseOf(reasonPayloadOf(reason), depth));
|
|
581
|
+
const layersOfCause = (cause, depth) => Cause.isCause(cause) ? reasonLayersOf(cause, depth) : layerWithCauseOf(cause, depth);
|
|
582
|
+
const nestedOf = (cause, depth) => cause === void 0 ? [] : layersOfCause(cause, depth);
|
|
583
|
+
const nestedCauseOf = (value, depth) => depth === 0 ? [] : nestedOf(causeFieldOf(value), depth);
|
|
584
|
+
const causeChainOf = (value) => [value, ...nestedCauseOf(value, CAUSE_DEPTH)];
|
|
585
|
+
const interruptTextOf = (reason) => reason.fiberId === void 0 ? "interrupted" : `interrupted by fiber #${reason.fiberId}`;
|
|
586
|
+
const reasonPayloadOf = (reason) => Match$1.value(reason).pipe(Match$1.when({ _tag: "Fail" }, (fail) => fail.error), Match$1.when({ _tag: "Die" }, (die) => die.defect), Match$1.when({ _tag: "Interrupt" }, (interrupt) => interruptTextOf(interrupt)), Match$1.exhaustive);
|
|
587
|
+
const reasonsOf = (cause) => cause.reasons.flatMap((reason) => causeChainOf(reasonPayloadOf(reason)));
|
|
588
|
+
const layersOf = (failure) => Cause.isCause(failure) ? reasonsOf(failure) : causeChainOf(failure);
|
|
589
|
+
const declineOf = (span) => stateOf(span) === "unfinished" ? NEVER_FINISHED : "";
|
|
590
|
+
const capitalize = (text) => text.length === 0 ? text : `${text.slice(0, 1).toUpperCase()}${text.slice(1)}`;
|
|
591
|
+
const keywordOf = (step) => capitalize(textAttrOf(step, STEP_KEYWORD) ?? "");
|
|
592
|
+
const stepTextOf = (step) => textAttrOf(step, STEP_TEXT) ?? "";
|
|
593
|
+
const stepSiteOf = (step) => siteTextOf(attrOf(step, CODE_SITE));
|
|
594
|
+
const stepLineOf = (step) => ` ${MARK[stateOf(step)]} ${keywordOf(step)} ${stepTextOf(step)} ${stepSiteOf(step)}${declineOf(step)}`;
|
|
595
|
+
const commandOf = (cell) => textAttrOf(cell, CELL_COMMAND_TAG) ?? "";
|
|
596
|
+
const cellPairsOf = (cell) => {
|
|
597
|
+
const members = pairsOf(attrOf(cell, CELL_MEMBER_TAGS));
|
|
598
|
+
const declared = pairsOf(attrOf(cell, CELL_DECLARED)).filter((pair) => !hasPair(members, pair.name));
|
|
599
|
+
return [...members, ...declared];
|
|
600
|
+
};
|
|
601
|
+
const pairsBracesOf = (cell) => bracesOf(cellPairsOf(cell));
|
|
602
|
+
const outcomeOf$1 = (cell) => {
|
|
603
|
+
const outcome = textAttrOf(cell, CELL_OUTCOME);
|
|
604
|
+
return outcome === void 0 ? "" : ` → ${outcome}`;
|
|
605
|
+
};
|
|
606
|
+
const blockOf = (cell) => ({
|
|
607
|
+
head: ` cell ${cellNameOf(cell)}: ${commandOf(cell)}${pairsBracesOf(cell)}${outcomeOf$1(cell)}`,
|
|
608
|
+
tail: ` decide ${stackSiteTextOf(attrOf(cell, CELL_DECIDE_STACKTRACE))} cell ${stackSiteTextOf(attrOf(cell, CELL_STACKTRACE))}`
|
|
609
|
+
});
|
|
610
|
+
const groupOf = (block, count) => ({
|
|
611
|
+
block,
|
|
612
|
+
count
|
|
613
|
+
});
|
|
614
|
+
const isSameBlock = (block, other) => and(block.head === other.head, block.tail === other.tail);
|
|
615
|
+
const appendBeside = (groups, block, last) => isSameBlock(last.block, block) ? [...groups.slice(0, -1), groupOf(block, last.count + 1)] : [...groups, groupOf(block, 1)];
|
|
616
|
+
const appendAfter = (groups, block, last) => last === void 0 ? [groupOf(block, 1)] : appendBeside(groups, block, last);
|
|
617
|
+
const appendGroup = (groups, block) => appendAfter(groups, block, groups.at(-1));
|
|
618
|
+
const groupsOf = (blocks) => blocks.reduce(appendGroup, []);
|
|
619
|
+
const groupLinesOf = (group) => [group.count > 1 ? `${group.block.head} (×${group.count})` : group.block.head, group.block.tail];
|
|
620
|
+
const cellsUnder = (spans, step) => cellSpansOf(spans).filter((cell) => stepAncestorOf(cell) === step);
|
|
621
|
+
const decisionLinesOf = (spans, step) => groupsOf(cellsUnder(spans, step).map(blockOf)).flatMap(groupLinesOf);
|
|
622
|
+
const trailStepsOf = (spans) => {
|
|
623
|
+
const steps = stepSpansOf(spans);
|
|
624
|
+
const failing = steps.find((step) => stateOf(step) !== "passed");
|
|
625
|
+
return failing === void 0 ? steps : steps.slice(0, steps.indexOf(failing) + 1);
|
|
626
|
+
};
|
|
627
|
+
const trailLinesOf = (spans) => trailStepsOf(spans).flatMap((step) => [stepLineOf(step), ...decisionLinesOf(spans, step)]);
|
|
628
|
+
const layerHintOf = (spans) => {
|
|
629
|
+
const cell = cellSpansOf(spans).at(0);
|
|
630
|
+
return cell === void 0 ? "" : ` (cell ${cellNameOf(cell)})`;
|
|
631
|
+
};
|
|
632
|
+
const failingStepTextOf = (step) => `Failing step: ${keywordOf(step)} ${stepTextOf(step)} ${stepSiteOf(step)}`;
|
|
633
|
+
const noFailingStepLineOf = (steps, spans) => steps.length === 0 ? `Failing step: no step ran${layerHintOf(spans)}` : "Failing step: none of the steps failed";
|
|
634
|
+
const failingStepLineOf = (spans) => {
|
|
635
|
+
const steps = stepSpansOf(spans);
|
|
636
|
+
const failing = steps.find((step) => stateOf(step) !== "passed");
|
|
637
|
+
return failing === void 0 ? noFailingStepLineOf(steps, spans) : failingStepTextOf(failing);
|
|
638
|
+
};
|
|
639
|
+
const isFailedSpan = (span) => stateOf(span) === "failed";
|
|
640
|
+
const isUnfinishedCell = (span) => and(isCellSpan(span), stateOf(span) === "unfinished");
|
|
641
|
+
const spanSiteOf = (span) => knownSiteOf(attrOf(span, CODE_SITE)) ?? knownSiteOf(cellStackSiteOf(attrOf(span, CELL_STACKTRACE)));
|
|
642
|
+
const cellSiteOf = (spans) => {
|
|
643
|
+
const cell = spans.toReversed().find(isCellSpan);
|
|
644
|
+
return cell === void 0 ? void 0 : spanSiteOf(cell);
|
|
645
|
+
};
|
|
646
|
+
const unfinishedDecideSiteOf = (spans) => {
|
|
647
|
+
const unfinished = spans.toReversed().find(isUnfinishedCell);
|
|
648
|
+
return unfinished === void 0 ? void 0 : knownSiteOf(cellStackSiteOf(attrOf(unfinished, CELL_DECIDE_STACKTRACE)));
|
|
649
|
+
};
|
|
650
|
+
const decideSiteOf = (spans) => unfinishedDecideSiteOf(spans) ?? cellSiteOf(spans);
|
|
651
|
+
const failedSiteOf = (spans) => {
|
|
652
|
+
const failed = spans.toReversed().find(isFailedSpan);
|
|
653
|
+
return failed === void 0 ? void 0 : spanSiteOf(failed);
|
|
654
|
+
};
|
|
655
|
+
const fallbackSiteOf = (spans) => failedSiteOf(spans) ?? decideSiteOf(spans);
|
|
656
|
+
const fallbackRaisedAtLineOf = (spans) => {
|
|
657
|
+
const site = fallbackSiteOf(spans);
|
|
658
|
+
return site === void 0 ? void 0 : ` raised at ${site}`;
|
|
659
|
+
};
|
|
660
|
+
const raisedAtLineOf = (frame, spans) => frame === void 0 ? fallbackRaisedAtLineOf(spans) : ` raised at ${frameTextOf(frame)}`;
|
|
661
|
+
const chainNamedFrame = (frame, chosen) => isSameFrame(frame, chosen) ? "" : ` raised at ${frameTextOf(frame)}`;
|
|
662
|
+
const chainFrameTextOf = (frame, chosen) => frame === void 0 ? "" : chainNamedFrame(frame, chosen);
|
|
663
|
+
const chainLineOf = (layer, chosen) => ` ${summaryOf(layer)}${chainFrameTextOf(usableFrameOf(layer), chosen)}`;
|
|
664
|
+
const chainContinuation = (line) => ` ${line}`;
|
|
665
|
+
const isMessageBearing = (layer) => or(tagFieldOf(layer) !== void 0, isErrorValue(layer));
|
|
666
|
+
const messageOf = (layer) => messageFieldOf(layer) ?? "";
|
|
667
|
+
const messageTailLines = (layer) => isMessageBearing(layer) ? restLines(messageOf(layer)).map(chainContinuation) : [];
|
|
668
|
+
const chainDetailLines = (layer) => isObject(layer) ? messageTailLines(layer) : [];
|
|
669
|
+
const chainLayerLines = (layer, chosen) => [chainLineOf(layer, chosen), ...chainDetailLines(layer)];
|
|
670
|
+
const chainLinesOf = (layers, chosen) => layers.slice(1).flatMap((layer) => chainLayerLines(layer, chosen));
|
|
671
|
+
const quoted = (text) => `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
672
|
+
const REPLAY_FIELD = "replay";
|
|
673
|
+
const propertyReplayTextOf = (failure) => {
|
|
674
|
+
const layer = propertyFailureOf(failure);
|
|
675
|
+
return layer === void 0 ? void 0 : textFieldOf(layer, REPLAY_FIELD);
|
|
676
|
+
};
|
|
677
|
+
const rerunReplayTextOf = (replay) => Option$1.getOrUndefined(Option$1.flatMap(Option$1.fromUndefinedOr(replay.seed), (seed) => Option$1.map(replayOfParts({
|
|
678
|
+
seed,
|
|
679
|
+
path: replay.path
|
|
680
|
+
}), replayTextOf)));
|
|
681
|
+
const kernelReplayTextOf = (replay) => replay === void 0 ? void 0 : rerunReplayTextOf(replay);
|
|
682
|
+
const replayPrefixText = (text) => text === void 0 ? "" : `CONFORMANCE_REPLAY="${text}" `;
|
|
683
|
+
const replayPrefixOf = (replayText, replay) => replayPrefixText(replayText ?? kernelReplayTextOf(replay));
|
|
684
|
+
const isCompleteIdentity = (identity) => and(identity.file.length > 0, identity.name.length > 0);
|
|
685
|
+
const filterPrefixOf = (identity) => identity.package.length === 0 ? "" : `pnpm --filter ${identity.package} exec `;
|
|
686
|
+
const rerunLineOf = (identity, replay, replayText) => isCompleteIdentity(identity) ? ` ${replayPrefixOf(replayText, replay)}${filterPrefixOf(identity)}vitest run ${identity.file} -t ` + quoted(identity.name) : void 0;
|
|
687
|
+
const rerunLinesOf = (identity, replay, replayText) => {
|
|
688
|
+
const line = rerunLineOf(identity, replay, replayText);
|
|
689
|
+
return line === void 0 ? [] : ["Rerun only this scenario:", line];
|
|
690
|
+
};
|
|
691
|
+
const baselineReplay = (replay) => and(replay.seed === void 0, replay.path.length > 0);
|
|
692
|
+
const replayOnBaseline = (replay) => replay === void 0 ? false : baselineReplay(replay);
|
|
693
|
+
const headMessageOf = (layers) => {
|
|
694
|
+
const entry = layers[0];
|
|
695
|
+
return isObject(entry) ? messageFieldOf(entry) : void 0;
|
|
696
|
+
};
|
|
697
|
+
const detailOf = (layers) => {
|
|
698
|
+
const message = headMessageOf(layers);
|
|
699
|
+
return message === void 0 ? [] : restLines(message);
|
|
700
|
+
};
|
|
701
|
+
const detailLinesOf = (lines) => lines.length === 0 ? [] : [...lines.map((line) => ` ${line}`), ""];
|
|
702
|
+
const scheduleLinesOf = (schedule) => schedule === void 0 ? [] : [
|
|
703
|
+
"",
|
|
704
|
+
`schedule: seed ${schedule.seed}`,
|
|
705
|
+
""
|
|
706
|
+
];
|
|
707
|
+
const recordPartsOf = (input, layers) => {
|
|
708
|
+
const chosen = chosenFrameOf(layers);
|
|
709
|
+
return {
|
|
710
|
+
headline: headlineOf(layers),
|
|
711
|
+
raisedAt: raisedAtLineOf(chosen, input.spans),
|
|
712
|
+
failingStep: failingStepLineOf(input.spans),
|
|
713
|
+
detail: detailOf(layers),
|
|
714
|
+
chain: chainLinesOf(layers, chosen),
|
|
715
|
+
trail: trailLinesOf(input.spans),
|
|
716
|
+
schedule: scheduleLinesOf(input.schedule),
|
|
717
|
+
rerun: rerunLinesOf(input.identity, input.replay, propertyReplayTextOf(input.failure))
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
const maybeLine = (line) => line === void 0 ? [] : [line];
|
|
721
|
+
const sectionLines = (header, lines) => lines.length === 0 ? [] : [
|
|
722
|
+
header,
|
|
723
|
+
...lines,
|
|
724
|
+
""
|
|
725
|
+
];
|
|
726
|
+
const linesOf$1 = (parts) => [
|
|
727
|
+
parts.headline,
|
|
728
|
+
...maybeLine(parts.raisedAt),
|
|
729
|
+
parts.failingStep,
|
|
730
|
+
...detailLinesOf(parts.detail),
|
|
731
|
+
"",
|
|
732
|
+
...sectionLines("Cause chain:", parts.chain),
|
|
733
|
+
...sectionLines("Steps and the decisions each caused:", parts.trail),
|
|
734
|
+
...parts.schedule,
|
|
735
|
+
...parts.rerun
|
|
736
|
+
];
|
|
737
|
+
const breachIf = (holds, breach) => holds ? [breach] : [];
|
|
738
|
+
const r6Broken = (parts, input) => or(parts.rerun.length === 0, replayOnBaseline(input.replay));
|
|
739
|
+
const breachesOf = (parts, input, record) => [
|
|
740
|
+
...breachIf(parts.headline.length === 0, "R1"),
|
|
741
|
+
...breachIf(not$1(LOCATION.test(record)), "R2"),
|
|
742
|
+
...breachIf(r6Broken(parts, input), "R6")
|
|
743
|
+
];
|
|
744
|
+
const hasRoot = (root) => root !== void 0 && root.length > 0;
|
|
745
|
+
const relativize = (record, root) => hasRoot(root) ? record.replaceAll(`${root}/`, "") : record;
|
|
746
|
+
/**
|
|
747
|
+
* Renders the record Vitest prints for one spec failure, and the contract rules it breaks.
|
|
748
|
+
*
|
|
749
|
+
* Never throws: a `Cause` renders layer by layer, a thrown `Error` renders as one layer, and any other value
|
|
750
|
+
* renders as itself. The first location the record names is the raising frame — the first stack frame outside
|
|
751
|
+
* `node_modules` and outside the spec libraries' own `src/` and `dist/` — and when no layer has one, the
|
|
752
|
+
* innermost failed span's site stands in, or the innermost unfinished cell's decide site for a deadlock (KTD6).
|
|
753
|
+
*
|
|
754
|
+
* @internal
|
|
755
|
+
*/
|
|
756
|
+
const renderFailureRecord = (input) => {
|
|
757
|
+
const layers = layersOf(input.failure);
|
|
758
|
+
const parts = recordPartsOf(input, layers);
|
|
759
|
+
const record = relativize(linesOf$1(parts).join("\n").trimEnd(), input.root);
|
|
760
|
+
const property = propertyFailureOf(input.failure);
|
|
761
|
+
return {
|
|
762
|
+
name: nameOf(layers),
|
|
763
|
+
record,
|
|
764
|
+
breaches: breachesOf(parts, input, record),
|
|
765
|
+
...property === void 0 ? {} : { failure: property }
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
const RECORD = Symbol.for("@systemfsoftware/vitest/FailureRecord");
|
|
769
|
+
const DIFF_FIELDS = [
|
|
770
|
+
"actual",
|
|
771
|
+
"expected",
|
|
772
|
+
"showDiff",
|
|
773
|
+
"operator",
|
|
774
|
+
"diff"
|
|
775
|
+
];
|
|
776
|
+
const ERROR_CAUSE_DEPTH = 8;
|
|
777
|
+
const isDiffLayer = (value) => fieldOf(value, "actual") !== void 0;
|
|
778
|
+
const marked = (error) => {
|
|
779
|
+
Object.defineProperty(error, RECORD, { value: true });
|
|
780
|
+
return error;
|
|
781
|
+
};
|
|
782
|
+
const recordSiteOf = (text) => LOCATION.exec(text)?.[0];
|
|
783
|
+
const withoutTag = (record) => {
|
|
784
|
+
const prefix = `${record.name}: `;
|
|
785
|
+
return record.record.startsWith(prefix) ? record.record.slice(prefix.length) : record.record;
|
|
786
|
+
};
|
|
787
|
+
const recordStackOf = (record, message) => {
|
|
788
|
+
const head = `${record.name}: ${message}`;
|
|
789
|
+
const site = recordSiteOf(record.record);
|
|
790
|
+
return site === void 0 ? head : `${head}\n at ${site}`;
|
|
791
|
+
};
|
|
792
|
+
const withStack = (error, record, message) => {
|
|
793
|
+
error.stack = recordStackOf(record, message);
|
|
794
|
+
return error;
|
|
795
|
+
};
|
|
796
|
+
const withName = (error, name) => {
|
|
797
|
+
error.name = name;
|
|
798
|
+
return error;
|
|
799
|
+
};
|
|
800
|
+
const reasonFieldOf = (reason) => fieldOf(reason, "error") ?? fieldOf(reason, "defect");
|
|
801
|
+
const firstReasonOf = (cause) => {
|
|
802
|
+
const reason = cause.reasons[0];
|
|
803
|
+
return reason === void 0 ? cause : reasonFieldOf(reason);
|
|
804
|
+
};
|
|
805
|
+
const payloadOf = (failure) => Cause.isCause(failure) ? firstReasonOf(failure) : failure;
|
|
806
|
+
const errorCauseChainOf = (value, depth) => depth <= 0 ? [] : [value, ...chainedCausesOf(value, depth)];
|
|
807
|
+
const chainedCausesOf = (value, depth) => {
|
|
808
|
+
const cause = causeFieldOf(value);
|
|
809
|
+
return cause === void 0 ? [] : errorCauseChainOf(cause, depth - 1);
|
|
810
|
+
};
|
|
811
|
+
const layerChainOf = (failure) => {
|
|
812
|
+
const first = payloadOf(failure);
|
|
813
|
+
return [first, ...chainedCausesOf(first, ERROR_CAUSE_DEPTH)];
|
|
814
|
+
};
|
|
815
|
+
const diffCarrierOf = (failure) => layerChainOf(failure).map(objectOrUndefined).filter(isObject).find(isDiffLayer);
|
|
816
|
+
const copyField = (error, carrier, field) => {
|
|
817
|
+
const value = fieldOf(carrier, field);
|
|
818
|
+
if (value === void 0) return;
|
|
819
|
+
Object.defineProperty(error, field, {
|
|
820
|
+
value,
|
|
821
|
+
enumerable: true,
|
|
822
|
+
configurable: true,
|
|
823
|
+
writable: true
|
|
824
|
+
});
|
|
825
|
+
};
|
|
826
|
+
const copyFields = (error, carrier) => {
|
|
827
|
+
for (const field of DIFF_FIELDS) copyField(error, carrier, field);
|
|
828
|
+
return error;
|
|
829
|
+
};
|
|
830
|
+
const copyDiffFields = (error, failure) => {
|
|
831
|
+
const carrier = diffCarrierOf(failure);
|
|
832
|
+
return carrier === void 0 ? error : copyFields(error, carrier);
|
|
833
|
+
};
|
|
834
|
+
/**
|
|
835
|
+
* The property-channel failure tags: a failure whose own fields ride onto the printed error and stay readable from
|
|
836
|
+
* the corpus record (R1, R2, R8). Tags, not the classes, so this module never imports the property engine.
|
|
837
|
+
*/
|
|
838
|
+
const PROPERTY_FAILURE_TAGS = [
|
|
839
|
+
"PropertyRefuted",
|
|
840
|
+
"PropertyExhausted",
|
|
841
|
+
"NonBooleanVerdict",
|
|
842
|
+
"CoverageBelowMinimum",
|
|
843
|
+
"SelfModelLaw",
|
|
844
|
+
"SeedStoreUnreadable",
|
|
845
|
+
"ReplayUnreadable",
|
|
846
|
+
"ReplayNoLongerReproduces",
|
|
847
|
+
"VacuousProperty"
|
|
848
|
+
];
|
|
849
|
+
const isPropertyFailureLayer = (value) => {
|
|
850
|
+
const tag = tagFieldOf(value);
|
|
851
|
+
return tag !== void 0 && PROPERTY_FAILURE_TAGS.includes(tag);
|
|
852
|
+
};
|
|
853
|
+
const propertyFailureOf = (failure) => layerChainOf(failure).map(objectOrUndefined).filter(isObject).find(isPropertyFailureLayer);
|
|
854
|
+
/**
|
|
855
|
+
* Copies the property failure's `_tag` and every own field onto the printed error, so a reporter reading the raw
|
|
856
|
+
* error sees them. The message and stack stay untouched.
|
|
857
|
+
*/
|
|
858
|
+
const copyOwnFields = (error, layer) => {
|
|
859
|
+
for (const field of Object.keys(layer)) copyField(error, layer, field);
|
|
860
|
+
return error;
|
|
861
|
+
};
|
|
862
|
+
const copyPropertyFields = (error, failure) => {
|
|
863
|
+
const layer = propertyFailureOf(failure);
|
|
864
|
+
return layer === void 0 ? error : copyOwnFields(error, layer);
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* The `Error` Vitest prints for one record (R2, R7, R8): its `name` is the record's failure tag, its `message` is
|
|
868
|
+
* the record, and its `stack` leads with that message — Vitest's JSON reporter hands a consumer `stack || message`,
|
|
869
|
+
* so the record has to ride there too — followed by the record's first location, so no `effect` or library frame
|
|
870
|
+
* stays. The tag is left out of the message because Vitest prints `name: message`: it would otherwise lead twice.
|
|
871
|
+
*
|
|
872
|
+
* A failure that carries a diff — Chai's `actual`, `expected`, `showDiff`, `operator` and Vitest's `diff` — has
|
|
873
|
+
* those fields copied from the innermost layer that holds them, because Vitest renders its Expected/Received
|
|
874
|
+
* block from the thrown error, not from the record text.
|
|
875
|
+
*
|
|
876
|
+
* @internal
|
|
877
|
+
*/
|
|
878
|
+
const failureRecordError = (record) => {
|
|
879
|
+
const message = withoutTag(record);
|
|
880
|
+
return marked(withStack(withName(new Error(message), record.name), record, message));
|
|
881
|
+
};
|
|
882
|
+
const isRefusalFailure = (value) => Schema$1.is(FailureRecordRefused)(value);
|
|
883
|
+
/**
|
|
884
|
+
* The refusal thrown in a record's place (R10, KTD7): fixed prose naming each breach, the original failure as its
|
|
885
|
+
* `cause`, and the record that failure would have printed beneath it. The record rides in the stack because
|
|
886
|
+
* Vitest's JSON reporter hands a consumer `stack || message`, where a cause is invisible; the prose never carries
|
|
887
|
+
* record text and is never read back, so a refusal cannot recurse.
|
|
888
|
+
*/
|
|
889
|
+
const refusalError = (failure, record) => {
|
|
890
|
+
const refusal = new FailureRecordRefused({
|
|
891
|
+
breaches: record.breaches,
|
|
892
|
+
cause: failure
|
|
893
|
+
});
|
|
894
|
+
refusal.stack = `${refusal.name}: ${refusal.message}\n${failureRecordError(record).stack ?? ""}`;
|
|
895
|
+
return refusal;
|
|
896
|
+
};
|
|
897
|
+
const printedError = (failure, record) => record.breaches.length > 0 ? refusalError(failure, record) : failureRecordError(record);
|
|
898
|
+
/** @internal */
|
|
899
|
+
const isFailureRecordError = (value) => isObject(value) && fieldOf(value, RECORD) === true;
|
|
900
|
+
/** @internal */
|
|
901
|
+
const throwFailureRecord = (input) => {
|
|
902
|
+
if (isRefusalFailure(input.failure)) throw input.failure;
|
|
903
|
+
const record = renderFailureRecord(input);
|
|
904
|
+
const printed = copyDiffFields(printedError(input.failure, record), input.failure);
|
|
905
|
+
throw copyPropertyFields(printed, input.failure);
|
|
906
|
+
};
|
|
907
|
+
const LEVEL_SEPARATOR = " > ";
|
|
908
|
+
const EMPTY_TEXT = "";
|
|
909
|
+
const textOrEmpty = (value) => isText(value) ? value : EMPTY_TEXT;
|
|
910
|
+
const firstNonEmptyText = (values) => textOrEmpty(values.find(isText));
|
|
911
|
+
const fileOf = (fullName) => textOrEmpty(fullName).split(LEVEL_SEPARATOR).at(0) ?? EMPTY_TEXT;
|
|
912
|
+
const identityOf = (task) => ({
|
|
913
|
+
package: providedPackage() ?? EMPTY_TEXT,
|
|
914
|
+
file: fileOf(task.fullName),
|
|
915
|
+
name: firstNonEmptyText([task.fullTestName, task.name])
|
|
916
|
+
});
|
|
917
|
+
const currentTask = () => TestRunner.getCurrentTest() ?? {};
|
|
918
|
+
/** @internal */
|
|
919
|
+
const testIdentityOf = (task) => identityOf(task ?? currentTask());
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/internal/driver.ts
|
|
922
|
+
/**
|
|
923
|
+
* The driver: one Effect that steps the body's generator in the fiber the test runs in.
|
|
924
|
+
*
|
|
925
|
+
* A `yield*` is a step, so the runner sees every one of them (KTD1):
|
|
926
|
+
* - a branded check is judged against this test's ledger, so it counts, and a failed judgement fails the check
|
|
927
|
+
* itself and stops the test right there (R8);
|
|
928
|
+
* - anything else opens a new observed state and then runs, which is what makes a second check on one state a
|
|
929
|
+
* second check in one window (R5), and a check inside a loop the same window as its siblings.
|
|
930
|
+
*
|
|
931
|
+
* The steps run in the same fiber as the test, so layers, virtual time, concurrency, the second run and scope
|
|
932
|
+
* finalizers keep working through the runner unchanged. A step that fails, and the test being interrupted, both
|
|
933
|
+
* return the generator: its `finally` blocks run and the steps after it never start (R8). When the body returns,
|
|
934
|
+
* the gate refuses what the body wrote and never judged, then a body that judged nothing at all (R4).
|
|
935
|
+
*/
|
|
936
|
+
/**
|
|
937
|
+
* A value the body yielded, as the driver runs it. What a body needs of its environment belongs to the layer the
|
|
938
|
+
* runner provides around this whole Effect, so the driver erases the channels here, exactly as the runner erases
|
|
939
|
+
* a body's own (`outputIsEffect` in `runner.ts`): the layer block is where a requirement is checked, and a body
|
|
940
|
+
* that needs a service nothing provides is refused by the runner at run time (R9).
|
|
941
|
+
*/
|
|
942
|
+
const isRunnable = (value) => Effect$1.isEffect(value);
|
|
943
|
+
/** A body that yielded something the driver cannot run ran nothing, so nothing in the test could fail. */
|
|
944
|
+
const refuseUnsteppable = "✗ the body yielded a value that is not an Effect. Yield the Effect itself, or a check: yield* expect(actual).toEqual(expected).";
|
|
945
|
+
/**
|
|
946
|
+
* A yielded Effect: a branded check judges itself, and anything else opens a new observed state before it runs,
|
|
947
|
+
* which is what resets the ledger's one-check window (R5).
|
|
948
|
+
*/
|
|
949
|
+
const runEffect = (value, ledger) => isCheck(value) ? value : Effect$1.andThen(Effect$1.sync(() => ledger.asserted.step()), value);
|
|
950
|
+
/** One step: anything the driver cannot run is refused, because then no step of the test ran at all. */
|
|
951
|
+
const runStep = (value, ledger) => isRunnable(value) ? runEffect(value, ledger) : Effect$1.die(refusalOf(refuseUnsteppable));
|
|
952
|
+
/** Steps the iterator to its end, feeding each yielded Effect's result back into the body. */
|
|
953
|
+
const stepThrough = (steps, ledger, input) => Effect$1.suspend(() => {
|
|
954
|
+
const state = steps.next(input);
|
|
955
|
+
if (state.done === true) return Effect$1.void;
|
|
956
|
+
return Effect$1.flatMap(runStep(state.value, ledger), (value) => stepThrough(steps, ledger, value));
|
|
957
|
+
});
|
|
958
|
+
/**
|
|
959
|
+
* After the body returns: a check written and never yielded is refused first, then a body that judged none.
|
|
960
|
+
* Both carry the same text the refusal carries on its compile channel, branded by `refusalOf` so the runner's
|
|
961
|
+
* second run re-throws them as the refusals they are.
|
|
962
|
+
*/
|
|
963
|
+
const gate = (ledger) => Effect$1.andThen(refusingUnyielded(ledger), judgedNone(ledger));
|
|
964
|
+
/** @internal */
|
|
965
|
+
const refusingUnyielded = (ledger) => Effect$1.suspend(() => ledger.unyielded() > 0 ? refuse(refuseUnyielded) : Effect$1.void);
|
|
966
|
+
const judgedNone = (ledger) => Effect$1.suspend(() => ledger.judged() === 0 ? refuse(noCheckRefusal) : Effect$1.void);
|
|
967
|
+
const refuse = (text) => Effect$1.die(refusalOf(text));
|
|
968
|
+
/**
|
|
969
|
+
* A failure or an interruption returns the generator, so its `finally` blocks run and the step after the failed
|
|
970
|
+
* check never does (R8).
|
|
971
|
+
*/
|
|
972
|
+
const unwind = (steps) => Effect$1.sync(() => {
|
|
973
|
+
steps.return?.(void 0);
|
|
974
|
+
});
|
|
975
|
+
const driveBody = (body, args, ledger) => Effect$1.suspend(() => {
|
|
976
|
+
const steps = body(...args);
|
|
977
|
+
return Effect$1.onExit(Effect$1.flatMap(stepThrough(steps, ledger, void 0), () => gate(ledger)), (exit) => Exit.isSuccess(exit) ? Effect$1.void : unwind(steps));
|
|
978
|
+
});
|
|
979
|
+
/** The body's steps, with this test's ledger provided as the `Asserted` every check in it reaches for. */
|
|
980
|
+
const driven = (body, args, ledger) => Effect$1.provideService(driveBody(body, args, ledger), Asserted, ledger.asserted);
|
|
981
|
+
/**
|
|
982
|
+
* Runs one test's body: `body` called with `args`, stepped against `ledger`, with the ledger's `asserted`
|
|
983
|
+
* provided as `Asserted` so every check in the body — including one yielded inside a forked fiber — reaches it.
|
|
984
|
+
*
|
|
985
|
+
* @internal
|
|
986
|
+
*/
|
|
987
|
+
const drive = Function.dual(3, driven);
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/internal/property/coverage.ts
|
|
990
|
+
/**
|
|
991
|
+
* @internal The R14 coverage classes: labelled input predicates, each with a minimum share of runs.
|
|
992
|
+
*
|
|
993
|
+
* A class fails only when QuickCheck's sequential test (`stdConfidence`) is confident its share is below
|
|
994
|
+
* the minimum, and is satisfied once the test is confident the share is at least 0.9 of the minimum
|
|
995
|
+
* (`TOLERANCE` at a certainty of 10^9). The tolerance is what makes the test terminate: a class sitting
|
|
996
|
+
* exactly at its minimum would otherwise draw forever.
|
|
997
|
+
*/
|
|
998
|
+
const CERTAINTY = 10 ** 9;
|
|
999
|
+
const TOLERANCE = .9;
|
|
1000
|
+
const FIRST_BATCH = 64;
|
|
1001
|
+
const BATCH_CAP = 4096;
|
|
1002
|
+
const DRAW_CAP = 1e5;
|
|
1003
|
+
/** The certainty's two-sided tail probability: `alpha / 2` for `alpha = 1 / CERTAINTY`. */
|
|
1004
|
+
const TAIL_PROBABILITY = 1 / (2 * CERTAINTY);
|
|
1005
|
+
const C1 = -.007784894002430293;
|
|
1006
|
+
const C2 = -.3223964580411365;
|
|
1007
|
+
const C3 = -2.400758277161838;
|
|
1008
|
+
const C4 = -2.549732539343734;
|
|
1009
|
+
const C5 = 4.374664141464968;
|
|
1010
|
+
const C6 = 2.938163982698783;
|
|
1011
|
+
const D1 = .007784695709041462;
|
|
1012
|
+
const D2 = .3224671290700398;
|
|
1013
|
+
const D3 = 2.445134137142996;
|
|
1014
|
+
const D4 = 3.754408661907416;
|
|
1015
|
+
const TAIL_NUMERATOR = [
|
|
1016
|
+
C1,
|
|
1017
|
+
C2,
|
|
1018
|
+
C3,
|
|
1019
|
+
C4,
|
|
1020
|
+
C5,
|
|
1021
|
+
C6
|
|
1022
|
+
];
|
|
1023
|
+
const TAIL_DENOMINATOR = [
|
|
1024
|
+
D1,
|
|
1025
|
+
D2,
|
|
1026
|
+
D3,
|
|
1027
|
+
D4
|
|
1028
|
+
];
|
|
1029
|
+
/** Horner's rule over a coefficient list, highest power first. */
|
|
1030
|
+
const poly = (coefficients, x) => coefficients.reduceRight((total, coefficient) => total * x + coefficient, 0);
|
|
1031
|
+
const tailSpread = (p) => Math.sqrt(-2 * Math.log(p));
|
|
1032
|
+
const tailInverse = (spread) => poly(TAIL_NUMERATOR, spread) / poly(TAIL_DENOMINATOR, spread);
|
|
1033
|
+
/** The inverse standard-normal CDF at the tail probability the certainty asks for. */
|
|
1034
|
+
const TAIL_Z = tailInverse(tailSpread(TAIL_PROBABILITY));
|
|
1035
|
+
const wilson = (hits, runs, z) => {
|
|
1036
|
+
const share = hits / runs;
|
|
1037
|
+
const zz = z * z;
|
|
1038
|
+
return (share + zz / (2 * runs) + z * Math.sqrt(share * (1 - share) / runs + zz / (4 * runs * runs))) / (1 + zz / runs);
|
|
1039
|
+
};
|
|
1040
|
+
const wilsonLower = (entry, runs) => wilson(entry.hits, runs, -TAIL_Z);
|
|
1041
|
+
const wilsonUpper = (entry, runs) => wilson(entry.hits, runs, TAIL_Z);
|
|
1042
|
+
const covered = (entry, runs) => wilsonLower(entry, runs) >= TOLERANCE * entry.minimum;
|
|
1043
|
+
const uncovered = (entry, runs) => wilsonUpper(entry, runs) < entry.minimum;
|
|
1044
|
+
const decided = (entry, runs) => covered(entry, runs) || uncovered(entry, runs);
|
|
1045
|
+
const settled = (entry, runs) => runs === 0 ? false : decided(entry, runs);
|
|
1046
|
+
const entriesOf = (classes) => Object.keys(classes).map((label) => entryAt(classes, label));
|
|
1047
|
+
const entryAt = (classes, label) => [label, classOf(classes, label)];
|
|
1048
|
+
const classOf = (classes, label) => classes[label] ?? {
|
|
1049
|
+
hits: 0,
|
|
1050
|
+
minimum: 0
|
|
1051
|
+
};
|
|
1052
|
+
const judgeOf = (classes, runs) => ({
|
|
1053
|
+
classes: new Map(entriesOf(classes)),
|
|
1054
|
+
runs
|
|
1055
|
+
});
|
|
1056
|
+
const openLabels = (judge) => [...judge.classes].filter(([, entry]) => !settled(entry, judge.runs)).map(([label]) => label);
|
|
1057
|
+
const failedEntries = (judge) => [...judge.classes].filter(([, entry]) => uncovered(entry, judge.runs));
|
|
1058
|
+
const failedFailures = (judge) => failedEntries(judge).map(([label, entry]) => ({
|
|
1059
|
+
label,
|
|
1060
|
+
hits: entry.hits,
|
|
1061
|
+
runs: judge.runs,
|
|
1062
|
+
minimum: entry.minimum
|
|
1063
|
+
}));
|
|
1064
|
+
/** @internal */
|
|
1065
|
+
const countHit = Function.dual(2, (classes, label) => {
|
|
1066
|
+
const prior = classes.get(label);
|
|
1067
|
+
if (prior !== void 0) classes.set(label, {
|
|
1068
|
+
hits: prior.hits + 1,
|
|
1069
|
+
minimum: prior.minimum
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
const countLabels = (classes, labels) => {
|
|
1073
|
+
for (const label of labels) countHit(classes, label);
|
|
1074
|
+
return classes;
|
|
1075
|
+
};
|
|
1076
|
+
const countDraws = (classes, drawn) => drawn.reduce(countLabels, new Map(classes));
|
|
1077
|
+
const withDraws = (judge, drawn) => ({
|
|
1078
|
+
classes: countDraws(judge.classes, drawn),
|
|
1079
|
+
runs: judge.runs + drawn.length
|
|
1080
|
+
});
|
|
1081
|
+
const drawBatch = (judge, draw, batch) => withDraws(judge, draw.more(sizeOf(judge, batch)));
|
|
1082
|
+
const sizeOf = (judge, batch) => Math.min(batch, DRAW_CAP - judge.runs);
|
|
1083
|
+
const grown = (batch) => Math.min(batch * 2, BATCH_CAP);
|
|
1084
|
+
const keepDrawing = (judge) => openLabels(judge).length > 0 && judge.runs < DRAW_CAP;
|
|
1085
|
+
const toppedUp = (judge, draw, batch) => keepDrawing(judge) ? toppedUp(drawBatch(judge, draw, batch), draw, grown(batch)) : judge;
|
|
1086
|
+
/**
|
|
1087
|
+
* Runs R14's sequential draw: tops up the undecided classes until every class is decided or the draw cap
|
|
1088
|
+
* is reached, then hands back the confidently under-covered classes as data, each with its label, hits, the runs
|
|
1089
|
+
* counted and the required share.
|
|
1090
|
+
*
|
|
1091
|
+
* @internal
|
|
1092
|
+
*/
|
|
1093
|
+
const judgeCoverage = (options) => failedFailures(toppedUp(judgeOf(options.classes, options.runs), options.draw, FIRST_BATCH));
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/internal/property/error.schema.ts
|
|
1096
|
+
/**
|
|
1097
|
+
* The closed set of property-channel failures (KTD1, R1): refuted, non-boolean, under-covered and self-model, plus
|
|
1098
|
+
* the vacuous impostor verdict, the seed store's read failure and a stale explicit replay. Every variant is a `Schema.TaggedError` carrying
|
|
1099
|
+
* its own data — the property it names, a rendered-and-projected witness, coverage classes, the law that refused
|
|
1100
|
+
* itself — and a zero-argument `message` getter computed only from those fields. Wording is free; nothing reads a
|
|
1101
|
+
* message back.
|
|
1102
|
+
*
|
|
1103
|
+
* @since 4.0.0
|
|
1104
|
+
*/
|
|
1105
|
+
/**
|
|
1106
|
+
* A property's seed: a whole number a generator chose, never negative (CONST-D1).
|
|
1107
|
+
*
|
|
1108
|
+
* @internal
|
|
1109
|
+
*/
|
|
1110
|
+
const PropertySeed = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertySeed"));
|
|
1111
|
+
/**
|
|
1112
|
+
* The count of runs a property's check was configured with, never negative (CONST-D1).
|
|
1113
|
+
*
|
|
1114
|
+
* @internal
|
|
1115
|
+
*/
|
|
1116
|
+
const PropertyRunCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyRunCount"));
|
|
1117
|
+
/** @internal */
|
|
1118
|
+
const PropertyShrinkCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyShrinkCount"));
|
|
1119
|
+
/**
|
|
1120
|
+
* One witness: the text the renderer prints for a value, and a JSON-safe projection of the same traversal (R3, KD7).
|
|
1121
|
+
*
|
|
1122
|
+
* @internal
|
|
1123
|
+
*/
|
|
1124
|
+
const Witness = Schema.Struct({
|
|
1125
|
+
rendered: Schema.String,
|
|
1126
|
+
value: Schema.Json
|
|
1127
|
+
});
|
|
1128
|
+
/**
|
|
1129
|
+
* One property involved in a failure: its full name, its declaration site, its seed and its run count (R2).
|
|
1130
|
+
*
|
|
1131
|
+
* @internal
|
|
1132
|
+
*/
|
|
1133
|
+
const PropertyRun = Schema.Struct({
|
|
1134
|
+
name: Schema.String,
|
|
1135
|
+
site: Schema.NullOr(Schema.String),
|
|
1136
|
+
seed: PropertySeed,
|
|
1137
|
+
runs: PropertyRunCount
|
|
1138
|
+
});
|
|
1139
|
+
/**
|
|
1140
|
+
* The kinds a non-boolean verdict can be: every `typeof` result, plus an `Effect` on the sync lane (R4).
|
|
1141
|
+
*
|
|
1142
|
+
* @internal
|
|
1143
|
+
*/
|
|
1144
|
+
const VerdictKind = Schema.Literals([
|
|
1145
|
+
"undefined",
|
|
1146
|
+
"object",
|
|
1147
|
+
"boolean",
|
|
1148
|
+
"number",
|
|
1149
|
+
"bigint",
|
|
1150
|
+
"string",
|
|
1151
|
+
"symbol",
|
|
1152
|
+
"function",
|
|
1153
|
+
"effect"
|
|
1154
|
+
]);
|
|
1155
|
+
/**
|
|
1156
|
+
* A falsified property: the property that ran, the shrunk counterexample the generator settled on, and how many
|
|
1157
|
+
* shrink steps it took to reach it (R2-R4).
|
|
1158
|
+
*
|
|
1159
|
+
* @internal
|
|
1160
|
+
*/
|
|
1161
|
+
var PropertyRefuted = class extends Schema.TaggedError()("PropertyRefuted", {
|
|
1162
|
+
property: PropertyRun,
|
|
1163
|
+
counterexample: Witness,
|
|
1164
|
+
shrinks: PropertyShrinkCount,
|
|
1165
|
+
replay: Schema.String
|
|
1166
|
+
}) {
|
|
1167
|
+
get message() {
|
|
1168
|
+
return `${this.property.name}: the property was falsified within ${String(this.property.runs)} run(s), and ${String(this.shrinks)} shrink(s) settled on its input. Shrunk input: ${this.counterexample.rendered}`;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
/** @internal */
|
|
1172
|
+
var PropertyExhausted = class extends Schema.TaggedError()("PropertyExhausted", {
|
|
1173
|
+
property: PropertyRun,
|
|
1174
|
+
discards: PropertyRunCount,
|
|
1175
|
+
budget: Schema.Struct({
|
|
1176
|
+
runs: PropertyRunCount,
|
|
1177
|
+
maxDiscards: Schema.NullOr(PropertyRunCount)
|
|
1178
|
+
}),
|
|
1179
|
+
replay: Schema.String
|
|
1180
|
+
}) {
|
|
1181
|
+
get message() {
|
|
1182
|
+
const cap = this.budget.maxDiscards === null ? "no configured discard cap" : `a discard cap of ${String(this.budget.maxDiscards)}`;
|
|
1183
|
+
return `${this.property.name}: the generator discarded ${String(this.discards)} draw(s) under ${cap} within ${String(this.budget.runs)} run(s), so the property was never evaluated. Narrow the generator so it draws admissible values, or raise \`maxDiscards\`.`;
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* A verdict that was not a literal boolean: the property that ran, the first drawn values whose verdict was not a
|
|
1188
|
+
* boolean, and every kind of value the property returned (R4).
|
|
1189
|
+
*
|
|
1190
|
+
* @internal
|
|
1191
|
+
*/
|
|
1192
|
+
var NonBooleanVerdict = class extends Schema.TaggedError()("NonBooleanVerdict", {
|
|
1193
|
+
property: PropertyRun,
|
|
1194
|
+
drawn: Witness,
|
|
1195
|
+
returned: Schema.Array(VerdictKind),
|
|
1196
|
+
replay: Schema.String
|
|
1197
|
+
}) {
|
|
1198
|
+
get message() {
|
|
1199
|
+
return `${this.property.name}: the property returned no boolean; it returned ${this.returned.join(", ")}. A property must return a literal boolean verdict — asserting with expect(), or returning an Option, Result or object, is not a verdict.`;
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
/**
|
|
1203
|
+
* One coverage class left under its minimum: its label, hits, runs and required share (R7).
|
|
1204
|
+
*
|
|
1205
|
+
* @internal
|
|
1206
|
+
*/
|
|
1207
|
+
const CoverageClassFailure = Schema.Struct({
|
|
1208
|
+
label: Schema.String,
|
|
1209
|
+
hits: Schema.Int,
|
|
1210
|
+
runs: Schema.Int,
|
|
1211
|
+
minimum: Schema.Finite
|
|
1212
|
+
});
|
|
1213
|
+
/**
|
|
1214
|
+
* A property whose declared coverage classes stayed under their minima: the property and the failed classes, in
|
|
1215
|
+
* declaration order (R7).
|
|
1216
|
+
*
|
|
1217
|
+
* @internal
|
|
1218
|
+
*/
|
|
1219
|
+
var CoverageBelowMinimum = class extends Schema.TaggedError()("CoverageBelowMinimum", {
|
|
1220
|
+
property: PropertyRun,
|
|
1221
|
+
classes: Schema.Array(CoverageClassFailure),
|
|
1222
|
+
replay: Schema.String
|
|
1223
|
+
}) {
|
|
1224
|
+
get message() {
|
|
1225
|
+
const lines = this.classes.map((entry) => `coverage ${entry.label}: ${entry.hits}/${entry.runs} of runs, below the required ${entry.minimum}`);
|
|
1226
|
+
return `${this.property.name}: ${lines.join("\n")}`;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
/**
|
|
1230
|
+
* A model law given the subject itself as its oracle, so it compares nothing (R13).
|
|
1231
|
+
*
|
|
1232
|
+
* @internal
|
|
1233
|
+
*/
|
|
1234
|
+
var SelfModelLaw = class extends Schema.TaggedError()("SelfModelLaw", {
|
|
1235
|
+
law: Schema.String,
|
|
1236
|
+
site: Schema.NullOr(Schema.String)
|
|
1237
|
+
}) {
|
|
1238
|
+
get message() {
|
|
1239
|
+
return `${this.law}: the model is the subject itself; a model law must compare against an independent oracle.`;
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
/**
|
|
1243
|
+
* A seed-store line that could not be decoded (R16, KTD6).
|
|
1244
|
+
*
|
|
1245
|
+
* @internal
|
|
1246
|
+
*/
|
|
1247
|
+
var SeedStoreUnreadable = class extends Schema.TaggedError()("SeedStoreUnreadable", {
|
|
1248
|
+
file: Schema.String,
|
|
1249
|
+
line: Schema.Int,
|
|
1250
|
+
detail: Schema.String
|
|
1251
|
+
}) {
|
|
1252
|
+
get message() {
|
|
1253
|
+
return `the seed store ${this.file} line ${this.line} could not be read: ${this.detail}`;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
/**
|
|
1257
|
+
* What a subject's constant impostor froze for one property: each frozen key — the subject itself, or a record
|
|
1258
|
+
* member — with the value its first call returned (R4, R6). Empty when the property never called the subject.
|
|
1259
|
+
*
|
|
1260
|
+
* @internal
|
|
1261
|
+
*/
|
|
1262
|
+
const FrozenOutput = Schema.Struct({
|
|
1263
|
+
member: Schema.String,
|
|
1264
|
+
output: Witness
|
|
1265
|
+
});
|
|
1266
|
+
/**
|
|
1267
|
+
* The impostor's frozen outputs (R4).
|
|
1268
|
+
*
|
|
1269
|
+
* @internal
|
|
1270
|
+
*/
|
|
1271
|
+
const Frozen = Schema.TaggedStruct("Frozen", { outputs: Schema.Array(FrozenOutput) });
|
|
1272
|
+
/**
|
|
1273
|
+
* A property whose body never called the subject during the vacuity check: nothing was frozen (R6).
|
|
1274
|
+
*
|
|
1275
|
+
* @internal
|
|
1276
|
+
*/
|
|
1277
|
+
const NeverCalled = Schema.TaggedStruct("NeverCalled", {});
|
|
1278
|
+
/**
|
|
1279
|
+
* One non-refuting property of a vacuous subject: the property it ran, and whether the impostor was called (R5).
|
|
1280
|
+
*
|
|
1281
|
+
* @internal
|
|
1282
|
+
*/
|
|
1283
|
+
const VacuousPropertyRun = Schema.Struct({
|
|
1284
|
+
property: PropertyRun,
|
|
1285
|
+
frozen: Schema.Union([Frozen, NeverCalled])
|
|
1286
|
+
});
|
|
1287
|
+
/**
|
|
1288
|
+
* One vacuous subject: what it is, and every non-refuting property in the file that ran over it (R5).
|
|
1289
|
+
*
|
|
1290
|
+
* @internal
|
|
1291
|
+
*/
|
|
1292
|
+
const VacuousSubject = Schema.Struct({
|
|
1293
|
+
label: Schema.String,
|
|
1294
|
+
properties: Schema.Array(VacuousPropertyRun)
|
|
1295
|
+
});
|
|
1296
|
+
/**
|
|
1297
|
+
* A law kind exempt from the impostor gate in a refused file: its name and the kind that exempted it (R6).
|
|
1298
|
+
*
|
|
1299
|
+
* @internal
|
|
1300
|
+
*/
|
|
1301
|
+
const ExemptLaw = Schema.Struct({
|
|
1302
|
+
name: Schema.String,
|
|
1303
|
+
kind: Schema.String
|
|
1304
|
+
});
|
|
1305
|
+
const REPAIR_ADVICE = "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.";
|
|
1306
|
+
const fakeTextOf = (run) => Match.value(run.frozen).pipe(Match.tag("Frozen", (frozen) => frozen.outputs.map((entry) => `${entry.member} always returned ${entry.output.rendered}`)), Match.tag("NeverCalled", () => []), Match.exhaustive);
|
|
1307
|
+
const subjectMessageOf = (subject) => {
|
|
1308
|
+
const names = subject.properties.map((run) => run.property.name).join(", ");
|
|
1309
|
+
const runs = subject.properties.reduce((total, run) => total + run.property.runs, 0);
|
|
1310
|
+
const fakes = subject.properties.flatMap(fakeTextOf);
|
|
1311
|
+
const fake = fakes.length === 0 ? "was never called" : fakes.join("; ");
|
|
1312
|
+
return `${names}: no property in this file refuted the constant impostor of this subject (${subject.label}), so nothing here pins it down. It held for ${String(runs)} run(s) against a fake that ${fake}, whatever the input. ${REPAIR_ADVICE}`;
|
|
1313
|
+
};
|
|
1314
|
+
/**
|
|
1315
|
+
* Thrown at file end when no property in the file refuted a subject's constant impostor (R12, R5, R6).
|
|
1316
|
+
*
|
|
1317
|
+
* @internal
|
|
1318
|
+
*/
|
|
1319
|
+
var VacuousProperty = class extends Schema.TaggedError()("VacuousProperty", {
|
|
1320
|
+
subjects: Schema.Array(VacuousSubject),
|
|
1321
|
+
exempt: Schema.Array(ExemptLaw),
|
|
1322
|
+
replay: Schema.String
|
|
1323
|
+
}) {
|
|
1324
|
+
get message() {
|
|
1325
|
+
return this.subjects.map(subjectMessageOf).join("\n\n");
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
/** @internal */
|
|
1329
|
+
var ReplayUnreadable = class extends Schema.TaggedError()("ReplayUnreadable", { text: Schema.String }) {
|
|
1330
|
+
get message() {
|
|
1331
|
+
return `CONFORMANCE_REPLAY names neither a seed and path nor a property entry: ${this.text}`;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
/**
|
|
1335
|
+
* An explicit `CONFORMANCE_REPLAY` replay whose recorded failure no longer reproduces: after the root re-check,
|
|
1336
|
+
* the replay still mismatched, so no draw reproduces the failure the text names. The recorded seed-store path is
|
|
1337
|
+
* not this — there the entry is one candidate among the novel draws, and only a draw that still fails refutes.
|
|
1338
|
+
*
|
|
1339
|
+
* @internal
|
|
1340
|
+
*/
|
|
1341
|
+
var ReplayNoLongerReproduces = class extends Schema.TaggedError()("ReplayNoLongerReproduces", {
|
|
1342
|
+
property: PropertyRun,
|
|
1343
|
+
replay: Schema.String
|
|
1344
|
+
}) {
|
|
1345
|
+
get message() {
|
|
1346
|
+
return `${this.property.name}: the CONFORMANCE_REPLAY replay ${this.replay} no longer reproduces a failure for this property; the case it named is gone or its generator changed.`;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
/**
|
|
1350
|
+
* Thrown when a law kind is given a record `of`, which cannot be spread into its subject.
|
|
1351
|
+
*
|
|
1352
|
+
* @internal
|
|
1353
|
+
*/
|
|
1354
|
+
var NonTupleOf = class extends Schema.TaggedError()("NonTupleOf", { detail: Schema.String }) {
|
|
1355
|
+
get message() {
|
|
1356
|
+
return this.detail;
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/internal/property/replay.schema.ts
|
|
1361
|
+
/**
|
|
1362
|
+
* The token effect's falsification runner records, as the Schema that decodes it (KTD5). One falsification's
|
|
1363
|
+
* coordinates ride in a JSON array: a leading `0` naming a numeric seed, the seed text, the attempt, the size,
|
|
1364
|
+
* the shrink path, and the failure tag.
|
|
1365
|
+
*
|
|
1366
|
+
* @since 4.0.0
|
|
1367
|
+
*/
|
|
1368
|
+
/** @internal */
|
|
1369
|
+
const ReplayToken = Schema$1.fromJsonString(Schema$1.Tuple([
|
|
1370
|
+
Schema$1.Finite,
|
|
1371
|
+
Schema$1.Union([Schema$1.Finite, Schema$1.String]),
|
|
1372
|
+
Schema$1.Finite,
|
|
1373
|
+
Schema$1.Finite,
|
|
1374
|
+
Schema$1.Array(Schema$1.Finite),
|
|
1375
|
+
Schema$1.String
|
|
1376
|
+
]));
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/internal/property/replay.ts
|
|
1379
|
+
const NUMERIC_SEED = 0;
|
|
1380
|
+
/** @internal */
|
|
1381
|
+
const tokenOfReplay = (entry) => JSON.stringify([
|
|
1382
|
+
NUMERIC_SEED,
|
|
1383
|
+
`${entry.seed}`,
|
|
1384
|
+
entry.attempt,
|
|
1385
|
+
entry.size,
|
|
1386
|
+
entry.path,
|
|
1387
|
+
entry.failure
|
|
1388
|
+
]);
|
|
1389
|
+
const FAILURE_CLASSES = {
|
|
1390
|
+
ReturnedFalse: true,
|
|
1391
|
+
PropertyError: true
|
|
1392
|
+
};
|
|
1393
|
+
/**
|
|
1394
|
+
* The tokens that replay only the root draw of `token`, one per failure class: the recorded attempt with an empty
|
|
1395
|
+
* shrink path. Empty for an absent or unreadable token.
|
|
1396
|
+
*
|
|
1397
|
+
* @internal
|
|
1398
|
+
*/
|
|
1399
|
+
const rootTokensOf = (token) => Option$1.match(Option$1.flatMap(Option$1.fromNullishOr(token), Schema$1.decodeOption(ReplayToken)), {
|
|
1400
|
+
onNone: () => [],
|
|
1401
|
+
onSome: (decoded) => Object.keys(FAILURE_CLASSES).map((failure) => JSON.stringify([
|
|
1402
|
+
decoded[0],
|
|
1403
|
+
`${decoded[1]}`,
|
|
1404
|
+
decoded[2],
|
|
1405
|
+
decoded[3],
|
|
1406
|
+
[],
|
|
1407
|
+
failure
|
|
1408
|
+
]))
|
|
1409
|
+
});
|
|
1410
|
+
/** @internal */
|
|
1411
|
+
const refutedReplayTextOf = (identity) => Option$1.getOrUndefined(Option$1.map(Schema$1.decodeOption(ReplayToken)(identity.token), (decoded) => refutedReplayText$1({
|
|
1412
|
+
property: identity.property,
|
|
1413
|
+
seed: identity.seed,
|
|
1414
|
+
runs: identity.runs,
|
|
1415
|
+
attempt: decoded[2],
|
|
1416
|
+
size: decoded[3],
|
|
1417
|
+
path: decoded[4],
|
|
1418
|
+
failure: decoded[5]
|
|
1419
|
+
})));
|
|
1420
|
+
/** @internal */
|
|
1421
|
+
const plainReplayTextOf = (identity) => plainReplayText(identity);
|
|
1422
|
+
/** @internal */
|
|
1423
|
+
const plainReplayEntriesText = (entries) => plainReplayTexts(entries);
|
|
1424
|
+
const isEntries = (channel) => Array.isArray(channel);
|
|
1425
|
+
/** @internal */
|
|
1426
|
+
const selectReplayEntry = (selection) => isEntries(selection.channel) ? selection.channel.find((entry) => entry.property === selection.hash) : void 0;
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/internal/property/impostor.ts
|
|
1429
|
+
/**
|
|
1430
|
+
* @internal The constant impostor of R12: a fake of the subject under test that
|
|
1431
|
+
* returns the first output it ever produced, for every later call, whatever the
|
|
1432
|
+
* input it was given.
|
|
1433
|
+
*
|
|
1434
|
+
* A property that still holds for the impostor does not constrain the subject.
|
|
1435
|
+
* Whether that is fatal is a per-file question — a subject's impostor must be
|
|
1436
|
+
* refuted by some property in its file — so this module builds the fake and
|
|
1437
|
+
* collects the verdicts; the engine decides them once, when the file finishes
|
|
1438
|
+
* (KTD10).
|
|
1439
|
+
*/
|
|
1440
|
+
const SUBJECT_KEY = "the subject";
|
|
1441
|
+
const isCallableSubject = (target) => typeof target === "function";
|
|
1442
|
+
const emptyState = () => ({ outputs: /* @__PURE__ */ new Map() });
|
|
1443
|
+
const frozenAt = (state, key, compute) => state.outputs.has(key) ? state.outputs.get(key) : remember(state, key, compute());
|
|
1444
|
+
const remember = (state, key, value) => {
|
|
1445
|
+
state.outputs.set(key, value);
|
|
1446
|
+
return value;
|
|
1447
|
+
};
|
|
1448
|
+
const callOutput = (state, fn, thisArg, args) => frozenAt(state, SUBJECT_KEY, () => Reflect.apply(fn, thisArg, args));
|
|
1449
|
+
const memberWrapper = (state, key, fn) => (...values) => frozenAt(state, key, () => Reflect.apply(fn, void 0, values));
|
|
1450
|
+
const memberWrapperOf = (state, fn, key) => fn === void 0 ? void 0 : memberWrapper(state, key, fn);
|
|
1451
|
+
const appliedImpostor = (state, target, thisArg, args) => isCallableSubject(target) ? callOutput(state, target, thisArg, args) : void 0;
|
|
1452
|
+
const memberImpostor = (state, target, key) => memberWrapperOf(state, target[key], key);
|
|
1453
|
+
const readImpostor = (state, target, property) => isCallableSubject(target) ? Reflect.get(target, property, target) : memberRead(state, target, property);
|
|
1454
|
+
const memberRead = (state, target, property) => typeof property === "string" ? memberImpostor(state, target, property) : void 0;
|
|
1455
|
+
const impostorHandler = (state) => ({
|
|
1456
|
+
apply: (target, thisArg, args) => appliedImpostor(state, target, thisArg, args),
|
|
1457
|
+
get: (target, property) => readImpostor(state, target, property)
|
|
1458
|
+
});
|
|
1459
|
+
const frozenOutputs = (state) => [...state.outputs].map(([member, output]) => ({
|
|
1460
|
+
member,
|
|
1461
|
+
output
|
|
1462
|
+
}));
|
|
1463
|
+
/**
|
|
1464
|
+
* A constant impostor of `subject`: a function returns its first output forever, and so does each member
|
|
1465
|
+
* of a record.
|
|
1466
|
+
*
|
|
1467
|
+
* @internal
|
|
1468
|
+
*/
|
|
1469
|
+
const impostorOf = (subject) => {
|
|
1470
|
+
const state = emptyState();
|
|
1471
|
+
return {
|
|
1472
|
+
impostor: new Proxy(subject, impostorHandler(state)),
|
|
1473
|
+
frozen: () => frozenOutputs(state)
|
|
1474
|
+
};
|
|
1475
|
+
};
|
|
1476
|
+
/** @internal */
|
|
1477
|
+
const subjectLabel = (subject) => isCallableSubject(subject) ? `a function of ${String(subject.length)} argument(s)` : `a record of ${Object.keys(subject).sort().join(", ")}`;
|
|
1478
|
+
const judgeFor = (subject) => ({
|
|
1479
|
+
label: subjectLabel(subject),
|
|
1480
|
+
verdicts: [],
|
|
1481
|
+
refuters: []
|
|
1482
|
+
});
|
|
1483
|
+
const addName = (names, name) => names.includes(name) ? names : [...names, name];
|
|
1484
|
+
const trackVerdict = (judge, verdict) => {
|
|
1485
|
+
const named = {
|
|
1486
|
+
...judge,
|
|
1487
|
+
refuters: addName(judge.refuters, verdict.property.name)
|
|
1488
|
+
};
|
|
1489
|
+
return verdict.refuted ? named : {
|
|
1490
|
+
...judge,
|
|
1491
|
+
verdicts: [...judge.verdicts, {
|
|
1492
|
+
property: verdict.property,
|
|
1493
|
+
identity: verdict.identity,
|
|
1494
|
+
frozen: verdict.frozen
|
|
1495
|
+
}]
|
|
1496
|
+
};
|
|
1497
|
+
};
|
|
1498
|
+
const recordJudge = (state, subject, verdict) => {
|
|
1499
|
+
const prior = state.judges.get(subject) ?? judgeFor(subject);
|
|
1500
|
+
state.judges.set(subject, trackVerdict(prior, verdict));
|
|
1501
|
+
};
|
|
1502
|
+
const isVacuous = (judge) => judge.refuters.length === 0;
|
|
1503
|
+
const frozenOf = (frozen) => frozen.length === 0 ? { _tag: "NeverCalled" } : {
|
|
1504
|
+
_tag: "Frozen",
|
|
1505
|
+
outputs: frozen.map(({ member, output }) => ({
|
|
1506
|
+
member,
|
|
1507
|
+
output: witnessOf(output)
|
|
1508
|
+
}))
|
|
1509
|
+
};
|
|
1510
|
+
const propertyRunOf$1 = (verdict) => ({
|
|
1511
|
+
property: verdict.property,
|
|
1512
|
+
frozen: frozenOf(verdict.frozen)
|
|
1513
|
+
});
|
|
1514
|
+
const subjectOf = (judge) => ({
|
|
1515
|
+
label: judge.label,
|
|
1516
|
+
properties: judge.verdicts.map(propertyRunOf$1)
|
|
1517
|
+
});
|
|
1518
|
+
const vacuousReplayTextOf = (judges) => plainReplayEntriesText(judges.flatMap((judge) => judge.verdicts.map((verdict) => ({
|
|
1519
|
+
property: verdict.identity,
|
|
1520
|
+
seed: verdict.property.seed,
|
|
1521
|
+
runs: verdict.property.runs
|
|
1522
|
+
}))));
|
|
1523
|
+
const refuseVacuous = (judges, exempt) => {
|
|
1524
|
+
const vacuous = judges.filter(isVacuous);
|
|
1525
|
+
return vacuous.length === 0 ? void 0 : new VacuousProperty({
|
|
1526
|
+
subjects: vacuous.map(subjectOf),
|
|
1527
|
+
exempt: [...exempt],
|
|
1528
|
+
replay: vacuousReplayTextOf(vacuous)
|
|
1529
|
+
});
|
|
1530
|
+
};
|
|
1531
|
+
const finaliseJudges = (state) => refuseVacuous([...state.judges.values()], state.exempts);
|
|
1532
|
+
const ledgerOf = (state) => ({
|
|
1533
|
+
record: (subject, verdict) => recordJudge(state, subject, verdict),
|
|
1534
|
+
recordExempt: (name, kind) => {
|
|
1535
|
+
if (state.exempts.some((exempt) => exempt.name === name)) return;
|
|
1536
|
+
state.exempts.push({
|
|
1537
|
+
name,
|
|
1538
|
+
kind
|
|
1539
|
+
});
|
|
1540
|
+
},
|
|
1541
|
+
finalise: () => finaliseJudges(state)
|
|
1542
|
+
});
|
|
1543
|
+
/**
|
|
1544
|
+
* Collects the impostor verdicts of every property in the file and decides, at file end, which subjects
|
|
1545
|
+
* nothing refuted.
|
|
1546
|
+
*
|
|
1547
|
+
* @internal
|
|
1548
|
+
*/
|
|
1549
|
+
const makeFileLedger = () => ledgerOf({
|
|
1550
|
+
judges: /* @__PURE__ */ new Map(),
|
|
1551
|
+
exempts: []
|
|
1552
|
+
});
|
|
1553
|
+
//#endregion
|
|
1554
|
+
//#region src/internal/property/kinds.ts
|
|
1555
|
+
/**
|
|
1556
|
+
* @internal The R13 law kinds: `model`, `metamorphic`, `roundTrip` and `invariant` are sugar over the
|
|
1557
|
+
* gated property (R11/R12); `idempotent` and `deterministic` are declared exempt — they skip the impostor
|
|
1558
|
+
* gate, are reported as exempt, and are the only exemptions.
|
|
1559
|
+
*
|
|
1560
|
+
* Each kind owns the comparison, so there is nowhere to write `return true`: the author hands over
|
|
1561
|
+
* functions, and the kind relates their outputs with Effect's structural `Equal`.
|
|
1562
|
+
*/
|
|
1563
|
+
/**
|
|
1564
|
+
* The generated values as the argument list a law subject is called with.
|
|
1565
|
+
*
|
|
1566
|
+
* @internal
|
|
1567
|
+
*/
|
|
1568
|
+
const spreadValues = (values) => Array.isArray(values) ? values : refuseRecordOf();
|
|
1569
|
+
const refuseRecordOf = () => {
|
|
1570
|
+
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" });
|
|
1571
|
+
};
|
|
1572
|
+
const call = (subject, values) => subject(...spreadValues(values));
|
|
1573
|
+
const equalOutputs = (original, transformed) => Equal.equals(original, transformed);
|
|
1574
|
+
/** @internal */
|
|
1575
|
+
const modelHolds = (oracle) => (subject, values) => Equal.equals(call(subject, values), call(oracle, values));
|
|
1576
|
+
/** @internal */
|
|
1577
|
+
const metamorphicHolds = (relation) => (subject, values) => (relation.relate ?? equalOutputs)(call(subject, values), call(subject, relation.transform(...spreadValues(values))));
|
|
1578
|
+
/** @internal */
|
|
1579
|
+
const roundTripHolds = (decode) => (subject, values) => Equal.equals(decode(call(subject, values)), values);
|
|
1580
|
+
/** @internal */
|
|
1581
|
+
const invariantHolds = (holds) => (subject, values) => holds(call(subject, values), ...spreadValues(values));
|
|
1582
|
+
const firstOf = (values) => values[0];
|
|
1583
|
+
const sameTwice = (subject, value) => value === void 0 ? false : Equal.equals(subject(subject(value)), subject(value));
|
|
1584
|
+
/** @internal */
|
|
1585
|
+
const idempotentHolds = () => (subject, values) => sameTwice(subject, firstOf(spreadValues(values)));
|
|
1586
|
+
/** @internal */
|
|
1587
|
+
const deterministicHolds = () => (subject, values) => Equal.equals(call(subject, values), call(subject, values));
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/internal/property/seed-store.schema.ts
|
|
1590
|
+
/**
|
|
1591
|
+
* @internal The seed store's line grammar (KTD6, R16, R18): one JSON Lines entry per failing run, decoded as a
|
|
1592
|
+
* tagged union. A refuted run carries the property's full name plus the parts a generator replay token is rebuilt
|
|
1593
|
+
* from; a non-boolean run carries the name, the seed and the run count. The codec a line is read through lives
|
|
1594
|
+
* here; the decisions over an entry live in `seed-record.ts`.
|
|
1595
|
+
*
|
|
1596
|
+
* @since 4.0.0
|
|
1597
|
+
*/
|
|
1598
|
+
/** @internal */
|
|
1599
|
+
const SeedStoreRefuted = Schema$1.TaggedStruct("Refuted", {
|
|
1600
|
+
property: Schema$1.String,
|
|
1601
|
+
seed: PropertySeed,
|
|
1602
|
+
attempt: PropertyRunCount,
|
|
1603
|
+
size: PropertyRunCount,
|
|
1604
|
+
path: Schema$1.Array(ReplayStep),
|
|
1605
|
+
failure: ReplayFailureTag
|
|
1606
|
+
});
|
|
1607
|
+
/** @internal */
|
|
1608
|
+
const SeedStoreNonBoolean = Schema$1.TaggedStruct("NonBoolean", {
|
|
1609
|
+
property: Schema$1.String,
|
|
1610
|
+
seed: PropertySeed,
|
|
1611
|
+
runs: PropertyRunCount
|
|
1612
|
+
});
|
|
1613
|
+
/** @internal */
|
|
1614
|
+
const SeedStoreEntry = Schema$1.Union([SeedStoreRefuted, SeedStoreNonBoolean]);
|
|
1615
|
+
/** @internal */
|
|
1616
|
+
const SeedStoreLine = Schema$1.fromJsonString(SeedStoreEntry);
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/internal/property/seed-record.ts
|
|
1619
|
+
/**
|
|
1620
|
+
* @internal The seed store's pure decisions (KTD6, R16-R19): the codec a line is read through, the entries a
|
|
1621
|
+
* property claims, whether a failure is worth appending, and the replay a stored entry names. No file access
|
|
1622
|
+
* lives here — the shell `seed-store.ts` reads and appends lines, and the line grammar is `seed-store.schema.ts`.
|
|
1623
|
+
*
|
|
1624
|
+
* @since 4.0.0
|
|
1625
|
+
*/
|
|
1626
|
+
const decodeLine = (file, index, text) => Result.mapError(Schema$1.decodeResult(SeedStoreLine)(text), (error) => new SeedStoreUnreadable({
|
|
1627
|
+
file,
|
|
1628
|
+
line: index + 1,
|
|
1629
|
+
detail: error.message
|
|
1630
|
+
}));
|
|
1631
|
+
/** @internal */
|
|
1632
|
+
const decodeStoreLines = Function.dual(2, (file, lines) => Result.all(lines.map((text, index) => decodeLine(file, index, text))));
|
|
1633
|
+
/** @internal */
|
|
1634
|
+
const entriesForProperty = Function.dual(2, (entries, property) => entries.filter((entry) => entry.property === property));
|
|
1635
|
+
const sameEntry = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
1636
|
+
const derandomized = (budget) => budget?.seed !== void 0;
|
|
1637
|
+
const recordingOptedOut = (budget) => budget?.record === false;
|
|
1638
|
+
const alreadyRecorded = (existing, candidate) => existing.some((entry) => sameEntry(entry, candidate));
|
|
1639
|
+
const not = (value) => value === false;
|
|
1640
|
+
/** @internal */
|
|
1641
|
+
const shouldAppendEntry = (input) => [
|
|
1642
|
+
derandomized(input.budget),
|
|
1643
|
+
recordingOptedOut(input.budget),
|
|
1644
|
+
alreadyRecorded(input.existing, input.candidate)
|
|
1645
|
+
].every(not);
|
|
1646
|
+
/** @internal */
|
|
1647
|
+
const refutedEntryOf = (input) => Option$1.flatMap(Schema$1.decodeOption(ReplayToken)(input.token), (parts) => Schema$1.decodeUnknownOption(SeedStoreEntry)({
|
|
1648
|
+
_tag: "Refuted",
|
|
1649
|
+
property: input.property,
|
|
1650
|
+
seed: input.seed,
|
|
1651
|
+
attempt: parts[2],
|
|
1652
|
+
size: parts[3],
|
|
1653
|
+
path: parts[4],
|
|
1654
|
+
failure: parts[5]
|
|
1655
|
+
}));
|
|
1656
|
+
/** @internal */
|
|
1657
|
+
const nonBooleanEntryOf = (input) => Schema$1.decodeOption(SeedStoreEntry)({
|
|
1658
|
+
_tag: "NonBoolean",
|
|
1659
|
+
property: input.property,
|
|
1660
|
+
seed: input.seed,
|
|
1661
|
+
runs: input.runs
|
|
1662
|
+
});
|
|
1663
|
+
/** @internal */
|
|
1664
|
+
const replayEntryOf$1 = Function.dual(2, (entry, hash) => Match$1.value(entry).pipe(Match$1.tag("Refuted", (refuted) => Schema$1.decodeOption(RefutedPropertyReplay)({
|
|
1665
|
+
_tag: "Refuted",
|
|
1666
|
+
property: hash,
|
|
1667
|
+
seed: refuted.seed,
|
|
1668
|
+
runs: 1,
|
|
1669
|
+
attempt: refuted.attempt,
|
|
1670
|
+
size: refuted.size,
|
|
1671
|
+
path: refuted.path,
|
|
1672
|
+
failure: refuted.failure
|
|
1673
|
+
})), Match$1.tag("NonBoolean", (plain) => Schema$1.decodeOption(PlainPropertyReplay)({
|
|
1674
|
+
_tag: "Plain",
|
|
1675
|
+
property: hash,
|
|
1676
|
+
seed: plain.seed,
|
|
1677
|
+
runs: plain.runs
|
|
1678
|
+
})), Match$1.exhaustive));
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/internal/property/seed-store.ts
|
|
1681
|
+
/**
|
|
1682
|
+
* @internal The seed store's shell (KTD6, R16): the asynchronous file access the engine uses — read a test file's
|
|
1683
|
+
* store lines beside it, and append one failing entry's line. The line grammar is `seed-store.schema.ts` and the
|
|
1684
|
+
* decisions are `seed-record.ts`; nothing reads or writes a file synchronously here.
|
|
1685
|
+
*
|
|
1686
|
+
* @since 4.0.0
|
|
1687
|
+
*/
|
|
1688
|
+
const STORE_DIRECTORY = "__property_seeds__";
|
|
1689
|
+
/** @internal */
|
|
1690
|
+
const storeFileOf = (filepath) => Effect$1.map(Effect$1.service(Path.Path), (path) => path.join(path.dirname(filepath), STORE_DIRECTORY, `${path.basename(filepath)}.jsonl`));
|
|
1691
|
+
const linesOf = (text) => text.length === 0 ? [] : text.replace(/\n$/u, "").split("\n");
|
|
1692
|
+
const readText = (file) => Effect$1.flatMap(Effect$1.service(FileSystem.FileSystem), (fs) => fs.readFileString(file).pipe(Effect$1.catchReason("PlatformError", "NotFound", () => Effect$1.succeed(""))));
|
|
1693
|
+
/** @internal */
|
|
1694
|
+
const readStoreLines = (file) => Effect$1.map(readText(file).pipe(Effect$1.orDie), linesOf);
|
|
1695
|
+
const appendLine = (file, line) => Effect$1.flatMap(Effect$1.service(FileSystem.FileSystem), (fs) => Effect$1.flatMap(Effect$1.service(Path.Path), (path) => Effect$1.flatMap(fs.makeDirectory(path.dirname(file), { recursive: true }).pipe(Effect$1.orDie), () => fs.writeFileString(file, `${line}\n`, { flag: "a" }).pipe(Effect$1.orDie))));
|
|
1696
|
+
const appendEntry = (filepath, entry) => Effect$1.flatMap(storeFileOf(filepath), (file) => Effect$1.flatMap(Schema$1.encodeEffect(SeedStoreLine)(entry).pipe(Effect$1.orDie), (line) => appendLine(file, line)));
|
|
1697
|
+
/** @internal */
|
|
1698
|
+
const appendStoreEntry = Function.dual(2, appendEntry);
|
|
1699
|
+
//#endregion
|
|
1700
|
+
//#region src/internal/property/seed.ts
|
|
1701
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
1702
|
+
const FNV_PRIME = 16777619;
|
|
1703
|
+
const utf8 = new TextEncoder();
|
|
1704
|
+
const PART_SEPARATOR = "\0";
|
|
1705
|
+
/** @internal */
|
|
1706
|
+
const fnv1a32 = (text) => utf8.encode(text).reduce((hash, byte) => Math.imul(hash ^ byte, FNV_PRIME), FNV_OFFSET_BASIS) >>> 0;
|
|
1707
|
+
const identityParts = (identity) => [
|
|
1708
|
+
identity.package,
|
|
1709
|
+
identity.file,
|
|
1710
|
+
identity.name
|
|
1711
|
+
];
|
|
1712
|
+
/** @internal */
|
|
1713
|
+
const identitySeed = Function.dual(2, (salt, identity) => fnv1a32([String(salt), ...identityParts(identity)].join(PART_SEPARATOR)));
|
|
1714
|
+
/** @internal */
|
|
1715
|
+
const identityHash = (identity) => fnv1a32(identityParts(identity).join(PART_SEPARATOR));
|
|
1716
|
+
/** @internal */
|
|
1717
|
+
const topUpSeed = Function.dual(2, (seed, drawn) => fnv1a32(`${seed}${PART_SEPARATOR}${drawn}`));
|
|
1718
|
+
const ownSeed = (seed) => Match$1.value(seed).pipe(Match$1.when(Match$1.number, (literal) => literal), Match$1.when(Match$1.string, fnv1a32), Match$1.exhaustive);
|
|
1719
|
+
const providedSeed = (input) => Option$1.map(Option$1.fromNullishOr(input.provided), identitySeed(input.identity));
|
|
1720
|
+
const saltOrProvided = (input) => Option$1.orElse(Option$1.map(Option$1.fromNullishOr(input.own), ownSeed), () => providedSeed(input));
|
|
1721
|
+
/** @internal */
|
|
1722
|
+
const resolveSeed = (input) => Option$1.getOrElse(saltOrProvided(input), () => input.fresh);
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region src/internal/property/engine.ts
|
|
1725
|
+
/**
|
|
1726
|
+
* @internal The lawful property engine (R11-R15): a property names the function under test, takes a budget
|
|
1727
|
+
* that merges its own fields over the configured default over `runs: 100`, accepts only a literal boolean
|
|
1728
|
+
* verdict (or an `Effect` of one), judges coverage classes with QuickCheck's sequential test, and defers the
|
|
1729
|
+
* constant-impostor verdict to the end of the file.
|
|
1730
|
+
*
|
|
1731
|
+
* `of` accepts what upstream accepts — a tuple or record of Schemas or Arbitraries (KTD10) — and `holds`
|
|
1732
|
+
* receives the generated values exactly as drawn, typed by the gens that produced them.
|
|
1733
|
+
*/
|
|
1734
|
+
const REWRITE = "it.prop(name, { of, subject, runs }, holds)";
|
|
1735
|
+
const newViolations = () => ({
|
|
1736
|
+
kinds: [],
|
|
1737
|
+
drawn: void 0,
|
|
1738
|
+
seen: false
|
|
1739
|
+
});
|
|
1740
|
+
const DEFAULT_RUNS = 100;
|
|
1741
|
+
const isPositiveInteger = (runs) => Number.isInteger(runs) && runs > 0;
|
|
1742
|
+
const describeRuns = (runs) => Number.isInteger(runs) ? `${String(runs)}` : "a non-integer";
|
|
1743
|
+
const invalidPropertyBudget = (name, runs) => `${name}: pass a positive integer \`runs\`; received ${describeRuns(runs)}. Write ${REWRITE}.`;
|
|
1744
|
+
const invalidProvidedBudget = (runs) => `the configured property budget (${checkDefaultsKey}) supplies \`runs\`: ${describeRuns(runs)}; it must be a positive integer.`;
|
|
1745
|
+
const invalidPropertySeed = (name, seed) => `${name}: pass a non-negative integer \`seed\`; received ${describeRuns(seed)}. Write ${REWRITE}.`;
|
|
1746
|
+
const isNonInteger = (value) => Number.isInteger(value) === false;
|
|
1747
|
+
const isNegative = (value) => value < 0;
|
|
1748
|
+
const isInvalidSeedNumber = (value) => isNonInteger(value) || isNegative(value);
|
|
1749
|
+
const isInvalidSeed = (seed) => typeof seed === "number" && isInvalidSeedNumber(seed);
|
|
1750
|
+
const requireOwnSeed = (name, seed) => {
|
|
1751
|
+
if (isInvalidSeed(seed)) throw new InvalidBudget({ detail: invalidPropertySeed(name, seed) });
|
|
1752
|
+
};
|
|
1753
|
+
const requirePositiveRuns = (message, runs) => {
|
|
1754
|
+
if (isInvalidRuns(runs)) throw new InvalidBudget({ detail: message(runs) });
|
|
1755
|
+
};
|
|
1756
|
+
const isInvalidRuns = (runs) => runs !== void 0 && isPositiveInteger(runs) === false;
|
|
1757
|
+
/** The property's own explicit fields, a property-level `runs` outranking the same field in `arbitrary`. */
|
|
1758
|
+
const explicitOptions = (spec) => spec.runs === void 0 ? { ...spec.arbitrary } : {
|
|
1759
|
+
...spec.arbitrary,
|
|
1760
|
+
runs: spec.runs
|
|
1761
|
+
};
|
|
1762
|
+
const mergedOptions = (provided, explicit) => ({
|
|
1763
|
+
runs: DEFAULT_RUNS,
|
|
1764
|
+
...provided,
|
|
1765
|
+
...explicit
|
|
1766
|
+
});
|
|
1767
|
+
const resolvedRuns = (options) => options.runs ?? DEFAULT_RUNS;
|
|
1768
|
+
/**
|
|
1769
|
+
* The effective check options: the property's own fields over the configured default over `runs: 100`,
|
|
1770
|
+
* merged field by field so a property that sets only `runs` still inherits the configured size and caps.
|
|
1771
|
+
* The resolved seed replaces the provided and explicit `seed` fields, which are inputs to the derivation
|
|
1772
|
+
* rather than a shared stream (KTD4).
|
|
1773
|
+
*/
|
|
1774
|
+
const EMPTY_PROVIDED_BUDGET = {};
|
|
1775
|
+
const resolveBudget = (name, spec, task, fresh) => {
|
|
1776
|
+
const explicit = explicitOptions(spec);
|
|
1777
|
+
const provided = task.budget ?? EMPTY_PROVIDED_BUDGET;
|
|
1778
|
+
requirePositiveRuns((runs) => invalidPropertyBudget(name, runs), explicit.runs);
|
|
1779
|
+
requirePositiveRuns(invalidProvidedBudget, provided.runs);
|
|
1780
|
+
requireOwnSeed(name, explicit.seed);
|
|
1781
|
+
const options = mergedOptions(task.budget, explicit);
|
|
1782
|
+
const seed = resolveSeed({
|
|
1783
|
+
own: explicit.seed,
|
|
1784
|
+
provided: provided.seed,
|
|
1785
|
+
identity: task.identity,
|
|
1786
|
+
fresh
|
|
1787
|
+
});
|
|
1788
|
+
return {
|
|
1789
|
+
runs: resolvedRuns(options),
|
|
1790
|
+
options: {
|
|
1791
|
+
...options,
|
|
1792
|
+
seed
|
|
1793
|
+
},
|
|
1794
|
+
seed
|
|
1795
|
+
};
|
|
1796
|
+
};
|
|
1797
|
+
const toArbitrary = (input) => Schema$1.isSchema(input) ? Arbitrary.schema(input) : input;
|
|
1798
|
+
const assignArbitrary = (built, of, key) => {
|
|
1799
|
+
const input = of[key];
|
|
1800
|
+
if (input !== void 0) built[key] = toArbitrary(input);
|
|
1801
|
+
};
|
|
1802
|
+
const recordArbitraries = (of) => {
|
|
1803
|
+
const built = {};
|
|
1804
|
+
for (const key of Object.keys(of)) assignArbitrary(built, of, key);
|
|
1805
|
+
return built;
|
|
1806
|
+
};
|
|
1807
|
+
const isTupleOf = (of) => Array.isArray(of);
|
|
1808
|
+
function arbitraryOf(of) {
|
|
1809
|
+
return isTupleOf(of) ? Arbitrary.all(of.map(toArbitrary)) : Arbitrary.all(recordArbitraries(of));
|
|
1810
|
+
}
|
|
1811
|
+
const minimumOf = (entry) => entry[1];
|
|
1812
|
+
const seedClasses = (entries) => {
|
|
1813
|
+
const classes = /* @__PURE__ */ new Map();
|
|
1814
|
+
for (const [label, entry] of entries) classes.set(label, {
|
|
1815
|
+
hits: 0,
|
|
1816
|
+
minimum: minimumOf(entry)
|
|
1817
|
+
});
|
|
1818
|
+
return classes;
|
|
1819
|
+
};
|
|
1820
|
+
const satisfiedLabels = (entries, values) => entries.filter(([, entry]) => entry[0](...spreadValues(values))).map(([label]) => label);
|
|
1821
|
+
const observeRun = (classes, counter, entries, values) => {
|
|
1822
|
+
counter.runs = counter.runs + 1;
|
|
1823
|
+
for (const label of satisfiedLabels(entries, values)) countHit(classes, label);
|
|
1824
|
+
};
|
|
1825
|
+
const drawFor = (entries, arbitrary, seed) => {
|
|
1826
|
+
const drawn = Ref.makeUnsafe(0);
|
|
1827
|
+
return { more: (count) => {
|
|
1828
|
+
const soFar = Effect$1.runSync(Ref.getAndUpdate(drawn, (total) => total + count));
|
|
1829
|
+
return Effect$1.runSync(Arbitrary.sampleEffect(arbitrary, {
|
|
1830
|
+
count,
|
|
1831
|
+
seed: topUpSeed(seed, soFar)
|
|
1832
|
+
}).pipe(Effect$1.orDie)).map((values) => satisfiedLabels(entries, values));
|
|
1833
|
+
} };
|
|
1834
|
+
};
|
|
1835
|
+
const noCoverage = () => ({
|
|
1836
|
+
observe: () => void 0,
|
|
1837
|
+
judge: () => []
|
|
1838
|
+
});
|
|
1839
|
+
const liveCoverage = (cover, arbitrary, seed) => {
|
|
1840
|
+
const entries = Object.entries(cover);
|
|
1841
|
+
const classes = seedClasses(entries);
|
|
1842
|
+
const counter = { runs: 0 };
|
|
1843
|
+
return {
|
|
1844
|
+
observe: (values) => observeRun(classes, counter, entries, values),
|
|
1845
|
+
judge: () => judgeCoverage({
|
|
1846
|
+
classes: Object.fromEntries(classes),
|
|
1847
|
+
runs: counter.runs,
|
|
1848
|
+
draw: drawFor(entries, arbitrary, seed)
|
|
1849
|
+
})
|
|
1850
|
+
};
|
|
1851
|
+
};
|
|
1852
|
+
const makeCoverage = (cover, arbitrary, seed) => cover === void 0 ? noCoverage() : liveCoverage(cover, arbitrary, seed);
|
|
1853
|
+
const verdictKindOf = (verdict) => Match$1.value(verdict).pipe(Match$1.when(Effect$1.isEffect, () => "effect"), Match$1.orElse((value) => typeof value));
|
|
1854
|
+
const noteKind = (violations, kind) => {
|
|
1855
|
+
if (violations.kinds.includes(kind) === false) violations.kinds.push(kind);
|
|
1856
|
+
};
|
|
1857
|
+
const noteDrawn = (violations, values) => {
|
|
1858
|
+
if (violations.seen === false) {
|
|
1859
|
+
violations.seen = true;
|
|
1860
|
+
violations.drawn = values;
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1863
|
+
const noteVerdict = (violations, verdict, values) => {
|
|
1864
|
+
noteKind(violations, verdictKindOf(verdict));
|
|
1865
|
+
noteDrawn(violations, values);
|
|
1866
|
+
return true;
|
|
1867
|
+
};
|
|
1868
|
+
const literalVerdict = (verdict, violations, values) => typeof verdict === "boolean" ? verdict : noteVerdict(violations, verdict, values);
|
|
1869
|
+
const effectVerdict = (lane, verdict, violations, values) => lane === "sync" ? Effect$1.succeed(noteVerdict(violations, verdict, values)) : Effect$1.map(verdict, (value) => literalVerdict(value, violations, values));
|
|
1870
|
+
const verdictFor = (run, values, violations) => {
|
|
1871
|
+
run.observe(values);
|
|
1872
|
+
const verdict = run.holds(run.subject, values);
|
|
1873
|
+
return Effect$1.isEffect(verdict) ? effectVerdict(run.lane, verdict, violations, values) : Effect$1.succeed(literalVerdict(verdict, violations, values));
|
|
1874
|
+
};
|
|
1875
|
+
const tolerateInterruption = (cause) => Cause.hasInterrupts(cause) ? Effect$1.interrupt : Effect$1.fail(cause);
|
|
1876
|
+
const guardedVerdict = (run, violations) => (values) => Effect$1.catchCause(Effect$1.suspend(() => verdictFor(run, values, violations)), tolerateInterruption);
|
|
1877
|
+
const checkWith = (run, violations, options) => Arbitrary.checkEffect(run.arbitrary, guardedVerdict(run, violations), options);
|
|
1878
|
+
const isFalsified = Predicate.isTagged("Falsified");
|
|
1879
|
+
const isReplayMismatch = Predicate.isTagged("ReplayMismatch");
|
|
1880
|
+
/**
|
|
1881
|
+
* A replayed draw whose recorded shrink path no longer replays (`ReplayMismatch`) is re-checked at its root under
|
|
1882
|
+
* each failure class, so a root that still falsifies the property refutes it with the root as its counterexample.
|
|
1883
|
+
* The classes are tried in order and the check stops at the first that falsifies; when none does, the mismatch
|
|
1884
|
+
* stands: the recorded failure no longer reproduces.
|
|
1885
|
+
*/
|
|
1886
|
+
const rootCheckOf = (run, mismatch) => rootTokensOf(run.options.replay).reduce((previous, replay) => Effect$1.filterOrElse(previous, (found) => isFalsified(found), () => checkWith({
|
|
1887
|
+
...run,
|
|
1888
|
+
observe: noObserve
|
|
1889
|
+
}, newViolations(), {
|
|
1890
|
+
...run.options,
|
|
1891
|
+
replay
|
|
1892
|
+
})), Effect$1.succeed(mismatch));
|
|
1893
|
+
const runCheck = (run) => {
|
|
1894
|
+
const violations = newViolations();
|
|
1895
|
+
return checkWith(run, violations, run.options).pipe(Effect$1.filterOrElse((result) => !isReplayMismatch(result), (mismatch) => rootCheckOf(run, mismatch)), Effect$1.map((result) => ({
|
|
1896
|
+
violations,
|
|
1897
|
+
result
|
|
1898
|
+
})));
|
|
1899
|
+
};
|
|
1900
|
+
const reportOf = (checked) => isReplayMismatch(checked.result) ? void 0 : Arbitrary.formatCheckFailure(checked.result);
|
|
1901
|
+
const seeded = (seed) => Option$1.getOrThrow(Schema$1.decodeOption(PropertySeed)(seed));
|
|
1902
|
+
const runCounted = (runs) => Option$1.getOrThrow(Schema$1.decodeOption(PropertyRunCount)(runs));
|
|
1903
|
+
const propertyRunOf = (run, budget) => ({
|
|
1904
|
+
name: run.name,
|
|
1905
|
+
site: run.site ?? null,
|
|
1906
|
+
seed: seeded(budget.seed),
|
|
1907
|
+
runs: runCounted(budget.runs)
|
|
1908
|
+
});
|
|
1909
|
+
const REPLAY_VARIABLE = "CONFORMANCE_REPLAY";
|
|
1910
|
+
const replayEntryOf = (text, hash, site) => Option$1.match(text, {
|
|
1911
|
+
onNone: () => Effect$1.succeed(Option$1.none()),
|
|
1912
|
+
onSome: (value) => Option$1.match(Schema$1.decodeOption(ReplayChannelFromText)(value), {
|
|
1913
|
+
onNone: () => dieWithSite(new ReplayUnreadable({ text: value }), site),
|
|
1914
|
+
onSome: (channel) => Effect$1.succeed(Option$1.fromNullishOr(selectReplayEntry({
|
|
1915
|
+
channel,
|
|
1916
|
+
hash
|
|
1917
|
+
})))
|
|
1918
|
+
})
|
|
1919
|
+
});
|
|
1920
|
+
const replayedBudget = (budget, runs, seed, replay) => {
|
|
1921
|
+
return {
|
|
1922
|
+
runs,
|
|
1923
|
+
seed,
|
|
1924
|
+
options: replay === void 0 ? {
|
|
1925
|
+
...budget.options,
|
|
1926
|
+
runs,
|
|
1927
|
+
seed
|
|
1928
|
+
} : {
|
|
1929
|
+
...budget.options,
|
|
1930
|
+
runs,
|
|
1931
|
+
seed,
|
|
1932
|
+
replay
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
};
|
|
1936
|
+
const applyReplayEntry = (budget, entry) => entry === void 0 ? budget : Match$1.value(entry).pipe(Match$1.tag("Refuted", (refuted) => replayedBudget(budget, refuted.runs, refuted.seed, tokenOfReplay(refuted))), Match$1.tag("Plain", (plain) => replayedBudget(budget, plain.runs, plain.seed, void 0)), Match$1.exhaustive);
|
|
1937
|
+
const plainFailureReplay = (run, budget) => plainReplayTextOf({
|
|
1938
|
+
property: run.identityHash,
|
|
1939
|
+
seed: budget.seed,
|
|
1940
|
+
runs: budget.runs
|
|
1941
|
+
});
|
|
1942
|
+
const falsifiedReplayOf = (checked) => {
|
|
1943
|
+
const falsified = falsifiedOf(checked.result);
|
|
1944
|
+
return falsified === void 0 ? void 0 : falsified.replay;
|
|
1945
|
+
};
|
|
1946
|
+
const refutedReplayText = (run, budget, token) => Option$1.getOrElse(Option$1.fromUndefinedOr(refutedReplayTextOf({
|
|
1947
|
+
property: run.identityHash,
|
|
1948
|
+
seed: budget.seed,
|
|
1949
|
+
runs: budget.runs,
|
|
1950
|
+
token
|
|
1951
|
+
})), () => plainFailureReplay(run, budget));
|
|
1952
|
+
const refutedFailureReplay = (run, budget, checked) => {
|
|
1953
|
+
const token = falsifiedReplayOf(checked);
|
|
1954
|
+
return token === void 0 ? plainFailureReplay(run, budget) : refutedReplayText(run, budget, token);
|
|
1955
|
+
};
|
|
1956
|
+
const violationOf = (run, budget, checked) => checked.violations.kinds.length === 0 ? void 0 : new NonBooleanVerdict({
|
|
1957
|
+
property: propertyRunOf(run, budget),
|
|
1958
|
+
drawn: witnessOf(checked.violations.drawn),
|
|
1959
|
+
returned: [...checked.violations.kinds],
|
|
1960
|
+
replay: plainFailureReplay(run, budget)
|
|
1961
|
+
});
|
|
1962
|
+
const isRefuted = (checked) => checked.violations.kinds.length > 0 || reportOf(checked) !== void 0;
|
|
1963
|
+
const dieWithSite = (error, site) => Effect$1.die(withRaisingFrame(error, site));
|
|
1964
|
+
const shrinkCounted = (shrinks) => Option$1.getOrThrow(Schema$1.decodeOption(PropertyShrinkCount)(shrinks));
|
|
1965
|
+
const countOrNull = (value) => Option$1.getOrNull(Option$1.flatMap(Option$1.fromUndefinedOr(value), (count) => Schema$1.decodeOption(PropertyRunCount)(count)));
|
|
1966
|
+
const refutedOf = (property, falsified, replay) => new PropertyRefuted({
|
|
1967
|
+
property,
|
|
1968
|
+
counterexample: witnessOf(falsified.shrunkInput),
|
|
1969
|
+
shrinks: shrinkCounted(falsified.shrinks),
|
|
1970
|
+
replay
|
|
1971
|
+
});
|
|
1972
|
+
const dieRefuted = (run, budget, checked, falsified, site) => dieWithSite(refutedOf(propertyRunOf(run, budget), falsified, refutedFailureReplay(run, budget, checked)), site);
|
|
1973
|
+
const dieExhausted = (run, budget, exhausted, site) => dieWithSite(new PropertyExhausted({
|
|
1974
|
+
property: propertyRunOf(run, budget),
|
|
1975
|
+
discards: runCounted(exhausted.discards),
|
|
1976
|
+
budget: {
|
|
1977
|
+
runs: runCounted(budget.runs),
|
|
1978
|
+
maxDiscards: countOrNull(budget.options.maxDiscards)
|
|
1979
|
+
},
|
|
1980
|
+
replay: plainFailureReplay(run, budget)
|
|
1981
|
+
}), site);
|
|
1982
|
+
const dieUncovered = (run, budget, classes, site) => dieWithSite(new CoverageBelowMinimum({
|
|
1983
|
+
property: propertyRunOf(run, budget),
|
|
1984
|
+
classes,
|
|
1985
|
+
replay: plainFailureReplay(run, budget)
|
|
1986
|
+
}), site);
|
|
1987
|
+
const impostorHolds = (holds, impostor) => (_subject, values) => holds(impostor.impostor, values);
|
|
1988
|
+
const noObserve = () => void 0;
|
|
1989
|
+
const impostorRun = (run, propertyBudget, budgets) => {
|
|
1990
|
+
const impostor = impostorOf(run.subject);
|
|
1991
|
+
const retry = {
|
|
1992
|
+
...run,
|
|
1993
|
+
holds: impostorHolds(run.holds, impostor),
|
|
1994
|
+
observe: noObserve
|
|
1995
|
+
};
|
|
1996
|
+
return Effect$1.map(Effect$1.forEach(budgets, (budget) => runCheck({
|
|
1997
|
+
...retry,
|
|
1998
|
+
options: budget.options,
|
|
1999
|
+
seed: budget.seed
|
|
2000
|
+
}), { concurrency: 1 }), (checked) => {
|
|
2001
|
+
const verdict = {
|
|
2002
|
+
refuted: checked.some(isRefuted),
|
|
2003
|
+
property: propertyRunOf(retry, propertyBudget),
|
|
2004
|
+
identity: run.identityHash,
|
|
2005
|
+
frozen: impostor.frozen()
|
|
2006
|
+
};
|
|
2007
|
+
run.ledger.record(run.subject, verdict);
|
|
2008
|
+
});
|
|
2009
|
+
};
|
|
2010
|
+
const gateRun = (run, budget, gate, budgets) => gate ? impostorRun(run, budget, budgets) : Effect$1.void;
|
|
2011
|
+
const finishPassed = (run, budget, coverage, gate, budgets) => {
|
|
2012
|
+
const failures = coverage.judge();
|
|
2013
|
+
return failures.length === 0 ? gateRun(run, budget, gate, budgets) : dieUncovered(run, budget, failures, run.site);
|
|
2014
|
+
};
|
|
2015
|
+
const falsifiedOf = (result) => "replay" in result ? result : void 0;
|
|
2016
|
+
const noStore = (name) => ({
|
|
2017
|
+
filepath: null,
|
|
2018
|
+
name,
|
|
2019
|
+
existing: [],
|
|
2020
|
+
recorded: [],
|
|
2021
|
+
budget: void 0
|
|
2022
|
+
});
|
|
2023
|
+
const appendWhenRecorded = (store, candidate) => Option$1.match(Option$1.fromNullishOr(store.filepath), {
|
|
2024
|
+
onNone: () => Effect$1.void,
|
|
2025
|
+
onSome: (filepath) => shouldAppendEntry({
|
|
2026
|
+
existing: store.existing,
|
|
2027
|
+
candidate,
|
|
2028
|
+
budget: store.budget
|
|
2029
|
+
}) ? appendStoreEntry(filepath, candidate) : Effect$1.void
|
|
2030
|
+
});
|
|
2031
|
+
const appendThenDie = (store, candidate, die) => Effect$1.flatMap(Option$1.match(candidate, {
|
|
2032
|
+
onNone: () => Effect$1.void,
|
|
2033
|
+
onSome: (entry) => appendWhenRecorded(store, entry)
|
|
2034
|
+
}), () => die);
|
|
2035
|
+
const refutedCandidate = (store, budget, checked) => Option$1.flatMap(Option$1.fromNullishOr(falsifiedReplayOf(checked)), (token) => refutedEntryOf({
|
|
2036
|
+
property: store.name,
|
|
2037
|
+
seed: budget.seed,
|
|
2038
|
+
token
|
|
2039
|
+
}));
|
|
2040
|
+
const recordedReport = (registration, run, budget, checked) => Match$1.value(checked.result).pipe(Match$1.tag("Falsified", (falsified) => dieRefuted(run, budget, checked, falsified, registration.site)), Match$1.tag("Exhausted", (exhausted) => dieExhausted(run, budget, exhausted, registration.site)), Match$1.orElse(() => Effect$1.void));
|
|
2041
|
+
const settleRecorded = (registration, run, budget, checked) => Option$1.match(Option$1.fromNullishOr(violationOf(run, budget, checked)), {
|
|
2042
|
+
onNone: () => recordedReport(registration, run, budget, checked),
|
|
2043
|
+
onSome: (violation) => dieWithSite(violation, registration.site)
|
|
2044
|
+
});
|
|
2045
|
+
const settleRefuted = (registration, run, budget, coverage, checked, store) => Match$1.value(checked.result).pipe(Match$1.tag("Falsified", (falsified) => appendThenDie(store, refutedCandidate(store, budget, checked), dieRefuted(run, budget, checked, falsified, registration.site))), Match$1.tag("Exhausted", (exhausted) => appendThenDie(store, Option$1.none(), dieExhausted(run, budget, exhausted, registration.site))), Match$1.orElse(() => finishPassed(run, budget, coverage, registration.gate, [...store.recorded, budget])));
|
|
2046
|
+
const settle = (registration, run, budget, coverage, checked, store) => Option$1.match(Option$1.fromNullishOr(violationOf(run, budget, checked)), {
|
|
2047
|
+
onNone: () => settleRefuted(registration, run, budget, coverage, checked, store),
|
|
2048
|
+
onSome: (violation) => appendThenDie(store, nonBooleanEntryOf({
|
|
2049
|
+
property: store.name,
|
|
2050
|
+
seed: budget.seed,
|
|
2051
|
+
runs: budget.runs
|
|
2052
|
+
}), dieWithSite(violation, registration.site))
|
|
2053
|
+
});
|
|
2054
|
+
const readStoreEntries = (file, site) => Effect$1.flatMap(readStoreLines(file), (lines) => {
|
|
2055
|
+
const decoded = decodeStoreLines(file, lines);
|
|
2056
|
+
return Result.isSuccess(decoded) ? Effect$1.succeed(decoded.success) : dieWithSite(decoded.failure, site);
|
|
2057
|
+
});
|
|
2058
|
+
const readTaskStore = (task, site) => Option$1.match(Option$1.fromNullishOr(task.filepath), {
|
|
2059
|
+
onNone: () => Effect$1.succeed([]),
|
|
2060
|
+
onSome: (testFile) => Effect$1.flatMap(storeFileOf(testFile), (file) => readStoreEntries(file, site))
|
|
2061
|
+
});
|
|
2062
|
+
const recordedBudgets = (entries, baseBudget, hash) => entries.flatMap((stored) => Option$1.match(replayEntryOf$1(stored, hash), {
|
|
2063
|
+
onNone: () => [],
|
|
2064
|
+
onSome: (entry) => [applyReplayEntry(baseBudget, entry)]
|
|
2065
|
+
}));
|
|
2066
|
+
const checkOf = (registration, arbitrary, budget, observe, identityHash) => ({
|
|
2067
|
+
name: registration.name,
|
|
2068
|
+
lane: registration.lane,
|
|
2069
|
+
subject: registration.spec.subject,
|
|
2070
|
+
holds: registration.holds,
|
|
2071
|
+
arbitrary,
|
|
2072
|
+
observe,
|
|
2073
|
+
options: budget.options,
|
|
2074
|
+
seed: budget.seed,
|
|
2075
|
+
identityHash,
|
|
2076
|
+
site: registration.site,
|
|
2077
|
+
ledger: registration.runtime.ledger
|
|
2078
|
+
});
|
|
2079
|
+
const runRecordedChecks = (registration, arbitrary, hash, recorded) => Effect$1.forEach(recorded, (budget) => {
|
|
2080
|
+
const run = checkOf(registration, arbitrary, budget, noObserve, hash);
|
|
2081
|
+
return Effect$1.flatMap(runCheck(run), (checked) => settleRecorded(registration, run, budget, checked));
|
|
2082
|
+
}, {
|
|
2083
|
+
concurrency: 1,
|
|
2084
|
+
discard: true
|
|
2085
|
+
});
|
|
2086
|
+
const replayRun = (registration, baseBudget, arbitrary, hash, entry, text) => {
|
|
2087
|
+
const budget = applyReplayEntry(baseBudget, entry);
|
|
2088
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary, budget.seed);
|
|
2089
|
+
const run = checkOf(registration, arbitrary, budget, coverage.observe, hash);
|
|
2090
|
+
return Effect$1.flatMap(runCheck(run), (checked) => isReplayMismatch(checked.result) ? dieWithSite(new ReplayNoLongerReproduces({
|
|
2091
|
+
property: propertyRunOf(run, budget),
|
|
2092
|
+
replay: text
|
|
2093
|
+
}), registration.site) : settle(registration, run, budget, coverage, checked, noStore(registration.name)));
|
|
2094
|
+
};
|
|
2095
|
+
const storeRun = (registration, task, baseBudget, arbitrary, hash) => Effect$1.gen(function* () {
|
|
2096
|
+
const entries = yield* readTaskStore(task, registration.site);
|
|
2097
|
+
const existing = entriesForProperty(entries, task.identity.name);
|
|
2098
|
+
const recorded = recordedBudgets(existing, baseBudget, hash);
|
|
2099
|
+
yield* runRecordedChecks(registration, arbitrary, hash, recorded);
|
|
2100
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary, baseBudget.seed);
|
|
2101
|
+
const run = checkOf(registration, arbitrary, baseBudget, coverage.observe, hash);
|
|
2102
|
+
const checked = yield* runCheck(run);
|
|
2103
|
+
yield* settle(registration, run, baseBudget, coverage, checked, {
|
|
2104
|
+
filepath: task.filepath,
|
|
2105
|
+
name: task.identity.name,
|
|
2106
|
+
existing,
|
|
2107
|
+
recorded,
|
|
2108
|
+
budget: task.budget
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
2111
|
+
const propertyPlatform = Layer.unwrap(Effect$1.promise(async () => {
|
|
2112
|
+
const [fileSystem, path] = await Promise.all([import("@effect/platform-node/NodeFileSystem"), import("@effect/platform-node/NodePath")]);
|
|
2113
|
+
return Layer.merge(fileSystem.layer, path.layer);
|
|
2114
|
+
}));
|
|
2115
|
+
const program = (registration, task) => Effect$1.provide(Effect$1.gen(function* () {
|
|
2116
|
+
const fresh = (yield* Random.nextInt) >>> 0;
|
|
2117
|
+
const baseBudget = resolveBudget(registration.name, registration.spec, task, fresh);
|
|
2118
|
+
const hash = identityHash(task.identity);
|
|
2119
|
+
const replayText = yield* Config.option(Config.String(REPLAY_VARIABLE)).pipe(Effect$1.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv()), Effect$1.orDie);
|
|
2120
|
+
const entry = Option$1.getOrUndefined(yield* replayEntryOf(replayText, hash, registration.site));
|
|
2121
|
+
const arbitrary = arbitraryOf(registration.spec.of);
|
|
2122
|
+
yield* Option$1.match(Option$1.fromNullishOr(entry), {
|
|
2123
|
+
onNone: () => storeRun(registration, task, baseBudget, arbitrary, hash),
|
|
2124
|
+
onSome: (replay) => replayRun(registration, baseBudget, arbitrary, hash, replay, Option$1.getOrThrow(replayText))
|
|
2125
|
+
});
|
|
2126
|
+
}), propertyPlatform);
|
|
2127
|
+
const programOf = (registration) => (task) => registration.runtime.provide(program(registration, task));
|
|
2128
|
+
const isSelfModel = (subject, oracle) => Object.is(subject, oracle);
|
|
2129
|
+
/**
|
|
2130
|
+
* The property surface over one runtime: `prop`, `effectProp`, and the R13 law kinds routed through the gate.
|
|
2131
|
+
*
|
|
2132
|
+
* @internal
|
|
2133
|
+
*/
|
|
2134
|
+
const makeProperty = (runtime) => {
|
|
2135
|
+
const body = (name, spec, holds, lane, gate) => {
|
|
2136
|
+
const registration = {
|
|
2137
|
+
runtime,
|
|
2138
|
+
name,
|
|
2139
|
+
spec,
|
|
2140
|
+
holds,
|
|
2141
|
+
lane,
|
|
2142
|
+
gate,
|
|
2143
|
+
site: callFrame()
|
|
2144
|
+
};
|
|
2145
|
+
runtime.register(name, programOf(registration));
|
|
2146
|
+
};
|
|
2147
|
+
const refuseSelfModel = (name) => {
|
|
2148
|
+
const site = callFrame();
|
|
2149
|
+
runtime.register(name, (_task) => dieWithSite(new SelfModelLaw({
|
|
2150
|
+
law: name,
|
|
2151
|
+
site: site ?? null
|
|
2152
|
+
}), site));
|
|
2153
|
+
};
|
|
2154
|
+
const judged = (name, spec, holds) => body(name, spec, holds, "sync", true);
|
|
2155
|
+
const exempted = (name, spec, holds, kind) => {
|
|
2156
|
+
runtime.ledger.recordExempt(name, kind);
|
|
2157
|
+
body(`${name} [exempt: ${kind}]`, spec, holds, "sync", false);
|
|
2158
|
+
};
|
|
2159
|
+
const model = (name, spec, oracle) => isSelfModel(spec.subject, oracle) ? refuseSelfModel(name) : judged(name, spec, modelHolds(oracle));
|
|
2160
|
+
const idempotent = (name, spec) => exempted(name, spec, idempotentHolds(), "idempotent");
|
|
2161
|
+
const deterministic = (name, spec) => exempted(name, spec, deterministicHolds(), "deterministic");
|
|
2162
|
+
return {
|
|
2163
|
+
prop: (name, spec, holds) => body(name, spec, holds, "sync", true),
|
|
2164
|
+
effectProp: (name, spec, holds) => body(name, spec, holds, "effect", true),
|
|
2165
|
+
law: {
|
|
2166
|
+
model,
|
|
2167
|
+
metamorphic: (name, spec, relation) => judged(name, spec, metamorphicHolds(relation)),
|
|
2168
|
+
roundTrip: (name, spec, decode) => judged(name, spec, roundTripHolds(decode)),
|
|
2169
|
+
invariant: (name, spec, holds) => judged(name, spec, invariantHolds(holds)),
|
|
2170
|
+
idempotent,
|
|
2171
|
+
deterministic
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
};
|
|
2175
|
+
//#endregion
|
|
2176
|
+
//#region src/internal/span-recorder.ts
|
|
2177
|
+
/**
|
|
2178
|
+
* The run recorder: an in-memory {@link Tracer.Tracer} that keeps every span one run opened (KTD2). Nothing is
|
|
2179
|
+
* exported from a recorder — its spans are read in process by the failure renderer and dropped with the run.
|
|
2180
|
+
*
|
|
2181
|
+
* A recorder is per run: the kernel's baseline, each seeded run and each replay each get their own, and so does
|
|
2182
|
+
* every program the runner drives. Installing it is the caller's job, through `Effect.withTracer`.
|
|
2183
|
+
*
|
|
2184
|
+
* @since 4.0.0
|
|
2185
|
+
*/
|
|
2186
|
+
/** @internal */
|
|
2187
|
+
const createSpanRecorder = () => {
|
|
2188
|
+
const spans = [];
|
|
2189
|
+
return {
|
|
2190
|
+
tracer: Tracer.make({ span(options) {
|
|
2191
|
+
const span = new Tracer.NativeSpan(options);
|
|
2192
|
+
spans.push(span);
|
|
2193
|
+
return span;
|
|
2194
|
+
} }),
|
|
2195
|
+
spans
|
|
2196
|
+
};
|
|
2197
|
+
};
|
|
2198
|
+
//#endregion
|
|
2199
|
+
//#region src/internal/recorded-run.ts
|
|
2200
|
+
/**
|
|
2201
|
+
* The in-process way to run a program the way the runner runs a lane and read back the record its failure rendered
|
|
2202
|
+
* (R11, KTD10): a corpus drives a fixture through this instead of spawning Vitest, and without importing `src/`.
|
|
2203
|
+
*
|
|
2204
|
+
* The run gets its own check ledger — its own `expect` — so a corpus fixture's `Then` asserts on the check it
|
|
2205
|
+
* answered with, not on the check the corpus test itself already spent, and its own recorder, so the record is
|
|
2206
|
+
* rendered from exactly the spans the run opened.
|
|
2207
|
+
*
|
|
2208
|
+
* @since 4.0.0
|
|
2209
|
+
*/
|
|
2210
|
+
const renderRecord = (failure, spans, identity) => renderFailureRecord({
|
|
2211
|
+
failure,
|
|
2212
|
+
spans,
|
|
2213
|
+
identity,
|
|
2214
|
+
replay: void 0,
|
|
2215
|
+
root: providedRoot()
|
|
2216
|
+
});
|
|
2217
|
+
const outcomeOf = (exit, recorder, identity) => Exit.isSuccess(exit) ? void 0 : renderRecord(Cause.squash(exit.cause), recorder.spans, identity);
|
|
2218
|
+
const provideRun = (effect, ledger, ctx) => effect.pipe(Effect$1.provideService(Asserted, ledger.asserted), Effect$1.provideService(VitestTestContext, ctx));
|
|
2219
|
+
const runRecorded = (ctx, program) => {
|
|
2220
|
+
const ledger = makeLedger(ctx);
|
|
2221
|
+
const recorder = createSpanRecorder();
|
|
2222
|
+
return Effect$1.runPromiseExit(provideRun(Effect$1.tap(program(checksFor(ledger)), () => refusingUnyielded(ledger)), ledger, ctx).pipe(Effect$1.withTracer(recorder.tracer))).then((exit) => outcomeOf(exit, recorder, testIdentityOf()));
|
|
2223
|
+
};
|
|
2224
|
+
/**
|
|
2225
|
+
* Runs one program as the runner runs a lane, with a fresh ledger and its own recorder, and resolves with the
|
|
2226
|
+
* record its failure rendered — or `undefined` when it passed.
|
|
2227
|
+
*
|
|
2228
|
+
* @internal
|
|
2229
|
+
*/
|
|
2230
|
+
const recordOfRun = (program) => {
|
|
2231
|
+
const task = TestRunner.getCurrentTest();
|
|
2232
|
+
if (task === void 0) return Promise.resolve(void 0);
|
|
2233
|
+
return runRecorded(task.context, program);
|
|
2234
|
+
};
|
|
2235
|
+
const capturingRuntime = (captured, ledger) => ({
|
|
2236
|
+
register: (name, program) => {
|
|
2237
|
+
captured.push({
|
|
2238
|
+
name,
|
|
2239
|
+
program
|
|
2240
|
+
});
|
|
2241
|
+
},
|
|
2242
|
+
provide: (effect) => effect,
|
|
2243
|
+
ledger
|
|
2244
|
+
});
|
|
2245
|
+
const taskOfRecorded = (name, budget, store) => {
|
|
2246
|
+
const current = TestRunner.getCurrentTest();
|
|
2247
|
+
return {
|
|
2248
|
+
identity: {
|
|
2249
|
+
...testIdentityOf(current),
|
|
2250
|
+
name
|
|
2251
|
+
},
|
|
2252
|
+
filepath: Option$1.getOrNull(Option$1.fromNullishOr(store)),
|
|
2253
|
+
budget: budget ?? providedCheckDefaults()
|
|
2254
|
+
};
|
|
2255
|
+
};
|
|
2256
|
+
/** Runs every captured program in registration order, each as `recordOfProperty` runs one. */
|
|
2257
|
+
const runCaptured = (captured, budget, store) => captured.reduce((previous, entry) => previous.then((records) => {
|
|
2258
|
+
const task = taskOfRecorded(entry.name, budget, store);
|
|
2259
|
+
return recordOfRun(() => entry.program(task)).then((record) => [...records, record]);
|
|
2260
|
+
}), Promise.resolve([]));
|
|
2261
|
+
/** Registers a whole file's properties on a capturing runtime with a fresh ledger, and runs them in order. */
|
|
2262
|
+
const capturedRun = (register, budget, store) => {
|
|
2263
|
+
const captured = [];
|
|
2264
|
+
const ledger = makeFileLedger();
|
|
2265
|
+
register(makeProperty(capturingRuntime(captured, ledger)));
|
|
2266
|
+
return runCaptured(captured, budget, store).then((records) => ({
|
|
2267
|
+
records,
|
|
2268
|
+
ledger
|
|
2269
|
+
}));
|
|
2270
|
+
};
|
|
2271
|
+
/**
|
|
2272
|
+
* Runs one property the way the runner runs `it.prop`, with a fresh ledger and its own recorder, and resolves with
|
|
2273
|
+
* the record its falsification rendered — or `undefined` when it held.
|
|
2274
|
+
*
|
|
2275
|
+
* @internal
|
|
2276
|
+
*/
|
|
2277
|
+
const recordOfProperty = (input) => capturedRun((api) => api.prop(input.name, input.spec, input.holds), input.budget, input.store).then(({ records }) => records[0]);
|
|
2278
|
+
const EMPTY_FILE_OPTIONS = {};
|
|
2279
|
+
/**
|
|
2280
|
+
* Runs every property and law a file registers, in registration order, then finalises the file's one ledger:
|
|
2281
|
+
* resolves with each property's rendered failure and the file's single vacuous verdict, or `undefined` when the
|
|
2282
|
+
* file holds. The name a property is recorded under is the name it was registered with.
|
|
2283
|
+
*
|
|
2284
|
+
* @internal
|
|
2285
|
+
*/
|
|
2286
|
+
const recordOfFile = Function.dual((args) => typeof args[0] === "function", (register, options) => {
|
|
2287
|
+
const resolved = options ?? EMPTY_FILE_OPTIONS;
|
|
2288
|
+
return capturedRun(register, resolved.budget, resolved.store).then(({ records, ledger }) => ({
|
|
2289
|
+
records,
|
|
2290
|
+
vacuous: ledger.finalise()
|
|
2291
|
+
}));
|
|
2292
|
+
});
|
|
2293
|
+
//#endregion
|
|
2294
|
+
export { providedCheckDefaults as A, providedRoot as C, Replay as D, Slop as E, ReplayFromText as O, witnessOf as S, LeakedState as T, isFailureRecordError as _, makeProperty as a, testIdentityOf as b, NonBooleanVerdict as c, ReplayNoLongerReproduces as d, ReplayUnreadable as f, drive as g, VacuousProperty as h, createSpanRecorder as i, replayOfText as k, PropertyExhausted as l, SelfModelLaw as m, recordOfProperty as n, makeFileLedger as o, SeedStoreUnreadable as p, recordOfRun as r, CoverageBelowMinimum as s, recordOfFile as t, PropertyRefuted as u, renderFailureRecord as v, InvalidBudget as w, throwFailureRecord as x, summaryOf as y };
|