as-test 1.1.5 → 1.1.6-patch.1
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 +18 -1
- package/assembly/__fuzz__/nested/array.fuzz.ts +13 -0
- package/bin/commands/build-core.js +27 -49
- package/bin/commands/fuzz-core.js +13 -53
- package/bin/commands/run-core.js +30 -97
- package/bin/commands/web-session.js +4 -0
- package/bin/crash-store.js +1 -1
- package/bin/index.js +37 -75
- package/bin/util.js +80 -0
- package/package.json +1 -1
- package/transform/lib/coverage.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## 2026-05-
|
|
3
|
+
## 2026-05-19 - v1.1.6-patch.1
|
|
4
|
+
|
|
5
|
+
### Directory-preserving artifact layout
|
|
6
|
+
|
|
7
|
+
- feat: build artifacts, fuzz artifacts, snapshots, readable logs, coverage logs, and crash records now mirror the source tree under the configured input globs instead of being flattened into a single directory with a `____`-mangled disambiguator suffix. For `assembly/__tests__/nested/array.spec.ts` the artifact is `outDir/<mode>/nested/array.spec.wasm` (previously `outDir/<mode>/array.<mode>.<target>.assembly____tests____nested.wasm`). Sidecar `.js` / `.d.ts` files sit next to the wasm.
|
|
8
|
+
- feat: filename simplified to `<stem>.wasm` — the `.<mode>.<target>` suffix has been dropped since the mode is already a directory level and the target is implied by the mode config.
|
|
9
|
+
- feat: add `resolveGlobBase`, `resolveSpecRelativePath`, and `resolveArtifactPath` in `cli/util.ts` as the shared path helpers used by every code path that writes or looks up a per-spec artifact. Glob bases are computed component-wise (so `assembly/__tests` is not a prefix of `assembly/__tests__/foo.spec.ts`) and the longest matching base wins when multiple configured input patterns overlap. Patterns without a static prefix (e.g. `**/*.spec.ts`) fall back to the file's cwd-relative path.
|
|
10
|
+
- feat: add an up-front collision check in `build()` that throws a clear error naming both source files when two configured inputs would resolve to the same artifact path.
|
|
11
|
+
- fix: `ast test <one-spec>`, `ast run <one-spec>`, and `ast fuzz <one-spec>` no longer drop the disambiguator when only one of two same-basename files is being built. The build side and the runner side now compute the same path from the same configured input set, so the runner no longer reports `bindings artifact not found` after a single-file build.
|
|
12
|
+
- fix: build sites now `mkdir -p` the artifact's parent directory before invoking `asc` — pinned `assemblyscript@0.28.17`'s `-o` flag does not create parents and would otherwise ENOENT for any new nested directory.
|
|
13
|
+
- fix: `persistCrashRecord` now `mkdir -p`'s the entry's parent directory, supporting `/` in entry keys so nested specs and fuzz failures get their own crash files instead of clobbering by basename.
|
|
14
|
+
- fix: replace the hardcoded `/__tests__/` and `/__fuzz__/` markers in snapshot and readable-log path resolution with proper glob-base computation, so projects with custom input layouts (e.g. `tests/specs/**/*.spec.ts`) now nest correctly instead of silently falling back to basename-only paths.
|
|
15
|
+
- fix: clean break on snapshot file layout — the legacy `${base}.snap.json` and `${base}.${disambiguator}.snap.json` fallbacks have been removed. After upgrading, run `--create-snapshots` or `--overwrite-snapshots` once so snapshots are written at their new relative-path locations.
|
|
16
|
+
- chore: delete the duplicate-basename plumbing introduced by the patch immediately preceding this one. Every code path that used to thread `Set<string>` of duplicate basenames now reads paths directly. No back-compat for the old flat-with-suffix scheme — `ast clean` removes any orphan artifacts.
|
|
17
|
+
|
|
18
|
+
## 2026-05-14 - v1.1.6
|
|
4
19
|
|
|
5
20
|
- fix: coverage `mode` and `dependencies` filtering now correctly handles AssemblyScript-normalized `~lib/<pkg>/...` paths, which are the actual runtime paths emitted for `node_modules` imports.
|
|
6
21
|
- fix: `ENTRY_FILE` injected by the transform now uses the full relative path instead of the basename, preventing snapshot key collisions between specs with the same filename in different directories; snapshot lookup normalizes the file prefix to maintain backward compatibility with existing `.snap` files.
|
|
7
22
|
- fix: transform visitor and coverage instrumentation now resolve `NodeKind` values at runtime instead of relying on compile-time const enum inlining, so they remain correct across AssemblyScript versions.
|
|
8
23
|
- fix: add a no-op `TupleType` case to the transform visitor so files using tuple types no longer throw during instrumentation.
|
|
24
|
+
- fix: coverage transform no longer wraps `return this` in constructors, preventing AS231 ("A class with a constructor explicitly returning something else than 'this' must be '@final'").
|
|
25
|
+
- fix: coverage transform preserves expression-body arrows instead of converting them to block bodies, preventing TS1140 ("Type argument expected") on typed arrow parameters such as `[1,2,3].map((x: i32) => x + 1)`.
|
|
9
26
|
|
|
10
27
|
## 2026-05-14 - v1.1.4
|
|
11
28
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, fuzz, FuzzSeed } from "as-test";
|
|
2
|
+
|
|
3
|
+
fuzz(
|
|
4
|
+
"nested array.fuzz: index windows stay ordered",
|
|
5
|
+
(start: i32, end: i32): bool => {
|
|
6
|
+
expect(start <= end).toBe(true);
|
|
7
|
+
return end - start <= 64;
|
|
8
|
+
},
|
|
9
|
+
).generate((seed: FuzzSeed, run: (start: i32, end: i32) => bool): void => {
|
|
10
|
+
const start = seed.i32({ min: 0, max: 64 });
|
|
11
|
+
const width = seed.i32({ min: 0, max: 64 });
|
|
12
|
+
run(start, start + width);
|
|
13
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync } from "fs";
|
|
2
2
|
import { glob } from "glob";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { createMemoryStream, main as ascMain, } from "assemblyscript/dist/asc.js";
|
|
7
|
-
import { applyMode, getPkgRunner, loadConfig, tokenizeCommand, resolveProjectModule, } from "../util.js";
|
|
7
|
+
import { applyMode, getPkgRunner, loadConfig, resolveArtifactPath, resolveSpecRelativePath, tokenizeCommand, resolveProjectModule, } from "../util.js";
|
|
8
8
|
import { persistCrashRecord } from "../crash-store.js";
|
|
9
9
|
import { BuildWorkerPool } from "../build-worker-pool.js";
|
|
10
10
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
@@ -36,9 +36,10 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
|
|
|
36
36
|
ensureDeps(config);
|
|
37
37
|
}
|
|
38
38
|
const pkgRunner = getPkgRunner();
|
|
39
|
-
const
|
|
39
|
+
const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
40
|
+
const inputPatterns = resolveInputPatterns(sourceInputPatterns, selectors);
|
|
40
41
|
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
41
|
-
|
|
42
|
+
await assertNoArtifactCollisions(sourceInputPatterns);
|
|
42
43
|
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
43
44
|
const buildEnv = {
|
|
44
45
|
...mode.env,
|
|
@@ -50,7 +51,7 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
|
|
|
50
51
|
!hasCustomBuildCommand(config)) {
|
|
51
52
|
const pool = getSerialBuildWorkerPool();
|
|
52
53
|
for (const file of inputFiles) {
|
|
53
|
-
const outFile =
|
|
54
|
+
const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
|
|
54
55
|
const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
|
|
55
56
|
await pool.buildFileMode({
|
|
56
57
|
configPath,
|
|
@@ -64,7 +65,8 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
|
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
for (const file of inputFiles) {
|
|
67
|
-
const outFile =
|
|
68
|
+
const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
|
|
69
|
+
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
68
70
|
const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
|
|
69
71
|
try {
|
|
70
72
|
await buildFile(invocation, buildEnv);
|
|
@@ -79,6 +81,7 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
|
|
|
79
81
|
kind,
|
|
80
82
|
stage: "build",
|
|
81
83
|
file,
|
|
84
|
+
entryKey: resolveSpecRelativePath(file, sourceInputPatterns).replace(/\.ts$/i, ""),
|
|
82
85
|
mode: modeLabel,
|
|
83
86
|
cwd: process.cwd(),
|
|
84
87
|
buildCommand,
|
|
@@ -127,8 +130,8 @@ export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH
|
|
|
127
130
|
if (overrides.args?.length) {
|
|
128
131
|
config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
|
|
129
132
|
}
|
|
130
|
-
const
|
|
131
|
-
const outFile =
|
|
133
|
+
const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
134
|
+
const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
|
|
132
135
|
return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
133
136
|
}
|
|
134
137
|
export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
|
|
@@ -145,8 +148,8 @@ export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file,
|
|
|
145
148
|
if (hasCustomBuildCommand(config)) {
|
|
146
149
|
return null;
|
|
147
150
|
}
|
|
148
|
-
const
|
|
149
|
-
const outFile =
|
|
151
|
+
const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
152
|
+
const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
|
|
150
153
|
const invocation = getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
151
154
|
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
152
155
|
const buildEnv = {
|
|
@@ -245,46 +248,21 @@ function expandBuildCommand(template, file, outFile, target, modeName) {
|
|
|
245
248
|
.replace(/<target>/g, target)
|
|
246
249
|
.replace(/<mode>/g, modeName ?? "");
|
|
247
250
|
}
|
|
248
|
-
function
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.replace(/\.ts$/, "");
|
|
253
|
-
const legacy = !modeName
|
|
254
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
255
|
-
: `${base}.${modeName}.${target}.wasm`;
|
|
256
|
-
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
257
|
-
return legacy;
|
|
258
|
-
}
|
|
259
|
-
const disambiguator = resolveDisambiguator(file);
|
|
260
|
-
if (!disambiguator.length) {
|
|
261
|
-
return legacy;
|
|
262
|
-
}
|
|
263
|
-
const ext = path.extname(legacy);
|
|
264
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
265
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
266
|
-
}
|
|
267
|
-
function resolveDuplicateBasenames(files) {
|
|
268
|
-
const counts = new Map();
|
|
251
|
+
async function assertNoArtifactCollisions(configured) {
|
|
252
|
+
const patterns = Array.isArray(configured) ? configured : [configured];
|
|
253
|
+
const files = await glob(patterns);
|
|
254
|
+
const seen = new Map();
|
|
269
255
|
for (const file of files) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
function resolveDisambiguator(file) {
|
|
281
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
282
|
-
if (!relDir.length || relDir == ".")
|
|
283
|
-
return "";
|
|
284
|
-
return relDir
|
|
285
|
-
.replace(/[\\/]+/g, "__")
|
|
286
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
287
|
-
.replace(/^_+|_+$/g, "");
|
|
256
|
+
const artifact = resolveArtifactPath(file, patterns);
|
|
257
|
+
const prev = seen.get(artifact);
|
|
258
|
+
if (prev != null && prev !== file) {
|
|
259
|
+
throw new Error(`Two input files resolve to the same artifact path "${artifact}":\n` +
|
|
260
|
+
` - ${prev}\n` +
|
|
261
|
+
` - ${file}\n` +
|
|
262
|
+
`Rename one of them or narrow the input patterns to disambiguate.`);
|
|
263
|
+
}
|
|
264
|
+
seen.set(artifact, file);
|
|
265
|
+
}
|
|
288
266
|
}
|
|
289
267
|
function resolveInputPatterns(configured, selectors) {
|
|
290
268
|
const configuredInputs = Array.isArray(configured)
|
|
@@ -3,7 +3,7 @@ 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 { applyMode, loadConfig } from "../util.js";
|
|
6
|
+
import { applyMode, loadConfig, resolveArtifactPath, resolveSpecRelativePath, } from "../util.js";
|
|
7
7
|
import { persistCrashRecord } from "../crash-store.js";
|
|
8
8
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
9
9
|
const MAGIC = Buffer.from("WIPC");
|
|
@@ -19,14 +19,13 @@ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], mod
|
|
|
19
19
|
if (!inputFiles.length) {
|
|
20
20
|
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
21
21
|
}
|
|
22
|
-
const duplicateBasenames = resolveDuplicateBasenames(inputFiles);
|
|
23
22
|
const results = [];
|
|
24
23
|
for (const file of inputFiles) {
|
|
25
24
|
const buildStartedAt = Date.now();
|
|
26
25
|
await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" }, loadedConfig);
|
|
27
26
|
const buildFinishedAt = Date.now();
|
|
28
27
|
const buildTime = buildFinishedAt - buildStartedAt;
|
|
29
|
-
results.push(await runFuzzTarget(file, activeConfig.outDir,
|
|
28
|
+
results.push(await runFuzzTarget(file, activeConfig.outDir, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName));
|
|
30
29
|
}
|
|
31
30
|
return results;
|
|
32
31
|
}
|
|
@@ -70,9 +69,9 @@ function encodeRunsOverrideKind(kind) {
|
|
|
70
69
|
return 4;
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
|
-
async function runFuzzTarget(file, outDir,
|
|
72
|
+
async function runFuzzTarget(file, outDir, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName) {
|
|
74
73
|
const startedAt = Date.now();
|
|
75
|
-
const artifact =
|
|
74
|
+
const artifact = resolveArtifactPath(file, config.input);
|
|
76
75
|
const wasmPath = path.resolve(process.cwd(), outDir, artifact);
|
|
77
76
|
const jsPath = resolveBindingsHelperPath(wasmPath);
|
|
78
77
|
const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
|
|
@@ -140,7 +139,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
|
|
|
140
139
|
const crash = persistCrashRecord(config.crashDir, {
|
|
141
140
|
kind: "fuzz",
|
|
142
141
|
file,
|
|
143
|
-
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
142
|
+
entryKey: buildFuzzCrashEntryKey(file, config.input, modeName ?? "default"),
|
|
144
143
|
mode: modeName ?? "default",
|
|
145
144
|
seed: config.seed,
|
|
146
145
|
error: crashMessage,
|
|
@@ -188,7 +187,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
|
|
|
188
187
|
const crash = persistCrashRecord(config.crashDir, {
|
|
189
188
|
kind: "fuzz",
|
|
190
189
|
file,
|
|
191
|
-
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
190
|
+
entryKey: buildFuzzCrashEntryKey(file, config.input, modeName ?? "default"),
|
|
192
191
|
mode: modeName ?? "default",
|
|
193
192
|
seed: config.seed,
|
|
194
193
|
error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
|
|
@@ -223,7 +222,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
|
|
|
223
222
|
const crash = persistCrashRecord(config.crashDir, {
|
|
224
223
|
kind: "fuzz",
|
|
225
224
|
file,
|
|
226
|
-
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
225
|
+
entryKey: buildFuzzFailureEntryKey(file, config.input, fuzzer.name, modeName ?? "default"),
|
|
227
226
|
mode: modeName ?? "default",
|
|
228
227
|
seed: firstFailureSeed,
|
|
229
228
|
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", fuzzer.selector, 1),
|
|
@@ -285,11 +284,13 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
|
285
284
|
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
286
285
|
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
287
286
|
}
|
|
288
|
-
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
289
|
-
|
|
287
|
+
function buildFuzzFailureEntryKey(file, inputPatterns, name, modeName) {
|
|
288
|
+
const stem = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
|
|
289
|
+
return `${stem}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
290
290
|
}
|
|
291
|
-
function buildFuzzCrashEntryKey(file, modeName) {
|
|
292
|
-
|
|
291
|
+
function buildFuzzCrashEntryKey(file, inputPatterns, modeName) {
|
|
292
|
+
const stem = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
|
|
293
|
+
return `${stem}.${sanitizeEntryName(modeName)}`;
|
|
293
294
|
}
|
|
294
295
|
function sanitizeEntryName(name) {
|
|
295
296
|
return (name
|
|
@@ -398,47 +399,6 @@ function resolveFuzzInputPatterns(configured, selectors) {
|
|
|
398
399
|
}
|
|
399
400
|
return [...patterns];
|
|
400
401
|
}
|
|
401
|
-
function resolveArtifactFileName(file, duplicateBasenames, modeName) {
|
|
402
|
-
const base = path
|
|
403
|
-
.basename(file)
|
|
404
|
-
.replace(/\.spec\.ts$/, "")
|
|
405
|
-
.replace(/\.ts$/, "");
|
|
406
|
-
const legacy = !modeName
|
|
407
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
408
|
-
: `${base}.${modeName}.bindings.wasm`;
|
|
409
|
-
if (!duplicateBasenames.has(path.basename(file))) {
|
|
410
|
-
return legacy;
|
|
411
|
-
}
|
|
412
|
-
const disambiguator = resolveDisambiguator(file);
|
|
413
|
-
if (!disambiguator.length) {
|
|
414
|
-
return legacy;
|
|
415
|
-
}
|
|
416
|
-
const ext = path.extname(legacy);
|
|
417
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
418
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
419
|
-
}
|
|
420
|
-
function resolveDuplicateBasenames(files) {
|
|
421
|
-
const counts = new Map();
|
|
422
|
-
for (const file of files) {
|
|
423
|
-
const base = path.basename(file);
|
|
424
|
-
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
425
|
-
}
|
|
426
|
-
const duplicates = new Set();
|
|
427
|
-
for (const [base, count] of counts) {
|
|
428
|
-
if (count > 1)
|
|
429
|
-
duplicates.add(base);
|
|
430
|
-
}
|
|
431
|
-
return duplicates;
|
|
432
|
-
}
|
|
433
|
-
function resolveDisambiguator(file) {
|
|
434
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
435
|
-
if (!relDir.length || relDir == ".")
|
|
436
|
-
return "";
|
|
437
|
-
return relDir
|
|
438
|
-
.replace(/[\\/]+/g, "__")
|
|
439
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
440
|
-
.replace(/^_+|_+$/g, "");
|
|
441
|
-
}
|
|
442
402
|
function resolveBindingsHelperPath(wasmPath) {
|
|
443
403
|
const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
|
|
444
404
|
if (existsSync(bindingsPath))
|
package/bin/commands/run-core.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import { Channel, MessageType } from "../wipc.js";
|
|
5
|
-
import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
|
|
5
|
+
import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, resolveArtifactPath, resolveSpecRelativePath, tokenizeCommand, } from "../util.js";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { pathToFileURL } from "url";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -16,7 +16,7 @@ import { persistCrashRecord } from "../crash-store.js";
|
|
|
16
16
|
import { describeCoveragePoint } from "../coverage-points.js";
|
|
17
17
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
18
18
|
class SnapshotStore {
|
|
19
|
-
constructor(specFile, snapshotDir,
|
|
19
|
+
constructor(specFile, snapshotDir, inputPatterns) {
|
|
20
20
|
this.dirty = false;
|
|
21
21
|
this.created = 0;
|
|
22
22
|
this.updated = 0;
|
|
@@ -25,16 +25,16 @@ class SnapshotStore {
|
|
|
25
25
|
this.warnedMissing = new Set();
|
|
26
26
|
this.specBasename = path.basename(specFile);
|
|
27
27
|
const dir = path.join(process.cwd(), snapshotDir);
|
|
28
|
-
const relative =
|
|
28
|
+
const relative = resolveSpecRelativePath(specFile, inputPatterns).replace(/\.ts$/i, ".snap");
|
|
29
29
|
this.filePath = path.join(dir, relative);
|
|
30
|
-
const sourcePath =
|
|
30
|
+
const sourcePath = existsSync(this.filePath) ? this.filePath : null;
|
|
31
31
|
const loaded = sourcePath
|
|
32
32
|
? readSnapshotFile(sourcePath, specFile)
|
|
33
33
|
: { data: {}, normalized: false, preamble: "" };
|
|
34
34
|
this.data = loaded.data;
|
|
35
35
|
this.preamble = loaded.preamble;
|
|
36
|
-
this.existed = Boolean(sourcePath
|
|
37
|
-
this.dirty = Boolean(
|
|
36
|
+
this.existed = Boolean(sourcePath);
|
|
37
|
+
this.dirty = Boolean(loaded.normalized);
|
|
38
38
|
}
|
|
39
39
|
assert(key, actual, allowSnapshot, createSnapshots, overwriteSnapshots) {
|
|
40
40
|
key = canonicalizeSnapshotKey(key);
|
|
@@ -81,21 +81,6 @@ class SnapshotStore {
|
|
|
81
81
|
writeFileSync(this.filePath, formatSnapshotFile(this.data, this.filePath, this.existed ? this.preamble : defaultSnapshotPreamble()));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
function resolveSnapshotSourcePath(specFile, snapshotDir, duplicateSpecBasenames, preferredPath) {
|
|
85
|
-
if (existsSync(preferredPath))
|
|
86
|
-
return preferredPath;
|
|
87
|
-
const base = path.basename(specFile, ".ts");
|
|
88
|
-
const legacyFlat = path.join(snapshotDir, `${base}.snap.json`);
|
|
89
|
-
if (existsSync(legacyFlat))
|
|
90
|
-
return legacyFlat;
|
|
91
|
-
const disambiguator = resolveDisambiguator(specFile, duplicateSpecBasenames);
|
|
92
|
-
if (disambiguator.length) {
|
|
93
|
-
const legacyDisambiguated = path.join(snapshotDir, `${base}.${disambiguator}.snap.json`);
|
|
94
|
-
if (existsSync(legacyDisambiguated))
|
|
95
|
-
return legacyDisambiguated;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
84
|
function readSnapshotFile(filePath, specFile) {
|
|
100
85
|
const raw = readFileSync(filePath, "utf8");
|
|
101
86
|
if (filePath.endsWith(".json")) {
|
|
@@ -247,16 +232,11 @@ function trimSnapshotPreamble(lines) {
|
|
|
247
232
|
end--;
|
|
248
233
|
return lines.slice(0, end).join("\n");
|
|
249
234
|
}
|
|
235
|
+
// Only the basename of the returned path matters — callers feed this into
|
|
236
|
+
// `path.basename(...)` to localize snapshot keys (strip the "${basename}::"
|
|
237
|
+
// prefix). The full path is therefore synthetic but stable.
|
|
250
238
|
function resolveSnapshotSpecFile(filePath) {
|
|
251
|
-
|
|
252
|
-
const marker = "/snapshots/";
|
|
253
|
-
const markerIndex = normalized.lastIndexOf(marker);
|
|
254
|
-
const suffix = markerIndex >= 0
|
|
255
|
-
? normalized.slice(markerIndex + marker.length)
|
|
256
|
-
: path.basename(normalized);
|
|
257
|
-
const withoutMode = suffix.replace(/^default\//, "");
|
|
258
|
-
const relative = withoutMode.replace(/\.snap$/, ".ts");
|
|
259
|
-
return `assembly/__tests__/${relative}`;
|
|
239
|
+
return path.basename(filePath).replace(/\.snap$/, ".ts");
|
|
260
240
|
}
|
|
261
241
|
function localizeSnapshotKey(specFile, key) {
|
|
262
242
|
const prefix = `${path.basename(specFile)}::`;
|
|
@@ -297,16 +277,8 @@ function canonicalizeSnapshotLocalKey(localKey) {
|
|
|
297
277
|
}
|
|
298
278
|
return localKey;
|
|
299
279
|
}
|
|
300
|
-
function
|
|
301
|
-
const
|
|
302
|
-
const marker = `/${segment}/`;
|
|
303
|
-
const index = normalized.lastIndexOf(marker);
|
|
304
|
-
if (index >= 0)
|
|
305
|
-
return normalized.slice(index + marker.length);
|
|
306
|
-
return path.basename(normalized);
|
|
307
|
-
}
|
|
308
|
-
function writeReadableLog(logRoot, file, suites, modeName, buildCommand, runCommand, snapshotSummary) {
|
|
309
|
-
const relative = resolveArtifactRelativePath(file, "__tests__").replace(/\.ts$/, ".log");
|
|
280
|
+
function writeReadableLog(logRoot, file, inputPatterns, suites, modeName, buildCommand, runCommand, snapshotSummary) {
|
|
281
|
+
const relative = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, ".log");
|
|
310
282
|
const filePath = path.join(logRoot, relative);
|
|
311
283
|
const dir = path.dirname(filePath);
|
|
312
284
|
if (!existsSync(dir))
|
|
@@ -562,7 +534,6 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
562
534
|
const config = mode.config;
|
|
563
535
|
const inputPatterns = resolveInputPatterns(config.input, selectors);
|
|
564
536
|
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
565
|
-
const duplicateSpecBasenames = await resolveDuplicateSpecBasenames(config.input);
|
|
566
537
|
const snapshotEnabled = flags.snapshot !== false;
|
|
567
538
|
const createSnapshots = Boolean(flags.createSnapshots);
|
|
568
539
|
const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
|
|
@@ -623,7 +594,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
623
594
|
try {
|
|
624
595
|
for (let i = 0; i < inputFiles.length; i++) {
|
|
625
596
|
const file = inputFiles[i];
|
|
626
|
-
const outFile = path.join(config.outDir,
|
|
597
|
+
const outFile = path.join(config.outDir, resolveArtifactPath(file, config.input));
|
|
627
598
|
if (!existsSync(outFile)) {
|
|
628
599
|
const buildStartedAt = Date.now();
|
|
629
600
|
await build(resolvedConfigPath, [file], options.modeName, { coverage: flags.coverage }, {}, loadedConfig);
|
|
@@ -642,7 +613,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
642
613
|
.map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
|
|
643
614
|
};
|
|
644
615
|
const runCommandForLog = formatInvocation(invocation);
|
|
645
|
-
const snapshotStore = new SnapshotStore(file, config.snapshotDir,
|
|
616
|
+
const snapshotStore = new SnapshotStore(file, config.snapshotDir, config.input);
|
|
646
617
|
let report;
|
|
647
618
|
try {
|
|
648
619
|
const runtimeEnv = {
|
|
@@ -655,9 +626,10 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
655
626
|
? { BROWSER: config.runOptions.runtime.browser.trim() }
|
|
656
627
|
: {}),
|
|
657
628
|
};
|
|
629
|
+
const crashEntryKey = resolveSpecRelativePath(file, config.input).replace(/\.ts$/i, "");
|
|
658
630
|
report = webSession
|
|
659
|
-
? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
|
|
660
|
-
: await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
|
|
631
|
+
? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, crashEntryKey, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
|
|
632
|
+
: await runProcess(invocation, file, config.fuzz.crashDir, crashEntryKey, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
|
|
661
633
|
}
|
|
662
634
|
catch (error) {
|
|
663
635
|
const modeLabel = options.modeName ?? "default";
|
|
@@ -698,7 +670,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
698
670
|
mkdirSync(logRoot, { recursive: true });
|
|
699
671
|
}
|
|
700
672
|
for (const report of reports) {
|
|
701
|
-
writeReadableLog(logRoot, report.file, report.suites, options.modeName, options.buildCommandsByFile?.[report.file] ??
|
|
673
|
+
writeReadableLog(logRoot, report.file, config.input, report.suites, options.modeName, options.buildCommandsByFile?.[report.file] ??
|
|
702
674
|
options.buildCommand ??
|
|
703
675
|
"", report.runCommand, report.snapshotSummary);
|
|
704
676
|
}
|
|
@@ -713,10 +685,9 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
713
685
|
coverageDir != "none" &&
|
|
714
686
|
coverageSummary.files.length > 0) {
|
|
715
687
|
const resolvedCoverageDir = path.join(process.cwd(), coverageDir);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
writeFileSync(path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json"), JSON.stringify(coverageSummary, null, 2));
|
|
688
|
+
const coverageFilePath = path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json");
|
|
689
|
+
mkdirSync(path.dirname(coverageFilePath), { recursive: true });
|
|
690
|
+
writeFileSync(coverageFilePath, JSON.stringify(coverageSummary, null, 2));
|
|
720
691
|
}
|
|
721
692
|
if (options.emitRunComplete !== false) {
|
|
722
693
|
const totalModes = Math.max(options.modeSummaryTotal ?? 1, 1);
|
|
@@ -937,51 +908,6 @@ instantiate(imports)
|
|
|
937
908
|
}
|
|
938
909
|
return null;
|
|
939
910
|
}
|
|
940
|
-
function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
|
|
941
|
-
const base = path
|
|
942
|
-
.basename(file)
|
|
943
|
-
.replace(/\.spec\.ts$/, "")
|
|
944
|
-
.replace(/\.ts$/, "");
|
|
945
|
-
const legacy = !modeName
|
|
946
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
947
|
-
: `${base}.${modeName}.${target}.wasm`;
|
|
948
|
-
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
949
|
-
return legacy;
|
|
950
|
-
}
|
|
951
|
-
const disambiguator = resolveDisambiguator(file, duplicateSpecBasenames);
|
|
952
|
-
if (!disambiguator.length) {
|
|
953
|
-
return legacy;
|
|
954
|
-
}
|
|
955
|
-
const ext = path.extname(legacy);
|
|
956
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
957
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
958
|
-
}
|
|
959
|
-
async function resolveDuplicateSpecBasenames(configured) {
|
|
960
|
-
const patterns = Array.isArray(configured) ? configured : [configured];
|
|
961
|
-
const files = await glob(patterns);
|
|
962
|
-
const counts = new Map();
|
|
963
|
-
for (const file of files) {
|
|
964
|
-
const base = path.basename(file);
|
|
965
|
-
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
966
|
-
}
|
|
967
|
-
const duplicates = new Set();
|
|
968
|
-
for (const [base, count] of counts) {
|
|
969
|
-
if (count > 1)
|
|
970
|
-
duplicates.add(base);
|
|
971
|
-
}
|
|
972
|
-
return duplicates;
|
|
973
|
-
}
|
|
974
|
-
function resolveDisambiguator(file, duplicateSpecBasenames) {
|
|
975
|
-
if (!duplicateSpecBasenames.has(path.basename(file)))
|
|
976
|
-
return "";
|
|
977
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
978
|
-
if (!relDir.length || relDir == ".")
|
|
979
|
-
return "";
|
|
980
|
-
return relDir
|
|
981
|
-
.replace(/[\\/]+/g, "__")
|
|
982
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
983
|
-
.replace(/^_+|_+$/g, "");
|
|
984
|
-
}
|
|
985
911
|
function resolveRuntimeTargetEnv(target, wasmPath) {
|
|
986
912
|
if (target == "bindings") {
|
|
987
913
|
return resolveBindingsRuntimeEnv(wasmPath);
|
|
@@ -1602,7 +1528,7 @@ function compareCoveragePoints(a, b) {
|
|
|
1602
1528
|
return a.type.localeCompare(b.type);
|
|
1603
1529
|
return a.hash.localeCompare(b.hash);
|
|
1604
1530
|
}
|
|
1605
|
-
async function runProcess(invocation, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
1531
|
+
async function runProcess(invocation, specFile, crashDir, crashEntryKey, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
1606
1532
|
const child = spawn(invocation.command, invocation.args, {
|
|
1607
1533
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1608
1534
|
shell: false,
|
|
@@ -1803,6 +1729,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1803
1729
|
persistCrashRecord(crashDir, {
|
|
1804
1730
|
kind: "test",
|
|
1805
1731
|
file: specFile,
|
|
1732
|
+
entryKey: crashEntryKey,
|
|
1806
1733
|
mode: modeName ?? "default",
|
|
1807
1734
|
error: errorText,
|
|
1808
1735
|
stdout: stdoutBuffer,
|
|
@@ -1845,6 +1772,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1845
1772
|
persistCrashRecord(crashDir, {
|
|
1846
1773
|
kind: "test",
|
|
1847
1774
|
file: specFile,
|
|
1775
|
+
entryKey: crashEntryKey,
|
|
1848
1776
|
mode: modeName ?? "default",
|
|
1849
1777
|
error: fullError,
|
|
1850
1778
|
stdout: stdoutBuffer,
|
|
@@ -1873,6 +1801,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1873
1801
|
persistCrashRecord(crashDir, {
|
|
1874
1802
|
kind: "test",
|
|
1875
1803
|
file: specFile,
|
|
1804
|
+
entryKey: crashEntryKey,
|
|
1876
1805
|
mode: modeName ?? "default",
|
|
1877
1806
|
error: errorText || "runtime reported an unknown error",
|
|
1878
1807
|
stdout: stdoutBuffer,
|
|
@@ -1890,6 +1819,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1890
1819
|
persistCrashRecord(crashDir, {
|
|
1891
1820
|
kind: "test",
|
|
1892
1821
|
file: specFile,
|
|
1822
|
+
entryKey: crashEntryKey,
|
|
1893
1823
|
mode: modeName ?? "default",
|
|
1894
1824
|
error: fullError,
|
|
1895
1825
|
stdout: stdoutBuffer,
|
|
@@ -1910,6 +1840,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1910
1840
|
persistCrashRecord(crashDir, {
|
|
1911
1841
|
kind: "test",
|
|
1912
1842
|
file: specFile,
|
|
1843
|
+
entryKey: crashEntryKey,
|
|
1913
1844
|
mode: modeName ?? "default",
|
|
1914
1845
|
error: errorText || "runtime reported an unknown error",
|
|
1915
1846
|
stdout: stdoutBuffer,
|
|
@@ -1921,7 +1852,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1921
1852
|
}
|
|
1922
1853
|
return report;
|
|
1923
1854
|
}
|
|
1924
|
-
async function runWebSessionProcess(session, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
1855
|
+
async function runWebSessionProcess(session, specFile, crashDir, crashEntryKey, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
1925
1856
|
const input = new PassThrough();
|
|
1926
1857
|
const output = new PassThrough();
|
|
1927
1858
|
let report = null;
|
|
@@ -2145,6 +2076,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
2145
2076
|
persistCrashRecord(crashDir, {
|
|
2146
2077
|
kind: "test",
|
|
2147
2078
|
file: specFile,
|
|
2079
|
+
entryKey: crashEntryKey,
|
|
2148
2080
|
mode: modeName ?? "default",
|
|
2149
2081
|
error: fullError,
|
|
2150
2082
|
stdout: stdoutBuffer,
|
|
@@ -2179,6 +2111,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
|
|
|
2179
2111
|
persistCrashRecord(crashDir, {
|
|
2180
2112
|
kind: "test",
|
|
2181
2113
|
file: specFile,
|
|
2114
|
+
entryKey: crashEntryKey,
|
|
2182
2115
|
mode: modeName ?? "default",
|
|
2183
2116
|
error: fullError,
|
|
2184
2117
|
stdout: stdoutBuffer,
|
|
@@ -56,6 +56,10 @@ export class PersistentWebSessionHost {
|
|
|
56
56
|
? env.AS_TEST_HELPER_PATH
|
|
57
57
|
: null;
|
|
58
58
|
const jobId = String(this.nextJobId++);
|
|
59
|
+
// URL paths use only the basename — that is safe because each job has
|
|
60
|
+
// exactly one wasm and one helper, scoped by the jobId path component.
|
|
61
|
+
// Nested directory artifacts (e.g. nested/array.spec.wasm) are read
|
|
62
|
+
// from disk via `wasmPath`/`helperPath`, not via these URLs.
|
|
59
63
|
const browserEnv = {
|
|
60
64
|
...env,
|
|
61
65
|
AS_TEST_WASM_PATH: `/job/${jobId}/${path.basename(wasmPath)}`,
|
package/bin/crash-store.js
CHANGED
|
@@ -5,9 +5,9 @@ export function persistCrashRecord(rootDir, record) {
|
|
|
5
5
|
? record.entryKey
|
|
6
6
|
: crashEntryKey(record.file);
|
|
7
7
|
const dir = path.resolve(process.cwd(), rootDir);
|
|
8
|
-
mkdirSync(dir, { recursive: true });
|
|
9
8
|
const jsonPath = path.join(dir, `${entry}.json`);
|
|
10
9
|
const logPath = path.join(dir, `${entry}.log`);
|
|
10
|
+
mkdirSync(path.dirname(jsonPath), { recursive: true });
|
|
11
11
|
const payload = {
|
|
12
12
|
timestamp: new Date().toISOString(),
|
|
13
13
|
...record,
|
package/bin/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { executeInitCommand } from "./commands/init.js";
|
|
|
10
10
|
import { executeDoctorCommand } from "./commands/doctor.js";
|
|
11
11
|
import { executeCleanCommand } from "./commands/clean.js";
|
|
12
12
|
import { fuzz } from "./commands/fuzz-core.js";
|
|
13
|
-
import { applyMode, formatTime, formatSpecDisplayPath, getDefaultModeNames, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
13
|
+
import { applyMode, formatTime, formatSpecDisplayPath, getDefaultModeNames, getCliVersion, loadConfig, resolveArtifactPath, resolveModeNames, resolveSpecRelativePath, } from "./util.js";
|
|
14
14
|
import * as path from "path";
|
|
15
15
|
import { spawnSync } from "child_process";
|
|
16
16
|
import { glob } from "glob";
|
|
@@ -1140,7 +1140,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
|
|
|
1140
1140
|
const results = [];
|
|
1141
1141
|
let failed = false;
|
|
1142
1142
|
const buildIntervals = [];
|
|
1143
|
-
const
|
|
1143
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1144
1144
|
for (const file of files) {
|
|
1145
1145
|
const buildStartedAt = Date.now();
|
|
1146
1146
|
let result;
|
|
@@ -1148,7 +1148,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
|
|
|
1148
1148
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
1149
1149
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1150
1150
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1151
|
-
const artifactKey =
|
|
1151
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
1152
1152
|
result = await run(runFlags, configPath, [file], false, {
|
|
1153
1153
|
reporter,
|
|
1154
1154
|
webSession,
|
|
@@ -1156,7 +1156,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
|
|
|
1156
1156
|
emitRunStart: false,
|
|
1157
1157
|
emitRunComplete: false,
|
|
1158
1158
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1159
|
-
coverageFileName:
|
|
1159
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
1160
1160
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1161
1161
|
modeName,
|
|
1162
1162
|
});
|
|
@@ -1351,7 +1351,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1351
1351
|
failed: false,
|
|
1352
1352
|
passed: false,
|
|
1353
1353
|
}));
|
|
1354
|
-
const
|
|
1354
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1355
1355
|
const buildIntervals = [];
|
|
1356
1356
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
1357
1357
|
const file = files[fileIndex];
|
|
@@ -1365,7 +1365,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1365
1365
|
const modeName = modes[i];
|
|
1366
1366
|
try {
|
|
1367
1367
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
1368
|
-
const artifactKey =
|
|
1368
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
1369
1369
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1370
1370
|
reporter: silentReporter,
|
|
1371
1371
|
reporterKind: "default",
|
|
@@ -1373,7 +1373,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1373
1373
|
emitRunStart: false,
|
|
1374
1374
|
emitRunComplete: false,
|
|
1375
1375
|
logFileName: `run.${artifactKey}.log.json`,
|
|
1376
|
-
coverageFileName:
|
|
1376
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
1377
1377
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1378
1378
|
modeName,
|
|
1379
1379
|
});
|
|
@@ -1531,7 +1531,7 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
|
|
|
1531
1531
|
failed: false,
|
|
1532
1532
|
passed: false,
|
|
1533
1533
|
}));
|
|
1534
|
-
const
|
|
1534
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1535
1535
|
const buildIntervals = [];
|
|
1536
1536
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
1537
1537
|
const file = files[fileIndex];
|
|
@@ -1554,14 +1554,14 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
|
|
|
1554
1554
|
});
|
|
1555
1555
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1556
1556
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1557
|
-
const artifactKey =
|
|
1557
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
1558
1558
|
result = await run(runFlags, configPath, [file], false, {
|
|
1559
1559
|
reporter: silentReporter,
|
|
1560
1560
|
reporterKind: "default",
|
|
1561
1561
|
emitRunStart: false,
|
|
1562
1562
|
emitRunComplete: false,
|
|
1563
1563
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1564
|
-
coverageFileName:
|
|
1564
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
1565
1565
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1566
1566
|
modeName,
|
|
1567
1567
|
});
|
|
@@ -1740,7 +1740,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
|
|
|
1740
1740
|
const silentReporter = {};
|
|
1741
1741
|
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1742
1742
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1743
|
-
const
|
|
1743
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1744
1744
|
const ordered = new Array(files.length);
|
|
1745
1745
|
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1746
1746
|
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
@@ -1769,7 +1769,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
|
|
|
1769
1769
|
});
|
|
1770
1770
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1771
1771
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
1772
|
-
const artifactKey =
|
|
1772
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
1773
1773
|
result = await run(runFlags, configPath, [file], false, {
|
|
1774
1774
|
reporter: silentReporter,
|
|
1775
1775
|
reporterKind: "default",
|
|
@@ -1777,7 +1777,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
|
|
|
1777
1777
|
emitRunStart: false,
|
|
1778
1778
|
emitRunComplete: false,
|
|
1779
1779
|
logFileName: `run.${artifactKey}.log.json`,
|
|
1780
|
-
coverageFileName:
|
|
1780
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
1781
1781
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1782
1782
|
modeName,
|
|
1783
1783
|
});
|
|
@@ -1853,7 +1853,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1853
1853
|
snapshotEnabled,
|
|
1854
1854
|
createSnapshots: runFlags.createSnapshots,
|
|
1855
1855
|
});
|
|
1856
|
-
const
|
|
1856
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1857
1857
|
const results = new Array(files.length);
|
|
1858
1858
|
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1859
1859
|
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
@@ -1881,14 +1881,14 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1881
1881
|
});
|
|
1882
1882
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1883
1883
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1884
|
-
const artifactKey =
|
|
1884
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
1885
1885
|
result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1886
1886
|
reporter: buffered?.reporter,
|
|
1887
1887
|
reporterKind: buffered?.reporterKind,
|
|
1888
1888
|
suiteSelectors,
|
|
1889
1889
|
emitRunComplete: false,
|
|
1890
1890
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1891
|
-
coverageFileName:
|
|
1891
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
1892
1892
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1893
1893
|
modeName,
|
|
1894
1894
|
});
|
|
@@ -1967,7 +1967,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1967
1967
|
const silentReporter = {};
|
|
1968
1968
|
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1969
1969
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1970
|
-
const
|
|
1970
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
1971
1971
|
const ordered = new Array(files.length);
|
|
1972
1972
|
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1973
1973
|
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
@@ -1996,7 +1996,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1996
1996
|
});
|
|
1997
1997
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1998
1998
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1999
|
-
const artifactKey =
|
|
1999
|
+
const artifactKey = resolveArtifactStem(file, inputPatterns);
|
|
2000
2000
|
result = await run(runFlags, configPath, [file], false, {
|
|
2001
2001
|
reporter: silentReporter,
|
|
2002
2002
|
reporterKind: "default",
|
|
@@ -2004,7 +2004,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
2004
2004
|
emitRunStart: false,
|
|
2005
2005
|
emitRunComplete: false,
|
|
2006
2006
|
logFileName: `test.${artifactKey}.log.json`,
|
|
2007
|
-
coverageFileName:
|
|
2007
|
+
coverageFileName: `${artifactKey}.log.json`,
|
|
2008
2008
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
2009
2009
|
modeName,
|
|
2010
2010
|
});
|
|
@@ -2707,57 +2707,19 @@ function isBareSuiteSelector(selector) {
|
|
|
2707
2707
|
function stripSuiteSuffix(selector) {
|
|
2708
2708
|
return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
|
|
2709
2709
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
function resolvePerFileArtifactKey(file, duplicateSpecBasenames) {
|
|
2724
|
-
const base = path.basename(file);
|
|
2725
|
-
let raw = base;
|
|
2726
|
-
if (duplicateSpecBasenames.has(base)) {
|
|
2727
|
-
const disambiguator = resolvePerFileDisambiguator(file);
|
|
2728
|
-
if (disambiguator.length) {
|
|
2729
|
-
raw = `${base}.${disambiguator}`;
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2733
|
-
}
|
|
2734
|
-
function resolvePerFileDisambiguator(file) {
|
|
2735
|
-
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
2736
|
-
if (!relDir.length || relDir == ".")
|
|
2737
|
-
return "";
|
|
2738
|
-
return relDir
|
|
2739
|
-
.replace(/[\\/]+/g, "__")
|
|
2740
|
-
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
2741
|
-
.replace(/^_+|_+$/g, "");
|
|
2742
|
-
}
|
|
2743
|
-
function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpecBasenames) {
|
|
2744
|
-
const base = path
|
|
2745
|
-
.basename(file)
|
|
2746
|
-
.replace(/\.spec\.ts$/, "")
|
|
2747
|
-
.replace(/\.ts$/, "");
|
|
2748
|
-
const legacy = !modeName
|
|
2749
|
-
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
2750
|
-
: `${base}.${modeName}.${target}.wasm`;
|
|
2751
|
-
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
2752
|
-
return legacy;
|
|
2753
|
-
}
|
|
2754
|
-
const disambiguator = resolvePerFileDisambiguator(file);
|
|
2755
|
-
if (!disambiguator.length) {
|
|
2756
|
-
return legacy;
|
|
2757
|
-
}
|
|
2758
|
-
const ext = path.extname(legacy);
|
|
2759
|
-
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
2760
|
-
return `${stem}.${disambiguator}${ext}`;
|
|
2710
|
+
// Returns the spec relative path (under the configured input base) with the
|
|
2711
|
+
// trailing ".ts" stripped, suitable for use as a stable per-file key for
|
|
2712
|
+
// coverage and log filenames.
|
|
2713
|
+
function resolveArtifactStem(file, inputPatterns) {
|
|
2714
|
+
return resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
|
|
2715
|
+
}
|
|
2716
|
+
async function loadInputPatterns(configPath) {
|
|
2717
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2718
|
+
return loadConfig(resolvedConfigPath, false).input;
|
|
2719
|
+
}
|
|
2720
|
+
async function loadFuzzInputPatterns(configPath) {
|
|
2721
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2722
|
+
return loadConfig(resolvedConfigPath, false).fuzz.input;
|
|
2761
2723
|
}
|
|
2762
2724
|
async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
|
|
2763
2725
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
@@ -3235,8 +3197,8 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
3235
3197
|
? `No fuzz files matched: ${scope}`
|
|
3236
3198
|
: `No test files matched: ${scope}`);
|
|
3237
3199
|
}
|
|
3238
|
-
const
|
|
3239
|
-
const
|
|
3200
|
+
const inputPatterns = await loadInputPatterns(configPath);
|
|
3201
|
+
const fuzzInputPatterns = await loadFuzzInputPatterns(configPath);
|
|
3240
3202
|
if (specFiles.length) {
|
|
3241
3203
|
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
3242
3204
|
for (const file of specFiles) {
|
|
@@ -3284,21 +3246,21 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
3284
3246
|
if (specFiles.length) {
|
|
3285
3247
|
process.stdout.write(" artifacts:\n");
|
|
3286
3248
|
for (const file of specFiles) {
|
|
3287
|
-
const artifactName =
|
|
3249
|
+
const artifactName = resolveArtifactPath(file, inputPatterns);
|
|
3288
3250
|
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
3289
3251
|
}
|
|
3290
3252
|
}
|
|
3291
3253
|
if (fuzzFiles.length && command == "test") {
|
|
3292
3254
|
process.stdout.write(" fuzz artifacts:\n");
|
|
3293
3255
|
for (const file of fuzzFiles) {
|
|
3294
|
-
const artifactName =
|
|
3256
|
+
const artifactName = resolveArtifactPath(file, fuzzInputPatterns);
|
|
3295
3257
|
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
3296
3258
|
}
|
|
3297
3259
|
}
|
|
3298
3260
|
else if (command == "fuzz") {
|
|
3299
3261
|
process.stdout.write(" artifacts:\n");
|
|
3300
3262
|
for (const file of fuzzFiles) {
|
|
3301
|
-
const artifactName =
|
|
3263
|
+
const artifactName = resolveArtifactPath(file, fuzzInputPatterns);
|
|
3302
3264
|
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
3303
3265
|
}
|
|
3304
3266
|
}
|
package/bin/util.js
CHANGED
|
@@ -1398,3 +1398,83 @@ export function resolveProjectModule(specifier) {
|
|
|
1398
1398
|
}
|
|
1399
1399
|
return null;
|
|
1400
1400
|
}
|
|
1401
|
+
// picomatch-compatible glob metacharacters; first occurrence marks the start
|
|
1402
|
+
// of the dynamic part of a pattern and everything before it is the static base.
|
|
1403
|
+
const GLOB_META_RE = /[*?[\](){}!|+@]/;
|
|
1404
|
+
// Longest non-glob prefix of a single pattern, returned with native separators.
|
|
1405
|
+
// Examples:
|
|
1406
|
+
// "assembly/__tests__/**/*.spec.ts" -> "assembly/__tests__"
|
|
1407
|
+
// "**/*.spec.ts" -> ""
|
|
1408
|
+
// "assembly/foo.spec.ts" -> "assembly" (no glob → use dirname)
|
|
1409
|
+
// "/abs/path/**/*.ts" -> "/abs/path"
|
|
1410
|
+
export function resolveGlobBase(pattern) {
|
|
1411
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
1412
|
+
const metaIdx = normalized.search(GLOB_META_RE);
|
|
1413
|
+
let base;
|
|
1414
|
+
if (metaIdx < 0) {
|
|
1415
|
+
const dir = dirname(normalized);
|
|
1416
|
+
base = dir == "." ? "" : dir;
|
|
1417
|
+
}
|
|
1418
|
+
else {
|
|
1419
|
+
const slice = normalized.slice(0, metaIdx);
|
|
1420
|
+
const lastSlash = slice.lastIndexOf("/");
|
|
1421
|
+
base = lastSlash < 0 ? "" : slice.slice(0, lastSlash);
|
|
1422
|
+
}
|
|
1423
|
+
if (!base.length)
|
|
1424
|
+
return "";
|
|
1425
|
+
return base.split("/").join(sep);
|
|
1426
|
+
}
|
|
1427
|
+
// Strip the most-specific matching configured input base off `file`, returning
|
|
1428
|
+
// the path relative to that base (with native separators). If no base matches,
|
|
1429
|
+
// returns the basename of the file. Comparison is component-wise — so
|
|
1430
|
+
// "assembly/__tests" is not a prefix of "assembly/__tests__/foo.spec.ts".
|
|
1431
|
+
export function resolveSpecRelativePath(file, inputPatterns) {
|
|
1432
|
+
const patterns = Array.isArray(inputPatterns)
|
|
1433
|
+
? inputPatterns
|
|
1434
|
+
: [inputPatterns];
|
|
1435
|
+
const absFile = resolve(process.cwd(), file);
|
|
1436
|
+
const fileComponents = toComponents(absFile);
|
|
1437
|
+
let bestBaseAbs = null;
|
|
1438
|
+
let bestLength = -1;
|
|
1439
|
+
for (const pattern of patterns) {
|
|
1440
|
+
const base = resolveGlobBase(pattern);
|
|
1441
|
+
const absBase = base.length
|
|
1442
|
+
? resolve(process.cwd(), base)
|
|
1443
|
+
: resolve(process.cwd());
|
|
1444
|
+
const baseComponents = toComponents(absBase);
|
|
1445
|
+
if (!isComponentPrefix(baseComponents, fileComponents))
|
|
1446
|
+
continue;
|
|
1447
|
+
if (baseComponents.length > bestLength) {
|
|
1448
|
+
bestBaseAbs = absBase;
|
|
1449
|
+
bestLength = baseComponents.length;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (bestBaseAbs == null)
|
|
1453
|
+
return basename(file);
|
|
1454
|
+
const rel = relative(bestBaseAbs, absFile);
|
|
1455
|
+
return rel.length ? rel : basename(file);
|
|
1456
|
+
}
|
|
1457
|
+
// Compute the artifact path (relative to outDir) for a given spec/fuzz source
|
|
1458
|
+
// file. Strips ".ts" only, keeping ".spec" / ".fuzz" suffixes so spec and
|
|
1459
|
+
// fuzz artifacts can coexist in the same outDir.
|
|
1460
|
+
// assembly/__tests__/array.spec.ts -> "array.spec.wasm"
|
|
1461
|
+
// assembly/__tests__/nested/array.spec.ts -> "nested/array.spec.wasm"
|
|
1462
|
+
// assembly/__fuzz__/nested/array.fuzz.ts -> "nested/array.fuzz.wasm"
|
|
1463
|
+
export function resolveArtifactPath(file, inputPatterns) {
|
|
1464
|
+
const rel = resolveSpecRelativePath(file, inputPatterns);
|
|
1465
|
+
return rel.replace(/\.ts$/i, ".wasm");
|
|
1466
|
+
}
|
|
1467
|
+
function toComponents(absPath) {
|
|
1468
|
+
return absPath
|
|
1469
|
+
.split(/[\\/]+/)
|
|
1470
|
+
.filter((segment, idx) => segment.length || idx == 0);
|
|
1471
|
+
}
|
|
1472
|
+
function isComponentPrefix(prefix, full) {
|
|
1473
|
+
if (prefix.length > full.length)
|
|
1474
|
+
return false;
|
|
1475
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
1476
|
+
if (prefix[i] !== full[i])
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
package/package.json
CHANGED
|
@@ -298,10 +298,8 @@ export class CoverageTransform extends Visitor {
|
|
|
298
298
|
node.body.statements.unshift(coverStmt);
|
|
299
299
|
}
|
|
300
300
|
else if (node.body instanceof ExpressionStatement) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
const bodyBlock = node.body;
|
|
304
|
-
bodyBlock.statements.unshift(coverStmt);
|
|
301
|
+
const exprBody = node.body;
|
|
302
|
+
exprBody.expression = createCoverExpression(point.hash, exprBody.expression, node);
|
|
305
303
|
}
|
|
306
304
|
this.withScope(point, () => {
|
|
307
305
|
this.visit(node.name, node);
|
|
@@ -445,6 +443,8 @@ export class CoverageTransform extends Visitor {
|
|
|
445
443
|
super.visitReturnStatement(node);
|
|
446
444
|
if (!node.value || isBuiltinCallExpression(node.value))
|
|
447
445
|
return;
|
|
446
|
+
if (node.value.kind === NodeKind.This)
|
|
447
|
+
return;
|
|
448
448
|
const path = node.range.source.normalizedPath;
|
|
449
449
|
const point = this.createPoint(path, node.value, "Return");
|
|
450
450
|
const replacer = new RangeTransform(node);
|