as-test 1.2.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 +60 -0
- package/README.md +1 -4
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +68 -47
- package/assembly/src/expectation.ts +154 -123
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/mode.ts +55 -0
- 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 +293 -86
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -8
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -2
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +592 -97
- package/bin/reporters/default.js +34 -0
- package/bin/types.js +7 -0
- package/bin/util.js +52 -0
- package/package.json +4 -8
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +28 -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
|
@@ -3,10 +3,12 @@ import chalk from "chalk";
|
|
|
3
3
|
import {
|
|
4
4
|
build,
|
|
5
5
|
BuildFailureError,
|
|
6
|
+
flushModeWarnings,
|
|
6
7
|
formatInvocation as formatBuildInvocation,
|
|
7
8
|
getBuildInvocationPreview,
|
|
9
|
+
warnOnUnknownModeReferences,
|
|
8
10
|
} from "./commands/build.js";
|
|
9
|
-
import { createRunReporter, run } from "./commands/run.js";
|
|
11
|
+
import { createRunReporter, resetCollectedLogs, run } from "./commands/run.js";
|
|
10
12
|
import { executeBuildCommand } from "./commands/build.js";
|
|
11
13
|
import { executeRunCommand } from "./commands/run.js";
|
|
12
14
|
import { executeTestCommand } from "./commands/test.js";
|
|
@@ -24,16 +26,21 @@ import {
|
|
|
24
26
|
loadConfig,
|
|
25
27
|
resolveArtifactPath,
|
|
26
28
|
resolveModeNames,
|
|
29
|
+
resolveSnapshotPath,
|
|
27
30
|
resolveSpecRelativePath,
|
|
28
31
|
} from "./util.js";
|
|
32
|
+
import { normalizeFeatureName } from "./types.js";
|
|
29
33
|
import * as path from "path";
|
|
30
34
|
import { spawnSync } from "child_process";
|
|
31
35
|
import { glob } from "glob";
|
|
32
36
|
import { createInterface } from "readline";
|
|
33
37
|
import { existsSync, watch as fsWatch } from "fs";
|
|
38
|
+
import { minimatch } from "minimatch";
|
|
34
39
|
import { availableParallelism, cpus } from "os";
|
|
35
40
|
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
36
41
|
import { PersistentWebSessionHost } from "./commands/web-session.js";
|
|
42
|
+
import { buildRecorderStorage } from "./commands/build-core.js";
|
|
43
|
+
import { DependencyGraph } from "./dependency-graph.js";
|
|
37
44
|
const _args = process.argv.slice(2);
|
|
38
45
|
const flags = [];
|
|
39
46
|
const args = [];
|
|
@@ -282,20 +289,17 @@ function printCommandHelp(command) {
|
|
|
282
289
|
);
|
|
283
290
|
process.stdout.write("Compile selected specs into wasm artifacts.\n\n");
|
|
284
291
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
285
|
-
process.stdout.write(
|
|
286
|
-
" --config <path> Use a specific config file\n",
|
|
287
|
-
);
|
|
288
292
|
process.stdout.write(
|
|
289
293
|
" --mode <name[,name...]> Run one or multiple named config modes\n",
|
|
290
294
|
);
|
|
291
295
|
process.stdout.write(
|
|
292
|
-
" --enable <
|
|
296
|
+
" --enable <list> Enable features, comma-separated (e.g. coverage,try-as,simd)\n",
|
|
293
297
|
);
|
|
294
298
|
process.stdout.write(
|
|
295
|
-
" --disable <
|
|
299
|
+
" --disable <list> Disable features, comma-separated\n",
|
|
296
300
|
);
|
|
297
301
|
process.stdout.write(
|
|
298
|
-
" --
|
|
302
|
+
" --config <path> Use a specific config file\n",
|
|
299
303
|
);
|
|
300
304
|
process.stdout.write(
|
|
301
305
|
" --jobs <n> Run files through an ordered worker pool\n",
|
|
@@ -356,10 +360,10 @@ function printCommandHelp(command) {
|
|
|
356
360
|
);
|
|
357
361
|
process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
|
|
358
362
|
process.stdout.write(
|
|
359
|
-
" --enable <
|
|
363
|
+
" --enable <list> Enable features, comma-separated (e.g. coverage,try-as,simd)\n",
|
|
360
364
|
);
|
|
361
365
|
process.stdout.write(
|
|
362
|
-
" --disable <
|
|
366
|
+
" --disable <list> Disable features, comma-separated\n",
|
|
363
367
|
);
|
|
364
368
|
process.stdout.write(
|
|
365
369
|
" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n",
|
|
@@ -391,16 +395,19 @@ function printCommandHelp(command) {
|
|
|
391
395
|
);
|
|
392
396
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
393
397
|
process.stdout.write(
|
|
394
|
-
" --
|
|
398
|
+
" --mode <name[,name...]> Run one or multiple named config modes\n",
|
|
395
399
|
);
|
|
396
400
|
process.stdout.write(
|
|
397
|
-
" --
|
|
401
|
+
" --enable <list> Enable features, comma-separated (e.g. coverage,try-as,simd)\n",
|
|
398
402
|
);
|
|
399
403
|
process.stdout.write(
|
|
400
|
-
" --
|
|
404
|
+
" --disable <list> Disable features, comma-separated\n",
|
|
401
405
|
);
|
|
402
406
|
process.stdout.write(
|
|
403
|
-
" --
|
|
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",
|
|
404
411
|
);
|
|
405
412
|
process.stdout.write(
|
|
406
413
|
" --jobs <n> Run files through an ordered worker pool\n",
|
|
@@ -427,12 +434,6 @@ function printCommandHelp(command) {
|
|
|
427
434
|
" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n",
|
|
428
435
|
);
|
|
429
436
|
process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
|
|
430
|
-
process.stdout.write(
|
|
431
|
-
" --enable <feature> Enable feature (coverage|try-as)\n",
|
|
432
|
-
);
|
|
433
|
-
process.stdout.write(
|
|
434
|
-
" --disable <feature> Disable feature (coverage|try-as)\n",
|
|
435
|
-
);
|
|
436
437
|
process.stdout.write(
|
|
437
438
|
" --fuzz Run fuzz targets after the normal test pass\n",
|
|
438
439
|
);
|
|
@@ -473,7 +474,7 @@ function printCommandHelp(command) {
|
|
|
473
474
|
" --list-modes Preview configured and selected mode names\n",
|
|
474
475
|
);
|
|
475
476
|
process.stdout.write(
|
|
476
|
-
" --watch
|
|
477
|
+
" --watch, -w Re-run on source or spec changes\n",
|
|
477
478
|
);
|
|
478
479
|
process.stdout.write(" --help, -h Show this help\n");
|
|
479
480
|
return;
|
|
@@ -534,6 +535,12 @@ function printCommandHelp(command) {
|
|
|
534
535
|
process.stdout.write(
|
|
535
536
|
" --example <minimal|full|none> Set example template\n",
|
|
536
537
|
);
|
|
538
|
+
process.stdout.write(
|
|
539
|
+
" --enable <list> Enable features, comma-separated (coverage,try-as)\n",
|
|
540
|
+
);
|
|
541
|
+
process.stdout.write(
|
|
542
|
+
" --disable <list> Disable features, comma-separated\n",
|
|
543
|
+
);
|
|
537
544
|
process.stdout.write(
|
|
538
545
|
" --install Install dependencies after scaffolding\n",
|
|
539
546
|
);
|
|
@@ -685,7 +692,7 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
685
692
|
if (arg == "--parallel") {
|
|
686
693
|
continue;
|
|
687
694
|
}
|
|
688
|
-
if (arg == "--watch") {
|
|
695
|
+
if (arg == "--watch" || arg == "-w") {
|
|
689
696
|
continue;
|
|
690
697
|
}
|
|
691
698
|
if (
|
|
@@ -709,8 +716,9 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
709
716
|
return values;
|
|
710
717
|
}
|
|
711
718
|
function resolveFeatureToggles(rawArgs, command) {
|
|
712
|
-
if (command !== "build" && command !== "run" && command !== "test")
|
|
713
|
-
|
|
719
|
+
if (command !== "build" && command !== "run" && command !== "test")
|
|
720
|
+
return { featureOverrides: {} };
|
|
721
|
+
const out = { featureOverrides: {} };
|
|
714
722
|
let seenCommand = false;
|
|
715
723
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
716
724
|
const arg = rawArgs[i];
|
|
@@ -722,7 +730,9 @@ function resolveFeatureToggles(rawArgs, command) {
|
|
|
722
730
|
const enabled = arg == "--enable";
|
|
723
731
|
const next = rawArgs[i + 1];
|
|
724
732
|
if (next && !next.startsWith("-")) {
|
|
725
|
-
|
|
733
|
+
for (const name of splitFeatureList(next)) {
|
|
734
|
+
applyFeatureToggle(out, name, enabled);
|
|
735
|
+
}
|
|
726
736
|
i++;
|
|
727
737
|
}
|
|
728
738
|
continue;
|
|
@@ -730,9 +740,9 @@ function resolveFeatureToggles(rawArgs, command) {
|
|
|
730
740
|
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
731
741
|
const enabled = arg.startsWith("--enable=");
|
|
732
742
|
const eq = arg.indexOf("=");
|
|
733
|
-
const value = arg.slice(eq + 1)
|
|
734
|
-
|
|
735
|
-
applyFeatureToggle(out,
|
|
743
|
+
const value = arg.slice(eq + 1);
|
|
744
|
+
for (const name of splitFeatureList(value)) {
|
|
745
|
+
applyFeatureToggle(out, name, enabled);
|
|
736
746
|
}
|
|
737
747
|
}
|
|
738
748
|
}
|
|
@@ -1280,19 +1290,24 @@ function parseIntegerFlag(flag, value) {
|
|
|
1280
1290
|
}
|
|
1281
1291
|
return Math.floor(parsed);
|
|
1282
1292
|
}
|
|
1293
|
+
function splitFeatureList(value) {
|
|
1294
|
+
return value
|
|
1295
|
+
.split(",")
|
|
1296
|
+
.map((part) => part.trim())
|
|
1297
|
+
.filter((part) => part.length > 0);
|
|
1298
|
+
}
|
|
1283
1299
|
function applyFeatureToggle(out, rawFeature, enabled) {
|
|
1284
|
-
const key = rawFeature
|
|
1300
|
+
const key = normalizeFeatureName(rawFeature);
|
|
1301
|
+
if (!key.length) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`empty feature name passed to ${enabled ? "--enable" : "--disable"}`,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1285
1306
|
if (key == "coverage") {
|
|
1286
1307
|
out.coverage = enabled;
|
|
1287
1308
|
return;
|
|
1288
1309
|
}
|
|
1289
|
-
|
|
1290
|
-
out.tryAs = enabled;
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
throw new Error(
|
|
1294
|
-
`unknown feature "${rawFeature}". Supported features: coverage, try-as`,
|
|
1295
|
-
);
|
|
1310
|
+
out.featureOverrides[key] = enabled;
|
|
1296
1311
|
}
|
|
1297
1312
|
function resolveCommandTokens(rawArgs, command) {
|
|
1298
1313
|
const values = [];
|
|
@@ -1308,6 +1323,10 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
1308
1323
|
return values;
|
|
1309
1324
|
}
|
|
1310
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();
|
|
1311
1330
|
if (args.buildPool) {
|
|
1312
1331
|
const buildInvocation = await getBuildInvocationPreview(
|
|
1313
1332
|
args.configPath,
|
|
@@ -1321,6 +1340,11 @@ async function buildFileForMode(args) {
|
|
|
1321
1340
|
modeName: args.modeName,
|
|
1322
1341
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1323
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,
|
|
1324
1348
|
});
|
|
1325
1349
|
} else {
|
|
1326
1350
|
await build(
|
|
@@ -1344,6 +1368,7 @@ async function runTestSequential(
|
|
|
1344
1368
|
reporterOverride,
|
|
1345
1369
|
webSession = null,
|
|
1346
1370
|
emitRunComplete = true,
|
|
1371
|
+
onSpecOutcome,
|
|
1347
1372
|
) {
|
|
1348
1373
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1349
1374
|
if (!files.length) {
|
|
@@ -1362,6 +1387,7 @@ async function runTestSequential(
|
|
|
1362
1387
|
runtimeName: reporterSession.runtimeName,
|
|
1363
1388
|
clean: runFlags.clean,
|
|
1364
1389
|
verbose: runFlags.verbose,
|
|
1390
|
+
showLogs: runFlags.showLogs,
|
|
1365
1391
|
snapshotEnabled,
|
|
1366
1392
|
createSnapshots: runFlags.createSnapshots,
|
|
1367
1393
|
});
|
|
@@ -1400,6 +1426,7 @@ async function runTestSequential(
|
|
|
1400
1426
|
}
|
|
1401
1427
|
results.push(result);
|
|
1402
1428
|
if (result?.failed) failed = true;
|
|
1429
|
+
onSpecOutcome?.({ file, mode: modeName, failed: !!result?.failed });
|
|
1403
1430
|
}
|
|
1404
1431
|
const summary = aggregateRunResults(results);
|
|
1405
1432
|
summary.stats = applyConfiguredFileTotalToStats(
|
|
@@ -1418,6 +1445,8 @@ async function runTestSequential(
|
|
|
1418
1445
|
coverageSummary: summary.coverageSummary,
|
|
1419
1446
|
stats: summary.stats,
|
|
1420
1447
|
reports: summary.reports,
|
|
1448
|
+
showLogs: runFlags.showLogs,
|
|
1449
|
+
logSummary: summary.logSummary,
|
|
1421
1450
|
modeSummary: buildSingleModeSummary(
|
|
1422
1451
|
summary.stats,
|
|
1423
1452
|
summary.snapshotSummary,
|
|
@@ -1425,6 +1454,7 @@ async function runTestSequential(
|
|
|
1425
1454
|
),
|
|
1426
1455
|
});
|
|
1427
1456
|
reporter.flush?.();
|
|
1457
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
1428
1458
|
}
|
|
1429
1459
|
return {
|
|
1430
1460
|
failed,
|
|
@@ -1434,6 +1464,7 @@ async function runTestSequential(
|
|
|
1434
1464
|
coverageSummary: summary.coverageSummary,
|
|
1435
1465
|
stats: summary.stats,
|
|
1436
1466
|
reports: summary.reports,
|
|
1467
|
+
logSummary: summary.logSummary,
|
|
1437
1468
|
},
|
|
1438
1469
|
};
|
|
1439
1470
|
}
|
|
@@ -1626,6 +1657,7 @@ async function runRuntimeMatrix(
|
|
|
1626
1657
|
runtimeName: reporterSession.runtimeName,
|
|
1627
1658
|
clean: runFlags.clean,
|
|
1628
1659
|
verbose: runFlags.verbose,
|
|
1660
|
+
showLogs: runFlags.showLogs,
|
|
1629
1661
|
snapshotEnabled,
|
|
1630
1662
|
createSnapshots: runFlags.createSnapshots,
|
|
1631
1663
|
});
|
|
@@ -1729,6 +1761,8 @@ async function runRuntimeMatrix(
|
|
|
1729
1761
|
coverageSummary: summary.coverageSummary,
|
|
1730
1762
|
stats: summary.stats,
|
|
1731
1763
|
reports: summary.reports,
|
|
1764
|
+
showLogs: runFlags.showLogs,
|
|
1765
|
+
logSummary: summary.logSummary,
|
|
1732
1766
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1733
1767
|
});
|
|
1734
1768
|
return allResults.some((result) => result.failed);
|
|
@@ -1743,6 +1777,7 @@ async function runTestModesCore(
|
|
|
1743
1777
|
buildFeatureToggles,
|
|
1744
1778
|
fuzzEnabled,
|
|
1745
1779
|
fuzzOverrides,
|
|
1780
|
+
onSpecOutcome,
|
|
1746
1781
|
) {
|
|
1747
1782
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1748
1783
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
@@ -1775,6 +1810,7 @@ async function runTestModesCore(
|
|
|
1775
1810
|
fileSummaryTotal,
|
|
1776
1811
|
fuzzEnabled,
|
|
1777
1812
|
fuzzOverrides,
|
|
1813
|
+
onSpecOutcome,
|
|
1778
1814
|
);
|
|
1779
1815
|
return failed;
|
|
1780
1816
|
}
|
|
@@ -1792,6 +1828,7 @@ async function runTestModesCore(
|
|
|
1792
1828
|
fuzzEnabled,
|
|
1793
1829
|
fuzzOverrides,
|
|
1794
1830
|
modeName,
|
|
1831
|
+
onSpecOutcome,
|
|
1795
1832
|
);
|
|
1796
1833
|
if (modeFailed) failed = true;
|
|
1797
1834
|
}
|
|
@@ -1810,6 +1847,7 @@ async function runTestModesCore(
|
|
|
1810
1847
|
fileSummaryTotal,
|
|
1811
1848
|
fuzzEnabled,
|
|
1812
1849
|
fuzzOverrides,
|
|
1850
|
+
onSpecOutcome,
|
|
1813
1851
|
);
|
|
1814
1852
|
return failed;
|
|
1815
1853
|
}
|
|
@@ -1838,6 +1876,7 @@ async function runTestModesCore(
|
|
|
1838
1876
|
reporterSession.reporter,
|
|
1839
1877
|
sharedWebSession,
|
|
1840
1878
|
!fuzzEnabled,
|
|
1879
|
+
onSpecOutcome,
|
|
1841
1880
|
);
|
|
1842
1881
|
if (modeResult.failed) failed = true;
|
|
1843
1882
|
if (fuzzEnabled) {
|
|
@@ -1866,6 +1905,8 @@ async function runTestModesCore(
|
|
|
1866
1905
|
coverageSummary: modeResult.summary.coverageSummary,
|
|
1867
1906
|
stats: modeResult.summary.stats,
|
|
1868
1907
|
reports: modeResult.summary.reports,
|
|
1908
|
+
showLogs: effectiveRunFlags.showLogs,
|
|
1909
|
+
logSummary: modeResult.summary.logSummary,
|
|
1869
1910
|
fuzzSummary: summarizeFuzzExecutions(fuzzResults),
|
|
1870
1911
|
modeSummary: buildSingleModeSummary(
|
|
1871
1912
|
modeResult.summary.stats,
|
|
@@ -1874,6 +1915,7 @@ async function runTestModesCore(
|
|
|
1874
1915
|
),
|
|
1875
1916
|
});
|
|
1876
1917
|
reporterSession.reporter.flush?.();
|
|
1918
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
1877
1919
|
}
|
|
1878
1920
|
}
|
|
1879
1921
|
} finally {
|
|
@@ -1932,99 +1974,461 @@ async function runWatchLoop(
|
|
|
1932
1974
|
) {
|
|
1933
1975
|
const resolvedConfigPath =
|
|
1934
1976
|
configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1935
|
-
const
|
|
1936
|
-
|
|
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
|
+
}
|
|
1937
2003
|
let isRunning = false;
|
|
1938
|
-
let
|
|
2004
|
+
let pendingTrigger = null;
|
|
1939
2005
|
let debounceTimer = null;
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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" +
|
|
1944
2058
|
chalk.dim(`[${new Date().toLocaleTimeString()}] `) +
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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),
|
|
1948
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
|
+
}
|
|
1949
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
|
+
};
|
|
1950
2186
|
try {
|
|
1951
|
-
await
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
+
});
|
|
1962
2202
|
} catch (error) {
|
|
1963
2203
|
const message = error instanceof Error ? error.message : String(error);
|
|
1964
2204
|
process.stderr.write(chalk.red("Error: ") + message + "\n");
|
|
1965
2205
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
+
}
|
|
1969
2290
|
}
|
|
1970
2291
|
async function triggerRerun() {
|
|
1971
|
-
const
|
|
1972
|
-
|
|
2292
|
+
const trigger = pendingTrigger ?? { kind: "initial" };
|
|
2293
|
+
pendingTrigger = null;
|
|
1973
2294
|
isRunning = true;
|
|
1974
2295
|
try {
|
|
1975
|
-
await doRun(
|
|
2296
|
+
await doRun(trigger);
|
|
1976
2297
|
} finally {
|
|
1977
2298
|
isRunning = false;
|
|
1978
|
-
if (
|
|
2299
|
+
if (pendingTrigger) {
|
|
1979
2300
|
void triggerRerun();
|
|
1980
2301
|
}
|
|
1981
2302
|
}
|
|
1982
2303
|
}
|
|
1983
|
-
function
|
|
1984
|
-
|
|
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;
|
|
1985
2308
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1986
2309
|
debounceTimer = setTimeout(() => {
|
|
1987
2310
|
if (isRunning) return;
|
|
1988
2311
|
void triggerRerun();
|
|
1989
|
-
},
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
if (!existsSync(dir)) continue;
|
|
1997
|
-
try {
|
|
1998
|
-
const w = fsWatch(dir, { recursive: true }, (_evt, filename) => {
|
|
1999
|
-
if (!filename) return;
|
|
2000
|
-
const full = path.join(dir, filename);
|
|
2001
|
-
if (shouldIgnoreWatchPath(full, config)) return;
|
|
2002
|
-
scheduleRerun(path.relative(process.cwd(), full));
|
|
2003
|
-
});
|
|
2004
|
-
watchers.push(w);
|
|
2005
|
-
} catch {
|
|
2006
|
-
// 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;
|
|
2007
2319
|
}
|
|
2320
|
+
scheduleTrigger({ kind: "change", file: filename }, 150);
|
|
2321
|
+
}
|
|
2322
|
+
function scheduleManualRerun(kind) {
|
|
2323
|
+
scheduleTrigger({ kind }, 0);
|
|
2008
2324
|
}
|
|
2009
|
-
|
|
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") {
|
|
2010
2334
|
try {
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
+
}
|
|
2013
2388
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2389
|
+
} catch {
|
|
2390
|
+
// some terminals don't support raw mode (e.g. piped stdin); just fall
|
|
2391
|
+
// back to SIGINT-only behavior.
|
|
2392
|
+
}
|
|
2016
2393
|
}
|
|
2017
2394
|
process.on("SIGINT", () => {
|
|
2018
|
-
|
|
2395
|
+
if (rawModeEnabled) stdin.setRawMode(false);
|
|
2396
|
+
closeAllWatchers();
|
|
2019
2397
|
process.exit(0);
|
|
2020
2398
|
});
|
|
2021
2399
|
// Keep the process alive
|
|
2022
2400
|
await new Promise(() => {});
|
|
2023
2401
|
}
|
|
2024
|
-
|
|
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) {
|
|
2025
2424
|
const cwd = process.cwd();
|
|
2026
2425
|
const dirs = new Set();
|
|
2027
|
-
|
|
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;
|
|
2028
2432
|
const starIdx = pattern.indexOf("*");
|
|
2029
2433
|
const base = starIdx >= 0 ? pattern.slice(0, starIdx) : pattern;
|
|
2030
2434
|
let dir = path.resolve(cwd, base);
|
|
@@ -2033,20 +2437,62 @@ function resolveWatchDirectories(config) {
|
|
|
2033
2437
|
}
|
|
2034
2438
|
if (existsSync(dir)) dirs.add(dir);
|
|
2035
2439
|
}
|
|
2036
|
-
const
|
|
2037
|
-
if (existsSync(
|
|
2038
|
-
if (dirs.size === 0) dirs.add(cwd);
|
|
2440
|
+
const snapshotDir = path.resolve(cwd, config.snapshotDir);
|
|
2441
|
+
if (existsSync(snapshotDir)) dirs.add(snapshotDir);
|
|
2039
2442
|
return [...dirs];
|
|
2040
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
|
+
}
|
|
2041
2464
|
function shouldIgnoreWatchPath(filePath, config) {
|
|
2042
2465
|
const cwd = process.cwd();
|
|
2043
2466
|
const rel = path.relative(cwd, filePath);
|
|
2044
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
|
+
}
|
|
2045
2477
|
const outRel = path.normalize(
|
|
2046
2478
|
path.relative(cwd, path.resolve(cwd, config.outDir)),
|
|
2047
2479
|
);
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
+
}
|
|
2050
2496
|
return false;
|
|
2051
2497
|
}
|
|
2052
2498
|
async function runTestMatrix(
|
|
@@ -2061,8 +2507,17 @@ async function runTestMatrix(
|
|
|
2061
2507
|
fileSummaryTotal,
|
|
2062
2508
|
fuzzEnabled,
|
|
2063
2509
|
fuzzOverrides,
|
|
2510
|
+
onSpecOutcome,
|
|
2064
2511
|
) {
|
|
2065
2512
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
2513
|
+
if (files.length && configPath) {
|
|
2514
|
+
try {
|
|
2515
|
+
const loaded = loadConfig(configPath, false);
|
|
2516
|
+
warnOnUnknownModeReferences(files, loaded.modes ?? {});
|
|
2517
|
+
} catch {
|
|
2518
|
+
// Best-effort: never fail the run on a scan error.
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2066
2521
|
if (!files.length) {
|
|
2067
2522
|
if (!fuzzEnabled) {
|
|
2068
2523
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -2083,6 +2538,7 @@ async function runTestMatrix(
|
|
|
2083
2538
|
runtimeName: reporterSession.runtimeName,
|
|
2084
2539
|
clean: runFlags.clean,
|
|
2085
2540
|
verbose: runFlags.verbose,
|
|
2541
|
+
showLogs: runFlags.showLogs,
|
|
2086
2542
|
snapshotEnabled,
|
|
2087
2543
|
createSnapshots: runFlags.createSnapshots,
|
|
2088
2544
|
});
|
|
@@ -2158,6 +2614,7 @@ async function runTestMatrix(
|
|
|
2158
2614
|
}
|
|
2159
2615
|
fileResults.push(result);
|
|
2160
2616
|
allResults.push(result);
|
|
2617
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2161
2618
|
}
|
|
2162
2619
|
if (reporterSession.reporterKind == "default") {
|
|
2163
2620
|
renderMatrixFileResult(
|
|
@@ -2211,10 +2668,13 @@ async function runTestMatrix(
|
|
|
2211
2668
|
coverageSummary: summary.coverageSummary,
|
|
2212
2669
|
stats: summary.stats,
|
|
2213
2670
|
reports: summary.reports,
|
|
2671
|
+
showLogs: runFlags.showLogs,
|
|
2672
|
+
logSummary: summary.logSummary,
|
|
2214
2673
|
fuzzSummary,
|
|
2215
2674
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2216
2675
|
});
|
|
2217
2676
|
reporter.flush?.();
|
|
2677
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
2218
2678
|
return failed;
|
|
2219
2679
|
}
|
|
2220
2680
|
async function runFuzzModes(
|
|
@@ -2295,6 +2755,7 @@ async function runRuntimeSingleParallel(
|
|
|
2295
2755
|
runtimeName: reporterSession.runtimeName,
|
|
2296
2756
|
clean: runFlags.clean,
|
|
2297
2757
|
verbose: runFlags.verbose,
|
|
2758
|
+
showLogs: runFlags.showLogs,
|
|
2298
2759
|
snapshotEnabled,
|
|
2299
2760
|
createSnapshots: runFlags.createSnapshots,
|
|
2300
2761
|
});
|
|
@@ -2365,6 +2826,8 @@ async function runRuntimeSingleParallel(
|
|
|
2365
2826
|
coverageSummary: summary.coverageSummary,
|
|
2366
2827
|
stats: summary.stats,
|
|
2367
2828
|
reports: summary.reports,
|
|
2829
|
+
showLogs: runFlags.showLogs,
|
|
2830
|
+
logSummary: summary.logSummary,
|
|
2368
2831
|
modeSummary: buildSingleModeSummary(
|
|
2369
2832
|
summary.stats,
|
|
2370
2833
|
summary.snapshotSummary,
|
|
@@ -2372,6 +2835,7 @@ async function runRuntimeSingleParallel(
|
|
|
2372
2835
|
),
|
|
2373
2836
|
});
|
|
2374
2837
|
reporter.flush?.();
|
|
2838
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
2375
2839
|
return results.some((result) => result.failed);
|
|
2376
2840
|
}
|
|
2377
2841
|
async function runRuntimeMatrixParallel(
|
|
@@ -2397,6 +2861,7 @@ async function runRuntimeMatrixParallel(
|
|
|
2397
2861
|
runtimeName: reporterSession.runtimeName,
|
|
2398
2862
|
clean: runFlags.clean,
|
|
2399
2863
|
verbose: runFlags.verbose,
|
|
2864
|
+
showLogs: runFlags.showLogs,
|
|
2400
2865
|
snapshotEnabled,
|
|
2401
2866
|
createSnapshots: runFlags.createSnapshots,
|
|
2402
2867
|
});
|
|
@@ -2513,9 +2978,12 @@ async function runRuntimeMatrixParallel(
|
|
|
2513
2978
|
coverageSummary: summary.coverageSummary,
|
|
2514
2979
|
stats: summary.stats,
|
|
2515
2980
|
reports: summary.reports,
|
|
2981
|
+
showLogs: runFlags.showLogs,
|
|
2982
|
+
logSummary: summary.logSummary,
|
|
2516
2983
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2517
2984
|
});
|
|
2518
2985
|
reporter.flush?.();
|
|
2986
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
2519
2987
|
return allResults.some((result) => result.failed);
|
|
2520
2988
|
}
|
|
2521
2989
|
async function runTestSingleParallel(
|
|
@@ -2530,6 +2998,7 @@ async function runTestSingleParallel(
|
|
|
2530
2998
|
fuzzEnabled,
|
|
2531
2999
|
fuzzOverrides,
|
|
2532
3000
|
modeName,
|
|
3001
|
+
onSpecOutcome,
|
|
2533
3002
|
) {
|
|
2534
3003
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
2535
3004
|
if (!files.length && !fuzzEnabled) {
|
|
@@ -2546,6 +3015,7 @@ async function runTestSingleParallel(
|
|
|
2546
3015
|
runtimeName: reporterSession.runtimeName,
|
|
2547
3016
|
clean: runFlags.clean,
|
|
2548
3017
|
verbose: runFlags.verbose,
|
|
3018
|
+
showLogs: runFlags.showLogs,
|
|
2549
3019
|
snapshotEnabled,
|
|
2550
3020
|
createSnapshots: runFlags.createSnapshots,
|
|
2551
3021
|
});
|
|
@@ -2616,6 +3086,7 @@ async function runTestSingleParallel(
|
|
|
2616
3086
|
}
|
|
2617
3087
|
buffered?.reporter.flush?.();
|
|
2618
3088
|
results[index] = result;
|
|
3089
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2619
3090
|
if (buffered && token != null) {
|
|
2620
3091
|
queueDisplay.complete(token, buffered.output());
|
|
2621
3092
|
}
|
|
@@ -2663,6 +3134,8 @@ async function runTestSingleParallel(
|
|
|
2663
3134
|
coverageSummary: summary.coverageSummary,
|
|
2664
3135
|
stats: summary.stats,
|
|
2665
3136
|
reports: summary.reports,
|
|
3137
|
+
showLogs: runFlags.showLogs,
|
|
3138
|
+
logSummary: summary.logSummary,
|
|
2666
3139
|
fuzzSummary,
|
|
2667
3140
|
modeSummary: buildSingleModeSummary(
|
|
2668
3141
|
summary.stats,
|
|
@@ -2671,6 +3144,7 @@ async function runTestSingleParallel(
|
|
|
2671
3144
|
),
|
|
2672
3145
|
});
|
|
2673
3146
|
reporter.flush?.();
|
|
3147
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
2674
3148
|
return failed;
|
|
2675
3149
|
}
|
|
2676
3150
|
async function runTestMatrixParallel(
|
|
@@ -2685,8 +3159,17 @@ async function runTestMatrixParallel(
|
|
|
2685
3159
|
fileSummaryTotal,
|
|
2686
3160
|
fuzzEnabled,
|
|
2687
3161
|
fuzzOverrides,
|
|
3162
|
+
onSpecOutcome,
|
|
2688
3163
|
) {
|
|
2689
3164
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
3165
|
+
if (files.length && configPath) {
|
|
3166
|
+
try {
|
|
3167
|
+
const loaded = loadConfig(configPath, false);
|
|
3168
|
+
warnOnUnknownModeReferences(files, loaded.modes ?? {});
|
|
3169
|
+
} catch {
|
|
3170
|
+
// Best-effort: never fail the run on a scan error.
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
2690
3173
|
if (!files.length) {
|
|
2691
3174
|
if (!fuzzEnabled) {
|
|
2692
3175
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -2710,6 +3193,7 @@ async function runTestMatrixParallel(
|
|
|
2710
3193
|
runtimeName: reporterSession.runtimeName,
|
|
2711
3194
|
clean: runFlags.clean,
|
|
2712
3195
|
verbose: runFlags.verbose,
|
|
3196
|
+
showLogs: runFlags.showLogs,
|
|
2713
3197
|
snapshotEnabled,
|
|
2714
3198
|
createSnapshots: runFlags.createSnapshots,
|
|
2715
3199
|
});
|
|
@@ -2775,6 +3259,7 @@ async function runTestMatrixParallel(
|
|
|
2775
3259
|
}
|
|
2776
3260
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
2777
3261
|
fileResults.push(result);
|
|
3262
|
+
onSpecOutcome?.({ file, mode: modeName, failed: result.failed });
|
|
2778
3263
|
}
|
|
2779
3264
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
2780
3265
|
if (token != null) {
|
|
@@ -2847,10 +3332,13 @@ async function runTestMatrixParallel(
|
|
|
2847
3332
|
coverageSummary: summary.coverageSummary,
|
|
2848
3333
|
stats: summary.stats,
|
|
2849
3334
|
reports: summary.reports,
|
|
3335
|
+
showLogs: runFlags.showLogs,
|
|
3336
|
+
logSummary: summary.logSummary,
|
|
2850
3337
|
fuzzSummary,
|
|
2851
3338
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
2852
3339
|
});
|
|
2853
3340
|
reporter.flush?.();
|
|
3341
|
+
flushModeWarnings(process.argv.includes("--show-warnings"));
|
|
2854
3342
|
return failed;
|
|
2855
3343
|
}
|
|
2856
3344
|
async function runFuzzMatrixResultsParallel(
|
|
@@ -3249,6 +3737,7 @@ function createBuildFailureRunResult(error) {
|
|
|
3249
3737
|
files: [],
|
|
3250
3738
|
},
|
|
3251
3739
|
reports: [report],
|
|
3740
|
+
logSummary: { count: 0, file: null, groups: [], text: "" },
|
|
3252
3741
|
};
|
|
3253
3742
|
}
|
|
3254
3743
|
function formatBuildFailureMessage(error) {
|
|
@@ -4236,6 +4725,9 @@ function aggregateRunResults(results) {
|
|
|
4236
4725
|
let fallbackCoverageUncovered = 0;
|
|
4237
4726
|
const fallbackCoverageFiles = [];
|
|
4238
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: "" };
|
|
4239
4731
|
for (const result of results) {
|
|
4240
4732
|
stats.passedFiles += result.stats.passedFiles;
|
|
4241
4733
|
stats.failedFiles += result.stats.failedFiles;
|
|
@@ -4275,6 +4767,9 @@ function aggregateRunResults(results) {
|
|
|
4275
4767
|
}
|
|
4276
4768
|
}
|
|
4277
4769
|
reports.push(...result.reports);
|
|
4770
|
+
if (result.logSummary && result.logSummary.count >= logSummary.count) {
|
|
4771
|
+
logSummary = result.logSummary;
|
|
4772
|
+
}
|
|
4278
4773
|
}
|
|
4279
4774
|
if (uniqueCoveragePoints.size > 0) {
|
|
4280
4775
|
const byFile = new Map();
|
|
@@ -4320,7 +4815,7 @@ function aggregateRunResults(results) {
|
|
|
4320
4815
|
coverageSummary.percent = coverageSummary.total
|
|
4321
4816
|
? (coverageSummary.covered * 100) / coverageSummary.total
|
|
4322
4817
|
: 100;
|
|
4323
|
-
return { stats, snapshotSummary, coverageSummary, reports };
|
|
4818
|
+
return { stats, snapshotSummary, coverageSummary, reports, logSummary };
|
|
4324
4819
|
}
|
|
4325
4820
|
function printCliError(error) {
|
|
4326
4821
|
const message = error instanceof Error ? error.message : String(error);
|