as-test 1.1.1 → 1.1.3
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 +11 -3
- package/README.md +14 -7
- package/as-test.config.schema.json +142 -142
- package/assembly/__fuzz__/math.fuzz.ts +17 -14
- package/assembly/__fuzz__/seed-perf.fuzz.ts +4 -2
- package/assembly/coverage.ts +12 -0
- package/assembly/index.ts +17 -4
- package/assembly/src/expectation.ts +7 -1
- package/assembly/src/fuzz.ts +44 -23
- package/assembly/src/suite.ts +2 -0
- package/assembly/test.ts +85 -0
- package/assembly/util/format.ts +10 -1
- package/assembly/util/wipc.ts +1 -2
- package/bin/build-worker-pool.js +7 -1
- package/bin/commands/build-core.js +6 -1
- package/bin/commands/build.js +1 -1
- package/bin/commands/clean-core.js +3 -1
- package/bin/commands/clean.js +0 -37
- package/bin/commands/fuzz-core.js +2 -2
- package/bin/commands/run-core.js +45 -24
- package/bin/commands/run.js +3 -1
- package/bin/commands/test.js +3 -1
- package/bin/commands/web-runner-source.js +14 -14
- package/bin/commands/web-session.js +6 -1
- package/bin/coverage-points.js +207 -2
- package/bin/crash-store.js +3 -1
- package/bin/index.js +357 -125
- package/bin/reporters/default.js +295 -64
- package/bin/util.js +36 -11
- package/lib/build/index.js +74 -24
- package/lib/src/index.ts +96 -35
- package/package.json +2 -1
- package/transform/lib/coverage.js +222 -71
package/bin/commands/run-core.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import { Channel, MessageType } from "../wipc.js";
|
|
5
|
-
import { applyMode, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
|
|
5
|
+
import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { pathToFileURL } from "url";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -390,7 +390,7 @@ function resolveSuiteSelectionMatches(suites, selectors, file) {
|
|
|
390
390
|
if (normalized.includes("/")) {
|
|
391
391
|
const resolved = resolveExplicitSuitePath(suites, normalized);
|
|
392
392
|
if (!resolved) {
|
|
393
|
-
throw new Error(`No suites matched "${selector}" in ${
|
|
393
|
+
throw new Error(`No suites matched "${selector}" in ${formatSpecDisplayPath(file)}.`);
|
|
394
394
|
}
|
|
395
395
|
matches.push({
|
|
396
396
|
kind: "path",
|
|
@@ -402,7 +402,7 @@ function resolveSuiteSelectionMatches(suites, selectors, file) {
|
|
|
402
402
|
}
|
|
403
403
|
const resolved = resolveBareSuiteSelector(suites, normalized);
|
|
404
404
|
if (!resolved) {
|
|
405
|
-
throw new Error(`No suites matched "${selector}" in ${
|
|
405
|
+
throw new Error(`No suites matched "${selector}" in ${formatSpecDisplayPath(file)}.`);
|
|
406
406
|
}
|
|
407
407
|
matches.push({
|
|
408
408
|
kind: "bare",
|
|
@@ -437,7 +437,9 @@ function resolveBareSuiteSelector(suites, selector) {
|
|
|
437
437
|
return null;
|
|
438
438
|
const matches = [];
|
|
439
439
|
walkSuites(suites, (suite, depth) => {
|
|
440
|
-
const leaf = String(suite.path ?? "")
|
|
440
|
+
const leaf = String(suite.path ?? "")
|
|
441
|
+
.split("/")
|
|
442
|
+
.pop() ?? "";
|
|
441
443
|
if (leaf == slug) {
|
|
442
444
|
matches.push({ path: String(suite.path), depth });
|
|
443
445
|
}
|
|
@@ -457,7 +459,9 @@ function walkSuites(suites, visitor, depth = 0) {
|
|
|
457
459
|
for (const suite of suites) {
|
|
458
460
|
if (visitor(suite, depth))
|
|
459
461
|
return true;
|
|
460
|
-
const childSuites = Array.isArray(suite?.suites)
|
|
462
|
+
const childSuites = Array.isArray(suite?.suites)
|
|
463
|
+
? suite.suites
|
|
464
|
+
: [];
|
|
461
465
|
if (walkSuites(childSuites, visitor, depth + 1))
|
|
462
466
|
return true;
|
|
463
467
|
}
|
|
@@ -466,7 +470,9 @@ function walkSuites(suites, visitor, depth = 0) {
|
|
|
466
470
|
function cloneSelectedSuites(suites, selected, file, modeName) {
|
|
467
471
|
const out = [];
|
|
468
472
|
for (const suite of suites) {
|
|
469
|
-
const childSuites = Array.isArray(suite.suites)
|
|
473
|
+
const childSuites = Array.isArray(suite.suites)
|
|
474
|
+
? suite.suites
|
|
475
|
+
: [];
|
|
470
476
|
const selectedChildren = cloneSelectedSuites(childSuites, selected, file, modeName);
|
|
471
477
|
const keep = selected.has(String(suite.path ?? "")) || selectedChildren.length > 0;
|
|
472
478
|
if (!keep)
|
|
@@ -506,8 +512,8 @@ function collectReadableFailures(suites, file, pathParts) {
|
|
|
506
512
|
out.push({
|
|
507
513
|
title: `${nextPath.join(" > ")}#${i + 1}`,
|
|
508
514
|
where: String(test.location ?? "").length
|
|
509
|
-
? `${
|
|
510
|
-
:
|
|
515
|
+
? `${formatSpecDisplayPath(file)}:${String(test.location ?? "")}`
|
|
516
|
+
: formatSpecDisplayPath(file),
|
|
511
517
|
message: String(test.message ?? ""),
|
|
512
518
|
left: JSON.stringify(test.left ?? ""),
|
|
513
519
|
right: JSON.stringify(test.right ?? ""),
|
|
@@ -648,7 +654,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
648
654
|
catch (error) {
|
|
649
655
|
const modeLabel = options.modeName ?? "default";
|
|
650
656
|
const details = error instanceof Error ? error.message : String(error);
|
|
651
|
-
throw new Error(`Failed to run ${
|
|
657
|
+
throw new Error(`Failed to run ${formatSpecDisplayPath(file)} in mode ${modeLabel} with ${details}`);
|
|
652
658
|
}
|
|
653
659
|
const normalized = normalizeReport(report);
|
|
654
660
|
const selectedSuites = options.suiteSelectors?.length
|
|
@@ -665,6 +671,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
665
671
|
suites: selectedSuites,
|
|
666
672
|
coverage: normalized.coverage,
|
|
667
673
|
runCommand: runCommandForLog,
|
|
674
|
+
buildCommand: options.buildCommandsByFile?.[file] ?? options.buildCommand ?? "",
|
|
668
675
|
snapshotSummary: {
|
|
669
676
|
matched: snapshotStore.matched,
|
|
670
677
|
created: snapshotStore.created,
|
|
@@ -712,6 +719,8 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
712
719
|
clean: cleanOutput,
|
|
713
720
|
snapshotEnabled,
|
|
714
721
|
showCoverage,
|
|
722
|
+
showCoverageAll: Boolean(flags.showCoverageAll),
|
|
723
|
+
verbose: Boolean(flags.verbose),
|
|
715
724
|
buildTime,
|
|
716
725
|
snapshotSummary,
|
|
717
726
|
coverageSummary,
|
|
@@ -1197,6 +1206,10 @@ function normalizeCoverage(value) {
|
|
|
1197
1206
|
column: Number(p.column ?? 0),
|
|
1198
1207
|
type: String(p.type ?? ""),
|
|
1199
1208
|
executed: Boolean(p.executed),
|
|
1209
|
+
parentHash: String(p.parentHash ?? ""),
|
|
1210
|
+
scopeKind: String(p.scopeKind ?? ""),
|
|
1211
|
+
scopeName: String(p.scopeName ?? ""),
|
|
1212
|
+
depth: Number(p.depth ?? 0),
|
|
1200
1213
|
};
|
|
1201
1214
|
})
|
|
1202
1215
|
.filter((point) => point.file.length > 0);
|
|
@@ -1468,10 +1481,14 @@ function matchesCoverageTextPattern(value, pattern) {
|
|
|
1468
1481
|
return globPatternToRegExp(normalized).test(value);
|
|
1469
1482
|
}
|
|
1470
1483
|
function compareCoveragePoints(a, b) {
|
|
1484
|
+
const depthA = a.depth ?? 0;
|
|
1485
|
+
const depthB = b.depth ?? 0;
|
|
1471
1486
|
if (a.line !== b.line)
|
|
1472
1487
|
return a.line - b.line;
|
|
1473
1488
|
if (a.column !== b.column)
|
|
1474
1489
|
return a.column - b.column;
|
|
1490
|
+
if (depthA !== depthB)
|
|
1491
|
+
return depthA - depthB;
|
|
1475
1492
|
if (a.type !== b.type)
|
|
1476
1493
|
return a.type.localeCompare(b.type);
|
|
1477
1494
|
return a.hash.localeCompare(b.hash);
|
|
@@ -1492,7 +1509,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1492
1509
|
const runtimeEvents = {
|
|
1493
1510
|
sawFileStart: false,
|
|
1494
1511
|
sawFileEnd: false,
|
|
1495
|
-
fileName:
|
|
1512
|
+
fileName: formatSpecDisplayPath(specFile),
|
|
1496
1513
|
fileVerdict: "none",
|
|
1497
1514
|
fileTime: "",
|
|
1498
1515
|
suiteStarts: 0,
|
|
@@ -1667,7 +1684,8 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1667
1684
|
const code = await new Promise((resolve) => {
|
|
1668
1685
|
child.on("close", (exitCode) => resolve(exitCode ?? 1));
|
|
1669
1686
|
});
|
|
1670
|
-
if (stderrPendingLine.length &&
|
|
1687
|
+
if (stderrPendingLine.length &&
|
|
1688
|
+
!shouldSuppressWasiWarningLine(stderrPendingLine)) {
|
|
1671
1689
|
stderrBuffer += stderrPendingLine;
|
|
1672
1690
|
}
|
|
1673
1691
|
const processSpawnError = spawnError;
|
|
@@ -1686,8 +1704,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1686
1704
|
if (reportStream.sawChunkStart) {
|
|
1687
1705
|
if (!reportStream.sawChunkEnd) {
|
|
1688
1706
|
parseError =
|
|
1689
|
-
parseError ??
|
|
1690
|
-
"missing report:end marker for chunked report payload";
|
|
1707
|
+
parseError ?? "missing report:end marker for chunked report payload";
|
|
1691
1708
|
}
|
|
1692
1709
|
else {
|
|
1693
1710
|
const chunkedPayload = reportStream.chunks.join("");
|
|
@@ -1806,7 +1823,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
1806
1823
|
const runtimeEvents = {
|
|
1807
1824
|
sawFileStart: false,
|
|
1808
1825
|
sawFileEnd: false,
|
|
1809
|
-
fileName:
|
|
1826
|
+
fileName: formatSpecDisplayPath(specFile),
|
|
1810
1827
|
fileVerdict: "none",
|
|
1811
1828
|
fileTime: "",
|
|
1812
1829
|
suiteStarts: 0,
|
|
@@ -1968,7 +1985,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
1968
1985
|
});
|
|
1969
1986
|
let code = 0;
|
|
1970
1987
|
try {
|
|
1971
|
-
await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")),
|
|
1988
|
+
await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")), formatSpecDisplayPath(specFile), (frame) => {
|
|
1972
1989
|
input.write(frame);
|
|
1973
1990
|
});
|
|
1974
1991
|
}
|
|
@@ -1976,8 +1993,9 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
1976
1993
|
code = 1;
|
|
1977
1994
|
await session.close(error instanceof Error ? error : new Error(String(error)));
|
|
1978
1995
|
stderrBuffer +=
|
|
1979
|
-
(error instanceof Error
|
|
1980
|
-
|
|
1996
|
+
(error instanceof Error
|
|
1997
|
+
? (error.stack ?? error.message)
|
|
1998
|
+
: String(error)) + "\n";
|
|
1981
1999
|
}
|
|
1982
2000
|
finally {
|
|
1983
2001
|
input.end();
|
|
@@ -1986,8 +2004,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
1986
2004
|
if (reportStream.sawChunkStart) {
|
|
1987
2005
|
if (!reportStream.sawChunkEnd) {
|
|
1988
2006
|
parseError =
|
|
1989
|
-
parseError ??
|
|
1990
|
-
"missing report:end marker for chunked report payload";
|
|
2007
|
+
parseError ?? "missing report:end marker for chunked report payload";
|
|
1991
2008
|
}
|
|
1992
2009
|
else {
|
|
1993
2010
|
const chunkedPayload = reportStream.chunks.join("");
|
|
@@ -2091,7 +2108,7 @@ function synthesizeReportFromRuntimeEvents(specFile, runtimeEvents) {
|
|
|
2091
2108
|
suites: [
|
|
2092
2109
|
{
|
|
2093
2110
|
file: specFile,
|
|
2094
|
-
description: runtimeEvents.fileName ||
|
|
2111
|
+
description: runtimeEvents.fileName || formatSpecDisplayPath(specFile),
|
|
2095
2112
|
depth: 0,
|
|
2096
2113
|
kind: "file",
|
|
2097
2114
|
verdict,
|
|
@@ -2136,7 +2153,7 @@ function appendRuntimeFailureReport(report, specFile, modeName, title, details,
|
|
|
2136
2153
|
const suites = Array.isArray(report?.suites) ? report.suites : [];
|
|
2137
2154
|
suites.push({
|
|
2138
2155
|
file: specFile,
|
|
2139
|
-
description:
|
|
2156
|
+
description: formatSpecDisplayPath(specFile),
|
|
2140
2157
|
depth: 0,
|
|
2141
2158
|
kind: "runtime-error",
|
|
2142
2159
|
verdict: "fail",
|
|
@@ -2221,9 +2238,11 @@ function readFileReport(stats, fileReport) {
|
|
|
2221
2238
|
: [];
|
|
2222
2239
|
const file = String(fileReportAny.file ?? "");
|
|
2223
2240
|
const modeName = String(fileReportAny.modeName ?? "");
|
|
2241
|
+
const runCommand = String(fileReportAny.runCommand ?? "");
|
|
2242
|
+
const buildCommand = String(fileReportAny.buildCommand ?? "");
|
|
2224
2243
|
let fileVerdict = "none";
|
|
2225
2244
|
for (const suite of suites) {
|
|
2226
|
-
fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite, file, modeName));
|
|
2245
|
+
fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite, file, modeName, runCommand, buildCommand));
|
|
2227
2246
|
}
|
|
2228
2247
|
if (fileVerdict == "fail") {
|
|
2229
2248
|
stats.failedFiles++;
|
|
@@ -2235,7 +2254,7 @@ function readFileReport(stats, fileReport) {
|
|
|
2235
2254
|
stats.skippedFiles++;
|
|
2236
2255
|
}
|
|
2237
2256
|
}
|
|
2238
|
-
function readSuite(stats, suite, file, modeName) {
|
|
2257
|
+
function readSuite(stats, suite, file, modeName, runCommand, buildCommand) {
|
|
2239
2258
|
const suiteAny = suite;
|
|
2240
2259
|
const kind = String(suiteAny.kind ?? "");
|
|
2241
2260
|
let verdict = normalizeVerdict(suiteAny.verdict);
|
|
@@ -2247,7 +2266,7 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
2247
2266
|
? suiteAny.suites
|
|
2248
2267
|
: [];
|
|
2249
2268
|
for (const subSuite of subSuites) {
|
|
2250
|
-
verdict = mergeVerdict(verdict, readSuite(stats, subSuite, file, modeName));
|
|
2269
|
+
verdict = mergeVerdict(verdict, readSuite(stats, subSuite, file, modeName, runCommand, buildCommand));
|
|
2251
2270
|
}
|
|
2252
2271
|
const tests = Array.isArray(suiteAny.tests)
|
|
2253
2272
|
? suiteAny.tests
|
|
@@ -2285,6 +2304,8 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
2285
2304
|
...suiteAny,
|
|
2286
2305
|
file,
|
|
2287
2306
|
modeName,
|
|
2307
|
+
runCommand,
|
|
2308
|
+
buildCommand,
|
|
2288
2309
|
});
|
|
2289
2310
|
}
|
|
2290
2311
|
else if (verdict == "ok") {
|
package/bin/commands/run.js
CHANGED
|
@@ -4,12 +4,14 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
|
|
|
4
4
|
const suiteSelectors = deps.resolveSuiteSelectors(rawArgs, "run");
|
|
5
5
|
const listFlags = deps.resolveListFlags(rawArgs, "run");
|
|
6
6
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "run");
|
|
7
|
+
const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "run");
|
|
7
8
|
const runFlags = {
|
|
8
9
|
snapshot: !flags.includes("--no-snapshot"),
|
|
9
10
|
createSnapshots: flags.includes("--create-snapshots"),
|
|
10
11
|
overwriteSnapshots: flags.includes("--overwrite-snapshots"),
|
|
11
12
|
clean: flags.includes("--clean"),
|
|
12
|
-
showCoverage:
|
|
13
|
+
showCoverage: showCoverageMode != undefined,
|
|
14
|
+
showCoverageAll: showCoverageMode == "all",
|
|
13
15
|
verbose: flags.includes("--verbose"),
|
|
14
16
|
...deps.resolveParallelJobs(rawArgs, "run"),
|
|
15
17
|
coverage: featureToggles.coverage,
|
package/bin/commands/test.js
CHANGED
|
@@ -8,12 +8,14 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
|
|
|
8
8
|
tryAs: featureToggles.tryAs,
|
|
9
9
|
coverage: featureToggles.coverage,
|
|
10
10
|
};
|
|
11
|
+
const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "test");
|
|
11
12
|
const runFlags = {
|
|
12
13
|
snapshot: !flags.includes("--no-snapshot"),
|
|
13
14
|
createSnapshots: flags.includes("--create-snapshots"),
|
|
14
15
|
overwriteSnapshots: flags.includes("--overwrite-snapshots"),
|
|
15
16
|
clean: flags.includes("--clean"),
|
|
16
|
-
showCoverage:
|
|
17
|
+
showCoverage: showCoverageMode != undefined,
|
|
18
|
+
showCoverageAll: showCoverageMode == "all",
|
|
17
19
|
verbose: flags.includes("--verbose"),
|
|
18
20
|
...deps.resolveParallelJobs(rawArgs, "test"),
|
|
19
21
|
coverage: featureToggles.coverage,
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
export function buildWebRunnerSource() {
|
|
2
2
|
return `// Feel free to edit this file!
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Runner files use the name <mode>.<type>.js, where <type> is bindings, wasi, or web.
|
|
4
|
+
// To create a runner for another mode, copy this file to <new-mode>.<type>.js.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { instantiate } from "as-test/lib";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let exports = null;
|
|
9
|
+
const imports = {};
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
instantiate(imports)
|
|
12
|
+
.then((instance) => {
|
|
13
|
+
exports = instance.exports;
|
|
14
|
+
instance.exports.start?.();
|
|
15
|
+
// Add extra startup logic here when needed.
|
|
16
|
+
})
|
|
17
|
+
.catch((error) => {
|
|
18
|
+
throw new Error("Failed to run web module: " + String(error));
|
|
19
|
+
});
|
|
20
20
|
`;
|
|
21
21
|
}
|
|
@@ -488,7 +488,12 @@ function resolveHeadlessFlags(commandValue) {
|
|
|
488
488
|
const lower = commandValue.toLowerCase();
|
|
489
489
|
if (lower.includes("firefox"))
|
|
490
490
|
return ["-headless"];
|
|
491
|
-
return [
|
|
491
|
+
return [
|
|
492
|
+
"--headless=new",
|
|
493
|
+
"--disable-gpu",
|
|
494
|
+
"--no-first-run",
|
|
495
|
+
"--no-default-browser-check",
|
|
496
|
+
];
|
|
492
497
|
}
|
|
493
498
|
function hasExecutable(command) {
|
|
494
499
|
if (!command.length)
|
package/bin/coverage-points.js
CHANGED
|
@@ -13,7 +13,61 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
|
|
|
13
13
|
highlightEnd: 0,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
const
|
|
16
|
+
const parameter = detectCoverageParameter(context.visible, context.focus, fallbackType);
|
|
17
|
+
if (parameter) {
|
|
18
|
+
return {
|
|
19
|
+
displayType: parameter.type,
|
|
20
|
+
subjectName: parameter.name,
|
|
21
|
+
visible: context.visible,
|
|
22
|
+
focus: context.focus,
|
|
23
|
+
highlightStart: parameter.start,
|
|
24
|
+
highlightEnd: parameter.end,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const ternary = detectCoverageTernary(context.visible, context.focus, fallbackType);
|
|
28
|
+
if (ternary) {
|
|
29
|
+
return {
|
|
30
|
+
displayType: ternary.type,
|
|
31
|
+
subjectName: null,
|
|
32
|
+
visible: context.visible,
|
|
33
|
+
focus: context.focus,
|
|
34
|
+
highlightStart: ternary.start,
|
|
35
|
+
highlightEnd: ternary.end,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const ifBranch = detectCoverageIfBranch(context.visible, fallbackType);
|
|
39
|
+
if (ifBranch) {
|
|
40
|
+
return {
|
|
41
|
+
displayType: ifBranch.type,
|
|
42
|
+
subjectName: null,
|
|
43
|
+
visible: context.visible,
|
|
44
|
+
focus: context.focus,
|
|
45
|
+
highlightStart: ifBranch.start,
|
|
46
|
+
highlightEnd: ifBranch.end,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const assignment = detectCoverageAssignment(context.visible, fallbackType);
|
|
50
|
+
if (assignment) {
|
|
51
|
+
return {
|
|
52
|
+
displayType: assignment.type,
|
|
53
|
+
subjectName: null,
|
|
54
|
+
visible: context.visible,
|
|
55
|
+
focus: context.focus,
|
|
56
|
+
highlightStart: assignment.start,
|
|
57
|
+
highlightEnd: assignment.end,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const declarationAllowed = fallbackType == "Expression" ||
|
|
61
|
+
fallbackType == "Block" ||
|
|
62
|
+
fallbackType == "Function" ||
|
|
63
|
+
fallbackType == "Method" ||
|
|
64
|
+
fallbackType == "Constructor" ||
|
|
65
|
+
fallbackType == "Variable" ||
|
|
66
|
+
fallbackType == "Property" ||
|
|
67
|
+
fallbackType == "Call";
|
|
68
|
+
const declaration = declarationAllowed
|
|
69
|
+
? detectCoverageDeclaration(context.visible)
|
|
70
|
+
: null;
|
|
17
71
|
if (declaration) {
|
|
18
72
|
const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
|
|
19
73
|
return {
|
|
@@ -25,7 +79,10 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
|
|
|
25
79
|
highlightEnd,
|
|
26
80
|
};
|
|
27
81
|
}
|
|
28
|
-
const
|
|
82
|
+
const callAllowed = fallbackType == "Expression" || fallbackType == "Call";
|
|
83
|
+
const call = callAllowed
|
|
84
|
+
? detectCoverageCall(context.visible, context.focus)
|
|
85
|
+
: null;
|
|
29
86
|
if (call) {
|
|
30
87
|
return {
|
|
31
88
|
displayType: "Call",
|
|
@@ -130,6 +187,147 @@ function detectCoverageDeclaration(visible) {
|
|
|
130
187
|
}
|
|
131
188
|
return null;
|
|
132
189
|
}
|
|
190
|
+
function detectCoverageParameter(visible, focus, fallbackType) {
|
|
191
|
+
const inlineParameter = detectCoverageInlineParameter(visible, focus, fallbackType);
|
|
192
|
+
if (inlineParameter) {
|
|
193
|
+
return inlineParameter;
|
|
194
|
+
}
|
|
195
|
+
const openParen = visible.indexOf("(");
|
|
196
|
+
const closeParen = visible.lastIndexOf(")");
|
|
197
|
+
if (openParen == -1 || closeParen == -1 || closeParen <= openParen) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (focus <= openParen || focus >= closeParen) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const params = visible.slice(openParen + 1, closeParen);
|
|
204
|
+
const matches = [
|
|
205
|
+
...params.matchAll(/([A-Za-z_]\w*)\s*:\s*[^,)=]+(?:=\s*[^,)]*)?/g),
|
|
206
|
+
];
|
|
207
|
+
if (!matches.length)
|
|
208
|
+
return null;
|
|
209
|
+
for (const match of matches) {
|
|
210
|
+
const localStart = match.index ?? -1;
|
|
211
|
+
if (localStart == -1)
|
|
212
|
+
continue;
|
|
213
|
+
const localEnd = localStart + match[0].length;
|
|
214
|
+
const absoluteStart = openParen + 1 + localStart;
|
|
215
|
+
const absoluteEnd = openParen + 1 + localEnd;
|
|
216
|
+
if (focus < absoluteStart || focus > absoluteEnd)
|
|
217
|
+
continue;
|
|
218
|
+
const name = match[1] ?? null;
|
|
219
|
+
if (!name)
|
|
220
|
+
return null;
|
|
221
|
+
const nameOffset = match[0].indexOf(name);
|
|
222
|
+
const equalsOffset = match[0].indexOf("=");
|
|
223
|
+
if (fallbackType == "DefaultValue" && equalsOffset != -1) {
|
|
224
|
+
const valueStart = absoluteStart + equalsOffset + 1;
|
|
225
|
+
const valueVisibleStart = skipCoverageWhitespace(visible, valueStart);
|
|
226
|
+
const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueVisibleStart, focus));
|
|
227
|
+
return {
|
|
228
|
+
type: "DefaultValue",
|
|
229
|
+
name,
|
|
230
|
+
start,
|
|
231
|
+
end,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
type: fallbackType == "Parameter" ? "Parameter" : "Property",
|
|
236
|
+
name,
|
|
237
|
+
start: absoluteStart + nameOffset,
|
|
238
|
+
end: absoluteStart + nameOffset + name.length,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function detectCoverageInlineParameter(visible, focus, fallbackType) {
|
|
244
|
+
const match = visible.match(/^([A-Za-z_]\w*)\s*:\s*[^=,]+(?:=\s*[^,]+)?[,]?$/);
|
|
245
|
+
if (!match)
|
|
246
|
+
return null;
|
|
247
|
+
const name = match[1] ?? null;
|
|
248
|
+
if (!name)
|
|
249
|
+
return null;
|
|
250
|
+
const nameStart = visible.indexOf(name);
|
|
251
|
+
const nameEnd = nameStart + name.length;
|
|
252
|
+
const equalsIndex = visible.indexOf("=");
|
|
253
|
+
if (fallbackType == "DefaultValue" && equalsIndex != -1) {
|
|
254
|
+
const valueStart = skipCoverageWhitespace(visible, equalsIndex + 1);
|
|
255
|
+
const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueStart, focus));
|
|
256
|
+
return {
|
|
257
|
+
type: "DefaultValue",
|
|
258
|
+
name,
|
|
259
|
+
start,
|
|
260
|
+
end,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
type: fallbackType == "Parameter" ? "Parameter" : "Property",
|
|
265
|
+
name,
|
|
266
|
+
start: nameStart,
|
|
267
|
+
end: nameEnd,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function detectCoverageTernary(visible, focus, fallbackType) {
|
|
271
|
+
if (fallbackType != "Ternary" && fallbackType != "LogicalBranch") {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const q = visible.indexOf("?");
|
|
275
|
+
if (q == -1)
|
|
276
|
+
return null;
|
|
277
|
+
if (fallbackType == "LogicalBranch") {
|
|
278
|
+
const [start, end] = resolveCoverageHighlightSpan(visible, focus);
|
|
279
|
+
return { type: "LogicalBranch", start, end };
|
|
280
|
+
}
|
|
281
|
+
const colon = visible.indexOf(":", q + 1);
|
|
282
|
+
if (colon == -1) {
|
|
283
|
+
const [start, end] = resolveCoverageHighlightSpan(visible, focus);
|
|
284
|
+
return { type: "Ternary", start, end };
|
|
285
|
+
}
|
|
286
|
+
const branchStart = focus <= colon ? q + 1 : colon + 1;
|
|
287
|
+
const normalizedStart = skipCoverageWhitespace(visible, branchStart);
|
|
288
|
+
const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(normalizedStart, focus));
|
|
289
|
+
return { type: "Ternary", start, end };
|
|
290
|
+
}
|
|
291
|
+
function detectCoverageIfBranch(visible, fallbackType) {
|
|
292
|
+
if (fallbackType != "IfBranch")
|
|
293
|
+
return null;
|
|
294
|
+
const match = visible.match(/^if\s*\(([^)]*)\)/);
|
|
295
|
+
if (!match)
|
|
296
|
+
return null;
|
|
297
|
+
const full = match[0];
|
|
298
|
+
const condition = match[1] ?? "";
|
|
299
|
+
const openParen = full.indexOf("(");
|
|
300
|
+
const conditionPadding = condition.length
|
|
301
|
+
? condition.length - condition.trimStart().length
|
|
302
|
+
: 0;
|
|
303
|
+
const conditionStart = openParen == -1 ? -1 : openParen + 1 + conditionPadding;
|
|
304
|
+
if (conditionStart == -1 || !condition.length) {
|
|
305
|
+
return { type: "IfBranch", start: 0, end: full.length };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
type: "IfBranch",
|
|
309
|
+
start: conditionStart,
|
|
310
|
+
end: conditionStart + condition.length,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function detectCoverageAssignment(visible, fallbackType) {
|
|
314
|
+
if (fallbackType != "Assignment")
|
|
315
|
+
return null;
|
|
316
|
+
const match = visible.match(/([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]+\])?)\s*(=|\+=|-=|\*=|\*\*=|\/=|%=|<<=|>>=|>>>=|&=|\|=|\^=)/);
|
|
317
|
+
if (!match)
|
|
318
|
+
return null;
|
|
319
|
+
const full = match[0];
|
|
320
|
+
const lhs = match[1] ?? "";
|
|
321
|
+
const operator = match[2] ?? "=";
|
|
322
|
+
const fullStart = visible.indexOf(full);
|
|
323
|
+
const lhsStart = fullStart + full.indexOf(lhs);
|
|
324
|
+
const operatorStart = fullStart + full.lastIndexOf(operator);
|
|
325
|
+
return {
|
|
326
|
+
type: "Assignment",
|
|
327
|
+
start: lhsStart,
|
|
328
|
+
end: operatorStart + operator.length,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
133
331
|
function detectCoverageCall(visible, focus) {
|
|
134
332
|
const matches = [...visible.matchAll(/\b([A-Za-z_]\w*)(?:<[^>()]+>)?\s*\(/g)];
|
|
135
333
|
if (!matches.length)
|
|
@@ -171,3 +369,10 @@ function detectCoverageCall(visible, focus) {
|
|
|
171
369
|
function isCoverageBoundary(ch) {
|
|
172
370
|
return /[\s()[\]{}.,;:+\-*/%&|^!?=<>]/.test(ch);
|
|
173
371
|
}
|
|
372
|
+
function skipCoverageWhitespace(visible, index) {
|
|
373
|
+
let current = Math.max(0, Math.min(visible.length - 1, index));
|
|
374
|
+
while (current < visible.length - 1 && /\s/.test(visible.charAt(current))) {
|
|
375
|
+
current++;
|
|
376
|
+
}
|
|
377
|
+
return current;
|
|
378
|
+
}
|
package/bin/crash-store.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
export function persistCrashRecord(rootDir, record) {
|
|
4
|
-
const entry = record.entryKey?.length
|
|
4
|
+
const entry = record.entryKey?.length
|
|
5
|
+
? record.entryKey
|
|
6
|
+
: crashEntryKey(record.file);
|
|
5
7
|
const dir = path.resolve(process.cwd(), rootDir);
|
|
6
8
|
mkdirSync(dir, { recursive: true });
|
|
7
9
|
const jsonPath = path.join(dir, `${entry}.json`);
|