as-test 1.0.1 → 1.0.4

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/bin/index.js CHANGED
@@ -1,19 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from "chalk";
3
- import { build } from "./commands/build.js";
3
+ import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } 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";
7
7
  import { executeTestCommand } from "./commands/test.js";
8
+ import { executeFuzzCommand } from "./commands/fuzz.js";
8
9
  import { executeInitCommand } from "./commands/init.js";
9
10
  import { executeDoctorCommand } from "./commands/doctor.js";
10
- import { applyMode, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
11
+ import { fuzz } from "./commands/fuzz-core.js";
12
+ import { applyMode, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
11
13
  import * as path from "path";
14
+ import { spawnSync } from "child_process";
12
15
  import { glob } from "glob";
16
+ import { createInterface } from "readline";
17
+ import { existsSync } from "fs";
18
+ import { availableParallelism, cpus } from "os";
19
+ import { BuildWorkerPool } from "./build-worker-pool.js";
13
20
  const _args = process.argv.slice(2);
14
21
  const flags = [];
15
22
  const args = [];
16
- const COMMANDS = ["run", "build", "test", "init", "doctor"];
23
+ const COMMANDS = ["run", "build", "test", "fuzz", "init", "doctor"];
17
24
  const version = getCliVersion();
18
25
  const configPath = resolveConfigPath(_args);
19
26
  const selectedModes = resolveModeNames(_args);
@@ -25,7 +32,7 @@ for (const arg of _args) {
25
32
  }
26
33
  if (!args.length) {
27
34
  if (flags.includes("--version") || flags.includes("-v")) {
28
- console.log("as-test v" + version.toString());
35
+ console.log(version.toString());
29
36
  }
30
37
  else {
31
38
  info();
@@ -43,6 +50,7 @@ else if (COMMANDS.includes(args[0])) {
43
50
  resolveCommandArgs,
44
51
  resolveListFlags,
45
52
  resolveFeatureToggles,
53
+ resolveBuildParallelJobs,
46
54
  resolveExecutionModes,
47
55
  listExecutionPlan,
48
56
  runBuildModes,
@@ -56,6 +64,8 @@ else if (COMMANDS.includes(args[0])) {
56
64
  resolveCommandArgs,
57
65
  resolveListFlags,
58
66
  resolveFeatureToggles,
67
+ resolveParallelJobs,
68
+ resolveBrowserOverride,
59
69
  resolveExecutionModes,
60
70
  listExecutionPlan,
61
71
  runRuntimeModes,
@@ -69,6 +79,9 @@ else if (COMMANDS.includes(args[0])) {
69
79
  resolveCommandArgs,
70
80
  resolveListFlags,
71
81
  resolveFeatureToggles,
82
+ resolveParallelJobs,
83
+ resolveBrowserOverride,
84
+ resolveFuzzOverrides,
72
85
  resolveExecutionModes,
73
86
  listExecutionPlan,
74
87
  runTestModes,
@@ -77,6 +90,19 @@ else if (COMMANDS.includes(args[0])) {
77
90
  process.exit(1);
78
91
  });
79
92
  }
93
+ else if (command === "fuzz") {
94
+ executeFuzzCommand(_args, configPath, selectedModes, {
95
+ resolveCommandArgs,
96
+ resolveListFlags,
97
+ resolveJobs,
98
+ resolveExecutionModes,
99
+ listExecutionPlan,
100
+ runFuzzModes,
101
+ }).catch((error) => {
102
+ printCliError(error);
103
+ process.exit(1);
104
+ });
105
+ }
80
106
  else if (command === "init") {
81
107
  executeInitCommand(_args, {
82
108
  resolveCommandTokens,
@@ -139,6 +165,13 @@ function info() {
139
165
  " " +
140
166
  "Build and run unit tests with selected runtime" +
141
167
  "\n");
168
+ console.log(" " +
169
+ chalk.bold.blueBright("fuzz") +
170
+ " " +
171
+ chalk.dim("<name>|<path-or-glob>") +
172
+ " " +
173
+ "Build and run fuzz targets" +
174
+ "\n");
142
175
  console.log(" " +
143
176
  chalk.bold.magentaBright("init") +
144
177
  " " +
@@ -153,63 +186,19 @@ function info() {
153
186
  "Validate environment/config/runtime setup");
154
187
  console.log("");
155
188
  console.log(chalk.bold("Flags:"));
156
- console.log(" " +
157
- chalk.bold.blue("--mode <name[,name...]>") +
158
- " " +
159
- "Run one or multiple named config modes");
160
- console.log(" " +
161
- chalk.bold.blue("--config <path>") +
162
- " " +
163
- "Use a specific config file");
164
- console.log(" " +
165
- chalk.bold.blue("--snapshot") +
166
- " " +
167
- "Snapshot assertions (enabled by default)");
168
- console.log(" " +
169
- chalk.bold.blue("--update-snapshots") +
170
- " " +
171
- "Create/update snapshot files on mismatch");
172
- console.log(" " +
173
- chalk.bold.blue("--no-snapshot") +
174
- " " +
175
- "Disable snapshot assertions for this run");
176
- console.log(" " +
177
- chalk.bold.blue("--show-coverage") +
178
- " " +
179
- "Print all coverage points with line:column refs");
180
- console.log(" " +
181
- chalk.bold.blue("--enable <feature>") +
182
- " " +
183
- "Enable as-test feature (coverage|try-as)");
184
- console.log(" " +
185
- chalk.bold.blue("--disable <feature>") +
186
- " " +
187
- "Disable as-test feature (coverage|try-as)");
188
- console.log(" " +
189
- chalk.bold.blue("--verbose") +
190
- " " +
191
- "Print each suite start/end line");
192
- console.log(" " +
193
- chalk.bold.blue("--reporter <name|path>") +
194
- " " +
195
- "Use built-in reporter (default|tap) or custom module path");
196
- console.log(" " +
197
- chalk.bold.blue("--list") +
198
- " " +
199
- "Preview resolved files/modes/artifacts without running");
200
- console.log(" " +
201
- chalk.bold.blue("--list-modes") +
189
+ console.log(" " +
190
+ chalk.bold.blue("--version, -v") +
202
191
  " " +
203
- "Preview configured and selected mode names");
204
- console.log(" " + chalk.bold.blue("--help, -h") + " Show help");
192
+ "Print current cli version");
193
+ console.log(" " +
194
+ chalk.bold.blue("--help, -h") +
195
+ " Show help menu");
205
196
  console.log("");
206
- console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
197
+ console.log(chalk.dim("If this tool provides value, please consider sponsoring my open-source work! https://jairus.dev/sponsor") + "\n");
198
+ console.log("View the docs: " +
199
+ chalk.blue("https://docs.jairus.dev/as-test"));
207
200
  console.log("View the repo: " +
208
- chalk.magenta("https://github.com/JairusSW/as-test"));
209
- // console.log(
210
- // "View the docs: " +
211
- // chalk.blue("https://docs.jairus.dev/as-test"),
212
- // );
201
+ chalk.blue("https://github.com/JairusSW/as-test"));
213
202
  }
214
203
  function isHelpFlag(value) {
215
204
  return value == "--help" || value == "-h";
@@ -238,6 +227,9 @@ function printCommandHelp(command) {
238
227
  process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
239
228
  process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
240
229
  process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
230
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
231
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
232
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
241
233
  process.stdout.write(" --list Preview resolved files/artifacts without building\n");
242
234
  process.stdout.write(" --list-modes Preview configured and selected mode names\n");
243
235
  process.stdout.write(" --help, -h Show this help\n");
@@ -249,7 +241,13 @@ function printCommandHelp(command) {
249
241
  process.stdout.write(chalk.bold("Flags:\n"));
250
242
  process.stdout.write(" --config <path> Use a specific config file\n");
251
243
  process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
252
- process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
244
+ process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
245
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
246
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
247
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
248
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
249
+ process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
250
+ process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
253
251
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
254
252
  process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
255
253
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
@@ -269,11 +267,24 @@ function printCommandHelp(command) {
269
267
  process.stdout.write(chalk.bold("Flags:\n"));
270
268
  process.stdout.write(" --config <path> Use a specific config file\n");
271
269
  process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
272
- process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
270
+ process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
271
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
272
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
273
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
274
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
275
+ process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
276
+ process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
273
277
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
274
278
  process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
275
279
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
276
280
  process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
281
+ process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
282
+ process.stdout.write(" --fuzz-runs <n> Override fuzz iteration count for this run\n");
283
+ process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
284
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
285
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
286
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
287
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
277
288
  process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
278
289
  process.stdout.write(" --tap Shortcut for --reporter tap\n");
279
290
  process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
@@ -283,11 +294,27 @@ function printCommandHelp(command) {
283
294
  process.stdout.write(" --help, -h Show this help\n");
284
295
  return;
285
296
  }
297
+ if (command == "fuzz") {
298
+ process.stdout.write(chalk.bold("Usage: ast fuzz [selectors...] [flags]\n\n"));
299
+ process.stdout.write("Build selected fuzz targets with bindings and execute them with generated inputs.\n\n");
300
+ process.stdout.write(chalk.bold("Flags:\n"));
301
+ process.stdout.write(" --config <path> Use a specific config file\n");
302
+ process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
303
+ process.stdout.write(" --runs <n> Override fuzz iteration count\n");
304
+ process.stdout.write(" --seed <n> Override fuzz seed\n");
305
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
306
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
307
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
308
+ process.stdout.write(" --list Preview resolved fuzz files without running\n");
309
+ process.stdout.write(" --list-modes Preview configured and selected mode names\n");
310
+ process.stdout.write(" --help, -h Show this help\n");
311
+ return;
312
+ }
286
313
  if (command == "init") {
287
314
  process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
288
315
  process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
289
316
  process.stdout.write(chalk.bold("Flags:\n"));
290
- process.stdout.write(" --target <wasi|bindings> Set build target\n");
317
+ process.stdout.write(" --target <wasi|bindings|web> Set build target\n");
291
318
  process.stdout.write(" --example <minimal|full|none> Set example template\n");
292
319
  process.stdout.write(" --install Install dependencies after scaffolding\n");
293
320
  process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
@@ -361,6 +388,9 @@ function resolveCommandArgs(rawArgs, command) {
361
388
  if (arg == "--tap") {
362
389
  continue;
363
390
  }
391
+ if (arg == "--fuzz") {
392
+ continue;
393
+ }
364
394
  if (arg == "--enable" || arg == "--disable") {
365
395
  i++;
366
396
  continue;
@@ -368,6 +398,28 @@ function resolveCommandArgs(rawArgs, command) {
368
398
  if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
369
399
  continue;
370
400
  }
401
+ if (arg == "--runs" ||
402
+ arg == "--seed" ||
403
+ arg == "--parallel" ||
404
+ arg == "--jobs" ||
405
+ arg == "--build-jobs" ||
406
+ arg == "--run-jobs" ||
407
+ arg == "--browser" ||
408
+ arg == "--fuzz-runs" ||
409
+ arg == "--fuzz-seed") {
410
+ i++;
411
+ continue;
412
+ }
413
+ if (arg.startsWith("--runs=") ||
414
+ arg.startsWith("--seed=") ||
415
+ arg.startsWith("--jobs=") ||
416
+ arg.startsWith("--build-jobs=") ||
417
+ arg.startsWith("--run-jobs=") ||
418
+ arg.startsWith("--browser=") ||
419
+ arg.startsWith("--fuzz-runs=") ||
420
+ arg.startsWith("--fuzz-seed=")) {
421
+ continue;
422
+ }
371
423
  if (arg.startsWith("-")) {
372
424
  continue;
373
425
  }
@@ -407,12 +459,51 @@ function resolveFeatureToggles(rawArgs, command) {
407
459
  }
408
460
  return out;
409
461
  }
462
+ function resolveFuzzOverrides(rawArgs, command) {
463
+ const out = {};
464
+ let seenCommand = false;
465
+ for (let i = 0; i < rawArgs.length; i++) {
466
+ const arg = rawArgs[i];
467
+ if (!seenCommand) {
468
+ if (arg == command)
469
+ seenCommand = true;
470
+ continue;
471
+ }
472
+ const direct = command == "fuzz"
473
+ ? {
474
+ runs: "--runs",
475
+ seed: "--seed",
476
+ }
477
+ : {
478
+ runs: "--fuzz-runs",
479
+ seed: "--fuzz-seed",
480
+ };
481
+ const runs = parseNumberFlag(rawArgs, i, direct.runs);
482
+ if (runs) {
483
+ out.runs = runs.number;
484
+ if (runs.consumeNext)
485
+ i++;
486
+ continue;
487
+ }
488
+ const seed = parseNumberFlag(rawArgs, i, direct.seed);
489
+ if (seed) {
490
+ out.seed = seed.number;
491
+ if (seed.consumeNext)
492
+ i++;
493
+ continue;
494
+ }
495
+ }
496
+ return out;
497
+ }
410
498
  function resolveListFlags(rawArgs, command) {
411
499
  const out = {
412
500
  list: false,
413
501
  listModes: false,
414
502
  };
415
- if (command !== "build" && command !== "run" && command !== "test") {
503
+ if (command !== "build" &&
504
+ command !== "run" &&
505
+ command !== "test" &&
506
+ command !== "fuzz") {
416
507
  return out;
417
508
  }
418
509
  let seenCommand = false;
@@ -430,6 +521,341 @@ function resolveListFlags(rawArgs, command) {
430
521
  }
431
522
  return out;
432
523
  }
524
+ function parseNumberFlag(rawArgs, index, flag) {
525
+ const arg = rawArgs[index];
526
+ if (arg == flag) {
527
+ const next = rawArgs[index + 1];
528
+ if (!next || next.startsWith("-")) {
529
+ throw new Error(`${flag} requires a numeric value`);
530
+ }
531
+ return {
532
+ key: flag,
533
+ number: parseIntegerFlag(flag, next),
534
+ consumeNext: true,
535
+ };
536
+ }
537
+ if (arg.startsWith(`${flag}=`)) {
538
+ return {
539
+ key: flag,
540
+ number: parseIntegerFlag(flag, arg.slice(flag.length + 1)),
541
+ consumeNext: false,
542
+ };
543
+ }
544
+ return null;
545
+ }
546
+ function parseStringFlag(rawArgs, index, flag) {
547
+ const arg = rawArgs[index];
548
+ if (arg == flag) {
549
+ const next = rawArgs[index + 1];
550
+ if (!next || next.startsWith("-")) {
551
+ throw new Error(`${flag} requires a value`);
552
+ }
553
+ return { key: flag, value: next, consumeNext: true };
554
+ }
555
+ if (arg.startsWith(`${flag}=`)) {
556
+ const value = arg.slice(flag.length + 1);
557
+ if (!value.length) {
558
+ throw new Error(`${flag} requires a value`);
559
+ }
560
+ return { key: flag, value, consumeNext: false };
561
+ }
562
+ return null;
563
+ }
564
+ function resolveBrowserOverride(rawArgs, command) {
565
+ let seenCommand = false;
566
+ for (let i = 0; i < rawArgs.length; i++) {
567
+ const arg = rawArgs[i];
568
+ if (!seenCommand) {
569
+ if (arg == command)
570
+ seenCommand = true;
571
+ continue;
572
+ }
573
+ const parsed = parseStringFlag(rawArgs, i, "--browser");
574
+ if (!parsed)
575
+ continue;
576
+ return parsed.value.trim() || undefined;
577
+ }
578
+ return undefined;
579
+ }
580
+ function resolveJobs(rawArgs, command) {
581
+ let seenCommand = false;
582
+ let parallel = false;
583
+ for (let i = 0; i < rawArgs.length; i++) {
584
+ const arg = rawArgs[i];
585
+ if (!seenCommand) {
586
+ if (arg == command)
587
+ seenCommand = true;
588
+ continue;
589
+ }
590
+ if (arg == "--parallel") {
591
+ parallel = true;
592
+ continue;
593
+ }
594
+ const parsed = parseNumberFlag(rawArgs, i, "--jobs");
595
+ if (!parsed)
596
+ continue;
597
+ if (parsed.number < 1) {
598
+ throw new Error("--jobs requires a positive integer");
599
+ }
600
+ return parsed.number;
601
+ }
602
+ return parallel ? 0 : 1;
603
+ }
604
+ function resolveBuildParallelJobs(rawArgs) {
605
+ const baseJobs = resolveJobs(rawArgs, "build");
606
+ let buildJobs = baseJobs;
607
+ let seenCommand = false;
608
+ for (let i = 0; i < rawArgs.length; i++) {
609
+ const arg = rawArgs[i];
610
+ if (!seenCommand) {
611
+ if (arg == "build")
612
+ seenCommand = true;
613
+ continue;
614
+ }
615
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
616
+ if (buildParsed) {
617
+ if (buildParsed.number < 1) {
618
+ throw new Error("--build-jobs requires a positive integer");
619
+ }
620
+ buildJobs = buildParsed.number;
621
+ continue;
622
+ }
623
+ }
624
+ const jobs = Math.max(baseJobs, buildJobs);
625
+ return { jobs, buildJobs };
626
+ }
627
+ function resolveParallelJobs(rawArgs, command) {
628
+ const baseJobs = resolveJobs(rawArgs, command);
629
+ let buildJobs = baseJobs;
630
+ let runJobs = baseJobs;
631
+ let seenCommand = false;
632
+ for (let i = 0; i < rawArgs.length; i++) {
633
+ const arg = rawArgs[i];
634
+ if (!seenCommand) {
635
+ if (arg == command)
636
+ seenCommand = true;
637
+ continue;
638
+ }
639
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
640
+ if (buildParsed) {
641
+ if (buildParsed.number < 1) {
642
+ throw new Error("--build-jobs requires a positive integer");
643
+ }
644
+ buildJobs = buildParsed.number;
645
+ continue;
646
+ }
647
+ const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
648
+ if (runParsed) {
649
+ if (runParsed.number < 1) {
650
+ throw new Error("--run-jobs requires a positive integer");
651
+ }
652
+ runJobs = runParsed.number;
653
+ continue;
654
+ }
655
+ }
656
+ const jobs = Math.max(baseJobs, buildJobs, runJobs);
657
+ return { jobs, buildJobs, runJobs };
658
+ }
659
+ function resolveFuzzParallelJobs(rawArgs) {
660
+ const baseJobs = resolveJobs(rawArgs, "fuzz");
661
+ let buildJobs = baseJobs;
662
+ let runJobs = baseJobs;
663
+ let seenCommand = false;
664
+ for (let i = 0; i < rawArgs.length; i++) {
665
+ const arg = rawArgs[i];
666
+ if (!seenCommand) {
667
+ if (arg == "fuzz")
668
+ seenCommand = true;
669
+ continue;
670
+ }
671
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
672
+ if (buildParsed) {
673
+ if (buildParsed.number < 1) {
674
+ throw new Error("--build-jobs requires a positive integer");
675
+ }
676
+ buildJobs = buildParsed.number;
677
+ continue;
678
+ }
679
+ const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
680
+ if (runParsed) {
681
+ if (runParsed.number < 1) {
682
+ throw new Error("--run-jobs requires a positive integer");
683
+ }
684
+ runJobs = runParsed.number;
685
+ continue;
686
+ }
687
+ }
688
+ const jobs = Math.max(baseJobs, buildJobs, runJobs);
689
+ return { jobs, buildJobs, runJobs };
690
+ }
691
+ function resolveEffectiveParallelJobs(settings, totalFiles) {
692
+ if (settings.jobs > 0) {
693
+ return {
694
+ jobs: Math.max(settings.jobs, settings.buildJobs, settings.runJobs),
695
+ buildJobs: settings.buildJobs > 0 ? settings.buildJobs : settings.jobs,
696
+ runJobs: settings.runJobs > 0 ? settings.runJobs : settings.jobs,
697
+ };
698
+ }
699
+ const autoJobs = resolveAutoJobs(totalFiles);
700
+ return {
701
+ jobs: Math.max(autoJobs, settings.buildJobs, settings.runJobs),
702
+ buildJobs: settings.buildJobs > 0 ? settings.buildJobs : autoJobs,
703
+ runJobs: settings.runJobs > 0 ? settings.runJobs : autoJobs,
704
+ };
705
+ }
706
+ function resolveAutoJobs(totalFiles) {
707
+ const cpuCount = typeof availableParallelism == "function"
708
+ ? availableParallelism()
709
+ : cpus().length;
710
+ const cpuBudget = Math.max(1, cpuCount - 1);
711
+ if (totalFiles <= 1)
712
+ return 1;
713
+ if (totalFiles <= 4)
714
+ return Math.min(2, cpuBudget, totalFiles);
715
+ if (totalFiles <= 12)
716
+ return Math.min(3, cpuBudget);
717
+ if (totalFiles <= 32)
718
+ return Math.min(4, cpuBudget);
719
+ return Math.min(Math.max(4, Math.ceil(totalFiles / 12)), cpuBudget);
720
+ }
721
+ function createBufferedStream() {
722
+ const chunks = [];
723
+ return {
724
+ isTTY: false,
725
+ write(chunk) {
726
+ chunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
727
+ return true;
728
+ },
729
+ read() {
730
+ return chunks.join("");
731
+ },
732
+ };
733
+ }
734
+ async function createBufferedReporter(configPath, modeName) {
735
+ const stream = createBufferedStream();
736
+ const session = await createRunReporter(configPath, undefined, modeName, {
737
+ stdout: stream,
738
+ stderr: stream,
739
+ });
740
+ return {
741
+ reporter: session.reporter,
742
+ reporterKind: session.reporterKind,
743
+ runtimeName: session.runtimeName,
744
+ output: () => stream.read(),
745
+ };
746
+ }
747
+ async function runOrderedPool(items, jobs, worker) {
748
+ const width = Math.max(1, jobs);
749
+ let nextIndex = 0;
750
+ let firstError = null;
751
+ async function runWorker() {
752
+ while (true) {
753
+ if (firstError != null)
754
+ return;
755
+ const index = nextIndex++;
756
+ if (index >= items.length)
757
+ return;
758
+ try {
759
+ await worker(items[index], index);
760
+ }
761
+ catch (error) {
762
+ if (firstError == null)
763
+ firstError = error;
764
+ return;
765
+ }
766
+ }
767
+ }
768
+ await Promise.all(Array.from({ length: Math.min(width, items.length) }, () => runWorker()));
769
+ if (firstError != null)
770
+ throw firstError;
771
+ }
772
+ function createAsyncLimiter(limit) {
773
+ const width = Math.max(1, limit);
774
+ let active = 0;
775
+ const queue = [];
776
+ return async function withLimit(task) {
777
+ if (active >= width) {
778
+ await new Promise((resolve) => queue.push(resolve));
779
+ }
780
+ active++;
781
+ try {
782
+ return await task();
783
+ }
784
+ finally {
785
+ active--;
786
+ const next = queue.shift();
787
+ if (next)
788
+ next();
789
+ }
790
+ };
791
+ }
792
+ function canRewriteParallelQueue() {
793
+ return Boolean(process.stdout.isTTY);
794
+ }
795
+ class ParallelQueueDisplay {
796
+ constructor(showStartLines) {
797
+ this.showStartLines = showStartLines;
798
+ this.active = new Map();
799
+ this.renderedLines = 0;
800
+ this.enabled = showStartLines && canRewriteParallelQueue();
801
+ }
802
+ start(file) {
803
+ const token = Symbol(file);
804
+ if (!this.showStartLines)
805
+ return token;
806
+ const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
807
+ if (!this.enabled)
808
+ return token;
809
+ this.clear();
810
+ this.active.set(token, line);
811
+ this.render();
812
+ return token;
813
+ }
814
+ complete(token, output) {
815
+ if (!this.showStartLines || !this.enabled) {
816
+ process.stdout.write(output);
817
+ return;
818
+ }
819
+ this.clear();
820
+ process.stdout.write(output);
821
+ this.active.delete(token);
822
+ this.render();
823
+ }
824
+ flush() {
825
+ if (!this.enabled)
826
+ return;
827
+ this.clear();
828
+ }
829
+ clear() {
830
+ if (!this.renderedLines)
831
+ return;
832
+ for (let i = 0; i < this.renderedLines; i++) {
833
+ process.stdout.write("\r\x1b[2K");
834
+ if (i < this.renderedLines - 1)
835
+ process.stdout.write("\x1b[1A");
836
+ }
837
+ this.renderedLines = 0;
838
+ }
839
+ render() {
840
+ if (!this.enabled)
841
+ return;
842
+ const lines = Array.from(this.active.values());
843
+ if (!lines.length)
844
+ return;
845
+ process.stdout.write(lines.join("\n"));
846
+ this.renderedLines = lines.length;
847
+ }
848
+ }
849
+ function renderQueuedFileStart(display, file) {
850
+ return display.start(file);
851
+ }
852
+ function parseIntegerFlag(flag, value) {
853
+ const parsed = Number(value);
854
+ if (!Number.isFinite(parsed) || parsed < 0) {
855
+ throw new Error(`${flag} requires a non-negative integer`);
856
+ }
857
+ return Math.floor(parsed);
858
+ }
433
859
  function applyFeatureToggle(out, rawFeature, enabled) {
434
860
  const key = rawFeature.trim().toLowerCase();
435
861
  if (key == "coverage") {
@@ -456,26 +882,32 @@ function resolveCommandTokens(rawArgs, command) {
456
882
  }
457
883
  return values;
458
884
  }
459
- async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
885
+ async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
460
886
  const files = await resolveSelectedFiles(configPath, selectors);
461
887
  if (!files.length) {
462
- throw await buildNoTestFilesMatchedError(configPath, selectors);
888
+ if (!allowNoSpecFiles) {
889
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
890
+ }
463
891
  }
464
892
  const reporterSession = await createRunReporter(configPath, undefined, modeName);
465
- const reporter = reporterSession.reporter;
893
+ const reporter = reporterOverride ?? reporterSession.reporter;
466
894
  const snapshotEnabled = runFlags.snapshot !== false;
467
895
  reporter.onRunStart?.({
468
896
  runtimeName: reporterSession.runtimeName,
469
897
  clean: runFlags.clean,
470
898
  verbose: runFlags.verbose,
471
899
  snapshotEnabled,
472
- updateSnapshots: runFlags.updateSnapshots,
900
+ createSnapshots: runFlags.createSnapshots,
473
901
  });
474
902
  const results = [];
475
903
  let failed = false;
904
+ const buildIntervals = [];
476
905
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
477
906
  for (const file of files) {
907
+ const buildStartedAt = Date.now();
478
908
  await build(configPath, [file], modeName, buildFeatureToggles);
909
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
910
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
479
911
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
480
912
  const result = await run(runFlags, configPath, [file], false, {
481
913
  reporter,
@@ -483,6 +915,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
483
915
  emitRunComplete: false,
484
916
  logFileName: `test.${artifactKey}.log.json`,
485
917
  coverageFileName: `coverage.${artifactKey}.log.json`,
918
+ buildCommand: formatBuildInvocation(buildInvocation),
486
919
  modeName,
487
920
  });
488
921
  results.push(result);
@@ -491,38 +924,109 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
491
924
  }
492
925
  const summary = aggregateRunResults(results);
493
926
  summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
494
- reporter.onRunComplete?.({
495
- clean: runFlags.clean,
496
- snapshotEnabled,
497
- showCoverage: runFlags.showCoverage,
498
- snapshotSummary: summary.snapshotSummary,
499
- coverageSummary: summary.coverageSummary,
500
- stats: summary.stats,
501
- reports: summary.reports,
502
- modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
503
- });
504
- return failed;
927
+ if (emitRunComplete) {
928
+ reporter.onRunComplete?.({
929
+ clean: runFlags.clean,
930
+ snapshotEnabled,
931
+ showCoverage: runFlags.showCoverage,
932
+ buildTime: getMergedIntervalDuration(buildIntervals),
933
+ snapshotSummary: summary.snapshotSummary,
934
+ coverageSummary: summary.coverageSummary,
935
+ stats: summary.stats,
936
+ reports: summary.reports,
937
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
938
+ });
939
+ reporter.flush?.();
940
+ }
941
+ return {
942
+ failed,
943
+ summary: {
944
+ buildTime: getMergedIntervalDuration(buildIntervals),
945
+ snapshotSummary: summary.snapshotSummary,
946
+ coverageSummary: summary.coverageSummary,
947
+ stats: summary.stats,
948
+ reports: summary.reports,
949
+ },
950
+ };
505
951
  }
506
- async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
952
+ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles, parallel) {
953
+ const files = await resolveSelectedFiles(configPath, selectors);
954
+ if (!files.length) {
955
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
956
+ }
957
+ const effective = resolveEffectiveParallelJobs({
958
+ jobs: parallel.jobs,
959
+ buildJobs: parallel.buildJobs,
960
+ runJobs: parallel.buildJobs,
961
+ }, files.length);
962
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
963
+ const loadedConfig = loadConfig(resolvedConfigPath, true);
964
+ const allStartedAt = Date.now();
965
+ let builtCount = 0;
507
966
  for (const modeName of modes) {
508
- await build(configPath, selectors, modeName, buildFeatureToggles);
967
+ const startedAt = Date.now();
968
+ if (effective.buildJobs > 1) {
969
+ const pool = new BuildWorkerPool(effective.buildJobs);
970
+ try {
971
+ await runOrderedPool(files, effective.buildJobs, async (file) => {
972
+ await pool.buildFileMode({
973
+ configPath,
974
+ file,
975
+ modeName,
976
+ featureToggles: buildFeatureToggles,
977
+ });
978
+ });
979
+ }
980
+ finally {
981
+ await pool.close();
982
+ }
983
+ }
984
+ else {
985
+ await build(configPath, selectors, modeName, buildFeatureToggles);
986
+ }
987
+ builtCount += files.length;
988
+ const active = applyMode(loadedConfig, modeName).config;
989
+ 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`);
509
990
  }
991
+ process.stdout.write(`${chalk.bold("Summary:")} built ${builtCount} file(s) across ${modes.length || 1} mode(s) in ${formatTime(Date.now() - allStartedAt)}\n`);
510
992
  }
511
993
  async function runRuntimeModes(runFlags, configPath, selectors, modes) {
512
- const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
994
+ await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
995
+ const modeSummaryTotal = Math.max(modes.length, 1);
513
996
  const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
997
+ const effectiveRunFlags = {
998
+ ...runFlags,
999
+ ...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
1000
+ };
1001
+ if (effectiveRunFlags.jobs > 1) {
1002
+ if (modes.length > 1) {
1003
+ const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
1004
+ process.exit(failed ? 1 : 0);
1005
+ return;
1006
+ }
1007
+ let failed = false;
1008
+ for (const modeName of modes) {
1009
+ const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal);
1010
+ if (result)
1011
+ failed = true;
1012
+ }
1013
+ process.exit(failed ? 1 : 0);
1014
+ return;
1015
+ }
514
1016
  if (modes.length > 1) {
515
- const failed = await runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
1017
+ const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
516
1018
  process.exit(failed ? 1 : 0);
517
1019
  return;
518
1020
  }
519
1021
  let failed = false;
1022
+ const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
520
1023
  for (const modeName of modes) {
521
- const result = await run(runFlags, configPath, selectors, false, {
1024
+ const result = await run(effectiveRunFlags, configPath, selectors, false, {
522
1025
  modeName,
523
1026
  modeSummaryTotal,
524
1027
  modeSummaryExecuted: 1,
525
1028
  fileSummaryTotal,
1029
+ buildCommandsByFile,
526
1030
  });
527
1031
  if (result.failed)
528
1032
  failed = true;
@@ -542,7 +1046,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
542
1046
  clean: runFlags.clean,
543
1047
  verbose: runFlags.verbose,
544
1048
  snapshotEnabled,
545
- updateSnapshots: runFlags.updateSnapshots,
1049
+ createSnapshots: runFlags.createSnapshots,
546
1050
  });
547
1051
  const silentReporter = {};
548
1052
  const allResults = [];
@@ -558,6 +1062,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
558
1062
  passed: false,
559
1063
  }));
560
1064
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1065
+ const buildIntervals = [];
561
1066
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
562
1067
  const file = files[fileIndex];
563
1068
  const fileName = path.basename(file);
@@ -569,6 +1074,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
569
1074
  for (let i = 0; i < modes.length; i++) {
570
1075
  const modeName = modes[i];
571
1076
  try {
1077
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
572
1078
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
573
1079
  const result = await run(runFlags, configPath, [file], false, {
574
1080
  reporter: silentReporter,
@@ -577,6 +1083,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
577
1083
  emitRunComplete: false,
578
1084
  logFileName: `run.${artifactKey}.log.json`,
579
1085
  coverageFileName: `coverage.${artifactKey}.log.json`,
1086
+ buildCommand: formatBuildInvocation(buildInvocation),
580
1087
  modeName,
581
1088
  });
582
1089
  modeTimes[i] = formatMatrixModeTime(result.stats.time);
@@ -612,6 +1119,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
612
1119
  clean: runFlags.clean,
613
1120
  snapshotEnabled,
614
1121
  showCoverage: runFlags.showCoverage,
1122
+ buildTime: 0,
615
1123
  snapshotSummary: summary.snapshotSummary,
616
1124
  coverageSummary: summary.coverageSummary,
617
1125
  stats: summary.stats,
@@ -620,26 +1128,75 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
620
1128
  });
621
1129
  return allResults.some((result) => result.failed);
622
1130
  }
623
- async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
624
- const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
625
- const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
1131
+ async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
1132
+ await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
1133
+ const modeSummaryTotal = Math.max(modes.length, 1);
1134
+ const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
1135
+ const effectiveRunFlags = {
1136
+ ...runFlags,
1137
+ ...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
1138
+ };
1139
+ if (effectiveRunFlags.jobs > 1) {
1140
+ if (modes.length > 1) {
1141
+ const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
1142
+ process.exit(failed ? 1 : 0);
1143
+ return;
1144
+ }
1145
+ let failed = false;
1146
+ for (const modeName of modes) {
1147
+ const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
1148
+ if (modeFailed)
1149
+ failed = true;
1150
+ }
1151
+ process.exit(failed ? 1 : 0);
1152
+ return;
1153
+ }
626
1154
  if (modes.length > 1) {
627
- const failed = await runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal);
1155
+ const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
628
1156
  process.exit(failed ? 1 : 0);
629
1157
  return;
630
1158
  }
631
1159
  let failed = false;
632
1160
  for (const modeName of modes) {
633
- const modeFailed = await runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName);
634
- if (modeFailed)
1161
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1162
+ const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
1163
+ if (modeResult.failed)
635
1164
  failed = true;
1165
+ if (fuzzEnabled) {
1166
+ if (reporterSession.reporterKind == "default") {
1167
+ process.stdout.write("\n");
1168
+ }
1169
+ const fuzzResults = await runFuzzMatrixResults(configPath, selectors, [modeName], fuzzOverrides, reporterSession.reporter);
1170
+ if (fuzzResults.some(hasFuzzFailures))
1171
+ failed = true;
1172
+ reporterSession.reporter.onRunComplete?.({
1173
+ clean: runFlags.clean,
1174
+ snapshotEnabled: effectiveRunFlags.snapshot !== false,
1175
+ showCoverage: effectiveRunFlags.showCoverage,
1176
+ buildTime: modeResult.summary.buildTime +
1177
+ getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
1178
+ snapshotSummary: modeResult.summary.snapshotSummary,
1179
+ coverageSummary: modeResult.summary.coverageSummary,
1180
+ stats: modeResult.summary.stats,
1181
+ reports: modeResult.summary.reports,
1182
+ fuzzSummary: summarizeFuzzExecutions(fuzzResults),
1183
+ modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
1184
+ });
1185
+ reporterSession.reporter.flush?.();
1186
+ }
636
1187
  }
637
1188
  process.exit(failed ? 1 : 0);
638
1189
  }
639
- async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
1190
+ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
640
1191
  const files = await resolveSelectedFiles(configPath, selectors);
641
1192
  if (!files.length) {
642
- throw await buildNoTestFilesMatchedError(configPath, selectors);
1193
+ if (!fuzzEnabled) {
1194
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1195
+ }
1196
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1197
+ if (!fuzzFiles.length) {
1198
+ throw await buildNoTestFilesMatchedError(configPath, selectors, true);
1199
+ }
643
1200
  }
644
1201
  const reporterSession = await createRunReporter(configPath);
645
1202
  const reporter = reporterSession.reporter;
@@ -649,7 +1206,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
649
1206
  clean: runFlags.clean,
650
1207
  verbose: runFlags.verbose,
651
1208
  snapshotEnabled,
652
- updateSnapshots: runFlags.updateSnapshots,
1209
+ createSnapshots: runFlags.createSnapshots,
653
1210
  });
654
1211
  const silentReporter = {};
655
1212
  const allResults = [];
@@ -665,6 +1222,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
665
1222
  passed: false,
666
1223
  }));
667
1224
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1225
+ const buildIntervals = [];
668
1226
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
669
1227
  const file = files[fileIndex];
670
1228
  const fileName = path.basename(file);
@@ -676,7 +1234,10 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
676
1234
  for (let i = 0; i < modes.length; i++) {
677
1235
  const modeName = modes[i];
678
1236
  try {
1237
+ const buildStartedAt = Date.now();
679
1238
  await build(configPath, [file], modeName, buildFeatureToggles);
1239
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1240
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
680
1241
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
681
1242
  const result = await run(runFlags, configPath, [file], false, {
682
1243
  reporter: silentReporter,
@@ -685,6 +1246,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
685
1246
  emitRunComplete: false,
686
1247
  logFileName: `test.${artifactKey}.log.json`,
687
1248
  coverageFileName: `coverage.${artifactKey}.log.json`,
1249
+ buildCommand: formatBuildInvocation(buildInvocation),
688
1250
  modeName,
689
1251
  });
690
1252
  modeTimes[i] = formatMatrixModeTime(result.stats.time);
@@ -716,53 +1278,585 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
716
1278
  }
717
1279
  const summary = aggregateRunResults(allResults);
718
1280
  summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1281
+ let failed = allResults.some((result) => result.failed);
1282
+ let fuzzSummary;
1283
+ if (fuzzEnabled) {
1284
+ if (reporterSession.reporterKind == "default") {
1285
+ process.stdout.write("\n");
1286
+ }
1287
+ const fuzzResults = await runFuzzMatrixResults(configPath, selectors, modes, fuzzOverrides, reporter);
1288
+ if (fuzzResults.some(hasFuzzFailures))
1289
+ failed = true;
1290
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1291
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1292
+ }
719
1293
  reporter.onRunComplete?.({
720
1294
  clean: runFlags.clean,
721
1295
  snapshotEnabled,
722
1296
  showCoverage: runFlags.showCoverage,
1297
+ buildTime: getMergedIntervalDuration(buildIntervals),
723
1298
  snapshotSummary: summary.snapshotSummary,
724
1299
  coverageSummary: summary.coverageSummary,
725
1300
  stats: summary.stats,
726
1301
  reports: summary.reports,
1302
+ fuzzSummary,
727
1303
  modeSummary: buildModeSummary(modeState, modeSummaryTotal),
728
1304
  });
729
- return allResults.some((result) => result.failed);
730
- }
731
- function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
732
- const verdict = resolveMatrixVerdict(results);
733
- const badge = verdict == "fail"
734
- ? chalk.bgRed.white(" FAIL ")
735
- : verdict == "ok"
736
- ? chalk.bgGreenBright.black(" PASS ")
737
- : chalk.bgBlackBright.white(" SKIP ");
738
- const avg = formatMatrixAverageTime(results);
739
- const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
740
- const suffix = showPerModeTimes
741
- ? ` ${chalk.dim(`(${modes.join(",")})`)}`
742
- : "";
743
- const line = `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
744
- if (liveMatrix)
745
- clearLiveLine();
746
- process.stdout.write(line + "\n");
747
- }
748
- function resolveMatrixVerdict(results) {
749
- if (results.some((result) => result.failed))
750
- return "fail";
751
- const hasPass = results.some((result) => result.stats.passedFiles > 0);
752
- if (hasPass)
753
- return "ok";
754
- return "skip";
755
- }
756
- function canRewriteStdout() {
757
- return Boolean(process.stdout.isTTY);
1305
+ reporter.flush?.();
1306
+ return failed;
758
1307
  }
759
- function clearLiveLine() {
760
- if (!canRewriteStdout())
1308
+ async function runFuzzModes(configPath, selectors, modes, rawArgs) {
1309
+ const overrides = resolveFuzzOverrides(rawArgs, "fuzz");
1310
+ const parallelSettings = resolveFuzzParallelJobs(rawArgs);
1311
+ const clean = rawArgs.includes("--clean");
1312
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1313
+ const { jobs, buildJobs, runJobs } = resolveEffectiveParallelJobs(parallelSettings, fuzzFiles.length);
1314
+ if (jobs > 1) {
1315
+ const results = await runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean);
1316
+ const reporterSession = await createRunReporter(configPath);
1317
+ reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
1318
+ reporterSession.reporter.flush?.();
1319
+ process.exit(results.some(hasFuzzFailures) ? 1 : 0);
761
1320
  return;
762
- process.stdout.write("\r\x1b[2K");
1321
+ }
1322
+ const reporterSession = await createRunReporter(configPath);
1323
+ const results = await runFuzzMatrixResults(configPath, selectors, modes, overrides, reporterSession.reporter);
1324
+ reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
1325
+ reporterSession.reporter.flush?.();
1326
+ process.exit(results.some(hasFuzzFailures) ? 1 : 0);
763
1327
  }
764
- function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
765
- if (!canRewriteStdout())
1328
+ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal) {
1329
+ const files = await resolveSelectedFiles(configPath, selectors);
1330
+ if (!files.length) {
1331
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1332
+ }
1333
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1334
+ const reporter = reporterSession.reporter;
1335
+ const snapshotEnabled = runFlags.snapshot !== false;
1336
+ reporter.onRunStart?.({
1337
+ runtimeName: reporterSession.runtimeName,
1338
+ clean: runFlags.clean,
1339
+ verbose: runFlags.verbose,
1340
+ snapshotEnabled,
1341
+ createSnapshots: runFlags.createSnapshots,
1342
+ });
1343
+ const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
1344
+ const results = new Array(files.length);
1345
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1346
+ const runLimit = createAsyncLimiter(runFlags.runJobs);
1347
+ const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
1348
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1349
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1350
+ const buffered = await createBufferedReporter(configPath, modeName);
1351
+ const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
1352
+ reporter: buffered.reporter,
1353
+ reporterKind: buffered.reporterKind,
1354
+ modeName,
1355
+ emitRunComplete: false,
1356
+ fileSummaryTotal: 1,
1357
+ modeSummaryTotal,
1358
+ modeSummaryExecuted: 1,
1359
+ buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
1360
+ }));
1361
+ buffered.reporter.flush?.();
1362
+ results[index] = result;
1363
+ queueDisplay.complete(token, buffered.output());
1364
+ });
1365
+ queueDisplay.flush();
1366
+ const summary = aggregateRunResults(results);
1367
+ summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
1368
+ reporter.onRunComplete?.({
1369
+ clean: runFlags.clean,
1370
+ snapshotEnabled,
1371
+ showCoverage: runFlags.showCoverage,
1372
+ buildTime: 0,
1373
+ snapshotSummary: summary.snapshotSummary,
1374
+ coverageSummary: summary.coverageSummary,
1375
+ stats: summary.stats,
1376
+ reports: summary.reports,
1377
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
1378
+ });
1379
+ reporter.flush?.();
1380
+ return results.some((result) => result.failed);
1381
+ }
1382
+ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
1383
+ const files = await resolveSelectedFiles(configPath, selectors);
1384
+ if (!files.length) {
1385
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1386
+ }
1387
+ const reporterSession = await createRunReporter(configPath);
1388
+ const reporter = reporterSession.reporter;
1389
+ const snapshotEnabled = runFlags.snapshot !== false;
1390
+ reporter.onRunStart?.({
1391
+ runtimeName: reporterSession.runtimeName,
1392
+ clean: runFlags.clean,
1393
+ verbose: runFlags.verbose,
1394
+ snapshotEnabled,
1395
+ createSnapshots: runFlags.createSnapshots,
1396
+ });
1397
+ const silentReporter = {};
1398
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
1399
+ const showPerModeTimes = Boolean(runFlags.verbose);
1400
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1401
+ const ordered = new Array(files.length);
1402
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1403
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1404
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1405
+ const buildIntervals = [];
1406
+ try {
1407
+ await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1408
+ const fileName = path.basename(file);
1409
+ const token = renderQueuedFileStart(queueDisplay, fileName);
1410
+ const fileResults = [];
1411
+ const modeTimes = modes.map(() => "...");
1412
+ for (let i = 0; i < modes.length; i++) {
1413
+ const modeName = modes[i];
1414
+ const buildStartedAt = Date.now();
1415
+ await buildPool.buildFileMode({
1416
+ configPath,
1417
+ file,
1418
+ modeName,
1419
+ });
1420
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1421
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
1422
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1423
+ const result = await run(runFlags, configPath, [file], false, {
1424
+ reporter: silentReporter,
1425
+ reporterKind: "default",
1426
+ emitRunStart: false,
1427
+ emitRunComplete: false,
1428
+ logFileName: `run.${artifactKey}.log.json`,
1429
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1430
+ buildCommand: formatBuildInvocation(buildInvocation),
1431
+ modeName,
1432
+ });
1433
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
1434
+ fileResults.push(result);
1435
+ }
1436
+ ordered[fileIndex] = { fileName, fileResults, modeTimes };
1437
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1438
+ });
1439
+ }
1440
+ finally {
1441
+ await buildPool.close();
1442
+ }
1443
+ queueDisplay.flush();
1444
+ const allResults = [];
1445
+ const modeState = modes.map(() => ({ failed: false, passed: false }));
1446
+ const fileState = files.map(() => ({ failed: false, passed: false }));
1447
+ for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
1448
+ const fileResults = ordered[fileIndex].fileResults;
1449
+ for (let i = 0; i < fileResults.length; i++) {
1450
+ const result = fileResults[i];
1451
+ allResults.push(result);
1452
+ if (result.failed)
1453
+ modeState[i].failed = true;
1454
+ else if (result.stats.passedFiles > 0)
1455
+ modeState[i].passed = true;
1456
+ }
1457
+ const verdict = resolveMatrixVerdict(fileResults);
1458
+ if (verdict == "fail")
1459
+ fileState[fileIndex].failed = true;
1460
+ else if (verdict == "ok")
1461
+ fileState[fileIndex].passed = true;
1462
+ }
1463
+ const summary = aggregateRunResults(allResults);
1464
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1465
+ reporter.onRunComplete?.({
1466
+ clean: runFlags.clean,
1467
+ snapshotEnabled,
1468
+ showCoverage: runFlags.showCoverage,
1469
+ buildTime: getMergedIntervalDuration(buildIntervals),
1470
+ snapshotSummary: summary.snapshotSummary,
1471
+ coverageSummary: summary.coverageSummary,
1472
+ stats: summary.stats,
1473
+ reports: summary.reports,
1474
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
1475
+ });
1476
+ reporter.flush?.();
1477
+ return allResults.some((result) => result.failed);
1478
+ }
1479
+ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
1480
+ const files = await resolveSelectedFiles(configPath, selectors);
1481
+ if (!files.length && !fuzzEnabled) {
1482
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1483
+ }
1484
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1485
+ const reporter = reporterSession.reporter;
1486
+ const snapshotEnabled = runFlags.snapshot !== false;
1487
+ reporter.onRunStart?.({
1488
+ runtimeName: reporterSession.runtimeName,
1489
+ clean: runFlags.clean,
1490
+ verbose: runFlags.verbose,
1491
+ snapshotEnabled,
1492
+ createSnapshots: runFlags.createSnapshots,
1493
+ });
1494
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1495
+ const results = new Array(files.length);
1496
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1497
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1498
+ const buildIntervals = [];
1499
+ if (files.length) {
1500
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1501
+ try {
1502
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1503
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1504
+ const buildStartedAt = Date.now();
1505
+ await buildPool.buildFileMode({
1506
+ configPath,
1507
+ file,
1508
+ modeName,
1509
+ featureToggles: buildFeatureToggles,
1510
+ });
1511
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1512
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1513
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1514
+ const buffered = await createBufferedReporter(configPath, modeName);
1515
+ const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
1516
+ reporter: buffered.reporter,
1517
+ reporterKind: buffered.reporterKind,
1518
+ emitRunComplete: false,
1519
+ logFileName: `test.${artifactKey}.log.json`,
1520
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1521
+ buildCommand: formatBuildInvocation(buildInvocation),
1522
+ modeName,
1523
+ });
1524
+ buffered.reporter.flush?.();
1525
+ results[index] = result;
1526
+ queueDisplay.complete(token, buffered.output());
1527
+ });
1528
+ }
1529
+ finally {
1530
+ await buildPool.close();
1531
+ }
1532
+ }
1533
+ queueDisplay.flush();
1534
+ const runResults = results.filter(Boolean);
1535
+ const summary = aggregateRunResults(runResults);
1536
+ summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
1537
+ let failed = runResults.some((result) => result.failed);
1538
+ let fuzzSummary;
1539
+ if (fuzzEnabled) {
1540
+ if (reporterSession.reporterKind == "default") {
1541
+ process.stdout.write("\n");
1542
+ }
1543
+ const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
1544
+ if (fuzzResults.some(hasFuzzFailures))
1545
+ failed = true;
1546
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1547
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1548
+ }
1549
+ reporter.onRunComplete?.({
1550
+ clean: runFlags.clean,
1551
+ snapshotEnabled,
1552
+ showCoverage: runFlags.showCoverage,
1553
+ buildTime: getMergedIntervalDuration(buildIntervals),
1554
+ snapshotSummary: summary.snapshotSummary,
1555
+ coverageSummary: summary.coverageSummary,
1556
+ stats: summary.stats,
1557
+ reports: summary.reports,
1558
+ fuzzSummary,
1559
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
1560
+ });
1561
+ reporter.flush?.();
1562
+ return failed;
1563
+ }
1564
+ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
1565
+ const files = await resolveSelectedFiles(configPath, selectors);
1566
+ if (!files.length) {
1567
+ if (!fuzzEnabled) {
1568
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1569
+ }
1570
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1571
+ if (!fuzzFiles.length) {
1572
+ throw await buildNoTestFilesMatchedError(configPath, selectors, true);
1573
+ }
1574
+ }
1575
+ const reporterSession = await createRunReporter(configPath);
1576
+ const reporter = reporterSession.reporter;
1577
+ const snapshotEnabled = runFlags.snapshot !== false;
1578
+ reporter.onRunStart?.({
1579
+ runtimeName: reporterSession.runtimeName,
1580
+ clean: runFlags.clean,
1581
+ verbose: runFlags.verbose,
1582
+ snapshotEnabled,
1583
+ createSnapshots: runFlags.createSnapshots,
1584
+ });
1585
+ const silentReporter = {};
1586
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
1587
+ const showPerModeTimes = Boolean(runFlags.verbose);
1588
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1589
+ const ordered = new Array(files.length);
1590
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1591
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1592
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1593
+ const buildIntervals = [];
1594
+ try {
1595
+ await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1596
+ const fileName = path.basename(file);
1597
+ const token = renderQueuedFileStart(queueDisplay, fileName);
1598
+ const fileResults = [];
1599
+ const modeTimes = modes.map(() => "...");
1600
+ for (let i = 0; i < modes.length; i++) {
1601
+ const modeName = modes[i];
1602
+ const buildStartedAt = Date.now();
1603
+ await buildPool.buildFileMode({
1604
+ configPath,
1605
+ file,
1606
+ modeName,
1607
+ featureToggles: buildFeatureToggles,
1608
+ });
1609
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1610
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1611
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1612
+ const result = await run(runFlags, configPath, [file], false, {
1613
+ reporter: silentReporter,
1614
+ reporterKind: "default",
1615
+ emitRunStart: false,
1616
+ emitRunComplete: false,
1617
+ logFileName: `test.${artifactKey}.log.json`,
1618
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1619
+ buildCommand: formatBuildInvocation(buildInvocation),
1620
+ modeName,
1621
+ });
1622
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
1623
+ fileResults.push(result);
1624
+ }
1625
+ ordered[fileIndex] = { fileName, fileResults, modeTimes };
1626
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1627
+ });
1628
+ }
1629
+ finally {
1630
+ await buildPool.close();
1631
+ }
1632
+ queueDisplay.flush();
1633
+ const allResults = [];
1634
+ const modeState = modes.map(() => ({ failed: false, passed: false }));
1635
+ const fileState = files.map(() => ({ failed: false, passed: false }));
1636
+ for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
1637
+ const entry = ordered[fileIndex];
1638
+ for (let i = 0; i < entry.fileResults.length; i++) {
1639
+ const result = entry.fileResults[i];
1640
+ allResults.push(result);
1641
+ if (result.failed)
1642
+ modeState[i].failed = true;
1643
+ else if (result.stats.passedFiles > 0)
1644
+ modeState[i].passed = true;
1645
+ }
1646
+ const verdict = resolveMatrixVerdict(entry.fileResults);
1647
+ if (verdict == "fail")
1648
+ fileState[fileIndex].failed = true;
1649
+ else if (verdict == "ok")
1650
+ fileState[fileIndex].passed = true;
1651
+ }
1652
+ const summary = aggregateRunResults(allResults);
1653
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1654
+ let failed = allResults.some((result) => result.failed);
1655
+ let fuzzSummary;
1656
+ if (fuzzEnabled) {
1657
+ if (reporterSession.reporterKind == "default") {
1658
+ process.stdout.write("\n");
1659
+ }
1660
+ const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
1661
+ if (fuzzResults.some(hasFuzzFailures))
1662
+ failed = true;
1663
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1664
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1665
+ }
1666
+ reporter.onRunComplete?.({
1667
+ clean: runFlags.clean,
1668
+ snapshotEnabled,
1669
+ showCoverage: runFlags.showCoverage,
1670
+ buildTime: getMergedIntervalDuration(buildIntervals),
1671
+ snapshotSummary: summary.snapshotSummary,
1672
+ coverageSummary: summary.coverageSummary,
1673
+ stats: summary.stats,
1674
+ reports: summary.reports,
1675
+ fuzzSummary,
1676
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
1677
+ });
1678
+ reporter.flush?.();
1679
+ return failed;
1680
+ }
1681
+ async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
1682
+ const files = await resolveSelectedFuzzFiles(configPath, selectors);
1683
+ if (!files.length) {
1684
+ throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
1685
+ }
1686
+ const ordered = new Array(files.length);
1687
+ const queueDisplay = new ParallelQueueDisplay(!clean);
1688
+ const poolWidth = Math.max(jobs, buildJobs, runJobs);
1689
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1690
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1691
+ const fileResults = [];
1692
+ for (const modeName of modes) {
1693
+ const modeResults = await fuzz(configPath, [file], modeName, overrides);
1694
+ fileResults.push(...modeResults);
1695
+ }
1696
+ ordered[index] = fileResults;
1697
+ const buffered = await createBufferedReporter(configPath);
1698
+ buffered.reporter.onFuzzFileComplete?.({ file, results: fileResults });
1699
+ buffered.reporter.flush?.();
1700
+ queueDisplay.complete(token, buffered.output());
1701
+ });
1702
+ queueDisplay.flush();
1703
+ return ordered.flat();
1704
+ }
1705
+ async function runFuzzMatrixResults(configPath, selectors, modes, overrides, reporter) {
1706
+ const files = await resolveSelectedFuzzFiles(configPath, selectors);
1707
+ if (!files.length) {
1708
+ throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
1709
+ }
1710
+ const results = [];
1711
+ for (const file of files) {
1712
+ const fileResults = [];
1713
+ for (const modeName of modes) {
1714
+ const modeResults = await fuzz(configPath, [file], modeName, overrides);
1715
+ fileResults.push(...modeResults);
1716
+ results.push(...modeResults);
1717
+ }
1718
+ reporter?.onFuzzFileComplete?.({ file, results: fileResults });
1719
+ }
1720
+ return results;
1721
+ }
1722
+ function hasFuzzFailures(result) {
1723
+ if (result.crashes > 0)
1724
+ return true;
1725
+ return result.fuzzers.some((fuzzer) => fuzzer.failed > 0);
1726
+ }
1727
+ function buildFuzzCompleteEvent(results, modes) {
1728
+ return {
1729
+ results,
1730
+ time: results.reduce((sum, item) => sum + item.time, 0),
1731
+ buildTime: getMergedIntervalDuration(collectFuzzBuildIntervals(results)),
1732
+ fuzzingSummary: summarizeFuzzExecutions(results),
1733
+ suiteSummary: summarizeFuzzSuites(results),
1734
+ modeSummary: summarizeFuzzModes(results, modes),
1735
+ };
1736
+ }
1737
+ function collectFuzzBuildIntervals(results) {
1738
+ return results.map((result) => ({
1739
+ start: result.buildStartedAt,
1740
+ end: result.buildFinishedAt,
1741
+ }));
1742
+ }
1743
+ function getMergedIntervalDuration(intervals) {
1744
+ if (!intervals.length)
1745
+ return 0;
1746
+ const sorted = intervals
1747
+ .map((interval) => ({
1748
+ start: Math.min(interval.start, interval.end),
1749
+ end: Math.max(interval.start, interval.end),
1750
+ }))
1751
+ .sort((a, b) => a.start - b.start);
1752
+ let total = 0;
1753
+ let currentStart = sorted[0].start;
1754
+ let currentEnd = sorted[0].end;
1755
+ for (let i = 1; i < sorted.length; i++) {
1756
+ const interval = sorted[i];
1757
+ if (interval.start <= currentEnd) {
1758
+ currentEnd = Math.max(currentEnd, interval.end);
1759
+ continue;
1760
+ }
1761
+ total += currentEnd - currentStart;
1762
+ currentStart = interval.start;
1763
+ currentEnd = interval.end;
1764
+ }
1765
+ total += currentEnd - currentStart;
1766
+ return total;
1767
+ }
1768
+ function summarizeFuzzExecutions(results) {
1769
+ return {
1770
+ failed: results.reduce((sum, item) => sum +
1771
+ item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.failed + fuzzer.crashed, 0), 0),
1772
+ skipped: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.skipped, 0), 0),
1773
+ total: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.runs, 0), 0),
1774
+ };
1775
+ }
1776
+ function summarizeFuzzSuites(results) {
1777
+ return {
1778
+ failed: results.reduce((sum, item) => sum +
1779
+ item.fuzzers.filter((fuzzer) => fuzzer.failed > 0 || fuzzer.crashed > 0)
1780
+ .length, 0),
1781
+ skipped: results.reduce((sum, item) => sum + item.fuzzers.filter((fuzzer) => fuzzer.skipped > 0).length, 0),
1782
+ total: results.reduce((sum, item) => sum + item.fuzzers.length, 0),
1783
+ };
1784
+ }
1785
+ function summarizeFuzzModes(results, modes) {
1786
+ const total = Math.max(modes.length, 1);
1787
+ const state = new Map();
1788
+ for (const modeName of modes) {
1789
+ state.set(modeName ?? "default", { failed: false, passed: false });
1790
+ }
1791
+ for (const result of results) {
1792
+ const current = state.get(result.modeName) ?? {
1793
+ failed: false,
1794
+ passed: false,
1795
+ };
1796
+ if (hasFuzzFailures(result))
1797
+ current.failed = true;
1798
+ else if (!isSkippedFuzzResult(result))
1799
+ current.passed = true;
1800
+ state.set(result.modeName, current);
1801
+ }
1802
+ let failed = 0;
1803
+ let skipped = 0;
1804
+ for (const mode of state.values()) {
1805
+ if (mode.failed)
1806
+ failed++;
1807
+ else if (!mode.passed)
1808
+ skipped++;
1809
+ }
1810
+ return { failed, skipped, total };
1811
+ }
1812
+ function isSkippedFuzzResult(result) {
1813
+ return (result.crashes == 0 &&
1814
+ result.fuzzers.length > 0 &&
1815
+ result.fuzzers.every((fuzzer) => fuzzer.skipped > 0));
1816
+ }
1817
+ function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
1818
+ const line = formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes);
1819
+ if (liveMatrix)
1820
+ clearLiveLine();
1821
+ process.stdout.write(line + "\n");
1822
+ }
1823
+ function formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes) {
1824
+ const verdict = resolveMatrixVerdict(results);
1825
+ const badge = verdict == "fail"
1826
+ ? chalk.bgRed.white(" FAIL ")
1827
+ : verdict == "ok"
1828
+ ? chalk.bgGreenBright.black(" PASS ")
1829
+ : chalk.bgBlackBright.white(" SKIP ");
1830
+ const avg = formatMatrixAverageTime(results);
1831
+ const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
1832
+ const failedModes = results
1833
+ .map((result, index) => (result.failed ? modes[index] : null))
1834
+ .filter((mode) => Boolean(mode));
1835
+ const suffix = showPerModeTimes
1836
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
1837
+ : failedModes.length
1838
+ ? ` ${chalk.dim(`(failed: ${failedModes.join(", ")})`)}`
1839
+ : "";
1840
+ return `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
1841
+ }
1842
+ function resolveMatrixVerdict(results) {
1843
+ if (results.some((result) => result.failed))
1844
+ return "fail";
1845
+ const hasPass = results.some((result) => result.stats.passedFiles > 0);
1846
+ if (hasPass)
1847
+ return "ok";
1848
+ return "skip";
1849
+ }
1850
+ function canRewriteStdout() {
1851
+ return Boolean(process.stdout.isTTY);
1852
+ }
1853
+ function clearLiveLine() {
1854
+ if (!canRewriteStdout())
1855
+ return;
1856
+ process.stdout.write("\r\x1b[2K");
1857
+ }
1858
+ function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
1859
+ if (!canRewriteStdout())
766
1860
  return;
767
1861
  const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
768
1862
  const suffix = showPerModeTimes
@@ -850,10 +1944,19 @@ function resolveConfiguredModeTotal(configPath) {
850
1944
  const configuredModes = Object.keys(config.modes).length;
851
1945
  return configuredModes || 1;
852
1946
  }
853
- async function resolveConfiguredFileTotal(configPath) {
854
- const files = await resolveSelectedFiles(configPath, []);
1947
+ async function resolveConfiguredFileTotal(configPath, selectors = []) {
1948
+ const files = await resolveSelectedFiles(configPath, selectors);
855
1949
  return files.length;
856
1950
  }
1951
+ async function previewBuildCommands(configPath, selectors, modeName, featureToggles) {
1952
+ const files = await resolveSelectedFiles(configPath, selectors);
1953
+ const out = {};
1954
+ for (const file of files) {
1955
+ const invocation = await getBuildInvocationPreview(configPath, file, modeName, featureToggles);
1956
+ out[file] = formatBuildInvocation(invocation);
1957
+ }
1958
+ return out;
1959
+ }
857
1960
  function resolveExecutionModes(configPath, selectedModes) {
858
1961
  if (selectedModes.length)
859
1962
  return selectedModes;
@@ -872,15 +1975,35 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
872
1975
  const specs = matches.filter((file) => file.endsWith(".spec.ts"));
873
1976
  return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
874
1977
  }
875
- async function buildNoTestFilesMatchedError(configPath, selectors) {
1978
+ async function resolveSelectedFuzzFiles(configPath, selectors) {
1979
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1980
+ const config = loadConfig(resolvedConfigPath, false);
1981
+ const patterns = resolveFuzzPatterns(config.fuzz.input, selectors);
1982
+ const matches = await glob(patterns);
1983
+ const fuzzFiles = matches.filter((file) => file.endsWith(".fuzz.ts"));
1984
+ return [...new Set(fuzzFiles)].sort((a, b) => a.localeCompare(b));
1985
+ }
1986
+ async function resolveSelectedTestInputs(configPath, selectors) {
1987
+ const [specs, fuzz] = await Promise.all([
1988
+ resolveSelectedFiles(configPath, selectors),
1989
+ resolveSelectedFuzzFiles(configPath, selectors),
1990
+ ]);
1991
+ return { specs, fuzz };
1992
+ }
1993
+ async function buildNoTestFilesMatchedError(configPath, selectors, includeFuzz = false) {
876
1994
  const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
877
1995
  const lines = [`No test files matched: ${scope}`];
878
1996
  const configuredFiles = await resolveSelectedFiles(configPath, [], false);
1997
+ const configuredFuzzFiles = includeFuzz
1998
+ ? await resolveSelectedFuzzFiles(configPath, [])
1999
+ : [];
879
2000
  if (!selectors.length) {
880
2001
  lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
881
2002
  return new Error(lines.join("\n"));
882
2003
  }
883
- const suggestions = suggestClosestSuites(selectors, configuredFiles);
2004
+ const suggestions = suggestClosestSuites(selectors, includeFuzz
2005
+ ? [...configuredFiles, ...configuredFuzzFiles]
2006
+ : configuredFiles);
884
2007
  if (suggestions.length) {
885
2008
  lines.push(`Closest suite names: ${suggestions.join(", ")}`);
886
2009
  }
@@ -894,6 +2017,13 @@ async function buildNoTestFilesMatchedError(configPath, selectors) {
894
2017
  else {
895
2018
  lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
896
2019
  }
2020
+ if (includeFuzz && configuredFuzzFiles.length) {
2021
+ const sample = configuredFuzzFiles
2022
+ .slice(0, 5)
2023
+ .map((file) => path.basename(file))
2024
+ .join(", ");
2025
+ lines.push(`Configured fuzzers (${configuredFuzzFiles.length}): ${sample}${configuredFuzzFiles.length > 5 ? ", ..." : ""}`);
2026
+ }
897
2027
  lines.push('Run "ast test --list" to inspect resolved files.');
898
2028
  return new Error(lines.join("\n"));
899
2029
  }
@@ -980,6 +2110,27 @@ function resolveInputPatterns(configured, selectors) {
980
2110
  }
981
2111
  return [...patterns];
982
2112
  }
2113
+ function resolveFuzzPatterns(configured, selectors) {
2114
+ const configuredInputs = Array.isArray(configured)
2115
+ ? configured
2116
+ : [configured];
2117
+ if (!selectors.length)
2118
+ return configuredInputs;
2119
+ const patterns = new Set();
2120
+ for (const selector of expandSelectors(selectors)) {
2121
+ if (!selector)
2122
+ continue;
2123
+ if (isBareSuiteSelector(selector)) {
2124
+ const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
2125
+ for (const configuredInput of configuredInputs) {
2126
+ patterns.add(path.join(path.dirname(configuredInput), `${base}.fuzz.ts`));
2127
+ }
2128
+ continue;
2129
+ }
2130
+ patterns.add(selector);
2131
+ }
2132
+ return [...patterns];
2133
+ }
983
2134
  function expandSelectors(selectors) {
984
2135
  const expanded = [];
985
2136
  for (const selector of selectors) {
@@ -1064,7 +2215,184 @@ function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpec
1064
2215
  const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
1065
2216
  return `${stem}.${disambiguator}${ext}`;
1066
2217
  }
1067
- async function listExecutionPlan(command, configPath, selectors, modes, listFlags) {
2218
+ async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
2219
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
2220
+ const config = loadConfig(resolvedConfigPath, true);
2221
+ const missing = [];
2222
+ for (const modeName of modes) {
2223
+ const applied = applyMode(config, modeName);
2224
+ const active = applied.config;
2225
+ if (!usesWebBrowser(active))
2226
+ continue;
2227
+ const requestedBrowser = browserOverride?.trim() || active.runOptions.runtime.browser.trim();
2228
+ const resolved = resolveBrowserSelection(requestedBrowser);
2229
+ if (!resolved) {
2230
+ missing.push({ modeName, browser: requestedBrowser });
2231
+ continue;
2232
+ }
2233
+ active.runOptions.runtime.browser = resolved.browser;
2234
+ process.env.BROWSER = resolved.browser;
2235
+ }
2236
+ if (!missing.length)
2237
+ return;
2238
+ await handleMissingWebBrowsers(missing);
2239
+ }
2240
+ function resolveBrowserSelection(requested = "") {
2241
+ if (requested.trim().length) {
2242
+ return resolveNamedBrowser(requested);
2243
+ }
2244
+ const envBrowser = process.env.BROWSER?.trim() ?? "";
2245
+ if (envBrowser.length) {
2246
+ return resolveNamedBrowser(envBrowser);
2247
+ }
2248
+ const candidates = [
2249
+ "chromium",
2250
+ "chromium-browser",
2251
+ "google-chrome",
2252
+ "google-chrome-stable",
2253
+ "chrome",
2254
+ "msedge",
2255
+ "firefox",
2256
+ ];
2257
+ for (const candidate of candidates) {
2258
+ if (hasExecutable(candidate)) {
2259
+ return { browser: candidate };
2260
+ }
2261
+ }
2262
+ const playwrightFallback = resolvePlaywrightBrowserExecutable("chromium") ??
2263
+ resolvePlaywrightBrowserExecutable("firefox");
2264
+ if (playwrightFallback) {
2265
+ return { browser: playwrightFallback };
2266
+ }
2267
+ return null;
2268
+ }
2269
+ function resolveNamedBrowser(browser) {
2270
+ const normalized = browser.trim().toLowerCase();
2271
+ if (!normalized.length)
2272
+ return null;
2273
+ if (browser.includes("/") ||
2274
+ browser.includes("\\") ||
2275
+ path.isAbsolute(browser)) {
2276
+ return hasExecutable(browser) ? { browser } : null;
2277
+ }
2278
+ const aliases = {
2279
+ chromium: ["chromium", "chromium-browser"],
2280
+ chrome: [
2281
+ "google-chrome",
2282
+ "google-chrome-stable",
2283
+ "chrome",
2284
+ "chromium",
2285
+ "chromium-browser",
2286
+ ],
2287
+ firefox: ["firefox"],
2288
+ webkit: [],
2289
+ };
2290
+ const candidates = aliases[normalized] ?? [browser];
2291
+ for (const candidate of candidates) {
2292
+ if (hasExecutable(candidate)) {
2293
+ return { browser: candidate };
2294
+ }
2295
+ }
2296
+ const playwrightFallback = resolvePlaywrightBrowserExecutable(normalized);
2297
+ if (playwrightFallback) {
2298
+ return { browser: playwrightFallback };
2299
+ }
2300
+ return null;
2301
+ }
2302
+ function usesWebBrowser(config) {
2303
+ return (config.buildOptions.target == "web" ||
2304
+ config.runOptions.runtime.browser.length > 0 ||
2305
+ config.runOptions.runtime.cmd.includes("default.web.js"));
2306
+ }
2307
+ async function handleMissingWebBrowsers(missing) {
2308
+ const scope = missing
2309
+ .map((entry) => entry.browser?.length
2310
+ ? `${entry.modeName ?? "default"} (${entry.browser})`
2311
+ : (entry.modeName ?? "default"))
2312
+ .join(", ");
2313
+ const details = "no web-capable browser was found in PATH, BROWSER, or Playwright cache";
2314
+ if (!canPromptForWebInstall()) {
2315
+ throw new Error(`web target requires a browser for mode(s) ${scope}; ${details}. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit".`);
2316
+ }
2317
+ process.stdout.write(chalk.bold.blue("◇ Browser Setup Needed") +
2318
+ "\n" +
2319
+ `│ ${details}\n` +
2320
+ "│\n");
2321
+ const choice = await promptLine("Install Chromium with Playwright now? [Y/n] ");
2322
+ const normalized = choice.trim().toLowerCase();
2323
+ if (normalized == "n" || normalized == "no") {
2324
+ throw new Error('browser install skipped. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit", then rerun.');
2325
+ }
2326
+ if (normalized != "" && normalized != "y" && normalized != "yes") {
2327
+ throw new Error(`invalid answer "${choice}". Expected yes or no.`);
2328
+ }
2329
+ const selected = "chromium";
2330
+ process.stdout.write(chalk.dim(`installing ${selected} via Playwright...\n`));
2331
+ const install = spawnSync("npx", ["-y", "playwright", "install", selected], {
2332
+ stdio: "inherit",
2333
+ shell: false,
2334
+ });
2335
+ if (install.status !== 0) {
2336
+ throw new Error(`Playwright browser install failed for ${selected}`);
2337
+ }
2338
+ const browserPath = resolvePlaywrightBrowserExecutable(selected);
2339
+ if (!browserPath) {
2340
+ throw new Error(`Playwright installed ${selected}, but as-test could not locate the browser executable`);
2341
+ }
2342
+ process.env.BROWSER = browserPath;
2343
+ }
2344
+ function canPromptForWebInstall() {
2345
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
2346
+ }
2347
+ function promptLine(question) {
2348
+ return new Promise((resolve) => {
2349
+ const rl = createInterface({
2350
+ input: process.stdin,
2351
+ output: process.stdout,
2352
+ });
2353
+ rl.question(question, (answer) => {
2354
+ rl.close();
2355
+ resolve(answer);
2356
+ });
2357
+ });
2358
+ }
2359
+ function resolvePlaywrightBrowserExecutable(browser) {
2360
+ const cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
2361
+ if (!cacheRoot.length || !existsSync(cacheRoot))
2362
+ return null;
2363
+ const map = {
2364
+ chromium: ["chromium-*/chrome-linux64/chrome"],
2365
+ chrome: ["chromium-*/chrome-linux64/chrome"],
2366
+ firefox: ["firefox-*/firefox/firefox"],
2367
+ webkit: ["webkit-*/pw_run.sh"],
2368
+ };
2369
+ const patterns = map[browser] ?? [];
2370
+ for (const pattern of patterns) {
2371
+ const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
2372
+ if (matches.length)
2373
+ return matches[matches.length - 1];
2374
+ }
2375
+ return null;
2376
+ }
2377
+ function hasExecutable(command) {
2378
+ if (!command.length)
2379
+ return false;
2380
+ if (command.includes("/") || command.includes("\\")) {
2381
+ return existsSync(command);
2382
+ }
2383
+ const pathValue = process.env.PATH ?? "";
2384
+ const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
2385
+ for (const base of pathValue.split(path.delimiter)) {
2386
+ if (!base.length)
2387
+ continue;
2388
+ for (const suffix of suffixes) {
2389
+ if (existsSync(path.join(base, command + suffix)))
2390
+ return true;
2391
+ }
2392
+ }
2393
+ return false;
2394
+ }
2395
+ async function listExecutionPlan(command, configPath, selectors, modes, listFlags, fuzzEnabled = false) {
1068
2396
  const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1069
2397
  const config = loadConfig(resolvedConfigPath, true);
1070
2398
  const configuredModes = Object.keys(config.modes);
@@ -1093,36 +2421,86 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
1093
2421
  }
1094
2422
  if (!listFlags.list)
1095
2423
  return;
1096
- const files = await resolveSelectedFiles(configPath, selectors);
1097
- if (!files.length) {
2424
+ const specFiles = command == "fuzz" ? [] : await resolveSelectedFiles(configPath, selectors);
2425
+ const fuzzFiles = command == "fuzz"
2426
+ ? await resolveSelectedFuzzFiles(configPath, selectors)
2427
+ : command == "test" && fuzzEnabled
2428
+ ? await resolveSelectedFuzzFiles(configPath, selectors)
2429
+ : [];
2430
+ const files = command == "fuzz" ? fuzzFiles : specFiles;
2431
+ if (!specFiles.length && !fuzzFiles.length) {
1098
2432
  const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
1099
- throw new Error(`No test files matched: ${scope}`);
2433
+ throw new Error(command == "fuzz"
2434
+ ? `No fuzz files matched: ${scope}`
2435
+ : `No test files matched: ${scope}`);
1100
2436
  }
1101
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1102
- process.stdout.write(chalk.bold("Resolved files:\n"));
1103
- for (const file of files) {
1104
- process.stdout.write(` - ${file}\n`);
2437
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(specFiles);
2438
+ const duplicateFuzzBasenames = resolveDuplicateSpecBasenames(fuzzFiles);
2439
+ if (specFiles.length) {
2440
+ process.stdout.write(chalk.bold("Resolved files:\n"));
2441
+ for (const file of specFiles) {
2442
+ process.stdout.write(` - ${file}\n`);
2443
+ }
2444
+ process.stdout.write("\n");
2445
+ }
2446
+ if (fuzzFiles.length && command == "test") {
2447
+ process.stdout.write(chalk.bold("Resolved fuzz files:\n"));
2448
+ for (const file of fuzzFiles) {
2449
+ process.stdout.write(` - ${file}\n`);
2450
+ }
2451
+ process.stdout.write("\n");
2452
+ }
2453
+ if (command == "fuzz" && fuzzFiles.length) {
2454
+ process.stdout.write(chalk.bold("Resolved files:\n"));
2455
+ for (const file of fuzzFiles) {
2456
+ process.stdout.write(` - ${file}\n`);
2457
+ }
2458
+ process.stdout.write("\n");
1105
2459
  }
1106
- process.stdout.write("\n");
1107
2460
  for (const modeName of modes) {
1108
2461
  const applied = applyMode(config, modeName);
1109
2462
  const active = applied.config;
1110
2463
  const modeLabel = modeName ?? "default";
1111
2464
  process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
1112
- process.stdout.write(` target: ${active.buildOptions.target}\n`);
2465
+ process.stdout.write(` target: ${command == "fuzz" ? "bindings" : active.buildOptions.target}\n`);
1113
2466
  process.stdout.write(` outDir: ${active.outDir}\n`);
1114
- if (command != "build") {
2467
+ if (command == "run" || command == "test") {
1115
2468
  process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
2469
+ if (usesWebBrowser(active)) {
2470
+ process.stdout.write(` browser: ${active.runOptions.runtime.browser || "(auto)"}\n`);
2471
+ }
1116
2472
  }
1117
- const envOverrides = modeName
1118
- ? (config.modes[modeName]?.env ?? {})
1119
- : config.env;
2473
+ const envOverrides = {
2474
+ ...config.env,
2475
+ ...(modeName ? (config.modes[modeName]?.env ?? {}) : {}),
2476
+ ...(command == "build"
2477
+ ? active.buildOptions.env
2478
+ : command == "run" || command == "test"
2479
+ ? active.runOptions.env
2480
+ : {}),
2481
+ };
1120
2482
  const envKeys = Object.keys(envOverrides);
1121
2483
  process.stdout.write(` env overrides: ${envKeys.length}${envKeys.length ? ` (${envKeys.join(", ")})` : ""}\n`);
1122
- process.stdout.write(" artifacts:\n");
1123
- for (const file of files) {
1124
- const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
1125
- process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2484
+ if (specFiles.length) {
2485
+ process.stdout.write(" artifacts:\n");
2486
+ for (const file of specFiles) {
2487
+ const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
2488
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2489
+ }
2490
+ }
2491
+ if (fuzzFiles.length && command == "test") {
2492
+ process.stdout.write(" fuzz artifacts:\n");
2493
+ for (const file of fuzzFiles) {
2494
+ const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
2495
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2496
+ }
2497
+ }
2498
+ else if (command == "fuzz") {
2499
+ process.stdout.write(" artifacts:\n");
2500
+ for (const file of fuzzFiles) {
2501
+ const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
2502
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2503
+ }
1126
2504
  }
1127
2505
  process.stdout.write("\n");
1128
2506
  }