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.
@@ -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 inputPatterns = resolveInputPatterns(config.input, selectors);
737
- const includePatterns = inputPatterns.filter((p) => !p.startsWith("!"));
738
- const ignorePatterns = inputPatterns
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
- reports.push({
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,
@@ -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");