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.
- package/CHANGELOG.md +8 -3
- package/README.md +14 -7
- package/as-test.config.schema.json +142 -142
- package/assembly/__fuzz__/math.fuzz.ts +17 -14
- package/assembly/__fuzz__/seed-perf.fuzz.ts +4 -2
- package/assembly/index.ts +1 -4
- package/assembly/src/expectation.ts +7 -1
- package/assembly/src/fuzz.ts +44 -23
- package/assembly/util/format.ts +10 -1
- package/assembly/util/wipc.ts +1 -2
- package/bin/build-worker-pool.js +7 -1
- package/bin/commands/build-core.js +6 -1
- package/bin/commands/build.js +1 -1
- package/bin/commands/clean-core.js +3 -1
- package/bin/commands/clean.js +0 -37
- package/bin/commands/fuzz-core.js +2 -2
- package/bin/commands/run-core.js +35 -24
- package/bin/commands/web-runner-source.js +14 -14
- package/bin/commands/web-session.js +6 -1
- package/bin/crash-store.js +3 -1
- package/bin/index.js +301 -123
- package/bin/reporters/default.js +175 -33
- package/bin/util.js +36 -11
- package/lib/build/index.js +74 -24
- package/lib/src/index.ts +96 -35
- package/package.json +1 -1
- package/transform/lib/coverage.js +3 -1
package/bin/reporters/default.js
CHANGED
|
@@ -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 = [
|
|
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 ")} ${
|
|
119
|
+
return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
|
|
117
120
|
if (this.fileHasWarning)
|
|
118
|
-
return `${chalk.bgYellow.black(" WARN ")} ${
|
|
121
|
+
return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
|
|
119
122
|
if (verdict == "ok")
|
|
120
|
-
return `${chalk.bgGreenBright.black(" PASS ")} ${
|
|
121
|
-
return `${chalk.bgBlackBright.white(" SKIP ")} ${
|
|
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} ${
|
|
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(
|
|
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 ?? [])
|
|
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
|
-
? ` (${
|
|
426
|
-
: ` (${
|
|
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
|
|
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, [],
|
|
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,
|
|
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
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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,
|
|
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
|
|
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}
|
|
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 ||
|
|
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]) => [
|
|
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 ||
|
|
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 &&
|
|
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 ||
|
|
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 &&
|
|
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 &&
|
|
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)) {
|
package/lib/build/index.js
CHANGED
|
@@ -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 =
|
|
29
|
-
|
|
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
|
|
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 ||
|
|
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>",
|
|
474
|
+
res.end(html.replace("</body>", " <script>window.__AS_TEST_ENV__ = " +
|
|
424
475
|
JSON.stringify(webRuntimeEnv) +
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1159
|
+
WebAssembly.instantiate =
|
|
1160
|
+
originalInstantiate;
|
|
1111
1161
|
}
|
|
1112
1162
|
if (!instance) {
|
|
1113
1163
|
throw new Error("bindings helper did not produce a WebAssembly.Instance");
|