as-test 1.3.0 → 1.4.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 +41 -0
- package/README.md +1 -4
- package/assembly/index.ts +66 -47
- package/assembly/src/expectation.ts +44 -86
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/reflect.ts +122 -0
- package/assembly/src/stringify.ts +240 -0
- package/assembly/src/suite.ts +48 -27
- package/assembly/src/tests.ts +7 -7
- package/assembly/util/wipc.ts +2 -2
- package/bin/build-worker-pool.js +9 -0
- package/bin/build-worker.js +27 -3
- package/bin/commands/build-core.js +144 -82
- package/bin/commands/init-core.js +0 -3
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +2 -1
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +534 -79
- package/bin/reporters/default.js +34 -0
- package/bin/util.js +9 -0
- package/package.json +3 -7
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +2 -0
- package/transform/lib/log.js +3 -7
- package/transform/lib/types.js +4 -2
- package/transform/lib/transform.js +0 -502
package/bin/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getBuildInvocationPreview,
|
|
9
9
|
warnOnUnknownModeReferences,
|
|
10
10
|
} from "./commands/build.js";
|
|
11
|
-
import { createRunReporter, run } from "./commands/run.js";
|
|
11
|
+
import { createRunReporter, resetCollectedLogs, run } from "./commands/run.js";
|
|
12
12
|
import { executeBuildCommand } from "./commands/build.js";
|
|
13
13
|
import { executeRunCommand } from "./commands/run.js";
|
|
14
14
|
import { executeTestCommand } from "./commands/test.js";
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
loadConfig,
|
|
27
27
|
resolveArtifactPath,
|
|
28
28
|
resolveModeNames,
|
|
29
|
+
resolveSnapshotPath,
|
|
29
30
|
resolveSpecRelativePath,
|
|
30
31
|
} from "./util.js";
|
|
31
32
|
import { normalizeFeatureName } from "./types.js";
|
|
@@ -34,9 +35,12 @@ import { spawnSync } from "child_process";
|
|
|
34
35
|
import { glob } from "glob";
|
|
35
36
|
import { createInterface } from "readline";
|
|
36
37
|
import { existsSync, watch as fsWatch } from "fs";
|
|
38
|
+
import { minimatch } from "minimatch";
|
|
37
39
|
import { availableParallelism, cpus } from "os";
|
|
38
40
|
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
39
41
|
import { PersistentWebSessionHost } from "./commands/web-session.js";
|
|
42
|
+
import { buildRecorderStorage } from "./commands/build-core.js";
|
|
43
|
+
import { DependencyGraph } from "./dependency-graph.js";
|
|
40
44
|
const _args = process.argv.slice(2);
|
|
41
45
|
const flags = [];
|
|
42
46
|
const args = [];
|
|
@@ -285,9 +289,6 @@ function printCommandHelp(command) {
|
|
|
285
289
|
);
|
|
286
290
|
process.stdout.write("Compile selected specs into wasm artifacts.\n\n");
|
|
287
291
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
288
|
-
process.stdout.write(
|
|
289
|
-
" --config <path> Use a specific config file\n",
|
|
290
|
-
);
|
|
291
292
|
process.stdout.write(
|
|
292
293
|
" --mode <name[,name...]> Run one or multiple named config modes\n",
|
|
293
294
|
);
|
|
@@ -298,7 +299,7 @@ function printCommandHelp(command) {
|
|
|
298
299
|
" --disable <list> Disable features, comma-separated\n",
|
|
299
300
|
);
|
|
300
301
|
process.stdout.write(
|
|
301
|
-
" --
|
|
302
|
+
" --config <path> Use a specific config file\n",
|
|
302
303
|
);
|
|
303
304
|
process.stdout.write(
|
|
304
305
|
" --jobs <n> Run files through an ordered worker pool\n",
|
|
@@ -394,16 +395,19 @@ function printCommandHelp(command) {
|
|
|
394
395
|
);
|
|
395
396
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
396
397
|
process.stdout.write(
|
|
397
|
-
" --
|
|
398
|
+
" --mode <name[,name...]> Run one or multiple named config modes\n",
|
|
398
399
|
);
|
|
399
400
|
process.stdout.write(
|
|
400
|
-
" --
|
|
401
|
+
" --enable <list> Enable features, comma-separated (e.g. coverage,try-as,simd)\n",
|
|
401
402
|
);
|
|
402
403
|
process.stdout.write(
|
|
403
|
-
" --
|
|
404
|
+
" --disable <list> Disable features, comma-separated\n",
|
|
404
405
|
);
|
|
405
406
|
process.stdout.write(
|
|
406
|
-
" --
|
|
407
|
+
" --config <path> Use a specific config file\n",
|
|
408
|
+
);
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n",
|
|
407
411
|
);
|
|
408
412
|
process.stdout.write(
|
|
409
413
|
" --jobs <n> Run files through an ordered worker pool\n",
|
|
@@ -430,12 +434,6 @@ function printCommandHelp(command) {
|
|
|
430
434
|
" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n",
|
|
431
435
|
);
|
|
432
436
|
process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
|
|
433
|
-
process.stdout.write(
|
|
434
|
-
" --enable <list> Enable features, comma-separated (e.g. coverage,try-as,simd)\n",
|
|
435
|
-
);
|
|
436
|
-
process.stdout.write(
|
|
437
|
-
" --disable <list> Disable features, comma-separated\n",
|
|
438
|
-
);
|
|
439
437
|
process.stdout.write(
|
|
440
438
|
" --fuzz Run fuzz targets after the normal test pass\n",
|
|
441
439
|
);
|
|
@@ -476,7 +474,7 @@ function printCommandHelp(command) {
|
|
|
476
474
|
" --list-modes Preview configured and selected mode names\n",
|
|
477
475
|
);
|
|
478
476
|
process.stdout.write(
|
|
479
|
-
" --watch
|
|
477
|
+
" --watch, -w Re-run on source or spec changes\n",
|
|
480
478
|
);
|
|
481
479
|
process.stdout.write(" --help, -h Show this help\n");
|
|
482
480
|
return;
|
|
@@ -694,7 +692,7 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
694
692
|
if (arg == "--parallel") {
|
|
695
693
|
continue;
|
|
696
694
|
}
|
|
697
|
-
if (arg == "--watch") {
|
|
695
|
+
if (arg == "--watch" || arg == "-w") {
|
|
698
696
|
continue;
|
|
699
697
|
}
|
|
700
698
|
if (
|
|
@@ -1325,6 +1323,10 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
1325
1323
|
return values;
|
|
1326
1324
|
}
|
|
1327
1325
|
async function buildFileForMode(args) {
|
|
1326
|
+
// If the caller has an active buildRecorderStorage context (e.g. the watch
|
|
1327
|
+
// loop), forward reads from the pool's worker process back into the same
|
|
1328
|
+
// recorder so the dependency graph still gets populated under --parallel.
|
|
1329
|
+
const recorder = buildRecorderStorage.getStore();
|
|
1328
1330
|
if (args.buildPool) {
|
|
1329
1331
|
const buildInvocation = await getBuildInvocationPreview(
|
|
1330
1332
|
args.configPath,
|
|
@@ -1338,6 +1340,11 @@ async function buildFileForMode(args) {
|
|
|
1338
1340
|
modeName: args.modeName,
|
|
1339
1341
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1340
1342
|
featureToggles: args.buildFeatureToggles,
|
|
1343
|
+
onReads: recorder
|
|
1344
|
+
? (reads) => {
|
|
1345
|
+
for (const r of reads) recorder.record(r.mode, r.spec, r.file);
|
|
1346
|
+
}
|
|
1347
|
+
: undefined,
|
|
1341
1348
|
});
|
|
1342
1349
|
} else {
|
|
1343
1350
|
await build(
|
|
@@ -1361,6 +1368,7 @@ async function runTestSequential(
|
|
|
1361
1368
|
reporterOverride,
|
|
1362
1369
|
webSession = null,
|
|
1363
1370
|
emitRunComplete = true,
|
|
1371
|
+
onSpecOutcome,
|
|
1364
1372
|
) {
|
|
1365
1373
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1366
1374
|
if (!files.length) {
|
|
@@ -1379,6 +1387,7 @@ async function runTestSequential(
|
|
|
1379
1387
|
runtimeName: reporterSession.runtimeName,
|
|
1380
1388
|
clean: runFlags.clean,
|
|
1381
1389
|
verbose: runFlags.verbose,
|
|
1390
|
+
showLogs: runFlags.showLogs,
|
|
1382
1391
|
snapshotEnabled,
|
|
1383
1392
|
createSnapshots: runFlags.createSnapshots,
|
|
1384
1393
|
});
|
|
@@ -1417,6 +1426,7 @@ async function runTestSequential(
|
|
|
1417
1426
|
}
|
|
1418
1427
|
results.push(result);
|
|
1419
1428
|
if (result?.failed) failed = true;
|
|
1429
|
+
onSpecOutcome?.({ file, mode: modeName, failed: !!result?.failed });
|
|
1420
1430
|
}
|
|
1421
1431
|
const summary = aggregateRunResults(results);
|
|
1422
1432
|
summary.stats = applyConfiguredFileTotalToStats(
|
|
@@ -1435,6 +1445,8 @@ async function runTestSequential(
|
|
|
1435
1445
|
coverageSummary: summary.coverageSummary,
|
|
1436
1446
|
stats: summary.stats,
|
|
1437
1447
|
reports: summary.reports,
|
|
1448
|
+
showLogs: runFlags.showLogs,
|
|
1449
|
+
logSummary: summary.logSummary,
|
|
1438
1450
|
modeSummary: buildSingleModeSummary(
|
|
1439
1451
|
summary.stats,
|
|
1440
1452
|
summary.snapshotSummary,
|
|
@@ -1452,6 +1464,7 @@ async function runTestSequential(
|
|
|
1452
1464
|
coverageSummary: summary.coverageSummary,
|
|
1453
1465
|
stats: summary.stats,
|
|
1454
1466
|
reports: summary.reports,
|
|
1467
|
+
logSummary: summary.logSummary,
|
|
1455
1468
|
},
|
|
1456
1469
|
};
|
|
1457
1470
|
}
|
|
@@ -1644,6 +1657,7 @@ async function runRuntimeMatrix(
|
|
|
1644
1657
|
runtimeName: reporterSession.runtimeName,
|
|
1645
1658
|
clean: runFlags.clean,
|
|
1646
1659
|
verbose: runFlags.verbose,
|
|
1660
|
+
showLogs: runFlags.showLogs,
|
|
1647
1661
|
snapshotEnabled,
|
|
1648
1662
|
createSnapshots: runFlags.createSnapshots,
|
|
1649
1663
|
});
|
|
@@ -1747,6 +1761,8 @@ async function runRuntimeMatrix(
|
|
|
1747
1761
|
coverageSummary: summary.coverageSummary,
|
|
1748
1762
|
stats: summary.stats,
|
|
1749
1763
|
reports: summary.reports,
|
|
1764
|
+
showLogs: runFlags.showLogs,
|
|
1765
|
+
logSummary: summary.logSummary,
|
|
1750
1766
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1751
1767
|
});
|
|
1752
1768
|
return allResults.some((result) => result.failed);
|
|
@@ -1761,6 +1777,7 @@ async function runTestModesCore(
|
|
|
1761
1777
|
buildFeatureToggles,
|
|
1762
1778
|
fuzzEnabled,
|
|
1763
1779
|
fuzzOverrides,
|
|
1780
|
+
onSpecOutcome,
|
|
1764
1781
|
) {
|
|
1765
1782
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1766
1783
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
@@ -1793,6 +1810,7 @@ async function runTestModesCore(
|
|
|
1793
1810
|
fileSummaryTotal,
|
|
1794
1811
|
fuzzEnabled,
|
|
1795
1812
|
fuzzOverrides,
|
|
1813
|
+
onSpecOutcome,
|
|
1796
1814
|
);
|
|
1797
1815
|
return failed;
|
|
1798
1816
|
}
|
|
@@ -1810,6 +1828,7 @@ async function runTestModesCore(
|
|
|
1810
1828
|
fuzzEnabled,
|
|
1811
1829
|
fuzzOverrides,
|
|
1812
1830
|
modeName,
|
|
1831
|
+
onSpecOutcome,
|
|
1813
1832
|
);
|
|
1814
1833
|
if (modeFailed) failed = true;
|
|
1815
1834
|
}
|
|
@@ -1828,6 +1847,7 @@ async function runTestModesCore(
|
|
|
1828
1847
|
fileSummaryTotal,
|
|
1829
1848
|
fuzzEnabled,
|
|
1830
1849
|
fuzzOverrides,
|
|
1850
|
+
onSpecOutcome,
|
|
1831
1851
|
);
|
|
1832
1852
|
return failed;
|
|
1833
1853
|
}
|
|
@@ -1856,6 +1876,7 @@ async function runTestModesCore(
|
|
|
1856
1876
|
reporterSession.reporter,
|
|
1857
1877
|
sharedWebSession,
|
|
1858
1878
|
!fuzzEnabled,
|
|
1879
|
+
onSpecOutcome,
|
|
1859
1880
|
);
|
|
1860
1881
|
if (modeResult.failed) failed = true;
|
|
1861
1882
|
if (fuzzEnabled) {
|
|
@@ -1884,6 +1905,8 @@ async function runTestModesCore(
|
|
|
1884
1905
|
coverageSummary: modeResult.summary.coverageSummary,
|
|
1885
1906
|
stats: modeResult.summary.stats,
|
|
1886
1907
|
reports: modeResult.summary.reports,
|
|
1908
|
+
showLogs: effectiveRunFlags.showLogs,
|
|
1909
|
+
logSummary: modeResult.summary.logSummary,
|
|
1887
1910
|
fuzzSummary: summarizeFuzzExecutions(fuzzResults),
|
|
1888
1911
|
modeSummary: buildSingleModeSummary(
|
|
1889
1912
|
modeResult.summary.stats,
|
|
@@ -1951,99 +1974,461 @@ async function runWatchLoop(
|
|
|
1951
1974
|
) {
|
|
1952
1975
|
const resolvedConfigPath =
|
|
1953
1976
|
configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1977
|
+
const absConfigPath = path.resolve(resolvedConfigPath);
|
|
1978
|
+
let config = loadConfig(resolvedConfigPath, false);
|
|
1979
|
+
// Respect the user's parallelism flags. Worker-pool builds forward their
|
|
1980
|
+
// file-read records back through IPC (see BuildWorkerPool / build-worker
|
|
1981
|
+
// and buildFileForMode), so the dependency graph stays correct under
|
|
1982
|
+
// --parallel as well.
|
|
1983
|
+
const watchRunFlags = { ...runFlags };
|
|
1984
|
+
const graph = new DependencyGraph();
|
|
1985
|
+
let graphPopulated = false;
|
|
1986
|
+
// Sticky failure tracker: each entry is "this (spec, mode) was last seen
|
|
1987
|
+
// failing." Only updated for (spec, mode) pairs that actually re-ran this
|
|
1988
|
+
// iteration, so a spec the watch loop skipped stays visible at the bottom
|
|
1989
|
+
// until it passes again. `space` retries this exact set.
|
|
1990
|
+
const failingSpecs = new Map();
|
|
1991
|
+
function recordSpecOutcome(file, mode, failed) {
|
|
1992
|
+
const abs = path.resolve(file);
|
|
1993
|
+
const modes = failingSpecs.get(abs) ?? new Set();
|
|
1994
|
+
if (failed) {
|
|
1995
|
+
modes.add(mode);
|
|
1996
|
+
failingSpecs.set(abs, modes);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (!modes.size) return;
|
|
2000
|
+
modes.delete(mode);
|
|
2001
|
+
if (modes.size === 0) failingSpecs.delete(abs);
|
|
2002
|
+
}
|
|
1956
2003
|
let isRunning = false;
|
|
1957
|
-
let
|
|
2004
|
+
let pendingTrigger = null;
|
|
1958
2005
|
let debounceTimer = null;
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2006
|
+
// Toggled with `w`. When false, file changes are remembered in
|
|
2007
|
+
// `changedWhilePaused` but not auto-run — the user invokes runs manually
|
|
2008
|
+
// (`a` / space). Resuming re-runs everything if anything changed meanwhile.
|
|
2009
|
+
let autoRun = true;
|
|
2010
|
+
const changedWhilePaused = new Set();
|
|
2011
|
+
// Keyed by absolute directory path so attachWatcherFor is idempotent;
|
|
2012
|
+
// makes it safe to re-scan & top up watchers after every iteration without
|
|
2013
|
+
// double-attaching to the same dir.
|
|
2014
|
+
const attachedDirWatchers = new Map();
|
|
2015
|
+
let configFileWatcher = null;
|
|
2016
|
+
function describeAffected(specs) {
|
|
2017
|
+
const list = Array.from(specs).map((s) => path.relative(process.cwd(), s));
|
|
2018
|
+
if (list.length <= 3) return list.join(", ");
|
|
2019
|
+
return `${list.slice(0, 3).join(", ")} (+${list.length - 3} more)`;
|
|
2020
|
+
}
|
|
2021
|
+
function watchFooter() {
|
|
2022
|
+
if (!autoRun) {
|
|
2023
|
+
const pending = changedWhilePaused.size
|
|
2024
|
+
? ` (${changedWhilePaused.size} change(s) pending)`
|
|
2025
|
+
: "";
|
|
2026
|
+
return chalk.dim(
|
|
2027
|
+
`Auto-run paused${pending}. ` +
|
|
2028
|
+
chalk.bold("w") +
|
|
2029
|
+
" = resume, " +
|
|
2030
|
+
chalk.bold("a") +
|
|
2031
|
+
" = re-run all, " +
|
|
2032
|
+
chalk.bold("space") +
|
|
2033
|
+
" = retry failing, " +
|
|
2034
|
+
chalk.bold("ctrl+c") +
|
|
2035
|
+
" = stop.\n",
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
return chalk.dim(
|
|
2039
|
+
"Watching for changes. " +
|
|
2040
|
+
chalk.bold("space") +
|
|
2041
|
+
" = retry failing, " +
|
|
2042
|
+
chalk.bold("a") +
|
|
2043
|
+
" = re-run all, " +
|
|
2044
|
+
chalk.bold("w") +
|
|
2045
|
+
" = pause, " +
|
|
2046
|
+
chalk.bold("ctrl+c") +
|
|
2047
|
+
" = stop.\n",
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
function writeWatchHeader(headline, detail) {
|
|
2051
|
+
// Preserve scrollback — never `console.clear()`. A blank line plus a
|
|
2052
|
+
// dim rule visually delimits each iteration so prior output stays
|
|
2053
|
+
// readable above.
|
|
2054
|
+
process.stdout.write(
|
|
2055
|
+
"\n" +
|
|
2056
|
+
chalk.dim("─".repeat(Math.max(24, process.stdout.columns ?? 60))) +
|
|
2057
|
+
"\n" +
|
|
1963
2058
|
chalk.dim(`[${new Date().toLocaleTimeString()}] `) +
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2059
|
+
chalk.yellow(headline) +
|
|
2060
|
+
(detail ? chalk.bold(detail) : "") +
|
|
2061
|
+
"\n",
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
// Render the sticky "currently failing" pin. Renders nothing when empty so
|
|
2065
|
+
// happy paths stay visually clean. Mode tags shown only when the user has
|
|
2066
|
+
// configured >1 mode; in the single-mode case the bare path is enough.
|
|
2067
|
+
function renderFailingSpecs() {
|
|
2068
|
+
if (failingSpecs.size === 0) return "";
|
|
2069
|
+
const multiMode = modes.length > 1;
|
|
2070
|
+
const entries = Array.from(failingSpecs.entries()).map(([abs, modeSet]) => {
|
|
2071
|
+
const rel = path.relative(process.cwd(), abs);
|
|
2072
|
+
const tags = multiMode
|
|
2073
|
+
? Array.from(modeSet, (m) => m ?? "default").sort()
|
|
2074
|
+
: [];
|
|
2075
|
+
return { rel, tags };
|
|
2076
|
+
});
|
|
2077
|
+
entries.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
2078
|
+
const MAX_LINES = 8;
|
|
2079
|
+
const shown = entries.slice(0, MAX_LINES);
|
|
2080
|
+
const overflow = entries.length - shown.length;
|
|
2081
|
+
const lines = [];
|
|
2082
|
+
lines.push(chalk.red.bold(`Currently failing (${failingSpecs.size}):`));
|
|
2083
|
+
for (const { rel, tags } of shown) {
|
|
2084
|
+
const tagSuffix = tags.length ? chalk.dim(` [${tags.join(", ")}]`) : "";
|
|
2085
|
+
lines.push(` ${chalk.gray(rel)}${tagSuffix}`);
|
|
2086
|
+
}
|
|
2087
|
+
if (overflow > 0) {
|
|
2088
|
+
lines.push(chalk.dim(` (+${overflow} more)`));
|
|
2089
|
+
}
|
|
2090
|
+
return lines.join("\n") + "\n\n";
|
|
2091
|
+
}
|
|
2092
|
+
async function doRun(trigger) {
|
|
2093
|
+
let runSelectors = selectors;
|
|
2094
|
+
let scopedRun = false;
|
|
2095
|
+
if (trigger.kind === "manual-rerun") {
|
|
2096
|
+
if (failingSpecs.size === 0) {
|
|
2097
|
+
// Nothing to retry; stay silent so we don't pollute scrollback.
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
const failingPaths = new Set(failingSpecs.keys());
|
|
2101
|
+
writeWatchHeader("Retrying failing specs");
|
|
2102
|
+
process.stdout.write(
|
|
2103
|
+
chalk.dim(`Retrying ${failingPaths.size} failing spec(s): `) +
|
|
2104
|
+
chalk.bold(describeAffected(failingPaths)) +
|
|
2105
|
+
"\n",
|
|
2106
|
+
);
|
|
2107
|
+
runSelectors = Array.from(failingPaths).map((spec) =>
|
|
2108
|
+
path.relative(process.cwd(), spec),
|
|
1967
2109
|
);
|
|
2110
|
+
scopedRun = true;
|
|
2111
|
+
} else if (trigger.kind === "manual-runall") {
|
|
2112
|
+
writeWatchHeader("Re-running all specs");
|
|
2113
|
+
} else if (trigger.kind === "change") {
|
|
2114
|
+
const absChanged = path.resolve(trigger.file);
|
|
2115
|
+
if (absChanged === absConfigPath) {
|
|
2116
|
+
writeWatchHeader("Change detected: ", trigger.file);
|
|
2117
|
+
process.stdout.write(
|
|
2118
|
+
chalk.dim("Config changed; reloading and rebuilding everything.\n"),
|
|
2119
|
+
);
|
|
2120
|
+
try {
|
|
2121
|
+
config = loadConfig(resolvedConfigPath, false);
|
|
2122
|
+
} catch (e) {
|
|
2123
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2124
|
+
process.stderr.write(
|
|
2125
|
+
chalk.red("Failed to reload config: ") + msg + "\n",
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
graph.clear();
|
|
2129
|
+
graphPopulated = false;
|
|
2130
|
+
failingSpecs.clear();
|
|
2131
|
+
// The previous config's input/snapshotDir may no longer be relevant;
|
|
2132
|
+
// drop every dir watcher and re-derive from the fresh config so we
|
|
2133
|
+
// don't leak watchers for dirs we no longer care about.
|
|
2134
|
+
closeDirWatchers();
|
|
2135
|
+
refreshWatchedDirs();
|
|
2136
|
+
} else if (graphPopulated) {
|
|
2137
|
+
const affected = new Set(graph.specsAffectedBy(absChanged));
|
|
2138
|
+
// A new .spec.ts file the graph hasn't seen yet should still be
|
|
2139
|
+
// built and run. Cheapest heuristic without re-globbing: treat any
|
|
2140
|
+
// .spec.ts under cwd that we don't already know as a candidate.
|
|
2141
|
+
if (
|
|
2142
|
+
affected.size === 0 &&
|
|
2143
|
+
/\.spec\.ts$/i.test(absChanged) &&
|
|
2144
|
+
existsSync(absChanged) &&
|
|
2145
|
+
!graph.knownSpecs().has(absChanged)
|
|
2146
|
+
) {
|
|
2147
|
+
affected.add(absChanged);
|
|
2148
|
+
}
|
|
2149
|
+
if (affected.size === 0) {
|
|
2150
|
+
// Nothing depends on this file; skip silently so scrollback isn't
|
|
2151
|
+
// littered with no-op events for unrelated edits.
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
writeWatchHeader("Change detected: ", trigger.file);
|
|
2155
|
+
process.stdout.write(
|
|
2156
|
+
chalk.dim(`Rebuilding ${affected.size} affected spec(s): `) +
|
|
2157
|
+
chalk.bold(describeAffected(affected)) +
|
|
2158
|
+
"\n",
|
|
2159
|
+
);
|
|
2160
|
+
runSelectors = Array.from(affected).map((spec) =>
|
|
2161
|
+
path.relative(process.cwd(), spec),
|
|
2162
|
+
);
|
|
2163
|
+
scopedRun = true;
|
|
2164
|
+
} else {
|
|
2165
|
+
// Change arrived before the graph was populated (e.g. queued during
|
|
2166
|
+
// the initial run). Run everything; surface the header so the user
|
|
2167
|
+
// knows the iteration was caused by the edit.
|
|
2168
|
+
writeWatchHeader("Change detected: ", trigger.file);
|
|
2169
|
+
}
|
|
1968
2170
|
}
|
|
2171
|
+
process.stdout.write("\n");
|
|
2172
|
+
// A full re-run starts the aggregated-log collector fresh and clears any
|
|
2173
|
+
// changes tracked while paused (they're now covered). Scoped reruns keep
|
|
2174
|
+
// prior specs' logs (each re-run spec's entry is replaced in place), so
|
|
2175
|
+
// latest.log stays complete instead of collapsing to just the rerun.
|
|
2176
|
+
if (!scopedRun) {
|
|
2177
|
+
resetCollectedLogs();
|
|
2178
|
+
changedWhilePaused.clear();
|
|
2179
|
+
}
|
|
2180
|
+
const collected = [];
|
|
2181
|
+
const recorder = {
|
|
2182
|
+
record: (mode, specFile, absPath) => {
|
|
2183
|
+
collected.push({ mode, spec: specFile, file: absPath });
|
|
2184
|
+
},
|
|
2185
|
+
};
|
|
1969
2186
|
try {
|
|
1970
|
-
await
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2187
|
+
await buildRecorderStorage.run(recorder, async () => {
|
|
2188
|
+
await runTestModesCore(
|
|
2189
|
+
watchRunFlags,
|
|
2190
|
+
configPath,
|
|
2191
|
+
runSelectors,
|
|
2192
|
+
suiteSelectors,
|
|
2193
|
+
fuzzerSelectors,
|
|
2194
|
+
modes,
|
|
2195
|
+
buildFeatureToggles,
|
|
2196
|
+
fuzzEnabled,
|
|
2197
|
+
fuzzOverrides,
|
|
2198
|
+
(outcome) =>
|
|
2199
|
+
recordSpecOutcome(outcome.file, outcome.mode, outcome.failed),
|
|
2200
|
+
);
|
|
2201
|
+
});
|
|
1981
2202
|
} catch (error) {
|
|
1982
2203
|
const message = error instanceof Error ? error.message : String(error);
|
|
1983
2204
|
process.stderr.write(chalk.red("Error: ") + message + "\n");
|
|
1984
2205
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2206
|
+
// Bucket reads by (mode, spec) then merge into the graph. Skipped specs
|
|
2207
|
+
// (those not touched this run) keep their prior entries — important for
|
|
2208
|
+
// scoped reruns and for builds that fail before any reads happen.
|
|
2209
|
+
const bySpec = new Map();
|
|
2210
|
+
for (const entry of collected) {
|
|
2211
|
+
const key = `${entry.mode ?? ""}${entry.spec}`;
|
|
2212
|
+
let bucket = bySpec.get(key);
|
|
2213
|
+
if (!bucket) {
|
|
2214
|
+
bucket = { mode: entry.mode, spec: entry.spec, files: new Set() };
|
|
2215
|
+
bySpec.set(key, bucket);
|
|
2216
|
+
}
|
|
2217
|
+
bucket.files.add(entry.file);
|
|
2218
|
+
}
|
|
2219
|
+
for (const bucket of bySpec.values()) {
|
|
2220
|
+
if (config.snapshotDir) {
|
|
2221
|
+
bucket.files.add(
|
|
2222
|
+
resolveSnapshotPath(bucket.spec, config.snapshotDir, config.input),
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
graph.recordBuild(bucket.spec, bucket.mode, bucket.files);
|
|
2226
|
+
}
|
|
2227
|
+
// Only mark populated after a full (unscoped) run that recorded at least
|
|
2228
|
+
// one spec — that way we never trust a scoped run to validate the full
|
|
2229
|
+
// dependency picture.
|
|
2230
|
+
if (!scopedRun && bySpec.size > 0) {
|
|
2231
|
+
graphPopulated = true;
|
|
2232
|
+
}
|
|
2233
|
+
// Top up watchers after every iteration so newly-created dirs (the
|
|
2234
|
+
// snapshot dir on first run, dirs introduced by config edits, etc.) get
|
|
2235
|
+
// monitored without restarting the loop.
|
|
2236
|
+
refreshWatchedDirs();
|
|
2237
|
+
attachConfigWatcher();
|
|
2238
|
+
process.stdout.write("\n" + renderFailingSpecs() + watchFooter());
|
|
2239
|
+
}
|
|
2240
|
+
function attachWatcherFor(absDir, recursive) {
|
|
2241
|
+
if (attachedDirWatchers.has(absDir)) return;
|
|
2242
|
+
if (!existsSync(absDir)) return;
|
|
2243
|
+
try {
|
|
2244
|
+
const w = fsWatch(absDir, { recursive }, (_evt, filename) => {
|
|
2245
|
+
if (!filename) return;
|
|
2246
|
+
const full = path.join(absDir, filename);
|
|
2247
|
+
if (shouldIgnoreWatchPath(full, config)) return;
|
|
2248
|
+
scheduleRerun(path.relative(process.cwd(), full));
|
|
2249
|
+
});
|
|
2250
|
+
attachedDirWatchers.set(absDir, w);
|
|
2251
|
+
} catch {
|
|
2252
|
+
// some dirs (or filesystems) can't be watched recursively; skip.
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
// Called after every iteration so dirs created lazily (snapshotDir on
|
|
2256
|
+
// first run, dirs introduced by config reload, etc.) get watchers.
|
|
2257
|
+
function refreshWatchedDirs() {
|
|
2258
|
+
const cwd = process.cwd();
|
|
2259
|
+
for (const dir of resolveWatchDirectories(config, modes)) {
|
|
2260
|
+
attachWatcherFor(dir, true);
|
|
2261
|
+
}
|
|
2262
|
+
for (const file of graph.allRecordedFiles()) {
|
|
2263
|
+
const rel = path.relative(cwd, file);
|
|
2264
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
2265
|
+
if (rel.startsWith("node_modules") || rel.startsWith(".git")) continue;
|
|
2266
|
+
attachWatcherFor(path.dirname(file), false);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function attachConfigWatcher() {
|
|
2270
|
+
if (configFileWatcher) return;
|
|
2271
|
+
if (!existsSync(resolvedConfigPath)) return;
|
|
2272
|
+
try {
|
|
2273
|
+
configFileWatcher = fsWatch(resolvedConfigPath, () => {
|
|
2274
|
+
scheduleRerun(path.relative(process.cwd(), resolvedConfigPath));
|
|
2275
|
+
});
|
|
2276
|
+
} catch {
|
|
2277
|
+
// ignore — fs.watch on a single file isn't supported everywhere.
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function closeDirWatchers() {
|
|
2281
|
+
for (const w of attachedDirWatchers.values()) w.close();
|
|
2282
|
+
attachedDirWatchers.clear();
|
|
2283
|
+
}
|
|
2284
|
+
function closeAllWatchers() {
|
|
2285
|
+
closeDirWatchers();
|
|
2286
|
+
if (configFileWatcher) {
|
|
2287
|
+
configFileWatcher.close();
|
|
2288
|
+
configFileWatcher = null;
|
|
2289
|
+
}
|
|
1988
2290
|
}
|
|
1989
2291
|
async function triggerRerun() {
|
|
1990
|
-
const
|
|
1991
|
-
|
|
2292
|
+
const trigger = pendingTrigger ?? { kind: "initial" };
|
|
2293
|
+
pendingTrigger = null;
|
|
1992
2294
|
isRunning = true;
|
|
1993
2295
|
try {
|
|
1994
|
-
await doRun(
|
|
2296
|
+
await doRun(trigger);
|
|
1995
2297
|
} finally {
|
|
1996
2298
|
isRunning = false;
|
|
1997
|
-
if (
|
|
2299
|
+
if (pendingTrigger) {
|
|
1998
2300
|
void triggerRerun();
|
|
1999
2301
|
}
|
|
2000
2302
|
}
|
|
2001
2303
|
}
|
|
2002
|
-
function
|
|
2003
|
-
|
|
2304
|
+
function scheduleTrigger(next, delayMs) {
|
|
2305
|
+
// Manual triggers preempt any pending change-trigger; if a manual one is
|
|
2306
|
+
// already pending, leave it (latest wins regardless).
|
|
2307
|
+
pendingTrigger = next;
|
|
2004
2308
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2005
2309
|
debounceTimer = setTimeout(() => {
|
|
2006
2310
|
if (isRunning) return;
|
|
2007
2311
|
void triggerRerun();
|
|
2008
|
-
},
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
if (!existsSync(dir)) continue;
|
|
2016
|
-
try {
|
|
2017
|
-
const w = fsWatch(dir, { recursive: true }, (_evt, filename) => {
|
|
2018
|
-
if (!filename) return;
|
|
2019
|
-
const full = path.join(dir, filename);
|
|
2020
|
-
if (shouldIgnoreWatchPath(full, config)) return;
|
|
2021
|
-
scheduleRerun(path.relative(process.cwd(), full));
|
|
2022
|
-
});
|
|
2023
|
-
watchers.push(w);
|
|
2024
|
-
} catch {
|
|
2025
|
-
// directory may not support recursive watching; skip
|
|
2312
|
+
}, delayMs);
|
|
2313
|
+
}
|
|
2314
|
+
function scheduleRerun(filename) {
|
|
2315
|
+
if (!autoRun) {
|
|
2316
|
+
// Manual mode: remember the edit (for the footer + resume) but don't run.
|
|
2317
|
+
changedWhilePaused.add(path.resolve(filename));
|
|
2318
|
+
return;
|
|
2026
2319
|
}
|
|
2320
|
+
scheduleTrigger({ kind: "change", file: filename }, 150);
|
|
2321
|
+
}
|
|
2322
|
+
function scheduleManualRerun(kind) {
|
|
2323
|
+
scheduleTrigger({ kind }, 0);
|
|
2027
2324
|
}
|
|
2028
|
-
|
|
2325
|
+
// Attach watchers before the initial run so file events that happen during
|
|
2326
|
+
// the run are queued for the next iteration.
|
|
2327
|
+
refreshWatchedDirs();
|
|
2328
|
+
attachConfigWatcher();
|
|
2329
|
+
// Initial run populates the graph as a side effect of recording every read.
|
|
2330
|
+
await doRun({ kind: "initial" });
|
|
2331
|
+
const stdin = process.stdin;
|
|
2332
|
+
let rawModeEnabled = false;
|
|
2333
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
2029
2334
|
try {
|
|
2030
|
-
|
|
2031
|
-
|
|
2335
|
+
stdin.setRawMode(true);
|
|
2336
|
+
rawModeEnabled = true;
|
|
2337
|
+
stdin.resume();
|
|
2338
|
+
stdin.on("data", (chunk) => {
|
|
2339
|
+
// A held key (or any chord with multiple bytes) arrives as a single
|
|
2340
|
+
// chunk; we only need to honour the first actionable byte. Without
|
|
2341
|
+
// this guard, holding `space` would schedule a fresh rerun for every
|
|
2342
|
+
// byte in the chunk.
|
|
2343
|
+
for (const byte of chunk) {
|
|
2344
|
+
if (byte === 0x03) {
|
|
2345
|
+
if (rawModeEnabled) stdin.setRawMode(false);
|
|
2346
|
+
closeAllWatchers();
|
|
2347
|
+
process.exit(0);
|
|
2348
|
+
}
|
|
2349
|
+
if (isRunning) break;
|
|
2350
|
+
if (byte === 0x77 || byte === 0x57) {
|
|
2351
|
+
// `w` — toggle auto-run / manual mode.
|
|
2352
|
+
autoRun = !autoRun;
|
|
2353
|
+
if (autoRun) {
|
|
2354
|
+
const hadPending = changedWhilePaused.size > 0;
|
|
2355
|
+
if (hadPending) {
|
|
2356
|
+
process.stdout.write(
|
|
2357
|
+
"\n" +
|
|
2358
|
+
chalk.dim(
|
|
2359
|
+
"Auto-run resumed — re-running all (files changed while paused).\n",
|
|
2360
|
+
),
|
|
2361
|
+
);
|
|
2362
|
+
scheduleManualRerun("manual-runall");
|
|
2363
|
+
} else {
|
|
2364
|
+
process.stdout.write(
|
|
2365
|
+
"\n" + chalk.dim("Auto-run resumed.\n") + watchFooter(),
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
} else {
|
|
2369
|
+
process.stdout.write(
|
|
2370
|
+
"\n" +
|
|
2371
|
+
chalk.dim(
|
|
2372
|
+
"Auto-run paused — edits won't re-run. Press w to resume, or a / space to run now.\n",
|
|
2373
|
+
) +
|
|
2374
|
+
watchFooter(),
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
if (byte === 0x20 || byte === 0x0d || byte === 0x0a) {
|
|
2380
|
+
scheduleManualRerun("manual-rerun");
|
|
2381
|
+
break;
|
|
2382
|
+
}
|
|
2383
|
+
if (byte === 0x61 || byte === 0x41) {
|
|
2384
|
+
scheduleManualRerun("manual-runall");
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2032
2388
|
});
|
|
2033
|
-
|
|
2034
|
-
|
|
2389
|
+
} catch {
|
|
2390
|
+
// some terminals don't support raw mode (e.g. piped stdin); just fall
|
|
2391
|
+
// back to SIGINT-only behavior.
|
|
2392
|
+
}
|
|
2035
2393
|
}
|
|
2036
2394
|
process.on("SIGINT", () => {
|
|
2037
|
-
|
|
2395
|
+
if (rawModeEnabled) stdin.setRawMode(false);
|
|
2396
|
+
closeAllWatchers();
|
|
2038
2397
|
process.exit(0);
|
|
2039
2398
|
});
|
|
2040
2399
|
// Keep the process alive
|
|
2041
2400
|
await new Promise(() => {});
|
|
2042
2401
|
}
|
|
2043
|
-
|
|
2402
|
+
// Union of every glob the user has declared as a spec/fuzz source —
|
|
2403
|
+
// top-level plus each mode override. We watch only directories derived from
|
|
2404
|
+
// these so unrelated files (e.g. CLI source while developing as-test itself)
|
|
2405
|
+
// don't trigger spurious iterations.
|
|
2406
|
+
function collectInputPatterns(config, modes) {
|
|
2407
|
+
const out = new Set();
|
|
2408
|
+
for (const p of config.input) out.add(p);
|
|
2409
|
+
for (const p of config.fuzz.input) out.add(p);
|
|
2410
|
+
for (const modeName of modes) {
|
|
2411
|
+
if (!modeName) continue;
|
|
2412
|
+
let merged;
|
|
2413
|
+
try {
|
|
2414
|
+
merged = applyMode(config, modeName);
|
|
2415
|
+
} catch {
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
for (const p of merged.config.input) out.add(p);
|
|
2419
|
+
for (const p of merged.config.fuzz.input) out.add(p);
|
|
2420
|
+
}
|
|
2421
|
+
return [...out];
|
|
2422
|
+
}
|
|
2423
|
+
function resolveWatchDirectories(config, modes) {
|
|
2044
2424
|
const cwd = process.cwd();
|
|
2045
2425
|
const dirs = new Set();
|
|
2046
|
-
|
|
2426
|
+
const patterns = collectInputPatterns(config, modes);
|
|
2427
|
+
for (const pattern of patterns) {
|
|
2428
|
+
// `!`-prefixed entries are exclusions, not include sources — their
|
|
2429
|
+
// "base" would be `!` (which never exists), causing the walk-up loop
|
|
2430
|
+
// to land on cwd and watch the whole repo.
|
|
2431
|
+
if (pattern.startsWith("!")) continue;
|
|
2047
2432
|
const starIdx = pattern.indexOf("*");
|
|
2048
2433
|
const base = starIdx >= 0 ? pattern.slice(0, starIdx) : pattern;
|
|
2049
2434
|
let dir = path.resolve(cwd, base);
|
|
@@ -2052,20 +2437,62 @@ function resolveWatchDirectories(config) {
|
|
|
2052
2437
|
}
|
|
2053
2438
|
if (existsSync(dir)) dirs.add(dir);
|
|
2054
2439
|
}
|
|
2055
|
-
const
|
|
2056
|
-
if (existsSync(
|
|
2057
|
-
if (dirs.size === 0) dirs.add(cwd);
|
|
2440
|
+
const snapshotDir = path.resolve(cwd, config.snapshotDir);
|
|
2441
|
+
if (existsSync(snapshotDir)) dirs.add(snapshotDir);
|
|
2058
2442
|
return [...dirs];
|
|
2059
2443
|
}
|
|
2444
|
+
// Returns true if `rel` (a cwd-relative path with `/` separators) matches any
|
|
2445
|
+
// `!`-prefixed pattern from the supplied input arrays. Lets users opt out of
|
|
2446
|
+
// watch events for files they've already excluded from their spec/fuzz globs
|
|
2447
|
+
// — no separate watch config needed.
|
|
2448
|
+
function matchesAnyExclusion(rel, inputs) {
|
|
2449
|
+
const normalized = rel.split(path.sep).join("/");
|
|
2450
|
+
for (const input of inputs) {
|
|
2451
|
+
if (!input) continue;
|
|
2452
|
+
const patterns = Array.isArray(input) ? input : [input];
|
|
2453
|
+
for (const raw of patterns) {
|
|
2454
|
+
if (typeof raw != "string" || !raw.startsWith("!")) continue;
|
|
2455
|
+
const pattern = raw.slice(1);
|
|
2456
|
+
if (!pattern.length) continue;
|
|
2457
|
+
if (minimatch(normalized, pattern, { dot: true, matchBase: true })) {
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2060
2464
|
function shouldIgnoreWatchPath(filePath, config) {
|
|
2061
2465
|
const cwd = process.cwd();
|
|
2062
2466
|
const rel = path.relative(cwd, filePath);
|
|
2063
2467
|
if (rel.startsWith("node_modules") || rel.startsWith(".git")) return true;
|
|
2468
|
+
// Dotfiles (.swp, .DS_Store, …) are always noise.
|
|
2469
|
+
const base = path.basename(rel);
|
|
2470
|
+
if (base.startsWith(".")) return true;
|
|
2471
|
+
// Respect `!`-prefixed glob negations in the user's existing input arrays.
|
|
2472
|
+
// Files the user already excluded from their spec/fuzz globs are also out
|
|
2473
|
+
// of scope for watch — no separate "watch.ignore" config needed.
|
|
2474
|
+
if (matchesAnyExclusion(rel, [config.input, config.fuzz?.input])) {
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2064
2477
|
const outRel = path.normalize(
|
|
2065
2478
|
path.relative(cwd, path.resolve(cwd, config.outDir)),
|
|
2066
2479
|
);
|
|
2067
|
-
|
|
2068
|
-
|
|
2480
|
+
const snapRel = path.normalize(
|
|
2481
|
+
path.relative(cwd, path.resolve(cwd, config.snapshotDir)),
|
|
2482
|
+
);
|
|
2483
|
+
const underSnap = rel === snapRel || rel.startsWith(snapRel + path.sep);
|
|
2484
|
+
const underOut = rel === outRel || rel.startsWith(outRel + path.sep);
|
|
2485
|
+
// Snapshots often live under outDir (default ./.as-test/snapshots); they
|
|
2486
|
+
// are dependencies of their specs, so we keep watching them even when the
|
|
2487
|
+
// rest of the output tree is ignored.
|
|
2488
|
+
if (underOut && !underSnap) return true;
|
|
2489
|
+
if (
|
|
2490
|
+
!rel.endsWith(".ts") &&
|
|
2491
|
+
!rel.endsWith(".snap") &&
|
|
2492
|
+
!rel.endsWith("as-test.config.json")
|
|
2493
|
+
) {
|
|
2494
|
+
return true;
|
|
2495
|
+
}
|
|
2069
2496
|
return false;
|
|
2070
2497
|
}
|
|
2071
2498
|
async function runTestMatrix(
|
|
@@ -2080,6 +2507,7 @@ async function runTestMatrix(
|
|
|
2080
2507
|
fileSummaryTotal,
|
|
2081
2508
|
fuzzEnabled,
|
|
2082
2509
|
fuzzOverrides,
|
|
2510
|
+
onSpecOutcome,
|
|
2083
2511
|
) {
|
|
2084
2512
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
2085
2513
|
if (files.length && configPath) {
|
|
@@ -2110,6 +2538,7 @@ async function runTestMatrix(
|
|
|
2110
2538
|
runtimeName: reporterSession.runtimeName,
|
|
2111
2539
|
clean: runFlags.clean,
|
|
2112
2540
|
verbose: runFlags.verbose,
|
|
2541
|
+
showLogs: runFlags.showLogs,
|
|
2113
2542
|
snapshotEnabled,
|
|
2114
2543
|
createSnapshots: runFlags.createSnapshots,
|
|
2115
2544
|
});
|
|
@@ -2185,6 +2614,7 @@ async function runTestMatrix(
|
|
|
2185
2614
|
}
|
|
2186
2615
|
fileResults.push(result);
|
|
2187
2616
|
allResults.push(result);
|
|
2617
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2188
2618
|
}
|
|
2189
2619
|
if (reporterSession.reporterKind == "default") {
|
|
2190
2620
|
renderMatrixFileResult(
|
|
@@ -2238,6 +2668,8 @@ async function runTestMatrix(
|
|
|
2238
2668
|
coverageSummary: summary.coverageSummary,
|
|
2239
2669
|
stats: summary.stats,
|
|
2240
2670
|
reports: summary.reports,
|
|
2671
|
+
showLogs: runFlags.showLogs,
|
|
2672
|
+
logSummary: summary.logSummary,
|
|
2241
2673
|
fuzzSummary,
|
|
2242
2674
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2243
2675
|
});
|
|
@@ -2323,6 +2755,7 @@ async function runRuntimeSingleParallel(
|
|
|
2323
2755
|
runtimeName: reporterSession.runtimeName,
|
|
2324
2756
|
clean: runFlags.clean,
|
|
2325
2757
|
verbose: runFlags.verbose,
|
|
2758
|
+
showLogs: runFlags.showLogs,
|
|
2326
2759
|
snapshotEnabled,
|
|
2327
2760
|
createSnapshots: runFlags.createSnapshots,
|
|
2328
2761
|
});
|
|
@@ -2393,6 +2826,8 @@ async function runRuntimeSingleParallel(
|
|
|
2393
2826
|
coverageSummary: summary.coverageSummary,
|
|
2394
2827
|
stats: summary.stats,
|
|
2395
2828
|
reports: summary.reports,
|
|
2829
|
+
showLogs: runFlags.showLogs,
|
|
2830
|
+
logSummary: summary.logSummary,
|
|
2396
2831
|
modeSummary: buildSingleModeSummary(
|
|
2397
2832
|
summary.stats,
|
|
2398
2833
|
summary.snapshotSummary,
|
|
@@ -2426,6 +2861,7 @@ async function runRuntimeMatrixParallel(
|
|
|
2426
2861
|
runtimeName: reporterSession.runtimeName,
|
|
2427
2862
|
clean: runFlags.clean,
|
|
2428
2863
|
verbose: runFlags.verbose,
|
|
2864
|
+
showLogs: runFlags.showLogs,
|
|
2429
2865
|
snapshotEnabled,
|
|
2430
2866
|
createSnapshots: runFlags.createSnapshots,
|
|
2431
2867
|
});
|
|
@@ -2542,6 +2978,8 @@ async function runRuntimeMatrixParallel(
|
|
|
2542
2978
|
coverageSummary: summary.coverageSummary,
|
|
2543
2979
|
stats: summary.stats,
|
|
2544
2980
|
reports: summary.reports,
|
|
2981
|
+
showLogs: runFlags.showLogs,
|
|
2982
|
+
logSummary: summary.logSummary,
|
|
2545
2983
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2546
2984
|
});
|
|
2547
2985
|
reporter.flush?.();
|
|
@@ -2560,6 +2998,7 @@ async function runTestSingleParallel(
|
|
|
2560
2998
|
fuzzEnabled,
|
|
2561
2999
|
fuzzOverrides,
|
|
2562
3000
|
modeName,
|
|
3001
|
+
onSpecOutcome,
|
|
2563
3002
|
) {
|
|
2564
3003
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
2565
3004
|
if (!files.length && !fuzzEnabled) {
|
|
@@ -2576,6 +3015,7 @@ async function runTestSingleParallel(
|
|
|
2576
3015
|
runtimeName: reporterSession.runtimeName,
|
|
2577
3016
|
clean: runFlags.clean,
|
|
2578
3017
|
verbose: runFlags.verbose,
|
|
3018
|
+
showLogs: runFlags.showLogs,
|
|
2579
3019
|
snapshotEnabled,
|
|
2580
3020
|
createSnapshots: runFlags.createSnapshots,
|
|
2581
3021
|
});
|
|
@@ -2646,6 +3086,7 @@ async function runTestSingleParallel(
|
|
|
2646
3086
|
}
|
|
2647
3087
|
buffered?.reporter.flush?.();
|
|
2648
3088
|
results[index] = result;
|
|
3089
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2649
3090
|
if (buffered && token != null) {
|
|
2650
3091
|
queueDisplay.complete(token, buffered.output());
|
|
2651
3092
|
}
|
|
@@ -2693,6 +3134,8 @@ async function runTestSingleParallel(
|
|
|
2693
3134
|
coverageSummary: summary.coverageSummary,
|
|
2694
3135
|
stats: summary.stats,
|
|
2695
3136
|
reports: summary.reports,
|
|
3137
|
+
showLogs: runFlags.showLogs,
|
|
3138
|
+
logSummary: summary.logSummary,
|
|
2696
3139
|
fuzzSummary,
|
|
2697
3140
|
modeSummary: buildSingleModeSummary(
|
|
2698
3141
|
summary.stats,
|
|
@@ -2716,6 +3159,7 @@ async function runTestMatrixParallel(
|
|
|
2716
3159
|
fileSummaryTotal,
|
|
2717
3160
|
fuzzEnabled,
|
|
2718
3161
|
fuzzOverrides,
|
|
3162
|
+
onSpecOutcome,
|
|
2719
3163
|
) {
|
|
2720
3164
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
2721
3165
|
if (files.length && configPath) {
|
|
@@ -2749,6 +3193,7 @@ async function runTestMatrixParallel(
|
|
|
2749
3193
|
runtimeName: reporterSession.runtimeName,
|
|
2750
3194
|
clean: runFlags.clean,
|
|
2751
3195
|
verbose: runFlags.verbose,
|
|
3196
|
+
showLogs: runFlags.showLogs,
|
|
2752
3197
|
snapshotEnabled,
|
|
2753
3198
|
createSnapshots: runFlags.createSnapshots,
|
|
2754
3199
|
});
|
|
@@ -2814,6 +3259,7 @@ async function runTestMatrixParallel(
|
|
|
2814
3259
|
}
|
|
2815
3260
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
2816
3261
|
fileResults.push(result);
|
|
3262
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2817
3263
|
}
|
|
2818
3264
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
2819
3265
|
if (token != null) {
|
|
@@ -2886,6 +3332,8 @@ async function runTestMatrixParallel(
|
|
|
2886
3332
|
coverageSummary: summary.coverageSummary,
|
|
2887
3333
|
stats: summary.stats,
|
|
2888
3334
|
reports: summary.reports,
|
|
3335
|
+
showLogs: runFlags.showLogs,
|
|
3336
|
+
logSummary: summary.logSummary,
|
|
2889
3337
|
fuzzSummary,
|
|
2890
3338
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2891
3339
|
});
|
|
@@ -3289,6 +3737,7 @@ function createBuildFailureRunResult(error) {
|
|
|
3289
3737
|
files: [],
|
|
3290
3738
|
},
|
|
3291
3739
|
reports: [report],
|
|
3740
|
+
logSummary: { count: 0, file: null, groups: [], text: "" },
|
|
3292
3741
|
};
|
|
3293
3742
|
}
|
|
3294
3743
|
function formatBuildFailureMessage(error) {
|
|
@@ -4276,6 +4725,9 @@ function aggregateRunResults(results) {
|
|
|
4276
4725
|
let fallbackCoverageUncovered = 0;
|
|
4277
4726
|
const fallbackCoverageFiles = [];
|
|
4278
4727
|
const reports = [];
|
|
4728
|
+
// run() rewrites the cumulative latest.log on every call and returns the
|
|
4729
|
+
// running total, so the result with the highest count is the most complete.
|
|
4730
|
+
let logSummary = { count: 0, file: null, groups: [], text: "" };
|
|
4279
4731
|
for (const result of results) {
|
|
4280
4732
|
stats.passedFiles += result.stats.passedFiles;
|
|
4281
4733
|
stats.failedFiles += result.stats.failedFiles;
|
|
@@ -4315,6 +4767,9 @@ function aggregateRunResults(results) {
|
|
|
4315
4767
|
}
|
|
4316
4768
|
}
|
|
4317
4769
|
reports.push(...result.reports);
|
|
4770
|
+
if (result.logSummary && result.logSummary.count >= logSummary.count) {
|
|
4771
|
+
logSummary = result.logSummary;
|
|
4772
|
+
}
|
|
4318
4773
|
}
|
|
4319
4774
|
if (uniqueCoveragePoints.size > 0) {
|
|
4320
4775
|
const byFile = new Map();
|
|
@@ -4360,7 +4815,7 @@ function aggregateRunResults(results) {
|
|
|
4360
4815
|
coverageSummary.percent = coverageSummary.total
|
|
4361
4816
|
? (coverageSummary.covered * 100) / coverageSummary.total
|
|
4362
4817
|
: 100;
|
|
4363
|
-
return { stats, snapshotSummary, coverageSummary, reports };
|
|
4818
|
+
return { stats, snapshotSummary, coverageSummary, reports, logSummary };
|
|
4364
4819
|
}
|
|
4365
4820
|
function printCliError(error) {
|
|
4366
4821
|
const message = error instanceof Error ? error.message : String(error);
|