pabst-checker 0.12.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/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/build-spec.d.ts +2 -0
- package/dist/build-spec.js +46 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +119 -0
- package/dist/codegen.d.ts +6 -0
- package/dist/codegen.js +30 -0
- package/dist/desugar.d.ts +5 -0
- package/dist/desugar.js +136 -0
- package/dist/discover.d.ts +16 -0
- package/dist/discover.js +113 -0
- package/dist/domains.d.ts +47 -0
- package/dist/domains.js +130 -0
- package/dist/emit.d.ts +2 -0
- package/dist/emit.js +74 -0
- package/dist/envelope.d.ts +29 -0
- package/dist/envelope.js +28 -0
- package/dist/equations.d.ts +8 -0
- package/dist/equations.js +256 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +9 -0
- package/dist/extract.d.ts +21 -0
- package/dist/extract.js +177 -0
- package/dist/formula-ast.d.ts +26 -0
- package/dist/formula-ast.js +18 -0
- package/dist/formula-lexer.d.ts +31 -0
- package/dist/formula-lexer.js +191 -0
- package/dist/formula-parser.d.ts +2 -0
- package/dist/formula-parser.js +122 -0
- package/dist/free-idents.d.ts +6 -0
- package/dist/free-idents.js +60 -0
- package/dist/ir.d.ts +45 -0
- package/dist/ir.js +1 -0
- package/dist/issue.d.ts +26 -0
- package/dist/issue.js +18 -0
- package/dist/lower.d.ts +12 -0
- package/dist/lower.js +37 -0
- package/dist/parse-formula.d.ts +12 -0
- package/dist/parse-formula.js +78 -0
- package/dist/prefix-parser.d.ts +7 -0
- package/dist/prefix-parser.js +188 -0
- package/dist/qualified-name.d.ts +5 -0
- package/dist/qualified-name.js +11 -0
- package/dist/range.d.ts +5 -0
- package/dist/range.js +152 -0
- package/dist/regex-guard.d.ts +28 -0
- package/dist/regex-guard.js +100 -0
- package/dist/run.d.ts +26 -0
- package/dist/run.js +50 -0
- package/dist/runtime.d.ts +25 -0
- package/dist/runtime.js +62 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.js +20 -0
- package/package.json +67 -0
package/dist/discover.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, globSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { PabstError } from "./errors.js";
|
|
5
|
+
const TS_EXT = /\.(ts|tsx|mts|cts)$/;
|
|
6
|
+
const DECL_EXT = /\.d\.(ts|mts|cts)$/;
|
|
7
|
+
/**
|
|
8
|
+
* True for TypeScript source files pabst should scan: .ts/.tsx/.mts/.cts,
|
|
9
|
+
* excluding declaration files — tsc copies JSDoc into declarations, so
|
|
10
|
+
* scanning them alongside their sources extracts every property twice.
|
|
11
|
+
*/
|
|
12
|
+
export function isTsSource(file) {
|
|
13
|
+
return TS_EXT.test(file) && !DECL_EXT.test(file);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The files pabst should scan: matches of the given patterns or, with no
|
|
17
|
+
* patterns, zero-argument discovery (the tsconfig.json file list, then the
|
|
18
|
+
* src/ convention). Throws PabstError when nothing usable is found.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveFiles(patterns) {
|
|
21
|
+
if (patterns.length > 0) {
|
|
22
|
+
return { files: globbedFiles(patterns), source: "arguments" };
|
|
23
|
+
}
|
|
24
|
+
return discoverFiles();
|
|
25
|
+
}
|
|
26
|
+
// A pattern that itself names declaration files (index.d.ts, types/**/*.d.ts)
|
|
27
|
+
// is an explicit ask for them and is honored; any other pattern drops
|
|
28
|
+
// declaration matches (see isTsSource) so build output sitting next to its
|
|
29
|
+
// sources is not scanned twice.
|
|
30
|
+
function globbedFiles(patterns) {
|
|
31
|
+
const files = [
|
|
32
|
+
...new Set(patterns.flatMap((p) => DECL_EXT.test(p) ? globSync(p) : globSync(p).filter(isTsSource))),
|
|
33
|
+
];
|
|
34
|
+
if (files.length === 0)
|
|
35
|
+
throw new PabstError("no matching .ts files");
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
const NO_SOURCES = 'cannot determine where your source code is; pass files or globs (e.g. pabst test "src/**/*.ts")';
|
|
39
|
+
// TS18003 ("No inputs were found in config file") and TS18002 ("The 'files'
|
|
40
|
+
// list in config file is empty") are the config saying it names no files —
|
|
41
|
+
// that is "no answer here", not a broken config, so discovery falls through
|
|
42
|
+
// to the src/ convention.
|
|
43
|
+
const NO_INPUTS_ERRORS = new Set([18002, 18003]);
|
|
44
|
+
// Diagnostics about compilerOptions — unknown option (5023), unknown option
|
|
45
|
+
// with a suggestion (5025), wrong value type (5024), value outside the known
|
|
46
|
+
// set (6046) — cannot change which files the config enumerates, and routinely
|
|
47
|
+
// arise from version skew between the user's TypeScript and the one pabst
|
|
48
|
+
// bundles (a flag or target newer than ours). The file list is still exactly
|
|
49
|
+
// what tsc would compile, so these are ignored.
|
|
50
|
+
const OPTION_ERRORS = new Set([5023, 5024, 5025, 6046]);
|
|
51
|
+
// Every other diagnostic (bad JSON, unresolvable extends, misspelled root
|
|
52
|
+
// options like "excludes", ...) can make the file list differ from what the
|
|
53
|
+
// user's config describes, so it is user-facing and fatal: silently testing
|
|
54
|
+
// different files than the config means would be worse than stopping.
|
|
55
|
+
function fatalError(diagnostics) {
|
|
56
|
+
return diagnostics.find((e) => !NO_INPUTS_ERRORS.has(e.code) && !OPTION_ERRORS.has(e.code));
|
|
57
|
+
}
|
|
58
|
+
function configError(diagnostic) {
|
|
59
|
+
const text = ts.flattenDiagnosticMessageText(diagnostic.messageText, " ");
|
|
60
|
+
return new PabstError(`tsconfig.json: ${text}`);
|
|
61
|
+
}
|
|
62
|
+
// path.relative yields ".."-led (or, across Windows drives, absolute) paths
|
|
63
|
+
// for files outside cwd. Generated tests are rooted in ./.pabst/, so a source
|
|
64
|
+
// outside the package cannot be tested from here — a monorepo neighbor's
|
|
65
|
+
// files are that neighbor's run, not this one's — and is dropped.
|
|
66
|
+
function insideCwd(rel) {
|
|
67
|
+
return (rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The files tsc would compile for ./tsconfig.json, as sorted relative paths
|
|
71
|
+
* filtered to scannable sources; undefined when there is no tsconfig.json,
|
|
72
|
+
* empty when the config resolves to no usable files.
|
|
73
|
+
*/
|
|
74
|
+
function tsconfigFiles(cwd) {
|
|
75
|
+
const configPath = path.join(cwd, "tsconfig.json");
|
|
76
|
+
if (!existsSync(configPath))
|
|
77
|
+
return undefined;
|
|
78
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
79
|
+
if (read.error)
|
|
80
|
+
throw configError(read.error);
|
|
81
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, cwd, undefined, configPath);
|
|
82
|
+
const fatal = fatalError(parsed.errors);
|
|
83
|
+
if (fatal)
|
|
84
|
+
throw configError(fatal);
|
|
85
|
+
const files = parsed.fileNames
|
|
86
|
+
.map((f) => path.relative(cwd, f))
|
|
87
|
+
.filter(insideCwd)
|
|
88
|
+
.filter(isTsSource)
|
|
89
|
+
.sort();
|
|
90
|
+
// A "files" list names paths without checking them, so a stale entry comes
|
|
91
|
+
// back with no diagnostic; stop with a real message rather than crashing on
|
|
92
|
+
// the eventual read. (include-glob matches exist by construction.)
|
|
93
|
+
const missing = files.find((f) => !existsSync(path.resolve(cwd, f)));
|
|
94
|
+
if (missing) {
|
|
95
|
+
throw new PabstError(`tsconfig.json: file not found: ${missing}`);
|
|
96
|
+
}
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Zero-argument mode: find the project's TypeScript sources. Uses the file
|
|
101
|
+
* list of ./tsconfig.json when it names any, then the src/ convention;
|
|
102
|
+
* throws PabstError when nothing is found.
|
|
103
|
+
*/
|
|
104
|
+
function discoverFiles() {
|
|
105
|
+
const fromConfig = tsconfigFiles(process.cwd());
|
|
106
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
107
|
+
return { files: fromConfig, source: "tsconfig.json" };
|
|
108
|
+
}
|
|
109
|
+
const files = globSync("src/**/*").filter(isTsSource).sort();
|
|
110
|
+
if (files.length > 0)
|
|
111
|
+
return { files, source: "src/" };
|
|
112
|
+
throw new PabstError(NO_SOURCES);
|
|
113
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Binder, Domain, Range } from "./ir.js";
|
|
2
|
+
export declare const DOMAIN_TABLE: Record<Domain, string>;
|
|
3
|
+
export declare function isDomain(s: string): s is Domain;
|
|
4
|
+
export declare const MAX_SAFE = 9007199254740991n;
|
|
5
|
+
export interface IntBounds {
|
|
6
|
+
lo: bigint;
|
|
7
|
+
hi: bigint;
|
|
8
|
+
/** A finite endpoint fell outside the safe integer range, so the
|
|
9
|
+
* interval was intersected with it (possibly to emptiness: lo > hi). */
|
|
10
|
+
clamped: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** The inclusive fc.integer bounds an int/nat interval lowers to: open
|
|
13
|
+
* endpoints fold into ±1 (fc.integer has no exclusion options), nat floors
|
|
14
|
+
* at 0, and the result is intersected with the safe integer range. Both
|
|
15
|
+
* sides are always concrete — fc.integer's implicit defaults are 32-bit,
|
|
16
|
+
* so a far-out explicit bound with an omitted side would crash it — and an
|
|
17
|
+
* unbounded (∞) side means the safe-range limit. */
|
|
18
|
+
export declare function intBounds(domain: "int" | "nat", range: Range): IntBounds;
|
|
19
|
+
/** The inclusive fc.bigInt bounds a bigint interval lowers to. An
|
|
20
|
+
* unbounded side stays undefined — fc.bigInt, unlike fc.integer, widens
|
|
21
|
+
* its default range around a far-out explicit bound. */
|
|
22
|
+
export declare function bigintBounds(range: Range): {
|
|
23
|
+
lo?: bigint;
|
|
24
|
+
hi?: bigint;
|
|
25
|
+
};
|
|
26
|
+
/** One endpoint of a number interval as fc.double sees it: the literal
|
|
27
|
+
* text emitted into the generated test, and its numeric value (used to
|
|
28
|
+
* validate the constraints with fc itself at parse time). */
|
|
29
|
+
export interface NumberBound {
|
|
30
|
+
lit: string;
|
|
31
|
+
val: number;
|
|
32
|
+
}
|
|
33
|
+
export interface NumberConstraints {
|
|
34
|
+
min?: NumberBound;
|
|
35
|
+
minExcluded: boolean;
|
|
36
|
+
max?: NumberBound;
|
|
37
|
+
maxExcluded: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** The fc.double constraints a number interval lowers to. An open ∞
|
|
40
|
+
* endpoint becomes an excluded infinite bound, which fast-check clamps to
|
|
41
|
+
* ±MAX_VALUE; a closed ∞ endpoint is fc.double's default (inclusive ±∞),
|
|
42
|
+
* so that side is simply omitted. */
|
|
43
|
+
export declare function numberConstraints(range: Range): NumberConstraints;
|
|
44
|
+
/** The binder's guards are mutually exclusive per domain, so arbitraryFor
|
|
45
|
+
* takes the binder itself rather than growing a positional parameter per
|
|
46
|
+
* guard kind. */
|
|
47
|
+
export declare function arbitraryFor(binder: Pick<Binder, "domain" | "range" | "pattern">): string;
|
package/dist/domains.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { PabstError } from "./errors.js";
|
|
2
|
+
import { anchoredSource, regexGuardDomainError } from "./regex-guard.js";
|
|
3
|
+
export const DOMAIN_TABLE = {
|
|
4
|
+
int: "fc.integer()",
|
|
5
|
+
nat: "fc.nat()",
|
|
6
|
+
number: "fc.double()",
|
|
7
|
+
boolean: "fc.boolean()",
|
|
8
|
+
string: "fc.string()",
|
|
9
|
+
bigint: "fc.bigInt()",
|
|
10
|
+
};
|
|
11
|
+
export function isDomain(s) {
|
|
12
|
+
return Object.prototype.hasOwnProperty.call(DOMAIN_TABLE, s);
|
|
13
|
+
}
|
|
14
|
+
export const MAX_SAFE = 9007199254740991n;
|
|
15
|
+
/** The inclusive fc.integer bounds an int/nat interval lowers to: open
|
|
16
|
+
* endpoints fold into ±1 (fc.integer has no exclusion options), nat floors
|
|
17
|
+
* at 0, and the result is intersected with the safe integer range. Both
|
|
18
|
+
* sides are always concrete — fc.integer's implicit defaults are 32-bit,
|
|
19
|
+
* so a far-out explicit bound with an omitted side would crash it — and an
|
|
20
|
+
* unbounded (∞) side means the safe-range limit. */
|
|
21
|
+
export function intBounds(domain, range) {
|
|
22
|
+
let lo = range.min === undefined
|
|
23
|
+
? -MAX_SAFE
|
|
24
|
+
: BigInt(range.min) + (range.minOpen ? 1n : 0n);
|
|
25
|
+
let hi = range.max === undefined
|
|
26
|
+
? MAX_SAFE
|
|
27
|
+
: BigInt(range.max) - (range.maxOpen ? 1n : 0n);
|
|
28
|
+
if (domain === "nat" && lo < 0n)
|
|
29
|
+
lo = 0n;
|
|
30
|
+
const clamped = lo < -MAX_SAFE || lo > MAX_SAFE || hi < -MAX_SAFE || hi > MAX_SAFE;
|
|
31
|
+
if (lo < -MAX_SAFE)
|
|
32
|
+
lo = -MAX_SAFE;
|
|
33
|
+
if (hi > MAX_SAFE)
|
|
34
|
+
hi = MAX_SAFE;
|
|
35
|
+
return { lo, hi, clamped };
|
|
36
|
+
}
|
|
37
|
+
/** The inclusive fc.bigInt bounds a bigint interval lowers to. An
|
|
38
|
+
* unbounded side stays undefined — fc.bigInt, unlike fc.integer, widens
|
|
39
|
+
* its default range around a far-out explicit bound. */
|
|
40
|
+
export function bigintBounds(range) {
|
|
41
|
+
const lo = range.min === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: BigInt(range.min) + (range.minOpen ? 1n : 0n);
|
|
44
|
+
const hi = range.max === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: BigInt(range.max) - (range.maxOpen ? 1n : 0n);
|
|
47
|
+
const bounds = {};
|
|
48
|
+
if (lo !== undefined)
|
|
49
|
+
bounds.lo = lo;
|
|
50
|
+
if (hi !== undefined)
|
|
51
|
+
bounds.hi = hi;
|
|
52
|
+
return bounds;
|
|
53
|
+
}
|
|
54
|
+
/** The fc.double constraints a number interval lowers to. An open ∞
|
|
55
|
+
* endpoint becomes an excluded infinite bound, which fast-check clamps to
|
|
56
|
+
* ±MAX_VALUE; a closed ∞ endpoint is fc.double's default (inclusive ±∞),
|
|
57
|
+
* so that side is simply omitted. */
|
|
58
|
+
export function numberConstraints(range) {
|
|
59
|
+
const c = {
|
|
60
|
+
minExcluded: range.minOpen === true,
|
|
61
|
+
maxExcluded: range.maxOpen === true,
|
|
62
|
+
};
|
|
63
|
+
if (range.min !== undefined) {
|
|
64
|
+
c.min = { lit: range.min, val: Number(range.min) };
|
|
65
|
+
}
|
|
66
|
+
else if (range.minOpen) {
|
|
67
|
+
c.min = { lit: "Number.NEGATIVE_INFINITY", val: Number.NEGATIVE_INFINITY };
|
|
68
|
+
}
|
|
69
|
+
if (range.max !== undefined) {
|
|
70
|
+
c.max = { lit: range.max, val: Number(range.max) };
|
|
71
|
+
}
|
|
72
|
+
else if (range.maxOpen) {
|
|
73
|
+
c.max = { lit: "Number.POSITIVE_INFINITY", val: Number.POSITIVE_INFINITY };
|
|
74
|
+
}
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
/** The binder's guards are mutually exclusive per domain, so arbitraryFor
|
|
78
|
+
* takes the binder itself rather than growing a positional parameter per
|
|
79
|
+
* guard kind. */
|
|
80
|
+
export function arbitraryFor(binder) {
|
|
81
|
+
const { domain, range, pattern } = binder;
|
|
82
|
+
if (pattern) {
|
|
83
|
+
if (domain !== "string") {
|
|
84
|
+
// Unreachable via the parser (parseRegexGuard rejects these), kept
|
|
85
|
+
// as a backstop for direct callers.
|
|
86
|
+
throw regexGuardDomainError(domain);
|
|
87
|
+
}
|
|
88
|
+
// Safe to re-emit as a literal: the source came from a literal scan,
|
|
89
|
+
// so any '/' in it is escaped or inside a character class.
|
|
90
|
+
return `fc.stringMatching(/${anchoredSource(pattern.source)}/${pattern.flags})`;
|
|
91
|
+
}
|
|
92
|
+
if (!range)
|
|
93
|
+
return DOMAIN_TABLE[domain];
|
|
94
|
+
switch (domain) {
|
|
95
|
+
case "int":
|
|
96
|
+
case "nat": {
|
|
97
|
+
// A ranged nat is just a bounded integer; fc.nat has no `min`.
|
|
98
|
+
const { lo, hi } = intBounds(domain, range);
|
|
99
|
+
return `fc.integer({ min: ${lo}, max: ${hi} })`;
|
|
100
|
+
}
|
|
101
|
+
case "number": {
|
|
102
|
+
// Bounded fc.double still generates NaN unless told otherwise, and
|
|
103
|
+
// NaN satisfies no interval.
|
|
104
|
+
const c = numberConstraints(range);
|
|
105
|
+
const opts = [];
|
|
106
|
+
if (c.min)
|
|
107
|
+
opts.push(`min: ${c.min.lit}`);
|
|
108
|
+
if (c.minExcluded)
|
|
109
|
+
opts.push(`minExcluded: true`);
|
|
110
|
+
if (c.max)
|
|
111
|
+
opts.push(`max: ${c.max.lit}`);
|
|
112
|
+
if (c.maxExcluded)
|
|
113
|
+
opts.push(`maxExcluded: true`);
|
|
114
|
+
opts.push(`noNaN: true`);
|
|
115
|
+
return `fc.double({ ${opts.join(", ")} })`;
|
|
116
|
+
}
|
|
117
|
+
case "bigint": {
|
|
118
|
+
const { lo, hi } = bigintBounds(range);
|
|
119
|
+
return `fc.bigInt${render(lo === undefined ? undefined : `min: ${lo}n`, hi === undefined ? undefined : `max: ${hi}n`)}`;
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
// Unreachable via the parser (parseRange rejects these), kept as a
|
|
123
|
+
// backstop for direct callers.
|
|
124
|
+
throw new PabstError(`domain '${domain}' does not support interval constraints`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function render(...opts) {
|
|
128
|
+
const present = opts.filter((o) => o !== undefined);
|
|
129
|
+
return present.length === 0 ? "()" : `({ ${present.join(", ")} })`;
|
|
130
|
+
}
|
package/dist/emit.d.ts
ADDED
package/dist/emit.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { arbitraryFor } from "./domains.js";
|
|
3
|
+
import { qualifiedName } from "./qualified-name.js";
|
|
4
|
+
const SRC_EXT = /\.(ts|tsx|mts|cts|js|mjs|cjs)$/;
|
|
5
|
+
export function emit(specs, sourceFile, outFile, seed) {
|
|
6
|
+
const srcAbs = path.resolve(sourceFile).replace(SRC_EXT, "");
|
|
7
|
+
const outDir = path.dirname(path.resolve(outFile));
|
|
8
|
+
let rel = path.relative(outDir, srcAbs).split(path.sep).join("/");
|
|
9
|
+
if (!rel.startsWith("."))
|
|
10
|
+
rel = "./" + rel;
|
|
11
|
+
const allExports = [...new Set(specs.flatMap((s) => s.freeExports))].sort();
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push(`import { describe } from "vitest";`);
|
|
14
|
+
lines.push(`import { test, fc } from "@fast-check/vitest";`);
|
|
15
|
+
lines.push(`import { report as __pabstReport, bool as __bool } from "pabst-checker/runtime";`);
|
|
16
|
+
lines.push(`import * as __M from "${rel}";`);
|
|
17
|
+
if (allExports.length > 0)
|
|
18
|
+
lines.push(`const { ${allExports.join(", ")} } = __M;`);
|
|
19
|
+
lines.push("");
|
|
20
|
+
lines.push(`describe("pabst", () => {`);
|
|
21
|
+
// Group by class (undefined = top-level function), then by member name.
|
|
22
|
+
const byClass = new Map();
|
|
23
|
+
for (const s of specs) {
|
|
24
|
+
let methods = byClass.get(s.className);
|
|
25
|
+
if (!methods) {
|
|
26
|
+
methods = new Map();
|
|
27
|
+
byClass.set(s.className, methods);
|
|
28
|
+
}
|
|
29
|
+
const arr = methods.get(s.functionName) ?? [];
|
|
30
|
+
arr.push(s);
|
|
31
|
+
methods.set(s.functionName, arr);
|
|
32
|
+
}
|
|
33
|
+
for (const [className, methods] of byClass) {
|
|
34
|
+
if (className === undefined) {
|
|
35
|
+
for (const [fnName, fnSpecs] of methods) {
|
|
36
|
+
lines.push(` describe(${JSON.stringify(fnName)}, () => {`);
|
|
37
|
+
for (const s of fnSpecs)
|
|
38
|
+
lines.push(emitProp(s, sourceFile, seed, " "));
|
|
39
|
+
lines.push(` });`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push(` describe(${JSON.stringify(className)}, () => {`);
|
|
44
|
+
for (const [methodName, mSpecs] of methods) {
|
|
45
|
+
lines.push(` describe(${JSON.stringify(methodName)}, () => {`);
|
|
46
|
+
for (const s of mSpecs)
|
|
47
|
+
lines.push(emitProp(s, sourceFile, seed, " "));
|
|
48
|
+
lines.push(` });`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(` });`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push(`});`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
function emitProp(s, sourceFile, seed, indent) {
|
|
58
|
+
const arbs = s.binders.map((b) => arbitraryFor(b)).join(", ");
|
|
59
|
+
const vars = s.binders.map((b) => b.varName).join(", ");
|
|
60
|
+
const varNames = s.binders.map((b) => JSON.stringify(b.varName)).join(", ");
|
|
61
|
+
const name = JSON.stringify(s.name);
|
|
62
|
+
const file = JSON.stringify(sourceFile);
|
|
63
|
+
const fn = JSON.stringify(qualifiedName(s.functionName, s.className, s.isStatic));
|
|
64
|
+
const reporter = `(d) => __pabstReport(${file}, ${fn}, ${name}, [${varNames}], d)`;
|
|
65
|
+
const params = `{ seed: ${seed}, reporter: ${reporter} }`;
|
|
66
|
+
const out = [];
|
|
67
|
+
out.push(`${indent}test.prop([${arbs}], ${params})(${name}, (${vars}) => {`);
|
|
68
|
+
for (const p of s.preconditions)
|
|
69
|
+
out.push(`${indent} fc.pre(${p});`);
|
|
70
|
+
out.push(`${indent} const __r = (${s.body});`);
|
|
71
|
+
out.push(`${indent} return __r;`);
|
|
72
|
+
out.push(`${indent}});`);
|
|
73
|
+
return out.join("\n");
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Issue, type Envelope } from "./issue.js";
|
|
2
|
+
/** The subset of vitest's JSON reporter output the envelope consumes. */
|
|
3
|
+
export interface AssertionResult {
|
|
4
|
+
status: string;
|
|
5
|
+
failureMessages: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface FileResult {
|
|
8
|
+
status?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
assertionResults: AssertionResult[];
|
|
11
|
+
}
|
|
12
|
+
export interface VitestJson {
|
|
13
|
+
numPassedTests: number;
|
|
14
|
+
numFailedTests: number;
|
|
15
|
+
success: boolean;
|
|
16
|
+
testResults: FileResult[];
|
|
17
|
+
}
|
|
18
|
+
/** Run-level metadata the CLI captures, independent of the vitest run. */
|
|
19
|
+
export interface RunMeta {
|
|
20
|
+
version: string;
|
|
21
|
+
startedAt: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
seed: number;
|
|
24
|
+
generated: number;
|
|
25
|
+
}
|
|
26
|
+
/** Parse every failed assertion's tagged payload into an Issue. */
|
|
27
|
+
export declare function collectIssues(json: VitestJson): Issue[];
|
|
28
|
+
/** Assemble the full run envelope from metadata and a parsed vitest run. */
|
|
29
|
+
export declare function buildEnvelope(meta: RunMeta, json: VitestJson): Envelope;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseIssue } from "./issue.js";
|
|
2
|
+
/** Parse every failed assertion's tagged payload into an Issue. */
|
|
3
|
+
export function collectIssues(json) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
for (const file of json.testResults ?? []) {
|
|
6
|
+
for (const a of file.assertionResults ?? []) {
|
|
7
|
+
if (a.status !== "failed")
|
|
8
|
+
continue;
|
|
9
|
+
const issue = parseIssue(a.failureMessages[0] ?? "");
|
|
10
|
+
if (issue)
|
|
11
|
+
issues.push(issue);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return issues;
|
|
15
|
+
}
|
|
16
|
+
/** Assemble the full run envelope from metadata and a parsed vitest run. */
|
|
17
|
+
export function buildEnvelope(meta, json) {
|
|
18
|
+
return {
|
|
19
|
+
version: meta.version,
|
|
20
|
+
startedAt: meta.startedAt,
|
|
21
|
+
cwd: meta.cwd,
|
|
22
|
+
seed: meta.seed,
|
|
23
|
+
generated: meta.generated,
|
|
24
|
+
passed: json.numPassedTests,
|
|
25
|
+
failed: json.numFailedTests,
|
|
26
|
+
issues: collectIssues(json),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate an atom's JS and desugar its equation (see README, "Equations"):
|
|
3
|
+
* a depth-0 `A ≡ B` becomes `Object.is(A, B)` and `A ≢ B` becomes
|
|
4
|
+
* `!Object.is(A, B)`. The glyphs are available only at the atom's top
|
|
5
|
+
* level — in nested positions (callbacks, arguments, template
|
|
6
|
+
* substitutions) call Object.is directly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function desugarEquations(text: string): string;
|