as-test 1.5.1 → 1.6.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 +32 -0
- package/as-test.config.schema.json +40 -0
- package/bin/build-cache.js +278 -0
- package/bin/commands/build-core.js +84 -78
- package/bin/commands/clean-core.js +4 -0
- package/bin/commands/run-core.js +186 -66
- package/bin/commands/test.js +2 -0
- package/bin/index.js +257 -92
- package/bin/reporters/default.js +56 -5
- package/bin/selectors.js +208 -0
- package/bin/types.js +18 -0
- package/bin/util.js +94 -0
- package/package.json +2 -2
- package/transform/lib/index.js +2 -2
package/bin/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
flushModeWarnings,
|
|
7
7
|
formatInvocation as formatBuildInvocation,
|
|
8
8
|
getBuildInvocationPreview,
|
|
9
|
+
getBuildReuseInfo,
|
|
9
10
|
warnOnUnknownModeReferences,
|
|
10
11
|
} from "./commands/build.js";
|
|
11
12
|
import { createRunReporter, resetCollectedLogs, run } from "./commands/run.js";
|
|
@@ -41,6 +42,9 @@ import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
|
41
42
|
import { PersistentWebSessionHost } from "./commands/web-session.js";
|
|
42
43
|
import { buildRecorderStorage } from "./commands/build-core.js";
|
|
43
44
|
import { DependencyGraph } from "./dependency-graph.js";
|
|
45
|
+
import { BuildCache, cacheStorage, resolveCacheDir } from "./build-cache.js";
|
|
46
|
+
import { resolveCacheSettings } from "./util.js";
|
|
47
|
+
import { resolveSpecFiles, emitSelectorWarnings } from "./selectors.js";
|
|
44
48
|
const _args = process.argv.slice(2);
|
|
45
49
|
const flags = [];
|
|
46
50
|
const args = [];
|
|
@@ -476,6 +480,12 @@ function printCommandHelp(command) {
|
|
|
476
480
|
process.stdout.write(
|
|
477
481
|
" --watch, -w Re-run on source or spec changes\n",
|
|
478
482
|
);
|
|
483
|
+
process.stdout.write(
|
|
484
|
+
" --cache Skip recompiling/rerunning specs unchanged since the last run\n",
|
|
485
|
+
);
|
|
486
|
+
process.stdout.write(
|
|
487
|
+
" --no-cache Ignore the cache for this run (overrides config)\n",
|
|
488
|
+
);
|
|
479
489
|
process.stdout.write(" --help, -h Show this help\n");
|
|
480
490
|
return;
|
|
481
491
|
}
|
|
@@ -1238,31 +1248,57 @@ class ParallelQueueDisplay {
|
|
|
1238
1248
|
this.showStartLines = showStartLines;
|
|
1239
1249
|
this.active = new Map();
|
|
1240
1250
|
this.renderedLines = 0;
|
|
1251
|
+
// Files complete out of order under --parallel (a cache replay finishes
|
|
1252
|
+
// instantly while a fresh build is still running). To keep output in the
|
|
1253
|
+
// order specs were resolved, each token gets a start sequence (start() is
|
|
1254
|
+
// called in resolved/index order) and completed outputs are emitted only as
|
|
1255
|
+
// the contiguous prefix of that sequence fills in.
|
|
1256
|
+
this.seqByToken = new Map();
|
|
1257
|
+
this.nextSeq = 0;
|
|
1258
|
+
this.nextFlushSeq = 0;
|
|
1259
|
+
this.pending = new Map();
|
|
1241
1260
|
this.enabled = showStartLines && canRewriteParallelQueue();
|
|
1242
1261
|
}
|
|
1243
1262
|
start(file) {
|
|
1244
1263
|
const token = Symbol(file);
|
|
1245
|
-
|
|
1246
|
-
const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
|
|
1264
|
+
this.seqByToken.set(token, this.nextSeq++);
|
|
1247
1265
|
if (!this.enabled) return token;
|
|
1266
|
+
const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
|
|
1248
1267
|
this.clear();
|
|
1249
1268
|
this.active.set(token, line);
|
|
1250
1269
|
this.render();
|
|
1251
1270
|
return token;
|
|
1252
1271
|
}
|
|
1253
1272
|
complete(token, output) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
this.clear();
|
|
1259
|
-
process.stdout.write(output);
|
|
1273
|
+
const seq = this.seqByToken.get(token) ?? this.nextFlushSeq;
|
|
1274
|
+
this.seqByToken.delete(token);
|
|
1260
1275
|
this.active.delete(token);
|
|
1261
|
-
this.
|
|
1276
|
+
this.pending.set(seq, output);
|
|
1277
|
+
this.flushOrdered();
|
|
1278
|
+
}
|
|
1279
|
+
// Emit the contiguous run of completed outputs starting at nextFlushSeq, so
|
|
1280
|
+
// results print in resolved order regardless of completion order.
|
|
1281
|
+
flushOrdered() {
|
|
1282
|
+
if (!this.pending.has(this.nextFlushSeq)) return;
|
|
1283
|
+
if (this.enabled) this.clear();
|
|
1284
|
+
while (this.pending.has(this.nextFlushSeq)) {
|
|
1285
|
+
process.stdout.write(this.pending.get(this.nextFlushSeq));
|
|
1286
|
+
this.pending.delete(this.nextFlushSeq);
|
|
1287
|
+
this.nextFlushSeq++;
|
|
1288
|
+
}
|
|
1289
|
+
if (this.enabled) this.render();
|
|
1262
1290
|
}
|
|
1263
1291
|
flush() {
|
|
1264
|
-
|
|
1265
|
-
|
|
1292
|
+
// Drain anything still buffered (e.g. a gap left by an errored spec) in
|
|
1293
|
+
// sequence order, then clear the live block.
|
|
1294
|
+
if (this.pending.size) {
|
|
1295
|
+
if (this.enabled) this.clear();
|
|
1296
|
+
for (const seq of [...this.pending.keys()].sort((a, b) => a - b)) {
|
|
1297
|
+
process.stdout.write(this.pending.get(seq));
|
|
1298
|
+
}
|
|
1299
|
+
this.pending.clear();
|
|
1300
|
+
}
|
|
1301
|
+
if (this.enabled) this.clear();
|
|
1266
1302
|
}
|
|
1267
1303
|
clear() {
|
|
1268
1304
|
if (!this.renderedLines) return;
|
|
@@ -1328,6 +1364,29 @@ async function buildFileForMode(args) {
|
|
|
1328
1364
|
// recorder so the dependency graph still gets populated under --parallel.
|
|
1329
1365
|
const recorder = buildRecorderStorage.getStore();
|
|
1330
1366
|
if (args.buildPool) {
|
|
1367
|
+
// The non-pool branch delegates to build(), which owns the cache logic.
|
|
1368
|
+
// The pool builds in child processes, so handle the cache here: skip when
|
|
1369
|
+
// fresh, else collect the worker's reported reads and record them.
|
|
1370
|
+
const cacheCtx = cacheStorage.getStore();
|
|
1371
|
+
const reuse = cacheCtx
|
|
1372
|
+
? await getBuildReuseInfo(
|
|
1373
|
+
args.configPath,
|
|
1374
|
+
args.file,
|
|
1375
|
+
args.modeName,
|
|
1376
|
+
args.buildFeatureToggles,
|
|
1377
|
+
)
|
|
1378
|
+
: null;
|
|
1379
|
+
if (
|
|
1380
|
+
cacheCtx &&
|
|
1381
|
+
reuse &&
|
|
1382
|
+
cacheCtx.cache.isBuildFresh(args.modeName, args.file, {
|
|
1383
|
+
signature: reuse.signature,
|
|
1384
|
+
coverageEnabled: reuse.coverageEnabled,
|
|
1385
|
+
})
|
|
1386
|
+
) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const reads = cacheCtx && reuse ? new Set() : null;
|
|
1331
1390
|
const buildInvocation = await getBuildInvocationPreview(
|
|
1332
1391
|
args.configPath,
|
|
1333
1392
|
args.file,
|
|
@@ -1340,12 +1399,24 @@ async function buildFileForMode(args) {
|
|
|
1340
1399
|
modeName: args.modeName,
|
|
1341
1400
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1342
1401
|
featureToggles: args.buildFeatureToggles,
|
|
1343
|
-
onReads:
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1402
|
+
onReads:
|
|
1403
|
+
recorder || reads
|
|
1404
|
+
? (entries) => {
|
|
1405
|
+
for (const r of entries) {
|
|
1406
|
+
recorder?.record(r.mode, r.spec, r.file);
|
|
1407
|
+
reads?.add(r.file);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
: undefined,
|
|
1348
1411
|
});
|
|
1412
|
+
if (cacheCtx && reuse && reads) {
|
|
1413
|
+
cacheCtx.cache.recordBuild(args.modeName, args.file, {
|
|
1414
|
+
signature: reuse.signature,
|
|
1415
|
+
outFile: reuse.outFile,
|
|
1416
|
+
deps: reads,
|
|
1417
|
+
coverageEnabled: reuse.coverageEnabled,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1349
1420
|
} else {
|
|
1350
1421
|
await build(
|
|
1351
1422
|
args.configPath,
|
|
@@ -1948,17 +2019,70 @@ async function runTestModes(
|
|
|
1948
2019
|
);
|
|
1949
2020
|
return;
|
|
1950
2021
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2022
|
+
// Opt-in incremental cache for the whole non-watch run (watch keeps its own
|
|
2023
|
+
// in-memory dependency graph). Fuzzing is non-deterministic, so never cache.
|
|
2024
|
+
const baseConfig = loadConfig(
|
|
2025
|
+
configPath ?? path.join(process.cwd(), "./as-test.config.json"),
|
|
2026
|
+
);
|
|
2027
|
+
const { mode: cacheMode, maxTimeMs } = resolveCacheSettings(
|
|
2028
|
+
baseConfig.cache,
|
|
2029
|
+
{
|
|
2030
|
+
cache: runFlags.cache,
|
|
2031
|
+
noCache: runFlags.noCache,
|
|
2032
|
+
},
|
|
1961
2033
|
);
|
|
2034
|
+
const cacheEnabled = cacheMode !== "off" && !fuzzEnabled;
|
|
2035
|
+
if (!cacheEnabled) {
|
|
2036
|
+
const failed = await runTestModesCore(
|
|
2037
|
+
runFlags,
|
|
2038
|
+
configPath,
|
|
2039
|
+
selectors,
|
|
2040
|
+
suiteSelectors,
|
|
2041
|
+
fuzzerSelectors,
|
|
2042
|
+
modes,
|
|
2043
|
+
buildFeatureToggles,
|
|
2044
|
+
fuzzEnabled,
|
|
2045
|
+
fuzzOverrides,
|
|
2046
|
+
);
|
|
2047
|
+
process.exit(failed ? 1 : 0);
|
|
2048
|
+
}
|
|
2049
|
+
const cacheDir = resolveCacheDir(baseConfig.outDir);
|
|
2050
|
+
const cache = BuildCache.load(cacheDir, getCliVersion(), { maxTimeMs });
|
|
2051
|
+
// Replay (Tier 2) is unsafe while writing snapshots — those mutate .snap, so
|
|
2052
|
+
// a replayed snapshot summary would be wrong. Build-skip still applies.
|
|
2053
|
+
const replay =
|
|
2054
|
+
cacheMode === "full" &&
|
|
2055
|
+
!runFlags.createSnapshots &&
|
|
2056
|
+
!runFlags.overwriteSnapshots;
|
|
2057
|
+
// Only prune entries on a full run: a selector-scoped run (`ast test foo`)
|
|
2058
|
+
// resolves a subset, so pruning to it would wipe every other spec's entry.
|
|
2059
|
+
let liveKeys = null;
|
|
2060
|
+
if (!selectors.length) {
|
|
2061
|
+
const files = await resolveSelectedFiles(configPath, [], false);
|
|
2062
|
+
liveKeys = new Set();
|
|
2063
|
+
for (const modeName of modes) {
|
|
2064
|
+
for (const file of files) liveKeys.add(cache.keyFor(modeName, file));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
let failed = false;
|
|
2068
|
+
try {
|
|
2069
|
+
failed = await cacheStorage.run({ cache, replay }, () =>
|
|
2070
|
+
runTestModesCore(
|
|
2071
|
+
runFlags,
|
|
2072
|
+
configPath,
|
|
2073
|
+
selectors,
|
|
2074
|
+
suiteSelectors,
|
|
2075
|
+
fuzzerSelectors,
|
|
2076
|
+
modes,
|
|
2077
|
+
buildFeatureToggles,
|
|
2078
|
+
fuzzEnabled,
|
|
2079
|
+
fuzzOverrides,
|
|
2080
|
+
),
|
|
2081
|
+
);
|
|
2082
|
+
} finally {
|
|
2083
|
+
if (liveKeys) cache.prune(liveKeys);
|
|
2084
|
+
cache.save();
|
|
2085
|
+
}
|
|
1962
2086
|
process.exit(failed ? 1 : 0);
|
|
1963
2087
|
}
|
|
1964
2088
|
async function runWatchLoop(
|
|
@@ -1976,6 +2100,43 @@ async function runWatchLoop(
|
|
|
1976
2100
|
configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1977
2101
|
const absConfigPath = path.resolve(resolvedConfigPath);
|
|
1978
2102
|
let config = loadConfig(resolvedConfigPath, false);
|
|
2103
|
+
// Persistent incremental cache, honored under --watch too (the dependency
|
|
2104
|
+
// graph above only governs which specs re-run; the cache skips recompiling /
|
|
2105
|
+
// replays unchanged ones, which makes the initial watch run and "run all"
|
|
2106
|
+
// fast). Resolved from flags+config; toggleable live with `c`.
|
|
2107
|
+
const initialCache = resolveCacheSettings(config.cache, {
|
|
2108
|
+
cache: runFlags.cache,
|
|
2109
|
+
noCache: runFlags.noCache,
|
|
2110
|
+
});
|
|
2111
|
+
let cacheMode = fuzzEnabled ? "off" : initialCache.mode;
|
|
2112
|
+
const cacheMaxTimeMs = initialCache.maxTimeMs;
|
|
2113
|
+
// The mode `c` toggles back on to (the configured mode, or "full" if the
|
|
2114
|
+
// cache started off).
|
|
2115
|
+
const cacheToggleMode =
|
|
2116
|
+
initialCache.mode === "off" ? "full" : initialCache.mode;
|
|
2117
|
+
// Wraps a run in the cache context (fresh per run so `now`/maxTime are
|
|
2118
|
+
// current), unless the cache is off. Entry-pruning is intentionally skipped
|
|
2119
|
+
// under watch — scoped re-runs resolve only a subset of specs, so pruning to
|
|
2120
|
+
// them would wipe the rest of the cache.
|
|
2121
|
+
async function withCache(fn) {
|
|
2122
|
+
if (cacheMode === "off") {
|
|
2123
|
+
await fn();
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const cacheDir = resolveCacheDir(config.outDir);
|
|
2127
|
+
const cache = BuildCache.load(cacheDir, getCliVersion(), {
|
|
2128
|
+
maxTimeMs: cacheMaxTimeMs,
|
|
2129
|
+
});
|
|
2130
|
+
const replay =
|
|
2131
|
+
cacheMode === "full" &&
|
|
2132
|
+
!runFlags.createSnapshots &&
|
|
2133
|
+
!runFlags.overwriteSnapshots;
|
|
2134
|
+
try {
|
|
2135
|
+
await cacheStorage.run({ cache, replay }, fn);
|
|
2136
|
+
} finally {
|
|
2137
|
+
cache.save();
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
1979
2140
|
// Respect the user's parallelism flags. Worker-pool builds forward their
|
|
1980
2141
|
// file-read records back through IPC (see BuildWorkerPool / build-worker
|
|
1981
2142
|
// and buildFileForMode), so the dependency graph stays correct under
|
|
@@ -2025,12 +2186,14 @@ async function runWatchLoop(
|
|
|
2025
2186
|
: "";
|
|
2026
2187
|
return chalk.dim(
|
|
2027
2188
|
`Auto-run paused${pending}. ` +
|
|
2028
|
-
chalk.bold("w") +
|
|
2029
|
-
" = resume, " +
|
|
2030
|
-
chalk.bold("a") +
|
|
2031
|
-
" = re-run all, " +
|
|
2032
2189
|
chalk.bold("space") +
|
|
2033
2190
|
" = retry failing, " +
|
|
2191
|
+
chalk.bold("a") +
|
|
2192
|
+
" = re-run all, " +
|
|
2193
|
+
chalk.bold("w") +
|
|
2194
|
+
" = resume, " +
|
|
2195
|
+
chalk.bold("c") +
|
|
2196
|
+
` = cache (${cacheMode === "off" ? "off" : "on"}), ` +
|
|
2034
2197
|
chalk.bold("ctrl+c") +
|
|
2035
2198
|
" = stop.\n",
|
|
2036
2199
|
);
|
|
@@ -2043,10 +2206,23 @@ async function runWatchLoop(
|
|
|
2043
2206
|
" = re-run all, " +
|
|
2044
2207
|
chalk.bold("w") +
|
|
2045
2208
|
" = pause, " +
|
|
2209
|
+
chalk.bold("c") +
|
|
2210
|
+
` = cache (${cacheMode === "off" ? "off" : "on"}), ` +
|
|
2046
2211
|
chalk.bold("ctrl+c") +
|
|
2047
2212
|
" = stop.\n",
|
|
2048
2213
|
);
|
|
2049
2214
|
}
|
|
2215
|
+
// Rewrites the footer line in place (the cursor sits one line below it after
|
|
2216
|
+
// every doRun / prior rewrite), so toggling w/c updates the hint without
|
|
2217
|
+
// accumulating new lines in scrollback. Falls back to a plain write when not
|
|
2218
|
+
// on a rewritable TTY.
|
|
2219
|
+
function rewriteFooter() {
|
|
2220
|
+
if (process.stdout.isTTY) {
|
|
2221
|
+
process.stdout.write("\x1b[1A\r\x1b[2K" + watchFooter());
|
|
2222
|
+
} else {
|
|
2223
|
+
process.stdout.write(watchFooter());
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2050
2226
|
function writeWatchHeader(headline, detail) {
|
|
2051
2227
|
// Preserve scrollback — never `console.clear()`. A blank line plus a
|
|
2052
2228
|
// dim rule visually delimits each iteration so prior output stays
|
|
@@ -2184,21 +2360,23 @@ async function runWatchLoop(
|
|
|
2184
2360
|
},
|
|
2185
2361
|
};
|
|
2186
2362
|
try {
|
|
2187
|
-
await
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2363
|
+
await withCache(() =>
|
|
2364
|
+
buildRecorderStorage.run(recorder, async () => {
|
|
2365
|
+
await runTestModesCore(
|
|
2366
|
+
watchRunFlags,
|
|
2367
|
+
configPath,
|
|
2368
|
+
runSelectors,
|
|
2369
|
+
suiteSelectors,
|
|
2370
|
+
fuzzerSelectors,
|
|
2371
|
+
modes,
|
|
2372
|
+
buildFeatureToggles,
|
|
2373
|
+
fuzzEnabled,
|
|
2374
|
+
fuzzOverrides,
|
|
2375
|
+
(outcome) =>
|
|
2376
|
+
recordSpecOutcome(outcome.file, outcome.mode, outcome.failed),
|
|
2377
|
+
);
|
|
2378
|
+
}),
|
|
2379
|
+
);
|
|
2202
2380
|
} catch (error) {
|
|
2203
2381
|
const message = error instanceof Error ? error.message : String(error);
|
|
2204
2382
|
process.stderr.write(chalk.red("Error: ") + message + "\n");
|
|
@@ -2351,31 +2529,14 @@ async function runWatchLoop(
|
|
|
2351
2529
|
}
|
|
2352
2530
|
if (isRunning) break;
|
|
2353
2531
|
if (byte === 0x77 || byte === 0x57) {
|
|
2354
|
-
// `w` — toggle auto-run / manual mode.
|
|
2532
|
+
// `w` — toggle auto-run / manual mode. Resuming with edits pending
|
|
2533
|
+
// kicks off a run (which prints its own header); otherwise just
|
|
2534
|
+
// refresh the footer in place.
|
|
2355
2535
|
autoRun = !autoRun;
|
|
2356
|
-
if (autoRun) {
|
|
2357
|
-
|
|
2358
|
-
if (hadPending) {
|
|
2359
|
-
process.stdout.write(
|
|
2360
|
-
"\n" +
|
|
2361
|
-
chalk.dim(
|
|
2362
|
-
"Auto-run resumed — re-running all (files changed while paused).\n",
|
|
2363
|
-
),
|
|
2364
|
-
);
|
|
2365
|
-
scheduleManualRerun("manual-runall");
|
|
2366
|
-
} else {
|
|
2367
|
-
process.stdout.write(
|
|
2368
|
-
"\n" + chalk.dim("Auto-run resumed.\n") + watchFooter(),
|
|
2369
|
-
);
|
|
2370
|
-
}
|
|
2536
|
+
if (autoRun && changedWhilePaused.size > 0) {
|
|
2537
|
+
scheduleManualRerun("manual-runall");
|
|
2371
2538
|
} else {
|
|
2372
|
-
|
|
2373
|
-
"\n" +
|
|
2374
|
-
chalk.dim(
|
|
2375
|
-
"Auto-run paused — edits won't re-run. Press w to resume, or a / space to run now.\n",
|
|
2376
|
-
) +
|
|
2377
|
-
watchFooter(),
|
|
2378
|
-
);
|
|
2539
|
+
rewriteFooter();
|
|
2379
2540
|
}
|
|
2380
2541
|
break;
|
|
2381
2542
|
}
|
|
@@ -2387,6 +2548,15 @@ async function runWatchLoop(
|
|
|
2387
2548
|
scheduleManualRerun("manual-runall");
|
|
2388
2549
|
break;
|
|
2389
2550
|
}
|
|
2551
|
+
if (byte === 0x63 || byte === 0x43) {
|
|
2552
|
+
// `c` — toggle the incremental cache for subsequent runs. No-op
|
|
2553
|
+
// while fuzzing (the cache is unsafe there). Updates the footer in
|
|
2554
|
+
// place to reflect the new state.
|
|
2555
|
+
if (fuzzEnabled) break;
|
|
2556
|
+
cacheMode = cacheMode === "off" ? cacheToggleMode : "off";
|
|
2557
|
+
rewriteFooter();
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2390
2560
|
}
|
|
2391
2561
|
});
|
|
2392
2562
|
} catch {
|
|
@@ -3574,6 +3744,22 @@ function formatMatrixFileResultLine(
|
|
|
3574
3744
|
showPerModeTimes,
|
|
3575
3745
|
) {
|
|
3576
3746
|
const verdict = resolveMatrixVerdict(results);
|
|
3747
|
+
// A file whose every mode was replayed from cache is de-emphasized: dim-grey
|
|
3748
|
+
// badge + filename, and "(cache)" in place of the timing.
|
|
3749
|
+
const cached =
|
|
3750
|
+
results.length > 0 &&
|
|
3751
|
+
results.every(
|
|
3752
|
+
(r) => r.reports.length > 0 && r.reports.every((rep) => rep.cached),
|
|
3753
|
+
);
|
|
3754
|
+
if (cached) {
|
|
3755
|
+
const badge =
|
|
3756
|
+
verdict == "fail"
|
|
3757
|
+
? chalk.bgRed.white(" FAIL ")
|
|
3758
|
+
: verdict == "ok"
|
|
3759
|
+
? chalk.bgGreenBright.white(" PASS ")
|
|
3760
|
+
: chalk.bgBlackBright.white(" SKIP ");
|
|
3761
|
+
return `${badge} ${chalk.dim(file)} ${chalk.dim("(cache)")}`;
|
|
3762
|
+
}
|
|
3577
3763
|
const badge =
|
|
3578
3764
|
verdict == "fail"
|
|
3579
3765
|
? chalk.bgRed.white(" FAIL ")
|
|
@@ -3855,9 +4041,9 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
|
3855
4041
|
const resolvedConfigPath =
|
|
3856
4042
|
configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
3857
4043
|
const config = loadConfig(resolvedConfigPath, warn);
|
|
3858
|
-
const
|
|
3859
|
-
|
|
3860
|
-
const specs =
|
|
4044
|
+
const { files, warnings } = await resolveSpecFiles(config.input, selectors);
|
|
4045
|
+
if (warn) emitSelectorWarnings(warnings);
|
|
4046
|
+
const specs = files.filter((file) => file.endsWith(".spec.ts"));
|
|
3861
4047
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
3862
4048
|
}
|
|
3863
4049
|
async function resolveSelectedFuzzFiles(
|
|
@@ -3996,27 +4182,6 @@ function levenshteinDistance(left, right) {
|
|
|
3996
4182
|
}
|
|
3997
4183
|
return matrix[left.length][right.length];
|
|
3998
4184
|
}
|
|
3999
|
-
function resolveInputPatterns(configured, selectors) {
|
|
4000
|
-
const configuredInputs = Array.isArray(configured)
|
|
4001
|
-
? configured
|
|
4002
|
-
: [configured];
|
|
4003
|
-
if (!selectors.length) return configuredInputs;
|
|
4004
|
-
const patterns = new Set();
|
|
4005
|
-
for (const selector of expandSelectors(selectors)) {
|
|
4006
|
-
if (!selector) continue;
|
|
4007
|
-
if (isBareSuiteSelector(selector)) {
|
|
4008
|
-
const base = stripSuiteSuffix(selector);
|
|
4009
|
-
for (const configuredInput of configuredInputs) {
|
|
4010
|
-
patterns.add(
|
|
4011
|
-
path.join(path.dirname(configuredInput), `${base}.spec.ts`),
|
|
4012
|
-
);
|
|
4013
|
-
}
|
|
4014
|
-
continue;
|
|
4015
|
-
}
|
|
4016
|
-
patterns.add(selector);
|
|
4017
|
-
}
|
|
4018
|
-
return [...patterns];
|
|
4019
|
-
}
|
|
4020
4185
|
function resolveFuzzPatterns(configured, selectors) {
|
|
4021
4186
|
const configuredInputs = Array.isArray(configured)
|
|
4022
4187
|
? configured
|
package/bin/reporters/default.js
CHANGED
|
@@ -116,14 +116,28 @@ class DefaultReporter {
|
|
|
116
116
|
renderFileResult(event) {
|
|
117
117
|
const verdict = event.verdict ?? "none";
|
|
118
118
|
const time = event.time ? ` ${chalk.dim(event.time)}` : "";
|
|
119
|
-
const
|
|
119
|
+
const name = formatSpecDisplayPath(event.file);
|
|
120
|
+
// A replayed (cached) result is de-emphasized: badge, filename, and tag are
|
|
121
|
+
// all dimmed so freshly-run specs stand out from unchanged ones.
|
|
122
|
+
if (event.cached) {
|
|
123
|
+
// Replayed-from-cache: keep the coloured verdict badge (white text) so it
|
|
124
|
+
// stays scannable, but dim the filename and show "(cache)" in place of the
|
|
125
|
+
// timing so freshly-run specs still stand out.
|
|
126
|
+
const badge =
|
|
127
|
+
verdict == "fail"
|
|
128
|
+
? chalk.bgRed.white(" FAIL ")
|
|
129
|
+
: verdict == "ok"
|
|
130
|
+
? chalk.bgGreenBright.white(" PASS ")
|
|
131
|
+
: chalk.bgBlackBright.white(" SKIP ");
|
|
132
|
+
return `${badge} ${chalk.dim(name)} ${chalk.dim("(cache)")}`;
|
|
133
|
+
}
|
|
120
134
|
if (verdict == "fail")
|
|
121
|
-
return `${chalk.bgRed.white(" FAIL ")} ${
|
|
135
|
+
return `${chalk.bgRed.white(" FAIL ")} ${name}${time}`;
|
|
122
136
|
if (this.fileHasWarning)
|
|
123
|
-
return `${chalk.bgYellow.black(" WARN ")} ${
|
|
137
|
+
return `${chalk.bgYellow.black(" WARN ")} ${name}${time}`;
|
|
124
138
|
if (verdict == "ok")
|
|
125
|
-
return `${chalk.bgGreenBright.black(" PASS ")} ${
|
|
126
|
-
return `${chalk.bgBlackBright.white(" SKIP ")} ${
|
|
139
|
+
return `${chalk.bgGreenBright.black(" PASS ")} ${name}${time}`;
|
|
140
|
+
return `${chalk.bgBlackBright.white(" SKIP ")} ${name}${time}`;
|
|
127
141
|
}
|
|
128
142
|
onRunStart(event) {
|
|
129
143
|
this.verboseMode = Boolean(event.verbose);
|
|
@@ -805,8 +819,18 @@ function renderTotals(stats, event) {
|
|
|
805
819
|
skipped: stats.skippedTests,
|
|
806
820
|
total: stats.failedTests + stats.passedTests + stats.skippedTests,
|
|
807
821
|
};
|
|
822
|
+
const cacheSummary = computeCacheSummary(event.reports);
|
|
808
823
|
const layout = createSummaryLayout([
|
|
809
824
|
event.fuzzSummary,
|
|
825
|
+
// "cached" and "failed" are the same length, so the cache summary aligns in
|
|
826
|
+
// the shared first column.
|
|
827
|
+
cacheSummary
|
|
828
|
+
? {
|
|
829
|
+
failed: cacheSummary.cached,
|
|
830
|
+
skipped: cacheSummary.skipped,
|
|
831
|
+
total: cacheSummary.total,
|
|
832
|
+
}
|
|
833
|
+
: undefined,
|
|
810
834
|
filesSummary,
|
|
811
835
|
suitesSummary,
|
|
812
836
|
testsSummary,
|
|
@@ -821,6 +845,9 @@ function renderTotals(stats, event) {
|
|
|
821
845
|
if (event.modeSummary) {
|
|
822
846
|
renderModeSummary(event.modeSummary, layout);
|
|
823
847
|
}
|
|
848
|
+
if (cacheSummary) {
|
|
849
|
+
renderCacheSummary(cacheSummary, layout);
|
|
850
|
+
}
|
|
824
851
|
process.stdout.write(
|
|
825
852
|
chalk.bold("Time:".padEnd(9)) +
|
|
826
853
|
formatTime(stats.time) +
|
|
@@ -828,6 +855,30 @@ function renderTotals(stats, event) {
|
|
|
828
855
|
"\n",
|
|
829
856
|
);
|
|
830
857
|
}
|
|
858
|
+
// When the cache is active, every report carries a `cached` flag (true =
|
|
859
|
+
// replayed from cache, false = freshly run). Returns the hit/miss split, or
|
|
860
|
+
// undefined when the cache is off (no report sets the flag) so no line shows.
|
|
861
|
+
function computeCacheSummary(reports) {
|
|
862
|
+
const flagged = reports.filter((r) => typeof r?.cached === "boolean");
|
|
863
|
+
if (!flagged.length) return undefined;
|
|
864
|
+
const cached = flagged.filter((r) => r.cached).length;
|
|
865
|
+
return { cached, skipped: flagged.length - cached, total: flagged.length };
|
|
866
|
+
}
|
|
867
|
+
// Renders the "Cache:" line in the shared three-column layout (cached / skipped
|
|
868
|
+
// / total) so it lines up with Files/Suites/Tests/Modes.
|
|
869
|
+
function renderCacheSummary(summary, layout) {
|
|
870
|
+
const cachedText = `${summary.cached} cached`;
|
|
871
|
+
const skippedText = `${summary.skipped} skipped`;
|
|
872
|
+
const totalText = `${summary.total} total`;
|
|
873
|
+
process.stdout.write(chalk.bold("Cache:".padEnd(9)));
|
|
874
|
+
process.stdout.write(
|
|
875
|
+
chalk.bold.greenBright(cachedText.padStart(layout.failedWidth)),
|
|
876
|
+
);
|
|
877
|
+
process.stdout.write(", ");
|
|
878
|
+
process.stdout.write(chalk.gray(skippedText.padStart(layout.skippedWidth)));
|
|
879
|
+
process.stdout.write(", ");
|
|
880
|
+
process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
|
|
881
|
+
}
|
|
831
882
|
function renderModeSummary(summary, layout) {
|
|
832
883
|
renderSummaryLine("Modes:", summary, layout);
|
|
833
884
|
}
|