@systemfsoftware/vitest 0.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +66 -4
- package/dist/engine-D7Q6-BxD.d.ts +503 -0
- package/dist/{failure-DC_SJoxW.mjs → failure-nvq5TwWX.mjs} +1075 -216
- package/dist/failure.d.ts +28 -115
- package/dist/failure.mjs +2 -2
- package/dist/index.d.ts +3 -10
- package/dist/index.mjs +19 -6
- package/package.json +7 -4
- package/dist/engine-ZbxyPNiN.d.ts +0 -166
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
|
|
2
2
|
import { 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-D_-acRSc.mjs";
|
|
3
|
-
import * as V from "vitest";
|
|
4
3
|
import { TestRunner, inject } from "vitest";
|
|
5
4
|
import * as Cause from "effect/Cause";
|
|
6
5
|
import * as Effect$1 from "effect/Effect";
|
|
7
6
|
import * as Equal from "effect/Equal";
|
|
8
7
|
import * as Exit from "effect/Exit";
|
|
9
8
|
import * as Function from "effect/Function";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
10
10
|
import * as Schema$1 from "effect/Schema";
|
|
11
11
|
import * as Option$1 from "effect/Option";
|
|
12
|
-
import * as Match from "effect/Match";
|
|
12
|
+
import * as Match$1 from "effect/Match";
|
|
13
13
|
import * as Tracer from "effect/Tracer";
|
|
14
|
-
import { Effect, Option, Schema, SchemaGetter, SchemaIssue } from "effect";
|
|
15
|
-
import
|
|
16
|
-
import
|
|
14
|
+
import { Effect, Match, Option, Schema, SchemaGetter, SchemaIssue } from "effect";
|
|
15
|
+
import { layer } from "@effect/platform-node/NodeFileSystem";
|
|
16
|
+
import { layer as layer$1 } from "@effect/platform-node/NodePath";
|
|
17
|
+
import * as Arbitrary from "effect/Arbitrary";
|
|
18
|
+
import * as Config from "effect/Config";
|
|
19
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
20
|
+
import * as Random from "effect/Random";
|
|
21
|
+
import * as Ref from "effect/Ref";
|
|
22
|
+
import * as Result from "effect/Result";
|
|
23
|
+
import * as FileSystem from "effect/FileSystem";
|
|
24
|
+
import * as Path from "effect/Path";
|
|
17
25
|
//#region src/internal/property/defaults.ts
|
|
18
26
|
/** @internal The `test.provide` key the shared config fills with this run's tier's check options. */
|
|
19
27
|
const checkDefaultsKey = "@systemfsoftware/vitest:property-check";
|
|
@@ -22,13 +30,19 @@ const providedCheckDefaults = () => inject(checkDefaultsKey);
|
|
|
22
30
|
//#endregion
|
|
23
31
|
//#region src/replay.schema.ts
|
|
24
32
|
/**
|
|
25
|
-
* The one replay grammar (KTD10, R6): the `CONFORMANCE_REPLAY` text a failure record's rerun
|
|
26
|
-
* the
|
|
27
|
-
* decisions it took, in order — `seed=7;path=1,2,3` — and the path is empty when the run took no decision
|
|
28
|
-
* (`seed=1;path=`). A text that names neither is not a replay.
|
|
33
|
+
* The one replay grammar (KTD10, KTD5, KD8, R6, R9-R12): the `CONFORMANCE_REPLAY` text a failure record's rerun
|
|
34
|
+
* line carries and the engine reads back to re-run the same verdict. Two forms share it:
|
|
29
35
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
36
|
+
* - the kernel's schedule, `seed=<whole number>;path=<comma-separated whole numbers>` — the path is empty when the
|
|
37
|
+
* run took no decision (`seed=1;path=`);
|
|
38
|
+
* - a property entry, `property=<identity hash>;seed=<whole number>;runs=<whole number>` for a run that was not
|
|
39
|
+
* refuted, and the same entry plus `;attempt=<n>;size=<n>;path=<comma-separated decisions>;failure=<tag>` for a
|
|
40
|
+
* refuted run, so the engine rebuilds the generator's replay token. Several entries are joined by `|`, which no
|
|
41
|
+
* entry can contain: every key is lowercase letters and every value is digits, commas or a failure tag.
|
|
42
|
+
*
|
|
43
|
+
* A text that names neither form is not a replay. The seed and every decision are branded whole numbers
|
|
44
|
+
* (CONST-D3), and the text is a codec rooted at `Schema.String`, so one declaration serves both the writer and the
|
|
45
|
+
* reader instead of a pattern each.
|
|
32
46
|
*
|
|
33
47
|
* @since 4.0.0
|
|
34
48
|
*/
|
|
@@ -41,25 +55,70 @@ const Replay = Schema.Struct({
|
|
|
41
55
|
seed: ReplaySeed,
|
|
42
56
|
path: Schema.Array(ReplayStep)
|
|
43
57
|
});
|
|
58
|
+
/** A property's identity hash: the unsigned whole number `identityHash` derives from its identity (KTD4). */
|
|
59
|
+
const PropertyHash = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyHash"));
|
|
60
|
+
/** The count of runs a property entry names, never negative (CONST-D3). */
|
|
61
|
+
const ReplayRunCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ReplayRunCount"));
|
|
62
|
+
/** The failure kind effect's falsification runner records in its replay token. */
|
|
63
|
+
const ReplayFailureTag = Schema.Literals(["ReturnedFalse", "PropertyError"]);
|
|
64
|
+
/**
|
|
65
|
+
* A property entry for a run that was not refuted: the property's identity hash, the seed it ran with and its
|
|
66
|
+
* run count (R9, R11).
|
|
67
|
+
*
|
|
68
|
+
* @since 4.0.0
|
|
69
|
+
*/
|
|
70
|
+
const PlainPropertyReplay = Schema.TaggedStruct("Plain", {
|
|
71
|
+
property: PropertyHash,
|
|
72
|
+
seed: ReplaySeed,
|
|
73
|
+
runs: ReplayRunCount
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* A property entry for a refuted run: the plain fields, plus the generator replay token's parts the engine needs
|
|
77
|
+
* to rebuild it — the attempt, the size, the shrink path and the failure tag (R9, R12, KTD5).
|
|
78
|
+
*
|
|
79
|
+
* @since 4.0.0
|
|
80
|
+
*/
|
|
81
|
+
const RefutedPropertyReplay = Schema.TaggedStruct("Refuted", {
|
|
82
|
+
property: PropertyHash,
|
|
83
|
+
seed: ReplaySeed,
|
|
84
|
+
runs: ReplayRunCount,
|
|
85
|
+
attempt: ReplayRunCount,
|
|
86
|
+
size: ReplayRunCount,
|
|
87
|
+
path: Schema.Array(ReplayStep),
|
|
88
|
+
failure: ReplayFailureTag
|
|
89
|
+
});
|
|
90
|
+
/** One property entry: a plain run, or a refuted one (R9). */
|
|
91
|
+
const PropertyReplay = Schema.Union([RefutedPropertyReplay, PlainPropertyReplay]);
|
|
92
|
+
/** What a `CONFORMANCE_REPLAY` text names: the kernel's replay, or one or more property entries (KTD5). */
|
|
93
|
+
const ReplayChannel = Schema.Union([Replay, Schema.Array(PropertyReplay)]);
|
|
44
94
|
const SEED_PREFIX = "seed=";
|
|
45
95
|
const PATH_PREFIX = "path=";
|
|
96
|
+
const PROPERTY_PREFIX = "property=";
|
|
97
|
+
const RUNS_PREFIX = "runs=";
|
|
98
|
+
const ATTEMPT_PREFIX = "attempt=";
|
|
99
|
+
const SIZE_PREFIX = "size=";
|
|
100
|
+
const FAILURE_PREFIX = "failure=";
|
|
46
101
|
const PAIR_SEPARATOR = ";";
|
|
47
|
-
const
|
|
102
|
+
const LIST_SEPARATOR = ",";
|
|
103
|
+
const ENTRY_SEPARATOR = "|";
|
|
48
104
|
const WHOLE_NUMBER = /^\d+$/u;
|
|
105
|
+
const ASSIGNMENT = /^([a-z]+)=([\s\S]*)$/u;
|
|
49
106
|
const NOT_A_REPLAY = "a replay reads \"seed=<whole number>;path=<comma-separated whole numbers>\"";
|
|
107
|
+
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 \"|\"";
|
|
50
108
|
const wholeNumberOf = (text) => WHOLE_NUMBER.test(text) ? Number(text) : void 0;
|
|
51
109
|
const seedDigitsOf = (text) => text.startsWith(SEED_PREFIX) ? text.slice(5) : void 0;
|
|
52
110
|
const pathDigitsOf = (text) => text.startsWith(PATH_PREFIX) ? text.slice(5) : void 0;
|
|
53
|
-
const entriesOf$1 = (text) => text.length === 0 ? [] : text.split(
|
|
111
|
+
const entriesOf$1 = (text) => text.length === 0 ? [] : text.split(LIST_SEPARATOR);
|
|
54
112
|
const wholeNumbersOf = (entries) => {
|
|
55
113
|
const numbers = entries.map(wholeNumberOf);
|
|
56
114
|
return numbers.every((value) => value !== void 0) ? numbers : void 0;
|
|
57
115
|
};
|
|
58
116
|
const seedIn = (part) => Option.flatMap(Option.fromUndefinedOr(seedDigitsOf(part)), (digits) => Option.fromUndefinedOr(wholeNumberOf(digits)));
|
|
59
117
|
const decisionsIn = (part) => Option.flatMap(Option.fromUndefinedOr(pathDigitsOf(part)), (entries) => Option.fromUndefinedOr(wholeNumbersOf(entriesOf$1(entries))));
|
|
118
|
+
const exactlyTwoParts = (parts) => parts.length === 2;
|
|
60
119
|
const pairPartsOf = (text) => {
|
|
61
|
-
const
|
|
62
|
-
return
|
|
120
|
+
const parts = text.split(PAIR_SEPARATOR);
|
|
121
|
+
return exactlyTwoParts(parts) ? Option.all([Option.fromUndefinedOr(parts[0]), Option.fromUndefinedOr(parts[1])]) : Option.none();
|
|
63
122
|
};
|
|
64
123
|
/** The seed and decisions a pair names, or nothing when the pair is not `seed=…;path=…`. */
|
|
65
124
|
const namedPairOf = (text) => Option.flatMap(pairPartsOf(text), ([seedPart, pathPart]) => Option.all([seedIn(seedPart), decisionsIn(pathPart)]));
|
|
@@ -69,7 +128,7 @@ const replayOfName = (text) => Option.flatMap(namedPairOf(text), ([seed, path])
|
|
|
69
128
|
seed,
|
|
70
129
|
path
|
|
71
130
|
}));
|
|
72
|
-
const textOfEncodedReplay = (replay) => `${SEED_PREFIX}${replay.seed}${PAIR_SEPARATOR}${PATH_PREFIX}${replay.path.join(
|
|
131
|
+
const textOfEncodedReplay = (replay) => `${SEED_PREFIX}${replay.seed}${PAIR_SEPARATOR}${PATH_PREFIX}${replay.path.join(LIST_SEPARATOR)}`;
|
|
73
132
|
const replayRefusal = (text) => /* @__PURE__ */ new Error(`CONFORMANCE_REPLAY names neither a seed nor a decision path: ${text}`);
|
|
74
133
|
/**
|
|
75
134
|
* The `CONFORMANCE_REPLAY` text as the replay it names. A text a seed and a path cannot be read out of is refused
|
|
@@ -88,9 +147,79 @@ const replayOfText = (text) => Option.match(replayOfName(text), {
|
|
|
88
147
|
* @since 4.0.0
|
|
89
148
|
*/
|
|
90
149
|
const replayTextOf = (replay) => textOfEncodedReplay(replay);
|
|
150
|
+
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]));
|
|
151
|
+
const fieldsOf$1 = (text) => Option.all(text.split(PAIR_SEPARATOR).map(fieldOfPart));
|
|
152
|
+
const textIn = (fields, key) => Option.map(Option.fromNullishOr(fields.find(([name]) => name === key)), (field) => field[1]);
|
|
153
|
+
/** The entry's keys name exactly `keys`, in order — so a reordered or repeated field is refused (KD8). */
|
|
154
|
+
const keysAre = (fields, keys) => fields.length === keys.length && fields.every(([name], at) => name === keys[at]);
|
|
155
|
+
const naturalIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Option.fromUndefinedOr(wholeNumberOf(text)));
|
|
156
|
+
const pathIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Option.fromUndefinedOr(wholeNumbersOf(entriesOf$1(text))));
|
|
157
|
+
const failureIn = (fields, key) => Option.flatMap(textIn(fields, key), (text) => Schema.decodeUnknownOption(ReplayFailureTag)(text));
|
|
158
|
+
const PLAIN_KEYS = [
|
|
159
|
+
"property",
|
|
160
|
+
"seed",
|
|
161
|
+
"runs"
|
|
162
|
+
];
|
|
163
|
+
const REFUTED_KEYS = [
|
|
164
|
+
"property",
|
|
165
|
+
"seed",
|
|
166
|
+
"runs",
|
|
167
|
+
"attempt",
|
|
168
|
+
"size",
|
|
169
|
+
"path",
|
|
170
|
+
"failure"
|
|
171
|
+
];
|
|
172
|
+
const plainEncodedOf = (fields) => Option.map(Option.all([
|
|
173
|
+
naturalIn(fields, "property"),
|
|
174
|
+
naturalIn(fields, "seed"),
|
|
175
|
+
naturalIn(fields, "runs")
|
|
176
|
+
]), ([property, seed, runs]) => ({
|
|
177
|
+
_tag: "Plain",
|
|
178
|
+
property,
|
|
179
|
+
seed,
|
|
180
|
+
runs
|
|
181
|
+
}));
|
|
182
|
+
const refutedEncodedOf = (fields) => Option.map(Option.all([
|
|
183
|
+
naturalIn(fields, "property"),
|
|
184
|
+
naturalIn(fields, "seed"),
|
|
185
|
+
naturalIn(fields, "runs"),
|
|
186
|
+
naturalIn(fields, "attempt"),
|
|
187
|
+
naturalIn(fields, "size"),
|
|
188
|
+
pathIn(fields, "path"),
|
|
189
|
+
failureIn(fields, "failure")
|
|
190
|
+
]), ([property, seed, runs, attempt, size, path, failure]) => ({
|
|
191
|
+
_tag: "Refuted",
|
|
192
|
+
property,
|
|
193
|
+
seed,
|
|
194
|
+
runs,
|
|
195
|
+
attempt,
|
|
196
|
+
size,
|
|
197
|
+
path,
|
|
198
|
+
failure
|
|
199
|
+
}));
|
|
200
|
+
const plainEntryOf = (fields) => Option.flatMap(plainEncodedOf(fields), (encoded) => Schema.decodeOption(PlainPropertyReplay)(encoded));
|
|
201
|
+
const refutedEntryOf$1 = (fields) => Option.flatMap(refutedEncodedOf(fields), (encoded) => Schema.decodeOption(RefutedPropertyReplay)(encoded));
|
|
202
|
+
const plainWhenKeysOf = (fields) => keysAre(fields, PLAIN_KEYS) ? plainEntryOf(fields) : Option.none();
|
|
203
|
+
const refutedWhenKeysOf = (fields) => keysAre(fields, REFUTED_KEYS) ? refutedEntryOf$1(fields) : Option.none();
|
|
204
|
+
const propertyEntryOf = (text) => Option.flatMap(fieldsOf$1(text), (fields) => Option.orElse(plainWhenKeysOf(fields), () => refutedWhenKeysOf(fields)));
|
|
205
|
+
const propertyChannelOf = (text) => text.length === 0 ? Option.none() : Option.all(text.split(ENTRY_SEPARATOR).map(propertyEntryOf));
|
|
206
|
+
const channelOfName = (text) => Option.match(replayOfName(text), {
|
|
207
|
+
onNone: () => propertyChannelOf(text),
|
|
208
|
+
onSome: (replay) => Option.some(replay)
|
|
209
|
+
});
|
|
210
|
+
const isEntries$1 = (channel) => Array.isArray(channel);
|
|
211
|
+
/** The text a plain property entry is written as (R9, R11). */
|
|
212
|
+
const plainReplayText = (fields) => `${PROPERTY_PREFIX}${fields.property}${PAIR_SEPARATOR}${SEED_PREFIX}${fields.seed}${PAIR_SEPARATOR}${RUNS_PREFIX}${fields.runs}`;
|
|
213
|
+
/** The text several plain entries are written as, joined by `|` (R11). */
|
|
214
|
+
const plainReplayTexts = (entries) => entries.map(plainReplayText).join(ENTRY_SEPARATOR);
|
|
215
|
+
/** The text a refuted property entry is written as (R9, R12). */
|
|
216
|
+
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}`;
|
|
217
|
+
const entryTextOf = (entry) => Match.value(entry).pipe(Match.tag("Refuted", (refuted) => refutedReplayText$1(refuted)), Match.tag("Plain", (plain) => plainReplayText(plain)), Match.exhaustive);
|
|
218
|
+
/** The text a channel is written as: the kernel's replay, or the entries joined by `|` (KTD5). */
|
|
219
|
+
const replayChannelTextOf = (channel) => isEntries$1(channel) ? channel.map(entryTextOf).join(ENTRY_SEPARATOR) : textOfEncodedReplay(channel);
|
|
91
220
|
/**
|
|
92
|
-
* The replay codec: `Schema.String` in, a replay out. A text that names no seed and path fails the decode,
|
|
93
|
-
* every replay encodes back to the text it was read from.
|
|
221
|
+
* The kernel's replay codec: `Schema.String` in, a replay out. A text that names no seed and path fails the decode,
|
|
222
|
+
* and every replay encodes back to the text it was read from.
|
|
94
223
|
*
|
|
95
224
|
* @since 4.0.0
|
|
96
225
|
*/
|
|
@@ -98,6 +227,16 @@ const ReplayFromText = Schema.String.pipe(Schema.decodeTo(Replay, {
|
|
|
98
227
|
decode: SchemaGetter.transformEffect((text, options) => Effect.fromOption(replayOfName(text), () => new SchemaIssue.InvalidValue({ message: NOT_A_REPLAY }, text, options))),
|
|
99
228
|
encode: SchemaGetter.transform(textOfEncodedReplay)
|
|
100
229
|
}));
|
|
230
|
+
/**
|
|
231
|
+
* The channel codec: `Schema.String` in, the kernel's replay or the property entries out. A text that names neither
|
|
232
|
+
* form fails the decode, and every channel encodes back to the text it was read from.
|
|
233
|
+
*
|
|
234
|
+
* @since 4.0.0
|
|
235
|
+
*/
|
|
236
|
+
const ReplayChannelFromText = Schema.String.pipe(Schema.decodeTo(ReplayChannel, {
|
|
237
|
+
decode: SchemaGetter.transformEffect((text, options) => Effect.fromOption(channelOfName(text), () => new SchemaIssue.InvalidValue({ message: NOT_A_CHANNEL }, text, options))),
|
|
238
|
+
encode: SchemaGetter.transform((channel) => replayChannelTextOf(channel))
|
|
239
|
+
}));
|
|
101
240
|
//#endregion
|
|
102
241
|
//#region src/internal/errors.schema.ts
|
|
103
242
|
/**
|
|
@@ -129,12 +268,6 @@ const REFUSED_BREACH_TEXT = {
|
|
|
129
268
|
*/
|
|
130
269
|
const refusedRecordDetail = (breaches) => `✗ refused a failure record: ${breaches.map((breach) => REFUSED_BREACH_TEXT[breach]).join(", and ")}`;
|
|
131
270
|
/** @internal */
|
|
132
|
-
var NonBooleanVerdict = class extends Schema.TaggedError()("NonBooleanVerdict", { detail: Schema.String }) {
|
|
133
|
-
get message() {
|
|
134
|
-
return this.detail;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
/** @internal */
|
|
138
271
|
var LeakedState = class extends Schema.TaggedError()("LeakedState", { detail: Schema.String }) {
|
|
139
272
|
get message() {
|
|
140
273
|
return this.detail;
|
|
@@ -256,7 +389,7 @@ const MARK = {
|
|
|
256
389
|
failed: "✗",
|
|
257
390
|
unfinished: "✗"
|
|
258
391
|
};
|
|
259
|
-
const not = (value) => !value;
|
|
392
|
+
const not$1 = (value) => !value;
|
|
260
393
|
const and = (left, right) => left && right;
|
|
261
394
|
const or = (left, right) => left || right;
|
|
262
395
|
const firstPresent = (values) => values.find((value) => value !== void 0);
|
|
@@ -289,16 +422,85 @@ const pairTextsOf = (pairs) => pairs.map(pairTextOf);
|
|
|
289
422
|
const hasPair = (pairs, name) => pairs.some((pair) => pair.name === name);
|
|
290
423
|
const pairsOf = (value) => isObject(value) ? Object.keys(value).map((key) => pairOf(key, renderValue(fieldOf(value, key)))) : [];
|
|
291
424
|
const fieldsOf = (value) => pairsOf(value).filter((pair) => isFieldName(pair.name));
|
|
292
|
-
const renderValue = (value) =>
|
|
425
|
+
const renderValue = (value) => witnessOf(value).rendered;
|
|
293
426
|
const renderScalar = (value) => isText(value) ? JSON.stringify(value) : renderPrimitive(value);
|
|
294
427
|
const renderPrimitive = (value) => isBigInt(value) ? `${value}n` : renderPlain(value);
|
|
295
428
|
const renderPlain = (value) => isPrimitive(value) ? `${value}` : renderOther(value);
|
|
296
429
|
const renderOther = (value) => isNull(value) ? "null" : renderUndefined(value);
|
|
297
430
|
const renderUndefined = (value) => value === void 0 ? "undefined" : renderContainer(value);
|
|
298
431
|
const renderContainer = (value) => isArray(value) ? renderList(value) : typeof value;
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
432
|
+
const listTextOf = (children) => `[${children.join(",")}]`;
|
|
433
|
+
const recordTextOf = (pairs) => `{${pairTextsOf(pairs).join(",")}}`;
|
|
434
|
+
const renderList = (value) => listTextOf(value.map(renderValue));
|
|
435
|
+
const renderRecord$1 = (value) => recordTextOf(pairsOf(value));
|
|
436
|
+
const KIND_FIELD = "_kind";
|
|
437
|
+
const VALUE_FIELD = "value";
|
|
438
|
+
const DESCRIPTION_FIELD = "description";
|
|
439
|
+
const UNDEFINED_JSON = { [KIND_FIELD]: "undefined" };
|
|
440
|
+
const CIRCULAR_WITNESS = {
|
|
441
|
+
rendered: "[Circular]",
|
|
442
|
+
value: { [KIND_FIELD]: "circular" }
|
|
443
|
+
};
|
|
444
|
+
const markOf = (kind, field, text) => ({
|
|
445
|
+
[KIND_FIELD]: kind,
|
|
446
|
+
[field]: text
|
|
447
|
+
});
|
|
448
|
+
const isSymbol = (value) => typeof value === "symbol";
|
|
449
|
+
const isCallable = (value) => typeof value === "function";
|
|
450
|
+
const callableNameOf = (value) => {
|
|
451
|
+
const name = fieldOf(value, NAME_FIELD);
|
|
452
|
+
return isText(name) ? name : "";
|
|
453
|
+
};
|
|
454
|
+
const numberJsonOf = (value) => Number.isFinite(value) ? value : markOf("number", VALUE_FIELD, `${value}`);
|
|
455
|
+
const bigintJsonOf = (value) => markOf("bigint", VALUE_FIELD, `${value}`);
|
|
456
|
+
const functionJsonOf = (value) => markOf("function", NAME_FIELD, callableNameOf(value));
|
|
457
|
+
const symbolJsonOf = (value) => markOf("symbol", DESCRIPTION_FIELD, value.description ?? "");
|
|
458
|
+
const scalarJsonOf = (value) => isText(value) ? value : nonTextJsonOf(value);
|
|
459
|
+
const nonTextJsonOf = (value) => isBoolean(value) ? value : nullOrOtherJsonOf(value);
|
|
460
|
+
const nullOrOtherJsonOf = (value) => isNull(value) ? value : numberOrOtherJsonOf(value);
|
|
461
|
+
const numberOrOtherJsonOf = (value) => isNumber(value) ? numberJsonOf(value) : bigintOrOtherJsonOf(value);
|
|
462
|
+
const bigintOrOtherJsonOf = (value) => isBigInt(value) ? bigintJsonOf(value) : callableOrUndefinedJsonOf(value);
|
|
463
|
+
const callableOrUndefinedJsonOf = (value) => isCallable(value) ? functionJsonOf(value) : symbolOrUndefinedJsonOf(value);
|
|
464
|
+
const symbolOrUndefinedJsonOf = (value) => isSymbol(value) ? symbolJsonOf(value) : UNDEFINED_JSON;
|
|
465
|
+
/**
|
|
466
|
+
* The one traversal a value's rendered text and its JSON projection come from (R3, KD7): `rendered` is exactly
|
|
467
|
+
* `renderValue`'s text, `value` is the same walk with every value JSON cannot carry marked by kind, and a cycle
|
|
468
|
+
* marks `circular` rather than recursing forever.
|
|
469
|
+
*
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
const witnessOf = (value) => witnessAt(value, []);
|
|
473
|
+
const witnessAt = (value, seen) => isObject(value) ? witnessedStructure(value, seen) : witnessedScalar(value);
|
|
474
|
+
const witnessedStructure = (value, seen) => seen.includes(value) ? CIRCULAR_WITNESS : witnessedObject(value, seen);
|
|
475
|
+
const witnessedObject = (value, seen) => isArray(value) ? witnessedList(value, seen) : witnessedRecord(value, seen);
|
|
476
|
+
const witnessedScalar = (value) => ({
|
|
477
|
+
rendered: renderScalar(value),
|
|
478
|
+
value: scalarJsonOf(value)
|
|
479
|
+
});
|
|
480
|
+
const witnessedList = (value, seen) => {
|
|
481
|
+
const children = value.map((item) => witnessAt(item, seenWith(seen, value)));
|
|
482
|
+
return {
|
|
483
|
+
rendered: listTextOf(children.map(renderedOf)),
|
|
484
|
+
value: children.map(witnessValueOf)
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
const witnessedRecord = (value, seen) => {
|
|
488
|
+
const entries = entriesAt(value, seenWith(seen, value));
|
|
489
|
+
return {
|
|
490
|
+
rendered: tagFieldOf(value) ?? recordTextOf(entryPairsOf(entries)),
|
|
491
|
+
value: entriesJsonOf(entries)
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
const seenWith = (seen, value) => [...seen, value];
|
|
495
|
+
const entriesAt = (value, seen) => Object.keys(value).map((key) => witnessEntryOf(key, witnessAt(fieldOf(value, key), seen)));
|
|
496
|
+
const witnessEntryOf = (name, witness) => ({
|
|
497
|
+
name,
|
|
498
|
+
witness
|
|
499
|
+
});
|
|
500
|
+
const entryPairsOf = (entries) => entries.map((entry) => pairOf(entry.name, entry.witness.rendered));
|
|
501
|
+
const entriesJsonOf = (entries) => Object.fromEntries(entries.map((entry) => [entry.name, entry.witness.value]));
|
|
502
|
+
const renderedOf = (witness) => witness.rendered;
|
|
503
|
+
const witnessValueOf = (witness) => witness.value;
|
|
302
504
|
const bracesOf = (pairs) => ` {${pairTextsOf(pairs).join(",")}}`;
|
|
303
505
|
const renderFields = (value) => bracesOf(fieldsOf(value));
|
|
304
506
|
const textSummaryOf = (text) => text.length === 0 ? text : JSON.stringify(text);
|
|
@@ -359,7 +561,7 @@ const textAttrOf = (span, key) => {
|
|
|
359
561
|
const ancestorsIn = (span) => span instanceof Tracer.NativeSpan ? [span, ...ancestorsOf(span.parent)] : [];
|
|
360
562
|
const ancestorsOf = (parent) => Option$1.isNone(parent) ? [] : ancestorsIn(parent.value);
|
|
361
563
|
const stepAncestorOf = (span) => ancestorsOf(span.parent).find((ancestor) => ancestor.name === STEP_SPAN);
|
|
362
|
-
const stateOf = (span) => Match.value(span.status).pipe(Match.when({ _tag: "Started" }, () => "unfinished"), Match.when({ _tag: "Ended" }, (ended) => endedStateOf(ended.exit)), Match.exhaustive);
|
|
564
|
+
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);
|
|
363
565
|
const endedStateOf = (exit) => Exit.isSuccess(exit) ? "passed" : "failed";
|
|
364
566
|
const stepSpansOf = (spans) => spans.filter(isTopStepSpan);
|
|
365
567
|
const isTopStepSpan = (span) => and(span.name === STEP_SPAN, stepAncestorOf(span) === void 0);
|
|
@@ -376,7 +578,7 @@ const nestedOf = (cause, depth) => cause === void 0 ? [] : layersOfCause(cause,
|
|
|
376
578
|
const nestedCauseOf = (value, depth) => depth === 0 ? [] : nestedOf(causeFieldOf(value), depth);
|
|
377
579
|
const causeChainOf = (value) => [value, ...nestedCauseOf(value, CAUSE_DEPTH)];
|
|
378
580
|
const interruptTextOf = (reason) => reason.fiberId === void 0 ? "interrupted" : `interrupted by fiber #${reason.fiberId}`;
|
|
379
|
-
const reasonPayloadOf = (reason) => Match.value(reason).pipe(Match.when({ _tag: "Fail" }, (fail) => fail.error), Match.when({ _tag: "Die" }, (die) => die.defect), Match.when({ _tag: "Interrupt" }, (interrupt) => interruptTextOf(interrupt)), Match.exhaustive);
|
|
581
|
+
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);
|
|
380
582
|
const reasonsOf = (cause) => cause.reasons.flatMap((reason) => causeChainOf(reasonPayloadOf(reason)));
|
|
381
583
|
const layersOf = (failure) => Cause.isCause(failure) ? reasonsOf(failure) : causeChainOf(failure);
|
|
382
584
|
const declineOf = (span) => stateOf(span) === "unfinished" ? NEVER_FINISHED : "";
|
|
@@ -462,17 +664,23 @@ const chainDetailLines = (layer) => isObject(layer) ? messageTailLines(layer) :
|
|
|
462
664
|
const chainLayerLines = (layer, chosen) => [chainLineOf(layer, chosen), ...chainDetailLines(layer)];
|
|
463
665
|
const chainLinesOf = (layers, chosen) => layers.slice(1).flatMap((layer) => chainLayerLines(layer, chosen));
|
|
464
666
|
const quoted = (text) => `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
667
|
+
const REPLAY_FIELD = "replay";
|
|
668
|
+
const propertyReplayTextOf = (failure) => {
|
|
669
|
+
const layer = propertyFailureOf(failure);
|
|
670
|
+
return layer === void 0 ? void 0 : textFieldOf(layer, REPLAY_FIELD);
|
|
671
|
+
};
|
|
465
672
|
const rerunReplayTextOf = (replay) => Option$1.getOrUndefined(Option$1.flatMap(Option$1.fromUndefinedOr(replay.seed), (seed) => Option$1.map(replayOfParts({
|
|
466
673
|
seed,
|
|
467
674
|
path: replay.path
|
|
468
675
|
}), replayTextOf)));
|
|
676
|
+
const kernelReplayTextOf = (replay) => replay === void 0 ? void 0 : rerunReplayTextOf(replay);
|
|
469
677
|
const replayPrefixText = (text) => text === void 0 ? "" : `CONFORMANCE_REPLAY="${text}" `;
|
|
470
|
-
const replayPrefixOf = (replay) => replayPrefixText(
|
|
678
|
+
const replayPrefixOf = (replayText, replay) => replayPrefixText(replayText ?? kernelReplayTextOf(replay));
|
|
471
679
|
const isCompleteIdentity = (identity) => and(identity.file.length > 0, identity.name.length > 0);
|
|
472
680
|
const filterPrefixOf = (identity) => identity.package.length === 0 ? "" : `pnpm --filter ${identity.package} exec `;
|
|
473
|
-
const rerunLineOf = (identity, replay) => isCompleteIdentity(identity) ? ` ${replayPrefixOf(replay)}${filterPrefixOf(identity)}vitest run ${identity.file} -t ` + quoted(identity.name) : void 0;
|
|
474
|
-
const rerunLinesOf = (identity, replay) => {
|
|
475
|
-
const line = rerunLineOf(identity, replay);
|
|
681
|
+
const rerunLineOf = (identity, replay, replayText) => isCompleteIdentity(identity) ? ` ${replayPrefixOf(replayText, replay)}${filterPrefixOf(identity)}vitest run ${identity.file} -t ` + quoted(identity.name) : void 0;
|
|
682
|
+
const rerunLinesOf = (identity, replay, replayText) => {
|
|
683
|
+
const line = rerunLineOf(identity, replay, replayText);
|
|
476
684
|
return line === void 0 ? [] : ["Rerun only this scenario:", line];
|
|
477
685
|
};
|
|
478
686
|
const baselineReplay = (replay) => and(replay.seed === void 0, replay.path.length > 0);
|
|
@@ -501,7 +709,7 @@ const recordPartsOf = (input, layers) => {
|
|
|
501
709
|
chain: chainLinesOf(layers, chosen),
|
|
502
710
|
trail: trailLinesOf(input.spans),
|
|
503
711
|
schedule: scheduleLinesOf(input.schedule),
|
|
504
|
-
rerun: rerunLinesOf(input.identity, input.replay)
|
|
712
|
+
rerun: rerunLinesOf(input.identity, input.replay, propertyReplayTextOf(input.failure))
|
|
505
713
|
};
|
|
506
714
|
};
|
|
507
715
|
const maybeLine = (line) => line === void 0 ? [] : [line];
|
|
@@ -510,7 +718,7 @@ const sectionLines = (header, lines) => lines.length === 0 ? [] : [
|
|
|
510
718
|
...lines,
|
|
511
719
|
""
|
|
512
720
|
];
|
|
513
|
-
const linesOf = (parts) => [
|
|
721
|
+
const linesOf$1 = (parts) => [
|
|
514
722
|
parts.headline,
|
|
515
723
|
...maybeLine(parts.raisedAt),
|
|
516
724
|
parts.failingStep,
|
|
@@ -525,7 +733,7 @@ const breachIf = (holds, breach) => holds ? [breach] : [];
|
|
|
525
733
|
const r6Broken = (parts, input) => or(parts.rerun.length === 0, replayOnBaseline(input.replay));
|
|
526
734
|
const breachesOf = (parts, input, record) => [
|
|
527
735
|
...breachIf(parts.headline.length === 0, "R1"),
|
|
528
|
-
...breachIf(not(LOCATION.test(record)), "R2"),
|
|
736
|
+
...breachIf(not$1(LOCATION.test(record)), "R2"),
|
|
529
737
|
...breachIf(r6Broken(parts, input), "R6")
|
|
530
738
|
];
|
|
531
739
|
const hasRoot = (root) => root !== void 0 && root.length > 0;
|
|
@@ -543,11 +751,13 @@ const relativize = (record, root) => hasRoot(root) ? record.replaceAll(`${root}/
|
|
|
543
751
|
const renderFailureRecord = (input) => {
|
|
544
752
|
const layers = layersOf(input.failure);
|
|
545
753
|
const parts = recordPartsOf(input, layers);
|
|
546
|
-
const record = relativize(linesOf(parts).join("\n").trimEnd(), input.root);
|
|
754
|
+
const record = relativize(linesOf$1(parts).join("\n").trimEnd(), input.root);
|
|
755
|
+
const property = propertyFailureOf(input.failure);
|
|
547
756
|
return {
|
|
548
757
|
name: nameOf(layers),
|
|
549
758
|
record,
|
|
550
|
-
breaches: breachesOf(parts, input, record)
|
|
759
|
+
breaches: breachesOf(parts, input, record),
|
|
760
|
+
...property === void 0 ? {} : { failure: property }
|
|
551
761
|
};
|
|
552
762
|
};
|
|
553
763
|
const RECORD = Symbol.for("@systemfsoftware/vitest/FailureRecord");
|
|
@@ -617,6 +827,36 @@ const copyDiffFields = (error, failure) => {
|
|
|
617
827
|
return carrier === void 0 ? error : copyFields(error, carrier);
|
|
618
828
|
};
|
|
619
829
|
/**
|
|
830
|
+
* The property-channel failure tags: a failure whose own fields ride onto the printed error and stay readable from
|
|
831
|
+
* the corpus record (R1, R2, R8). Tags, not the classes, so this module never imports the property engine.
|
|
832
|
+
*/
|
|
833
|
+
const PROPERTY_FAILURE_TAGS = [
|
|
834
|
+
"PropertyRefuted",
|
|
835
|
+
"NonBooleanVerdict",
|
|
836
|
+
"CoverageBelowMinimum",
|
|
837
|
+
"SelfModelLaw",
|
|
838
|
+
"SeedStoreUnreadable",
|
|
839
|
+
"ReplayUnreadable",
|
|
840
|
+
"VacuousProperty"
|
|
841
|
+
];
|
|
842
|
+
const isPropertyFailureLayer = (value) => {
|
|
843
|
+
const tag = tagFieldOf(value);
|
|
844
|
+
return tag !== void 0 && PROPERTY_FAILURE_TAGS.includes(tag);
|
|
845
|
+
};
|
|
846
|
+
const propertyFailureOf = (failure) => layerChainOf(failure).map(objectOrUndefined).filter(isObject).find(isPropertyFailureLayer);
|
|
847
|
+
/**
|
|
848
|
+
* Copies the property failure's `_tag` and every own field onto the printed error, so a reporter reading the raw
|
|
849
|
+
* error sees them. The message and stack stay untouched.
|
|
850
|
+
*/
|
|
851
|
+
const copyOwnFields = (error, layer) => {
|
|
852
|
+
for (const field of Object.keys(layer)) copyField(error, layer, field);
|
|
853
|
+
return error;
|
|
854
|
+
};
|
|
855
|
+
const copyPropertyFields = (error, failure) => {
|
|
856
|
+
const layer = propertyFailureOf(failure);
|
|
857
|
+
return layer === void 0 ? error : copyOwnFields(error, layer);
|
|
858
|
+
};
|
|
859
|
+
/**
|
|
620
860
|
* The `Error` Vitest prints for one record (R2, R7, R8): its `name` is the record's failure tag, its `message` is
|
|
621
861
|
* the record, and its `stack` leads with that message — Vitest's JSON reporter hands a consumer `stack || message`,
|
|
622
862
|
* so the record has to ride there too — followed by the record's first location, so no `effect` or library frame
|
|
@@ -654,7 +894,8 @@ const isFailureRecordError = (value) => isObject(value) && fieldOf(value, RECORD
|
|
|
654
894
|
const throwFailureRecord = (input) => {
|
|
655
895
|
if (isRefusalFailure(input.failure)) throw input.failure;
|
|
656
896
|
const record = renderFailureRecord(input);
|
|
657
|
-
|
|
897
|
+
const printed = copyDiffFields(printedError(input.failure, record), input.failure);
|
|
898
|
+
throw copyPropertyFields(printed, input.failure);
|
|
658
899
|
};
|
|
659
900
|
const LEVEL_SEPARATOR = " > ";
|
|
660
901
|
const EMPTY_TEXT = "";
|
|
@@ -684,7 +925,6 @@ const TOLERANCE = .9;
|
|
|
684
925
|
const FIRST_BATCH = 64;
|
|
685
926
|
const BATCH_CAP = 4096;
|
|
686
927
|
const DRAW_CAP = 1e5;
|
|
687
|
-
const FAILURE_HEAD = "coverage";
|
|
688
928
|
/** The certainty's two-sided tail probability: `alpha / 2` for `alpha = 1 / CERTAINTY`. */
|
|
689
929
|
const TAIL_PROBABILITY = 1 / (2 * CERTAINTY);
|
|
690
930
|
const C1 = -.007784894002430293;
|
|
@@ -722,12 +962,6 @@ const wilson = (hits, runs, z) => {
|
|
|
722
962
|
const zz = z * z;
|
|
723
963
|
return (share + zz / (2 * runs) + z * Math.sqrt(share * (1 - share) / runs + zz / (4 * runs * runs))) / (1 + zz / runs);
|
|
724
964
|
};
|
|
725
|
-
/**
|
|
726
|
-
* Thrown when a coverage class is confidently below its minimum share of runs.
|
|
727
|
-
*
|
|
728
|
-
* @internal
|
|
729
|
-
*/
|
|
730
|
-
var CoverageBelowMinimum = class extends Data.Error {};
|
|
731
965
|
const wilsonLower = (entry, runs) => wilson(entry.hits, runs, -TAIL_Z);
|
|
732
966
|
const wilsonUpper = (entry, runs) => wilson(entry.hits, runs, TAIL_Z);
|
|
733
967
|
const covered = (entry, runs) => wilsonLower(entry, runs) >= TOLERANCE * entry.minimum;
|
|
@@ -746,9 +980,12 @@ const judgeOf = (classes, runs) => ({
|
|
|
746
980
|
});
|
|
747
981
|
const openLabels = (judge) => [...judge.classes].filter(([, entry]) => !settled(entry, judge.runs)).map(([label]) => label);
|
|
748
982
|
const failedEntries = (judge) => [...judge.classes].filter(([, entry]) => uncovered(entry, judge.runs));
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
983
|
+
const failedFailures = (judge) => failedEntries(judge).map(([label, entry]) => ({
|
|
984
|
+
label,
|
|
985
|
+
hits: entry.hits,
|
|
986
|
+
runs: judge.runs,
|
|
987
|
+
minimum: entry.minimum
|
|
988
|
+
}));
|
|
752
989
|
/** @internal */
|
|
753
990
|
const countHit = Function.dual(2, (classes, label) => {
|
|
754
991
|
const prior = classes.get(label);
|
|
@@ -771,48 +1008,237 @@ const sizeOf = (judge, batch) => Math.min(batch, DRAW_CAP - judge.runs);
|
|
|
771
1008
|
const grown = (batch) => Math.min(batch * 2, BATCH_CAP);
|
|
772
1009
|
const keepDrawing = (judge) => openLabels(judge).length > 0 && judge.runs < DRAW_CAP;
|
|
773
1010
|
const toppedUp = (judge, draw, batch) => keepDrawing(judge) ? toppedUp(drawBatch(judge, draw, batch), draw, grown(batch)) : judge;
|
|
774
|
-
const reportOf$1 = (judge) => failedEntries(judge).length === 0 ? void 0 : reportFailures(judge);
|
|
775
1011
|
/**
|
|
776
1012
|
* Runs R14's sequential draw: tops up the undecided classes until every class is decided or the draw cap
|
|
777
|
-
* is reached, then
|
|
1013
|
+
* is reached, then hands back the confidently under-covered classes as data, each with its label, hits, the runs
|
|
1014
|
+
* counted and the required share.
|
|
778
1015
|
*
|
|
779
1016
|
* @internal
|
|
780
1017
|
*/
|
|
781
|
-
const judgeCoverage = (options) =>
|
|
1018
|
+
const judgeCoverage = (options) => failedFailures(toppedUp(judgeOf(options.classes, options.runs), options.draw, FIRST_BATCH));
|
|
782
1019
|
//#endregion
|
|
783
1020
|
//#region src/internal/property/error.schema.ts
|
|
784
1021
|
/**
|
|
785
|
-
* The
|
|
786
|
-
*
|
|
1022
|
+
* The closed set of property-channel failures (KTD1, R1): refuted, non-boolean, under-covered and self-model, plus
|
|
1023
|
+
* the vacuous impostor verdict and the seed store's read failure. Every variant is a `Schema.TaggedError` carrying
|
|
1024
|
+
* its own data — the property it names, a rendered-and-projected witness, coverage classes, the law that refused
|
|
1025
|
+
* itself — and a zero-argument `message` getter computed only from those fields. Wording is free; nothing reads a
|
|
1026
|
+
* message back.
|
|
1027
|
+
*
|
|
1028
|
+
* @since 4.0.0
|
|
1029
|
+
*/
|
|
1030
|
+
/**
|
|
1031
|
+
* A property's seed: a whole number a generator chose, never negative (CONST-D3).
|
|
1032
|
+
*
|
|
1033
|
+
* @internal
|
|
1034
|
+
*/
|
|
1035
|
+
const PropertySeed = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertySeed"));
|
|
1036
|
+
/**
|
|
1037
|
+
* The count of runs a property's check was configured with, never negative (CONST-D3).
|
|
1038
|
+
*
|
|
1039
|
+
* @internal
|
|
1040
|
+
*/
|
|
1041
|
+
const PropertyRunCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyRunCount"));
|
|
1042
|
+
/** @internal */
|
|
1043
|
+
const PropertyShrinkCount = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.brand("PropertyShrinkCount"));
|
|
1044
|
+
/**
|
|
1045
|
+
* One witness: the text the renderer prints for a value, and a JSON-safe projection of the same traversal (R3, KD7).
|
|
787
1046
|
*
|
|
788
1047
|
* @internal
|
|
789
1048
|
*/
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
1049
|
+
const Witness = Schema.Struct({
|
|
1050
|
+
rendered: Schema.String,
|
|
1051
|
+
value: Schema.Json
|
|
793
1052
|
});
|
|
794
1053
|
/**
|
|
795
|
-
*
|
|
796
|
-
* one, so the rerun line can replay it (KTD5).
|
|
1054
|
+
* One property involved in a failure: its full name, its declaration site, its seed and its run count (R2).
|
|
797
1055
|
*
|
|
798
1056
|
* @internal
|
|
799
1057
|
*/
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1058
|
+
const PropertyRun = Schema.Struct({
|
|
1059
|
+
name: Schema.String,
|
|
1060
|
+
site: Schema.NullOr(Schema.String),
|
|
1061
|
+
seed: PropertySeed,
|
|
1062
|
+
runs: PropertyRunCount
|
|
1063
|
+
});
|
|
1064
|
+
/**
|
|
1065
|
+
* The kinds a non-boolean verdict can be: every `typeof` result, plus an `Effect` on the sync lane (R4).
|
|
1066
|
+
*
|
|
1067
|
+
* @internal
|
|
1068
|
+
*/
|
|
1069
|
+
const VerdictKind = Schema.Literals([
|
|
1070
|
+
"undefined",
|
|
1071
|
+
"object",
|
|
1072
|
+
"boolean",
|
|
1073
|
+
"number",
|
|
1074
|
+
"bigint",
|
|
1075
|
+
"string",
|
|
1076
|
+
"symbol",
|
|
1077
|
+
"function",
|
|
1078
|
+
"effect"
|
|
1079
|
+
]);
|
|
1080
|
+
/**
|
|
1081
|
+
* A falsified property: the property that ran, the shrunk counterexample the generator settled on, and how many
|
|
1082
|
+
* shrink steps it took to reach it (R2-R4).
|
|
1083
|
+
*
|
|
1084
|
+
* @internal
|
|
1085
|
+
*/
|
|
1086
|
+
var PropertyRefuted = class extends Schema.TaggedError()("PropertyRefuted", {
|
|
1087
|
+
property: PropertyRun,
|
|
1088
|
+
counterexample: Witness,
|
|
1089
|
+
shrinks: PropertyShrinkCount,
|
|
1090
|
+
replay: Schema.String
|
|
803
1091
|
}) {
|
|
804
1092
|
get message() {
|
|
805
|
-
return this.
|
|
1093
|
+
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}`;
|
|
806
1094
|
}
|
|
807
1095
|
};
|
|
808
1096
|
/**
|
|
809
|
-
*
|
|
1097
|
+
* A verdict that was not a literal boolean: the property that ran, the first drawn values whose verdict was not a
|
|
1098
|
+
* boolean, and every kind of value the property returned (R4).
|
|
810
1099
|
*
|
|
811
1100
|
* @internal
|
|
812
1101
|
*/
|
|
813
|
-
var
|
|
1102
|
+
var NonBooleanVerdict = class extends Schema.TaggedError()("NonBooleanVerdict", {
|
|
1103
|
+
property: PropertyRun,
|
|
1104
|
+
drawn: Witness,
|
|
1105
|
+
returned: Schema.Array(VerdictKind),
|
|
1106
|
+
replay: Schema.String
|
|
1107
|
+
}) {
|
|
814
1108
|
get message() {
|
|
815
|
-
return this.
|
|
1109
|
+
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.`;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
/**
|
|
1113
|
+
* One coverage class left under its minimum: its label, hits, runs and required share (R7).
|
|
1114
|
+
*
|
|
1115
|
+
* @internal
|
|
1116
|
+
*/
|
|
1117
|
+
const CoverageClassFailure = Schema.Struct({
|
|
1118
|
+
label: Schema.String,
|
|
1119
|
+
hits: Schema.Int,
|
|
1120
|
+
runs: Schema.Int,
|
|
1121
|
+
minimum: Schema.Finite
|
|
1122
|
+
});
|
|
1123
|
+
/**
|
|
1124
|
+
* A property whose declared coverage classes stayed under their minima: the property and the failed classes, in
|
|
1125
|
+
* declaration order (R7).
|
|
1126
|
+
*
|
|
1127
|
+
* @internal
|
|
1128
|
+
*/
|
|
1129
|
+
var CoverageBelowMinimum = class extends Schema.TaggedError()("CoverageBelowMinimum", {
|
|
1130
|
+
property: PropertyRun,
|
|
1131
|
+
classes: Schema.Array(CoverageClassFailure),
|
|
1132
|
+
replay: Schema.String
|
|
1133
|
+
}) {
|
|
1134
|
+
get message() {
|
|
1135
|
+
const lines = this.classes.map((entry) => `coverage ${entry.label}: ${entry.hits}/${entry.runs} of runs, below the required ${entry.minimum}`);
|
|
1136
|
+
return `${this.property.name}: ${lines.join("\n")}`;
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
/**
|
|
1140
|
+
* A model law given the subject itself as its oracle, so it compares nothing (R13).
|
|
1141
|
+
*
|
|
1142
|
+
* @internal
|
|
1143
|
+
*/
|
|
1144
|
+
var SelfModelLaw = class extends Schema.TaggedError()("SelfModelLaw", {
|
|
1145
|
+
name: Schema.String,
|
|
1146
|
+
site: Schema.NullOr(Schema.String)
|
|
1147
|
+
}) {
|
|
1148
|
+
get message() {
|
|
1149
|
+
return `${this.name}: the model is the subject itself; a model law must compare against an independent oracle.`;
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
/**
|
|
1153
|
+
* A seed-store line that could not be decoded (R16, KTD6).
|
|
1154
|
+
*
|
|
1155
|
+
* @internal
|
|
1156
|
+
*/
|
|
1157
|
+
var SeedStoreUnreadable = class extends Schema.TaggedError()("SeedStoreUnreadable", {
|
|
1158
|
+
file: Schema.String,
|
|
1159
|
+
line: Schema.Int,
|
|
1160
|
+
detail: Schema.String
|
|
1161
|
+
}) {
|
|
1162
|
+
get message() {
|
|
1163
|
+
return `the seed store ${this.file} line ${this.line} could not be read: ${this.detail}`;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
/**
|
|
1167
|
+
* What a subject's constant impostor froze for one property: each frozen key — the subject itself, or a record
|
|
1168
|
+
* member — with the value its first call returned (R4, R6). Empty when the property never called the subject.
|
|
1169
|
+
*
|
|
1170
|
+
* @internal
|
|
1171
|
+
*/
|
|
1172
|
+
const FrozenOutput = Schema.Struct({
|
|
1173
|
+
member: Schema.String,
|
|
1174
|
+
output: Witness
|
|
1175
|
+
});
|
|
1176
|
+
/**
|
|
1177
|
+
* The impostor's frozen outputs (R4).
|
|
1178
|
+
*
|
|
1179
|
+
* @internal
|
|
1180
|
+
*/
|
|
1181
|
+
const Frozen = Schema.TaggedStruct("Frozen", { outputs: Schema.Array(FrozenOutput) });
|
|
1182
|
+
/**
|
|
1183
|
+
* A property whose body never called the subject during the vacuity check: nothing was frozen (R6).
|
|
1184
|
+
*
|
|
1185
|
+
* @internal
|
|
1186
|
+
*/
|
|
1187
|
+
const NeverCalled = Schema.TaggedStruct("NeverCalled", {});
|
|
1188
|
+
/**
|
|
1189
|
+
* One non-refuting property of a vacuous subject: the property it ran, and whether the impostor was called (R5).
|
|
1190
|
+
*
|
|
1191
|
+
* @internal
|
|
1192
|
+
*/
|
|
1193
|
+
const VacuousPropertyRun = Schema.Struct({
|
|
1194
|
+
property: PropertyRun,
|
|
1195
|
+
frozen: Schema.Union([Frozen, NeverCalled])
|
|
1196
|
+
});
|
|
1197
|
+
/**
|
|
1198
|
+
* One vacuous subject: what it is, and every non-refuting property in the file that ran over it (R5).
|
|
1199
|
+
*
|
|
1200
|
+
* @internal
|
|
1201
|
+
*/
|
|
1202
|
+
const VacuousSubject = Schema.Struct({
|
|
1203
|
+
label: Schema.String,
|
|
1204
|
+
properties: Schema.Array(VacuousPropertyRun)
|
|
1205
|
+
});
|
|
1206
|
+
/**
|
|
1207
|
+
* A law kind exempt from the impostor gate in a refused file: its name and the kind that exempted it (R6).
|
|
1208
|
+
*
|
|
1209
|
+
* @internal
|
|
1210
|
+
*/
|
|
1211
|
+
const ExemptLaw = Schema.Struct({
|
|
1212
|
+
name: Schema.String,
|
|
1213
|
+
kind: Schema.String
|
|
1214
|
+
});
|
|
1215
|
+
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.";
|
|
1216
|
+
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);
|
|
1217
|
+
const subjectMessageOf = (subject) => {
|
|
1218
|
+
const names = subject.properties.map((run) => run.property.name).join(", ");
|
|
1219
|
+
const runs = subject.properties.reduce((total, run) => total + run.property.runs, 0);
|
|
1220
|
+
const fakes = subject.properties.flatMap(fakeTextOf);
|
|
1221
|
+
const fake = fakes.length === 0 ? "was never called" : fakes.join("; ");
|
|
1222
|
+
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}`;
|
|
1223
|
+
};
|
|
1224
|
+
/**
|
|
1225
|
+
* Thrown at file end when no property in the file refuted a subject's constant impostor (R12, R5, R6).
|
|
1226
|
+
*
|
|
1227
|
+
* @internal
|
|
1228
|
+
*/
|
|
1229
|
+
var VacuousProperty = class extends Schema.TaggedError()("VacuousProperty", {
|
|
1230
|
+
subjects: Schema.Array(VacuousSubject),
|
|
1231
|
+
exempt: Schema.Array(ExemptLaw),
|
|
1232
|
+
replay: Schema.String
|
|
1233
|
+
}) {
|
|
1234
|
+
get message() {
|
|
1235
|
+
return this.subjects.map(subjectMessageOf).join("\n\n");
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
/** @internal */
|
|
1239
|
+
var ReplayUnreadable = class extends Schema.TaggedError()("ReplayUnreadable", { text: Schema.String }) {
|
|
1240
|
+
get message() {
|
|
1241
|
+
return `CONFORMANCE_REPLAY names neither a seed and path nor a property entry: ${this.text}`;
|
|
816
1242
|
}
|
|
817
1243
|
};
|
|
818
1244
|
/**
|
|
@@ -820,19 +1246,82 @@ var VacuousProperty = class extends Schema$1.TaggedError()("VacuousProperty", {
|
|
|
820
1246
|
*
|
|
821
1247
|
* @internal
|
|
822
1248
|
*/
|
|
823
|
-
var NonTupleOf = class extends Schema
|
|
1249
|
+
var NonTupleOf = class extends Schema.TaggedError()("NonTupleOf", { detail: Schema.String }) {
|
|
824
1250
|
get message() {
|
|
825
1251
|
return this.detail;
|
|
826
1252
|
}
|
|
827
1253
|
};
|
|
828
1254
|
//#endregion
|
|
1255
|
+
//#region src/internal/property/replay.schema.ts
|
|
1256
|
+
/**
|
|
1257
|
+
* The token effect's falsification runner records, as the Schema that decodes it (KTD5). One falsification's
|
|
1258
|
+
* coordinates ride in a JSON array: a leading `0` naming a numeric seed, the seed text, the attempt, the size,
|
|
1259
|
+
* the shrink path, and the failure tag.
|
|
1260
|
+
*
|
|
1261
|
+
* @since 4.0.0
|
|
1262
|
+
*/
|
|
1263
|
+
/** @internal */
|
|
1264
|
+
const ReplayToken = Schema$1.fromJsonString(Schema$1.Tuple([
|
|
1265
|
+
Schema$1.Finite,
|
|
1266
|
+
Schema$1.Union([Schema$1.Finite, Schema$1.String]),
|
|
1267
|
+
Schema$1.Finite,
|
|
1268
|
+
Schema$1.Finite,
|
|
1269
|
+
Schema$1.Array(Schema$1.Finite),
|
|
1270
|
+
Schema$1.String
|
|
1271
|
+
]));
|
|
1272
|
+
//#endregion
|
|
1273
|
+
//#region src/internal/property/replay.ts
|
|
1274
|
+
/**
|
|
1275
|
+
* The property replay channel (KTD5, R9-R12): the text a property failure carries, the generator replay token a
|
|
1276
|
+
* refuted entry rebuilds, and the entry the engine selects for a property's identity hash.
|
|
1277
|
+
*
|
|
1278
|
+
* The token `effect`'s falsification runner writes is decoded here through its codec — never hand-parsed — into the
|
|
1279
|
+
* parts a refuted entry names, and rebuilt from them the way `makeReplay` reads them back.
|
|
1280
|
+
*
|
|
1281
|
+
* @since 4.0.0
|
|
1282
|
+
*/
|
|
1283
|
+
const NUMERIC_SEED = 0;
|
|
1284
|
+
/** @internal */
|
|
1285
|
+
const tokenOfReplay = (entry) => JSON.stringify([
|
|
1286
|
+
NUMERIC_SEED,
|
|
1287
|
+
`${entry.seed}`,
|
|
1288
|
+
entry.attempt,
|
|
1289
|
+
entry.size,
|
|
1290
|
+
entry.path,
|
|
1291
|
+
entry.failure
|
|
1292
|
+
]);
|
|
1293
|
+
/** @internal */
|
|
1294
|
+
const refutedReplayTextOf = (identity) => Option$1.getOrUndefined(Option$1.map(Schema$1.decodeOption(ReplayToken)(identity.token), (decoded) => refutedReplayText$1({
|
|
1295
|
+
property: identity.property,
|
|
1296
|
+
seed: identity.seed,
|
|
1297
|
+
runs: identity.runs,
|
|
1298
|
+
attempt: decoded[2],
|
|
1299
|
+
size: decoded[3],
|
|
1300
|
+
path: decoded[4],
|
|
1301
|
+
failure: decoded[5]
|
|
1302
|
+
})));
|
|
1303
|
+
/** @internal */
|
|
1304
|
+
const plainReplayTextOf = (identity) => plainReplayText(identity);
|
|
1305
|
+
/** @internal */
|
|
1306
|
+
const plainReplayEntriesText = (entries) => plainReplayTexts(entries);
|
|
1307
|
+
const isEntries = (channel) => Array.isArray(channel);
|
|
1308
|
+
/** @internal */
|
|
1309
|
+
const selectReplayEntry = (selection) => isEntries(selection.channel) ? selection.channel.find((entry) => entry.property === selection.hash) : void 0;
|
|
1310
|
+
//#endregion
|
|
829
1311
|
//#region src/internal/property/impostor.ts
|
|
1312
|
+
/**
|
|
1313
|
+
* @internal The constant impostor of R12: a fake of the subject under test that
|
|
1314
|
+
* returns the first output it ever produced, for every later call, whatever the
|
|
1315
|
+
* input it was given.
|
|
1316
|
+
*
|
|
1317
|
+
* A property that still holds for the impostor does not constrain the subject.
|
|
1318
|
+
* Whether that is fatal is a per-file question — a subject's impostor must be
|
|
1319
|
+
* refuted by some property in its file — so this module builds the fake and
|
|
1320
|
+
* collects the verdicts; the engine decides them once, when the file finishes
|
|
1321
|
+
* (KTD10).
|
|
1322
|
+
*/
|
|
830
1323
|
const SUBJECT_KEY = "the subject";
|
|
831
|
-
const NO_VALUE = "";
|
|
832
|
-
const isCallable = (value) => typeof value === "function";
|
|
833
1324
|
const isCallableSubject = (target) => typeof target === "function";
|
|
834
|
-
const describeValue = (value) => isCallable(value) ? "a function" : String(value);
|
|
835
|
-
const describeEntry = (key, value) => `${key} always returned ${describeValue(value)}`;
|
|
836
1325
|
const emptyState = () => ({ outputs: /* @__PURE__ */ new Map() });
|
|
837
1326
|
const frozenAt = (state, key, compute) => state.outputs.has(key) ? state.outputs.get(key) : remember(state, key, compute());
|
|
838
1327
|
const remember = (state, key, value) => {
|
|
@@ -850,7 +1339,10 @@ const impostorHandler = (state) => ({
|
|
|
850
1339
|
apply: (target, thisArg, args) => appliedImpostor(state, target, thisArg, args),
|
|
851
1340
|
get: (target, property) => readImpostor(state, target, property)
|
|
852
1341
|
});
|
|
853
|
-
const
|
|
1342
|
+
const frozenOutputs = (state) => [...state.outputs].map(([member, output]) => ({
|
|
1343
|
+
member,
|
|
1344
|
+
output
|
|
1345
|
+
}));
|
|
854
1346
|
/**
|
|
855
1347
|
* A constant impostor of `subject`: a function returns its first output forever, and so does each member
|
|
856
1348
|
* of a record.
|
|
@@ -861,48 +1353,75 @@ const impostorOf = (subject) => {
|
|
|
861
1353
|
const state = emptyState();
|
|
862
1354
|
return {
|
|
863
1355
|
impostor: new Proxy(subject, impostorHandler(state)),
|
|
864
|
-
frozen: () =>
|
|
1356
|
+
frozen: () => frozenOutputs(state)
|
|
865
1357
|
};
|
|
866
1358
|
};
|
|
867
1359
|
/** @internal */
|
|
868
1360
|
const subjectLabel = (subject) => isCallableSubject(subject) ? `a function of ${String(subject.length)} argument(s)` : `a record of ${Object.keys(subject).sort().join(", ")}`;
|
|
869
1361
|
const judgeFor = (subject) => ({
|
|
870
1362
|
label: subjectLabel(subject),
|
|
871
|
-
|
|
872
|
-
refuters: []
|
|
873
|
-
frozen: NO_VALUE,
|
|
874
|
-
runs: 0
|
|
1363
|
+
verdicts: [],
|
|
1364
|
+
refuters: []
|
|
875
1365
|
});
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
runs: verdict.runs
|
|
1366
|
+
const addName = (names, name) => names.includes(name) ? names : [...names, name];
|
|
1367
|
+
const trackVerdict = (judge, verdict) => {
|
|
1368
|
+
const named = {
|
|
1369
|
+
...judge,
|
|
1370
|
+
refuters: addName(judge.refuters, verdict.property.name)
|
|
1371
|
+
};
|
|
1372
|
+
return verdict.refuted ? named : {
|
|
1373
|
+
...judge,
|
|
1374
|
+
verdicts: [...judge.verdicts, {
|
|
1375
|
+
property: verdict.property,
|
|
1376
|
+
identity: verdict.identity,
|
|
1377
|
+
frozen: verdict.frozen
|
|
1378
|
+
}]
|
|
890
1379
|
};
|
|
891
1380
|
};
|
|
892
|
-
const recordJudge = (state, subject,
|
|
1381
|
+
const recordJudge = (state, subject, verdict) => {
|
|
893
1382
|
const prior = state.judges.get(subject) ?? judgeFor(subject);
|
|
894
|
-
state.judges.set(subject, trackVerdict(prior,
|
|
1383
|
+
state.judges.set(subject, trackVerdict(prior, verdict));
|
|
895
1384
|
};
|
|
896
1385
|
const isVacuous = (judge) => judge.refuters.length === 0;
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1386
|
+
const frozenOf = (frozen) => frozen.length === 0 ? { _tag: "NeverCalled" } : {
|
|
1387
|
+
_tag: "Frozen",
|
|
1388
|
+
outputs: frozen.map(({ member, output }) => ({
|
|
1389
|
+
member,
|
|
1390
|
+
output: witnessOf(output)
|
|
1391
|
+
}))
|
|
1392
|
+
};
|
|
1393
|
+
const propertyRunOf$1 = (verdict) => ({
|
|
1394
|
+
property: verdict.property,
|
|
1395
|
+
frozen: frozenOf(verdict.frozen)
|
|
1396
|
+
});
|
|
1397
|
+
const subjectOf = (judge) => ({
|
|
1398
|
+
label: judge.label,
|
|
1399
|
+
properties: judge.verdicts.map(propertyRunOf$1)
|
|
1400
|
+
});
|
|
1401
|
+
const vacuousReplayTextOf = (judges) => plainReplayEntriesText(judges.flatMap((judge) => judge.verdicts.map((verdict) => ({
|
|
1402
|
+
property: verdict.identity,
|
|
1403
|
+
seed: verdict.property.seed,
|
|
1404
|
+
runs: verdict.property.runs
|
|
1405
|
+
}))));
|
|
1406
|
+
const refuseVacuous = (judges, exempt) => {
|
|
1407
|
+
const vacuous = judges.filter(isVacuous);
|
|
1408
|
+
return vacuous.length === 0 ? void 0 : new VacuousProperty({
|
|
1409
|
+
subjects: vacuous.map(subjectOf),
|
|
1410
|
+
exempt: [...exempt],
|
|
1411
|
+
replay: vacuousReplayTextOf(vacuous)
|
|
1412
|
+
});
|
|
1413
|
+
};
|
|
1414
|
+
const finaliseJudges = (state) => refuseVacuous([...state.judges.values()], state.exempts);
|
|
1415
|
+
const ledgerOf = (state) => ({
|
|
1416
|
+
record: (subject, verdict) => recordJudge(state, subject, verdict),
|
|
1417
|
+
recordExempt: (name, kind) => {
|
|
1418
|
+
if (state.exempts.some((exempt) => exempt.name === name)) return;
|
|
1419
|
+
state.exempts.push({
|
|
1420
|
+
name,
|
|
1421
|
+
kind
|
|
1422
|
+
});
|
|
1423
|
+
},
|
|
1424
|
+
finalise: () => finaliseJudges(state)
|
|
906
1425
|
});
|
|
907
1426
|
/**
|
|
908
1427
|
* Collects the impostor verdicts of every property in the file and decides, at file end, which subjects
|
|
@@ -910,7 +1429,10 @@ const ledgerOf = (state, onProblems) => ({
|
|
|
910
1429
|
*
|
|
911
1430
|
* @internal
|
|
912
1431
|
*/
|
|
913
|
-
const makeFileLedger = (
|
|
1432
|
+
const makeFileLedger = () => ledgerOf({
|
|
1433
|
+
judges: /* @__PURE__ */ new Map(),
|
|
1434
|
+
exempts: []
|
|
1435
|
+
});
|
|
914
1436
|
//#endregion
|
|
915
1437
|
//#region src/internal/property/kinds.ts
|
|
916
1438
|
/**
|
|
@@ -947,46 +1469,140 @@ const idempotentHolds = () => (subject, values) => sameTwice(subject, firstOf(sp
|
|
|
947
1469
|
/** @internal */
|
|
948
1470
|
const deterministicHolds = () => (subject, values) => Equal.equals(call(subject, values), call(subject, values));
|
|
949
1471
|
//#endregion
|
|
950
|
-
//#region src/internal/property/
|
|
1472
|
+
//#region src/internal/property/seed-store.schema.ts
|
|
951
1473
|
/**
|
|
952
|
-
* The
|
|
953
|
-
*
|
|
954
|
-
* the
|
|
1474
|
+
* @internal The seed store's line grammar (KTD6, R16, R18): one JSON Lines entry per failing run, decoded as a
|
|
1475
|
+
* tagged union. A refuted run carries the property's full name plus the parts a generator replay token is rebuilt
|
|
1476
|
+
* from; a non-boolean run carries the name, the seed and the run count. The codec a line is read through lives
|
|
1477
|
+
* here; the decisions over an entry live in `seed-record.ts`.
|
|
955
1478
|
*
|
|
956
1479
|
* @since 4.0.0
|
|
957
1480
|
*/
|
|
958
1481
|
/** @internal */
|
|
959
|
-
const
|
|
960
|
-
Schema$1.
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
Schema$1.Array(
|
|
965
|
-
|
|
966
|
-
|
|
1482
|
+
const SeedStoreRefuted = Schema$1.TaggedStruct("Refuted", {
|
|
1483
|
+
property: Schema$1.String,
|
|
1484
|
+
seed: PropertySeed,
|
|
1485
|
+
attempt: PropertyRunCount,
|
|
1486
|
+
size: PropertyRunCount,
|
|
1487
|
+
path: Schema$1.Array(ReplayStep),
|
|
1488
|
+
failure: ReplayFailureTag
|
|
1489
|
+
});
|
|
1490
|
+
/** @internal */
|
|
1491
|
+
const SeedStoreNonBoolean = Schema$1.TaggedStruct("NonBoolean", {
|
|
1492
|
+
property: Schema$1.String,
|
|
1493
|
+
seed: PropertySeed,
|
|
1494
|
+
runs: PropertyRunCount
|
|
1495
|
+
});
|
|
1496
|
+
/** @internal */
|
|
1497
|
+
const SeedStoreEntry = Schema$1.Union([SeedStoreRefuted, SeedStoreNonBoolean]);
|
|
1498
|
+
/** @internal */
|
|
1499
|
+
const SeedStoreLine = Schema$1.fromJsonString(SeedStoreEntry);
|
|
967
1500
|
//#endregion
|
|
968
|
-
//#region src/internal/property/
|
|
1501
|
+
//#region src/internal/property/seed-record.ts
|
|
969
1502
|
/**
|
|
970
|
-
* The
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
* The token `effect`'s falsification runner writes is decoded here — never hand-parsed — into the seed and path R6
|
|
974
|
-
* prints.
|
|
1503
|
+
* @internal The seed store's pure decisions (KTD6, R16-R19): the codec a line is read through, the entries a
|
|
1504
|
+
* property claims, whether a failure is worth appending, and the replay a stored entry names. No file access
|
|
1505
|
+
* lives here — the shell `seed-store.ts` reads and appends lines, and the line grammar is `seed-store.schema.ts`.
|
|
975
1506
|
*
|
|
976
1507
|
* @since 4.0.0
|
|
977
1508
|
*/
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
seed,
|
|
983
|
-
path: token[4]
|
|
1509
|
+
const decodeLine = (file, index, text) => Result.mapError(Schema$1.decodeResult(SeedStoreLine)(text), (error) => new SeedStoreUnreadable({
|
|
1510
|
+
file,
|
|
1511
|
+
line: index + 1,
|
|
1512
|
+
detail: error.message
|
|
984
1513
|
}));
|
|
985
1514
|
/** @internal */
|
|
986
|
-
const
|
|
987
|
-
const refutationOf = (value) => Schema$1.is(PropertyRefuted)(value) ? value : void 0;
|
|
1515
|
+
const decodeStoreLines = Function.dual(2, (file, lines) => Result.all(lines.map((text, index) => decodeLine(file, index, text))));
|
|
988
1516
|
/** @internal */
|
|
989
|
-
const
|
|
1517
|
+
const entriesForProperty = Function.dual(2, (entries, property) => entries.filter((entry) => entry.property === property));
|
|
1518
|
+
const sameEntry = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
1519
|
+
const derandomized = (budget) => budget?.seed !== void 0;
|
|
1520
|
+
const recordingOptedOut = (budget) => budget?.record === false;
|
|
1521
|
+
const alreadyRecorded = (existing, candidate) => existing.some((entry) => sameEntry(entry, candidate));
|
|
1522
|
+
const not = (value) => value === false;
|
|
1523
|
+
/** @internal */
|
|
1524
|
+
const shouldAppendEntry = (input) => [
|
|
1525
|
+
derandomized(input.budget),
|
|
1526
|
+
recordingOptedOut(input.budget),
|
|
1527
|
+
alreadyRecorded(input.existing, input.candidate)
|
|
1528
|
+
].every(not);
|
|
1529
|
+
/** @internal */
|
|
1530
|
+
const refutedEntryOf = (input) => Option$1.flatMap(Schema$1.decodeOption(ReplayToken)(input.token), (parts) => Schema$1.decodeUnknownOption(SeedStoreEntry)({
|
|
1531
|
+
_tag: "Refuted",
|
|
1532
|
+
property: input.property,
|
|
1533
|
+
seed: input.seed,
|
|
1534
|
+
attempt: parts[2],
|
|
1535
|
+
size: parts[3],
|
|
1536
|
+
path: parts[4],
|
|
1537
|
+
failure: parts[5]
|
|
1538
|
+
}));
|
|
1539
|
+
/** @internal */
|
|
1540
|
+
const nonBooleanEntryOf = (input) => Schema$1.decodeOption(SeedStoreEntry)({
|
|
1541
|
+
_tag: "NonBoolean",
|
|
1542
|
+
property: input.property,
|
|
1543
|
+
seed: input.seed,
|
|
1544
|
+
runs: input.runs
|
|
1545
|
+
});
|
|
1546
|
+
/** @internal */
|
|
1547
|
+
const replayEntryOf$1 = Function.dual(2, (entry, hash) => Match$1.value(entry).pipe(Match$1.tag("Refuted", (refuted) => Schema$1.decodeOption(RefutedPropertyReplay)({
|
|
1548
|
+
_tag: "Refuted",
|
|
1549
|
+
property: hash,
|
|
1550
|
+
seed: refuted.seed,
|
|
1551
|
+
runs: 1,
|
|
1552
|
+
attempt: refuted.attempt,
|
|
1553
|
+
size: refuted.size,
|
|
1554
|
+
path: refuted.path,
|
|
1555
|
+
failure: refuted.failure
|
|
1556
|
+
})), Match$1.tag("NonBoolean", (plain) => Schema$1.decodeOption(PlainPropertyReplay)({
|
|
1557
|
+
_tag: "Plain",
|
|
1558
|
+
property: hash,
|
|
1559
|
+
seed: plain.seed,
|
|
1560
|
+
runs: plain.runs
|
|
1561
|
+
})), Match$1.exhaustive));
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region src/internal/property/seed-store.ts
|
|
1564
|
+
/**
|
|
1565
|
+
* @internal The seed store's shell (KTD6, R16): the asynchronous file access the engine uses — read a test file's
|
|
1566
|
+
* store lines beside it, and append one failing entry's line. The line grammar is `seed-store.schema.ts` and the
|
|
1567
|
+
* decisions are `seed-record.ts`; nothing reads or writes a file synchronously here.
|
|
1568
|
+
*
|
|
1569
|
+
* @since 4.0.0
|
|
1570
|
+
*/
|
|
1571
|
+
const STORE_DIRECTORY = "__property_seeds__";
|
|
1572
|
+
/** @internal */
|
|
1573
|
+
const storeFileOf = (filepath) => Effect$1.map(Effect$1.service(Path.Path), (path) => path.join(path.dirname(filepath), STORE_DIRECTORY, `${path.basename(filepath)}.jsonl`));
|
|
1574
|
+
const linesOf = (text) => text.length === 0 ? [] : text.replace(/\n$/u, "").split("\n");
|
|
1575
|
+
const readText = (file) => Effect$1.flatMap(Effect$1.service(FileSystem.FileSystem), (fs) => fs.readFileString(file).pipe(Effect$1.catchReason("PlatformError", "NotFound", () => Effect$1.succeed(""))));
|
|
1576
|
+
/** @internal */
|
|
1577
|
+
const readStoreLines = (file) => Effect$1.map(readText(file).pipe(Effect$1.orDie), linesOf);
|
|
1578
|
+
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))));
|
|
1579
|
+
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)));
|
|
1580
|
+
/** @internal */
|
|
1581
|
+
const appendStoreEntry = Function.dual(2, appendEntry);
|
|
1582
|
+
//#endregion
|
|
1583
|
+
//#region src/internal/property/seed.ts
|
|
1584
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
1585
|
+
const FNV_PRIME = 16777619;
|
|
1586
|
+
const utf8 = new TextEncoder();
|
|
1587
|
+
const PART_SEPARATOR = "\0";
|
|
1588
|
+
/** @internal */
|
|
1589
|
+
const fnv1a32 = (text) => utf8.encode(text).reduce((hash, byte) => Math.imul(hash ^ byte, FNV_PRIME), FNV_OFFSET_BASIS) >>> 0;
|
|
1590
|
+
const identityParts = (identity) => [
|
|
1591
|
+
identity.package,
|
|
1592
|
+
identity.file,
|
|
1593
|
+
identity.name
|
|
1594
|
+
];
|
|
1595
|
+
/** @internal */
|
|
1596
|
+
const identitySeed = Function.dual(2, (salt, identity) => fnv1a32([String(salt), ...identityParts(identity)].join(PART_SEPARATOR)));
|
|
1597
|
+
/** @internal */
|
|
1598
|
+
const identityHash = (identity) => fnv1a32(identityParts(identity).join(PART_SEPARATOR));
|
|
1599
|
+
/** @internal */
|
|
1600
|
+
const topUpSeed = Function.dual(2, (seed, drawn) => fnv1a32(`${seed}${PART_SEPARATOR}${drawn}`));
|
|
1601
|
+
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);
|
|
1602
|
+
const providedSeed = (input) => Option$1.map(Option$1.fromNullishOr(input.provided), identitySeed(input.identity));
|
|
1603
|
+
const saltOrProvided = (input) => Option$1.orElse(Option$1.map(Option$1.fromNullishOr(input.own), ownSeed), () => providedSeed(input));
|
|
1604
|
+
/** @internal */
|
|
1605
|
+
const resolveSeed = (input) => Option$1.getOrElse(saltOrProvided(input), () => input.fresh);
|
|
990
1606
|
//#endregion
|
|
991
1607
|
//#region src/internal/property/engine.ts
|
|
992
1608
|
/**
|
|
@@ -999,11 +1615,24 @@ const replayOfFailure = (value) => Option$1.getOrUndefined(Option$1.flatMap(Opti
|
|
|
999
1615
|
* receives the generated values exactly as drawn, typed by the gens that produced them.
|
|
1000
1616
|
*/
|
|
1001
1617
|
const REWRITE = "it.prop(name, { of, subject, runs }, holds)";
|
|
1618
|
+
const newViolations = () => ({
|
|
1619
|
+
kinds: [],
|
|
1620
|
+
drawn: void 0,
|
|
1621
|
+
seen: false
|
|
1622
|
+
});
|
|
1002
1623
|
const DEFAULT_RUNS = 100;
|
|
1003
1624
|
const isPositiveInteger = (runs) => Number.isInteger(runs) && runs > 0;
|
|
1004
1625
|
const describeRuns = (runs) => Number.isInteger(runs) ? `${String(runs)}` : "a non-integer";
|
|
1005
1626
|
const invalidPropertyBudget = (name, runs) => `${name}: pass a positive integer \`runs\`; received ${describeRuns(runs)}. Write ${REWRITE}.`;
|
|
1006
1627
|
const invalidProvidedBudget = (runs) => `the configured property budget (${checkDefaultsKey}) supplies \`runs\`: ${describeRuns(runs)}; it must be a positive integer.`;
|
|
1628
|
+
const invalidPropertySeed = (name, seed) => `${name}: pass a non-negative integer \`seed\`; received ${describeRuns(seed)}. Write ${REWRITE}.`;
|
|
1629
|
+
const isNonInteger = (value) => Number.isInteger(value) === false;
|
|
1630
|
+
const isNegative = (value) => value < 0;
|
|
1631
|
+
const isInvalidSeedNumber = (value) => isNonInteger(value) || isNegative(value);
|
|
1632
|
+
const isInvalidSeed = (seed) => typeof seed === "number" && isInvalidSeedNumber(seed);
|
|
1633
|
+
const requireOwnSeed = (name, seed) => {
|
|
1634
|
+
if (isInvalidSeed(seed)) throw new InvalidBudget({ detail: invalidPropertySeed(name, seed) });
|
|
1635
|
+
};
|
|
1007
1636
|
const requirePositiveRuns = (message, runs) => {
|
|
1008
1637
|
if (isInvalidRuns(runs)) throw new InvalidBudget({ detail: message(runs) });
|
|
1009
1638
|
};
|
|
@@ -1022,15 +1651,30 @@ const resolvedRuns = (options) => options.runs ?? DEFAULT_RUNS;
|
|
|
1022
1651
|
/**
|
|
1023
1652
|
* The effective check options: the property's own fields over the configured default over `runs: 100`,
|
|
1024
1653
|
* merged field by field so a property that sets only `runs` still inherits the configured size and caps.
|
|
1654
|
+
* The resolved seed replaces the provided and explicit `seed` fields, which are inputs to the derivation
|
|
1655
|
+
* rather than a shared stream (KTD4).
|
|
1025
1656
|
*/
|
|
1026
|
-
const
|
|
1657
|
+
const EMPTY_PROVIDED_BUDGET = {};
|
|
1658
|
+
const resolveBudget = (name, spec, task, fresh) => {
|
|
1027
1659
|
const explicit = explicitOptions(spec);
|
|
1660
|
+
const provided = task.budget ?? EMPTY_PROVIDED_BUDGET;
|
|
1028
1661
|
requirePositiveRuns((runs) => invalidPropertyBudget(name, runs), explicit.runs);
|
|
1029
|
-
requirePositiveRuns(invalidProvidedBudget, provided
|
|
1030
|
-
|
|
1662
|
+
requirePositiveRuns(invalidProvidedBudget, provided.runs);
|
|
1663
|
+
requireOwnSeed(name, explicit.seed);
|
|
1664
|
+
const options = mergedOptions(task.budget, explicit);
|
|
1665
|
+
const seed = resolveSeed({
|
|
1666
|
+
own: explicit.seed,
|
|
1667
|
+
provided: provided.seed,
|
|
1668
|
+
identity: task.identity,
|
|
1669
|
+
fresh
|
|
1670
|
+
});
|
|
1031
1671
|
return {
|
|
1032
1672
|
runs: resolvedRuns(options),
|
|
1033
|
-
options
|
|
1673
|
+
options: {
|
|
1674
|
+
...options,
|
|
1675
|
+
seed
|
|
1676
|
+
},
|
|
1677
|
+
seed
|
|
1034
1678
|
};
|
|
1035
1679
|
};
|
|
1036
1680
|
const toArbitrary = (input) => Schema$1.isSchema(input) ? Arbitrary.schema(input) : input;
|
|
@@ -1061,12 +1705,21 @@ const observeRun = (classes, counter, entries, values) => {
|
|
|
1061
1705
|
counter.runs = counter.runs + 1;
|
|
1062
1706
|
for (const label of satisfiedLabels(entries, values)) countHit(classes, label);
|
|
1063
1707
|
};
|
|
1064
|
-
const drawFor = (entries, arbitrary
|
|
1708
|
+
const drawFor = (entries, arbitrary, seed) => {
|
|
1709
|
+
const drawn = Ref.makeUnsafe(0);
|
|
1710
|
+
return { more: (count) => {
|
|
1711
|
+
const soFar = Effect$1.runSync(Ref.getAndUpdate(drawn, (total) => total + count));
|
|
1712
|
+
return Effect$1.runSync(Arbitrary.sampleEffect(arbitrary, {
|
|
1713
|
+
count,
|
|
1714
|
+
seed: topUpSeed(seed, soFar)
|
|
1715
|
+
}).pipe(Effect$1.orDie)).map((values) => satisfiedLabels(entries, values));
|
|
1716
|
+
} };
|
|
1717
|
+
};
|
|
1065
1718
|
const noCoverage = () => ({
|
|
1066
1719
|
observe: () => void 0,
|
|
1067
|
-
judge: () =>
|
|
1720
|
+
judge: () => []
|
|
1068
1721
|
});
|
|
1069
|
-
const liveCoverage = (cover, arbitrary) => {
|
|
1722
|
+
const liveCoverage = (cover, arbitrary, seed) => {
|
|
1070
1723
|
const entries = Object.entries(cover);
|
|
1071
1724
|
const classes = seedClasses(entries);
|
|
1072
1725
|
const counter = { runs: 0 };
|
|
@@ -1075,78 +1728,206 @@ const liveCoverage = (cover, arbitrary) => {
|
|
|
1075
1728
|
judge: () => judgeCoverage({
|
|
1076
1729
|
classes: Object.fromEntries(classes),
|
|
1077
1730
|
runs: counter.runs,
|
|
1078
|
-
draw: drawFor(entries, arbitrary)
|
|
1731
|
+
draw: drawFor(entries, arbitrary, seed)
|
|
1079
1732
|
})
|
|
1080
1733
|
};
|
|
1081
1734
|
};
|
|
1082
|
-
const makeCoverage = (cover, arbitrary) => cover === void 0 ? noCoverage() : liveCoverage(cover, arbitrary);
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1085
|
-
violations.
|
|
1735
|
+
const makeCoverage = (cover, arbitrary, seed) => cover === void 0 ? noCoverage() : liveCoverage(cover, arbitrary, seed);
|
|
1736
|
+
const verdictKindOf = (verdict) => Match$1.value(verdict).pipe(Match$1.when(Effect$1.isEffect, () => "effect"), Match$1.orElse((value) => typeof value));
|
|
1737
|
+
const noteKind = (violations, kind) => {
|
|
1738
|
+
if (violations.kinds.includes(kind) === false) violations.kinds.push(kind);
|
|
1739
|
+
};
|
|
1740
|
+
const noteDrawn = (violations, values) => {
|
|
1741
|
+
if (violations.seen === false) {
|
|
1742
|
+
violations.seen = true;
|
|
1743
|
+
violations.drawn = values;
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
const noteVerdict = (violations, verdict, values) => {
|
|
1747
|
+
noteKind(violations, verdictKindOf(verdict));
|
|
1748
|
+
noteDrawn(violations, values);
|
|
1086
1749
|
return true;
|
|
1087
1750
|
};
|
|
1088
|
-
const literalVerdict = (verdict, violations) => typeof verdict === "boolean" ? verdict :
|
|
1089
|
-
const effectVerdict = (lane, verdict, violations) => lane === "sync" ? Effect$1.succeed(
|
|
1751
|
+
const literalVerdict = (verdict, violations, values) => typeof verdict === "boolean" ? verdict : noteVerdict(violations, verdict, values);
|
|
1752
|
+
const effectVerdict = (lane, verdict, violations, values) => lane === "sync" ? Effect$1.succeed(noteVerdict(violations, verdict, values)) : Effect$1.map(verdict, (value) => literalVerdict(value, violations, values));
|
|
1090
1753
|
const verdictFor = (run, values, violations) => {
|
|
1091
1754
|
run.observe(values);
|
|
1092
1755
|
const verdict = run.holds(run.subject, values);
|
|
1093
|
-
return Effect$1.isEffect(verdict) ? effectVerdict(run.lane, verdict, violations) : Effect$1.succeed(literalVerdict(verdict, violations));
|
|
1756
|
+
return Effect$1.isEffect(verdict) ? effectVerdict(run.lane, verdict, violations, values) : Effect$1.succeed(literalVerdict(verdict, violations, values));
|
|
1094
1757
|
};
|
|
1095
1758
|
const tolerateInterruption = (cause) => Cause.hasInterrupts(cause) ? Effect$1.interrupt : Effect$1.fail(cause);
|
|
1096
1759
|
const guardedVerdict = (run, violations) => (values) => Effect$1.catchCause(Effect$1.suspend(() => verdictFor(run, values, violations)), tolerateInterruption);
|
|
1097
|
-
const violationMessage = (name, violations) => `${name}: the property returned no boolean; it returned ${violations.join(", ")}. A property must return a literal boolean verdict — asserting with expect(), or returning an Option, Result or object, is not a verdict; write ${REWRITE}.`;
|
|
1098
1760
|
const runCheck = (run) => {
|
|
1099
|
-
const violations =
|
|
1761
|
+
const violations = newViolations();
|
|
1100
1762
|
return Arbitrary.checkEffect(run.arbitrary, guardedVerdict(run, violations), run.options).pipe(Effect$1.map((result) => ({
|
|
1101
1763
|
violations,
|
|
1102
1764
|
result
|
|
1103
1765
|
})));
|
|
1104
1766
|
};
|
|
1105
1767
|
const reportOf = (checked) => Arbitrary.formatCheckFailure(checked.result);
|
|
1106
|
-
const
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1768
|
+
const seeded = (seed) => Option$1.getOrThrow(Schema$1.decodeOption(PropertySeed)(seed));
|
|
1769
|
+
const runCounted = (runs) => Option$1.getOrThrow(Schema$1.decodeOption(PropertyRunCount)(runs));
|
|
1770
|
+
const propertyRunOf = (run, budget) => ({
|
|
1771
|
+
name: run.name,
|
|
1772
|
+
site: run.site ?? null,
|
|
1773
|
+
seed: seeded(budget.seed),
|
|
1774
|
+
runs: runCounted(budget.runs)
|
|
1775
|
+
});
|
|
1776
|
+
const REPLAY_VARIABLE = "CONFORMANCE_REPLAY";
|
|
1777
|
+
const replayEntryOf = (text, hash, site) => Option$1.match(text, {
|
|
1778
|
+
onNone: () => Effect$1.succeed(Option$1.none()),
|
|
1779
|
+
onSome: (value) => Option$1.match(Schema$1.decodeOption(ReplayChannelFromText)(value), {
|
|
1780
|
+
onNone: () => dieWithSite(new ReplayUnreadable({ text: value }), site),
|
|
1781
|
+
onSome: (channel) => Effect$1.succeed(Option$1.fromNullishOr(selectReplayEntry({
|
|
1782
|
+
channel,
|
|
1783
|
+
hash
|
|
1784
|
+
})))
|
|
1785
|
+
})
|
|
1786
|
+
});
|
|
1787
|
+
const replayedBudget = (budget, runs, seed, replay) => {
|
|
1788
|
+
return {
|
|
1789
|
+
runs,
|
|
1790
|
+
seed,
|
|
1791
|
+
options: replay === void 0 ? {
|
|
1792
|
+
...budget.options,
|
|
1793
|
+
runs,
|
|
1794
|
+
seed
|
|
1795
|
+
} : {
|
|
1796
|
+
...budget.options,
|
|
1797
|
+
runs,
|
|
1798
|
+
seed,
|
|
1799
|
+
replay
|
|
1800
|
+
}
|
|
1124
1801
|
};
|
|
1125
|
-
gatedLedger.record(run.subject, run.name, verdict);
|
|
1126
1802
|
};
|
|
1127
|
-
const
|
|
1803
|
+
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);
|
|
1804
|
+
const plainFailureReplay = (run, budget) => plainReplayTextOf({
|
|
1805
|
+
property: run.identityHash,
|
|
1806
|
+
seed: budget.seed,
|
|
1807
|
+
runs: budget.runs
|
|
1808
|
+
});
|
|
1809
|
+
const falsifiedReplayOf = (checked) => {
|
|
1810
|
+
const falsified = falsifiedOf(checked.result);
|
|
1811
|
+
return falsified === void 0 ? void 0 : falsified.replay;
|
|
1812
|
+
};
|
|
1813
|
+
const refutedReplayText = (run, budget, token) => Option$1.getOrElse(Option$1.fromUndefinedOr(refutedReplayTextOf({
|
|
1814
|
+
property: run.identityHash,
|
|
1815
|
+
seed: budget.seed,
|
|
1816
|
+
runs: budget.runs,
|
|
1817
|
+
token
|
|
1818
|
+
})), () => plainFailureReplay(run, budget));
|
|
1819
|
+
const refutedFailureReplay = (run, budget, checked) => {
|
|
1820
|
+
const token = falsifiedReplayOf(checked);
|
|
1821
|
+
return token === void 0 ? plainFailureReplay(run, budget) : refutedReplayText(run, budget, token);
|
|
1822
|
+
};
|
|
1823
|
+
const violationOf = (run, budget, checked) => checked.violations.kinds.length === 0 ? void 0 : new NonBooleanVerdict({
|
|
1824
|
+
property: propertyRunOf(run, budget),
|
|
1825
|
+
drawn: witnessOf(checked.violations.drawn),
|
|
1826
|
+
returned: [...checked.violations.kinds],
|
|
1827
|
+
replay: plainFailureReplay(run, budget)
|
|
1828
|
+
});
|
|
1829
|
+
const isRefuted = (checked) => checked.violations.kinds.length > 0 || reportOf(checked) !== void 0;
|
|
1830
|
+
const dieWithSite = (error, site) => Effect$1.die(withRaisingFrame(error, site));
|
|
1831
|
+
const shrinkCounted = (shrinks) => Option$1.getOrThrow(Schema$1.decodeOption(PropertyShrinkCount)(shrinks));
|
|
1832
|
+
const NO_FALSIFICATION = {
|
|
1833
|
+
counterexample: witnessOf(void 0),
|
|
1834
|
+
shrinks: shrinkCounted(0)
|
|
1835
|
+
};
|
|
1836
|
+
const falsificationOf = (falsified) => falsified === void 0 ? NO_FALSIFICATION : {
|
|
1837
|
+
counterexample: witnessOf(falsified.shrunkInput),
|
|
1838
|
+
shrinks: shrinkCounted(falsified.shrinks)
|
|
1839
|
+
};
|
|
1840
|
+
const refutedOf = (property, checked, replay) => new PropertyRefuted({
|
|
1841
|
+
property,
|
|
1842
|
+
...falsificationOf(falsifiedOf(checked.result)),
|
|
1843
|
+
replay
|
|
1844
|
+
});
|
|
1845
|
+
const dieReported = (run, budget, checked, site) => dieWithSite(refutedOf(propertyRunOf(run, budget), checked, refutedFailureReplay(run, budget, checked)), site);
|
|
1846
|
+
const dieUncovered = (run, budget, classes, site) => dieWithSite(new CoverageBelowMinimum({
|
|
1847
|
+
property: propertyRunOf(run, budget),
|
|
1848
|
+
classes,
|
|
1849
|
+
replay: plainFailureReplay(run, budget)
|
|
1850
|
+
}), site);
|
|
1851
|
+
const impostorHolds = (holds, impostor) => (_subject, values) => holds(impostor.impostor, values);
|
|
1852
|
+
const noObserve = () => void 0;
|
|
1853
|
+
const impostorRun = (run, propertyBudget, budgets) => {
|
|
1128
1854
|
const impostor = impostorOf(run.subject);
|
|
1129
1855
|
const retry = {
|
|
1130
1856
|
...run,
|
|
1131
1857
|
holds: impostorHolds(run.holds, impostor),
|
|
1132
1858
|
observe: noObserve
|
|
1133
1859
|
};
|
|
1134
|
-
return
|
|
1860
|
+
return Effect$1.map(Effect$1.forEach(budgets, (budget) => runCheck({
|
|
1861
|
+
...retry,
|
|
1862
|
+
options: budget.options,
|
|
1863
|
+
seed: budget.seed
|
|
1864
|
+
}), { concurrency: 1 }), (checked) => {
|
|
1865
|
+
const verdict = {
|
|
1866
|
+
refuted: checked.some(isRefuted),
|
|
1867
|
+
property: propertyRunOf(retry, propertyBudget),
|
|
1868
|
+
identity: run.identityHash,
|
|
1869
|
+
frozen: impostor.frozen()
|
|
1870
|
+
};
|
|
1871
|
+
run.ledger.record(run.subject, verdict);
|
|
1872
|
+
});
|
|
1135
1873
|
};
|
|
1136
|
-
const
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
return failure === void 0 ? gateRun(run, budget, gate) : dieUncovered(`${run.name}: ${failure}`, run.site);
|
|
1874
|
+
const gateRun = (run, budget, gate, budgets) => gate ? impostorRun(run, budget, budgets) : Effect$1.void;
|
|
1875
|
+
const finishPassed = (run, budget, coverage, gate, budgets) => {
|
|
1876
|
+
const failures = coverage.judge();
|
|
1877
|
+
return failures.length === 0 ? gateRun(run, budget, gate, budgets) : dieUncovered(run, budget, failures, run.site);
|
|
1141
1878
|
};
|
|
1142
1879
|
const falsifiedOf = (result) => "replay" in result ? result : void 0;
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1880
|
+
const noStore = (name) => ({
|
|
1881
|
+
filepath: null,
|
|
1882
|
+
name,
|
|
1883
|
+
existing: [],
|
|
1884
|
+
recorded: [],
|
|
1885
|
+
budget: void 0
|
|
1886
|
+
});
|
|
1887
|
+
const appendWhenRecorded = (store, candidate) => Option$1.match(Option$1.fromNullishOr(store.filepath), {
|
|
1888
|
+
onNone: () => Effect$1.void,
|
|
1889
|
+
onSome: (filepath) => shouldAppendEntry({
|
|
1890
|
+
existing: store.existing,
|
|
1891
|
+
candidate,
|
|
1892
|
+
budget: store.budget
|
|
1893
|
+
}) ? appendStoreEntry(filepath, candidate) : Effect$1.void
|
|
1894
|
+
});
|
|
1895
|
+
const appendThenDie = (store, candidate, die) => Effect$1.flatMap(Option$1.match(candidate, {
|
|
1896
|
+
onNone: () => Effect$1.void,
|
|
1897
|
+
onSome: (entry) => appendWhenRecorded(store, entry)
|
|
1898
|
+
}), () => die);
|
|
1899
|
+
const refutedCandidate = (store, budget, checked) => Option$1.flatMap(Option$1.fromNullishOr(falsifiedReplayOf(checked)), (token) => refutedEntryOf({
|
|
1900
|
+
property: store.name,
|
|
1901
|
+
seed: budget.seed,
|
|
1902
|
+
token
|
|
1903
|
+
}));
|
|
1904
|
+
const recordedReport = (registration, run, budget, checked) => reportOf(checked) === void 0 ? Effect$1.void : dieReported(run, budget, checked, registration.site);
|
|
1905
|
+
const settleRecorded = (registration, run, budget, checked) => Option$1.match(Option$1.fromNullishOr(violationOf(run, budget, checked)), {
|
|
1906
|
+
onNone: () => recordedReport(registration, run, budget, checked),
|
|
1907
|
+
onSome: (violation) => dieWithSite(violation, registration.site)
|
|
1908
|
+
});
|
|
1909
|
+
const settleRefuted = (registration, run, budget, coverage, checked, store) => reportOf(checked) === void 0 ? finishPassed(run, budget, coverage, registration.gate, [...store.recorded, budget]) : appendThenDie(store, refutedCandidate(store, budget, checked), dieReported(run, budget, checked, registration.site));
|
|
1910
|
+
const settle = (registration, run, budget, coverage, checked, store) => Option$1.match(Option$1.fromNullishOr(violationOf(run, budget, checked)), {
|
|
1911
|
+
onNone: () => settleRefuted(registration, run, budget, coverage, checked, store),
|
|
1912
|
+
onSome: (violation) => appendThenDie(store, nonBooleanEntryOf({
|
|
1913
|
+
property: store.name,
|
|
1914
|
+
seed: budget.seed,
|
|
1915
|
+
runs: budget.runs
|
|
1916
|
+
}), dieWithSite(violation, registration.site))
|
|
1917
|
+
});
|
|
1918
|
+
const readStoreEntries = (file, site) => Effect$1.flatMap(readStoreLines(file), (lines) => {
|
|
1919
|
+
const decoded = decodeStoreLines(file, lines);
|
|
1920
|
+
return Result.isSuccess(decoded) ? Effect$1.succeed(decoded.success) : dieWithSite(decoded.failure, site);
|
|
1921
|
+
});
|
|
1922
|
+
const readTaskStore = (task, site) => Option$1.match(Option$1.fromNullishOr(task.filepath), {
|
|
1923
|
+
onNone: () => Effect$1.succeed([]),
|
|
1924
|
+
onSome: (testFile) => Effect$1.flatMap(storeFileOf(testFile), (file) => readStoreEntries(file, site))
|
|
1925
|
+
});
|
|
1926
|
+
const recordedBudgets = (entries, baseBudget, hash) => entries.flatMap((stored) => Option$1.match(replayEntryOf$1(stored, hash), {
|
|
1927
|
+
onNone: () => [],
|
|
1928
|
+
onSome: (entry) => [applyReplayEntry(baseBudget, entry)]
|
|
1929
|
+
}));
|
|
1930
|
+
const checkOf = (registration, arbitrary, budget, observe, identityHash) => ({
|
|
1150
1931
|
name: registration.name,
|
|
1151
1932
|
lane: registration.lane,
|
|
1152
1933
|
subject: registration.spec.subject,
|
|
@@ -1154,23 +1935,54 @@ const checkOf = (registration, arbitrary, budget, observe) => ({
|
|
|
1154
1935
|
arbitrary,
|
|
1155
1936
|
observe,
|
|
1156
1937
|
options: budget.options,
|
|
1157
|
-
|
|
1938
|
+
seed: budget.seed,
|
|
1939
|
+
identityHash,
|
|
1940
|
+
site: registration.site,
|
|
1941
|
+
ledger: registration.runtime.ledger
|
|
1158
1942
|
});
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
yield* settle(registration, run, budget, coverage, checked);
|
|
1943
|
+
const runRecordedChecks = (registration, arbitrary, hash, recorded) => Effect$1.forEach(recorded, (budget) => {
|
|
1944
|
+
const run = checkOf(registration, arbitrary, budget, noObserve, hash);
|
|
1945
|
+
return Effect$1.flatMap(runCheck(run), (checked) => settleRecorded(registration, run, budget, checked));
|
|
1946
|
+
}, {
|
|
1947
|
+
concurrency: 1,
|
|
1948
|
+
discard: true
|
|
1166
1949
|
});
|
|
1167
|
-
const
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1950
|
+
const replayRun = (registration, baseBudget, arbitrary, hash, entry) => {
|
|
1951
|
+
const budget = applyReplayEntry(baseBudget, entry);
|
|
1952
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary, budget.seed);
|
|
1953
|
+
const run = checkOf(registration, arbitrary, budget, coverage.observe, hash);
|
|
1954
|
+
return Effect$1.flatMap(runCheck(run), (checked) => settle(registration, run, budget, coverage, checked, noStore(registration.name)));
|
|
1955
|
+
};
|
|
1956
|
+
const storeRun = (registration, task, baseBudget, arbitrary, hash) => Effect$1.gen(function* () {
|
|
1957
|
+
const entries = yield* readTaskStore(task, registration.site);
|
|
1958
|
+
const existing = entriesForProperty(entries, task.identity.name);
|
|
1959
|
+
const recorded = recordedBudgets(existing, baseBudget, hash);
|
|
1960
|
+
yield* runRecordedChecks(registration, arbitrary, hash, recorded);
|
|
1961
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary, baseBudget.seed);
|
|
1962
|
+
const run = checkOf(registration, arbitrary, baseBudget, coverage.observe, hash);
|
|
1963
|
+
const checked = yield* runCheck(run);
|
|
1964
|
+
yield* settle(registration, run, baseBudget, coverage, checked, {
|
|
1965
|
+
filepath: task.filepath,
|
|
1966
|
+
name: task.identity.name,
|
|
1967
|
+
existing,
|
|
1968
|
+
recorded,
|
|
1969
|
+
budget: task.budget
|
|
1970
|
+
});
|
|
1172
1971
|
});
|
|
1173
|
-
const
|
|
1972
|
+
const propertyPlatform = Layer.merge(layer, layer$1);
|
|
1973
|
+
const program = (registration, task) => Effect$1.provide(Effect$1.gen(function* () {
|
|
1974
|
+
const fresh = (yield* Random.nextInt) >>> 0;
|
|
1975
|
+
const baseBudget = resolveBudget(registration.name, registration.spec, task, fresh);
|
|
1976
|
+
const hash = identityHash(task.identity);
|
|
1977
|
+
const replayText = yield* Config.option(Config.String(REPLAY_VARIABLE)).pipe(Effect$1.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv()), Effect$1.orDie);
|
|
1978
|
+
const entry = Option$1.getOrUndefined(yield* replayEntryOf(replayText, hash, registration.site));
|
|
1979
|
+
const arbitrary = arbitraryOf(registration.spec.of);
|
|
1980
|
+
yield* Option$1.match(Option$1.fromNullishOr(entry), {
|
|
1981
|
+
onNone: () => storeRun(registration, task, baseBudget, arbitrary, hash),
|
|
1982
|
+
onSome: (replay) => replayRun(registration, baseBudget, arbitrary, hash, replay)
|
|
1983
|
+
});
|
|
1984
|
+
}), propertyPlatform);
|
|
1985
|
+
const programOf = (registration) => (task) => registration.runtime.provide(program(registration, task));
|
|
1174
1986
|
const isSelfModel = (subject, oracle) => Object.is(subject, oracle);
|
|
1175
1987
|
/**
|
|
1176
1988
|
* The property surface over one runtime: `prop`, `effectProp`, and the R13 law kinds routed through the gate.
|
|
@@ -1192,20 +2004,27 @@ const makeProperty = (runtime) => {
|
|
|
1192
2004
|
};
|
|
1193
2005
|
const refuseSelfModel = (name) => {
|
|
1194
2006
|
const site = callFrame();
|
|
1195
|
-
runtime.register(name, () => dieWithSite(new
|
|
2007
|
+
runtime.register(name, (_task) => dieWithSite(new SelfModelLaw({
|
|
2008
|
+
name,
|
|
2009
|
+
site: site ?? null
|
|
2010
|
+
}), site));
|
|
2011
|
+
};
|
|
2012
|
+
const judged = (name, spec, holds) => body(name, spec, holds, "sync", true);
|
|
2013
|
+
const exempted = (name, spec, holds, kind) => {
|
|
2014
|
+
runtime.ledger.recordExempt(name, kind);
|
|
2015
|
+
body(`${name} [exempt: ${kind}]`, spec, holds, "sync", false);
|
|
1196
2016
|
};
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
const deterministic = (name, spec) => gated(name, spec, deterministicHolds(), "deterministic", true);
|
|
2017
|
+
const model = (name, spec, oracle) => isSelfModel(spec.subject, oracle) ? refuseSelfModel(name) : judged(name, spec, modelHolds(oracle));
|
|
2018
|
+
const idempotent = (name, spec) => exempted(name, spec, idempotentHolds(), "idempotent");
|
|
2019
|
+
const deterministic = (name, spec) => exempted(name, spec, deterministicHolds(), "deterministic");
|
|
1201
2020
|
return {
|
|
1202
2021
|
prop: (name, spec, holds) => body(name, spec, holds, "sync", true),
|
|
1203
2022
|
effectProp: (name, spec, holds) => body(name, spec, holds, "effect", true),
|
|
1204
2023
|
law: {
|
|
1205
2024
|
model,
|
|
1206
|
-
metamorphic: (name, spec, relation) =>
|
|
1207
|
-
roundTrip: (name, spec, decode) =>
|
|
1208
|
-
invariant: (name, spec, holds) =>
|
|
2025
|
+
metamorphic: (name, spec, relation) => judged(name, spec, metamorphicHolds(relation)),
|
|
2026
|
+
roundTrip: (name, spec, decode) => judged(name, spec, roundTripHolds(decode)),
|
|
2027
|
+
invariant: (name, spec, holds) => judged(name, spec, invariantHolds(holds)),
|
|
1209
2028
|
idempotent,
|
|
1210
2029
|
deterministic
|
|
1211
2030
|
}
|
|
@@ -1250,7 +2069,7 @@ const renderRecord = (failure, spans, identity) => renderFailureRecord({
|
|
|
1250
2069
|
failure,
|
|
1251
2070
|
spans,
|
|
1252
2071
|
identity,
|
|
1253
|
-
replay:
|
|
2072
|
+
replay: void 0,
|
|
1254
2073
|
root: providedRoot()
|
|
1255
2074
|
});
|
|
1256
2075
|
const outcomeOf = (exit, recorder, identity) => Exit.isSuccess(exit) ? void 0 : renderRecord(Cause.squash(exit.cause), recorder.spans, identity);
|
|
@@ -1271,23 +2090,63 @@ const recordOfRun = (program) => {
|
|
|
1271
2090
|
if (task === void 0) return Promise.resolve(void 0);
|
|
1272
2091
|
return runRecorded(task.context, program);
|
|
1273
2092
|
};
|
|
1274
|
-
const capturingRuntime = (
|
|
1275
|
-
register: (
|
|
1276
|
-
|
|
2093
|
+
const capturingRuntime = (captured, ledger) => ({
|
|
2094
|
+
register: (name, program) => {
|
|
2095
|
+
captured.push({
|
|
2096
|
+
name,
|
|
2097
|
+
program
|
|
2098
|
+
});
|
|
1277
2099
|
},
|
|
1278
|
-
provide: (effect) => effect
|
|
2100
|
+
provide: (effect) => effect,
|
|
2101
|
+
ledger
|
|
1279
2102
|
});
|
|
1280
|
-
const
|
|
2103
|
+
const taskOfRecorded = (name, budget, store) => {
|
|
2104
|
+
const current = TestRunner.getCurrentTest();
|
|
2105
|
+
return {
|
|
2106
|
+
identity: {
|
|
2107
|
+
...testIdentityOf(current),
|
|
2108
|
+
name
|
|
2109
|
+
},
|
|
2110
|
+
filepath: Option$1.getOrNull(Option$1.fromNullishOr(store)),
|
|
2111
|
+
budget: budget ?? providedCheckDefaults()
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
2114
|
+
/** Runs every captured program in registration order, each as `recordOfProperty` runs one. */
|
|
2115
|
+
const runCaptured = (captured, budget, store) => captured.reduce((previous, entry) => previous.then((records) => {
|
|
2116
|
+
const task = taskOfRecorded(entry.name, budget, store);
|
|
2117
|
+
return recordOfRun(() => entry.program(task)).then((record) => [...records, record]);
|
|
2118
|
+
}), Promise.resolve([]));
|
|
2119
|
+
/** Registers a whole file's properties on a capturing runtime with a fresh ledger, and runs them in order. */
|
|
2120
|
+
const capturedRun = (register, budget, store) => {
|
|
2121
|
+
const captured = [];
|
|
2122
|
+
const ledger = makeFileLedger();
|
|
2123
|
+
register(makeProperty(capturingRuntime(captured, ledger)));
|
|
2124
|
+
return runCaptured(captured, budget, store).then((records) => ({
|
|
2125
|
+
records,
|
|
2126
|
+
ledger
|
|
2127
|
+
}));
|
|
2128
|
+
};
|
|
1281
2129
|
/**
|
|
1282
2130
|
* Runs one property the way the runner runs `it.prop`, with a fresh ledger and its own recorder, and resolves with
|
|
1283
2131
|
* the record its falsification rendered — or `undefined` when it held.
|
|
1284
2132
|
*
|
|
1285
2133
|
* @internal
|
|
1286
2134
|
*/
|
|
1287
|
-
const recordOfProperty = (input) => {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
2135
|
+
const recordOfProperty = (input) => capturedRun((api) => api.prop(input.name, input.spec, input.holds), input.budget, input.store).then(({ records }) => records[0]);
|
|
2136
|
+
const EMPTY_FILE_OPTIONS = {};
|
|
2137
|
+
/**
|
|
2138
|
+
* Runs every property and law a file registers, in registration order, then finalises the file's one ledger:
|
|
2139
|
+
* resolves with each property's rendered failure and the file's single vacuous verdict, or `undefined` when the
|
|
2140
|
+
* file holds. The name a property is recorded under is the name it was registered with.
|
|
2141
|
+
*
|
|
2142
|
+
* @internal
|
|
2143
|
+
*/
|
|
2144
|
+
const recordOfFile = Function.dual((args) => typeof args[0] === "function", (register, options) => {
|
|
2145
|
+
const resolved = options ?? EMPTY_FILE_OPTIONS;
|
|
2146
|
+
return capturedRun(register, resolved.budget, resolved.store).then(({ records, ledger }) => ({
|
|
2147
|
+
records,
|
|
2148
|
+
vacuous: ledger.finalise()
|
|
2149
|
+
}));
|
|
2150
|
+
});
|
|
1292
2151
|
//#endregion
|
|
1293
|
-
export { ReplayFromText as _,
|
|
2152
|
+
export { Slop as C, providedCheckDefaults as D, replayOfText as E, LeakedState as S, ReplayFromText as T, testIdentityOf as _, makeProperty as a, providedRoot as b, NonBooleanVerdict as c, SeedStoreUnreadable as d, SelfModelLaw as f, summaryOf as g, renderFailureRecord as h, createSpanRecorder as i, PropertyRefuted as l, isFailureRecordError as m, recordOfProperty as n, makeFileLedger as o, VacuousProperty as p, recordOfRun as r, CoverageBelowMinimum as s, recordOfFile as t, ReplayUnreadable as u, throwFailureRecord as v, Replay as w, InvalidBudget as x, witnessOf as y };
|