as-test 1.1.4 → 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.
@@ -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,27 +16,29 @@ 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, duplicateSpecBasenames = new Set()) {
19
+ constructor(specFile, snapshotDir, inputPatterns) {
20
20
  this.dirty = false;
21
21
  this.created = 0;
22
22
  this.updated = 0;
23
23
  this.matched = 0;
24
24
  this.failed = 0;
25
25
  this.warnedMissing = new Set();
26
+ this.specBasename = path.basename(specFile);
26
27
  const dir = path.join(process.cwd(), snapshotDir);
27
- const relative = resolveArtifactRelativePath(specFile, "__tests__").replace(/\.ts$/, ".snap");
28
+ const relative = resolveSpecRelativePath(specFile, inputPatterns).replace(/\.ts$/i, ".snap");
28
29
  this.filePath = path.join(dir, relative);
29
- const sourcePath = resolveSnapshotSourcePath(specFile, dir, duplicateSpecBasenames, this.filePath) ?? null;
30
+ const sourcePath = existsSync(this.filePath) ? this.filePath : null;
30
31
  const loaded = sourcePath
31
32
  ? readSnapshotFile(sourcePath, specFile)
32
33
  : { data: {}, normalized: false, preamble: "" };
33
34
  this.data = loaded.data;
34
35
  this.preamble = loaded.preamble;
35
- this.existed = Boolean(sourcePath && existsSync(sourcePath));
36
- this.dirty = Boolean((sourcePath && sourcePath != this.filePath) || loaded.normalized);
36
+ this.existed = Boolean(sourcePath);
37
+ this.dirty = Boolean(loaded.normalized);
37
38
  }
38
39
  assert(key, actual, allowSnapshot, createSnapshots, overwriteSnapshots) {
39
40
  key = canonicalizeSnapshotKey(key);
41
+ key = normalizeSnapshotKeyPrefix(key, this.specBasename);
40
42
  if (!allowSnapshot)
41
43
  return { ok: true, expected: actual, warnMissing: false };
42
44
  if (!(key in this.data)) {
@@ -79,21 +81,6 @@ class SnapshotStore {
79
81
  writeFileSync(this.filePath, formatSnapshotFile(this.data, this.filePath, this.existed ? this.preamble : defaultSnapshotPreamble()));
80
82
  }
81
83
  }
82
- function resolveSnapshotSourcePath(specFile, snapshotDir, duplicateSpecBasenames, preferredPath) {
83
- if (existsSync(preferredPath))
84
- return preferredPath;
85
- const base = path.basename(specFile, ".ts");
86
- const legacyFlat = path.join(snapshotDir, `${base}.snap.json`);
87
- if (existsSync(legacyFlat))
88
- return legacyFlat;
89
- const disambiguator = resolveDisambiguator(specFile, duplicateSpecBasenames);
90
- if (disambiguator.length) {
91
- const legacyDisambiguated = path.join(snapshotDir, `${base}.${disambiguator}.snap.json`);
92
- if (existsSync(legacyDisambiguated))
93
- return legacyDisambiguated;
94
- }
95
- return null;
96
- }
97
84
  function readSnapshotFile(filePath, specFile) {
98
85
  const raw = readFileSync(filePath, "utf8");
99
86
  if (filePath.endsWith(".json")) {
@@ -245,21 +232,22 @@ function trimSnapshotPreamble(lines) {
245
232
  end--;
246
233
  return lines.slice(0, end).join("\n");
247
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.
248
238
  function resolveSnapshotSpecFile(filePath) {
249
- const normalized = filePath.replace(/\\/g, "/");
250
- const marker = "/snapshots/";
251
- const markerIndex = normalized.lastIndexOf(marker);
252
- const suffix = markerIndex >= 0
253
- ? normalized.slice(markerIndex + marker.length)
254
- : path.basename(normalized);
255
- const withoutMode = suffix.replace(/^default\//, "");
256
- const relative = withoutMode.replace(/\.snap$/, ".ts");
257
- return `assembly/__tests__/${relative}`;
239
+ return path.basename(filePath).replace(/\.snap$/, ".ts");
258
240
  }
259
241
  function localizeSnapshotKey(specFile, key) {
260
242
  const prefix = `${path.basename(specFile)}::`;
261
243
  return key.startsWith(prefix) ? key.slice(prefix.length) : key;
262
244
  }
245
+ function normalizeSnapshotKeyPrefix(key, specBasename) {
246
+ const sep = key.indexOf("::");
247
+ if (sep < 0)
248
+ return key;
249
+ return `${specBasename}::${key.slice(sep + 2)}`;
250
+ }
263
251
  function qualifySnapshotKey(specFile, key) {
264
252
  return `${path.basename(specFile)}::${key}`;
265
253
  }
@@ -289,16 +277,8 @@ function canonicalizeSnapshotLocalKey(localKey) {
289
277
  }
290
278
  return localKey;
291
279
  }
292
- function resolveArtifactRelativePath(sourceFile, segment) {
293
- const normalized = sourceFile.replace(/\\/g, "/");
294
- const marker = `/${segment}/`;
295
- const index = normalized.lastIndexOf(marker);
296
- if (index >= 0)
297
- return normalized.slice(index + marker.length);
298
- return path.basename(normalized);
299
- }
300
- function writeReadableLog(logRoot, file, suites, modeName, buildCommand, runCommand, snapshotSummary) {
301
- 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");
302
282
  const filePath = path.join(logRoot, relative);
303
283
  const dir = path.dirname(filePath);
304
284
  if (!existsSync(dir))
@@ -554,7 +534,6 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
554
534
  const config = mode.config;
555
535
  const inputPatterns = resolveInputPatterns(config.input, selectors);
556
536
  const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
557
- const duplicateSpecBasenames = await resolveDuplicateSpecBasenames(config.input);
558
537
  const snapshotEnabled = flags.snapshot !== false;
559
538
  const createSnapshots = Boolean(flags.createSnapshots);
560
539
  const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
@@ -615,7 +594,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
615
594
  try {
616
595
  for (let i = 0; i < inputFiles.length; i++) {
617
596
  const file = inputFiles[i];
618
- const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName, duplicateSpecBasenames));
597
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, config.input));
619
598
  if (!existsSync(outFile)) {
620
599
  const buildStartedAt = Date.now();
621
600
  await build(resolvedConfigPath, [file], options.modeName, { coverage: flags.coverage }, {}, loadedConfig);
@@ -634,7 +613,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
634
613
  .map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
635
614
  };
636
615
  const runCommandForLog = formatInvocation(invocation);
637
- const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
616
+ const snapshotStore = new SnapshotStore(file, config.snapshotDir, config.input);
638
617
  let report;
639
618
  try {
640
619
  const runtimeEnv = {
@@ -647,9 +626,10 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
647
626
  ? { BROWSER: config.runOptions.runtime.browser.trim() }
648
627
  : {}),
649
628
  };
629
+ const crashEntryKey = resolveSpecRelativePath(file, config.input).replace(/\.ts$/i, "");
650
630
  report = webSession
651
- ? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
652
- : 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);
653
633
  }
654
634
  catch (error) {
655
635
  const modeLabel = options.modeName ?? "default";
@@ -690,7 +670,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
690
670
  mkdirSync(logRoot, { recursive: true });
691
671
  }
692
672
  for (const report of reports) {
693
- 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] ??
694
674
  options.buildCommand ??
695
675
  "", report.runCommand, report.snapshotSummary);
696
676
  }
@@ -705,10 +685,9 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
705
685
  coverageDir != "none" &&
706
686
  coverageSummary.files.length > 0) {
707
687
  const resolvedCoverageDir = path.join(process.cwd(), coverageDir);
708
- if (!existsSync(resolvedCoverageDir)) {
709
- mkdirSync(resolvedCoverageDir, { recursive: true });
710
- }
711
- 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));
712
691
  }
713
692
  if (options.emitRunComplete !== false) {
714
693
  const totalModes = Math.max(options.modeSummaryTotal ?? 1, 1);
@@ -929,51 +908,6 @@ instantiate(imports)
929
908
  }
930
909
  return null;
931
910
  }
932
- function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
933
- const base = path
934
- .basename(file)
935
- .replace(/\.spec\.ts$/, "")
936
- .replace(/\.ts$/, "");
937
- const legacy = !modeName
938
- ? `${path.basename(file).replace(".ts", ".wasm")}`
939
- : `${base}.${modeName}.${target}.wasm`;
940
- if (!duplicateSpecBasenames.has(path.basename(file))) {
941
- return legacy;
942
- }
943
- const disambiguator = resolveDisambiguator(file, duplicateSpecBasenames);
944
- if (!disambiguator.length) {
945
- return legacy;
946
- }
947
- const ext = path.extname(legacy);
948
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
949
- return `${stem}.${disambiguator}${ext}`;
950
- }
951
- async function resolveDuplicateSpecBasenames(configured) {
952
- const patterns = Array.isArray(configured) ? configured : [configured];
953
- const files = await glob(patterns);
954
- const counts = new Map();
955
- for (const file of files) {
956
- const base = path.basename(file);
957
- counts.set(base, (counts.get(base) ?? 0) + 1);
958
- }
959
- const duplicates = new Set();
960
- for (const [base, count] of counts) {
961
- if (count > 1)
962
- duplicates.add(base);
963
- }
964
- return duplicates;
965
- }
966
- function resolveDisambiguator(file, duplicateSpecBasenames) {
967
- if (!duplicateSpecBasenames.has(path.basename(file)))
968
- return "";
969
- const relDir = path.dirname(path.relative(process.cwd(), file));
970
- if (!relDir.length || relDir == ".")
971
- return "";
972
- return relDir
973
- .replace(/[\\/]+/g, "__")
974
- .replace(/[^A-Za-z0-9._-]/g, "_")
975
- .replace(/^_+|_+$/g, "");
976
- }
977
911
  function resolveRuntimeTargetEnv(target, wasmPath) {
978
912
  if (target == "bindings") {
979
913
  return resolveBindingsRuntimeEnv(wasmPath);
@@ -1382,9 +1316,52 @@ function isAllowedCoverageSourceFile(file) {
1382
1316
  const lower = file.toLowerCase();
1383
1317
  return lower.endsWith(".ts") || lower.endsWith(".as");
1384
1318
  }
1319
+ // AssemblyScript normalizes node_modules/<pkg>/... to ~lib/<pkg>/... in Source.normalizedPath.
1320
+ // This set contains the root names that are actual AS stdlib modules, so we can distinguish
1321
+ // real stdlib (~lib/array.ts) from third-party packages (~lib/json-as/assembly/index.ts).
1322
+ const AS_STDLIB_ROOT_NAMES = new Set([
1323
+ "array",
1324
+ "arraybuffer",
1325
+ "atomics",
1326
+ "bindings",
1327
+ "builtins",
1328
+ "compat",
1329
+ "console",
1330
+ "crypto",
1331
+ "dataview",
1332
+ "date",
1333
+ "diagnostics",
1334
+ "error",
1335
+ "function",
1336
+ "iterator",
1337
+ "map",
1338
+ "math",
1339
+ "memory",
1340
+ "number",
1341
+ "object",
1342
+ "polyfills",
1343
+ "process",
1344
+ "reference",
1345
+ "regexp",
1346
+ "rt",
1347
+ "set",
1348
+ "shared",
1349
+ "staticarray",
1350
+ "string",
1351
+ "symbol",
1352
+ "table",
1353
+ "typedarray",
1354
+ "uri",
1355
+ "util",
1356
+ "vector",
1357
+ ]);
1385
1358
  function isAssemblyScriptStdlibFile(file) {
1386
- if (file.startsWith("~lib/"))
1387
- return true;
1359
+ if (file.startsWith("~lib/")) {
1360
+ // Extract the first path segment after ~lib/ (strip any file extension)
1361
+ const after = file.slice("~lib/".length);
1362
+ const root = (after.split("/")[0] ?? "").replace(/\.[^.]+$/, "");
1363
+ return AS_STDLIB_ROOT_NAMES.has(root);
1364
+ }
1388
1365
  if (file.includes("/~lib/"))
1389
1366
  return true;
1390
1367
  if (file.startsWith("assemblyscript/std/"))
@@ -1402,6 +1379,21 @@ function classifyCoverageFile(file) {
1402
1379
  }
1403
1380
  function resolveCoverageDependencyPackage(file) {
1404
1381
  const normalized = file.replace(/\\/g, "/");
1382
+ // AssemblyScript normalizes node_modules/<pkg>/... to ~lib/<pkg>/... at compile time.
1383
+ // Handle that path format so coverage.mode and coverage.dependencies work at runtime.
1384
+ if (normalized.startsWith("~lib/")) {
1385
+ const after = normalized.slice("~lib/".length);
1386
+ const segments = after.split("/").filter(Boolean);
1387
+ if (!segments.length)
1388
+ return null;
1389
+ if (segments[0].startsWith("@")) {
1390
+ if (segments.length < 2)
1391
+ return null;
1392
+ return `${segments[0]}/${segments[1]}`;
1393
+ }
1394
+ // Strip file extension for bare module entries like ~lib/json-as.ts (unusual but safe)
1395
+ return segments[0].replace(/\.[^.]+$/, "") || null;
1396
+ }
1405
1397
  const marker = "/node_modules/";
1406
1398
  const prefixed = normalized.startsWith("node_modules/")
1407
1399
  ? `/${normalized}`
@@ -1536,7 +1528,7 @@ function compareCoveragePoints(a, b) {
1536
1528
  return a.type.localeCompare(b.type);
1537
1529
  return a.hash.localeCompare(b.hash);
1538
1530
  }
1539
- 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) {
1540
1532
  const child = spawn(invocation.command, invocation.args, {
1541
1533
  stdio: ["pipe", "pipe", "pipe"],
1542
1534
  shell: false,
@@ -1737,6 +1729,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1737
1729
  persistCrashRecord(crashDir, {
1738
1730
  kind: "test",
1739
1731
  file: specFile,
1732
+ entryKey: crashEntryKey,
1740
1733
  mode: modeName ?? "default",
1741
1734
  error: errorText,
1742
1735
  stdout: stdoutBuffer,
@@ -1779,6 +1772,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1779
1772
  persistCrashRecord(crashDir, {
1780
1773
  kind: "test",
1781
1774
  file: specFile,
1775
+ entryKey: crashEntryKey,
1782
1776
  mode: modeName ?? "default",
1783
1777
  error: fullError,
1784
1778
  stdout: stdoutBuffer,
@@ -1807,6 +1801,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1807
1801
  persistCrashRecord(crashDir, {
1808
1802
  kind: "test",
1809
1803
  file: specFile,
1804
+ entryKey: crashEntryKey,
1810
1805
  mode: modeName ?? "default",
1811
1806
  error: errorText || "runtime reported an unknown error",
1812
1807
  stdout: stdoutBuffer,
@@ -1824,6 +1819,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1824
1819
  persistCrashRecord(crashDir, {
1825
1820
  kind: "test",
1826
1821
  file: specFile,
1822
+ entryKey: crashEntryKey,
1827
1823
  mode: modeName ?? "default",
1828
1824
  error: fullError,
1829
1825
  stdout: stdoutBuffer,
@@ -1844,6 +1840,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1844
1840
  persistCrashRecord(crashDir, {
1845
1841
  kind: "test",
1846
1842
  file: specFile,
1843
+ entryKey: crashEntryKey,
1847
1844
  mode: modeName ?? "default",
1848
1845
  error: errorText || "runtime reported an unknown error",
1849
1846
  stdout: stdoutBuffer,
@@ -1855,7 +1852,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1855
1852
  }
1856
1853
  return report;
1857
1854
  }
1858
- 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) {
1859
1856
  const input = new PassThrough();
1860
1857
  const output = new PassThrough();
1861
1858
  let report = null;
@@ -2079,6 +2076,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
2079
2076
  persistCrashRecord(crashDir, {
2080
2077
  kind: "test",
2081
2078
  file: specFile,
2079
+ entryKey: crashEntryKey,
2082
2080
  mode: modeName ?? "default",
2083
2081
  error: fullError,
2084
2082
  stdout: stdoutBuffer,
@@ -2113,6 +2111,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
2113
2111
  persistCrashRecord(crashDir, {
2114
2112
  kind: "test",
2115
2113
  file: specFile,
2114
+ entryKey: crashEntryKey,
2116
2115
  mode: modeName ?? "default",
2117
2116
  error: fullError,
2118
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)}`,
@@ -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,