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/commands/run-core.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
-
import { glob } from "glob";
|
|
4
3
|
import { minimatch } from "minimatch";
|
|
5
4
|
import { Channel, MessageType } from "../wipc.js";
|
|
6
5
|
import {
|
|
@@ -21,6 +20,12 @@ import { PassThrough } from "stream";
|
|
|
21
20
|
import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
22
21
|
import { PersistentWebSessionHost } from "./web-session.js";
|
|
23
22
|
import { build } from "./build-core.js";
|
|
23
|
+
import {
|
|
24
|
+
cacheStorage,
|
|
25
|
+
reportHasFailure,
|
|
26
|
+
sha256OfFile,
|
|
27
|
+
} from "../build-cache.js";
|
|
28
|
+
import { resolveSpecFiles, emitSelectorWarnings } from "../selectors.js";
|
|
24
29
|
import { createReporter as createDefaultReporter } from "../reporters/default.js";
|
|
25
30
|
import { createTapReporter } from "../reporters/tap.js";
|
|
26
31
|
import { persistCrashRecord } from "../crash-store.js";
|
|
@@ -733,14 +738,9 @@ export async function run(
|
|
|
733
738
|
}
|
|
734
739
|
}
|
|
735
740
|
}
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
.filter((p) => p.startsWith("!"))
|
|
740
|
-
.map((p) => p.slice(1));
|
|
741
|
-
const inputFiles = (
|
|
742
|
-
await glob(includePatterns, { ignore: ignorePatterns })
|
|
743
|
-
).sort((a, b) => a.localeCompare(b));
|
|
741
|
+
const { files: inputFiles, warnings: selectorWarnings } =
|
|
742
|
+
await resolveSpecFiles(config.input, selectors);
|
|
743
|
+
emitSelectorWarnings(selectorWarnings);
|
|
744
744
|
const snapshotEnabled = flags.snapshot !== false;
|
|
745
745
|
const createSnapshots = Boolean(flags.createSnapshots);
|
|
746
746
|
const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
|
|
@@ -813,6 +813,7 @@ export async function run(
|
|
|
813
813
|
? await PersistentWebSessionHost.start(false)
|
|
814
814
|
: null;
|
|
815
815
|
const webSession = options.webSession ?? ownedWebSession;
|
|
816
|
+
const cacheCtx = cacheStorage.getStore();
|
|
816
817
|
try {
|
|
817
818
|
for (let i = 0; i < inputFiles.length; i++) {
|
|
818
819
|
const file = inputFiles[i];
|
|
@@ -820,6 +821,72 @@ export async function run(
|
|
|
820
821
|
config.outDir,
|
|
821
822
|
resolveArtifactPath(file, config.input),
|
|
822
823
|
);
|
|
824
|
+
// Tier 2: replay a stored passing report instead of running. build() ran
|
|
825
|
+
// first this session and validated the build is fresh (else it cleared
|
|
826
|
+
// the stored report), so here we only re-check the run-specific inputs.
|
|
827
|
+
if (cacheCtx?.replay) {
|
|
828
|
+
const snapPath = resolveSnapshotPath(
|
|
829
|
+
file,
|
|
830
|
+
config.snapshotDir,
|
|
831
|
+
config.input,
|
|
832
|
+
);
|
|
833
|
+
const snapshotSha = existsSync(snapPath)
|
|
834
|
+
? sha256OfFile(snapPath)
|
|
835
|
+
: null;
|
|
836
|
+
if (
|
|
837
|
+
cacheCtx.cache.canReplay(options.modeName, file, {
|
|
838
|
+
runtimeCmd: runtimeCommand,
|
|
839
|
+
snapshotSha,
|
|
840
|
+
})
|
|
841
|
+
) {
|
|
842
|
+
const cached = cacheCtx.cache.getReport(options.modeName, file);
|
|
843
|
+
if (cached && !reportHasFailure(cached)) {
|
|
844
|
+
const cachedSuites = Array.isArray(cached.suites)
|
|
845
|
+
? cached.suites
|
|
846
|
+
: [];
|
|
847
|
+
const selected = options.suiteSelectors?.length
|
|
848
|
+
? filterSelectedSuites(
|
|
849
|
+
cachedSuites,
|
|
850
|
+
options.suiteSelectors,
|
|
851
|
+
file,
|
|
852
|
+
options.modeName ?? "default",
|
|
853
|
+
)
|
|
854
|
+
: cachedSuites;
|
|
855
|
+
replayCachedReport(reporter, file, selected);
|
|
856
|
+
const cachedSnap = cached.snapshotSummary ?? {
|
|
857
|
+
matched: 0,
|
|
858
|
+
created: 0,
|
|
859
|
+
updated: 0,
|
|
860
|
+
failed: 0,
|
|
861
|
+
};
|
|
862
|
+
snapshotSummary.matched += cachedSnap.matched ?? 0;
|
|
863
|
+
snapshotSummary.created += cachedSnap.created ?? 0;
|
|
864
|
+
snapshotSummary.updated += cachedSnap.updated ?? 0;
|
|
865
|
+
snapshotSummary.failed += cachedSnap.failed ?? 0;
|
|
866
|
+
reports.push({
|
|
867
|
+
file,
|
|
868
|
+
modeName: options.modeName ?? "default",
|
|
869
|
+
suites: selected,
|
|
870
|
+
coverage: cached.coverage ?? {
|
|
871
|
+
total: 0,
|
|
872
|
+
covered: 0,
|
|
873
|
+
uncovered: 0,
|
|
874
|
+
percent: 100,
|
|
875
|
+
points: [],
|
|
876
|
+
},
|
|
877
|
+
runCommand: cached.runCommand ?? "",
|
|
878
|
+
buildCommand:
|
|
879
|
+
options.buildCommandsByFile?.[file] ??
|
|
880
|
+
options.buildCommand ??
|
|
881
|
+
cached.buildCommand ??
|
|
882
|
+
"",
|
|
883
|
+
snapshotSummary: cachedSnap,
|
|
884
|
+
cached: true,
|
|
885
|
+
});
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
823
890
|
if (!existsSync(outFile)) {
|
|
824
891
|
const buildStartedAt = Date.now();
|
|
825
892
|
await build(
|
|
@@ -934,7 +1001,7 @@ export async function run(
|
|
|
934
1001
|
snapshotSummary.created += snapshotStore.created;
|
|
935
1002
|
snapshotSummary.updated += snapshotStore.updated;
|
|
936
1003
|
snapshotSummary.failed += snapshotStore.failed;
|
|
937
|
-
|
|
1004
|
+
const fileReport = {
|
|
938
1005
|
file,
|
|
939
1006
|
modeName: options.modeName ?? "default",
|
|
940
1007
|
suites: selectedSuites,
|
|
@@ -948,7 +1015,29 @@ export async function run(
|
|
|
948
1015
|
updated: snapshotStore.updated,
|
|
949
1016
|
failed: snapshotStore.failed,
|
|
950
1017
|
},
|
|
951
|
-
|
|
1018
|
+
// When the cache is active, mark freshly-run reports `false` (replays
|
|
1019
|
+
// are marked `true`) so the summary can show a cache hit/miss split.
|
|
1020
|
+
// Left undefined when the cache is off so no Cache line is shown.
|
|
1021
|
+
...(cacheCtx?.cache ? { cached: false } : {}),
|
|
1022
|
+
};
|
|
1023
|
+
reports.push(fileReport);
|
|
1024
|
+
// Persist this report so an unchanged future run can replay it (Tier 2).
|
|
1025
|
+
// recordBuild ran during build() this session, so the entry exists.
|
|
1026
|
+
if (cacheCtx?.cache) {
|
|
1027
|
+
const snapPath = resolveSnapshotPath(
|
|
1028
|
+
file,
|
|
1029
|
+
config.snapshotDir,
|
|
1030
|
+
config.input,
|
|
1031
|
+
);
|
|
1032
|
+
const snapshotSha = existsSync(snapPath)
|
|
1033
|
+
? sha256OfFile(snapPath)
|
|
1034
|
+
: null;
|
|
1035
|
+
cacheCtx.cache.recordReport(options.modeName, file, {
|
|
1036
|
+
report: fileReport,
|
|
1037
|
+
snapshotSha,
|
|
1038
|
+
runtimeCmd: runtimeCommand,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
952
1041
|
}
|
|
953
1042
|
} finally {
|
|
954
1043
|
await ownedWebSession?.close();
|
|
@@ -1364,61 +1453,6 @@ function runtimeNameFromCommand(command) {
|
|
|
1364
1453
|
const token = command.trim().split(/\s+/)[0];
|
|
1365
1454
|
return token && token.length ? token : "runtime";
|
|
1366
1455
|
}
|
|
1367
|
-
function resolveInputPatterns(configured, selectors) {
|
|
1368
|
-
const configuredInputs = Array.isArray(configured)
|
|
1369
|
-
? configured
|
|
1370
|
-
: [configured];
|
|
1371
|
-
if (!selectors.length) return configuredInputs;
|
|
1372
|
-
const patterns = new Set();
|
|
1373
|
-
for (const selector of expandSelectors(selectors)) {
|
|
1374
|
-
if (!selector) continue;
|
|
1375
|
-
if (isBareSuiteSelector(selector)) {
|
|
1376
|
-
const base = stripSuiteSuffix(selector);
|
|
1377
|
-
for (const configuredInput of configuredInputs) {
|
|
1378
|
-
patterns.add(
|
|
1379
|
-
path.join(path.dirname(configuredInput), `${base}.spec.ts`),
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
continue;
|
|
1383
|
-
}
|
|
1384
|
-
patterns.add(selector);
|
|
1385
|
-
}
|
|
1386
|
-
return [...patterns];
|
|
1387
|
-
}
|
|
1388
|
-
function expandSelectors(selectors) {
|
|
1389
|
-
const expanded = [];
|
|
1390
|
-
for (const selector of selectors) {
|
|
1391
|
-
if (!selector) continue;
|
|
1392
|
-
if (!shouldSplitSelector(selector)) {
|
|
1393
|
-
expanded.push(selector);
|
|
1394
|
-
continue;
|
|
1395
|
-
}
|
|
1396
|
-
for (const token of selector.split(",")) {
|
|
1397
|
-
const trimmed = token.trim();
|
|
1398
|
-
if (!trimmed.length) continue;
|
|
1399
|
-
expanded.push(trimmed);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
return expanded;
|
|
1403
|
-
}
|
|
1404
|
-
function shouldSplitSelector(selector) {
|
|
1405
|
-
return (
|
|
1406
|
-
selector.includes(",") &&
|
|
1407
|
-
!selector.includes("/") &&
|
|
1408
|
-
!selector.includes("\\") &&
|
|
1409
|
-
!/[*?[\]{}]/.test(selector)
|
|
1410
|
-
);
|
|
1411
|
-
}
|
|
1412
|
-
function isBareSuiteSelector(selector) {
|
|
1413
|
-
return (
|
|
1414
|
-
!selector.includes("/") &&
|
|
1415
|
-
!selector.includes("\\") &&
|
|
1416
|
-
!/[*?[\]{}]/.test(selector)
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
function stripSuiteSuffix(selector) {
|
|
1420
|
-
return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
|
|
1421
|
-
}
|
|
1422
1456
|
function normalizeReport(raw) {
|
|
1423
1457
|
if (Array.isArray(raw)) {
|
|
1424
1458
|
return {
|
|
@@ -2220,6 +2254,22 @@ async function runProcess(
|
|
|
2220
2254
|
});
|
|
2221
2255
|
return synthesized;
|
|
2222
2256
|
}
|
|
2257
|
+
// A spec file with no test suites never calls `run()`, so it emits no
|
|
2258
|
+
// lifecycle frames and exits cleanly. That is an empty test file, not a
|
|
2259
|
+
// crash — mark it skipped instead of surfacing "missing report payload".
|
|
2260
|
+
if (
|
|
2261
|
+
code === 0 &&
|
|
2262
|
+
reportStream.dataFrames === 0 &&
|
|
2263
|
+
!runtimeEvents.sawFileStart &&
|
|
2264
|
+
!runtimeEvents.sawFileEnd &&
|
|
2265
|
+
runtimeEvents.suiteStarts === 0 &&
|
|
2266
|
+
!hasMeaningfulRuntimeOutput(stderrBuffer)
|
|
2267
|
+
) {
|
|
2268
|
+
reporter.onWarning?.({
|
|
2269
|
+
message: `${formatSpecDisplayPath(specFile)} contains no tests; marked as skipped`,
|
|
2270
|
+
});
|
|
2271
|
+
return createEmptyFileSkipReport(specFile, modeName);
|
|
2272
|
+
}
|
|
2223
2273
|
const errorText = "missing report payload from test runtime";
|
|
2224
2274
|
const diagnostics = buildRuntimeReportDiagnostics(
|
|
2225
2275
|
code,
|
|
@@ -2710,6 +2760,24 @@ function buildRuntimeReportDiagnostics(
|
|
|
2710
2760
|
`runtime events: fileStart=${runtimeEvents.sawFileStart ? "yes" : "no"}, fileEnd=${runtimeEvents.sawFileEnd ? "yes" : "no"}, fileVerdict=${runtimeEvents.fileVerdict}, suiteStarts=${runtimeEvents.suiteStarts}, suiteEnds=${runtimeEvents.suiteEnds}, assertionFails=${runtimeEvents.assertionFails}, warnings=${runtimeEvents.warnings}, logs=${runtimeEvents.logs}`,
|
|
2711
2761
|
].join("\n");
|
|
2712
2762
|
}
|
|
2763
|
+
function createEmptyFileSkipReport(specFile, modeName) {
|
|
2764
|
+
// No suites: a file with zero suites contributes no skipped-suite count, just
|
|
2765
|
+
// a skipped file (an empty `suites` array yields a "none" file verdict, which
|
|
2766
|
+
// collectRunStats tallies as a skipped file). The accompanying onWarning tells
|
|
2767
|
+
// the user the file had no tests.
|
|
2768
|
+
return {
|
|
2769
|
+
file: specFile,
|
|
2770
|
+
modeName: modeName ?? "default",
|
|
2771
|
+
suites: [],
|
|
2772
|
+
coverage: {
|
|
2773
|
+
total: 0,
|
|
2774
|
+
covered: 0,
|
|
2775
|
+
uncovered: 0,
|
|
2776
|
+
percent: 100,
|
|
2777
|
+
points: [],
|
|
2778
|
+
},
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2713
2781
|
function createRuntimeFailureReport(
|
|
2714
2782
|
specFile,
|
|
2715
2783
|
modeName,
|
|
@@ -2805,6 +2873,58 @@ function shouldSuppressWasiWarningLine(line) {
|
|
|
2805
2873
|
}
|
|
2806
2874
|
return false;
|
|
2807
2875
|
}
|
|
2876
|
+
// Drive the reporter from a stored (passing) file report so a replayed spec
|
|
2877
|
+
// still scrolls past the live display and is counted, exactly as a fresh run
|
|
2878
|
+
// would render it. Only passing/skipped reports are replayed, so there are no
|
|
2879
|
+
// assertion failures to re-emit.
|
|
2880
|
+
function replayCachedReport(reporter, file, suites) {
|
|
2881
|
+
reporter.onFileStart?.({
|
|
2882
|
+
file,
|
|
2883
|
+
depth: 0,
|
|
2884
|
+
suiteKind: "file",
|
|
2885
|
+
description: file,
|
|
2886
|
+
});
|
|
2887
|
+
let verdict = "none";
|
|
2888
|
+
for (const suite of suites) {
|
|
2889
|
+
verdict = mergeReplayVerdict(
|
|
2890
|
+
verdict,
|
|
2891
|
+
emitReplaySuite(reporter, file, suite),
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
reporter.onFileEnd?.({
|
|
2895
|
+
file,
|
|
2896
|
+
depth: 0,
|
|
2897
|
+
suiteKind: "file",
|
|
2898
|
+
description: file,
|
|
2899
|
+
verdict,
|
|
2900
|
+
cached: true,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
function emitReplaySuite(reporter, file, suite) {
|
|
2904
|
+
const depth = Number(suite?.depth ?? 0);
|
|
2905
|
+
const kind = String(suite?.kind ?? "");
|
|
2906
|
+
const description = String(suite?.description ?? "");
|
|
2907
|
+
reporter.onSuiteStart?.({ file, depth, suiteKind: kind, description });
|
|
2908
|
+
let verdict = String(suite?.verdict ?? "none");
|
|
2909
|
+
const subs = Array.isArray(suite?.suites) ? suite.suites : [];
|
|
2910
|
+
for (const sub of subs) {
|
|
2911
|
+
verdict = mergeReplayVerdict(verdict, emitReplaySuite(reporter, file, sub));
|
|
2912
|
+
}
|
|
2913
|
+
reporter.onSuiteEnd?.({
|
|
2914
|
+
file,
|
|
2915
|
+
depth,
|
|
2916
|
+
suiteKind: kind,
|
|
2917
|
+
description,
|
|
2918
|
+
verdict: String(suite?.verdict ?? verdict),
|
|
2919
|
+
});
|
|
2920
|
+
return verdict;
|
|
2921
|
+
}
|
|
2922
|
+
function mergeReplayVerdict(a, b) {
|
|
2923
|
+
if (a === "fail" || b === "fail") return "fail";
|
|
2924
|
+
if (a === "ok" || b === "ok") return "ok";
|
|
2925
|
+
if (a === "skip" || b === "skip") return "skip";
|
|
2926
|
+
return "none";
|
|
2927
|
+
}
|
|
2808
2928
|
function collectRunStats(reports) {
|
|
2809
2929
|
const stats = {
|
|
2810
2930
|
passedFiles: 0,
|
package/bin/commands/test.js
CHANGED
|
@@ -29,6 +29,8 @@ export async function executeTestCommand(
|
|
|
29
29
|
browser: deps.resolveBrowserOverride(rawArgs, "test"),
|
|
30
30
|
reporterPath: deps.resolveReporterOverride(rawArgs, "test"),
|
|
31
31
|
watch: flags.includes("--watch") || flags.includes("-w"),
|
|
32
|
+
cache: flags.includes("--cache"),
|
|
33
|
+
noCache: flags.includes("--no-cache"),
|
|
32
34
|
};
|
|
33
35
|
const fuzzEnabled = flags.includes("--fuzz");
|
|
34
36
|
const fuzzOverrides = deps.resolveFuzzOverrides(rawArgs, "test");
|