as-test 1.4.1 → 1.5.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/lib/src/index.ts CHANGED
@@ -247,7 +247,11 @@ async function instantiateRawInstance(
247
247
  if (typeof helper.instantiate != "function") {
248
248
  throw new Error("bindings helper missing instantiate export");
249
249
  }
250
- const mergedImports = mergeImports(withNodeIo({}), imports);
250
+ const mergedImports = stubMissingImports(
251
+ module,
252
+ mergeImports(withNodeIo({}), imports),
253
+ ["wasi_snapshot_preview1"],
254
+ );
251
255
  const instance = await captureHelperInstance(async () => {
252
256
  await helper.instantiate!(module, mergedImports);
253
257
  });
@@ -268,6 +272,10 @@ async function instantiateEsmInstance(
268
272
  "esm bindings do not support custom imports in as-test/lib; pass {} or switch to raw bindings",
269
273
  );
270
274
  }
275
+ // The esm helper auto-instantiates at import time and writes the WIPC report
276
+ // by calling the global process.stdout.write with an ArrayBuffer. Patch node
277
+ // IO before importing so that write (and stdin.read) accept the raw buffer.
278
+ patchNodeIo();
271
279
  const instance = await captureHelperInstance(async () => {
272
280
  await import(`${pathToFileURL(helperPath).href}?t=${Date.now()}`);
273
281
  });
@@ -347,6 +355,8 @@ async function instantiateWebInstance(
347
355
  let ready = false;
348
356
  let wsSocket: Duplex | null = null;
349
357
  let wsBuffer = Buffer.alloc(0);
358
+ let wsFragmentOpcode = 0;
359
+ let wsFragments: Buffer[] = [];
350
360
  let stdinBuffer = Buffer.alloc(0);
351
361
  let browserProcess: ChildProcess | null = null;
352
362
  let browserStderr = "";
@@ -488,15 +498,33 @@ async function instantiateWebInstance(
488
498
  payload = Buffer.from(payload);
489
499
  }
490
500
  wsBuffer = wsBuffer.subarray(offset + maskLength + length);
491
- if (opcode == 0x8) {
501
+ // Reassemble fragmented messages: a non-final data frame (0x1/0x2 with
502
+ // FIN=0) starts a sequence continued by 0x0 frames until FIN=1. Chromium
503
+ // fragments large (64KB) frames, so without this the wipc stream
504
+ // desyncs and the report payload is corrupted.
505
+ const fin = (first & 0x80) !== 0;
506
+ let effectiveOpcode = opcode;
507
+ if (opcode == 0x0) {
508
+ wsFragments.push(payload);
509
+ if (!fin) continue;
510
+ payload = Buffer.concat(wsFragments);
511
+ wsFragments = [];
512
+ effectiveOpcode = wsFragmentOpcode;
513
+ wsFragmentOpcode = 0;
514
+ } else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
515
+ wsFragmentOpcode = opcode;
516
+ wsFragments = [payload];
517
+ continue;
518
+ }
519
+ if (effectiveOpcode == 0x8) {
492
520
  finish(0);
493
521
  return;
494
522
  }
495
- if (opcode == 0x1) {
523
+ if (effectiveOpcode == 0x1) {
496
524
  onControl(payload.toString("utf8"));
497
525
  continue;
498
526
  }
499
- if (opcode == 0x2) {
527
+ if (effectiveOpcode == 0x2) {
500
528
  process.stdout.write(payload);
501
529
  }
502
530
  }
@@ -703,20 +731,33 @@ function createWasmImports(
703
731
  module: WebAssembly.Module,
704
732
  imports: AnyImports,
705
733
  ): AnyImports {
706
- const mergedImports = mergeImports(withNodeIo({}), imports);
707
- if (!mergedImports.env || typeof mergedImports.env != "object") {
708
- mergedImports.env = {};
709
- }
734
+ return stubMissingImports(module, mergeImports(withNodeIo({}), imports));
735
+ }
736
+
737
+ // Fills any function import the module declares but the caller didn't provide
738
+ // with a no-op stub, so instantiation never fails on a missing import. This
739
+ // covers imports an unmocked `mockImport` target reintroduces (which keep
740
+ // their real binding but have no host implementation in tests). `skipModules`
741
+ // leaves bindings-helper-owned modules (env / wasi) for the helper to fill.
742
+ function stubMissingImports(
743
+ module: WebAssembly.Module,
744
+ imports: AnyImports,
745
+ skipModules: readonly string[] = [],
746
+ ): AnyImports {
747
+ const skip = new Set(skipModules);
748
+ const bag = imports as Record<string, Record<string, unknown>>;
710
749
  for (const entry of WebAssembly.Module.imports(module)) {
711
- if (
712
- entry.module == "env" &&
713
- entry.kind == "function" &&
714
- !(entry.name in mergedImports.env)
715
- ) {
716
- mergedImports.env[entry.name] = () => 0;
750
+ if (entry.kind != "function" || skip.has(entry.module)) continue;
751
+ let mod = bag[entry.module];
752
+ if (!mod || typeof mod != "object") {
753
+ mod = {};
754
+ bag[entry.module] = mod;
755
+ }
756
+ if (!(entry.name in mod)) {
757
+ mod[entry.name] = () => 0;
717
758
  }
718
759
  }
719
- return mergedImports;
760
+ return imports;
720
761
  }
721
762
 
722
763
  let patchedWasiWarning = false;
@@ -1306,6 +1347,16 @@ async function captureHelperInstance(
1306
1347
  source: BufferSource | WebAssembly.Module,
1307
1348
  importObject?: WebAssembly.Imports,
1308
1349
  ) => {
1350
+ // Stub any import the bindings helper left unprovided (e.g. an unmocked
1351
+ // mockImport target with no host implementation), so esm/raw bindings
1352
+ // instantiate instead of failing with a LinkError. env/wasi stay with the
1353
+ // helper.
1354
+ if (source instanceof WebAssembly.Module && importObject) {
1355
+ stubMissingImports(source, importObject as AnyImports, [
1356
+ "env",
1357
+ "wasi_snapshot_preview1",
1358
+ ]);
1359
+ }
1309
1360
  const result = await originalInstantiate(source, importObject);
1310
1361
  if (result instanceof WebAssembly.Instance) {
1311
1362
  instance = result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -74,30 +74,31 @@
74
74
  "access": "public"
75
75
  },
76
76
  "scripts": {
77
- "test": "npm run test:as && npm run test:integration",
78
- "test:as": "node ./bin/index.js test --parallel --enable try-as",
79
- "test:integration": "npm run build:cli && npm run build:lib && node --test tests/*.test.mjs",
80
- "test:ci": "node ./bin/index.js test --parallel --tap --enable try-as --config ./as-test.ci.config.json",
77
+ "build": "npm run build:cli && npm run build:lib && npm run build:transform",
78
+ "build:cli": "rm -rf ./bin/ && tsc -p cli && prettier -w ./cli/ && chmod +x ./bin/index.js",
79
+ "build:lib": "tsc -p ./tsconfig.lib.json",
80
+ "build:transform": "rm -rf ./transform/lib && tsc -p ./transform && prettier -w ./transform/",
81
+ "test": "node ./bin/index.js test --parallel",
82
+ "test:integration": "node --test tests/*.test.mjs",
83
+ "test:modes": "npm run test -- --mode node:bindings,node:bindings:raw,node:bindings:esm,node:wasi,wasmtime,wasmer,wazero,firefox,chromium,webkit,firefox:headless,chromium:headless,webkit:headless",
84
+ "test:examples": "npm --prefix ./examples run test",
85
+ "test:all": "npm run build && npm run test:modes && npm run test:integration && npm run test:examples",
86
+ "test:ci": "npm run test -- --mode node:bindings,node:wasi,wasmtime",
81
87
  "fuzz": "node ./bin/index.js fuzz",
82
88
  "bench:seed": "node ./bin/index.js fuzz --config ./as-test.bench.config.json --clean",
83
89
  "bench:seed:compare": "bash ./tools/bench-seed-compare.sh 7",
84
- "test:examples": "npm --prefix ./examples run test",
85
- "ci:act": "bash ./tools/act.sh push",
86
- "ci:act:pr": "bash ./tools/act.sh pull_request",
87
- "ci:act:tests": "act push -W .github/workflows/as-test.yml",
88
- "ci:act:examples": "act push -W .github/workflows/examples.yml",
89
90
  "typecheck": "tsc -p cli --noEmit && tsc -p tsconfig.lib.json --noEmit && tsc -p transform --noEmit",
90
91
  "lint": "eslint transform/src/**/*.ts tools/**/*.js eslint.config.js",
91
- "build:lib": "tsc -p ./tsconfig.lib.json",
92
- "build:transform": "rm -rf ./transform/lib && tsc -p ./transform && prettier -w ./transform/",
93
- "build:cli": "rm -rf ./bin/ && tsc -p cli && prettier -w ./cli/ && chmod +x ./bin/index.js",
94
- "build:run": "npm run build:cli",
92
+ "format": "prettier -w .",
95
93
  "docs:dev": "vitepress dev docs",
96
94
  "docs:build": "vitepress build docs",
97
95
  "docs:preview": "vitepress preview docs",
98
- "format": "prettier -w .",
99
- "release:check": "npm run build:cli && npm run build:lib && npm run build:transform && npm run test && npm run test:examples && npm pack --dry-run --cache /tmp/as-test-npm-cache",
100
- "prepublishOnly": "npm run build:cli && npm run build:lib && npm run build:transform && npm run test && npm run format",
96
+ "release:check": "npm run build && npm run test && npm run test:examples && npm pack --dry-run --cache /tmp/as-test-npm-cache",
97
+ "prepublishOnly": "npm run build && npm run test && npm run format",
98
+ "ci:act": "bash ./tools/act.sh push",
99
+ "ci:act:pr": "bash ./tools/act.sh pull_request",
100
+ "ci:act:tests": "act push -W .github/workflows/as-test.yml",
101
+ "ci:act:examples": "act push -W .github/workflows/examples.yml",
101
102
  "commitmsg:verify": "bash ./scripts/commit-msg.sh",
102
103
  "precommit:verify": "bash ./scripts/pre-commit.sh",
103
104
  "prepare": "husky"
@@ -4,7 +4,7 @@ import { readFileSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { SimpleParser, isStdlib } from "./util.js";
6
6
  const EQUALS_METHOD = "__AS_TEST_EQUALS";
7
- const TOJSON_METHOD = "toJSON";
7
+ const GENERATED_TOJSON_METHOD = "__AS_TEST_TO_JSON";
8
8
  const REFLECT_LOCAL = "__AS_TEST_REFLECT_EQUALS_INTERNAL";
9
9
  const STRINGIFY_LOCAL = "__AS_TEST_STRINGIFY_INTERNAL";
10
10
  const ALREADY_INJECTED_EQUALS = new WeakSet();
@@ -165,7 +165,7 @@ export class EqualsTransform {
165
165
  injectToJSONMethod(klass) {
166
166
  if (ALREADY_INJECTED_TOJSON.has(klass))
167
167
  return false;
168
- if (declaresMethod(klass, TOJSON_METHOD))
168
+ if (declaresMethod(klass, GENERATED_TOJSON_METHOD))
169
169
  return false;
170
170
  if (hasAnyDecorator(klass, JSON_DECORATORS))
171
171
  return false;
@@ -189,7 +189,7 @@ export class EqualsTransform {
189
189
  const body = parts.length
190
190
  ? `return "{" + ${parts.join(" + ")} + "}";`
191
191
  : `return "{}";`;
192
- const code = `toJSON(): string { ${body} }`;
192
+ const code = `${GENERATED_TOJSON_METHOD}(): string { ${body} }`;
193
193
  try {
194
194
  const method = SimpleParser.parseClassMember(code, klass);
195
195
  klass.members.push(method);
@@ -37,6 +37,10 @@ export default class Transformer extends Transform {
37
37
  for (const target of mockedImportTargets) {
38
38
  mock.importMocked.add(target);
39
39
  }
40
+ const unmockedImportTargets = collectUnmockImportTargets(sources);
41
+ for (const target of unmockedImportTargets) {
42
+ mock.importUnmocked.add(target);
43
+ }
40
44
  for (const source of sources) {
41
45
  const sourceInfo = analyzeSourceText(source.text);
42
46
  const shouldInjectRunCall = source.sourceKind == 1 &&
@@ -110,8 +114,13 @@ function patchModeName(parser, modeName) {
110
114
  }
111
115
  }
112
116
  function collectMockImportTargets(sources) {
117
+ return collectImportTargets(sources, /\bmockImport\s*\(\s*["']([^"']+)["']/g);
118
+ }
119
+ function collectUnmockImportTargets(sources) {
120
+ return collectImportTargets(sources, /\bunmockImport\s*\(\s*["']([^"']+)["']/g);
121
+ }
122
+ function collectImportTargets(sources, pattern) {
113
123
  const out = new Set();
114
- const pattern = /\bmockImport\s*\(\s*["']([^"']+)["']/g;
115
124
  for (const source of sources) {
116
125
  const text = stripComments(source.text);
117
126
  for (const match of text.matchAll(pattern)) {
@@ -6,8 +6,11 @@ export class MockTransform extends Visitor {
6
6
  globalStatements = [];
7
7
  mocked = new Set();
8
8
  importMocked = new Set();
9
- visitCallExpression(node) {
10
- super.visitCallExpression(node);
9
+ importUnmocked = new Set();
10
+ pendingHoist = [];
11
+ pendingRemoval = [];
12
+ visitCallExpression(node, ref = null) {
13
+ super.visitCallExpression(node, ref);
11
14
  const name = normalizeName(expressionName(node.expression));
12
15
  if (this.mocked.has(name + "_mock")) {
13
16
  node.expression = Node.createIdentifierExpression(name + "_mock", node.expression.range);
@@ -25,6 +28,7 @@ export class MockTransform extends Visitor {
25
28
  if (!oldFn)
26
29
  return;
27
30
  this.mocked.delete(normalizeName(expressionName(oldFn)) + "_mock");
31
+ this.scheduleRemoval(node, ref);
28
32
  return;
29
33
  }
30
34
  if (name == "unmockImport") {
@@ -34,26 +38,21 @@ export class MockTransform extends Visitor {
34
38
  return;
35
39
  const oldValue = node.args[0];
36
40
  const callback = node.args[1];
37
- if (!oldValue || !callback)
41
+ if (!oldValue || !callback || !callback.declaration)
38
42
  return;
39
- const newName = normalizeName(expressionName(oldValue));
40
- const newFn = Node.createFunctionDeclaration(Node.createIdentifierExpression(newName + "_mock", callback.range), callback.declaration.decorators, 0, callback.declaration.typeParameters, callback.declaration.signature, callback.declaration.body, callback.declaration.arrowKind, callback.range);
41
- const currentSource = this.srcCurrent;
42
- if (!currentSource)
43
- return;
44
- const stmts = currentSource.statements;
45
- let index = -1;
46
- for (let i = 0; i < stmts.length; i++) {
47
- const stmt = stmts[i];
48
- if (stmt.range.start != node.range.start)
49
- continue;
50
- index = i;
51
- break;
52
- }
53
- if (index === -1)
43
+ const mockName = normalizeName(expressionName(oldValue)) + "_mock";
44
+ const newFn = Node.createFunctionDeclaration(Node.createIdentifierExpression(mockName, callback.range), callback.declaration.decorators, 0, callback.declaration.typeParameters, callback.declaration.signature, callback.declaration.body, callback.declaration.arrowKind, callback.range);
45
+ this.pendingHoist.push(newFn);
46
+ this.mocked.add(mockName);
47
+ this.scheduleRemoval(node, ref);
48
+ }
49
+ scheduleRemoval(node, ref) {
50
+ const container = asStatementContainer(ref);
51
+ if (!container)
54
52
  return;
55
- stmts.splice(index, 1, newFn);
56
- this.mocked.add(newFn.name.text);
53
+ const stmt = container.statements.find((s) => s.expression === node);
54
+ if (stmt)
55
+ this.pendingRemoval.push({ container, stmt });
57
56
  }
58
57
  visitFunctionDeclaration(node, isDefault) {
59
58
  if (this.mocked.has(node.name.text))
@@ -62,11 +61,23 @@ export class MockTransform extends Visitor {
62
61
  }
63
62
  visitSource(node) {
64
63
  this.mocked = new Set();
64
+ this.pendingHoist = [];
65
+ this.pendingRemoval = [];
65
66
  this.srcCurrent = node;
66
67
  super.visitSource(node);
67
68
  const currentSource = this.srcCurrent;
68
69
  if (!currentSource)
69
70
  return;
71
+ for (const { container, stmt } of this.pendingRemoval) {
72
+ const i = container.statements.indexOf(stmt);
73
+ if (i !== -1)
74
+ container.statements.splice(i, 1);
75
+ }
76
+ this.pendingRemoval = [];
77
+ if (this.pendingHoist.length) {
78
+ currentSource.statements.unshift(...this.pendingHoist);
79
+ this.pendingHoist = [];
80
+ }
70
81
  const stmts = currentSource.statements;
71
82
  for (let index = 0; index < stmts.length; index++) {
72
83
  const node = stmts[index];
@@ -98,6 +109,17 @@ export class MockTransform extends Visitor {
98
109
  index++;
99
110
  continue;
100
111
  }
112
+ if (this.importUnmocked.has(path) && node.signature.returnType) {
113
+ const realName = "__as_test_real_" + node.name.text;
114
+ const r = node.range;
115
+ const wrapper = buildFallbackWrapper(node, path, realName, r);
116
+ const mutable = node;
117
+ mutable.name = Node.createIdentifierExpression(realName, r);
118
+ mutable.flags &= ~2;
119
+ stmts.splice(index + 1, 0, wrapper);
120
+ index++;
121
+ continue;
122
+ }
101
123
  const args = [
102
124
  Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", node.range), Node.createIdentifierExpression("get", node.range), node.range), null, [Node.createStringLiteralExpression(path, node.range)], node.range),
103
125
  ];
@@ -120,6 +142,27 @@ function isBodylessTopLevelFunction(node) {
120
142
  "signature" in candidate &&
121
143
  candidate.body == null);
122
144
  }
145
+ function buildFallbackWrapper(node, path, realName, range) {
146
+ const mockImportGet = Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", range), Node.createIdentifierExpression("get", range), range), null, [Node.createStringLiteralExpression(path, range)], range);
147
+ const indirectArgs = [mockImportGet];
148
+ const forwardArgs = [];
149
+ for (const param of node.signature.parameters) {
150
+ indirectArgs.push(Node.createIdentifierExpression(param.name.text, range));
151
+ forwardArgs.push(Node.createIdentifierExpression(param.name.text, range));
152
+ }
153
+ const ifMocked = Node.createIfStatement(Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", range), Node.createIdentifierExpression("has", range), range), null, [Node.createStringLiteralExpression(path, range)], range), Node.createReturnStatement(Node.createCallExpression(Node.createIdentifierExpression("call_indirect", range), null, indirectArgs, range), range), null, range);
154
+ const callReal = Node.createReturnStatement(Node.createCallExpression(Node.createIdentifierExpression(realName, range), null, forwardArgs, range), range);
155
+ const signature = Node.createFunctionType(node.signature.parameters, node.signature.returnType, node.signature.explicitThisType, node.signature.isNullable, range);
156
+ const exported = (node.flags & 2) != 0;
157
+ return Node.createFunctionDeclaration(Node.createIdentifierExpression(node.name.text, range), null, exported ? 2 : 0, node.typeParameters, signature, Node.createBlockStatement([ifMocked, callReal], range), 0, range);
158
+ }
159
+ function asStatementContainer(ref) {
160
+ const candidate = ref;
161
+ if (candidate && Array.isArray(candidate.statements)) {
162
+ return candidate;
163
+ }
164
+ return null;
165
+ }
123
166
  function normalizeName(value) {
124
167
  return value
125
168
  .replaceAll(".", "_")