as-test 1.1.10 → 1.3.0
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 +48 -0
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +2 -0
- package/assembly/src/expectation.ts +152 -44
- package/assembly/src/mode.ts +55 -0
- package/bin/commands/build-core.js +190 -65
- package/bin/commands/build.js +3 -1
- package/bin/commands/fuzz-core.js +30 -56
- package/bin/commands/init-core.js +253 -5
- package/bin/commands/run-core.js +38 -119
- package/bin/commands/test.js +1 -1
- package/bin/commands/web-session.js +4 -0
- package/bin/crash-store.js +1 -1
- package/bin/index.js +94 -152
- package/bin/types.js +7 -0
- package/bin/util.js +117 -0
- package/package.json +14 -9
- package/transform/lib/index.js +26 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { INTERNAL_FEATURE_NAMES, normalizeFeatureName } from "../types.js";
|
|
2
3
|
import { glob } from "glob";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import { spawn } from "child_process";
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
applyMode,
|
|
12
13
|
getPkgRunner,
|
|
13
14
|
loadConfig,
|
|
15
|
+
resolveArtifactPath,
|
|
16
|
+
resolveSpecRelativePath,
|
|
14
17
|
tokenizeCommand,
|
|
15
18
|
resolveProjectModule,
|
|
16
19
|
} from "../util.js";
|
|
@@ -64,11 +67,8 @@ export async function build(
|
|
|
64
67
|
const inputFiles = (await glob(inputPatterns)).sort((a, b) =>
|
|
65
68
|
a.localeCompare(b),
|
|
66
69
|
);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// an artifact name the runner won't look up (it always globs full input).
|
|
70
|
-
const duplicateSpecBasenames =
|
|
71
|
-
await resolveAllConfiguredDuplicateBasenames(sourceInputPatterns);
|
|
70
|
+
await assertNoArtifactCollisions(sourceInputPatterns);
|
|
71
|
+
warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
|
|
72
72
|
const coverageEnabled = resolveCoverageEnabled(
|
|
73
73
|
config.coverage,
|
|
74
74
|
featureToggles.coverage,
|
|
@@ -77,6 +77,7 @@ export async function build(
|
|
|
77
77
|
...mode.env,
|
|
78
78
|
...config.buildOptions.env,
|
|
79
79
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
80
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
80
81
|
};
|
|
81
82
|
if (
|
|
82
83
|
!resolvedConfig &&
|
|
@@ -85,7 +86,10 @@ export async function build(
|
|
|
85
86
|
) {
|
|
86
87
|
const pool = getSerialBuildWorkerPool();
|
|
87
88
|
for (const file of inputFiles) {
|
|
88
|
-
const outFile =
|
|
89
|
+
const outFile = path.join(
|
|
90
|
+
config.outDir,
|
|
91
|
+
resolveArtifactPath(file, sourceInputPatterns),
|
|
92
|
+
);
|
|
89
93
|
const invocation = getBuildCommand(
|
|
90
94
|
config,
|
|
91
95
|
pkgRunner,
|
|
@@ -106,7 +110,11 @@ export async function build(
|
|
|
106
110
|
return;
|
|
107
111
|
}
|
|
108
112
|
for (const file of inputFiles) {
|
|
109
|
-
const outFile =
|
|
113
|
+
const outFile = path.join(
|
|
114
|
+
config.outDir,
|
|
115
|
+
resolveArtifactPath(file, sourceInputPatterns),
|
|
116
|
+
);
|
|
117
|
+
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
110
118
|
const invocation = getBuildCommand(
|
|
111
119
|
config,
|
|
112
120
|
pkgRunner,
|
|
@@ -127,6 +135,10 @@ export async function build(
|
|
|
127
135
|
kind,
|
|
128
136
|
stage: "build",
|
|
129
137
|
file,
|
|
138
|
+
entryKey: resolveSpecRelativePath(file, sourceInputPatterns).replace(
|
|
139
|
+
/\.ts$/i,
|
|
140
|
+
"",
|
|
141
|
+
),
|
|
130
142
|
mode: modeLabel,
|
|
131
143
|
cwd: process.cwd(),
|
|
132
144
|
buildCommand,
|
|
@@ -189,9 +201,10 @@ export async function getBuildInvocationPreview(
|
|
|
189
201
|
}
|
|
190
202
|
const sourceInputPatterns =
|
|
191
203
|
overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
const outFile = path.join(
|
|
205
|
+
config.outDir,
|
|
206
|
+
resolveArtifactPath(file, sourceInputPatterns),
|
|
207
|
+
);
|
|
195
208
|
return getBuildCommand(
|
|
196
209
|
config,
|
|
197
210
|
getPkgRunner(),
|
|
@@ -229,9 +242,10 @@ export async function getBuildReuseInfo(
|
|
|
229
242
|
}
|
|
230
243
|
const sourceInputPatterns =
|
|
231
244
|
overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
const outFile = path.join(
|
|
246
|
+
config.outDir,
|
|
247
|
+
resolveArtifactPath(file, sourceInputPatterns),
|
|
248
|
+
);
|
|
235
249
|
const invocation = getBuildCommand(
|
|
236
250
|
config,
|
|
237
251
|
getPkgRunner(),
|
|
@@ -248,6 +262,7 @@ export async function getBuildReuseInfo(
|
|
|
248
262
|
...mode.env,
|
|
249
263
|
...config.buildOptions.env,
|
|
250
264
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
265
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
251
266
|
};
|
|
252
267
|
return {
|
|
253
268
|
signature: JSON.stringify({
|
|
@@ -262,6 +277,129 @@ export async function getBuildReuseInfo(
|
|
|
262
277
|
function hasCustomBuildCommand(config) {
|
|
263
278
|
return !!config.buildOptions.cmd.trim().length;
|
|
264
279
|
}
|
|
280
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
281
|
+
// mode names not present in the configured set. Collects all (file, name)
|
|
282
|
+
// hits and prints a single formatted block to stdout. The implicit "default"
|
|
283
|
+
// name is only valid when no configured mode has `default: true` — otherwise
|
|
284
|
+
// a named mode always runs and `AS_TEST_MODE_NAME` is never literal "default".
|
|
285
|
+
const MODE_CALL_RE = /\bmode\s*\(\s*\[([^\]]*)\]/g;
|
|
286
|
+
const MODE_STRING_RE = /["']([^"']*)["']/g;
|
|
287
|
+
const STRIP_COMMENTS_RE = /\/\*[\s\S]*?\*\/|\/\/.*$/gm;
|
|
288
|
+
const reportedModeWarnings = new Set();
|
|
289
|
+
const pendingModeWarningsByFile = new Map();
|
|
290
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
291
|
+
// mode names not present in the configured set, and buffers them for later
|
|
292
|
+
// printing via flushModeWarnings(). Called as early as possible (before the
|
|
293
|
+
// reporter starts streaming progress). De-duplicates across invocations.
|
|
294
|
+
export function warnOnUnknownModeReferences(files, configuredModes) {
|
|
295
|
+
const modeEntries = Object.entries(configuredModes ?? {});
|
|
296
|
+
const fallsBackToImplicitDefault =
|
|
297
|
+
modeEntries.length === 0 ||
|
|
298
|
+
modeEntries.every(([, mode]) => mode?.default === false);
|
|
299
|
+
const knownModes = new Set(modeEntries.map(([name]) => name));
|
|
300
|
+
if (fallsBackToImplicitDefault) knownModes.add("default");
|
|
301
|
+
const knownList = [...knownModes].sort();
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
let text;
|
|
304
|
+
try {
|
|
305
|
+
text = readFileSync(file, "utf8");
|
|
306
|
+
} catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
text = text.replace(STRIP_COMMENTS_RE, "");
|
|
310
|
+
for (const callMatch of text.matchAll(MODE_CALL_RE)) {
|
|
311
|
+
const arrayContents = callMatch[1] ?? "";
|
|
312
|
+
for (const strMatch of arrayContents.matchAll(MODE_STRING_RE)) {
|
|
313
|
+
let value = strMatch[1] ?? "";
|
|
314
|
+
if (value.length === 0) continue;
|
|
315
|
+
if (value.charCodeAt(0) === 33 /* '!' */) value = value.slice(1);
|
|
316
|
+
if (value.length === 0) continue;
|
|
317
|
+
if (knownModes.has(value)) continue;
|
|
318
|
+
const key = `${file}\x1f${value}`;
|
|
319
|
+
if (reportedModeWarnings.has(key)) continue;
|
|
320
|
+
reportedModeWarnings.add(key);
|
|
321
|
+
const warning = {
|
|
322
|
+
name: value,
|
|
323
|
+
suggestion: closestKnownMode(value, knownList),
|
|
324
|
+
};
|
|
325
|
+
const list = pendingModeWarningsByFile.get(file);
|
|
326
|
+
if (list) list.push(warning);
|
|
327
|
+
else pendingModeWarningsByFile.set(file, [warning]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Drains buffered mode warnings. When `showAll` is true, prints the full
|
|
333
|
+
// per-warning block; otherwise prints a one-line summary that tells the user
|
|
334
|
+
// to re-run with `--show-warnings`. No-op when there are no warnings.
|
|
335
|
+
export function flushModeWarnings(showAll) {
|
|
336
|
+
if (pendingModeWarningsByFile.size === 0) return;
|
|
337
|
+
const hits = [];
|
|
338
|
+
for (const [file, list] of pendingModeWarningsByFile) {
|
|
339
|
+
for (const w of list) {
|
|
340
|
+
hits.push({ file, name: w.name, suggestion: w.suggestion });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
pendingModeWarningsByFile.clear();
|
|
344
|
+
if (hits.length === 0) return;
|
|
345
|
+
if (!showAll) {
|
|
346
|
+
const count = hits.length;
|
|
347
|
+
const noun = count === 1 ? "warning" : "warnings";
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
`\nFound ${chalk.yellow.bold(count)} ${noun}. Run with ${chalk.dim("--show-warnings")} to view.\n`,
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const lines = [chalk.yellow.bold("WARNINGS:")];
|
|
354
|
+
for (const hit of hits) {
|
|
355
|
+
let line = ` - unknown mode reference ${chalk.bold(`"${hit.name}"`)} in ${chalk.dim(hit.file)}`;
|
|
356
|
+
if (hit.suggestion) {
|
|
357
|
+
line += ` - did you mean ${chalk.cyan(`"${hit.suggestion}"`)}?`;
|
|
358
|
+
}
|
|
359
|
+
lines.push(line);
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write("\n" + lines.join("\n") + "\n");
|
|
362
|
+
}
|
|
363
|
+
// Returns the configured mode whose Levenshtein distance to `name` is below
|
|
364
|
+
// a small threshold (proportional to the longer string's length). Returns
|
|
365
|
+
// null when nothing's close enough — avoids suggesting wildly different names.
|
|
366
|
+
function closestKnownMode(name, candidates) {
|
|
367
|
+
let best = null;
|
|
368
|
+
let bestDist = Infinity;
|
|
369
|
+
for (const candidate of candidates) {
|
|
370
|
+
const d = levenshtein(name, candidate);
|
|
371
|
+
const threshold = Math.max(
|
|
372
|
+
2,
|
|
373
|
+
Math.floor(Math.max(name.length, candidate.length) * 0.4),
|
|
374
|
+
);
|
|
375
|
+
if (d < bestDist && d <= threshold) {
|
|
376
|
+
bestDist = d;
|
|
377
|
+
best = candidate;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return best;
|
|
381
|
+
}
|
|
382
|
+
function levenshtein(a, b) {
|
|
383
|
+
const m = a.length;
|
|
384
|
+
const n = b.length;
|
|
385
|
+
if (m === 0) return n;
|
|
386
|
+
if (n === 0) return m;
|
|
387
|
+
let prev = new Array(n + 1);
|
|
388
|
+
let curr = new Array(n + 1);
|
|
389
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
390
|
+
for (let i = 1; i <= m; i++) {
|
|
391
|
+
curr[0] = i;
|
|
392
|
+
for (let j = 1; j <= n; j++) {
|
|
393
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
394
|
+
const del = prev[j] + 1;
|
|
395
|
+
const ins = curr[j - 1] + 1;
|
|
396
|
+
const sub = prev[j - 1] + cost;
|
|
397
|
+
curr[j] = Math.min(del, ins, sub);
|
|
398
|
+
}
|
|
399
|
+
[prev, curr] = [curr, prev];
|
|
400
|
+
}
|
|
401
|
+
return prev[n];
|
|
402
|
+
}
|
|
265
403
|
function getBuildCommand(
|
|
266
404
|
config,
|
|
267
405
|
pkgRunner,
|
|
@@ -356,54 +494,23 @@ function expandBuildCommand(template, file, outFile, target, modeName) {
|
|
|
356
494
|
.replace(/<target>/g, target)
|
|
357
495
|
.replace(/<mode>/g, modeName ?? "");
|
|
358
496
|
}
|
|
359
|
-
function
|
|
360
|
-
file,
|
|
361
|
-
target,
|
|
362
|
-
modeName,
|
|
363
|
-
duplicateSpecBasenames = new Set(),
|
|
364
|
-
) {
|
|
365
|
-
const base = path
|
|
366
|
-
.basename(file)
|
|
367
|
-
.replace(/\.spec\.ts$/, "")
|
|
368
|
-
.replace(/\.ts$/, "");
|
|
369
|
-
const legacy = !modeName
|
|
370
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
371
|
-
: `${base}.${modeName}.${target}.wasm`;
|
|
372
|
-
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
373
|
-
return legacy;
|
|
374
|
-
}
|
|
375
|
-
const disambiguator = resolveDisambiguator(file);
|
|
376
|
-
if (!disambiguator.length) {
|
|
377
|
-
return legacy;
|
|
378
|
-
}
|
|
379
|
-
const ext = path.extname(legacy);
|
|
380
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
381
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
382
|
-
}
|
|
383
|
-
async function resolveAllConfiguredDuplicateBasenames(configured) {
|
|
497
|
+
async function assertNoArtifactCollisions(configured) {
|
|
384
498
|
const patterns = Array.isArray(configured) ? configured : [configured];
|
|
385
499
|
const files = await glob(patterns);
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
function resolveDuplicateBasenames(files) {
|
|
389
|
-
const counts = new Map();
|
|
500
|
+
const seen = new Map();
|
|
390
501
|
for (const file of files) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
502
|
+
const artifact = resolveArtifactPath(file, patterns);
|
|
503
|
+
const prev = seen.get(artifact);
|
|
504
|
+
if (prev != null && prev !== file) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Two input files resolve to the same artifact path "${artifact}":\n` +
|
|
507
|
+
` - ${prev}\n` +
|
|
508
|
+
` - ${file}\n` +
|
|
509
|
+
`Rename one of them or narrow the input patterns to disambiguate.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
seen.set(artifact, file);
|
|
397
513
|
}
|
|
398
|
-
return duplicates;
|
|
399
|
-
}
|
|
400
|
-
function resolveDisambiguator(file) {
|
|
401
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
402
|
-
if (!relDir.length || relDir == ".") return "";
|
|
403
|
-
return relDir
|
|
404
|
-
.replace(/[\\/]+/g, "__")
|
|
405
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
406
|
-
.replace(/^_+|_+$/g, "");
|
|
407
514
|
}
|
|
408
515
|
function resolveInputPatterns(configured, selectors) {
|
|
409
516
|
const configuredInputs = Array.isArray(configured)
|
|
@@ -590,7 +697,8 @@ function getBuildStdout(error) {
|
|
|
590
697
|
}
|
|
591
698
|
function getDefaultBuildArgs(config, featureToggles) {
|
|
592
699
|
const buildArgs = [];
|
|
593
|
-
const
|
|
700
|
+
const effectiveFeatures = resolveEffectiveFeatures(config, featureToggles);
|
|
701
|
+
const tryAsEnabled = resolveTryAsEnabled(effectiveFeatures.has("try-as"));
|
|
594
702
|
buildArgs.push("--transform", "as-test/transform");
|
|
595
703
|
if (
|
|
596
704
|
resolveProjectModule("json-as/transform") &&
|
|
@@ -607,6 +715,10 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
607
715
|
if (tryAsEnabled) {
|
|
608
716
|
buildArgs.push("--use", "AS_TEST_TRY_AS=1");
|
|
609
717
|
}
|
|
718
|
+
for (const feature of effectiveFeatures) {
|
|
719
|
+
if (INTERNAL_FEATURE_NAMES.has(feature)) continue;
|
|
720
|
+
buildArgs.push("--enable", feature);
|
|
721
|
+
}
|
|
610
722
|
// Should also strip any bindings-enabling from asconfig
|
|
611
723
|
if (
|
|
612
724
|
config.buildOptions.target == "bindings" ||
|
|
@@ -705,16 +817,29 @@ function transformsContainJsonAs(value) {
|
|
|
705
817
|
}
|
|
706
818
|
return false;
|
|
707
819
|
}
|
|
708
|
-
function
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
820
|
+
function resolveEffectiveFeatures(config, featureToggles) {
|
|
821
|
+
const effective = new Set();
|
|
822
|
+
for (const name of config.features) {
|
|
823
|
+
effective.add(normalizeFeatureName(name));
|
|
824
|
+
}
|
|
825
|
+
const overrides = featureToggles.featureOverrides ?? {};
|
|
826
|
+
for (const [name, enabled] of Object.entries(overrides)) {
|
|
827
|
+
const key = normalizeFeatureName(name);
|
|
828
|
+
if (!key.length) continue;
|
|
829
|
+
if (enabled) effective.add(key);
|
|
830
|
+
else effective.delete(key);
|
|
831
|
+
}
|
|
832
|
+
effective.delete("");
|
|
833
|
+
return effective;
|
|
834
|
+
}
|
|
835
|
+
function resolveTryAsEnabled(enabled) {
|
|
836
|
+
if (!enabled) return false;
|
|
837
|
+
if (!hasTryAsRuntime()) {
|
|
712
838
|
throw new Error(
|
|
713
839
|
'try-as feature was enabled, but package "try-as" is not installed',
|
|
714
840
|
);
|
|
715
841
|
}
|
|
716
|
-
|
|
717
|
-
return false;
|
|
842
|
+
return true;
|
|
718
843
|
}
|
|
719
844
|
function resolveCoverageEnabled(rawCoverage, override) {
|
|
720
845
|
if (override != undefined) return override;
|
package/bin/commands/build.js
CHANGED
|
@@ -5,6 +5,8 @@ export {
|
|
|
5
5
|
formatInvocation,
|
|
6
6
|
getBuildInvocationPreview,
|
|
7
7
|
getBuildReuseInfo,
|
|
8
|
+
warnOnUnknownModeReferences,
|
|
9
|
+
flushModeWarnings,
|
|
8
10
|
} from "./build-core.js";
|
|
9
11
|
export async function executeBuildCommand(
|
|
10
12
|
rawArgs,
|
|
@@ -17,8 +19,8 @@ export async function executeBuildCommand(
|
|
|
17
19
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
|
|
18
20
|
const parallel = deps.resolveBuildParallelJobs(rawArgs);
|
|
19
21
|
const buildFeatureToggles = {
|
|
20
|
-
tryAs: featureToggles.tryAs,
|
|
21
22
|
coverage: featureToggles.coverage,
|
|
23
|
+
featureOverrides: featureToggles.featureOverrides,
|
|
22
24
|
};
|
|
23
25
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
24
26
|
if (listFlags.list || listFlags.listModes) {
|
|
@@ -3,7 +3,12 @@ import * as path from "path";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import { glob } from "glob";
|
|
5
5
|
import { build } from "./build-core.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
applyMode,
|
|
8
|
+
loadConfig,
|
|
9
|
+
resolveArtifactPath,
|
|
10
|
+
resolveSpecRelativePath,
|
|
11
|
+
} from "../util.js";
|
|
7
12
|
import { persistCrashRecord } from "../crash-store.js";
|
|
8
13
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
9
14
|
const MAGIC = Buffer.from("WIPC");
|
|
@@ -29,13 +34,6 @@ export async function fuzz(
|
|
|
29
34
|
`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`,
|
|
30
35
|
);
|
|
31
36
|
}
|
|
32
|
-
// Disambiguation must consider the full configured fuzz input set, not the
|
|
33
|
-
// selector-filtered subset, so artifact names stay consistent across runs.
|
|
34
|
-
const fullPatterns = Array.isArray(config.input)
|
|
35
|
-
? config.input
|
|
36
|
-
: [config.input];
|
|
37
|
-
const allFuzzFiles = await glob(fullPatterns);
|
|
38
|
-
const duplicateBasenames = resolveDuplicateBasenames(allFuzzFiles);
|
|
39
37
|
const results = [];
|
|
40
38
|
for (const file of inputFiles) {
|
|
41
39
|
const buildStartedAt = Date.now();
|
|
@@ -53,7 +51,6 @@ export async function fuzz(
|
|
|
53
51
|
await runFuzzTarget(
|
|
54
52
|
file,
|
|
55
53
|
activeConfig.outDir,
|
|
56
|
-
duplicateBasenames,
|
|
57
54
|
config,
|
|
58
55
|
fuzzerSelectors,
|
|
59
56
|
buildStartedAt,
|
|
@@ -111,7 +108,6 @@ function encodeRunsOverrideKind(kind) {
|
|
|
111
108
|
async function runFuzzTarget(
|
|
112
109
|
file,
|
|
113
110
|
outDir,
|
|
114
|
-
duplicateBasenames,
|
|
115
111
|
config,
|
|
116
112
|
fuzzerSelectors,
|
|
117
113
|
buildStartedAt,
|
|
@@ -120,7 +116,7 @@ async function runFuzzTarget(
|
|
|
120
116
|
modeName,
|
|
121
117
|
) {
|
|
122
118
|
const startedAt = Date.now();
|
|
123
|
-
const artifact =
|
|
119
|
+
const artifact = resolveArtifactPath(file, config.input);
|
|
124
120
|
const wasmPath = path.resolve(process.cwd(), outDir, artifact);
|
|
125
121
|
const jsPath = resolveBindingsHelperPath(wasmPath);
|
|
126
122
|
const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
|
|
@@ -185,7 +181,11 @@ async function runFuzzTarget(
|
|
|
185
181
|
const crash = persistCrashRecord(config.crashDir, {
|
|
186
182
|
kind: "fuzz",
|
|
187
183
|
file,
|
|
188
|
-
entryKey: buildFuzzCrashEntryKey(
|
|
184
|
+
entryKey: buildFuzzCrashEntryKey(
|
|
185
|
+
file,
|
|
186
|
+
config.input,
|
|
187
|
+
modeName ?? "default",
|
|
188
|
+
),
|
|
189
189
|
mode: modeName ?? "default",
|
|
190
190
|
seed: config.seed,
|
|
191
191
|
error: crashMessage,
|
|
@@ -232,7 +232,11 @@ async function runFuzzTarget(
|
|
|
232
232
|
const crash = persistCrashRecord(config.crashDir, {
|
|
233
233
|
kind: "fuzz",
|
|
234
234
|
file,
|
|
235
|
-
entryKey: buildFuzzCrashEntryKey(
|
|
235
|
+
entryKey: buildFuzzCrashEntryKey(
|
|
236
|
+
file,
|
|
237
|
+
config.input,
|
|
238
|
+
modeName ?? "default",
|
|
239
|
+
),
|
|
236
240
|
mode: modeName ?? "default",
|
|
237
241
|
seed: config.seed,
|
|
238
242
|
error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
|
|
@@ -269,6 +273,7 @@ async function runFuzzTarget(
|
|
|
269
273
|
file,
|
|
270
274
|
entryKey: buildFuzzFailureEntryKey(
|
|
271
275
|
file,
|
|
276
|
+
config.input,
|
|
272
277
|
fuzzer.name,
|
|
273
278
|
modeName ?? "default",
|
|
274
279
|
),
|
|
@@ -341,11 +346,19 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
|
341
346
|
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
342
347
|
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
343
348
|
}
|
|
344
|
-
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
345
|
-
|
|
349
|
+
function buildFuzzFailureEntryKey(file, inputPatterns, name, modeName) {
|
|
350
|
+
const stem = resolveSpecRelativePath(file, inputPatterns).replace(
|
|
351
|
+
/\.ts$/i,
|
|
352
|
+
"",
|
|
353
|
+
);
|
|
354
|
+
return `${stem}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
346
355
|
}
|
|
347
|
-
function buildFuzzCrashEntryKey(file, modeName) {
|
|
348
|
-
|
|
356
|
+
function buildFuzzCrashEntryKey(file, inputPatterns, modeName) {
|
|
357
|
+
const stem = resolveSpecRelativePath(file, inputPatterns).replace(
|
|
358
|
+
/\.ts$/i,
|
|
359
|
+
"",
|
|
360
|
+
);
|
|
361
|
+
return `${stem}.${sanitizeEntryName(modeName)}`;
|
|
349
362
|
}
|
|
350
363
|
function sanitizeEntryName(name) {
|
|
351
364
|
return (
|
|
@@ -458,45 +471,6 @@ function resolveFuzzInputPatterns(configured, selectors) {
|
|
|
458
471
|
}
|
|
459
472
|
return [...patterns];
|
|
460
473
|
}
|
|
461
|
-
function resolveArtifactFileName(file, duplicateBasenames, modeName) {
|
|
462
|
-
const base = path
|
|
463
|
-
.basename(file)
|
|
464
|
-
.replace(/\.spec\.ts$/, "")
|
|
465
|
-
.replace(/\.ts$/, "");
|
|
466
|
-
const legacy = !modeName
|
|
467
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
468
|
-
: `${base}.${modeName}.bindings.wasm`;
|
|
469
|
-
if (!duplicateBasenames.has(path.basename(file))) {
|
|
470
|
-
return legacy;
|
|
471
|
-
}
|
|
472
|
-
const disambiguator = resolveDisambiguator(file);
|
|
473
|
-
if (!disambiguator.length) {
|
|
474
|
-
return legacy;
|
|
475
|
-
}
|
|
476
|
-
const ext = path.extname(legacy);
|
|
477
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
478
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
479
|
-
}
|
|
480
|
-
function resolveDuplicateBasenames(files) {
|
|
481
|
-
const counts = new Map();
|
|
482
|
-
for (const file of files) {
|
|
483
|
-
const base = path.basename(file);
|
|
484
|
-
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
485
|
-
}
|
|
486
|
-
const duplicates = new Set();
|
|
487
|
-
for (const [base, count] of counts) {
|
|
488
|
-
if (count > 1) duplicates.add(base);
|
|
489
|
-
}
|
|
490
|
-
return duplicates;
|
|
491
|
-
}
|
|
492
|
-
function resolveDisambiguator(file) {
|
|
493
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
494
|
-
if (!relDir.length || relDir == ".") return "";
|
|
495
|
-
return relDir
|
|
496
|
-
.replace(/[\\/]+/g, "__")
|
|
497
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
498
|
-
.replace(/^_+|_+$/g, "");
|
|
499
|
-
}
|
|
500
474
|
function resolveBindingsHelperPath(wasmPath) {
|
|
501
475
|
const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
|
|
502
476
|
if (existsSync(bindingsPath)) return bindingsPath;
|