@systemfsoftware/vitest 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/{checks-DiuzHhQM.d.ts → checks-Cq9of-Lx.d.ts} +105 -1
- package/dist/engine-YsoQ_9we.d.ts +158 -0
- package/dist/failure-BquJJPFZ.mjs +1217 -0
- package/dist/failure.d.ts +80 -0
- package/dist/failure.mjs +4 -0
- package/dist/{guard-BocOhXt2.mjs → guard-p_8ARgPm.mjs} +15 -11
- package/dist/guard.mjs +1 -1
- package/dist/index.d.ts +4 -155
- package/dist/index.mjs +46 -516
- package/dist/integration.d.ts +1 -1
- package/dist/integration.mjs +1 -1
- package/dist/{test-context-hvSoKLtw.mjs → test-context-ChvjVyCo.mjs} +128 -6
- package/package.json +9 -4
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
|
|
2
|
+
import { d as framesOf$1, f as isUserPath, i as checksFor, o as makeLedger, p as withRaisingFrame, r as Asserted, s as callFrame, t as VitestTestContext, u as firstUserSiteOf } from "./test-context-ChvjVyCo.mjs";
|
|
3
|
+
import { b as refusedRecordDetail } from "./guard-p_8ARgPm.mjs";
|
|
4
|
+
import * as V from "vitest";
|
|
5
|
+
import { TestRunner, inject } from "vitest";
|
|
6
|
+
import * as Cause from "effect/Cause";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Function from "effect/Function";
|
|
10
|
+
import * as Schema$1 from "effect/Schema";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import { Schema } from "effect";
|
|
13
|
+
import * as Match from "effect/Match";
|
|
14
|
+
import * as Tracer from "effect/Tracer";
|
|
15
|
+
import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary";
|
|
16
|
+
import * as Data from "effect/Data";
|
|
17
|
+
import * as Equal from "effect/Equal";
|
|
18
|
+
//#region src/internal/property/defaults.ts
|
|
19
|
+
/** @internal The `test.provide` key the shared config fills with this run's tier's check options. */
|
|
20
|
+
const checkDefaultsKey = "@systemfsoftware/vitest:property-check";
|
|
21
|
+
/** @internal The configured property-check defaults, or `undefined` when the run provided none. */
|
|
22
|
+
const providedCheckDefaults = () => inject(checkDefaultsKey);
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/internal/errors.schema.ts
|
|
25
|
+
/** @internal */
|
|
26
|
+
var NonBooleanVerdict = class extends Schema.TaggedError()("NonBooleanVerdict", { detail: Schema.String }) {
|
|
27
|
+
get message() {
|
|
28
|
+
return this.detail;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** @internal */
|
|
32
|
+
var LeakedState = class extends Schema.TaggedError()("LeakedState", { detail: Schema.String }) {
|
|
33
|
+
get message() {
|
|
34
|
+
return this.detail;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** @internal */
|
|
38
|
+
var Slop = class extends Schema.TaggedError()("Slop", { detail: Schema.String }) {
|
|
39
|
+
get message() {
|
|
40
|
+
return this.detail;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** @internal */
|
|
44
|
+
var InvalidBudget = class extends Schema.TaggedError()("InvalidBudget", { detail: Schema.String }) {
|
|
45
|
+
get message() {
|
|
46
|
+
return this.detail;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** @internal */
|
|
50
|
+
var FailureRecordRefused = class extends Schema.TaggedError()("FailureRecordRefused", {
|
|
51
|
+
breaches: Schema.Array(Schema.Literals([
|
|
52
|
+
"R1",
|
|
53
|
+
"R2",
|
|
54
|
+
"R6"
|
|
55
|
+
])),
|
|
56
|
+
cause: Schema.optional(Schema.Unknown)
|
|
57
|
+
}) {
|
|
58
|
+
get message() {
|
|
59
|
+
return refusedRecordDetail(this.breaches);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/internal/failure-record.ts
|
|
64
|
+
/**
|
|
65
|
+
* The one renderer for a spec failure: the failure and the spans its run recorded become the record Vitest
|
|
66
|
+
* prints, together with the contract rules that record breaks (KTD1, KTD7).
|
|
67
|
+
*
|
|
68
|
+
* The record is ordered by R2-R6: the raising frame leads, the failing step and its spec line follow, then every
|
|
69
|
+
* cause layer, then the step trail with the cell decisions each step caused, then the rerun command. The
|
|
70
|
+
* renderer is pure and never throws: a value it cannot read renders as text, never as an exception.
|
|
71
|
+
*
|
|
72
|
+
* @since 4.0.0
|
|
73
|
+
*/
|
|
74
|
+
const STEP_SPAN = "gherkin.step";
|
|
75
|
+
const STEP_KEYWORD = "gherkin.keyword";
|
|
76
|
+
const STEP_TEXT = "gherkin.text";
|
|
77
|
+
const CODE_SITE = "code.site";
|
|
78
|
+
const CELL_NAME = "cell.name";
|
|
79
|
+
const CELL_STACKTRACE = "cell.stacktrace";
|
|
80
|
+
const CELL_DECIDE_STACKTRACE = "cell.decide_stacktrace";
|
|
81
|
+
const CELL_COMMAND_TAG = "cell.command_tag";
|
|
82
|
+
const CELL_MEMBER_TAGS = "cell.member_tags";
|
|
83
|
+
const CELL_DECLARED = "cell.declared";
|
|
84
|
+
const CELL_OUTCOME = "cell.outcome";
|
|
85
|
+
const TAG_FIELD = "_tag";
|
|
86
|
+
const MESSAGE_FIELD = "message";
|
|
87
|
+
const NAME_FIELD = "name";
|
|
88
|
+
const STACK_FIELD = "stack";
|
|
89
|
+
const CAUSE_FIELD = "cause";
|
|
90
|
+
const FILE_PROTOCOL = "file://";
|
|
91
|
+
const UNKNOWN_SITE = "?";
|
|
92
|
+
const DEFAULT_FAILURE_NAME = "Error";
|
|
93
|
+
const NEVER_FINISHED = " (never finished)";
|
|
94
|
+
const CAUSE_DEPTH$1 = 6;
|
|
95
|
+
/** The fields a layer's own values exclude: the chain renders the cause, and the raise site names the stack. */
|
|
96
|
+
const SKIPPED_FIELDS = {
|
|
97
|
+
[TAG_FIELD]: true,
|
|
98
|
+
[MESSAGE_FIELD]: true,
|
|
99
|
+
[NAME_FIELD]: true,
|
|
100
|
+
[STACK_FIELD]: true,
|
|
101
|
+
[CAUSE_FIELD]: true
|
|
102
|
+
};
|
|
103
|
+
/** The contract's "first location": any `<path>:<line>` the record names. */
|
|
104
|
+
const LOCATION$1 = /(?:[\w.@-]+\/)*[\w.@-]+\.[cm]?[jt]sx?:\d+/u;
|
|
105
|
+
const COLUMN_SUFFIX = /:\d+$/u;
|
|
106
|
+
const MARK = {
|
|
107
|
+
passed: "✓",
|
|
108
|
+
failed: "✗",
|
|
109
|
+
unfinished: "✗"
|
|
110
|
+
};
|
|
111
|
+
const not = (value) => !value;
|
|
112
|
+
const and = (left, right) => left && right;
|
|
113
|
+
const or = (left, right) => left || right;
|
|
114
|
+
const firstPresent = (values) => values.find((value) => value !== void 0);
|
|
115
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null;
|
|
116
|
+
const isText$2 = (value) => typeof value === "string";
|
|
117
|
+
const isArray = (value) => Array.isArray(value);
|
|
118
|
+
const isNumber = (value) => typeof value === "number";
|
|
119
|
+
const isBoolean = (value) => typeof value === "boolean";
|
|
120
|
+
const isBigInt = (value) => typeof value === "bigint";
|
|
121
|
+
const isNull = (value) => value === null;
|
|
122
|
+
const isPrimitive = (value) => or(or(isNumber(value), isBoolean(value)), isBigInt(value));
|
|
123
|
+
const objectOrUndefined$1 = (value) => isObject$1(value) ? value : void 0;
|
|
124
|
+
const isFieldName = (name) => SKIPPED_FIELDS[name] !== true;
|
|
125
|
+
const presentOrUndefined = (text) => text.length === 0 ? void 0 : text;
|
|
126
|
+
const nonEmptyText$1 = (text) => text === void 0 ? void 0 : presentOrUndefined(text);
|
|
127
|
+
const fieldOf$1 = (value, key) => Reflect.get(value, key);
|
|
128
|
+
const textFieldOf = (value, key) => {
|
|
129
|
+
const field = fieldOf$1(value, key);
|
|
130
|
+
return isText$2(field) ? field : void 0;
|
|
131
|
+
};
|
|
132
|
+
const nonEmptyFieldOf = (value, key) => nonEmptyText$1(textFieldOf(value, key));
|
|
133
|
+
const tagFieldOf = (value) => nonEmptyFieldOf(value, TAG_FIELD);
|
|
134
|
+
const messageFieldOf = (value) => nonEmptyFieldOf(value, MESSAGE_FIELD);
|
|
135
|
+
const pairOf = (name, text) => ({
|
|
136
|
+
name,
|
|
137
|
+
text
|
|
138
|
+
});
|
|
139
|
+
const pairTextOf = (pair) => `${JSON.stringify(pair.name)}:${pair.text}`;
|
|
140
|
+
const pairTextsOf = (pairs) => pairs.map(pairTextOf);
|
|
141
|
+
const hasPair = (pairs, name) => pairs.some((pair) => pair.name === name);
|
|
142
|
+
const pairsOf = (value) => isObject$1(value) ? Object.keys(value).map((key) => pairOf(key, renderValue(fieldOf$1(value, key)))) : [];
|
|
143
|
+
const fieldsOf = (value) => pairsOf(value).filter((pair) => isFieldName(pair.name));
|
|
144
|
+
const renderValue = (value) => isObject$1(value) ? renderObject(value) : renderScalar(value);
|
|
145
|
+
const renderScalar = (value) => isText$2(value) ? JSON.stringify(value) : renderPrimitive(value);
|
|
146
|
+
const renderPrimitive = (value) => isBigInt(value) ? `${value}n` : renderPlain(value);
|
|
147
|
+
const renderPlain = (value) => isPrimitive(value) ? `${value}` : renderOther(value);
|
|
148
|
+
const renderOther = (value) => isNull(value) ? "null" : renderUndefined(value);
|
|
149
|
+
const renderUndefined = (value) => value === void 0 ? "undefined" : renderContainer(value);
|
|
150
|
+
const renderContainer = (value) => isArray(value) ? renderList(value) : typeof value;
|
|
151
|
+
const renderList = (value) => `[${value.map(renderValue).join(",")}]`;
|
|
152
|
+
const renderObject = (value) => tagFieldOf(value) ?? renderRecord$1(value);
|
|
153
|
+
const renderRecord$1 = (value) => `{${pairTextsOf(pairsOf(value)).join(",")}}`;
|
|
154
|
+
const bracesOf = (pairs) => ` {${pairTextsOf(pairs).join(",")}}`;
|
|
155
|
+
const renderFields = (value) => bracesOf(fieldsOf(value));
|
|
156
|
+
const textSummaryOf = (text) => text.length === 0 ? text : JSON.stringify(text);
|
|
157
|
+
const scalarSummaryOf = (value) => isText$2(value) ? textSummaryOf(value) : renderScalar(value);
|
|
158
|
+
/** @internal */
|
|
159
|
+
const summaryOf = (value) => isObject$1(value) ? objectSummaryOf(value) : scalarSummaryOf(value);
|
|
160
|
+
const objectSummaryOf = (value) => {
|
|
161
|
+
const tag = tagFieldOf(value);
|
|
162
|
+
return tag === void 0 ? untaggedSummaryOf(value) : taggedSummaryOf(tag, value);
|
|
163
|
+
};
|
|
164
|
+
const untaggedSummaryOf = (value) => isErrorValue(value) ? errorSummaryOf(value) : valueRecordSummaryOf(value);
|
|
165
|
+
const valueRecordSummaryOf = (value) => isArray(value) ? renderList(value) : renderRecord$1(value);
|
|
166
|
+
const isErrorValue = (value) => value instanceof Error;
|
|
167
|
+
const firstLine = (text) => text.split("\n")[0] ?? text;
|
|
168
|
+
const restLines = (text) => text.split("\n").slice(1);
|
|
169
|
+
const taggedSummaryOf = (tag, value) => {
|
|
170
|
+
const message = messageFieldOf(value);
|
|
171
|
+
return message === void 0 ? `${tag}${renderFields(value)}` : `${tag}: ${firstLine(message)}`;
|
|
172
|
+
};
|
|
173
|
+
const errorSummaryOf = (value) => {
|
|
174
|
+
const name = objectNameOf(value);
|
|
175
|
+
const message = messageFieldOf(value);
|
|
176
|
+
return message === void 0 ? `${name}${renderFields(value)}` : `${name}: ${firstLine(message)}`;
|
|
177
|
+
};
|
|
178
|
+
const objectNameOf = (value) => firstPresent([tagFieldOf(value), nonEmptyFieldOf(value, NAME_FIELD)]) ?? DEFAULT_FAILURE_NAME;
|
|
179
|
+
const headlineOf = (layers) => layers.length === 0 ? "" : summaryOf(layers[0]);
|
|
180
|
+
const nameOf = (layers) => {
|
|
181
|
+
const entry = layers[0];
|
|
182
|
+
return isObject$1(entry) ? objectNameOf(entry) : DEFAULT_FAILURE_NAME;
|
|
183
|
+
};
|
|
184
|
+
const stripProtocol = (path) => path.startsWith(FILE_PROTOCOL) ? path.slice(7) : path;
|
|
185
|
+
const stripColumn = (site) => site.replace(COLUMN_SUFFIX, "");
|
|
186
|
+
const knownSiteOf = (site) => isText$2(site) ? stripColumn(stripProtocol(site)) : void 0;
|
|
187
|
+
const siteTextOf = (site) => knownSiteOf(site) ?? UNKNOWN_SITE;
|
|
188
|
+
/** The author's site in a raw captured stack, the form a cell span records instead of a pre-resolved site. */
|
|
189
|
+
const cellStackSiteOf = (stack) => isText$2(stack) ? firstUserSiteOf(stack) : void 0;
|
|
190
|
+
const stackSiteTextOf = (stack) => knownSiteOf(cellStackSiteOf(stack)) ?? UNKNOWN_SITE;
|
|
191
|
+
const stackIn = (value) => textFieldOf(value, STACK_FIELD) ?? "";
|
|
192
|
+
const stackOf$1 = (value) => isObject$1(value) ? stackIn(value) : "";
|
|
193
|
+
const framesOf = (value) => framesOf$1(stackOf$1(value));
|
|
194
|
+
const isFrame = (frame) => frame !== void 0;
|
|
195
|
+
const usableFrameOf = (value) => framesOf(value).find(isUserFrame);
|
|
196
|
+
const isUserFrame = (frame) => isUserPath(frame.path);
|
|
197
|
+
const frameTextOf = (frame) => `${frame.path}:${frame.line}${frameNameSuffix(frame.fn)}`;
|
|
198
|
+
const isAuthorName = (fn) => fn !== void 0 && !fn.includes("~effect/");
|
|
199
|
+
const frameNameSuffix = (fn) => isAuthorName(fn) ? ` (${fn})` : "";
|
|
200
|
+
const isSameSite = (frame, other) => and(frame.path === other.path, frame.line === other.line);
|
|
201
|
+
const isSameFrame = (frame, other) => other === void 0 ? false : isSameSite(frame, other);
|
|
202
|
+
const chosenFrameOf = (layers) => {
|
|
203
|
+
const inner = layers.slice(1).map(usableFrameOf).filter(isFrame).at(-1);
|
|
204
|
+
return inner === void 0 ? usableFrameOf(layers[0]) : inner;
|
|
205
|
+
};
|
|
206
|
+
const attrOf = (span, key) => span.attributes.get(key);
|
|
207
|
+
const textAttrOf = (span, key) => {
|
|
208
|
+
const value = attrOf(span, key);
|
|
209
|
+
return isText$2(value) ? value : void 0;
|
|
210
|
+
};
|
|
211
|
+
const ancestorsIn = (span) => span instanceof Tracer.NativeSpan ? [span, ...ancestorsOf(span.parent)] : [];
|
|
212
|
+
const ancestorsOf = (parent) => Option.isNone(parent) ? [] : ancestorsIn(parent.value);
|
|
213
|
+
const stepAncestorOf = (span) => ancestorsOf(span.parent).find((ancestor) => ancestor.name === STEP_SPAN);
|
|
214
|
+
const stateOf = (span) => Match.value(span.status).pipe(Match.when({ _tag: "Started" }, () => "unfinished"), Match.when({ _tag: "Ended" }, (ended) => endedStateOf(ended.exit)), Match.exhaustive);
|
|
215
|
+
const endedStateOf = (exit) => Exit.isSuccess(exit) ? "passed" : "failed";
|
|
216
|
+
const stepSpansOf = (spans) => spans.filter(isTopStepSpan);
|
|
217
|
+
const isTopStepSpan = (span) => and(span.name === STEP_SPAN, stepAncestorOf(span) === void 0);
|
|
218
|
+
const isCellSpan = (span) => isText$2(attrOf(span, CELL_STACKTRACE));
|
|
219
|
+
const cellSpansOf = (spans) => spans.filter(isCellSpan);
|
|
220
|
+
const cellNameOf = (cell) => textAttrOf(cell, CELL_NAME) ?? cell.name;
|
|
221
|
+
const causeFieldOf = (value) => isObject$1(value) ? objectOrUndefined$1(fieldOf$1(value, CAUSE_FIELD)) : void 0;
|
|
222
|
+
/** One layer with whatever its own `cause` adds, depth-limited. */
|
|
223
|
+
const layerWithCauseOf = (value, depth) => [value, ...nestedCauseOf(value, depth - 1)];
|
|
224
|
+
/** The layers the reasons of an Effect `Cause` add, each rendered as any other layer. */
|
|
225
|
+
const reasonLayersOf = (cause, depth) => cause.reasons.flatMap((reason) => layerWithCauseOf(reasonPayloadOf$1(reason), depth));
|
|
226
|
+
const layersOfCause = (cause, depth) => Cause.isCause(cause) ? reasonLayersOf(cause, depth) : layerWithCauseOf(cause, depth);
|
|
227
|
+
const nestedOf = (cause, depth) => cause === void 0 ? [] : layersOfCause(cause, depth);
|
|
228
|
+
const nestedCauseOf = (value, depth) => depth === 0 ? [] : nestedOf(causeFieldOf(value), depth);
|
|
229
|
+
const causeChainOf$1 = (value) => [value, ...nestedCauseOf(value, CAUSE_DEPTH$1)];
|
|
230
|
+
const interruptTextOf = (reason) => reason.fiberId === void 0 ? "interrupted" : `interrupted by fiber #${reason.fiberId}`;
|
|
231
|
+
const reasonPayloadOf$1 = (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);
|
|
232
|
+
const reasonsOf = (cause) => cause.reasons.flatMap((reason) => causeChainOf$1(reasonPayloadOf$1(reason)));
|
|
233
|
+
const layersOf = (failure) => Cause.isCause(failure) ? reasonsOf(failure) : causeChainOf$1(failure);
|
|
234
|
+
const declineOf = (span) => stateOf(span) === "unfinished" ? NEVER_FINISHED : "";
|
|
235
|
+
const capitalize = (text) => text.length === 0 ? text : `${text.slice(0, 1).toUpperCase()}${text.slice(1)}`;
|
|
236
|
+
const keywordOf = (step) => capitalize(textAttrOf(step, STEP_KEYWORD) ?? "");
|
|
237
|
+
const stepTextOf = (step) => textAttrOf(step, STEP_TEXT) ?? "";
|
|
238
|
+
const stepSiteOf = (step) => siteTextOf(attrOf(step, CODE_SITE));
|
|
239
|
+
const stepLineOf = (step) => ` ${MARK[stateOf(step)]} ${keywordOf(step)} ${stepTextOf(step)} ${stepSiteOf(step)}${declineOf(step)}`;
|
|
240
|
+
const commandOf = (cell) => textAttrOf(cell, CELL_COMMAND_TAG) ?? "";
|
|
241
|
+
const cellPairsOf = (cell) => {
|
|
242
|
+
const members = pairsOf(attrOf(cell, CELL_MEMBER_TAGS));
|
|
243
|
+
const declared = pairsOf(attrOf(cell, CELL_DECLARED)).filter((pair) => !hasPair(members, pair.name));
|
|
244
|
+
return [...members, ...declared];
|
|
245
|
+
};
|
|
246
|
+
const pairsBracesOf = (cell) => bracesOf(cellPairsOf(cell));
|
|
247
|
+
const outcomeOf$1 = (cell) => {
|
|
248
|
+
const outcome = textAttrOf(cell, CELL_OUTCOME);
|
|
249
|
+
return outcome === void 0 ? "" : ` → ${outcome}`;
|
|
250
|
+
};
|
|
251
|
+
const blockOf = (cell) => ({
|
|
252
|
+
head: ` cell ${cellNameOf(cell)}: ${commandOf(cell)}${pairsBracesOf(cell)}${outcomeOf$1(cell)}`,
|
|
253
|
+
tail: ` decide ${stackSiteTextOf(attrOf(cell, CELL_DECIDE_STACKTRACE))} cell ${stackSiteTextOf(attrOf(cell, CELL_STACKTRACE))}`
|
|
254
|
+
});
|
|
255
|
+
const groupOf = (block, count) => ({
|
|
256
|
+
block,
|
|
257
|
+
count
|
|
258
|
+
});
|
|
259
|
+
const isSameBlock = (block, other) => and(block.head === other.head, block.tail === other.tail);
|
|
260
|
+
const appendBeside = (groups, block, last) => isSameBlock(last.block, block) ? [...groups.slice(0, -1), groupOf(block, last.count + 1)] : [...groups, groupOf(block, 1)];
|
|
261
|
+
const appendAfter = (groups, block, last) => last === void 0 ? [groupOf(block, 1)] : appendBeside(groups, block, last);
|
|
262
|
+
const appendGroup = (groups, block) => appendAfter(groups, block, groups.at(-1));
|
|
263
|
+
const groupsOf = (blocks) => blocks.reduce(appendGroup, []);
|
|
264
|
+
const groupLinesOf = (group) => [group.count > 1 ? `${group.block.head} (×${group.count})` : group.block.head, group.block.tail];
|
|
265
|
+
const cellsUnder = (spans, step) => cellSpansOf(spans).filter((cell) => stepAncestorOf(cell) === step);
|
|
266
|
+
const decisionLinesOf = (spans, step) => groupsOf(cellsUnder(spans, step).map(blockOf)).flatMap(groupLinesOf);
|
|
267
|
+
const trailStepsOf = (spans) => {
|
|
268
|
+
const steps = stepSpansOf(spans);
|
|
269
|
+
const failing = steps.find((step) => stateOf(step) !== "passed");
|
|
270
|
+
return failing === void 0 ? steps : steps.slice(0, steps.indexOf(failing) + 1);
|
|
271
|
+
};
|
|
272
|
+
const trailLinesOf = (spans) => trailStepsOf(spans).flatMap((step) => [stepLineOf(step), ...decisionLinesOf(spans, step)]);
|
|
273
|
+
const layerHintOf = (spans) => {
|
|
274
|
+
const cell = cellSpansOf(spans).at(0);
|
|
275
|
+
return cell === void 0 ? "" : ` (cell ${cellNameOf(cell)})`;
|
|
276
|
+
};
|
|
277
|
+
const failingStepTextOf = (step) => `Failing step: ${keywordOf(step)} ${stepTextOf(step)} ${stepSiteOf(step)}`;
|
|
278
|
+
const noFailingStepLineOf = (steps, spans) => steps.length === 0 ? `Failing step: no step ran${layerHintOf(spans)}` : "Failing step: none of the steps failed";
|
|
279
|
+
const failingStepLineOf = (spans) => {
|
|
280
|
+
const steps = stepSpansOf(spans);
|
|
281
|
+
const failing = steps.find((step) => stateOf(step) !== "passed");
|
|
282
|
+
return failing === void 0 ? noFailingStepLineOf(steps, spans) : failingStepTextOf(failing);
|
|
283
|
+
};
|
|
284
|
+
const isFailedSpan = (span) => stateOf(span) === "failed";
|
|
285
|
+
const isUnfinishedCell = (span) => and(isCellSpan(span), stateOf(span) === "unfinished");
|
|
286
|
+
const spanSiteOf = (span) => knownSiteOf(attrOf(span, CODE_SITE)) ?? knownSiteOf(cellStackSiteOf(attrOf(span, CELL_STACKTRACE)));
|
|
287
|
+
const cellSiteOf = (spans) => {
|
|
288
|
+
const cell = spans.toReversed().find(isCellSpan);
|
|
289
|
+
return cell === void 0 ? void 0 : spanSiteOf(cell);
|
|
290
|
+
};
|
|
291
|
+
const unfinishedDecideSiteOf = (spans) => {
|
|
292
|
+
const unfinished = spans.toReversed().find(isUnfinishedCell);
|
|
293
|
+
return unfinished === void 0 ? void 0 : knownSiteOf(cellStackSiteOf(attrOf(unfinished, CELL_DECIDE_STACKTRACE)));
|
|
294
|
+
};
|
|
295
|
+
const decideSiteOf = (spans) => unfinishedDecideSiteOf(spans) ?? cellSiteOf(spans);
|
|
296
|
+
const failedSiteOf = (spans) => {
|
|
297
|
+
const failed = spans.toReversed().find(isFailedSpan);
|
|
298
|
+
return failed === void 0 ? void 0 : spanSiteOf(failed);
|
|
299
|
+
};
|
|
300
|
+
const fallbackSiteOf = (spans) => failedSiteOf(spans) ?? decideSiteOf(spans);
|
|
301
|
+
const fallbackRaisedAtLineOf = (spans) => {
|
|
302
|
+
const site = fallbackSiteOf(spans);
|
|
303
|
+
return site === void 0 ? void 0 : ` raised at ${site}`;
|
|
304
|
+
};
|
|
305
|
+
const raisedAtLineOf = (frame, spans) => frame === void 0 ? fallbackRaisedAtLineOf(spans) : ` raised at ${frameTextOf(frame)}`;
|
|
306
|
+
const chainNamedFrame = (frame, chosen) => isSameFrame(frame, chosen) ? "" : ` raised at ${frameTextOf(frame)}`;
|
|
307
|
+
const chainFrameTextOf = (frame, chosen) => frame === void 0 ? "" : chainNamedFrame(frame, chosen);
|
|
308
|
+
const chainLineOf = (layer, chosen) => ` ${summaryOf(layer)}${chainFrameTextOf(usableFrameOf(layer), chosen)}`;
|
|
309
|
+
const chainContinuation = (line) => ` ${line}`;
|
|
310
|
+
const isMessageBearing = (layer) => or(tagFieldOf(layer) !== void 0, isErrorValue(layer));
|
|
311
|
+
const messageOf = (layer) => messageFieldOf(layer) ?? "";
|
|
312
|
+
const messageTailLines = (layer) => isMessageBearing(layer) ? restLines(messageOf(layer)).map(chainContinuation) : [];
|
|
313
|
+
const chainDetailLines = (layer) => isObject$1(layer) ? messageTailLines(layer) : [];
|
|
314
|
+
const chainLayerLines = (layer, chosen) => [chainLineOf(layer, chosen), ...chainDetailLines(layer)];
|
|
315
|
+
const chainLinesOf = (layers, chosen) => layers.slice(1).flatMap((layer) => chainLayerLines(layer, chosen));
|
|
316
|
+
const quoted = (text) => `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
317
|
+
const renderEntryNonNull = (value) => isNull(value) ? "null" : renderEntryValue(value);
|
|
318
|
+
const renderEntryValue = (value) => isPrimitive(value) ? `${value}` : renderEntryStructure(value);
|
|
319
|
+
const renderEntryStructure = (value) => isArray(value) ? renderEntryList(value) : renderEntryRecord(value);
|
|
320
|
+
const renderEntryList = (value) => `[${value.map(renderEntry).join(";")}]`;
|
|
321
|
+
const renderEntryRecord = (value) => `{${Object.entries(value).map(([key, entry]) => `${key}: ${renderEntry(entry)}`).join(";")}}`;
|
|
322
|
+
const renderEntry = (value) => isText$2(value) ? quoted(value) : renderEntryNonNull(value);
|
|
323
|
+
const replayTextOf = (replay) => replay.seed === void 0 ? void 0 : `seed=${replay.seed};path=${replay.path.map(renderEntry).join(",")}`;
|
|
324
|
+
const replayPrefixText = (text) => text === void 0 ? "" : `CONFORMANCE_REPLAY="${text}" `;
|
|
325
|
+
const replayPrefixOf = (replay) => replayPrefixText(replay === void 0 ? void 0 : replayTextOf(replay));
|
|
326
|
+
const isCompleteIdentity = (identity) => and(identity.file.length > 0, identity.name.length > 0);
|
|
327
|
+
const filterPrefixOf = (identity) => identity.package.length === 0 ? "" : `pnpm --filter ${identity.package} exec `;
|
|
328
|
+
const rerunLineOf = (identity, replay) => isCompleteIdentity(identity) ? ` ${replayPrefixOf(replay)}${filterPrefixOf(identity)}vitest run ${identity.file} -t ` + quoted(identity.name) : void 0;
|
|
329
|
+
const rerunLinesOf = (identity, replay) => {
|
|
330
|
+
const line = rerunLineOf(identity, replay);
|
|
331
|
+
return line === void 0 ? [] : ["Rerun only this scenario:", line];
|
|
332
|
+
};
|
|
333
|
+
const baselineReplay = (replay) => and(replay.seed === void 0, replay.path.length > 0);
|
|
334
|
+
const replayOnBaseline = (replay) => replay === void 0 ? false : baselineReplay(replay);
|
|
335
|
+
const headMessageOf = (layers) => {
|
|
336
|
+
const entry = layers[0];
|
|
337
|
+
return isObject$1(entry) ? messageFieldOf(entry) : void 0;
|
|
338
|
+
};
|
|
339
|
+
const detailOf = (layers) => {
|
|
340
|
+
const message = headMessageOf(layers);
|
|
341
|
+
return message === void 0 ? [] : restLines(message);
|
|
342
|
+
};
|
|
343
|
+
const detailLinesOf = (lines) => lines.length === 0 ? [] : [...lines.map((line) => ` ${line}`), ""];
|
|
344
|
+
const scheduleLinesOf = (schedule) => schedule === void 0 ? [] : [
|
|
345
|
+
"",
|
|
346
|
+
`schedule: seed ${schedule.seed}`,
|
|
347
|
+
""
|
|
348
|
+
];
|
|
349
|
+
const recordPartsOf = (input, layers) => {
|
|
350
|
+
const chosen = chosenFrameOf(layers);
|
|
351
|
+
return {
|
|
352
|
+
headline: headlineOf(layers),
|
|
353
|
+
raisedAt: raisedAtLineOf(chosen, input.spans),
|
|
354
|
+
failingStep: failingStepLineOf(input.spans),
|
|
355
|
+
detail: detailOf(layers),
|
|
356
|
+
chain: chainLinesOf(layers, chosen),
|
|
357
|
+
trail: trailLinesOf(input.spans),
|
|
358
|
+
schedule: scheduleLinesOf(input.schedule),
|
|
359
|
+
rerun: rerunLinesOf(input.identity, input.replay)
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const maybeLine = (line) => line === void 0 ? [] : [line];
|
|
363
|
+
const sectionLines = (header, lines) => lines.length === 0 ? [] : [
|
|
364
|
+
header,
|
|
365
|
+
...lines,
|
|
366
|
+
""
|
|
367
|
+
];
|
|
368
|
+
const linesOf = (parts) => [
|
|
369
|
+
parts.headline,
|
|
370
|
+
...maybeLine(parts.raisedAt),
|
|
371
|
+
parts.failingStep,
|
|
372
|
+
...detailLinesOf(parts.detail),
|
|
373
|
+
"",
|
|
374
|
+
...sectionLines("Cause chain:", parts.chain),
|
|
375
|
+
...sectionLines("Steps and the decisions each caused:", parts.trail),
|
|
376
|
+
...parts.schedule,
|
|
377
|
+
...parts.rerun
|
|
378
|
+
];
|
|
379
|
+
const breachIf = (holds, breach) => holds ? [breach] : [];
|
|
380
|
+
const r6Broken = (parts, input) => or(parts.rerun.length === 0, replayOnBaseline(input.replay));
|
|
381
|
+
const breachesOf = (parts, input, record) => [
|
|
382
|
+
...breachIf(parts.headline.length === 0, "R1"),
|
|
383
|
+
...breachIf(not(LOCATION$1.test(record)), "R2"),
|
|
384
|
+
...breachIf(r6Broken(parts, input), "R6")
|
|
385
|
+
];
|
|
386
|
+
const hasRoot = (root) => root !== void 0 && root.length > 0;
|
|
387
|
+
const relativize = (record, root) => hasRoot(root) ? record.replaceAll(`${root}/`, "") : record;
|
|
388
|
+
/**
|
|
389
|
+
* Renders the record Vitest prints for one spec failure, and the contract rules it breaks.
|
|
390
|
+
*
|
|
391
|
+
* Never throws: a `Cause` renders layer by layer, a thrown `Error` renders as one layer, and any other value
|
|
392
|
+
* renders as itself. The first location the record names is the raising frame — the first stack frame outside
|
|
393
|
+
* `node_modules` and outside the spec libraries' own `src/` and `dist/` — and when no layer has one, the
|
|
394
|
+
* innermost failed span's site stands in, or the innermost unfinished cell's decide site for a deadlock (KTD6).
|
|
395
|
+
*
|
|
396
|
+
* @internal
|
|
397
|
+
*/
|
|
398
|
+
const renderFailureRecord = (input) => {
|
|
399
|
+
const layers = layersOf(input.failure);
|
|
400
|
+
const parts = recordPartsOf(input, layers);
|
|
401
|
+
const record = relativize(linesOf(parts).join("\n").trimEnd(), input.root);
|
|
402
|
+
return {
|
|
403
|
+
name: nameOf(layers),
|
|
404
|
+
record,
|
|
405
|
+
breaches: breachesOf(parts, input, record)
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/internal/failure-error.ts
|
|
410
|
+
/**
|
|
411
|
+
* The Error Vitest prints for one record (R2, R7, R8): its `name` is the record's failure tag, its `message` is the
|
|
412
|
+
* record, and its `stack` leads with that message — Vitest's JSON reporter hands a consumer `stack || message`, so
|
|
413
|
+
* the record has to ride there too — followed by the record's first location, so no `effect` or library frame stays.
|
|
414
|
+
* The tag is left out of the message because Vitest prints `name: message`: the tag would otherwise lead twice.
|
|
415
|
+
*
|
|
416
|
+
* A failure that carries a diff — Chai's `actual`, `expected`, `showDiff`, `operator` and Vitest's `diff` — has
|
|
417
|
+
* those fields copied from the innermost layer that holds them, because Vitest renders its Expected/Received block
|
|
418
|
+
* from the thrown error, not from the record text.
|
|
419
|
+
*
|
|
420
|
+
* @since 4.0.0
|
|
421
|
+
*/
|
|
422
|
+
const LOCATION = /(?:[\w.@-]+\/)*[\w.@-]+\.[cm]?[jt]sx?:\d+/u;
|
|
423
|
+
/** The mark a rendered record's Error carries, so a throw site that receives one passes it through unchanged. */
|
|
424
|
+
const RECORD = Symbol.for("@systemfsoftware/vitest/FailureRecord");
|
|
425
|
+
/** The fields Vitest reads to render its diff; each is copied only when the layer actually holds one. */
|
|
426
|
+
const DIFF_FIELDS = [
|
|
427
|
+
"actual",
|
|
428
|
+
"expected",
|
|
429
|
+
"showDiff",
|
|
430
|
+
"operator",
|
|
431
|
+
"diff"
|
|
432
|
+
];
|
|
433
|
+
const CAUSE_DEPTH = 8;
|
|
434
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
435
|
+
const objectOrUndefined = (value) => isObject(value) ? value : void 0;
|
|
436
|
+
const fieldOf = (value, key) => Reflect.get(value, key);
|
|
437
|
+
const isDiffLayer = (value) => fieldOf(value, "actual") !== void 0;
|
|
438
|
+
const marked = (error) => {
|
|
439
|
+
Object.defineProperty(error, RECORD, { value: true });
|
|
440
|
+
return error;
|
|
441
|
+
};
|
|
442
|
+
const siteOf = (text) => LOCATION.exec(text)?.[0];
|
|
443
|
+
const withoutTag = (record) => {
|
|
444
|
+
const prefix = `${record.name}: `;
|
|
445
|
+
return record.record.startsWith(prefix) ? record.record.slice(prefix.length) : record.record;
|
|
446
|
+
};
|
|
447
|
+
const stackOf = (record, message) => {
|
|
448
|
+
const head = `${record.name}: ${message}`;
|
|
449
|
+
const site = siteOf(record.record);
|
|
450
|
+
return site === void 0 ? head : `${head}\n at ${site}`;
|
|
451
|
+
};
|
|
452
|
+
const withStack = (error, record, message) => {
|
|
453
|
+
error.stack = stackOf(record, message);
|
|
454
|
+
return error;
|
|
455
|
+
};
|
|
456
|
+
const withName = (error, name) => {
|
|
457
|
+
error.name = name;
|
|
458
|
+
return error;
|
|
459
|
+
};
|
|
460
|
+
const reasonPayloadOf = (reason) => fieldOf(reason, "error") ?? fieldOf(reason, "defect");
|
|
461
|
+
const firstReasonOf = (cause) => {
|
|
462
|
+
const reason = cause.reasons[0];
|
|
463
|
+
if (reason === void 0) return cause;
|
|
464
|
+
return reasonPayloadOf(reason);
|
|
465
|
+
};
|
|
466
|
+
const payloadOf = (failure) => Cause.isCause(failure) ? firstReasonOf(failure) : failure;
|
|
467
|
+
const causeOf = (value) => isObject(value) ? objectOrUndefined(fieldOf(value, "cause")) : void 0;
|
|
468
|
+
const chainedCausesOf = (value, depth) => {
|
|
469
|
+
const cause = causeOf(value);
|
|
470
|
+
return cause === void 0 ? [] : causeChainOf(cause, depth - 1);
|
|
471
|
+
};
|
|
472
|
+
const causeChainOf = (value, depth) => {
|
|
473
|
+
if (depth <= 0) return [];
|
|
474
|
+
return [value, ...chainedCausesOf(value, depth)];
|
|
475
|
+
};
|
|
476
|
+
const layerChainOf = (failure) => {
|
|
477
|
+
const first = payloadOf(failure);
|
|
478
|
+
return [first, ...chainedCausesOf(first, CAUSE_DEPTH)];
|
|
479
|
+
};
|
|
480
|
+
const diffCarrierOf = (failure) => layerChainOf(failure).map(objectOrUndefined).filter(isObject).find(isDiffLayer);
|
|
481
|
+
const copyField = (error, carrier, field) => {
|
|
482
|
+
const value = fieldOf(carrier, field);
|
|
483
|
+
if (value === void 0) return;
|
|
484
|
+
Object.defineProperty(error, field, {
|
|
485
|
+
value,
|
|
486
|
+
enumerable: true,
|
|
487
|
+
configurable: true,
|
|
488
|
+
writable: true
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
const copyFields = (error, carrier) => {
|
|
492
|
+
for (const field of DIFF_FIELDS) copyField(error, carrier, field);
|
|
493
|
+
return error;
|
|
494
|
+
};
|
|
495
|
+
const copyDiffFields = (error, failure) => {
|
|
496
|
+
const carrier = diffCarrierOf(failure);
|
|
497
|
+
return carrier === void 0 ? error : copyFields(error, carrier);
|
|
498
|
+
};
|
|
499
|
+
/** @internal */
|
|
500
|
+
const failureRecordError = (record) => {
|
|
501
|
+
const message = withoutTag(record);
|
|
502
|
+
return marked(withStack(withName(new Error(message), record.name), record, message));
|
|
503
|
+
};
|
|
504
|
+
const isRefusalFailure = (value) => Schema$1.is(FailureRecordRefused)(value);
|
|
505
|
+
/**
|
|
506
|
+
* The refusal thrown in a record's place (R10, KTD7): fixed prose naming each breach, the original failure as its
|
|
507
|
+
* `cause`, and the record that failure would have printed beneath it. The record rides in the stack because Vitest's
|
|
508
|
+
* JSON reporter hands a consumer `stack || message`, where a cause is invisible; the prose never carries record text
|
|
509
|
+
* and is never read back, so a refusal cannot recurse.
|
|
510
|
+
*/
|
|
511
|
+
const refusalError = (failure, record) => {
|
|
512
|
+
const refusal = new FailureRecordRefused({
|
|
513
|
+
breaches: record.breaches,
|
|
514
|
+
cause: failure
|
|
515
|
+
});
|
|
516
|
+
refusal.stack = `${refusal.name}: ${refusal.message}\n${failureRecordError(record).stack ?? ""}`;
|
|
517
|
+
return refusal;
|
|
518
|
+
};
|
|
519
|
+
const printedError = (failure, record) => record.breaches.length > 0 ? refusalError(failure, record) : failureRecordError(record);
|
|
520
|
+
/** Whether a value is the Error one rendered record threw, which a second throw site passes through unchanged.
|
|
521
|
+
*
|
|
522
|
+
* @internal
|
|
523
|
+
*/
|
|
524
|
+
const isFailureRecordError = (value) => isObject(value) && fieldOf(value, RECORD) === true;
|
|
525
|
+
/**
|
|
526
|
+
* Renders the record and throws what Vitest prints for it: the record itself when it holds its contract, a
|
|
527
|
+
* `FailureRecordRefused` when it breaks one, and a refusal passed in unchanged, so refusing cannot recurse (R10).
|
|
528
|
+
*
|
|
529
|
+
* @internal
|
|
530
|
+
*/
|
|
531
|
+
const throwFailureRecord = (input) => {
|
|
532
|
+
if (isRefusalFailure(input.failure)) throw input.failure;
|
|
533
|
+
const record = renderFailureRecord(input);
|
|
534
|
+
throw copyDiffFields(printedError(input.failure, record), input.failure);
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/internal/provided.ts
|
|
538
|
+
/**
|
|
539
|
+
* The values the shared vitest config provides for the fork: the package under test's npm name and the workspace
|
|
540
|
+
* root every rendered path is relativized against. Both are read out of the run's provided context (Vitest's
|
|
541
|
+
* `inject`), so no production source reads a Node built-in, and an absent value is simply `undefined` — a rerun
|
|
542
|
+
* line then drops its package filter, and paths print absolute (R6, KTD5).
|
|
543
|
+
*
|
|
544
|
+
* @since 4.0.0
|
|
545
|
+
*/
|
|
546
|
+
/** @internal */
|
|
547
|
+
const packageKey = "@systemfsoftware/vitest:package";
|
|
548
|
+
/** @internal */
|
|
549
|
+
const workspaceRootKey = "@systemfsoftware/vitest:workspace-root";
|
|
550
|
+
const isText$1 = (value) => typeof value === "string";
|
|
551
|
+
const nonEmpty = (value) => value.length === 0 ? void 0 : value;
|
|
552
|
+
const nonEmptyText = (value) => isText$1(value) ? nonEmpty(value) : void 0;
|
|
553
|
+
const read = (key) => {
|
|
554
|
+
try {
|
|
555
|
+
return nonEmptyText(inject(key));
|
|
556
|
+
} catch {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
/** @internal */
|
|
561
|
+
const providedPackage = () => read(packageKey);
|
|
562
|
+
/** @internal */
|
|
563
|
+
const providedRoot = () => read(workspaceRootKey);
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/internal/failure-identity.ts
|
|
566
|
+
/**
|
|
567
|
+
* The identity one record names: the package, the test file and the scenario the rerun command writes (KTD1, R6).
|
|
568
|
+
*
|
|
569
|
+
* The task comes from Vitest, `TestRunner.getCurrentTest()`, which Vitest maintains for the test being run, so it
|
|
570
|
+
* is readable wherever a failure is rendered — including from the promise a kernel run returns, where no Effect
|
|
571
|
+
* service is in reach. The package name comes from the shared config's provided context, so it is this package's
|
|
572
|
+
* real npm name and never a guess from a directory; a run that provided none renders a rerun line without a
|
|
573
|
+
* `pnpm --filter` prefix.
|
|
574
|
+
*
|
|
575
|
+
* @since 4.0.0
|
|
576
|
+
*/
|
|
577
|
+
const LEVEL_SEPARATOR = " > ";
|
|
578
|
+
const EMPTY = "";
|
|
579
|
+
const isText = (value) => typeof value === "string" && value.length > 0;
|
|
580
|
+
const textOrEmpty = (value) => isText(value) ? value : EMPTY;
|
|
581
|
+
const firstText = (values) => textOrEmpty(values.find(isText));
|
|
582
|
+
const fileOf = (fullName) => textOrEmpty(fullName).split(LEVEL_SEPARATOR).at(0) ?? EMPTY;
|
|
583
|
+
const identityOf = (task) => ({
|
|
584
|
+
package: providedPackage() ?? EMPTY,
|
|
585
|
+
file: fileOf(task.fullName),
|
|
586
|
+
name: firstText([task.fullTestName, task.name])
|
|
587
|
+
});
|
|
588
|
+
const currentTask = () => TestRunner.getCurrentTest() ?? {};
|
|
589
|
+
/**
|
|
590
|
+
* The identity of the test being run, or of the task handed in. A run outside a test, or one whose task carries
|
|
591
|
+
* no file, yields an incomplete identity, which the renderer reports as the R6 breach it is.
|
|
592
|
+
*
|
|
593
|
+
* @internal
|
|
594
|
+
*/
|
|
595
|
+
const testIdentityOf = (task) => identityOf(task ?? currentTask());
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/internal/property/coverage.ts
|
|
598
|
+
/**
|
|
599
|
+
* @internal The R14 coverage classes: labelled input predicates, each with a minimum share of runs.
|
|
600
|
+
*
|
|
601
|
+
* A class fails only when QuickCheck's sequential test (`stdConfidence`) is confident its share is below
|
|
602
|
+
* the minimum, and is satisfied once the test is confident the share is at least 0.9 of the minimum
|
|
603
|
+
* (`TOLERANCE` at a certainty of 10^9). The tolerance is what makes the test terminate: a class sitting
|
|
604
|
+
* exactly at its minimum would otherwise draw forever.
|
|
605
|
+
*/
|
|
606
|
+
const CERTAINTY = 10 ** 9;
|
|
607
|
+
const TOLERANCE = .9;
|
|
608
|
+
const FIRST_BATCH = 64;
|
|
609
|
+
const BATCH_CAP = 4096;
|
|
610
|
+
const DRAW_CAP = 1e5;
|
|
611
|
+
const FAILURE_HEAD = "coverage";
|
|
612
|
+
/** The certainty's two-sided tail probability: `alpha / 2` for `alpha = 1 / CERTAINTY`. */
|
|
613
|
+
const TAIL_PROBABILITY = 1 / (2 * CERTAINTY);
|
|
614
|
+
const C1 = -.007784894002430293;
|
|
615
|
+
const C2 = -.3223964580411365;
|
|
616
|
+
const C3 = -2.400758277161838;
|
|
617
|
+
const C4 = -2.549732539343734;
|
|
618
|
+
const C5 = 4.374664141464968;
|
|
619
|
+
const C6 = 2.938163982698783;
|
|
620
|
+
const D1 = .007784695709041462;
|
|
621
|
+
const D2 = .3224671290700398;
|
|
622
|
+
const D3 = 2.445134137142996;
|
|
623
|
+
const D4 = 3.754408661907416;
|
|
624
|
+
const TAIL_NUMERATOR = [
|
|
625
|
+
C1,
|
|
626
|
+
C2,
|
|
627
|
+
C3,
|
|
628
|
+
C4,
|
|
629
|
+
C5,
|
|
630
|
+
C6
|
|
631
|
+
];
|
|
632
|
+
const TAIL_DENOMINATOR = [
|
|
633
|
+
D1,
|
|
634
|
+
D2,
|
|
635
|
+
D3,
|
|
636
|
+
D4
|
|
637
|
+
];
|
|
638
|
+
/** Horner's rule over a coefficient list, highest power first. */
|
|
639
|
+
const poly = (coefficients, x) => coefficients.reduceRight((total, coefficient) => total * x + coefficient, 0);
|
|
640
|
+
const tailSpread = (p) => Math.sqrt(-2 * Math.log(p));
|
|
641
|
+
const tailInverse = (spread) => poly(TAIL_NUMERATOR, spread) / poly(TAIL_DENOMINATOR, spread);
|
|
642
|
+
/** The inverse standard-normal CDF at the tail probability the certainty asks for. */
|
|
643
|
+
const TAIL_Z = tailInverse(tailSpread(TAIL_PROBABILITY));
|
|
644
|
+
const wilson = (hits, runs, z) => {
|
|
645
|
+
const share = hits / runs;
|
|
646
|
+
const zz = z * z;
|
|
647
|
+
return (share + zz / (2 * runs) + z * Math.sqrt(share * (1 - share) / runs + zz / (4 * runs * runs))) / (1 + zz / runs);
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* Thrown when a coverage class is confidently below its minimum share of runs.
|
|
651
|
+
*
|
|
652
|
+
* @internal
|
|
653
|
+
*/
|
|
654
|
+
var CoverageBelowMinimum = class extends Data.Error {};
|
|
655
|
+
const wilsonLower = (entry, runs) => wilson(entry.hits, runs, -TAIL_Z);
|
|
656
|
+
const wilsonUpper = (entry, runs) => wilson(entry.hits, runs, TAIL_Z);
|
|
657
|
+
const covered = (entry, runs) => wilsonLower(entry, runs) >= TOLERANCE * entry.minimum;
|
|
658
|
+
const uncovered = (entry, runs) => wilsonUpper(entry, runs) < entry.minimum;
|
|
659
|
+
const decided = (entry, runs) => covered(entry, runs) || uncovered(entry, runs);
|
|
660
|
+
const settled = (entry, runs) => runs === 0 ? false : decided(entry, runs);
|
|
661
|
+
const entriesOf = (classes) => Object.keys(classes).map((label) => entryAt(classes, label));
|
|
662
|
+
const entryAt = (classes, label) => [label, classOf(classes, label)];
|
|
663
|
+
const classOf = (classes, label) => classes[label] ?? {
|
|
664
|
+
hits: 0,
|
|
665
|
+
minimum: 0
|
|
666
|
+
};
|
|
667
|
+
const judgeOf = (classes, runs) => ({
|
|
668
|
+
classes: new Map(entriesOf(classes)),
|
|
669
|
+
runs
|
|
670
|
+
});
|
|
671
|
+
const openLabels = (judge) => [...judge.classes].filter(([, entry]) => !settled(entry, judge.runs)).map(([label]) => label);
|
|
672
|
+
const failedEntries = (judge) => [...judge.classes].filter(([, entry]) => uncovered(entry, judge.runs));
|
|
673
|
+
const describeShare = (hits, runs) => `${String(hits)}/${String(runs)}`;
|
|
674
|
+
const describeFailure = (label, entry, runs) => `${FAILURE_HEAD} ${label}: ${describeShare(entry.hits, runs)} of runs, below the required ${String(entry.minimum)}`;
|
|
675
|
+
const reportFailures = (judge) => failedEntries(judge).map(([label, entry]) => describeFailure(label, entry, judge.runs)).join("\n");
|
|
676
|
+
/** @internal */
|
|
677
|
+
const countHit = Function.dual(2, (classes, label) => {
|
|
678
|
+
const prior = classes.get(label);
|
|
679
|
+
if (prior !== void 0) classes.set(label, {
|
|
680
|
+
hits: prior.hits + 1,
|
|
681
|
+
minimum: prior.minimum
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
const countLabels = (classes, labels) => {
|
|
685
|
+
for (const label of labels) countHit(classes, label);
|
|
686
|
+
return classes;
|
|
687
|
+
};
|
|
688
|
+
const countDraws = (classes, drawn) => drawn.reduce(countLabels, new Map(classes));
|
|
689
|
+
const withDraws = (judge, drawn) => ({
|
|
690
|
+
classes: countDraws(judge.classes, drawn),
|
|
691
|
+
runs: judge.runs + drawn.length
|
|
692
|
+
});
|
|
693
|
+
const drawBatch = (judge, draw, batch) => withDraws(judge, draw.more(sizeOf(judge, batch)));
|
|
694
|
+
const sizeOf = (judge, batch) => Math.min(batch, DRAW_CAP - judge.runs);
|
|
695
|
+
const grown = (batch) => Math.min(batch * 2, BATCH_CAP);
|
|
696
|
+
const keepDrawing = (judge) => openLabels(judge).length > 0 && judge.runs < DRAW_CAP;
|
|
697
|
+
const toppedUp = (judge, draw, batch) => keepDrawing(judge) ? toppedUp(drawBatch(judge, draw, batch), draw, grown(batch)) : judge;
|
|
698
|
+
const reportOf$1 = (judge) => failedEntries(judge).length === 0 ? void 0 : reportFailures(judge);
|
|
699
|
+
/**
|
|
700
|
+
* Runs R14's sequential draw: tops up the undecided classes until every class is decided or the draw cap
|
|
701
|
+
* is reached, then reports the confidently under-covered classes, naming each label and its observed share.
|
|
702
|
+
*
|
|
703
|
+
* @internal
|
|
704
|
+
*/
|
|
705
|
+
const judgeCoverage = (options) => reportOf$1(toppedUp(judgeOf(options.classes, options.runs), options.draw, FIRST_BATCH));
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/internal/property/error.schema.ts
|
|
708
|
+
/**
|
|
709
|
+
* The generator's choice that refuted a property, as data: the seed and the shrink path fast-check recorded. The
|
|
710
|
+
* renderer reads it off the failure value instead of parsing the message (KTD5).
|
|
711
|
+
*
|
|
712
|
+
* @internal
|
|
713
|
+
*/
|
|
714
|
+
const PropertyReplay = Schema$1.Struct({
|
|
715
|
+
seed: Schema$1.Finite,
|
|
716
|
+
path: Schema$1.Array(Schema$1.Finite)
|
|
717
|
+
});
|
|
718
|
+
/**
|
|
719
|
+
* A falsified property: the message the failure carries, and the generator's choice when the check result recorded
|
|
720
|
+
* one, so the rerun line can replay it (KTD5).
|
|
721
|
+
*
|
|
722
|
+
* @internal
|
|
723
|
+
*/
|
|
724
|
+
var PropertyRefuted = class extends Schema$1.TaggedError()("PropertyRefuted", {
|
|
725
|
+
detail: Schema$1.String,
|
|
726
|
+
replay: Schema$1.optional(PropertyReplay)
|
|
727
|
+
}) {
|
|
728
|
+
get message() {
|
|
729
|
+
return this.detail;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* Thrown at file end when no property in the file refuted a subject's constant impostor (R12).
|
|
734
|
+
*
|
|
735
|
+
* @internal
|
|
736
|
+
*/
|
|
737
|
+
var VacuousProperty = class extends Schema$1.TaggedError()("VacuousProperty", { detail: Schema$1.String }) {
|
|
738
|
+
get message() {
|
|
739
|
+
return this.detail;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
/**
|
|
743
|
+
* Thrown when a law kind is given a record `of`, which cannot be spread into its subject.
|
|
744
|
+
*
|
|
745
|
+
* @internal
|
|
746
|
+
*/
|
|
747
|
+
var NonTupleOf = class extends Schema$1.TaggedError()("NonTupleOf", { detail: Schema$1.String }) {
|
|
748
|
+
get message() {
|
|
749
|
+
return this.detail;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/internal/property/impostor.ts
|
|
754
|
+
const SUBJECT_KEY = "the subject";
|
|
755
|
+
const NO_VALUE = "";
|
|
756
|
+
const isCallable = (value) => typeof value === "function";
|
|
757
|
+
const isCallableSubject = (target) => typeof target === "function";
|
|
758
|
+
const describeValue = (value) => isCallable(value) ? "a function" : String(value);
|
|
759
|
+
const describeEntry = (key, value) => `${key} always returned ${describeValue(value)}`;
|
|
760
|
+
const emptyState = () => ({ outputs: /* @__PURE__ */ new Map() });
|
|
761
|
+
const frozenAt = (state, key, compute) => state.outputs.has(key) ? state.outputs.get(key) : remember(state, key, compute());
|
|
762
|
+
const remember = (state, key, value) => {
|
|
763
|
+
state.outputs.set(key, value);
|
|
764
|
+
return value;
|
|
765
|
+
};
|
|
766
|
+
const callOutput = (state, fn, thisArg, args) => frozenAt(state, SUBJECT_KEY, () => Reflect.apply(fn, thisArg, args));
|
|
767
|
+
const memberWrapper = (state, key, fn) => (...values) => frozenAt(state, key, () => Reflect.apply(fn, void 0, values));
|
|
768
|
+
const memberWrapperOf = (state, fn, key) => fn === void 0 ? void 0 : memberWrapper(state, key, fn);
|
|
769
|
+
const appliedImpostor = (state, target, thisArg, args) => isCallableSubject(target) ? callOutput(state, target, thisArg, args) : void 0;
|
|
770
|
+
const memberImpostor = (state, target, key) => memberWrapperOf(state, target[key], key);
|
|
771
|
+
const readImpostor = (state, target, property) => isCallableSubject(target) ? Reflect.get(target, property, target) : memberRead(state, target, property);
|
|
772
|
+
const memberRead = (state, target, property) => typeof property === "string" ? memberImpostor(state, target, property) : void 0;
|
|
773
|
+
const impostorHandler = (state) => ({
|
|
774
|
+
apply: (target, thisArg, args) => appliedImpostor(state, target, thisArg, args),
|
|
775
|
+
get: (target, property) => readImpostor(state, target, property)
|
|
776
|
+
});
|
|
777
|
+
const describeFrozen = (state) => [...state.outputs].map(([key, value]) => describeEntry(key, value)).join("; ");
|
|
778
|
+
/**
|
|
779
|
+
* A constant impostor of `subject`: a function returns its first output forever, and so does each member
|
|
780
|
+
* of a record.
|
|
781
|
+
*
|
|
782
|
+
* @internal
|
|
783
|
+
*/
|
|
784
|
+
const impostorOf = (subject) => {
|
|
785
|
+
const state = emptyState();
|
|
786
|
+
return {
|
|
787
|
+
impostor: new Proxy(subject, impostorHandler(state)),
|
|
788
|
+
frozen: () => describeFrozen(state)
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
/** @internal */
|
|
792
|
+
const subjectLabel = (subject) => isCallableSubject(subject) ? `a function of ${String(subject.length)} argument(s)` : `a record of ${Object.keys(subject).sort().join(", ")}`;
|
|
793
|
+
const judgeFor = (subject) => ({
|
|
794
|
+
label: subjectLabel(subject),
|
|
795
|
+
properties: [],
|
|
796
|
+
refuters: [],
|
|
797
|
+
frozen: NO_VALUE,
|
|
798
|
+
runs: 0
|
|
799
|
+
});
|
|
800
|
+
const trackProperty = (judge, property) => judge.properties.includes(property) ? judge : {
|
|
801
|
+
...judge,
|
|
802
|
+
properties: [...judge.properties, property]
|
|
803
|
+
};
|
|
804
|
+
const trackRefuter = (judge, property) => judge.refuters.includes(property) ? judge : {
|
|
805
|
+
...judge,
|
|
806
|
+
refuters: [...judge.refuters, property]
|
|
807
|
+
};
|
|
808
|
+
const trackVerdict = (judge, property, verdict) => {
|
|
809
|
+
const named = trackProperty(judge, property);
|
|
810
|
+
return verdict.refuted ? trackRefuter(named, property) : {
|
|
811
|
+
...named,
|
|
812
|
+
frozen: verdict.frozen,
|
|
813
|
+
runs: verdict.runs
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
const recordJudge = (state, subject, property, verdict) => {
|
|
817
|
+
const prior = state.judges.get(subject) ?? judgeFor(subject);
|
|
818
|
+
state.judges.set(subject, trackVerdict(prior, property, verdict));
|
|
819
|
+
};
|
|
820
|
+
const isVacuous = (judge) => judge.refuters.length === 0;
|
|
821
|
+
const describeVacuous = (judge) => `${judge.properties.join(", ")}: no property in this file refuted the constant impostor of this subject (${judge.label}), so nothing here pins it down. It held for ${judge.runs} run(s) against a fake that ${judge.frozen === NO_VALUE ? "was never called" : judge.frozen}, whatever the input. Laws that only relate outputs to each other (additivity, idempotence, commutativity, round trips through the subject) hold for such constants. Pin the output to the input: compare against an independent model (\`subject(x)\` equals a straightforward reimplementation), or conjoin a base case (\`subject([one])\` equals its known value). Also check that the body calls the \`subject\` it was given, not the imported implementation.`;
|
|
822
|
+
const finaliseJudges = (state, onProblems) => {
|
|
823
|
+
const vacuous = [...state.judges.values()].filter(isVacuous).map(describeVacuous);
|
|
824
|
+
if (vacuous.length === 0) return;
|
|
825
|
+
throw onProblems(vacuous.join("\n\n"));
|
|
826
|
+
};
|
|
827
|
+
const ledgerOf = (state, onProblems) => ({
|
|
828
|
+
record: (subject, property, verdict) => recordJudge(state, subject, property, verdict),
|
|
829
|
+
finalise: () => finaliseJudges(state, onProblems)
|
|
830
|
+
});
|
|
831
|
+
/**
|
|
832
|
+
* Collects the impostor verdicts of every property in the file and decides, at file end, which subjects
|
|
833
|
+
* nothing refuted.
|
|
834
|
+
*
|
|
835
|
+
* @internal
|
|
836
|
+
*/
|
|
837
|
+
const makeFileLedger = (onProblems) => ledgerOf({ judges: /* @__PURE__ */ new Map() }, onProblems);
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/internal/property/kinds.ts
|
|
840
|
+
/**
|
|
841
|
+
* @internal The R13 law kinds: `model`, `metamorphic`, `roundTrip` and `invariant` are sugar over the
|
|
842
|
+
* gated property (R11/R12); `idempotent` and `deterministic` are declared exempt — they skip the impostor
|
|
843
|
+
* gate, are reported as exempt, and are the only exemptions.
|
|
844
|
+
*
|
|
845
|
+
* Each kind owns the comparison, so there is nowhere to write `return true`: the author hands over
|
|
846
|
+
* functions, and the kind relates their outputs with Effect's structural `Equal`.
|
|
847
|
+
*/
|
|
848
|
+
/**
|
|
849
|
+
* The generated values as the argument list a law subject is called with.
|
|
850
|
+
*
|
|
851
|
+
* @internal
|
|
852
|
+
*/
|
|
853
|
+
const spreadValues = (values) => Array.isArray(values) ? values : refuseRecordOf();
|
|
854
|
+
const refuseRecordOf = () => {
|
|
855
|
+
throw new NonTupleOf({ detail: "a law kind spreads the generated values into its subject, so `of` must be a tuple; a record `of` holds functions of the record, which only it.prop reaches" });
|
|
856
|
+
};
|
|
857
|
+
const call = (subject, values) => subject(...spreadValues(values));
|
|
858
|
+
const equalOutputs = (original, transformed) => Equal.equals(original, transformed);
|
|
859
|
+
/** @internal */
|
|
860
|
+
const modelHolds = (oracle) => (subject, values) => Equal.equals(call(subject, values), call(oracle, values));
|
|
861
|
+
/** @internal */
|
|
862
|
+
const metamorphicHolds = (relation) => (subject, values) => (relation.relate ?? equalOutputs)(call(subject, values), call(subject, relation.transform(...spreadValues(values))));
|
|
863
|
+
/** @internal */
|
|
864
|
+
const roundTripHolds = (decode) => (subject, values) => Equal.equals(decode(call(subject, values)), values);
|
|
865
|
+
/** @internal */
|
|
866
|
+
const invariantHolds = (holds) => (subject, values) => holds(call(subject, values), ...spreadValues(values));
|
|
867
|
+
const firstOf = (values) => values[0];
|
|
868
|
+
const sameTwice = (subject, value) => value === void 0 ? false : Equal.equals(subject(subject(value)), subject(value));
|
|
869
|
+
/** @internal */
|
|
870
|
+
const idempotentHolds = () => (subject, values) => sameTwice(subject, firstOf(spreadValues(values)));
|
|
871
|
+
/** @internal */
|
|
872
|
+
const deterministicHolds = () => (subject, values) => Equal.equals(call(subject, values), call(subject, values));
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/internal/property/replay.schema.ts
|
|
875
|
+
/**
|
|
876
|
+
* The token effect's falsification runner records, as the Schema that decodes it (KTD5). One falsification's
|
|
877
|
+
* coordinates ride in a JSON array: a leading `0` naming a numeric seed, the seed text, the attempt, the size,
|
|
878
|
+
* the shrink path, and the failure tag.
|
|
879
|
+
*
|
|
880
|
+
* @since 4.0.0
|
|
881
|
+
*/
|
|
882
|
+
/** @internal */
|
|
883
|
+
const ReplayToken = Schema$1.fromJsonString(Schema$1.Tuple([
|
|
884
|
+
Schema$1.Finite,
|
|
885
|
+
Schema$1.Union([Schema$1.Finite, Schema$1.String]),
|
|
886
|
+
Schema$1.Finite,
|
|
887
|
+
Schema$1.Finite,
|
|
888
|
+
Schema$1.Array(Schema$1.Finite),
|
|
889
|
+
Schema$1.String
|
|
890
|
+
]));
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/internal/property/replay.ts
|
|
893
|
+
/**
|
|
894
|
+
* The replay value a property counterexample carries (KTD5): the seed and the shrink path the generator recorded,
|
|
895
|
+
* read off the failure value and out of the check result as data. Nothing here reads a failure message.
|
|
896
|
+
*
|
|
897
|
+
* The token `effect`'s falsification runner writes is decoded here — never hand-parsed — into the seed and path R6
|
|
898
|
+
* prints.
|
|
899
|
+
*
|
|
900
|
+
* @since 4.0.0
|
|
901
|
+
*/
|
|
902
|
+
/** The token's leading kind that names a numeric seed. */
|
|
903
|
+
const NUMERIC_SEED = 0;
|
|
904
|
+
const numericSeedOf = (token) => token[0] === NUMERIC_SEED ? Number(token[1]) : void 0;
|
|
905
|
+
const replayOf = (token) => Option.map(Option.fromNullishOr(numericSeedOf(token)), (seed) => ({
|
|
906
|
+
seed,
|
|
907
|
+
path: token[4]
|
|
908
|
+
}));
|
|
909
|
+
/** @internal */
|
|
910
|
+
const replayOfToken = (replay) => Option.getOrUndefined(Option.flatMap(Schema$1.decodeOption(ReplayToken)(replay), replayOf));
|
|
911
|
+
const refutationOf = (value) => Schema$1.is(PropertyRefuted)(value) ? value : void 0;
|
|
912
|
+
/** @internal */
|
|
913
|
+
const replayOfFailure = (value) => Option.getOrUndefined(Option.flatMap(Option.fromNullishOr(refutationOf(value)), (failure) => Option.fromNullishOr(failure.replay)));
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/internal/property/engine.ts
|
|
916
|
+
/**
|
|
917
|
+
* @internal The lawful property engine (R11-R15): a property names the function under test, takes a budget
|
|
918
|
+
* that merges its own fields over the configured default over `runs: 100`, accepts only a literal boolean
|
|
919
|
+
* verdict (or an `Effect` of one), judges coverage classes with QuickCheck's sequential test, and defers the
|
|
920
|
+
* constant-impostor verdict to the end of the file.
|
|
921
|
+
*
|
|
922
|
+
* `of` accepts what upstream accepts — a tuple or record of Schemas or Arbitraries (KTD10) — and `holds`
|
|
923
|
+
* receives the generated values exactly as drawn, typed by the gens that produced them.
|
|
924
|
+
*/
|
|
925
|
+
const REWRITE = "it.prop(name, { of, subject, runs }, holds)";
|
|
926
|
+
const DEFAULT_RUNS = 100;
|
|
927
|
+
const isPositiveInteger = (runs) => Number.isInteger(runs) && runs > 0;
|
|
928
|
+
const describeRuns = (runs) => Number.isInteger(runs) ? `${String(runs)}` : "a non-integer";
|
|
929
|
+
const invalidPropertyBudget = (name, runs) => `${name}: pass a positive integer \`runs\`; received ${describeRuns(runs)}. Write ${REWRITE}.`;
|
|
930
|
+
const invalidProvidedBudget = (runs) => `the configured property budget (${checkDefaultsKey}) supplies \`runs\`: ${describeRuns(runs)}; it must be a positive integer.`;
|
|
931
|
+
const requirePositiveRuns = (message, runs) => {
|
|
932
|
+
if (isInvalidRuns(runs)) throw new InvalidBudget({ detail: message(runs) });
|
|
933
|
+
};
|
|
934
|
+
const isInvalidRuns = (runs) => runs !== void 0 && isPositiveInteger(runs) === false;
|
|
935
|
+
/** The property's own explicit fields, a property-level `runs` outranking the same field in `arbitrary`. */
|
|
936
|
+
const explicitOptions = (spec) => spec.runs === void 0 ? { ...spec.arbitrary } : {
|
|
937
|
+
...spec.arbitrary,
|
|
938
|
+
runs: spec.runs
|
|
939
|
+
};
|
|
940
|
+
const mergedOptions = (provided, explicit) => ({
|
|
941
|
+
runs: DEFAULT_RUNS,
|
|
942
|
+
...provided,
|
|
943
|
+
...explicit
|
|
944
|
+
});
|
|
945
|
+
const resolvedRuns = (options) => options.runs ?? DEFAULT_RUNS;
|
|
946
|
+
/**
|
|
947
|
+
* The effective check options: the property's own fields over the configured default over `runs: 100`,
|
|
948
|
+
* merged field by field so a property that sets only `runs` still inherits the configured size and caps.
|
|
949
|
+
*/
|
|
950
|
+
const resolveBudget = (name, spec, provided) => {
|
|
951
|
+
const explicit = explicitOptions(spec);
|
|
952
|
+
requirePositiveRuns((runs) => invalidPropertyBudget(name, runs), explicit.runs);
|
|
953
|
+
requirePositiveRuns(invalidProvidedBudget, provided?.runs);
|
|
954
|
+
const options = mergedOptions(provided, explicit);
|
|
955
|
+
return {
|
|
956
|
+
runs: resolvedRuns(options),
|
|
957
|
+
options
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
const toArbitrary = (input) => Schema$1.isSchema(input) ? Arbitrary.schema(input) : input;
|
|
961
|
+
const assignArbitrary = (built, of, key) => {
|
|
962
|
+
const input = of[key];
|
|
963
|
+
if (input !== void 0) built[key] = toArbitrary(input);
|
|
964
|
+
};
|
|
965
|
+
const recordArbitraries = (of) => {
|
|
966
|
+
const built = {};
|
|
967
|
+
for (const key of Object.keys(of)) assignArbitrary(built, of, key);
|
|
968
|
+
return built;
|
|
969
|
+
};
|
|
970
|
+
const isTupleOf = (of) => Array.isArray(of);
|
|
971
|
+
function arbitraryOf(of) {
|
|
972
|
+
return isTupleOf(of) ? Arbitrary.all(of.map(toArbitrary)) : Arbitrary.all(recordArbitraries(of));
|
|
973
|
+
}
|
|
974
|
+
const minimumOf = (entry) => entry[1];
|
|
975
|
+
const seedClasses = (entries) => {
|
|
976
|
+
const classes = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const [label, entry] of entries) classes.set(label, {
|
|
978
|
+
hits: 0,
|
|
979
|
+
minimum: minimumOf(entry)
|
|
980
|
+
});
|
|
981
|
+
return classes;
|
|
982
|
+
};
|
|
983
|
+
const satisfiedLabels = (entries, values) => entries.filter(([, entry]) => entry[0](...spreadValues(values))).map(([label]) => label);
|
|
984
|
+
const observeRun = (classes, counter, entries, values) => {
|
|
985
|
+
counter.runs = counter.runs + 1;
|
|
986
|
+
for (const label of satisfiedLabels(entries, values)) countHit(classes, label);
|
|
987
|
+
};
|
|
988
|
+
const drawFor = (entries, arbitrary) => ({ more: (count) => Effect.runSync(Arbitrary.sampleEffect(arbitrary, { count }).pipe(Effect.orDie)).map((values) => satisfiedLabels(entries, values)) });
|
|
989
|
+
const noCoverage = () => ({
|
|
990
|
+
observe: () => void 0,
|
|
991
|
+
judge: () => void 0
|
|
992
|
+
});
|
|
993
|
+
const liveCoverage = (cover, arbitrary) => {
|
|
994
|
+
const entries = Object.entries(cover);
|
|
995
|
+
const classes = seedClasses(entries);
|
|
996
|
+
const counter = { runs: 0 };
|
|
997
|
+
return {
|
|
998
|
+
observe: (values) => observeRun(classes, counter, entries, values),
|
|
999
|
+
judge: () => judgeCoverage({
|
|
1000
|
+
classes: Object.fromEntries(classes),
|
|
1001
|
+
runs: counter.runs,
|
|
1002
|
+
draw: drawFor(entries, arbitrary)
|
|
1003
|
+
})
|
|
1004
|
+
};
|
|
1005
|
+
};
|
|
1006
|
+
const makeCoverage = (cover, arbitrary) => cover === void 0 ? noCoverage() : liveCoverage(cover, arbitrary);
|
|
1007
|
+
const describeVerdict = (verdict) => Effect.isEffect(verdict) ? "an Effect, which this lane never runs" : `a ${typeof verdict}`;
|
|
1008
|
+
const nonBoolean = (verdict, violations) => {
|
|
1009
|
+
violations.add(describeVerdict(verdict));
|
|
1010
|
+
return true;
|
|
1011
|
+
};
|
|
1012
|
+
const literalVerdict = (verdict, violations) => typeof verdict === "boolean" ? verdict : nonBoolean(verdict, violations);
|
|
1013
|
+
const effectVerdict = (lane, verdict, violations) => lane === "sync" ? Effect.succeed(nonBoolean(verdict, violations)) : Effect.map(verdict, (value) => literalVerdict(value, violations));
|
|
1014
|
+
const verdictFor = (run, values, violations) => {
|
|
1015
|
+
run.observe(values);
|
|
1016
|
+
const verdict = run.holds(run.subject, values);
|
|
1017
|
+
return Effect.isEffect(verdict) ? effectVerdict(run.lane, verdict, violations) : Effect.succeed(literalVerdict(verdict, violations));
|
|
1018
|
+
};
|
|
1019
|
+
const tolerateInterruption = (cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.fail(cause);
|
|
1020
|
+
const guardedVerdict = (run, violations) => (values) => Effect.catchCause(Effect.suspend(() => verdictFor(run, values, violations)), tolerateInterruption);
|
|
1021
|
+
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}.`;
|
|
1022
|
+
const runCheck = (run) => {
|
|
1023
|
+
const violations = /* @__PURE__ */ new Set();
|
|
1024
|
+
return Arbitrary.checkEffect(run.arbitrary, guardedVerdict(run, violations), run.options).pipe(Effect.map((result) => ({
|
|
1025
|
+
violations,
|
|
1026
|
+
result
|
|
1027
|
+
})));
|
|
1028
|
+
};
|
|
1029
|
+
const reportOf = (checked) => Arbitrary.formatCheckFailure(checked.result);
|
|
1030
|
+
const violationOf = (name, checked) => checked.violations.size === 0 ? void 0 : violationMessage(name, [...checked.violations]);
|
|
1031
|
+
const isRefuted = (checked) => checked.violations.size > 0 || reportOf(checked) !== void 0;
|
|
1032
|
+
const dieWithSite = (error, site) => Effect.die(withRaisingFrame(error, site));
|
|
1033
|
+
const dieViolation = (detail, site) => dieWithSite(new NonBooleanVerdict({ detail }), site);
|
|
1034
|
+
const dieReported = (name, report, replay, site) => {
|
|
1035
|
+
const detail = `${name}: the property was falsified. ${report}`;
|
|
1036
|
+
return dieWithSite(new PropertyRefuted(replay === void 0 ? { detail } : {
|
|
1037
|
+
detail,
|
|
1038
|
+
replay
|
|
1039
|
+
}), site);
|
|
1040
|
+
};
|
|
1041
|
+
const dieUncovered = (failure, site) => dieWithSite(new CoverageBelowMinimum({ message: failure }), site);
|
|
1042
|
+
const impostorHolds = (holds, impostor) => (_subject, values) => holds(impostor.impostor, values);
|
|
1043
|
+
const recordImpostor = (run, budget, impostor, checked) => {
|
|
1044
|
+
const verdict = {
|
|
1045
|
+
refuted: isRefuted(checked),
|
|
1046
|
+
frozen: impostor.frozen(),
|
|
1047
|
+
runs: budget.runs
|
|
1048
|
+
};
|
|
1049
|
+
gatedLedger.record(run.subject, run.name, verdict);
|
|
1050
|
+
};
|
|
1051
|
+
const impostorRun = (run, budget) => {
|
|
1052
|
+
const impostor = impostorOf(run.subject);
|
|
1053
|
+
const retry = {
|
|
1054
|
+
...run,
|
|
1055
|
+
holds: impostorHolds(run.holds, impostor),
|
|
1056
|
+
observe: noObserve
|
|
1057
|
+
};
|
|
1058
|
+
return runCheck(retry).pipe(Effect.map((checked) => recordImpostor(retry, budget, impostor, checked)));
|
|
1059
|
+
};
|
|
1060
|
+
const noObserve = () => void 0;
|
|
1061
|
+
const gateRun = (run, budget, gate) => gate ? impostorRun(run, budget) : Effect.void;
|
|
1062
|
+
const finishPassed = (run, budget, coverage, gate) => {
|
|
1063
|
+
const failure = coverage.judge();
|
|
1064
|
+
return failure === void 0 ? gateRun(run, budget, gate) : dieUncovered(`${run.name}: ${failure}`, run.site);
|
|
1065
|
+
};
|
|
1066
|
+
const falsifiedOf = (result) => "replay" in result ? result : void 0;
|
|
1067
|
+
const replayOfChecked = (checked) => Option.getOrUndefined(Option.flatMap(Option.fromNullishOr(falsifiedOf(checked.result)), (falsified) => Option.fromNullishOr(replayOfToken(falsified.replay))));
|
|
1068
|
+
const settleReport = (run, budget, coverage, gate, report, replay) => report === void 0 ? finishPassed(run, budget, coverage, gate) : dieReported(run.name, report, replay, run.site);
|
|
1069
|
+
const settle = (registration, run, budget, coverage, checked) => {
|
|
1070
|
+
const violation = violationOf(registration.name, checked);
|
|
1071
|
+
return violation === void 0 ? settleReport(run, budget, coverage, registration.gate, reportOf(checked), replayOfChecked(checked)) : dieViolation(violation, registration.site);
|
|
1072
|
+
};
|
|
1073
|
+
const checkOf = (registration, arbitrary, budget, observe) => ({
|
|
1074
|
+
name: registration.name,
|
|
1075
|
+
lane: registration.lane,
|
|
1076
|
+
subject: registration.spec.subject,
|
|
1077
|
+
holds: registration.holds,
|
|
1078
|
+
arbitrary,
|
|
1079
|
+
observe,
|
|
1080
|
+
options: budget.options,
|
|
1081
|
+
site: registration.site
|
|
1082
|
+
});
|
|
1083
|
+
const program = (registration) => Effect.gen(function* () {
|
|
1084
|
+
const budget = resolveBudget(registration.name, registration.spec, providedCheckDefaults());
|
|
1085
|
+
const arbitrary = arbitraryOf(registration.spec.of);
|
|
1086
|
+
const coverage = makeCoverage(registration.spec.cover, arbitrary);
|
|
1087
|
+
const run = checkOf(registration, arbitrary, budget, coverage.observe);
|
|
1088
|
+
const checked = yield* runCheck(run);
|
|
1089
|
+
yield* settle(registration, run, budget, coverage, checked);
|
|
1090
|
+
});
|
|
1091
|
+
const programOf = (registration) => () => registration.runtime.provide(program(registration));
|
|
1092
|
+
const selfModelMessage = (name) => `${name}: the model is the subject itself; a model law must compare against an independent oracle.`;
|
|
1093
|
+
const gatedLedger = makeFileLedger((message) => new VacuousProperty({ detail: message }));
|
|
1094
|
+
V.afterAll(() => {
|
|
1095
|
+
gatedLedger.finalise();
|
|
1096
|
+
});
|
|
1097
|
+
const registeredName = (name, kind, exempt) => exempt ? `${name} [exempt: ${kind}]` : name;
|
|
1098
|
+
const isSelfModel = (subject, oracle) => Object.is(subject, oracle);
|
|
1099
|
+
/**
|
|
1100
|
+
* The property surface over one runtime: `prop`, `effectProp`, and the R13 law kinds routed through the gate.
|
|
1101
|
+
*
|
|
1102
|
+
* @internal
|
|
1103
|
+
*/
|
|
1104
|
+
const makeProperty = (runtime) => {
|
|
1105
|
+
const body = (name, spec, holds, lane, gate) => {
|
|
1106
|
+
const registration = {
|
|
1107
|
+
runtime,
|
|
1108
|
+
name,
|
|
1109
|
+
spec,
|
|
1110
|
+
holds,
|
|
1111
|
+
lane,
|
|
1112
|
+
gate,
|
|
1113
|
+
site: callFrame()
|
|
1114
|
+
};
|
|
1115
|
+
runtime.register(name, programOf(registration));
|
|
1116
|
+
};
|
|
1117
|
+
const refuseSelfModel = (name) => {
|
|
1118
|
+
const site = callFrame();
|
|
1119
|
+
runtime.register(name, () => dieWithSite(new VacuousProperty({ detail: selfModelMessage(name) }), site));
|
|
1120
|
+
};
|
|
1121
|
+
const gated = (name, spec, holds, kind, exempt) => body(registeredName(name, kind, exempt), spec, holds, "sync", exempt === false);
|
|
1122
|
+
const model = (name, spec, oracle) => isSelfModel(spec.subject, oracle) ? refuseSelfModel(name) : gated(name, spec, modelHolds(oracle), "model", false);
|
|
1123
|
+
const idempotent = (name, spec) => gated(name, spec, idempotentHolds(), "idempotent", true);
|
|
1124
|
+
const deterministic = (name, spec) => gated(name, spec, deterministicHolds(), "deterministic", true);
|
|
1125
|
+
return {
|
|
1126
|
+
prop: (name, spec, holds) => body(name, spec, holds, "sync", true),
|
|
1127
|
+
effectProp: (name, spec, holds) => body(name, spec, holds, "effect", true),
|
|
1128
|
+
law: {
|
|
1129
|
+
model,
|
|
1130
|
+
metamorphic: (name, spec, relation) => gated(name, spec, metamorphicHolds(relation), "metamorphic", false),
|
|
1131
|
+
roundTrip: (name, spec, decode) => gated(name, spec, roundTripHolds(decode), "roundTrip", false),
|
|
1132
|
+
invariant: (name, spec, holds) => gated(name, spec, invariantHolds(holds), "invariant", false),
|
|
1133
|
+
idempotent,
|
|
1134
|
+
deterministic
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/internal/span-recorder.ts
|
|
1140
|
+
/**
|
|
1141
|
+
* The run recorder: an in-memory {@link Tracer.Tracer} that keeps every span one run opened (KTD2). Nothing is
|
|
1142
|
+
* exported from a recorder — its spans are read in process by the failure renderer and dropped with the run.
|
|
1143
|
+
*
|
|
1144
|
+
* A recorder is per run: the kernel's baseline, each seeded run and each replay each get their own, and so does
|
|
1145
|
+
* every program the runner drives. Installing it is the caller's job, through `Effect.withTracer`.
|
|
1146
|
+
*
|
|
1147
|
+
* @since 4.0.0
|
|
1148
|
+
*/
|
|
1149
|
+
/** @internal */
|
|
1150
|
+
const createSpanRecorder = () => {
|
|
1151
|
+
const spans = [];
|
|
1152
|
+
return {
|
|
1153
|
+
tracer: Tracer.make({ span(options) {
|
|
1154
|
+
const span = new Tracer.NativeSpan(options);
|
|
1155
|
+
spans.push(span);
|
|
1156
|
+
return span;
|
|
1157
|
+
} }),
|
|
1158
|
+
spans
|
|
1159
|
+
};
|
|
1160
|
+
};
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/internal/recorded-run.ts
|
|
1163
|
+
/**
|
|
1164
|
+
* The in-process way to run a program the way the runner runs a lane and read back the record its failure rendered
|
|
1165
|
+
* (R11, KTD10): a corpus drives a fixture through this instead of spawning Vitest, and without importing `src/`.
|
|
1166
|
+
*
|
|
1167
|
+
* The run gets its own check ledger — its own `expect` — so a corpus fixture's `Then` asserts on the check it
|
|
1168
|
+
* answered with, not on the check the corpus test itself already spent, and its own recorder, so the record is
|
|
1169
|
+
* rendered from exactly the spans the run opened.
|
|
1170
|
+
*
|
|
1171
|
+
* @since 4.0.0
|
|
1172
|
+
*/
|
|
1173
|
+
const renderRecord = (failure, spans, identity) => renderFailureRecord({
|
|
1174
|
+
failure,
|
|
1175
|
+
spans,
|
|
1176
|
+
identity,
|
|
1177
|
+
replay: replayOfFailure(failure),
|
|
1178
|
+
root: providedRoot()
|
|
1179
|
+
});
|
|
1180
|
+
const outcomeOf = (exit, recorder, identity) => Exit.isSuccess(exit) ? void 0 : renderRecord(Cause.squash(exit.cause), recorder.spans, identity);
|
|
1181
|
+
const provideRun = (effect, ledger, ctx) => effect.pipe(Effect.provideService(Asserted, ledger.asserted), Effect.provideService(VitestTestContext, ctx));
|
|
1182
|
+
const runRecorded = (ctx, program) => {
|
|
1183
|
+
const ledger = makeLedger(ctx);
|
|
1184
|
+
const recorder = createSpanRecorder();
|
|
1185
|
+
return Effect.runPromiseExit(provideRun(program(checksFor(ledger)), ledger, ctx).pipe(Effect.withTracer(recorder.tracer))).then((exit) => outcomeOf(exit, recorder, testIdentityOf()));
|
|
1186
|
+
};
|
|
1187
|
+
/**
|
|
1188
|
+
* Runs one program as the runner runs a lane, with a fresh ledger and its own recorder, and resolves with the
|
|
1189
|
+
* record its failure rendered — or `undefined` when it passed.
|
|
1190
|
+
*
|
|
1191
|
+
* @internal
|
|
1192
|
+
*/
|
|
1193
|
+
const recordOfRun = (program) => {
|
|
1194
|
+
const task = TestRunner.getCurrentTest();
|
|
1195
|
+
if (task === void 0) return Promise.resolve(void 0);
|
|
1196
|
+
return runRecorded(task.context, program);
|
|
1197
|
+
};
|
|
1198
|
+
const capturingRuntime = (programs) => ({
|
|
1199
|
+
register: (_name, program) => {
|
|
1200
|
+
programs.push(program);
|
|
1201
|
+
},
|
|
1202
|
+
provide: (effect) => effect
|
|
1203
|
+
});
|
|
1204
|
+
const capturedProgram = (programs) => Option.getOrThrow(Option.fromNullishOr(programs[0]));
|
|
1205
|
+
/**
|
|
1206
|
+
* Runs one property the way the runner runs `it.prop`, with a fresh ledger and its own recorder, and resolves with
|
|
1207
|
+
* the record its falsification rendered — or `undefined` when it held.
|
|
1208
|
+
*
|
|
1209
|
+
* @internal
|
|
1210
|
+
*/
|
|
1211
|
+
const recordOfProperty = (input) => {
|
|
1212
|
+
const programs = [];
|
|
1213
|
+
makeProperty(capturingRuntime(programs)).prop(input.name, input.spec, input.holds);
|
|
1214
|
+
return recordOfRun(capturedProgram(programs));
|
|
1215
|
+
};
|
|
1216
|
+
//#endregion
|
|
1217
|
+
export { replayOfFailure as a, isFailureRecordError as c, summaryOf as d, InvalidBudget as f, Slop as h, makeProperty as i, throwFailureRecord as l, NonBooleanVerdict as m, recordOfRun as n, testIdentityOf as o, LeakedState as p, createSpanRecorder as r, providedRoot as s, recordOfProperty as t, renderFailureRecord as u };
|