as-test 1.0.3 → 1.0.5

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 CHANGED
@@ -1,6 +1,20 @@
1
1
  # Change Log
2
2
 
3
- ## 2026-03-27 - 1.0.3
3
+ ## 2026-03-30 - v1.0.5
4
+
5
+ ### CLI
6
+
7
+ - fix: preserve selectors passed after `--parallel` so commands like `ast test --parallel math` still target the requested suite.
8
+ - fix: treat uncaught runtime stderr / missing report payloads as normal failed test results instead of transport-level crashes, with cleaner default reporter output.
9
+ - perf: reuse build artifacts across modes when the resolved non-custom build invocation and build environment are identical, copying the first artifact instead of recompiling.
10
+
11
+ ## 2026-03-27 - v1.0.4
12
+
13
+ ### Build Command
14
+
15
+ - fix: make `ast build` exit cleanly instead of hanging after work completes.
16
+ - feat: make `ast build` print per-mode build results and a final summary.
17
+ - feat: make `ast build` support `--parallel`, `--jobs`, and `--build-jobs`.
4
18
 
5
19
  ### Parallel Execution
6
20
 
package/README.md CHANGED
@@ -66,6 +66,23 @@ By default, `as-test` looks for:
66
66
 
67
67
  Generated files go into `.as-test/`.
68
68
 
69
+ Minimal `as-test.config.json`:
70
+
71
+ ```json
72
+ {
73
+ "input": ["assembly/__tests__/*.spec.ts"],
74
+ "output": ".as-test/",
75
+ "buildOptions": {
76
+ "target": "wasi"
77
+ },
78
+ "runOptions": {
79
+ "runtime": {
80
+ "cmd": "node .as-test/runners/default.wasi.js <file>"
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
69
86
  ## Writing Tests
70
87
 
71
88
  Tests usually live in `assembly/__tests__/*.spec.ts`.
@@ -102,6 +102,13 @@ function getSerialBuildWorkerPool() {
102
102
  }
103
103
  return serialBuildWorkerPool;
104
104
  }
105
+ export async function closeSerialBuildWorkerPool() {
106
+ if (!serialBuildWorkerPool)
107
+ return;
108
+ const pool = serialBuildWorkerPool;
109
+ serialBuildWorkerPool = null;
110
+ await pool.close();
111
+ }
105
112
  export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
106
113
  const loadedConfig = loadConfig(configPath, false);
107
114
  const mode = applyMode(loadedConfig, modeName);
@@ -117,6 +124,39 @@ export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH
117
124
  const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
118
125
  return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
119
126
  }
127
+ export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
128
+ const loadedConfig = loadConfig(configPath, false);
129
+ const mode = applyMode(loadedConfig, modeName);
130
+ const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
131
+ config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
132
+ if (overrides.target) {
133
+ config.buildOptions.target = overrides.target;
134
+ }
135
+ if (overrides.args?.length) {
136
+ config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
137
+ }
138
+ if (hasCustomBuildCommand(config)) {
139
+ return null;
140
+ }
141
+ const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
142
+ const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
143
+ const invocation = getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
144
+ const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
145
+ const buildEnv = {
146
+ ...mode.env,
147
+ ...config.buildOptions.env,
148
+ AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
149
+ };
150
+ return {
151
+ signature: JSON.stringify({
152
+ command: invocation.command,
153
+ args: stripOutputArgs(invocation.args),
154
+ apiArgs: invocation.apiArgs ? stripOutputArgs(invocation.apiArgs) : [],
155
+ env: sortRecord(buildEnv),
156
+ }),
157
+ outFile,
158
+ };
159
+ }
120
160
  function hasCustomBuildCommand(config) {
121
161
  return !!config.buildOptions.cmd.trim().length;
122
162
  }
@@ -169,6 +209,23 @@ function getUserBuildArgs(config) {
169
209
  }
170
210
  return args;
171
211
  }
212
+ function stripOutputArgs(args) {
213
+ const out = [];
214
+ for (let i = 0; i < args.length; i++) {
215
+ const arg = args[i];
216
+ if (arg == "-o" || arg == "--outFile") {
217
+ i++;
218
+ continue;
219
+ }
220
+ out.push(arg);
221
+ }
222
+ return out;
223
+ }
224
+ function sortRecord(record) {
225
+ return Object.fromEntries(Object.entries(record)
226
+ .filter((entry) => typeof entry[1] == "string")
227
+ .sort((a, b) => a[0].localeCompare(b[0])));
228
+ }
172
229
  function expandBuildCommand(template, file, outFile, target, modeName) {
173
230
  const name = path
174
231
  .basename(file)
@@ -1,9 +1,11 @@
1
+ import { closeSerialBuildWorkerPool, } from "./build-core.js";
1
2
  export { build } from "./build-core.js";
2
- export { formatInvocation, getBuildInvocationPreview } from "./build-core.js";
3
+ export { formatInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./build-core.js";
3
4
  export async function executeBuildCommand(rawArgs, configPath, selectedModes, deps) {
4
5
  const commandArgs = deps.resolveCommandArgs(rawArgs, "build");
5
6
  const listFlags = deps.resolveListFlags(rawArgs, "build");
6
7
  const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
8
+ const parallel = deps.resolveBuildParallelJobs(rawArgs);
7
9
  const buildFeatureToggles = {
8
10
  tryAs: featureToggles.tryAs,
9
11
  coverage: featureToggles.coverage,
@@ -13,5 +15,18 @@ export async function executeBuildCommand(rawArgs, configPath, selectedModes, de
13
15
  await deps.listExecutionPlan("build", configPath, commandArgs, modeTargets, listFlags);
14
16
  return;
15
17
  }
16
- await deps.runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles);
18
+ const previousBuildApi = process.env.AS_TEST_BUILD_API;
19
+ process.env.AS_TEST_BUILD_API = "1";
20
+ try {
21
+ await deps.runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles, parallel);
22
+ }
23
+ finally {
24
+ if (previousBuildApi == undefined) {
25
+ delete process.env.AS_TEST_BUILD_API;
26
+ }
27
+ else {
28
+ process.env.AS_TEST_BUILD_API = previousBuildApi;
29
+ }
30
+ await closeSerialBuildWorkerPool();
31
+ }
17
32
  }
@@ -1286,26 +1286,22 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1286
1286
  let report = null;
1287
1287
  let parseError = null;
1288
1288
  let stderrBuffer = "";
1289
+ let stderrPendingLine = "";
1289
1290
  let stdoutBuffer = "";
1290
- let suppressTraceWarningLine = false;
1291
1291
  let spawnError = null;
1292
1292
  child.on("error", (error) => {
1293
1293
  spawnError = error;
1294
1294
  });
1295
1295
  child.stderr.on("data", (chunk) => {
1296
- stderrBuffer += chunk.toString("utf8");
1297
- let newline = stderrBuffer.indexOf("\n");
1296
+ stderrPendingLine += chunk.toString("utf8");
1297
+ let newline = stderrPendingLine.indexOf("\n");
1298
1298
  while (newline >= 0) {
1299
- const line = stderrBuffer.slice(0, newline + 1);
1300
- stderrBuffer = stderrBuffer.slice(newline + 1);
1301
- if (shouldSuppressWasiWarningLine(line, suppressTraceWarningLine)) {
1302
- suppressTraceWarningLine = true;
1299
+ const line = stderrPendingLine.slice(0, newline + 1);
1300
+ stderrPendingLine = stderrPendingLine.slice(newline + 1);
1301
+ if (!shouldSuppressWasiWarningLine(line)) {
1302
+ stderrBuffer += line;
1303
1303
  }
1304
- else {
1305
- suppressTraceWarningLine = false;
1306
- process.stderr.write(line);
1307
- }
1308
- newline = stderrBuffer.indexOf("\n");
1304
+ newline = stderrPendingLine.indexOf("\n");
1309
1305
  }
1310
1306
  });
1311
1307
  class TestChannel extends Channel {
@@ -1409,55 +1405,138 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1409
1405
  const code = await new Promise((resolve) => {
1410
1406
  child.on("close", (exitCode) => resolve(exitCode ?? 1));
1411
1407
  });
1412
- if (stderrBuffer.length) {
1413
- if (!shouldSuppressWasiWarningLine(stderrBuffer, suppressTraceWarningLine)) {
1414
- process.stderr.write(stderrBuffer);
1415
- }
1408
+ if (stderrPendingLine.length && !shouldSuppressWasiWarningLine(stderrPendingLine)) {
1409
+ stderrBuffer += stderrPendingLine;
1416
1410
  }
1417
1411
  if (spawnError) {
1412
+ const errorText = spawnError.stack ?? spawnError.message;
1418
1413
  persistCrashRecord(crashDir, {
1419
1414
  kind: "test",
1420
1415
  file: specFile,
1421
1416
  mode: modeName ?? "default",
1422
- error: spawnError.stack ?? spawnError.message,
1417
+ error: errorText,
1423
1418
  stdout: stdoutBuffer,
1424
1419
  stderr: stderrBuffer,
1425
1420
  });
1426
- throw spawnError;
1421
+ return createRuntimeFailureReport(specFile, modeName, "failed to start test runtime", errorText, stdoutBuffer, stderrBuffer);
1427
1422
  }
1428
1423
  if (parseError) {
1424
+ const errorText = `could not parse report payload: ${parseError}`;
1429
1425
  persistCrashRecord(crashDir, {
1430
1426
  kind: "test",
1431
1427
  file: specFile,
1432
1428
  mode: modeName ?? "default",
1433
- error: `could not parse report payload: ${parseError}`,
1429
+ error: errorText,
1434
1430
  stdout: stdoutBuffer,
1435
1431
  stderr: stderrBuffer,
1436
1432
  });
1437
- throw new Error(`could not parse report payload: ${parseError}`);
1433
+ return createRuntimeFailureReport(specFile, modeName, "runtime returned an invalid report payload", errorText, stdoutBuffer, stderrBuffer);
1438
1434
  }
1439
1435
  if (!report) {
1436
+ const errorText = "missing report payload from test runtime";
1440
1437
  persistCrashRecord(crashDir, {
1441
1438
  kind: "test",
1442
1439
  file: specFile,
1443
1440
  mode: modeName ?? "default",
1444
- error: "missing report payload from test runtime",
1441
+ error: errorText,
1445
1442
  stdout: stdoutBuffer,
1446
1443
  stderr: stderrBuffer,
1447
1444
  });
1448
- throw new Error("missing report payload from test runtime");
1445
+ return createRuntimeFailureReport(specFile, modeName, "test runtime exited without sending a report", errorText, stdoutBuffer, stderrBuffer);
1449
1446
  }
1450
- if (code !== 0) {
1451
- // Let report determine failure counts, but keep non-zero child exits visible.
1452
- process.stderr.write(chalk.dim(`child process exited with code ${code}\n`));
1447
+ if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
1448
+ const errorParts = [];
1449
+ if (code !== 0) {
1450
+ errorParts.push(`child process exited with code ${code}`);
1451
+ }
1452
+ const stderrText = normalizeRuntimeOutput(stderrBuffer);
1453
+ if (stderrText.length) {
1454
+ errorParts.push(stderrText);
1455
+ }
1456
+ const errorText = errorParts.join("\n\n");
1457
+ persistCrashRecord(crashDir, {
1458
+ kind: "test",
1459
+ file: specFile,
1460
+ mode: modeName ?? "default",
1461
+ error: errorText || "runtime reported an unknown error",
1462
+ stdout: stdoutBuffer,
1463
+ stderr: stderrBuffer,
1464
+ });
1465
+ return appendRuntimeFailureReport(report, specFile, modeName, code !== 0
1466
+ ? `test runtime failed with exit code ${code}`
1467
+ : "test runtime wrote to stderr", errorText, stdoutBuffer, stderrBuffer);
1453
1468
  }
1454
1469
  return report;
1455
1470
  }
1456
- function shouldSuppressWasiWarningLine(line, suppressTraceWarningLine) {
1471
+ function createRuntimeFailureReport(specFile, modeName, title, details, stdout, stderr) {
1472
+ return appendRuntimeFailureReport({
1473
+ suites: [],
1474
+ coverage: {
1475
+ total: 0,
1476
+ covered: 0,
1477
+ uncovered: 0,
1478
+ percent: 100,
1479
+ points: [],
1480
+ },
1481
+ }, specFile, modeName, title, details, stdout, stderr);
1482
+ }
1483
+ function appendRuntimeFailureReport(report, specFile, modeName, title, details, stdout, stderr) {
1484
+ const suites = Array.isArray(report?.suites) ? report.suites : [];
1485
+ suites.push({
1486
+ file: specFile,
1487
+ description: path.basename(specFile),
1488
+ depth: 0,
1489
+ kind: "runtime-error",
1490
+ verdict: "fail",
1491
+ time: {
1492
+ start: 0,
1493
+ end: 0,
1494
+ },
1495
+ suites: [],
1496
+ logs: [],
1497
+ tests: [
1498
+ {
1499
+ order: 0,
1500
+ type: "runtime-error",
1501
+ verdict: "fail",
1502
+ left: null,
1503
+ right: null,
1504
+ instr: title,
1505
+ message: formatRuntimeFailureMessage(details, stdout, stderr),
1506
+ location: "",
1507
+ },
1508
+ ],
1509
+ modeName: modeName ?? "default",
1510
+ });
1511
+ report.suites = suites;
1512
+ return report;
1513
+ }
1514
+ function formatRuntimeFailureMessage(details, stdout, stderr) {
1515
+ const parts = [];
1516
+ const normalizedDetails = normalizeRuntimeOutput(details);
1517
+ const normalizedStderr = normalizeRuntimeOutput(stderr);
1518
+ const normalizedStdout = normalizeRuntimeOutput(stdout);
1519
+ if (normalizedDetails.length)
1520
+ parts.push(normalizedDetails);
1521
+ if (normalizedStderr.length && normalizedStderr !== normalizedDetails) {
1522
+ parts.push(`stderr:\n${normalizedStderr}`);
1523
+ }
1524
+ if (normalizedStdout.length) {
1525
+ parts.push(`stdout:\n${normalizedStdout}`);
1526
+ }
1527
+ return parts.join("\n\n");
1528
+ }
1529
+ function normalizeRuntimeOutput(value) {
1530
+ return value.replace(/\r\n/g, "\n").trim();
1531
+ }
1532
+ function hasMeaningfulRuntimeOutput(value) {
1533
+ return normalizeRuntimeOutput(value).length > 0;
1534
+ }
1535
+ function shouldSuppressWasiWarningLine(line) {
1457
1536
  if (line.includes("ExperimentalWarning: WASI is an experimental feature")) {
1458
1537
  return true;
1459
1538
  }
1460
- if (suppressTraceWarningLine && line.includes("--trace-warnings")) {
1539
+ if (line.includes("--trace-warnings")) {
1461
1540
  return true;
1462
1541
  }
1463
1542
  return false;
@@ -1506,6 +1585,7 @@ function readFileReport(stats, fileReport) {
1506
1585
  }
1507
1586
  function readSuite(stats, suite, file, modeName) {
1508
1587
  const suiteAny = suite;
1588
+ const kind = String(suiteAny.kind ?? "");
1509
1589
  let verdict = normalizeVerdict(suiteAny.verdict);
1510
1590
  const time = suiteAny.time;
1511
1591
  const start = Number(time?.start ?? 0);
@@ -1533,6 +1613,20 @@ function readSuite(stats, suite, file, modeName) {
1533
1613
  stats.skippedTests++;
1534
1614
  }
1535
1615
  }
1616
+ if (isTestCaseSuiteKind(kind)) {
1617
+ if (!subSuites.length && !tests.length) {
1618
+ if (verdict == "fail") {
1619
+ stats.failedTests++;
1620
+ }
1621
+ else if (verdict == "ok") {
1622
+ stats.passedTests++;
1623
+ }
1624
+ else if (verdict == "skip") {
1625
+ stats.skippedTests++;
1626
+ }
1627
+ }
1628
+ return verdict;
1629
+ }
1536
1630
  if (verdict == "fail") {
1537
1631
  stats.failedSuites++;
1538
1632
  stats.failedEntries.push({
@@ -1549,6 +1643,15 @@ function readSuite(stats, suite, file, modeName) {
1549
1643
  }
1550
1644
  return verdict;
1551
1645
  }
1646
+ function isTestCaseSuiteKind(kind) {
1647
+ return (kind == "test" ||
1648
+ kind == "it" ||
1649
+ kind == "only" ||
1650
+ kind == "xtest" ||
1651
+ kind == "xit" ||
1652
+ kind == "xonly" ||
1653
+ kind == "todo");
1654
+ }
1552
1655
  function normalizeVerdict(value) {
1553
1656
  const verdict = String(value ?? "none");
1554
1657
  if (verdict == "fail")
@@ -13,6 +13,7 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
13
13
  ...deps.resolveParallelJobs(rawArgs, "run"),
14
14
  coverage: featureToggles.coverage,
15
15
  browser: deps.resolveBrowserOverride(rawArgs, "run"),
16
+ reporterPath: deps.resolveReporterOverride(rawArgs, "run"),
16
17
  };
17
18
  const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
18
19
  if (listFlags.list || listFlags.listModes) {
@@ -16,6 +16,7 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
16
16
  ...deps.resolveParallelJobs(rawArgs, "test"),
17
17
  coverage: featureToggles.coverage,
18
18
  browser: deps.resolveBrowserOverride(rawArgs, "test"),
19
+ reporterPath: deps.resolveReporterOverride(rawArgs, "test"),
19
20
  };
20
21
  const fuzzEnabled = flags.includes("--fuzz");
21
22
  const fuzzOverrides = deps.resolveFuzzOverrides(rawArgs, "test");
package/bin/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from "chalk";
3
- import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } from "./commands/build.js";
3
+ import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./commands/build.js";
4
4
  import { createRunReporter, run } from "./commands/run.js";
5
5
  import { executeBuildCommand } from "./commands/build.js";
6
6
  import { executeRunCommand } from "./commands/run.js";
@@ -9,12 +9,12 @@ import { executeFuzzCommand } from "./commands/fuzz.js";
9
9
  import { executeInitCommand } from "./commands/init.js";
10
10
  import { executeDoctorCommand } from "./commands/doctor.js";
11
11
  import { fuzz } from "./commands/fuzz-core.js";
12
- import { applyMode, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
12
+ import { applyMode, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
13
13
  import * as path from "path";
14
14
  import { spawnSync } from "child_process";
15
15
  import { glob } from "glob";
16
16
  import { createInterface } from "readline";
17
- import { existsSync } from "fs";
17
+ import { copyFileSync, existsSync, mkdirSync } from "fs";
18
18
  import { availableParallelism, cpus } from "os";
19
19
  import { BuildWorkerPool } from "./build-worker-pool.js";
20
20
  const _args = process.argv.slice(2);
@@ -50,6 +50,7 @@ else if (COMMANDS.includes(args[0])) {
50
50
  resolveCommandArgs,
51
51
  resolveListFlags,
52
52
  resolveFeatureToggles,
53
+ resolveBuildParallelJobs,
53
54
  resolveExecutionModes,
54
55
  listExecutionPlan,
55
56
  runBuildModes,
@@ -65,6 +66,7 @@ else if (COMMANDS.includes(args[0])) {
65
66
  resolveFeatureToggles,
66
67
  resolveParallelJobs,
67
68
  resolveBrowserOverride,
69
+ resolveReporterOverride,
68
70
  resolveExecutionModes,
69
71
  listExecutionPlan,
70
72
  runRuntimeModes,
@@ -80,6 +82,7 @@ else if (COMMANDS.includes(args[0])) {
80
82
  resolveFeatureToggles,
81
83
  resolveParallelJobs,
82
84
  resolveBrowserOverride,
85
+ resolveReporterOverride,
83
86
  resolveFuzzOverrides,
84
87
  resolveExecutionModes,
85
88
  listExecutionPlan,
@@ -226,6 +229,9 @@ function printCommandHelp(command) {
226
229
  process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
227
230
  process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
228
231
  process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
232
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
233
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
234
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
229
235
  process.stdout.write(" --list Preview resolved files/artifacts without building\n");
230
236
  process.stdout.write(" --list-modes Preview configured and selected mode names\n");
231
237
  process.stdout.write(" --help, -h Show this help\n");
@@ -396,7 +402,6 @@ function resolveCommandArgs(rawArgs, command) {
396
402
  }
397
403
  if (arg == "--runs" ||
398
404
  arg == "--seed" ||
399
- arg == "--parallel" ||
400
405
  arg == "--jobs" ||
401
406
  arg == "--build-jobs" ||
402
407
  arg == "--run-jobs" ||
@@ -406,6 +411,9 @@ function resolveCommandArgs(rawArgs, command) {
406
411
  i++;
407
412
  continue;
408
413
  }
414
+ if (arg == "--parallel") {
415
+ continue;
416
+ }
409
417
  if (arg.startsWith("--runs=") ||
410
418
  arg.startsWith("--seed=") ||
411
419
  arg.startsWith("--jobs=") ||
@@ -573,6 +581,32 @@ function resolveBrowserOverride(rawArgs, command) {
573
581
  }
574
582
  return undefined;
575
583
  }
584
+ function resolveReporterOverride(rawArgs, command) {
585
+ let seenCommand = false;
586
+ for (let i = 0; i < rawArgs.length; i++) {
587
+ const arg = rawArgs[i];
588
+ if (!seenCommand) {
589
+ if (arg == command)
590
+ seenCommand = true;
591
+ continue;
592
+ }
593
+ if (arg == "--reporter") {
594
+ const next = rawArgs[i + 1];
595
+ if (next && !next.startsWith("-")) {
596
+ return next;
597
+ }
598
+ return undefined;
599
+ }
600
+ if (arg.startsWith("--reporter=")) {
601
+ const value = arg.slice("--reporter=".length);
602
+ return value.length ? value : undefined;
603
+ }
604
+ if (arg == "--tap") {
605
+ return "tap";
606
+ }
607
+ }
608
+ return undefined;
609
+ }
576
610
  function resolveJobs(rawArgs, command) {
577
611
  let seenCommand = false;
578
612
  let parallel = false;
@@ -597,6 +631,29 @@ function resolveJobs(rawArgs, command) {
597
631
  }
598
632
  return parallel ? 0 : 1;
599
633
  }
634
+ function resolveBuildParallelJobs(rawArgs) {
635
+ const baseJobs = resolveJobs(rawArgs, "build");
636
+ let buildJobs = baseJobs;
637
+ let seenCommand = false;
638
+ for (let i = 0; i < rawArgs.length; i++) {
639
+ const arg = rawArgs[i];
640
+ if (!seenCommand) {
641
+ if (arg == "build")
642
+ seenCommand = true;
643
+ continue;
644
+ }
645
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
646
+ if (buildParsed) {
647
+ if (buildParsed.number < 1) {
648
+ throw new Error("--build-jobs requires a positive integer");
649
+ }
650
+ buildJobs = buildParsed.number;
651
+ continue;
652
+ }
653
+ }
654
+ const jobs = Math.max(baseJobs, buildJobs);
655
+ return { jobs, buildJobs };
656
+ }
600
657
  function resolveParallelJobs(rawArgs, command) {
601
658
  const baseJobs = resolveJobs(rawArgs, command);
602
659
  let buildJobs = baseJobs;
@@ -704,9 +761,9 @@ function createBufferedStream() {
704
761
  },
705
762
  };
706
763
  }
707
- async function createBufferedReporter(configPath, modeName) {
764
+ async function createBufferedReporter(configPath, reporterPath, modeName) {
708
765
  const stream = createBufferedStream();
709
- const session = await createRunReporter(configPath, undefined, modeName, {
766
+ const session = await createRunReporter(configPath, reporterPath, modeName, {
710
767
  stdout: stream,
711
768
  stderr: stream,
712
769
  });
@@ -855,6 +912,32 @@ function resolveCommandTokens(rawArgs, command) {
855
912
  }
856
913
  return values;
857
914
  }
915
+ async function buildFileForMode(cache, args) {
916
+ const reuse = await getBuildReuseInfo(args.configPath, args.file, args.modeName, args.buildFeatureToggles);
917
+ if (reuse) {
918
+ const source = cache.get(reuse.signature);
919
+ if (source && source != reuse.outFile && existsSync(source)) {
920
+ mkdirSync(path.dirname(reuse.outFile), { recursive: true });
921
+ copyFileSync(source, reuse.outFile);
922
+ return true;
923
+ }
924
+ }
925
+ if (args.buildPool) {
926
+ await args.buildPool.buildFileMode({
927
+ configPath: args.configPath,
928
+ file: args.file,
929
+ modeName: args.modeName,
930
+ featureToggles: args.buildFeatureToggles,
931
+ });
932
+ }
933
+ else {
934
+ await build(args.configPath, [args.file], args.modeName, args.buildFeatureToggles);
935
+ }
936
+ if (reuse) {
937
+ cache.set(reuse.signature, reuse.outFile);
938
+ }
939
+ return false;
940
+ }
858
941
  async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
859
942
  const files = await resolveSelectedFiles(configPath, selectors);
860
943
  if (!files.length) {
@@ -862,7 +945,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
862
945
  throw await buildNoTestFilesMatchedError(configPath, selectors);
863
946
  }
864
947
  }
865
- const reporterSession = await createRunReporter(configPath, undefined, modeName);
948
+ const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
866
949
  const reporter = reporterOverride ?? reporterSession.reporter;
867
950
  const snapshotEnabled = runFlags.snapshot !== false;
868
951
  reporter.onRunStart?.({
@@ -922,10 +1005,55 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
922
1005
  },
923
1006
  };
924
1007
  }
925
- async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
1008
+ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles, parallel) {
1009
+ const files = await resolveSelectedFiles(configPath, selectors);
1010
+ if (!files.length) {
1011
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1012
+ }
1013
+ const effective = resolveEffectiveParallelJobs({
1014
+ jobs: parallel.jobs,
1015
+ buildJobs: parallel.buildJobs,
1016
+ runJobs: parallel.buildJobs,
1017
+ }, files.length);
1018
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1019
+ const loadedConfig = loadConfig(resolvedConfigPath, true);
1020
+ const allStartedAt = Date.now();
1021
+ let builtCount = 0;
1022
+ const buildReuseCache = new Map();
926
1023
  for (const modeName of modes) {
927
- await build(configPath, selectors, modeName, buildFeatureToggles);
1024
+ const startedAt = Date.now();
1025
+ if (effective.buildJobs > 1) {
1026
+ const pool = new BuildWorkerPool(effective.buildJobs);
1027
+ try {
1028
+ await runOrderedPool(files, effective.buildJobs, async (file) => {
1029
+ await buildFileForMode(buildReuseCache, {
1030
+ configPath,
1031
+ file,
1032
+ modeName,
1033
+ buildFeatureToggles,
1034
+ buildPool: pool,
1035
+ });
1036
+ });
1037
+ }
1038
+ finally {
1039
+ await pool.close();
1040
+ }
1041
+ }
1042
+ else {
1043
+ for (const file of files) {
1044
+ await buildFileForMode(buildReuseCache, {
1045
+ configPath,
1046
+ file,
1047
+ modeName,
1048
+ buildFeatureToggles,
1049
+ });
1050
+ }
1051
+ }
1052
+ builtCount += files.length;
1053
+ const active = applyMode(loadedConfig, modeName).config;
1054
+ process.stdout.write(`${chalk.bgGreenBright.black(" BUILT ")} ${modeName ?? "default"} ${chalk.dim(`(${active.buildOptions.target})`)} ${files.length} file(s) -> ${active.outDir} ${chalk.dim(formatTime(Date.now() - startedAt))}\n`);
928
1055
  }
1056
+ process.stdout.write(`${chalk.bold("Summary:")} built ${builtCount} file(s) across ${modes.length || 1} mode(s) in ${formatTime(Date.now() - allStartedAt)}\n`);
929
1057
  }
930
1058
  async function runRuntimeModes(runFlags, configPath, selectors, modes) {
931
1059
  await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
@@ -959,6 +1087,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
959
1087
  const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
960
1088
  for (const modeName of modes) {
961
1089
  const result = await run(effectiveRunFlags, configPath, selectors, false, {
1090
+ reporterPath: effectiveRunFlags.reporterPath,
962
1091
  modeName,
963
1092
  modeSummaryTotal,
964
1093
  modeSummaryExecuted: 1,
@@ -1000,11 +1129,13 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
1000
1129
  }));
1001
1130
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1002
1131
  const buildIntervals = [];
1132
+ const buildReuseCache = new Map();
1003
1133
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
1004
1134
  const file = files[fileIndex];
1005
1135
  const fileName = path.basename(file);
1006
1136
  const fileResults = [];
1007
1137
  const modeTimes = modes.map(() => "...");
1138
+ const buildReuseCache = new Map();
1008
1139
  if (liveMatrix) {
1009
1140
  renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
1010
1141
  }
@@ -1041,7 +1172,9 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
1041
1172
  throw error;
1042
1173
  }
1043
1174
  }
1044
- renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
1175
+ if (reporterSession.reporterKind == "default") {
1176
+ renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
1177
+ }
1045
1178
  const verdict = resolveMatrixVerdict(fileResults);
1046
1179
  if (verdict == "fail") {
1047
1180
  fileState[fileIndex].failed = true;
@@ -1095,7 +1228,7 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
1095
1228
  }
1096
1229
  let failed = false;
1097
1230
  for (const modeName of modes) {
1098
- const reporterSession = await createRunReporter(configPath, undefined, modeName);
1231
+ const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
1099
1232
  const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
1100
1233
  if (modeResult.failed)
1101
1234
  failed = true;
@@ -1165,6 +1298,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
1165
1298
  const fileName = path.basename(file);
1166
1299
  const fileResults = [];
1167
1300
  const modeTimes = modes.map(() => "...");
1301
+ const buildReuseCache = new Map();
1168
1302
  if (liveMatrix) {
1169
1303
  renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
1170
1304
  }
@@ -1172,7 +1306,12 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
1172
1306
  const modeName = modes[i];
1173
1307
  try {
1174
1308
  const buildStartedAt = Date.now();
1175
- await build(configPath, [file], modeName, buildFeatureToggles);
1309
+ await buildFileForMode(buildReuseCache, {
1310
+ configPath,
1311
+ file,
1312
+ modeName,
1313
+ buildFeatureToggles,
1314
+ });
1176
1315
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1177
1316
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1178
1317
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
@@ -1204,7 +1343,9 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
1204
1343
  throw error;
1205
1344
  }
1206
1345
  }
1207
- renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
1346
+ if (reporterSession.reporterKind == "default") {
1347
+ renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
1348
+ }
1208
1349
  const verdict = resolveMatrixVerdict(fileResults);
1209
1350
  if (verdict == "fail") {
1210
1351
  fileState[fileIndex].failed = true;
@@ -1267,7 +1408,7 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
1267
1408
  if (!files.length) {
1268
1409
  throw await buildNoTestFilesMatchedError(configPath, selectors);
1269
1410
  }
1270
- const reporterSession = await createRunReporter(configPath, undefined, modeName);
1411
+ const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
1271
1412
  const reporter = reporterSession.reporter;
1272
1413
  const snapshotEnabled = runFlags.snapshot !== false;
1273
1414
  reporter.onRunStart?.({
@@ -1279,15 +1420,20 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
1279
1420
  });
1280
1421
  const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
1281
1422
  const results = new Array(files.length);
1282
- const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1423
+ const useQueueDisplay = reporterSession.reporterKind == "default";
1424
+ const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
1283
1425
  const runLimit = createAsyncLimiter(runFlags.runJobs);
1284
1426
  const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
1285
1427
  await runOrderedPool(files, poolWidth, async (file, index) => {
1286
- const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1287
- const buffered = await createBufferedReporter(configPath, modeName);
1428
+ const token = useQueueDisplay
1429
+ ? renderQueuedFileStart(queueDisplay, path.basename(file))
1430
+ : null;
1431
+ const buffered = useQueueDisplay
1432
+ ? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
1433
+ : null;
1288
1434
  const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
1289
- reporter: buffered.reporter,
1290
- reporterKind: buffered.reporterKind,
1435
+ reporter: buffered?.reporter,
1436
+ reporterKind: buffered?.reporterKind,
1291
1437
  modeName,
1292
1438
  emitRunComplete: false,
1293
1439
  fileSummaryTotal: 1,
@@ -1295,9 +1441,11 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
1295
1441
  modeSummaryExecuted: 1,
1296
1442
  buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
1297
1443
  }));
1298
- buffered.reporter.flush?.();
1444
+ buffered?.reporter.flush?.();
1299
1445
  results[index] = result;
1300
- queueDisplay.complete(token, buffered.output());
1446
+ if (buffered && token != null) {
1447
+ queueDisplay.complete(token, buffered.output());
1448
+ }
1301
1449
  });
1302
1450
  queueDisplay.flush();
1303
1451
  const summary = aggregateRunResults(results);
@@ -1321,7 +1469,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
1321
1469
  if (!files.length) {
1322
1470
  throw await buildNoTestFilesMatchedError(configPath, selectors);
1323
1471
  }
1324
- const reporterSession = await createRunReporter(configPath);
1472
+ const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
1325
1473
  const reporter = reporterSession.reporter;
1326
1474
  const snapshotEnabled = runFlags.snapshot !== false;
1327
1475
  reporter.onRunStart?.({
@@ -1336,23 +1484,29 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
1336
1484
  const showPerModeTimes = Boolean(runFlags.verbose);
1337
1485
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1338
1486
  const ordered = new Array(files.length);
1339
- const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1487
+ const useQueueDisplay = reporterSession.reporterKind == "default";
1488
+ const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
1340
1489
  const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1341
1490
  const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1342
1491
  const buildIntervals = [];
1343
1492
  try {
1344
1493
  await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1345
1494
  const fileName = path.basename(file);
1346
- const token = renderQueuedFileStart(queueDisplay, fileName);
1495
+ const token = useQueueDisplay
1496
+ ? renderQueuedFileStart(queueDisplay, fileName)
1497
+ : null;
1347
1498
  const fileResults = [];
1348
1499
  const modeTimes = modes.map(() => "...");
1500
+ const buildReuseCache = new Map();
1349
1501
  for (let i = 0; i < modes.length; i++) {
1350
1502
  const modeName = modes[i];
1351
1503
  const buildStartedAt = Date.now();
1352
- await buildPool.buildFileMode({
1504
+ await buildFileForMode(buildReuseCache, {
1353
1505
  configPath,
1354
1506
  file,
1355
1507
  modeName,
1508
+ buildFeatureToggles: {},
1509
+ buildPool,
1356
1510
  });
1357
1511
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1358
1512
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
@@ -1371,7 +1525,9 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
1371
1525
  fileResults.push(result);
1372
1526
  }
1373
1527
  ordered[fileIndex] = { fileName, fileResults, modeTimes };
1374
- queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1528
+ if (token != null) {
1529
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1530
+ }
1375
1531
  });
1376
1532
  }
1377
1533
  finally {
@@ -1418,7 +1574,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
1418
1574
  if (!files.length && !fuzzEnabled) {
1419
1575
  throw await buildNoTestFilesMatchedError(configPath, selectors);
1420
1576
  }
1421
- const reporterSession = await createRunReporter(configPath, undefined, modeName);
1577
+ const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
1422
1578
  const reporter = reporterSession.reporter;
1423
1579
  const snapshotEnabled = runFlags.snapshot !== false;
1424
1580
  reporter.onRunStart?.({
@@ -1430,37 +1586,45 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
1430
1586
  });
1431
1587
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1432
1588
  const results = new Array(files.length);
1433
- const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1589
+ const useQueueDisplay = reporterSession.reporterKind == "default";
1590
+ const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
1434
1591
  const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1435
1592
  const buildIntervals = [];
1436
1593
  if (files.length) {
1437
1594
  const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1438
1595
  try {
1439
1596
  await runOrderedPool(files, poolWidth, async (file, index) => {
1440
- const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1597
+ const token = useQueueDisplay
1598
+ ? renderQueuedFileStart(queueDisplay, path.basename(file))
1599
+ : null;
1441
1600
  const buildStartedAt = Date.now();
1442
- await buildPool.buildFileMode({
1601
+ await buildFileForMode(new Map(), {
1443
1602
  configPath,
1444
1603
  file,
1445
1604
  modeName,
1446
- featureToggles: buildFeatureToggles,
1605
+ buildFeatureToggles,
1606
+ buildPool,
1447
1607
  });
1448
1608
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1449
1609
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1450
1610
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1451
- const buffered = await createBufferedReporter(configPath, modeName);
1611
+ const buffered = useQueueDisplay
1612
+ ? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
1613
+ : null;
1452
1614
  const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
1453
- reporter: buffered.reporter,
1454
- reporterKind: buffered.reporterKind,
1615
+ reporter: buffered?.reporter,
1616
+ reporterKind: buffered?.reporterKind,
1455
1617
  emitRunComplete: false,
1456
1618
  logFileName: `test.${artifactKey}.log.json`,
1457
1619
  coverageFileName: `coverage.${artifactKey}.log.json`,
1458
1620
  buildCommand: formatBuildInvocation(buildInvocation),
1459
1621
  modeName,
1460
1622
  });
1461
- buffered.reporter.flush?.();
1623
+ buffered?.reporter.flush?.();
1462
1624
  results[index] = result;
1463
- queueDisplay.complete(token, buffered.output());
1625
+ if (buffered && token != null) {
1626
+ queueDisplay.complete(token, buffered.output());
1627
+ }
1464
1628
  });
1465
1629
  }
1466
1630
  finally {
@@ -1509,7 +1673,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
1509
1673
  throw await buildNoTestFilesMatchedError(configPath, selectors, true);
1510
1674
  }
1511
1675
  }
1512
- const reporterSession = await createRunReporter(configPath);
1676
+ const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
1513
1677
  const reporter = reporterSession.reporter;
1514
1678
  const snapshotEnabled = runFlags.snapshot !== false;
1515
1679
  reporter.onRunStart?.({
@@ -1524,24 +1688,29 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
1524
1688
  const showPerModeTimes = Boolean(runFlags.verbose);
1525
1689
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1526
1690
  const ordered = new Array(files.length);
1527
- const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1691
+ const useQueueDisplay = reporterSession.reporterKind == "default";
1692
+ const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
1528
1693
  const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1529
1694
  const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1530
1695
  const buildIntervals = [];
1531
1696
  try {
1532
1697
  await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1533
1698
  const fileName = path.basename(file);
1534
- const token = renderQueuedFileStart(queueDisplay, fileName);
1699
+ const token = useQueueDisplay
1700
+ ? renderQueuedFileStart(queueDisplay, fileName)
1701
+ : null;
1535
1702
  const fileResults = [];
1536
1703
  const modeTimes = modes.map(() => "...");
1704
+ const buildReuseCache = new Map();
1537
1705
  for (let i = 0; i < modes.length; i++) {
1538
1706
  const modeName = modes[i];
1539
1707
  const buildStartedAt = Date.now();
1540
- await buildPool.buildFileMode({
1708
+ await buildFileForMode(buildReuseCache, {
1541
1709
  configPath,
1542
1710
  file,
1543
1711
  modeName,
1544
- featureToggles: buildFeatureToggles,
1712
+ buildFeatureToggles,
1713
+ buildPool,
1545
1714
  });
1546
1715
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1547
1716
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
@@ -1560,7 +1729,9 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
1560
1729
  fileResults.push(result);
1561
1730
  }
1562
1731
  ordered[fileIndex] = { fileName, fileResults, modeTimes };
1563
- queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1732
+ if (token != null) {
1733
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1734
+ }
1564
1735
  });
1565
1736
  }
1566
1737
  finally {
@@ -465,12 +465,30 @@ function collectSuiteFailures(suite, file, path, printed) {
465
465
  const loc = String(test.location ?? "");
466
466
  const where = loc.length ? `${file}:${loc}` : file;
467
467
  const modeName = String(suiteAny.modeName ?? "");
468
- const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}`;
468
+ const message = String(test.message ?? "");
469
+ const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
469
470
  if (printed.has(dedupeKey))
470
471
  continue;
471
472
  printed.add(dedupeKey);
472
473
  const left = JSON.stringify(test.left);
473
474
  const right = JSON.stringify(test.right);
475
+ if (left == "null" && right == "null") {
476
+ console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(title)} ${chalk.dim("(" + where + ")")}`);
477
+ if (modeName.length) {
478
+ console.log(chalk.dim(`Mode: ${modeName}`));
479
+ }
480
+ const normalizedMessage = normalizeFailureMessage(message);
481
+ if (normalizedMessage.length) {
482
+ for (const line of normalizedMessage.split("\n")) {
483
+ console.log(chalk.dim(line));
484
+ }
485
+ }
486
+ else {
487
+ console.log(chalk.dim("runtime error"));
488
+ }
489
+ console.log("");
490
+ continue;
491
+ }
474
492
  const diffResult = diff(left, right);
475
493
  let expected = "";
476
494
  for (const res of diffResult.diff) {
@@ -506,6 +524,9 @@ function collectSuiteFailures(suite, file, path, printed) {
506
524
  collectSuiteFailures(sub, file, nextPath, printed);
507
525
  }
508
526
  }
527
+ function normalizeFailureMessage(message) {
528
+ return message.replace(/\r\n/g, "\n").trim();
529
+ }
509
530
  function renderSnapshotSummary(snapshotSummary, leadingGap = true) {
510
531
  if (leadingGap) {
511
532
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,9 +64,9 @@
64
64
  "access": "public"
65
65
  },
66
66
  "scripts": {
67
- "test": "node ./bin/index.js test",
67
+ "test": "node ./bin/index.js test --parallel",
68
+ "test:ci": "node ./bin/index.js test --parallel --tap --config ./as-test.ci.config.json",
68
69
  "fuzz": "node ./bin/index.js fuzz",
69
- "test:ci": "node ./bin/index.js test --tap --config ./as-test.ci.config.json",
70
70
  "test:examples": "npm --prefix ./examples run test",
71
71
  "ci:act": "bash ./tools/act.sh push",
72
72
  "ci:act:pr": "bash ./tools/act.sh pull_request",