as-test 1.0.0 → 1.0.3

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";
11
+ import { fuzz } from "./commands/fuzz-core.js";
10
12
  import { applyMode, 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();
@@ -56,6 +63,8 @@ else if (COMMANDS.includes(args[0])) {
56
63
  resolveCommandArgs,
57
64
  resolveListFlags,
58
65
  resolveFeatureToggles,
66
+ resolveParallelJobs,
67
+ resolveBrowserOverride,
59
68
  resolveExecutionModes,
60
69
  listExecutionPlan,
61
70
  runRuntimeModes,
@@ -69,6 +78,9 @@ else if (COMMANDS.includes(args[0])) {
69
78
  resolveCommandArgs,
70
79
  resolveListFlags,
71
80
  resolveFeatureToggles,
81
+ resolveParallelJobs,
82
+ resolveBrowserOverride,
83
+ resolveFuzzOverrides,
72
84
  resolveExecutionModes,
73
85
  listExecutionPlan,
74
86
  runTestModes,
@@ -77,6 +89,19 @@ else if (COMMANDS.includes(args[0])) {
77
89
  process.exit(1);
78
90
  });
79
91
  }
92
+ else if (command === "fuzz") {
93
+ executeFuzzCommand(_args, configPath, selectedModes, {
94
+ resolveCommandArgs,
95
+ resolveListFlags,
96
+ resolveJobs,
97
+ resolveExecutionModes,
98
+ listExecutionPlan,
99
+ runFuzzModes,
100
+ }).catch((error) => {
101
+ printCliError(error);
102
+ process.exit(1);
103
+ });
104
+ }
80
105
  else if (command === "init") {
81
106
  executeInitCommand(_args, {
82
107
  resolveCommandTokens,
@@ -139,6 +164,13 @@ function info() {
139
164
  " " +
140
165
  "Build and run unit tests with selected runtime" +
141
166
  "\n");
167
+ console.log(" " +
168
+ chalk.bold.blueBright("fuzz") +
169
+ " " +
170
+ chalk.dim("<name>|<path-or-glob>") +
171
+ " " +
172
+ "Build and run fuzz targets" +
173
+ "\n");
142
174
  console.log(" " +
143
175
  chalk.bold.magentaBright("init") +
144
176
  " " +
@@ -153,63 +185,19 @@ function info() {
153
185
  "Validate environment/config/runtime setup");
154
186
  console.log("");
155
187
  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") +
188
+ console.log(" " +
189
+ chalk.bold.blue("--version, -v") +
202
190
  " " +
203
- "Preview configured and selected mode names");
204
- console.log(" " + chalk.bold.blue("--help, -h") + " Show help");
191
+ "Print current cli version");
192
+ console.log(" " +
193
+ chalk.bold.blue("--help, -h") +
194
+ " Show help menu");
205
195
  console.log("");
206
- console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
196
+ console.log(chalk.dim("If this tool provides value, please consider sponsoring my open-source work! https://jairus.dev/sponsor") + "\n");
197
+ console.log("View the docs: " +
198
+ chalk.blue("https://docs.jairus.dev/as-test"));
207
199
  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
- // );
200
+ chalk.blue("https://github.com/JairusSW/as-test"));
213
201
  }
214
202
  function isHelpFlag(value) {
215
203
  return value == "--help" || value == "-h";
@@ -249,7 +237,13 @@ function printCommandHelp(command) {
249
237
  process.stdout.write(chalk.bold("Flags:\n"));
250
238
  process.stdout.write(" --config <path> Use a specific config file\n");
251
239
  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");
240
+ process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
241
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
242
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
243
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
244
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
245
+ process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
246
+ process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
253
247
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
254
248
  process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
255
249
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
@@ -269,11 +263,24 @@ function printCommandHelp(command) {
269
263
  process.stdout.write(chalk.bold("Flags:\n"));
270
264
  process.stdout.write(" --config <path> Use a specific config file\n");
271
265
  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");
266
+ process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
267
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
268
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
269
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
270
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
271
+ process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
272
+ process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
273
273
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
274
274
  process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
275
275
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
276
276
  process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
277
+ process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
278
+ process.stdout.write(" --fuzz-runs <n> Override fuzz iteration count for this run\n");
279
+ process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
280
+ process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
281
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
282
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
283
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
277
284
  process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
278
285
  process.stdout.write(" --tap Shortcut for --reporter tap\n");
279
286
  process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
@@ -283,11 +290,27 @@ function printCommandHelp(command) {
283
290
  process.stdout.write(" --help, -h Show this help\n");
284
291
  return;
285
292
  }
293
+ if (command == "fuzz") {
294
+ process.stdout.write(chalk.bold("Usage: ast fuzz [selectors...] [flags]\n\n"));
295
+ process.stdout.write("Build selected fuzz targets with bindings and execute them with generated inputs.\n\n");
296
+ process.stdout.write(chalk.bold("Flags:\n"));
297
+ process.stdout.write(" --config <path> Use a specific config file\n");
298
+ process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
299
+ process.stdout.write(" --runs <n> Override fuzz iteration count\n");
300
+ process.stdout.write(" --seed <n> Override fuzz seed\n");
301
+ process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
302
+ process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
303
+ process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
304
+ process.stdout.write(" --list Preview resolved fuzz files without running\n");
305
+ process.stdout.write(" --list-modes Preview configured and selected mode names\n");
306
+ process.stdout.write(" --help, -h Show this help\n");
307
+ return;
308
+ }
286
309
  if (command == "init") {
287
310
  process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
288
311
  process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
289
312
  process.stdout.write(chalk.bold("Flags:\n"));
290
- process.stdout.write(" --target <wasi|bindings> Set build target\n");
313
+ process.stdout.write(" --target <wasi|bindings|web> Set build target\n");
291
314
  process.stdout.write(" --example <minimal|full|none> Set example template\n");
292
315
  process.stdout.write(" --install Install dependencies after scaffolding\n");
293
316
  process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
@@ -361,6 +384,9 @@ function resolveCommandArgs(rawArgs, command) {
361
384
  if (arg == "--tap") {
362
385
  continue;
363
386
  }
387
+ if (arg == "--fuzz") {
388
+ continue;
389
+ }
364
390
  if (arg == "--enable" || arg == "--disable") {
365
391
  i++;
366
392
  continue;
@@ -368,6 +394,28 @@ function resolveCommandArgs(rawArgs, command) {
368
394
  if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
369
395
  continue;
370
396
  }
397
+ if (arg == "--runs" ||
398
+ arg == "--seed" ||
399
+ arg == "--parallel" ||
400
+ arg == "--jobs" ||
401
+ arg == "--build-jobs" ||
402
+ arg == "--run-jobs" ||
403
+ arg == "--browser" ||
404
+ arg == "--fuzz-runs" ||
405
+ arg == "--fuzz-seed") {
406
+ i++;
407
+ continue;
408
+ }
409
+ if (arg.startsWith("--runs=") ||
410
+ arg.startsWith("--seed=") ||
411
+ arg.startsWith("--jobs=") ||
412
+ arg.startsWith("--build-jobs=") ||
413
+ arg.startsWith("--run-jobs=") ||
414
+ arg.startsWith("--browser=") ||
415
+ arg.startsWith("--fuzz-runs=") ||
416
+ arg.startsWith("--fuzz-seed=")) {
417
+ continue;
418
+ }
371
419
  if (arg.startsWith("-")) {
372
420
  continue;
373
421
  }
@@ -407,12 +455,51 @@ function resolveFeatureToggles(rawArgs, command) {
407
455
  }
408
456
  return out;
409
457
  }
458
+ function resolveFuzzOverrides(rawArgs, command) {
459
+ const out = {};
460
+ let seenCommand = false;
461
+ for (let i = 0; i < rawArgs.length; i++) {
462
+ const arg = rawArgs[i];
463
+ if (!seenCommand) {
464
+ if (arg == command)
465
+ seenCommand = true;
466
+ continue;
467
+ }
468
+ const direct = command == "fuzz"
469
+ ? {
470
+ runs: "--runs",
471
+ seed: "--seed",
472
+ }
473
+ : {
474
+ runs: "--fuzz-runs",
475
+ seed: "--fuzz-seed",
476
+ };
477
+ const runs = parseNumberFlag(rawArgs, i, direct.runs);
478
+ if (runs) {
479
+ out.runs = runs.number;
480
+ if (runs.consumeNext)
481
+ i++;
482
+ continue;
483
+ }
484
+ const seed = parseNumberFlag(rawArgs, i, direct.seed);
485
+ if (seed) {
486
+ out.seed = seed.number;
487
+ if (seed.consumeNext)
488
+ i++;
489
+ continue;
490
+ }
491
+ }
492
+ return out;
493
+ }
410
494
  function resolveListFlags(rawArgs, command) {
411
495
  const out = {
412
496
  list: false,
413
497
  listModes: false,
414
498
  };
415
- if (command !== "build" && command !== "run" && command !== "test") {
499
+ if (command !== "build" &&
500
+ command !== "run" &&
501
+ command !== "test" &&
502
+ command !== "fuzz") {
416
503
  return out;
417
504
  }
418
505
  let seenCommand = false;
@@ -430,6 +517,318 @@ function resolveListFlags(rawArgs, command) {
430
517
  }
431
518
  return out;
432
519
  }
520
+ function parseNumberFlag(rawArgs, index, flag) {
521
+ const arg = rawArgs[index];
522
+ if (arg == flag) {
523
+ const next = rawArgs[index + 1];
524
+ if (!next || next.startsWith("-")) {
525
+ throw new Error(`${flag} requires a numeric value`);
526
+ }
527
+ return {
528
+ key: flag,
529
+ number: parseIntegerFlag(flag, next),
530
+ consumeNext: true,
531
+ };
532
+ }
533
+ if (arg.startsWith(`${flag}=`)) {
534
+ return {
535
+ key: flag,
536
+ number: parseIntegerFlag(flag, arg.slice(flag.length + 1)),
537
+ consumeNext: false,
538
+ };
539
+ }
540
+ return null;
541
+ }
542
+ function parseStringFlag(rawArgs, index, flag) {
543
+ const arg = rawArgs[index];
544
+ if (arg == flag) {
545
+ const next = rawArgs[index + 1];
546
+ if (!next || next.startsWith("-")) {
547
+ throw new Error(`${flag} requires a value`);
548
+ }
549
+ return { key: flag, value: next, consumeNext: true };
550
+ }
551
+ if (arg.startsWith(`${flag}=`)) {
552
+ const value = arg.slice(flag.length + 1);
553
+ if (!value.length) {
554
+ throw new Error(`${flag} requires a value`);
555
+ }
556
+ return { key: flag, value, consumeNext: false };
557
+ }
558
+ return null;
559
+ }
560
+ function resolveBrowserOverride(rawArgs, command) {
561
+ let seenCommand = false;
562
+ for (let i = 0; i < rawArgs.length; i++) {
563
+ const arg = rawArgs[i];
564
+ if (!seenCommand) {
565
+ if (arg == command)
566
+ seenCommand = true;
567
+ continue;
568
+ }
569
+ const parsed = parseStringFlag(rawArgs, i, "--browser");
570
+ if (!parsed)
571
+ continue;
572
+ return parsed.value.trim() || undefined;
573
+ }
574
+ return undefined;
575
+ }
576
+ function resolveJobs(rawArgs, command) {
577
+ let seenCommand = false;
578
+ let parallel = false;
579
+ for (let i = 0; i < rawArgs.length; i++) {
580
+ const arg = rawArgs[i];
581
+ if (!seenCommand) {
582
+ if (arg == command)
583
+ seenCommand = true;
584
+ continue;
585
+ }
586
+ if (arg == "--parallel") {
587
+ parallel = true;
588
+ continue;
589
+ }
590
+ const parsed = parseNumberFlag(rawArgs, i, "--jobs");
591
+ if (!parsed)
592
+ continue;
593
+ if (parsed.number < 1) {
594
+ throw new Error("--jobs requires a positive integer");
595
+ }
596
+ return parsed.number;
597
+ }
598
+ return parallel ? 0 : 1;
599
+ }
600
+ function resolveParallelJobs(rawArgs, command) {
601
+ const baseJobs = resolveJobs(rawArgs, command);
602
+ let buildJobs = baseJobs;
603
+ let runJobs = baseJobs;
604
+ let seenCommand = false;
605
+ for (let i = 0; i < rawArgs.length; i++) {
606
+ const arg = rawArgs[i];
607
+ if (!seenCommand) {
608
+ if (arg == command)
609
+ seenCommand = true;
610
+ continue;
611
+ }
612
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
613
+ if (buildParsed) {
614
+ if (buildParsed.number < 1) {
615
+ throw new Error("--build-jobs requires a positive integer");
616
+ }
617
+ buildJobs = buildParsed.number;
618
+ continue;
619
+ }
620
+ const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
621
+ if (runParsed) {
622
+ if (runParsed.number < 1) {
623
+ throw new Error("--run-jobs requires a positive integer");
624
+ }
625
+ runJobs = runParsed.number;
626
+ continue;
627
+ }
628
+ }
629
+ const jobs = Math.max(baseJobs, buildJobs, runJobs);
630
+ return { jobs, buildJobs, runJobs };
631
+ }
632
+ function resolveFuzzParallelJobs(rawArgs) {
633
+ const baseJobs = resolveJobs(rawArgs, "fuzz");
634
+ let buildJobs = baseJobs;
635
+ let runJobs = baseJobs;
636
+ let seenCommand = false;
637
+ for (let i = 0; i < rawArgs.length; i++) {
638
+ const arg = rawArgs[i];
639
+ if (!seenCommand) {
640
+ if (arg == "fuzz")
641
+ seenCommand = true;
642
+ continue;
643
+ }
644
+ const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
645
+ if (buildParsed) {
646
+ if (buildParsed.number < 1) {
647
+ throw new Error("--build-jobs requires a positive integer");
648
+ }
649
+ buildJobs = buildParsed.number;
650
+ continue;
651
+ }
652
+ const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
653
+ if (runParsed) {
654
+ if (runParsed.number < 1) {
655
+ throw new Error("--run-jobs requires a positive integer");
656
+ }
657
+ runJobs = runParsed.number;
658
+ continue;
659
+ }
660
+ }
661
+ const jobs = Math.max(baseJobs, buildJobs, runJobs);
662
+ return { jobs, buildJobs, runJobs };
663
+ }
664
+ function resolveEffectiveParallelJobs(settings, totalFiles) {
665
+ if (settings.jobs > 0) {
666
+ return {
667
+ jobs: Math.max(settings.jobs, settings.buildJobs, settings.runJobs),
668
+ buildJobs: settings.buildJobs > 0 ? settings.buildJobs : settings.jobs,
669
+ runJobs: settings.runJobs > 0 ? settings.runJobs : settings.jobs,
670
+ };
671
+ }
672
+ const autoJobs = resolveAutoJobs(totalFiles);
673
+ return {
674
+ jobs: Math.max(autoJobs, settings.buildJobs, settings.runJobs),
675
+ buildJobs: settings.buildJobs > 0 ? settings.buildJobs : autoJobs,
676
+ runJobs: settings.runJobs > 0 ? settings.runJobs : autoJobs,
677
+ };
678
+ }
679
+ function resolveAutoJobs(totalFiles) {
680
+ const cpuCount = typeof availableParallelism == "function"
681
+ ? availableParallelism()
682
+ : cpus().length;
683
+ const cpuBudget = Math.max(1, cpuCount - 1);
684
+ if (totalFiles <= 1)
685
+ return 1;
686
+ if (totalFiles <= 4)
687
+ return Math.min(2, cpuBudget, totalFiles);
688
+ if (totalFiles <= 12)
689
+ return Math.min(3, cpuBudget);
690
+ if (totalFiles <= 32)
691
+ return Math.min(4, cpuBudget);
692
+ return Math.min(Math.max(4, Math.ceil(totalFiles / 12)), cpuBudget);
693
+ }
694
+ function createBufferedStream() {
695
+ const chunks = [];
696
+ return {
697
+ isTTY: false,
698
+ write(chunk) {
699
+ chunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
700
+ return true;
701
+ },
702
+ read() {
703
+ return chunks.join("");
704
+ },
705
+ };
706
+ }
707
+ async function createBufferedReporter(configPath, modeName) {
708
+ const stream = createBufferedStream();
709
+ const session = await createRunReporter(configPath, undefined, modeName, {
710
+ stdout: stream,
711
+ stderr: stream,
712
+ });
713
+ return {
714
+ reporter: session.reporter,
715
+ reporterKind: session.reporterKind,
716
+ runtimeName: session.runtimeName,
717
+ output: () => stream.read(),
718
+ };
719
+ }
720
+ async function runOrderedPool(items, jobs, worker) {
721
+ const width = Math.max(1, jobs);
722
+ let nextIndex = 0;
723
+ let firstError = null;
724
+ async function runWorker() {
725
+ while (true) {
726
+ if (firstError != null)
727
+ return;
728
+ const index = nextIndex++;
729
+ if (index >= items.length)
730
+ return;
731
+ try {
732
+ await worker(items[index], index);
733
+ }
734
+ catch (error) {
735
+ if (firstError == null)
736
+ firstError = error;
737
+ return;
738
+ }
739
+ }
740
+ }
741
+ await Promise.all(Array.from({ length: Math.min(width, items.length) }, () => runWorker()));
742
+ if (firstError != null)
743
+ throw firstError;
744
+ }
745
+ function createAsyncLimiter(limit) {
746
+ const width = Math.max(1, limit);
747
+ let active = 0;
748
+ const queue = [];
749
+ return async function withLimit(task) {
750
+ if (active >= width) {
751
+ await new Promise((resolve) => queue.push(resolve));
752
+ }
753
+ active++;
754
+ try {
755
+ return await task();
756
+ }
757
+ finally {
758
+ active--;
759
+ const next = queue.shift();
760
+ if (next)
761
+ next();
762
+ }
763
+ };
764
+ }
765
+ function canRewriteParallelQueue() {
766
+ return Boolean(process.stdout.isTTY);
767
+ }
768
+ class ParallelQueueDisplay {
769
+ constructor(showStartLines) {
770
+ this.showStartLines = showStartLines;
771
+ this.active = new Map();
772
+ this.renderedLines = 0;
773
+ this.enabled = showStartLines && canRewriteParallelQueue();
774
+ }
775
+ start(file) {
776
+ const token = Symbol(file);
777
+ if (!this.showStartLines)
778
+ return token;
779
+ const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
780
+ if (!this.enabled)
781
+ return token;
782
+ this.clear();
783
+ this.active.set(token, line);
784
+ this.render();
785
+ return token;
786
+ }
787
+ complete(token, output) {
788
+ if (!this.showStartLines || !this.enabled) {
789
+ process.stdout.write(output);
790
+ return;
791
+ }
792
+ this.clear();
793
+ process.stdout.write(output);
794
+ this.active.delete(token);
795
+ this.render();
796
+ }
797
+ flush() {
798
+ if (!this.enabled)
799
+ return;
800
+ this.clear();
801
+ }
802
+ clear() {
803
+ if (!this.renderedLines)
804
+ return;
805
+ for (let i = 0; i < this.renderedLines; i++) {
806
+ process.stdout.write("\r\x1b[2K");
807
+ if (i < this.renderedLines - 1)
808
+ process.stdout.write("\x1b[1A");
809
+ }
810
+ this.renderedLines = 0;
811
+ }
812
+ render() {
813
+ if (!this.enabled)
814
+ return;
815
+ const lines = Array.from(this.active.values());
816
+ if (!lines.length)
817
+ return;
818
+ process.stdout.write(lines.join("\n"));
819
+ this.renderedLines = lines.length;
820
+ }
821
+ }
822
+ function renderQueuedFileStart(display, file) {
823
+ return display.start(file);
824
+ }
825
+ function parseIntegerFlag(flag, value) {
826
+ const parsed = Number(value);
827
+ if (!Number.isFinite(parsed) || parsed < 0) {
828
+ throw new Error(`${flag} requires a non-negative integer`);
829
+ }
830
+ return Math.floor(parsed);
831
+ }
433
832
  function applyFeatureToggle(out, rawFeature, enabled) {
434
833
  const key = rawFeature.trim().toLowerCase();
435
834
  if (key == "coverage") {
@@ -456,26 +855,32 @@ function resolveCommandTokens(rawArgs, command) {
456
855
  }
457
856
  return values;
458
857
  }
459
- async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
858
+ async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
460
859
  const files = await resolveSelectedFiles(configPath, selectors);
461
860
  if (!files.length) {
462
- throw await buildNoTestFilesMatchedError(configPath, selectors);
861
+ if (!allowNoSpecFiles) {
862
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
863
+ }
463
864
  }
464
865
  const reporterSession = await createRunReporter(configPath, undefined, modeName);
465
- const reporter = reporterSession.reporter;
866
+ const reporter = reporterOverride ?? reporterSession.reporter;
466
867
  const snapshotEnabled = runFlags.snapshot !== false;
467
868
  reporter.onRunStart?.({
468
869
  runtimeName: reporterSession.runtimeName,
469
870
  clean: runFlags.clean,
470
871
  verbose: runFlags.verbose,
471
872
  snapshotEnabled,
472
- updateSnapshots: runFlags.updateSnapshots,
873
+ createSnapshots: runFlags.createSnapshots,
473
874
  });
474
875
  const results = [];
475
876
  let failed = false;
877
+ const buildIntervals = [];
476
878
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
477
879
  for (const file of files) {
880
+ const buildStartedAt = Date.now();
478
881
  await build(configPath, [file], modeName, buildFeatureToggles);
882
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
883
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
479
884
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
480
885
  const result = await run(runFlags, configPath, [file], false, {
481
886
  reporter,
@@ -483,6 +888,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
483
888
  emitRunComplete: false,
484
889
  logFileName: `test.${artifactKey}.log.json`,
485
890
  coverageFileName: `coverage.${artifactKey}.log.json`,
891
+ buildCommand: formatBuildInvocation(buildInvocation),
486
892
  modeName,
487
893
  });
488
894
  results.push(result);
@@ -491,17 +897,30 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
491
897
  }
492
898
  const summary = aggregateRunResults(results);
493
899
  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;
900
+ if (emitRunComplete) {
901
+ reporter.onRunComplete?.({
902
+ clean: runFlags.clean,
903
+ snapshotEnabled,
904
+ showCoverage: runFlags.showCoverage,
905
+ buildTime: getMergedIntervalDuration(buildIntervals),
906
+ snapshotSummary: summary.snapshotSummary,
907
+ coverageSummary: summary.coverageSummary,
908
+ stats: summary.stats,
909
+ reports: summary.reports,
910
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
911
+ });
912
+ reporter.flush?.();
913
+ }
914
+ return {
915
+ failed,
916
+ summary: {
917
+ buildTime: getMergedIntervalDuration(buildIntervals),
918
+ snapshotSummary: summary.snapshotSummary,
919
+ coverageSummary: summary.coverageSummary,
920
+ stats: summary.stats,
921
+ reports: summary.reports,
922
+ },
923
+ };
505
924
  }
506
925
  async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
507
926
  for (const modeName of modes) {
@@ -509,20 +928,42 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles)
509
928
  }
510
929
  }
511
930
  async function runRuntimeModes(runFlags, configPath, selectors, modes) {
512
- const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
931
+ await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
932
+ const modeSummaryTotal = Math.max(modes.length, 1);
513
933
  const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
934
+ const effectiveRunFlags = {
935
+ ...runFlags,
936
+ ...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
937
+ };
938
+ if (effectiveRunFlags.jobs > 1) {
939
+ if (modes.length > 1) {
940
+ const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
941
+ process.exit(failed ? 1 : 0);
942
+ return;
943
+ }
944
+ let failed = false;
945
+ for (const modeName of modes) {
946
+ const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal);
947
+ if (result)
948
+ failed = true;
949
+ }
950
+ process.exit(failed ? 1 : 0);
951
+ return;
952
+ }
514
953
  if (modes.length > 1) {
515
- const failed = await runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
954
+ const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
516
955
  process.exit(failed ? 1 : 0);
517
956
  return;
518
957
  }
519
958
  let failed = false;
959
+ const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
520
960
  for (const modeName of modes) {
521
- const result = await run(runFlags, configPath, selectors, false, {
961
+ const result = await run(effectiveRunFlags, configPath, selectors, false, {
522
962
  modeName,
523
963
  modeSummaryTotal,
524
964
  modeSummaryExecuted: 1,
525
965
  fileSummaryTotal,
966
+ buildCommandsByFile,
526
967
  });
527
968
  if (result.failed)
528
969
  failed = true;
@@ -542,7 +983,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
542
983
  clean: runFlags.clean,
543
984
  verbose: runFlags.verbose,
544
985
  snapshotEnabled,
545
- updateSnapshots: runFlags.updateSnapshots,
986
+ createSnapshots: runFlags.createSnapshots,
546
987
  });
547
988
  const silentReporter = {};
548
989
  const allResults = [];
@@ -558,6 +999,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
558
999
  passed: false,
559
1000
  }));
560
1001
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1002
+ const buildIntervals = [];
561
1003
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
562
1004
  const file = files[fileIndex];
563
1005
  const fileName = path.basename(file);
@@ -569,6 +1011,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
569
1011
  for (let i = 0; i < modes.length; i++) {
570
1012
  const modeName = modes[i];
571
1013
  try {
1014
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
572
1015
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
573
1016
  const result = await run(runFlags, configPath, [file], false, {
574
1017
  reporter: silentReporter,
@@ -577,6 +1020,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
577
1020
  emitRunComplete: false,
578
1021
  logFileName: `run.${artifactKey}.log.json`,
579
1022
  coverageFileName: `coverage.${artifactKey}.log.json`,
1023
+ buildCommand: formatBuildInvocation(buildInvocation),
580
1024
  modeName,
581
1025
  });
582
1026
  modeTimes[i] = formatMatrixModeTime(result.stats.time);
@@ -612,6 +1056,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
612
1056
  clean: runFlags.clean,
613
1057
  snapshotEnabled,
614
1058
  showCoverage: runFlags.showCoverage,
1059
+ buildTime: 0,
615
1060
  snapshotSummary: summary.snapshotSummary,
616
1061
  coverageSummary: summary.coverageSummary,
617
1062
  stats: summary.stats,
@@ -620,26 +1065,75 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
620
1065
  });
621
1066
  return allResults.some((result) => result.failed);
622
1067
  }
623
- async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
624
- const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
625
- const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
1068
+ async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
1069
+ await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
1070
+ const modeSummaryTotal = Math.max(modes.length, 1);
1071
+ const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
1072
+ const effectiveRunFlags = {
1073
+ ...runFlags,
1074
+ ...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
1075
+ };
1076
+ if (effectiveRunFlags.jobs > 1) {
1077
+ if (modes.length > 1) {
1078
+ const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
1079
+ process.exit(failed ? 1 : 0);
1080
+ return;
1081
+ }
1082
+ let failed = false;
1083
+ for (const modeName of modes) {
1084
+ const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
1085
+ if (modeFailed)
1086
+ failed = true;
1087
+ }
1088
+ process.exit(failed ? 1 : 0);
1089
+ return;
1090
+ }
626
1091
  if (modes.length > 1) {
627
- const failed = await runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal);
1092
+ const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
628
1093
  process.exit(failed ? 1 : 0);
629
1094
  return;
630
1095
  }
631
1096
  let failed = false;
632
1097
  for (const modeName of modes) {
633
- const modeFailed = await runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName);
634
- if (modeFailed)
1098
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1099
+ const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
1100
+ if (modeResult.failed)
635
1101
  failed = true;
1102
+ if (fuzzEnabled) {
1103
+ if (reporterSession.reporterKind == "default") {
1104
+ process.stdout.write("\n");
1105
+ }
1106
+ const fuzzResults = await runFuzzMatrixResults(configPath, selectors, [modeName], fuzzOverrides, reporterSession.reporter);
1107
+ if (fuzzResults.some(hasFuzzFailures))
1108
+ failed = true;
1109
+ reporterSession.reporter.onRunComplete?.({
1110
+ clean: runFlags.clean,
1111
+ snapshotEnabled: effectiveRunFlags.snapshot !== false,
1112
+ showCoverage: effectiveRunFlags.showCoverage,
1113
+ buildTime: modeResult.summary.buildTime +
1114
+ getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
1115
+ snapshotSummary: modeResult.summary.snapshotSummary,
1116
+ coverageSummary: modeResult.summary.coverageSummary,
1117
+ stats: modeResult.summary.stats,
1118
+ reports: modeResult.summary.reports,
1119
+ fuzzSummary: summarizeFuzzExecutions(fuzzResults),
1120
+ modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
1121
+ });
1122
+ reporterSession.reporter.flush?.();
1123
+ }
636
1124
  }
637
1125
  process.exit(failed ? 1 : 0);
638
1126
  }
639
- async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
1127
+ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
640
1128
  const files = await resolveSelectedFiles(configPath, selectors);
641
1129
  if (!files.length) {
642
- throw await buildNoTestFilesMatchedError(configPath, selectors);
1130
+ if (!fuzzEnabled) {
1131
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1132
+ }
1133
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1134
+ if (!fuzzFiles.length) {
1135
+ throw await buildNoTestFilesMatchedError(configPath, selectors, true);
1136
+ }
643
1137
  }
644
1138
  const reporterSession = await createRunReporter(configPath);
645
1139
  const reporter = reporterSession.reporter;
@@ -649,7 +1143,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
649
1143
  clean: runFlags.clean,
650
1144
  verbose: runFlags.verbose,
651
1145
  snapshotEnabled,
652
- updateSnapshots: runFlags.updateSnapshots,
1146
+ createSnapshots: runFlags.createSnapshots,
653
1147
  });
654
1148
  const silentReporter = {};
655
1149
  const allResults = [];
@@ -665,6 +1159,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
665
1159
  passed: false,
666
1160
  }));
667
1161
  const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1162
+ const buildIntervals = [];
668
1163
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
669
1164
  const file = files[fileIndex];
670
1165
  const fileName = path.basename(file);
@@ -676,7 +1171,10 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
676
1171
  for (let i = 0; i < modes.length; i++) {
677
1172
  const modeName = modes[i];
678
1173
  try {
1174
+ const buildStartedAt = Date.now();
679
1175
  await build(configPath, [file], modeName, buildFeatureToggles);
1176
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1177
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
680
1178
  const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
681
1179
  const result = await run(runFlags, configPath, [file], false, {
682
1180
  reporter: silentReporter,
@@ -685,6 +1183,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
685
1183
  emitRunComplete: false,
686
1184
  logFileName: `test.${artifactKey}.log.json`,
687
1185
  coverageFileName: `coverage.${artifactKey}.log.json`,
1186
+ buildCommand: formatBuildInvocation(buildInvocation),
688
1187
  modeName,
689
1188
  });
690
1189
  modeTimes[i] = formatMatrixModeTime(result.stats.time);
@@ -716,78 +1215,610 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
716
1215
  }
717
1216
  const summary = aggregateRunResults(allResults);
718
1217
  summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1218
+ let failed = allResults.some((result) => result.failed);
1219
+ let fuzzSummary;
1220
+ if (fuzzEnabled) {
1221
+ if (reporterSession.reporterKind == "default") {
1222
+ process.stdout.write("\n");
1223
+ }
1224
+ const fuzzResults = await runFuzzMatrixResults(configPath, selectors, modes, fuzzOverrides, reporter);
1225
+ if (fuzzResults.some(hasFuzzFailures))
1226
+ failed = true;
1227
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1228
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1229
+ }
719
1230
  reporter.onRunComplete?.({
720
1231
  clean: runFlags.clean,
721
1232
  snapshotEnabled,
722
1233
  showCoverage: runFlags.showCoverage,
1234
+ buildTime: getMergedIntervalDuration(buildIntervals),
723
1235
  snapshotSummary: summary.snapshotSummary,
724
1236
  coverageSummary: summary.coverageSummary,
725
1237
  stats: summary.stats,
726
1238
  reports: summary.reports,
1239
+ fuzzSummary,
727
1240
  modeSummary: buildModeSummary(modeState, modeSummaryTotal),
728
1241
  });
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);
758
- }
759
- function clearLiveLine() {
760
- if (!canRewriteStdout())
761
- return;
762
- process.stdout.write("\r\x1b[2K");
1242
+ reporter.flush?.();
1243
+ return failed;
763
1244
  }
764
- function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
765
- if (!canRewriteStdout())
1245
+ async function runFuzzModes(configPath, selectors, modes, rawArgs) {
1246
+ const overrides = resolveFuzzOverrides(rawArgs, "fuzz");
1247
+ const parallelSettings = resolveFuzzParallelJobs(rawArgs);
1248
+ const clean = rawArgs.includes("--clean");
1249
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1250
+ const { jobs, buildJobs, runJobs } = resolveEffectiveParallelJobs(parallelSettings, fuzzFiles.length);
1251
+ if (jobs > 1) {
1252
+ const results = await runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean);
1253
+ const reporterSession = await createRunReporter(configPath);
1254
+ reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
1255
+ reporterSession.reporter.flush?.();
1256
+ process.exit(results.some(hasFuzzFailures) ? 1 : 0);
766
1257
  return;
767
- const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
768
- const suffix = showPerModeTimes
769
- ? ` ${chalk.dim(`(${modes.join(",")})`)}`
770
- : "";
771
- const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
772
- process.stdout.write(`\r\x1b[2K${line}`);
773
- }
774
- function formatMatrixModeTime(ms) {
775
- const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
776
- return `${safeMs.toFixed(1)}ms`;
777
- }
778
- function formatMatrixAverageTime(results) {
779
- if (!results.length)
780
- return "0.0ms";
781
- let total = 0;
782
- for (const result of results) {
783
- total += Number.isFinite(result.stats.time)
784
- ? Math.max(0, result.stats.time)
785
- : 0;
786
1258
  }
787
- return `${(total / results.length).toFixed(1)}ms`;
1259
+ const reporterSession = await createRunReporter(configPath);
1260
+ const results = await runFuzzMatrixResults(configPath, selectors, modes, overrides, reporterSession.reporter);
1261
+ reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
1262
+ reporterSession.reporter.flush?.();
1263
+ process.exit(results.some(hasFuzzFailures) ? 1 : 0);
788
1264
  }
789
- function buildModeSummary(modeState, totalModes) {
790
- const total = Math.max(totalModes, modeState.length, 1);
1265
+ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal) {
1266
+ const files = await resolveSelectedFiles(configPath, selectors);
1267
+ if (!files.length) {
1268
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1269
+ }
1270
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1271
+ const reporter = reporterSession.reporter;
1272
+ const snapshotEnabled = runFlags.snapshot !== false;
1273
+ reporter.onRunStart?.({
1274
+ runtimeName: reporterSession.runtimeName,
1275
+ clean: runFlags.clean,
1276
+ verbose: runFlags.verbose,
1277
+ snapshotEnabled,
1278
+ createSnapshots: runFlags.createSnapshots,
1279
+ });
1280
+ const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
1281
+ const results = new Array(files.length);
1282
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1283
+ const runLimit = createAsyncLimiter(runFlags.runJobs);
1284
+ const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
1285
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1286
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1287
+ const buffered = await createBufferedReporter(configPath, modeName);
1288
+ const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
1289
+ reporter: buffered.reporter,
1290
+ reporterKind: buffered.reporterKind,
1291
+ modeName,
1292
+ emitRunComplete: false,
1293
+ fileSummaryTotal: 1,
1294
+ modeSummaryTotal,
1295
+ modeSummaryExecuted: 1,
1296
+ buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
1297
+ }));
1298
+ buffered.reporter.flush?.();
1299
+ results[index] = result;
1300
+ queueDisplay.complete(token, buffered.output());
1301
+ });
1302
+ queueDisplay.flush();
1303
+ const summary = aggregateRunResults(results);
1304
+ summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
1305
+ reporter.onRunComplete?.({
1306
+ clean: runFlags.clean,
1307
+ snapshotEnabled,
1308
+ showCoverage: runFlags.showCoverage,
1309
+ buildTime: 0,
1310
+ snapshotSummary: summary.snapshotSummary,
1311
+ coverageSummary: summary.coverageSummary,
1312
+ stats: summary.stats,
1313
+ reports: summary.reports,
1314
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
1315
+ });
1316
+ reporter.flush?.();
1317
+ return results.some((result) => result.failed);
1318
+ }
1319
+ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
1320
+ const files = await resolveSelectedFiles(configPath, selectors);
1321
+ if (!files.length) {
1322
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1323
+ }
1324
+ const reporterSession = await createRunReporter(configPath);
1325
+ const reporter = reporterSession.reporter;
1326
+ const snapshotEnabled = runFlags.snapshot !== false;
1327
+ reporter.onRunStart?.({
1328
+ runtimeName: reporterSession.runtimeName,
1329
+ clean: runFlags.clean,
1330
+ verbose: runFlags.verbose,
1331
+ snapshotEnabled,
1332
+ createSnapshots: runFlags.createSnapshots,
1333
+ });
1334
+ const silentReporter = {};
1335
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
1336
+ const showPerModeTimes = Boolean(runFlags.verbose);
1337
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1338
+ const ordered = new Array(files.length);
1339
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1340
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1341
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1342
+ const buildIntervals = [];
1343
+ try {
1344
+ await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1345
+ const fileName = path.basename(file);
1346
+ const token = renderQueuedFileStart(queueDisplay, fileName);
1347
+ const fileResults = [];
1348
+ const modeTimes = modes.map(() => "...");
1349
+ for (let i = 0; i < modes.length; i++) {
1350
+ const modeName = modes[i];
1351
+ const buildStartedAt = Date.now();
1352
+ await buildPool.buildFileMode({
1353
+ configPath,
1354
+ file,
1355
+ modeName,
1356
+ });
1357
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1358
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
1359
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1360
+ const result = await run(runFlags, configPath, [file], false, {
1361
+ reporter: silentReporter,
1362
+ reporterKind: "default",
1363
+ emitRunStart: false,
1364
+ emitRunComplete: false,
1365
+ logFileName: `run.${artifactKey}.log.json`,
1366
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1367
+ buildCommand: formatBuildInvocation(buildInvocation),
1368
+ modeName,
1369
+ });
1370
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
1371
+ fileResults.push(result);
1372
+ }
1373
+ ordered[fileIndex] = { fileName, fileResults, modeTimes };
1374
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1375
+ });
1376
+ }
1377
+ finally {
1378
+ await buildPool.close();
1379
+ }
1380
+ queueDisplay.flush();
1381
+ const allResults = [];
1382
+ const modeState = modes.map(() => ({ failed: false, passed: false }));
1383
+ const fileState = files.map(() => ({ failed: false, passed: false }));
1384
+ for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
1385
+ const fileResults = ordered[fileIndex].fileResults;
1386
+ for (let i = 0; i < fileResults.length; i++) {
1387
+ const result = fileResults[i];
1388
+ allResults.push(result);
1389
+ if (result.failed)
1390
+ modeState[i].failed = true;
1391
+ else if (result.stats.passedFiles > 0)
1392
+ modeState[i].passed = true;
1393
+ }
1394
+ const verdict = resolveMatrixVerdict(fileResults);
1395
+ if (verdict == "fail")
1396
+ fileState[fileIndex].failed = true;
1397
+ else if (verdict == "ok")
1398
+ fileState[fileIndex].passed = true;
1399
+ }
1400
+ const summary = aggregateRunResults(allResults);
1401
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1402
+ reporter.onRunComplete?.({
1403
+ clean: runFlags.clean,
1404
+ snapshotEnabled,
1405
+ showCoverage: runFlags.showCoverage,
1406
+ buildTime: getMergedIntervalDuration(buildIntervals),
1407
+ snapshotSummary: summary.snapshotSummary,
1408
+ coverageSummary: summary.coverageSummary,
1409
+ stats: summary.stats,
1410
+ reports: summary.reports,
1411
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
1412
+ });
1413
+ reporter.flush?.();
1414
+ return allResults.some((result) => result.failed);
1415
+ }
1416
+ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
1417
+ const files = await resolveSelectedFiles(configPath, selectors);
1418
+ if (!files.length && !fuzzEnabled) {
1419
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1420
+ }
1421
+ const reporterSession = await createRunReporter(configPath, undefined, modeName);
1422
+ const reporter = reporterSession.reporter;
1423
+ const snapshotEnabled = runFlags.snapshot !== false;
1424
+ reporter.onRunStart?.({
1425
+ runtimeName: reporterSession.runtimeName,
1426
+ clean: runFlags.clean,
1427
+ verbose: runFlags.verbose,
1428
+ snapshotEnabled,
1429
+ createSnapshots: runFlags.createSnapshots,
1430
+ });
1431
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1432
+ const results = new Array(files.length);
1433
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1434
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1435
+ const buildIntervals = [];
1436
+ if (files.length) {
1437
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1438
+ try {
1439
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1440
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1441
+ const buildStartedAt = Date.now();
1442
+ await buildPool.buildFileMode({
1443
+ configPath,
1444
+ file,
1445
+ modeName,
1446
+ featureToggles: buildFeatureToggles,
1447
+ });
1448
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1449
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1450
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1451
+ const buffered = await createBufferedReporter(configPath, modeName);
1452
+ const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
1453
+ reporter: buffered.reporter,
1454
+ reporterKind: buffered.reporterKind,
1455
+ emitRunComplete: false,
1456
+ logFileName: `test.${artifactKey}.log.json`,
1457
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1458
+ buildCommand: formatBuildInvocation(buildInvocation),
1459
+ modeName,
1460
+ });
1461
+ buffered.reporter.flush?.();
1462
+ results[index] = result;
1463
+ queueDisplay.complete(token, buffered.output());
1464
+ });
1465
+ }
1466
+ finally {
1467
+ await buildPool.close();
1468
+ }
1469
+ }
1470
+ queueDisplay.flush();
1471
+ const runResults = results.filter(Boolean);
1472
+ const summary = aggregateRunResults(runResults);
1473
+ summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
1474
+ let failed = runResults.some((result) => result.failed);
1475
+ let fuzzSummary;
1476
+ if (fuzzEnabled) {
1477
+ if (reporterSession.reporterKind == "default") {
1478
+ process.stdout.write("\n");
1479
+ }
1480
+ const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
1481
+ if (fuzzResults.some(hasFuzzFailures))
1482
+ failed = true;
1483
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1484
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1485
+ }
1486
+ reporter.onRunComplete?.({
1487
+ clean: runFlags.clean,
1488
+ snapshotEnabled,
1489
+ showCoverage: runFlags.showCoverage,
1490
+ buildTime: getMergedIntervalDuration(buildIntervals),
1491
+ snapshotSummary: summary.snapshotSummary,
1492
+ coverageSummary: summary.coverageSummary,
1493
+ stats: summary.stats,
1494
+ reports: summary.reports,
1495
+ fuzzSummary,
1496
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
1497
+ });
1498
+ reporter.flush?.();
1499
+ return failed;
1500
+ }
1501
+ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
1502
+ const files = await resolveSelectedFiles(configPath, selectors);
1503
+ if (!files.length) {
1504
+ if (!fuzzEnabled) {
1505
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
1506
+ }
1507
+ const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
1508
+ if (!fuzzFiles.length) {
1509
+ throw await buildNoTestFilesMatchedError(configPath, selectors, true);
1510
+ }
1511
+ }
1512
+ const reporterSession = await createRunReporter(configPath);
1513
+ const reporter = reporterSession.reporter;
1514
+ const snapshotEnabled = runFlags.snapshot !== false;
1515
+ reporter.onRunStart?.({
1516
+ runtimeName: reporterSession.runtimeName,
1517
+ clean: runFlags.clean,
1518
+ verbose: runFlags.verbose,
1519
+ snapshotEnabled,
1520
+ createSnapshots: runFlags.createSnapshots,
1521
+ });
1522
+ const silentReporter = {};
1523
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
1524
+ const showPerModeTimes = Boolean(runFlags.verbose);
1525
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1526
+ const ordered = new Array(files.length);
1527
+ const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
1528
+ const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
1529
+ const buildPool = new BuildWorkerPool(runFlags.buildJobs);
1530
+ const buildIntervals = [];
1531
+ try {
1532
+ await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
1533
+ const fileName = path.basename(file);
1534
+ const token = renderQueuedFileStart(queueDisplay, fileName);
1535
+ const fileResults = [];
1536
+ const modeTimes = modes.map(() => "...");
1537
+ for (let i = 0; i < modes.length; i++) {
1538
+ const modeName = modes[i];
1539
+ const buildStartedAt = Date.now();
1540
+ await buildPool.buildFileMode({
1541
+ configPath,
1542
+ file,
1543
+ modeName,
1544
+ featureToggles: buildFeatureToggles,
1545
+ });
1546
+ buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1547
+ const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1548
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1549
+ const result = await run(runFlags, configPath, [file], false, {
1550
+ reporter: silentReporter,
1551
+ reporterKind: "default",
1552
+ emitRunStart: false,
1553
+ emitRunComplete: false,
1554
+ logFileName: `test.${artifactKey}.log.json`,
1555
+ coverageFileName: `coverage.${artifactKey}.log.json`,
1556
+ buildCommand: formatBuildInvocation(buildInvocation),
1557
+ modeName,
1558
+ });
1559
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
1560
+ fileResults.push(result);
1561
+ }
1562
+ ordered[fileIndex] = { fileName, fileResults, modeTimes };
1563
+ queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
1564
+ });
1565
+ }
1566
+ finally {
1567
+ await buildPool.close();
1568
+ }
1569
+ queueDisplay.flush();
1570
+ const allResults = [];
1571
+ const modeState = modes.map(() => ({ failed: false, passed: false }));
1572
+ const fileState = files.map(() => ({ failed: false, passed: false }));
1573
+ for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
1574
+ const entry = ordered[fileIndex];
1575
+ for (let i = 0; i < entry.fileResults.length; i++) {
1576
+ const result = entry.fileResults[i];
1577
+ allResults.push(result);
1578
+ if (result.failed)
1579
+ modeState[i].failed = true;
1580
+ else if (result.stats.passedFiles > 0)
1581
+ modeState[i].passed = true;
1582
+ }
1583
+ const verdict = resolveMatrixVerdict(entry.fileResults);
1584
+ if (verdict == "fail")
1585
+ fileState[fileIndex].failed = true;
1586
+ else if (verdict == "ok")
1587
+ fileState[fileIndex].passed = true;
1588
+ }
1589
+ const summary = aggregateRunResults(allResults);
1590
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
1591
+ let failed = allResults.some((result) => result.failed);
1592
+ let fuzzSummary;
1593
+ if (fuzzEnabled) {
1594
+ if (reporterSession.reporterKind == "default") {
1595
+ process.stdout.write("\n");
1596
+ }
1597
+ const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
1598
+ if (fuzzResults.some(hasFuzzFailures))
1599
+ failed = true;
1600
+ fuzzSummary = summarizeFuzzExecutions(fuzzResults);
1601
+ buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
1602
+ }
1603
+ reporter.onRunComplete?.({
1604
+ clean: runFlags.clean,
1605
+ snapshotEnabled,
1606
+ showCoverage: runFlags.showCoverage,
1607
+ buildTime: getMergedIntervalDuration(buildIntervals),
1608
+ snapshotSummary: summary.snapshotSummary,
1609
+ coverageSummary: summary.coverageSummary,
1610
+ stats: summary.stats,
1611
+ reports: summary.reports,
1612
+ fuzzSummary,
1613
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
1614
+ });
1615
+ reporter.flush?.();
1616
+ return failed;
1617
+ }
1618
+ async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
1619
+ const files = await resolveSelectedFuzzFiles(configPath, selectors);
1620
+ if (!files.length) {
1621
+ throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
1622
+ }
1623
+ const ordered = new Array(files.length);
1624
+ const queueDisplay = new ParallelQueueDisplay(!clean);
1625
+ const poolWidth = Math.max(jobs, buildJobs, runJobs);
1626
+ await runOrderedPool(files, poolWidth, async (file, index) => {
1627
+ const token = renderQueuedFileStart(queueDisplay, path.basename(file));
1628
+ const fileResults = [];
1629
+ for (const modeName of modes) {
1630
+ const modeResults = await fuzz(configPath, [file], modeName, overrides);
1631
+ fileResults.push(...modeResults);
1632
+ }
1633
+ ordered[index] = fileResults;
1634
+ const buffered = await createBufferedReporter(configPath);
1635
+ buffered.reporter.onFuzzFileComplete?.({ file, results: fileResults });
1636
+ buffered.reporter.flush?.();
1637
+ queueDisplay.complete(token, buffered.output());
1638
+ });
1639
+ queueDisplay.flush();
1640
+ return ordered.flat();
1641
+ }
1642
+ async function runFuzzMatrixResults(configPath, selectors, modes, overrides, reporter) {
1643
+ const files = await resolveSelectedFuzzFiles(configPath, selectors);
1644
+ if (!files.length) {
1645
+ throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
1646
+ }
1647
+ const results = [];
1648
+ for (const file of files) {
1649
+ const fileResults = [];
1650
+ for (const modeName of modes) {
1651
+ const modeResults = await fuzz(configPath, [file], modeName, overrides);
1652
+ fileResults.push(...modeResults);
1653
+ results.push(...modeResults);
1654
+ }
1655
+ reporter?.onFuzzFileComplete?.({ file, results: fileResults });
1656
+ }
1657
+ return results;
1658
+ }
1659
+ function hasFuzzFailures(result) {
1660
+ if (result.crashes > 0)
1661
+ return true;
1662
+ return result.fuzzers.some((fuzzer) => fuzzer.failed > 0);
1663
+ }
1664
+ function buildFuzzCompleteEvent(results, modes) {
1665
+ return {
1666
+ results,
1667
+ time: results.reduce((sum, item) => sum + item.time, 0),
1668
+ buildTime: getMergedIntervalDuration(collectFuzzBuildIntervals(results)),
1669
+ fuzzingSummary: summarizeFuzzExecutions(results),
1670
+ suiteSummary: summarizeFuzzSuites(results),
1671
+ modeSummary: summarizeFuzzModes(results, modes),
1672
+ };
1673
+ }
1674
+ function collectFuzzBuildIntervals(results) {
1675
+ return results.map((result) => ({
1676
+ start: result.buildStartedAt,
1677
+ end: result.buildFinishedAt,
1678
+ }));
1679
+ }
1680
+ function getMergedIntervalDuration(intervals) {
1681
+ if (!intervals.length)
1682
+ return 0;
1683
+ const sorted = intervals
1684
+ .map((interval) => ({
1685
+ start: Math.min(interval.start, interval.end),
1686
+ end: Math.max(interval.start, interval.end),
1687
+ }))
1688
+ .sort((a, b) => a.start - b.start);
1689
+ let total = 0;
1690
+ let currentStart = sorted[0].start;
1691
+ let currentEnd = sorted[0].end;
1692
+ for (let i = 1; i < sorted.length; i++) {
1693
+ const interval = sorted[i];
1694
+ if (interval.start <= currentEnd) {
1695
+ currentEnd = Math.max(currentEnd, interval.end);
1696
+ continue;
1697
+ }
1698
+ total += currentEnd - currentStart;
1699
+ currentStart = interval.start;
1700
+ currentEnd = interval.end;
1701
+ }
1702
+ total += currentEnd - currentStart;
1703
+ return total;
1704
+ }
1705
+ function summarizeFuzzExecutions(results) {
1706
+ return {
1707
+ failed: results.reduce((sum, item) => sum +
1708
+ item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.failed + fuzzer.crashed, 0), 0),
1709
+ skipped: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.skipped, 0), 0),
1710
+ total: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.runs, 0), 0),
1711
+ };
1712
+ }
1713
+ function summarizeFuzzSuites(results) {
1714
+ return {
1715
+ failed: results.reduce((sum, item) => sum +
1716
+ item.fuzzers.filter((fuzzer) => fuzzer.failed > 0 || fuzzer.crashed > 0)
1717
+ .length, 0),
1718
+ skipped: results.reduce((sum, item) => sum + item.fuzzers.filter((fuzzer) => fuzzer.skipped > 0).length, 0),
1719
+ total: results.reduce((sum, item) => sum + item.fuzzers.length, 0),
1720
+ };
1721
+ }
1722
+ function summarizeFuzzModes(results, modes) {
1723
+ const total = Math.max(modes.length, 1);
1724
+ const state = new Map();
1725
+ for (const modeName of modes) {
1726
+ state.set(modeName ?? "default", { failed: false, passed: false });
1727
+ }
1728
+ for (const result of results) {
1729
+ const current = state.get(result.modeName) ?? {
1730
+ failed: false,
1731
+ passed: false,
1732
+ };
1733
+ if (hasFuzzFailures(result))
1734
+ current.failed = true;
1735
+ else if (!isSkippedFuzzResult(result))
1736
+ current.passed = true;
1737
+ state.set(result.modeName, current);
1738
+ }
1739
+ let failed = 0;
1740
+ let skipped = 0;
1741
+ for (const mode of state.values()) {
1742
+ if (mode.failed)
1743
+ failed++;
1744
+ else if (!mode.passed)
1745
+ skipped++;
1746
+ }
1747
+ return { failed, skipped, total };
1748
+ }
1749
+ function isSkippedFuzzResult(result) {
1750
+ return (result.crashes == 0 &&
1751
+ result.fuzzers.length > 0 &&
1752
+ result.fuzzers.every((fuzzer) => fuzzer.skipped > 0));
1753
+ }
1754
+ function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
1755
+ const line = formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes);
1756
+ if (liveMatrix)
1757
+ clearLiveLine();
1758
+ process.stdout.write(line + "\n");
1759
+ }
1760
+ function formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes) {
1761
+ const verdict = resolveMatrixVerdict(results);
1762
+ const badge = verdict == "fail"
1763
+ ? chalk.bgRed.white(" FAIL ")
1764
+ : verdict == "ok"
1765
+ ? chalk.bgGreenBright.black(" PASS ")
1766
+ : chalk.bgBlackBright.white(" SKIP ");
1767
+ const avg = formatMatrixAverageTime(results);
1768
+ const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
1769
+ const failedModes = results
1770
+ .map((result, index) => (result.failed ? modes[index] : null))
1771
+ .filter((mode) => Boolean(mode));
1772
+ const suffix = showPerModeTimes
1773
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
1774
+ : failedModes.length
1775
+ ? ` ${chalk.dim(`(failed: ${failedModes.join(", ")})`)}`
1776
+ : "";
1777
+ return `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
1778
+ }
1779
+ function resolveMatrixVerdict(results) {
1780
+ if (results.some((result) => result.failed))
1781
+ return "fail";
1782
+ const hasPass = results.some((result) => result.stats.passedFiles > 0);
1783
+ if (hasPass)
1784
+ return "ok";
1785
+ return "skip";
1786
+ }
1787
+ function canRewriteStdout() {
1788
+ return Boolean(process.stdout.isTTY);
1789
+ }
1790
+ function clearLiveLine() {
1791
+ if (!canRewriteStdout())
1792
+ return;
1793
+ process.stdout.write("\r\x1b[2K");
1794
+ }
1795
+ function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
1796
+ if (!canRewriteStdout())
1797
+ return;
1798
+ const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
1799
+ const suffix = showPerModeTimes
1800
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
1801
+ : "";
1802
+ const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
1803
+ process.stdout.write(`\r\x1b[2K${line}`);
1804
+ }
1805
+ function formatMatrixModeTime(ms) {
1806
+ const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
1807
+ return `${safeMs.toFixed(1)}ms`;
1808
+ }
1809
+ function formatMatrixAverageTime(results) {
1810
+ if (!results.length)
1811
+ return "0.0ms";
1812
+ let total = 0;
1813
+ for (const result of results) {
1814
+ total += Number.isFinite(result.stats.time)
1815
+ ? Math.max(0, result.stats.time)
1816
+ : 0;
1817
+ }
1818
+ return `${(total / results.length).toFixed(1)}ms`;
1819
+ }
1820
+ function buildModeSummary(modeState, totalModes) {
1821
+ const total = Math.max(totalModes, modeState.length, 1);
791
1822
  let skipped = Math.max(0, total - modeState.length);
792
1823
  let failed = 0;
793
1824
  for (const mode of modeState) {
@@ -850,10 +1881,19 @@ function resolveConfiguredModeTotal(configPath) {
850
1881
  const configuredModes = Object.keys(config.modes).length;
851
1882
  return configuredModes || 1;
852
1883
  }
853
- async function resolveConfiguredFileTotal(configPath) {
854
- const files = await resolveSelectedFiles(configPath, []);
1884
+ async function resolveConfiguredFileTotal(configPath, selectors = []) {
1885
+ const files = await resolveSelectedFiles(configPath, selectors);
855
1886
  return files.length;
856
1887
  }
1888
+ async function previewBuildCommands(configPath, selectors, modeName, featureToggles) {
1889
+ const files = await resolveSelectedFiles(configPath, selectors);
1890
+ const out = {};
1891
+ for (const file of files) {
1892
+ const invocation = await getBuildInvocationPreview(configPath, file, modeName, featureToggles);
1893
+ out[file] = formatBuildInvocation(invocation);
1894
+ }
1895
+ return out;
1896
+ }
857
1897
  function resolveExecutionModes(configPath, selectedModes) {
858
1898
  if (selectedModes.length)
859
1899
  return selectedModes;
@@ -872,15 +1912,35 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
872
1912
  const specs = matches.filter((file) => file.endsWith(".spec.ts"));
873
1913
  return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
874
1914
  }
875
- async function buildNoTestFilesMatchedError(configPath, selectors) {
1915
+ async function resolveSelectedFuzzFiles(configPath, selectors) {
1916
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1917
+ const config = loadConfig(resolvedConfigPath, false);
1918
+ const patterns = resolveFuzzPatterns(config.fuzz.input, selectors);
1919
+ const matches = await glob(patterns);
1920
+ const fuzzFiles = matches.filter((file) => file.endsWith(".fuzz.ts"));
1921
+ return [...new Set(fuzzFiles)].sort((a, b) => a.localeCompare(b));
1922
+ }
1923
+ async function resolveSelectedTestInputs(configPath, selectors) {
1924
+ const [specs, fuzz] = await Promise.all([
1925
+ resolveSelectedFiles(configPath, selectors),
1926
+ resolveSelectedFuzzFiles(configPath, selectors),
1927
+ ]);
1928
+ return { specs, fuzz };
1929
+ }
1930
+ async function buildNoTestFilesMatchedError(configPath, selectors, includeFuzz = false) {
876
1931
  const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
877
1932
  const lines = [`No test files matched: ${scope}`];
878
1933
  const configuredFiles = await resolveSelectedFiles(configPath, [], false);
1934
+ const configuredFuzzFiles = includeFuzz
1935
+ ? await resolveSelectedFuzzFiles(configPath, [])
1936
+ : [];
879
1937
  if (!selectors.length) {
880
1938
  lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
881
1939
  return new Error(lines.join("\n"));
882
1940
  }
883
- const suggestions = suggestClosestSuites(selectors, configuredFiles);
1941
+ const suggestions = suggestClosestSuites(selectors, includeFuzz
1942
+ ? [...configuredFiles, ...configuredFuzzFiles]
1943
+ : configuredFiles);
884
1944
  if (suggestions.length) {
885
1945
  lines.push(`Closest suite names: ${suggestions.join(", ")}`);
886
1946
  }
@@ -894,6 +1954,13 @@ async function buildNoTestFilesMatchedError(configPath, selectors) {
894
1954
  else {
895
1955
  lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
896
1956
  }
1957
+ if (includeFuzz && configuredFuzzFiles.length) {
1958
+ const sample = configuredFuzzFiles
1959
+ .slice(0, 5)
1960
+ .map((file) => path.basename(file))
1961
+ .join(", ");
1962
+ lines.push(`Configured fuzzers (${configuredFuzzFiles.length}): ${sample}${configuredFuzzFiles.length > 5 ? ", ..." : ""}`);
1963
+ }
897
1964
  lines.push('Run "ast test --list" to inspect resolved files.');
898
1965
  return new Error(lines.join("\n"));
899
1966
  }
@@ -980,6 +2047,27 @@ function resolveInputPatterns(configured, selectors) {
980
2047
  }
981
2048
  return [...patterns];
982
2049
  }
2050
+ function resolveFuzzPatterns(configured, selectors) {
2051
+ const configuredInputs = Array.isArray(configured)
2052
+ ? configured
2053
+ : [configured];
2054
+ if (!selectors.length)
2055
+ return configuredInputs;
2056
+ const patterns = new Set();
2057
+ for (const selector of expandSelectors(selectors)) {
2058
+ if (!selector)
2059
+ continue;
2060
+ if (isBareSuiteSelector(selector)) {
2061
+ const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
2062
+ for (const configuredInput of configuredInputs) {
2063
+ patterns.add(path.join(path.dirname(configuredInput), `${base}.fuzz.ts`));
2064
+ }
2065
+ continue;
2066
+ }
2067
+ patterns.add(selector);
2068
+ }
2069
+ return [...patterns];
2070
+ }
983
2071
  function expandSelectors(selectors) {
984
2072
  const expanded = [];
985
2073
  for (const selector of selectors) {
@@ -1064,7 +2152,184 @@ function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpec
1064
2152
  const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
1065
2153
  return `${stem}.${disambiguator}${ext}`;
1066
2154
  }
1067
- async function listExecutionPlan(command, configPath, selectors, modes, listFlags) {
2155
+ async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
2156
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
2157
+ const config = loadConfig(resolvedConfigPath, true);
2158
+ const missing = [];
2159
+ for (const modeName of modes) {
2160
+ const applied = applyMode(config, modeName);
2161
+ const active = applied.config;
2162
+ if (!usesWebBrowser(active))
2163
+ continue;
2164
+ const requestedBrowser = browserOverride?.trim() || active.runOptions.runtime.browser.trim();
2165
+ const resolved = resolveBrowserSelection(requestedBrowser);
2166
+ if (!resolved) {
2167
+ missing.push({ modeName, browser: requestedBrowser });
2168
+ continue;
2169
+ }
2170
+ active.runOptions.runtime.browser = resolved.browser;
2171
+ process.env.BROWSER = resolved.browser;
2172
+ }
2173
+ if (!missing.length)
2174
+ return;
2175
+ await handleMissingWebBrowsers(missing);
2176
+ }
2177
+ function resolveBrowserSelection(requested = "") {
2178
+ if (requested.trim().length) {
2179
+ return resolveNamedBrowser(requested);
2180
+ }
2181
+ const envBrowser = process.env.BROWSER?.trim() ?? "";
2182
+ if (envBrowser.length) {
2183
+ return resolveNamedBrowser(envBrowser);
2184
+ }
2185
+ const candidates = [
2186
+ "chromium",
2187
+ "chromium-browser",
2188
+ "google-chrome",
2189
+ "google-chrome-stable",
2190
+ "chrome",
2191
+ "msedge",
2192
+ "firefox",
2193
+ ];
2194
+ for (const candidate of candidates) {
2195
+ if (hasExecutable(candidate)) {
2196
+ return { browser: candidate };
2197
+ }
2198
+ }
2199
+ const playwrightFallback = resolvePlaywrightBrowserExecutable("chromium") ??
2200
+ resolvePlaywrightBrowserExecutable("firefox");
2201
+ if (playwrightFallback) {
2202
+ return { browser: playwrightFallback };
2203
+ }
2204
+ return null;
2205
+ }
2206
+ function resolveNamedBrowser(browser) {
2207
+ const normalized = browser.trim().toLowerCase();
2208
+ if (!normalized.length)
2209
+ return null;
2210
+ if (browser.includes("/") ||
2211
+ browser.includes("\\") ||
2212
+ path.isAbsolute(browser)) {
2213
+ return hasExecutable(browser) ? { browser } : null;
2214
+ }
2215
+ const aliases = {
2216
+ chromium: ["chromium", "chromium-browser"],
2217
+ chrome: [
2218
+ "google-chrome",
2219
+ "google-chrome-stable",
2220
+ "chrome",
2221
+ "chromium",
2222
+ "chromium-browser",
2223
+ ],
2224
+ firefox: ["firefox"],
2225
+ webkit: [],
2226
+ };
2227
+ const candidates = aliases[normalized] ?? [browser];
2228
+ for (const candidate of candidates) {
2229
+ if (hasExecutable(candidate)) {
2230
+ return { browser: candidate };
2231
+ }
2232
+ }
2233
+ const playwrightFallback = resolvePlaywrightBrowserExecutable(normalized);
2234
+ if (playwrightFallback) {
2235
+ return { browser: playwrightFallback };
2236
+ }
2237
+ return null;
2238
+ }
2239
+ function usesWebBrowser(config) {
2240
+ return (config.buildOptions.target == "web" ||
2241
+ config.runOptions.runtime.browser.length > 0 ||
2242
+ config.runOptions.runtime.cmd.includes("default.web.js"));
2243
+ }
2244
+ async function handleMissingWebBrowsers(missing) {
2245
+ const scope = missing
2246
+ .map((entry) => entry.browser?.length
2247
+ ? `${entry.modeName ?? "default"} (${entry.browser})`
2248
+ : (entry.modeName ?? "default"))
2249
+ .join(", ");
2250
+ const details = "no web-capable browser was found in PATH, BROWSER, or Playwright cache";
2251
+ if (!canPromptForWebInstall()) {
2252
+ 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".`);
2253
+ }
2254
+ process.stdout.write(chalk.bold.blue("◇ Browser Setup Needed") +
2255
+ "\n" +
2256
+ `│ ${details}\n` +
2257
+ "│\n");
2258
+ const choice = await promptLine("Install Chromium with Playwright now? [Y/n] ");
2259
+ const normalized = choice.trim().toLowerCase();
2260
+ if (normalized == "n" || normalized == "no") {
2261
+ 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.');
2262
+ }
2263
+ if (normalized != "" && normalized != "y" && normalized != "yes") {
2264
+ throw new Error(`invalid answer "${choice}". Expected yes or no.`);
2265
+ }
2266
+ const selected = "chromium";
2267
+ process.stdout.write(chalk.dim(`installing ${selected} via Playwright...\n`));
2268
+ const install = spawnSync("npx", ["-y", "playwright", "install", selected], {
2269
+ stdio: "inherit",
2270
+ shell: false,
2271
+ });
2272
+ if (install.status !== 0) {
2273
+ throw new Error(`Playwright browser install failed for ${selected}`);
2274
+ }
2275
+ const browserPath = resolvePlaywrightBrowserExecutable(selected);
2276
+ if (!browserPath) {
2277
+ throw new Error(`Playwright installed ${selected}, but as-test could not locate the browser executable`);
2278
+ }
2279
+ process.env.BROWSER = browserPath;
2280
+ }
2281
+ function canPromptForWebInstall() {
2282
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
2283
+ }
2284
+ function promptLine(question) {
2285
+ return new Promise((resolve) => {
2286
+ const rl = createInterface({
2287
+ input: process.stdin,
2288
+ output: process.stdout,
2289
+ });
2290
+ rl.question(question, (answer) => {
2291
+ rl.close();
2292
+ resolve(answer);
2293
+ });
2294
+ });
2295
+ }
2296
+ function resolvePlaywrightBrowserExecutable(browser) {
2297
+ const cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
2298
+ if (!cacheRoot.length || !existsSync(cacheRoot))
2299
+ return null;
2300
+ const map = {
2301
+ chromium: ["chromium-*/chrome-linux64/chrome"],
2302
+ chrome: ["chromium-*/chrome-linux64/chrome"],
2303
+ firefox: ["firefox-*/firefox/firefox"],
2304
+ webkit: ["webkit-*/pw_run.sh"],
2305
+ };
2306
+ const patterns = map[browser] ?? [];
2307
+ for (const pattern of patterns) {
2308
+ const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
2309
+ if (matches.length)
2310
+ return matches[matches.length - 1];
2311
+ }
2312
+ return null;
2313
+ }
2314
+ function hasExecutable(command) {
2315
+ if (!command.length)
2316
+ return false;
2317
+ if (command.includes("/") || command.includes("\\")) {
2318
+ return existsSync(command);
2319
+ }
2320
+ const pathValue = process.env.PATH ?? "";
2321
+ const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
2322
+ for (const base of pathValue.split(path.delimiter)) {
2323
+ if (!base.length)
2324
+ continue;
2325
+ for (const suffix of suffixes) {
2326
+ if (existsSync(path.join(base, command + suffix)))
2327
+ return true;
2328
+ }
2329
+ }
2330
+ return false;
2331
+ }
2332
+ async function listExecutionPlan(command, configPath, selectors, modes, listFlags, fuzzEnabled = false) {
1068
2333
  const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1069
2334
  const config = loadConfig(resolvedConfigPath, true);
1070
2335
  const configuredModes = Object.keys(config.modes);
@@ -1093,36 +2358,86 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
1093
2358
  }
1094
2359
  if (!listFlags.list)
1095
2360
  return;
1096
- const files = await resolveSelectedFiles(configPath, selectors);
1097
- if (!files.length) {
2361
+ const specFiles = command == "fuzz" ? [] : await resolveSelectedFiles(configPath, selectors);
2362
+ const fuzzFiles = command == "fuzz"
2363
+ ? await resolveSelectedFuzzFiles(configPath, selectors)
2364
+ : command == "test" && fuzzEnabled
2365
+ ? await resolveSelectedFuzzFiles(configPath, selectors)
2366
+ : [];
2367
+ const files = command == "fuzz" ? fuzzFiles : specFiles;
2368
+ if (!specFiles.length && !fuzzFiles.length) {
1098
2369
  const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
1099
- throw new Error(`No test files matched: ${scope}`);
2370
+ throw new Error(command == "fuzz"
2371
+ ? `No fuzz files matched: ${scope}`
2372
+ : `No test files matched: ${scope}`);
1100
2373
  }
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`);
2374
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(specFiles);
2375
+ const duplicateFuzzBasenames = resolveDuplicateSpecBasenames(fuzzFiles);
2376
+ if (specFiles.length) {
2377
+ process.stdout.write(chalk.bold("Resolved files:\n"));
2378
+ for (const file of specFiles) {
2379
+ process.stdout.write(` - ${file}\n`);
2380
+ }
2381
+ process.stdout.write("\n");
2382
+ }
2383
+ if (fuzzFiles.length && command == "test") {
2384
+ process.stdout.write(chalk.bold("Resolved fuzz files:\n"));
2385
+ for (const file of fuzzFiles) {
2386
+ process.stdout.write(` - ${file}\n`);
2387
+ }
2388
+ process.stdout.write("\n");
2389
+ }
2390
+ if (command == "fuzz" && fuzzFiles.length) {
2391
+ process.stdout.write(chalk.bold("Resolved files:\n"));
2392
+ for (const file of fuzzFiles) {
2393
+ process.stdout.write(` - ${file}\n`);
2394
+ }
2395
+ process.stdout.write("\n");
1105
2396
  }
1106
- process.stdout.write("\n");
1107
2397
  for (const modeName of modes) {
1108
2398
  const applied = applyMode(config, modeName);
1109
2399
  const active = applied.config;
1110
2400
  const modeLabel = modeName ?? "default";
1111
2401
  process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
1112
- process.stdout.write(` target: ${active.buildOptions.target}\n`);
2402
+ process.stdout.write(` target: ${command == "fuzz" ? "bindings" : active.buildOptions.target}\n`);
1113
2403
  process.stdout.write(` outDir: ${active.outDir}\n`);
1114
- if (command != "build") {
2404
+ if (command == "run" || command == "test") {
1115
2405
  process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
2406
+ if (usesWebBrowser(active)) {
2407
+ process.stdout.write(` browser: ${active.runOptions.runtime.browser || "(auto)"}\n`);
2408
+ }
1116
2409
  }
1117
- const envOverrides = modeName
1118
- ? (config.modes[modeName]?.env ?? {})
1119
- : config.env;
2410
+ const envOverrides = {
2411
+ ...config.env,
2412
+ ...(modeName ? (config.modes[modeName]?.env ?? {}) : {}),
2413
+ ...(command == "build"
2414
+ ? active.buildOptions.env
2415
+ : command == "run" || command == "test"
2416
+ ? active.runOptions.env
2417
+ : {}),
2418
+ };
1120
2419
  const envKeys = Object.keys(envOverrides);
1121
2420
  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`);
2421
+ if (specFiles.length) {
2422
+ process.stdout.write(" artifacts:\n");
2423
+ for (const file of specFiles) {
2424
+ const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
2425
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2426
+ }
2427
+ }
2428
+ if (fuzzFiles.length && command == "test") {
2429
+ process.stdout.write(" fuzz artifacts:\n");
2430
+ for (const file of fuzzFiles) {
2431
+ const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
2432
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2433
+ }
2434
+ }
2435
+ else if (command == "fuzz") {
2436
+ process.stdout.write(" artifacts:\n");
2437
+ for (const file of fuzzFiles) {
2438
+ const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
2439
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
2440
+ }
1126
2441
  }
1127
2442
  process.stdout.write("\n");
1128
2443
  }