as-test 1.1.1 → 1.1.2

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.
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { diff } from "typer-diff";
3
3
  import { readFileSync } from "fs";
4
4
  import * as path from "path";
5
- import { formatTime } from "../util.js";
5
+ import { formatSpecDisplayPath, formatTime } from "../util.js";
6
6
  import { describeCoveragePoint, readCoverageSourceLine, resolveCoverageHighlightSpan, } from "../coverage-points.js";
7
7
  export const createReporter = (context) => {
8
8
  return new DefaultReporter(context);
@@ -55,7 +55,9 @@ class DefaultReporter {
55
55
  renderLiveState() {
56
56
  if (!this.canRewriteLine() || !this.currentFile)
57
57
  return;
58
- const lines = [`${this.badgeRunning()} ${this.currentFile}`];
58
+ const lines = [
59
+ `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
60
+ ];
59
61
  for (const suite of this.openSuites) {
60
62
  lines.push(`${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`);
61
63
  }
@@ -67,7 +69,7 @@ class DefaultReporter {
67
69
  const lines = [
68
70
  fileEnd
69
71
  ? this.renderFileResult(fileEnd)
70
- : `${this.badgeRunning()} ${this.currentFile}`,
72
+ : `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
71
73
  ];
72
74
  for (const suite of this.verboseSuites) {
73
75
  const badge = suite.verdict == "running"
@@ -112,13 +114,14 @@ class DefaultReporter {
112
114
  renderFileResult(event) {
113
115
  const verdict = event.verdict ?? "none";
114
116
  const time = event.time ? ` ${chalk.dim(event.time)}` : "";
117
+ const file = formatSpecDisplayPath(event.file);
115
118
  if (verdict == "fail")
116
- return `${chalk.bgRed.white(" FAIL ")} ${event.file}${time}`;
119
+ return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
117
120
  if (this.fileHasWarning)
118
- return `${chalk.bgYellow.black(" WARN ")} ${event.file}${time}`;
121
+ return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
119
122
  if (verdict == "ok")
120
- return `${chalk.bgGreenBright.black(" PASS ")} ${event.file}${time}`;
121
- return `${chalk.bgBlackBright.white(" SKIP ")} ${event.file}${time}`;
123
+ return `${chalk.bgGreenBright.black(" PASS ")} ${file}${time}`;
124
+ return `${chalk.bgBlackBright.white(" SKIP ")} ${file}${time}`;
122
125
  }
123
126
  onRunStart(event) {
124
127
  this.verboseMode = Boolean(event.verbose);
@@ -139,16 +142,16 @@ class DefaultReporter {
139
142
  }
140
143
  if (!this.verboseMode) {
141
144
  if (!this.canRewriteLine()) {
142
- this.context.stdout.write(`${this.badgeRunning()} ${event.file}\n`);
145
+ this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
143
146
  return;
144
147
  }
145
148
  this.clearRenderedBlock();
146
- this.context.stdout.write(`${this.badgeRunning()} ${event.file}`);
149
+ this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}`);
147
150
  this.renderedLines = 1;
148
151
  return;
149
152
  }
150
153
  if (!this.canRewriteLine()) {
151
- this.context.stdout.write(`${this.badgeRunning()} ${event.file}\n`);
154
+ this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
152
155
  return;
153
156
  }
154
157
  this.renderLiveState();
@@ -315,7 +318,7 @@ function renderFuzzFileSummary(context, results) {
315
318
  const detail = formatTime(averageFuzzModeTime(results));
316
319
  const crashFile = firstFuzzCrashFile(results);
317
320
  const crashSuffix = crashFile != null ? ` ${chalk.dim(`-> ${crashFile}`)}` : "";
318
- context.stdout.write(`${itemBadge} ${path.basename(file)} ${chalk.dim(detail)}${crashSuffix}\n`);
321
+ context.stdout.write(`${itemBadge} ${formatSpecDisplayPath(file)} ${chalk.dim(detail)}${crashSuffix}\n`);
319
322
  renderFailedFuzzers(groupFuzzResultsByFile(results));
320
323
  }
321
324
  function renderFuzzSummary(context, event, hasRenderedTestFiles) {
@@ -335,7 +338,7 @@ function renderFailedFuzzers(results) {
335
338
  console.log("");
336
339
  rendered = true;
337
340
  }
338
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(path.basename(modeResult.file))} ${chalk.dim("(crash)")}`);
341
+ console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(formatSpecDisplayPath(modeResult.file))} ${chalk.dim("(crash)")}`);
339
342
  console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
340
343
  console.log(chalk.dim(`Runs: ${modeResult.runs} configured`));
341
344
  console.log(chalk.dim(`Repro: ${repro}`));
@@ -413,7 +416,9 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
413
416
  return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
414
417
  }
415
418
  function formatFailingSeeds(fuzzer) {
416
- return (fuzzer.failures ?? []).map((failure) => String(failure.seed)).join(", ");
419
+ return (fuzzer.failures ?? [])
420
+ .map((failure) => String(failure.seed))
421
+ .join(", ");
417
422
  }
418
423
  function toRelativeResultPath(file) {
419
424
  const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
@@ -422,8 +427,8 @@ function toRelativeResultPath(file) {
422
427
  function formatFuzzFailureTitle(file, name) {
423
428
  const location = findFuzzLocation(file, name);
424
429
  const suffix = location
425
- ? ` (${path.basename(file)}:${location})`
426
- : ` (${path.basename(file)})`;
430
+ ? ` (${formatSpecDisplayPath(file)}:${location})`
431
+ : ` (${formatSpecDisplayPath(file)})`;
427
432
  return `${chalk.dim(name)}${chalk.dim(suffix)}`;
428
433
  }
429
434
  function findFuzzLocation(file, name) {
@@ -460,19 +465,24 @@ function renderFailedSuites(failedEntries) {
460
465
  if (!failedEntries.length)
461
466
  return;
462
467
  console.log("");
463
- const printed = new Set();
468
+ const grouped = new Map();
464
469
  for (const failed of failedEntries) {
465
470
  const failedAny = failed;
466
471
  if (!failedAny?.file)
467
472
  continue;
468
473
  const file = String(failedAny.file);
469
- collectSuiteFailures(failed, file, [], printed);
474
+ collectSuiteFailures(failed, file, [], grouped);
475
+ }
476
+ for (const failure of grouped.values()) {
477
+ renderCollectedFailure(failure);
470
478
  }
471
479
  }
472
- function collectSuiteFailures(suite, file, path, printed, inheritedModeName = "") {
480
+ function collectSuiteFailures(suite, file, path, grouped, inheritedModeName = "") {
473
481
  const suiteAny = suite;
474
482
  const nextPath = [...path, String(suiteAny.description ?? "unknown")];
475
483
  const modeName = String(suiteAny.modeName ?? inheritedModeName);
484
+ const isRuntimeErrorSuite = String(suiteAny.kind ?? "") == "runtime-error";
485
+ const isBuildErrorSuite = String(suiteAny.kind ?? "") == "build-error";
476
486
  const tests = Array.isArray(suiteAny.tests)
477
487
  ? suiteAny.tests
478
488
  : [];
@@ -486,30 +496,159 @@ function collectSuiteFailures(suite, file, path, printed, inheritedModeName = ""
486
496
  const where = loc.length ? `${file}:${loc}` : file;
487
497
  const suitePath = String(suiteAny.path ?? "");
488
498
  const message = String(test.message ?? "");
489
- const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
490
- if (printed.has(dedupeKey))
491
- continue;
492
- printed.add(dedupeKey);
493
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(title)} ${chalk.dim("(" + where + ")")}`);
499
+ const left = test.left;
500
+ const right = test.right;
501
+ const dedupeKey = `${file}::${title}::${String(left)}::${String(right)}::${message}`;
502
+ let failure = grouped.get(dedupeKey);
503
+ if (!failure) {
504
+ failure = {
505
+ title,
506
+ where,
507
+ file,
508
+ suitePath,
509
+ left,
510
+ right,
511
+ message,
512
+ isRuntimeError: isRuntimeErrorSuite || String(test.type ?? "") == "runtime-error",
513
+ isBuildError: isBuildErrorSuite || String(test.type ?? "") == "build-error",
514
+ modes: new Set(),
515
+ runCommands: new Map(),
516
+ buildCommands: new Map(),
517
+ };
518
+ grouped.set(dedupeKey, failure);
519
+ }
494
520
  if (modeName.length) {
495
- console.log(chalk.dim(`Mode: ${modeName}`));
521
+ failure.modes.add(modeName);
522
+ }
523
+ const runCommand = String(suiteAny.runCommand ?? "");
524
+ if (modeName.length && runCommand.length) {
525
+ failure.runCommands.set(modeName, runCommand);
496
526
  }
497
- if (suitePath.length) {
498
- console.log(chalk.dim(`Repro: ${buildSuiteReproCommand(toRelativeResultPath(file), suitePath, modeName)}`));
527
+ const buildCommand = String(suiteAny.buildCommand ?? "");
528
+ if (modeName.length && buildCommand.length) {
529
+ failure.buildCommands.set(modeName, buildCommand);
499
530
  }
500
- renderAssertionFailureDetails(test.left, test.right, message);
501
531
  }
502
532
  const suites = Array.isArray(suiteAny.suites)
503
533
  ? suiteAny.suites
504
534
  : [];
505
535
  for (const sub of suites) {
506
- collectSuiteFailures(sub, file, nextPath, printed, modeName);
536
+ collectSuiteFailures(sub, file, nextPath, grouped, modeName);
537
+ }
538
+ }
539
+ function renderCollectedFailure(failure) {
540
+ console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(failure.title)} ${chalk.dim("(" + failure.where + ")")}`);
541
+ const modes = [...failure.modes].filter(Boolean).sort();
542
+ if (failure.isBuildError) {
543
+ renderBuildFailureDetails(failure, modes);
544
+ }
545
+ else if (failure.isRuntimeError) {
546
+ renderRuntimeFailureDetails(failure, modes);
547
+ }
548
+ else {
549
+ if (modes.length == 1) {
550
+ console.log(chalk.dim(`Mode: ${modes[0]}`));
551
+ }
552
+ else if (modes.length > 1) {
553
+ console.log(chalk.dim(`Modes: ${modes.join(", ")}`));
554
+ }
555
+ const relativeFile = toRelativeResultPath(failure.file);
556
+ const repro = failure.suitePath.length && modes.length == 1
557
+ ? buildSuiteReproCommand(relativeFile, failure.suitePath, modes[0])
558
+ : buildFileReproCommand(relativeFile, modes);
559
+ console.log(chalk.dim(`Repro: ${repro}`));
560
+ renderModeCommands("Build", failure.buildCommands, modes);
561
+ renderModeCommands("Run", failure.runCommands, modes);
562
+ }
563
+ renderAssertionFailureDetails(failure.left, failure.right, failure.message);
564
+ }
565
+ function renderBuildFailureDetails(failure, modes) {
566
+ console.log("");
567
+ console.log(chalk.bold(" Oops! Looks like the test failed to build!"));
568
+ console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
569
+ console.log("");
570
+ console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
571
+ console.log("");
572
+ console.log(chalk.dim(" To reproduce, run the following commands:"));
573
+ for (const mode of modes.length ? modes : ["default"]) {
574
+ console.log(chalk.dim(` Mode: ${mode}`));
575
+ const buildCommand = failure.buildCommands.get(mode);
576
+ if (buildCommand?.length) {
577
+ console.log(chalk.dim(` Build: ${buildCommand}`));
578
+ }
579
+ }
580
+ console.log("");
581
+ console.log(chalk.dim(" Here's a log dump too:"));
582
+ }
583
+ function renderRuntimeFailureDetails(failure, modes) {
584
+ console.log("");
585
+ console.log(chalk.bold(" Oops! Looks like the runtime crashed!"));
586
+ console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
587
+ console.log("");
588
+ console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
589
+ console.log("");
590
+ console.log(chalk.dim(" To reproduce, run the following commands:"));
591
+ for (const mode of modes.length ? modes : ["default"]) {
592
+ console.log(chalk.dim(` Mode: ${mode}`));
593
+ const buildCommand = failure.buildCommands.get(mode);
594
+ if (buildCommand?.length) {
595
+ console.log(chalk.dim(` Build: ${buildCommand}`));
596
+ }
597
+ const runCommand = buildRuntimeReproRunCommand(failure.runCommands.get(mode) ?? "", buildCommand ?? "");
598
+ if (runCommand.length) {
599
+ console.log(chalk.dim(` Run: ${runCommand}`));
600
+ }
507
601
  }
602
+ console.log("");
603
+ console.log(chalk.dim(" Here's a log dump too:"));
508
604
  }
509
605
  function buildSuiteReproCommand(file, suitePath, modeName) {
510
606
  const modeArg = modeName && modeName != "default" ? ` --mode ${modeName}` : "";
511
607
  return `ast run ${file}${modeArg} --suite ${suitePath}`;
512
608
  }
609
+ function buildFileReproCommand(file, modes) {
610
+ const normalizedModes = modes.filter(Boolean).sort();
611
+ if (normalizedModes.length == 1 && normalizedModes[0] != "default") {
612
+ return `ast run ${file} --mode ${normalizedModes[0]}`;
613
+ }
614
+ if (normalizedModes.length > 1 &&
615
+ normalizedModes.every((mode) => mode != "default")) {
616
+ return `ast run ${file} --mode ${normalizedModes.join(",")}`;
617
+ }
618
+ return `ast run ${file}`;
619
+ }
620
+ function renderModeCommands(label, commands, modes) {
621
+ if (!commands.size)
622
+ return;
623
+ const uniqueCommands = new Set([...commands.values()].filter(Boolean));
624
+ if (uniqueCommands.size == 1) {
625
+ console.log(chalk.dim(`${label}: ${[...uniqueCommands][0]}`));
626
+ return;
627
+ }
628
+ console.log(chalk.dim(`${label} commands:`));
629
+ for (const mode of modes) {
630
+ const command = commands.get(mode);
631
+ if (!command)
632
+ continue;
633
+ console.log(chalk.dim(` [${mode}] ${command}`));
634
+ }
635
+ }
636
+ function buildRuntimeReproRunCommand(runCommand, buildCommand) {
637
+ if (!runCommand.length)
638
+ return "";
639
+ const artifactPath = extractBuildArtifactPath(buildCommand);
640
+ if (!artifactPath) {
641
+ return runCommand;
642
+ }
643
+ if (runCommand.includes(".as-test/runners/default.")) {
644
+ return `${runCommand} ${artifactPath}`;
645
+ }
646
+ return runCommand;
647
+ }
648
+ function extractBuildArtifactPath(buildCommand) {
649
+ const outMatch = buildCommand.match(/(?:^|\s)(?:-o|--outFile)\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
650
+ return outMatch?.[1] ?? outMatch?.[2] ?? outMatch?.[3] ?? null;
651
+ }
513
652
  function normalizeFailureMessage(message) {
514
653
  return message.replace(/\r\n/g, "\n").trim();
515
654
  }
@@ -520,11 +659,13 @@ function renderAssertionFailureDetails(leftRaw, rightRaw, messageRaw) {
520
659
  if (left == "null" && right == "null") {
521
660
  const normalizedMessage = normalizeFailureMessage(message);
522
661
  if (normalizedMessage.length) {
662
+ console.log("");
523
663
  for (const line of normalizedMessage.split("\n")) {
524
664
  console.log(chalk.dim(line));
525
665
  }
526
666
  }
527
667
  else {
668
+ console.log("");
528
669
  console.log(chalk.dim("runtime error"));
529
670
  }
530
671
  return;
@@ -656,7 +797,9 @@ function renderCoverageSummary(summary, showCoverage) {
656
797
  const pct = summary.total
657
798
  ? ((summary.covered * 100) / summary.total).toFixed(2)
658
799
  : "100.00";
659
- const missingLabel = summary.uncovered == 1 ? "1 point missing" : `${summary.uncovered} points missing`;
800
+ const missingLabel = summary.uncovered == 1
801
+ ? "1 point missing"
802
+ : `${summary.uncovered} points missing`;
660
803
  const fileLabel = summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
661
804
  const color = Number(pct) >= 90
662
805
  ? chalk.greenBright
@@ -681,9 +824,7 @@ function renderCoverageSummary(summary, showCoverage) {
681
824
  : Number(filePct) >= 75
682
825
  ? chalk.yellowBright
683
826
  : chalk.redBright;
684
- const suffix = file.uncovered > 0
685
- ? `${file.uncovered} missing`
686
- : "fully covered";
827
+ const suffix = file.uncovered > 0 ? `${file.uncovered} missing` : "fully covered";
687
828
  console.log(` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`);
688
829
  }
689
830
  if (ranked.length > 8) {
@@ -732,7 +873,8 @@ function renderCoverageBar(percent) {
732
873
  function createCoverageGapLayout(points) {
733
874
  return {
734
875
  typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
735
- locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`.length), 1),
876
+ locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`
877
+ .length), 1),
736
878
  };
737
879
  }
738
880
  function formatCoverageSnippet(file, line, column) {
package/bin/util.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs";
2
2
  import { BuildOptions, Config, CoverageOptions, CoverageIgnoreOptions, FuzzConfig, ModeConfig, ReporterConfig, RunOptions, Runtime, } from "./types.js";
3
3
  import chalk from "chalk";
4
4
  import { createRequire } from "module";
5
- import { delimiter, dirname, join, resolve } from "path";
5
+ import { basename, delimiter, dirname, join, relative, resolve, sep, } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  const CONFIG_META = new WeakMap();
8
8
  export function formatTime(ms) {
@@ -28,6 +28,19 @@ export function formatTime(ms) {
28
28
  }
29
29
  return `${us}us`;
30
30
  }
31
+ export function formatSpecDisplayPath(file) {
32
+ const resolved = resolve(file);
33
+ const parts = resolved.split(/[/\\]+/);
34
+ const testsIndex = parts.lastIndexOf("__tests__");
35
+ if (testsIndex >= 0 && testsIndex < parts.length - 1) {
36
+ return parts.slice(testsIndex + 1).join("/");
37
+ }
38
+ const rel = relative(process.cwd(), resolved).split(sep).join("/");
39
+ if (rel.length && rel != "." && rel != ".." && !rel.startsWith("../")) {
40
+ return rel;
41
+ }
42
+ return basename(resolved);
43
+ }
31
44
  export function loadConfig(CONFIG_PATH, warn = false) {
32
45
  const resolvedPath = resolve(CONFIG_PATH);
33
46
  const raw = readConfigRaw(resolvedPath, warn);
@@ -102,9 +115,7 @@ function parseConfigRaw(raw, configPath) {
102
115
  : "";
103
116
  const cmd = runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
104
117
  ? runtimeRaw.cmd
105
- : runtimeRaw &&
106
- typeof runtimeRaw.run == "string" &&
107
- runtimeRaw.run.length
118
+ : runtimeRaw && typeof runtimeRaw.run == "string" && runtimeRaw.run.length
108
119
  ? runtimeRaw.run
109
120
  : legacyRun
110
121
  ? legacyRun
@@ -343,7 +354,9 @@ function validateCoverageValue(value, path, issues) {
343
354
  validateStringArrayField(obj, "include", path, issues);
344
355
  validateStringArrayField(obj, "exclude", path, issues);
345
356
  if ("ignore" in obj && obj.ignore != undefined) {
346
- if (!obj.ignore || typeof obj.ignore != "object" || Array.isArray(obj.ignore)) {
357
+ if (!obj.ignore ||
358
+ typeof obj.ignore != "object" ||
359
+ Array.isArray(obj.ignore)) {
347
360
  issues.push({
348
361
  path: `${path}.ignore`,
349
362
  message: "must be an object",
@@ -967,14 +980,19 @@ function cloneConfig(config) {
967
980
  cloned.runOptions = cloneRunOptions(config.runOptions);
968
981
  cloned.fuzz = cloneFuzzConfig(config.fuzz);
969
982
  cloned.coverage = cloneCoverageOptions(config.coverage);
970
- cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [name, cloneModeConfig(mode)]));
983
+ cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [
984
+ name,
985
+ cloneModeConfig(mode),
986
+ ]));
971
987
  CONFIG_META.set(cloned, getConfigMeta(config));
972
988
  return cloned;
973
989
  }
974
990
  function outputOverridesField(raw, field) {
975
991
  if (field in raw)
976
992
  return true;
977
- if (!raw.output || typeof raw.output != "object" || Array.isArray(raw.output)) {
993
+ if (!raw.output ||
994
+ typeof raw.output != "object" ||
995
+ Array.isArray(raw.output)) {
978
996
  return false;
979
997
  }
980
998
  const output = raw.output;
@@ -1023,7 +1041,9 @@ function mergeCoverageConfig(base, override, raw) {
1023
1041
  mergedBase.include = [...overrideOptions.include];
1024
1042
  if ("exclude" in rawObject)
1025
1043
  mergedBase.exclude = [...overrideOptions.exclude];
1026
- if (rawObject.ignore && typeof rawObject.ignore == "object" && !Array.isArray(rawObject.ignore)) {
1044
+ if (rawObject.ignore &&
1045
+ typeof rawObject.ignore == "object" &&
1046
+ !Array.isArray(rawObject.ignore)) {
1027
1047
  mergedBase.ignore = mergeCoverageIgnoreOptions(mergedBase.ignore, overrideOptions.ignore, rawObject.ignore);
1028
1048
  }
1029
1049
  return mergedBase;
@@ -1068,7 +1088,8 @@ function mergeRunOptions(base, override, raw) {
1068
1088
  const merged = cloneRunOptions(base);
1069
1089
  if ("runtime" in raw || "run" in raw) {
1070
1090
  const runtimeRaw = raw.runtime;
1071
- if ("run" in raw || (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
1091
+ if ("run" in raw ||
1092
+ (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
1072
1093
  merged.runtime.cmd = override.runtime.cmd;
1073
1094
  }
1074
1095
  if (runtimeRaw && "browser" in runtimeRaw) {
@@ -1126,10 +1147,14 @@ function mergeRootConfig(base, override) {
1126
1147
  if ("env" in raw) {
1127
1148
  merged.env = { ...override.env };
1128
1149
  }
1129
- if (raw.buildOptions && typeof raw.buildOptions == "object" && !Array.isArray(raw.buildOptions)) {
1150
+ if (raw.buildOptions &&
1151
+ typeof raw.buildOptions == "object" &&
1152
+ !Array.isArray(raw.buildOptions)) {
1130
1153
  merged.buildOptions = mergeBuildOptions(merged.buildOptions, override.buildOptions, raw.buildOptions);
1131
1154
  }
1132
- if (raw.runOptions && typeof raw.runOptions == "object" && !Array.isArray(raw.runOptions)) {
1155
+ if (raw.runOptions &&
1156
+ typeof raw.runOptions == "object" &&
1157
+ !Array.isArray(raw.runOptions)) {
1133
1158
  merged.runOptions = mergeRunOptions(merged.runOptions, override.runOptions, raw.runOptions);
1134
1159
  }
1135
1160
  if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
@@ -5,7 +5,6 @@ import http from "http";
5
5
  import os from "os";
6
6
  import path from "path";
7
7
  import { pathToFileURL } from "url";
8
- import { WASI } from "wasi";
9
8
  import { buildWebRunnerClientSource } from "./web-runner/client.js";
10
9
  import { buildWebRunnerHtml } from "./web-runner/html.js";
11
10
  import { buildWebRunnerWorkerSource } from "./web-runner/worker.js";
@@ -25,29 +24,81 @@ function withNodeIo(imports) {
25
24
  }
26
25
  export async function instantiate(imports) {
27
26
  validateImports(imports, "instantiate");
28
- const wasmPath = process.env.AS_TEST_WASM_PATH;
29
- if (!wasmPath || !wasmPath.length) {
30
- throw new Error("AS_TEST_WASM_PATH is not set; as-test must resolve the wasm artifact before launching the runner");
31
- }
32
- const target = (process.env.AS_TEST_RUNTIME_TARGET || "bindings");
27
+ const wasmPath = resolveWasmPath();
28
+ const target = resolveRuntimeTarget();
33
29
  if (target == "wasi") {
34
30
  return instantiateWasiInstance(wasmPath, imports);
35
31
  }
36
32
  if (target == "web") {
37
33
  return instantiateWebInstance(wasmPath, imports);
38
34
  }
39
- const kind = (process.env.AS_TEST_BINDINGS_KIND || "none");
35
+ const helperPath = resolveBindingsHelperPath(wasmPath);
36
+ const kind = resolveBindingsKind(helperPath);
40
37
  if (kind == "raw") {
41
- return instantiateRawInstance(wasmPath, imports);
38
+ return instantiateRawInstance(wasmPath, helperPath, imports);
42
39
  }
43
40
  if (kind == "esm") {
44
- return instantiateEsmInstance(wasmPath, imports);
41
+ return instantiateEsmInstance(wasmPath, helperPath, imports);
45
42
  }
46
43
  if (kind == "none") {
47
44
  return instantiateNoBindingsInstance(wasmPath, imports);
48
45
  }
49
46
  throw new Error(`unsupported bindings kind "${kind}"`);
50
47
  }
48
+ function resolveRuntimeTarget() {
49
+ const envTarget = process.env.AS_TEST_RUNTIME_TARGET?.trim();
50
+ if (envTarget == "bindings" || envTarget == "wasi" || envTarget == "web") {
51
+ return envTarget;
52
+ }
53
+ const runnerPath = String(process.argv[1] ?? "");
54
+ if (runnerPath.includes(".wasi."))
55
+ return "wasi";
56
+ if (runnerPath.includes(".web."))
57
+ return "web";
58
+ return "bindings";
59
+ }
60
+ function resolveWasmPath() {
61
+ const envWasmPath = process.env.AS_TEST_WASM_PATH?.trim();
62
+ if (envWasmPath?.length) {
63
+ return path.resolve(envWasmPath);
64
+ }
65
+ const argWasmPath = process.argv[2]?.trim();
66
+ if (argWasmPath?.length) {
67
+ return path.resolve(argWasmPath);
68
+ }
69
+ const runnerPath = String(process.argv[1] ?? "");
70
+ const runnerName = path.basename(runnerPath || "runner.js");
71
+ throw new Error([
72
+ "No wasm artifact was provided for this runner.",
73
+ "",
74
+ `Direct usage: node .as-test/runners/${runnerName} .as-test/build/<artifact>.wasm`,
75
+ "Managed usage: bunx ast test --mode <mode>",
76
+ "",
77
+ "as-test normally sets AS_TEST_WASM_PATH automatically when it launches the runner.",
78
+ ].join("\n"));
79
+ }
80
+ function resolveBindingsHelperPath(wasmPath) {
81
+ const envHelperPath = process.env.AS_TEST_HELPER_PATH?.trim();
82
+ if (envHelperPath?.length) {
83
+ return path.resolve(envHelperPath);
84
+ }
85
+ const candidate = wasmPath.replace(/\.wasm$/i, ".js");
86
+ return fs.existsSync(candidate) ? candidate : "";
87
+ }
88
+ function resolveBindingsKind(helperPath) {
89
+ const envKind = process.env.AS_TEST_BINDINGS_KIND?.trim();
90
+ if (envKind == "raw" || envKind == "esm" || envKind == "none") {
91
+ return envKind;
92
+ }
93
+ if (!helperPath.length) {
94
+ return "none";
95
+ }
96
+ const source = fs.readFileSync(helperPath, "utf8");
97
+ if (/\bexport\s+(?:async\s+)?function\s+instantiate\b/.test(source)) {
98
+ return "raw";
99
+ }
100
+ return "esm";
101
+ }
51
102
  function validateImports(imports, fnName) {
52
103
  if (arguments.length < 1) {
53
104
  throw new Error(`${fnName}(imports) requires an imports object; pass {} when unused`);
@@ -135,9 +186,8 @@ function mergeImports(...groups) {
135
186
  }
136
187
  return out;
137
188
  }
138
- async function instantiateRawInstance(wasmPath, imports) {
189
+ async function instantiateRawInstance(wasmPath, helperPath, imports) {
139
190
  validateImports(imports, "instantiateRawInstance");
140
- const helperPath = process.env.AS_TEST_HELPER_PATH || "";
141
191
  if (!helperPath.length) {
142
192
  throw new Error("bindings kind is raw but AS_TEST_HELPER_PATH is not set");
143
193
  }
@@ -153,9 +203,8 @@ async function instantiateRawInstance(wasmPath, imports) {
153
203
  });
154
204
  return decorateInstance(instance, "bindings");
155
205
  }
156
- async function instantiateEsmInstance(wasmPath, imports) {
206
+ async function instantiateEsmInstance(wasmPath, helperPath, imports) {
157
207
  validateImports(imports, "instantiateEsmInstance");
158
- const helperPath = process.env.AS_TEST_HELPER_PATH || "";
159
208
  if (!helperPath.length) {
160
209
  throw new Error("bindings kind is esm but AS_TEST_HELPER_PATH is not set");
161
210
  }
@@ -175,6 +224,7 @@ async function instantiateNoBindingsInstance(wasmPath, imports) {
175
224
  async function instantiateWasiInstance(wasmPath, imports) {
176
225
  validateImports(imports, "instantiateWasiInstance");
177
226
  suppressExperimentalWasiWarning();
227
+ const { WASI } = await import("wasi");
178
228
  const binary = fs.readFileSync(wasmPath);
179
229
  const module = new WebAssembly.Module(binary);
180
230
  const wasi = new WASI({
@@ -194,7 +244,8 @@ async function instantiateWebInstance(wasmPath, imports) {
194
244
  if (hasUserImports(imports)) {
195
245
  throw new Error("web runtime does not support custom imports in the default runner; pass {} or write a custom web runner");
196
246
  }
197
- const bindingsKind = (process.env.AS_TEST_BINDINGS_KIND || "raw");
247
+ const bindingsKind = (process.env.AS_TEST_BINDINGS_KIND ||
248
+ "raw");
198
249
  const helperPath = process.env.AS_TEST_HELPER_PATH
199
250
  ? path.resolve(process.cwd(), process.env.AS_TEST_HELPER_PATH)
200
251
  : wasmPath.replace(/\.wasm$/, ".js");
@@ -420,9 +471,9 @@ async function instantiateWebInstance(wasmPath, imports) {
420
471
  ...headers,
421
472
  "Content-Type": "text/html; charset=utf-8",
422
473
  });
423
- res.end(html.replace("</body>", ' <script>window.__AS_TEST_ENV__ = ' +
474
+ res.end(html.replace("</body>", " <script>window.__AS_TEST_ENV__ = " +
424
475
  JSON.stringify(webRuntimeEnv) +
425
- ';</script>\n </body>'));
476
+ ";</script>\n </body>"));
426
477
  return;
427
478
  }
428
479
  if (url == "/client.js") {
@@ -500,9 +551,7 @@ async function instantiateWebInstance(wasmPath, imports) {
500
551
  });
501
552
  socket.on("error", (error) => {
502
553
  if (!finished) {
503
- rejectOnce(error instanceof Error
504
- ? error
505
- : new Error(String(error)));
554
+ rejectOnce(error instanceof Error ? error : new Error(String(error)));
506
555
  }
507
556
  });
508
557
  flushPendingFrames();
@@ -600,7 +649,7 @@ function suppressExperimentalWasiWarning() {
600
649
  const message = typeof warning == "string"
601
650
  ? warning
602
651
  : String(warning && typeof warning == "object" && "message" in warning
603
- ? warning.message ?? ""
652
+ ? (warning.message ?? "")
604
653
  : "");
605
654
  if (name == "ExperimentalWarning" &&
606
655
  message.includes("WASI is an experimental feature")) {
@@ -731,7 +780,7 @@ function openWithReusableBrowserWindow(url) {
731
780
  }
732
781
  function openWithSystemBrowser(url) {
733
782
  if (process.env.BROWSER) {
734
- return spawnBrowserCommand(process.env.BROWSER, url, false)?.process ?? null;
783
+ return (spawnBrowserCommand(process.env.BROWSER, url, false)?.process ?? null);
735
784
  }
736
785
  if (process.platform == "darwin") {
737
786
  if (!hasExecutable("open"))
@@ -788,7 +837,7 @@ function buildMacBrowserOpenScript(appName, url) {
788
837
  return [
789
838
  `tell application "${escapedApp}"`,
790
839
  "activate",
791
- 'if (count of windows) = 0 then make new window',
840
+ "if (count of windows) = 0 then make new window",
792
841
  `set URL of active tab of front window to "${escapedUrl}"`,
793
842
  "end tell",
794
843
  ];
@@ -797,7 +846,7 @@ function buildMacBrowserOpenScript(appName, url) {
797
846
  return [
798
847
  `tell application "${escapedApp}"`,
799
848
  "activate",
800
- 'if (count of windows) = 0 then make new document',
849
+ "if (count of windows) = 0 then make new document",
801
850
  `set URL of front document to "${escapedUrl}"`,
802
851
  "end tell",
803
852
  ];
@@ -1107,7 +1156,8 @@ async function captureHelperInstance(runHelper) {
1107
1156
  await runHelper();
1108
1157
  }
1109
1158
  finally {
1110
- WebAssembly.instantiate = originalInstantiate;
1159
+ WebAssembly.instantiate =
1160
+ originalInstantiate;
1111
1161
  }
1112
1162
  if (!instance) {
1113
1163
  throw new Error("bindings helper did not produce a WebAssembly.Instance");