diff --git a/test-parity/node-suite/vm/README.md b/test-parity/node-suite/vm/README.md index d7717d6ff2..e24c43ee12 100644 --- a/test-parity/node-suite/vm/README.md +++ b/test-parity/node-suite/vm/README.md @@ -1,19 +1,89 @@ -# node:vm parity fixtures +# `node:vm` granular parity -This directory covers the default Node `node:vm` surface that is visible -without `--experimental-vm-modules`: import and require shapes, callable export -metadata, `vm.constants`, `process.getBuiltinModule("vm")`, `vm.isContext({})`, -and the narrowed deterministic execution subset for `Script`, context-backed -sandbox mutation/isolation, and `compileFunction`. +This directory tests Perry's documented `node:vm` surface with independent, +print-and-diff fixtures. Node 26.5.0 is the oracle. Every executable source +passed to `Script`, `runIn*Context`, or `compileFunction` is statically declared +in the fixture, so failures in the core groups are not hidden dependencies on +runtime source discovery. -The `modules/` fixtures opt into Node's `--experimental-vm-modules` flag and -Perry's matching `PERRY_EXPERIMENTAL_VM_MODULES=1` gate to cover deterministic -`SourceTextModule`/`SyntheticModule` lifecycle behavior separately from the -default surface. +## Coverage -Intentionally open leaves: +- `api/`, `imports/`, and `require/`: export identity, classes, constants, + callable metadata, property descriptors, ESM/CommonJS shape, and the builtin + module lookup path. +- `context/` and `validation/`: `createContext()`/`isContext()`, object and + array sandboxes, `DONT_CONTEXTIFY`, name/origin and code-generation + validation, string-code generation policy, and deterministic `microtaskMode` + draining. The `context/properties/` group covers accessor forwarding, + descriptors, writable/configurable behavior, deletion, inheritance, circular + references, symbol keys, and own-key enumeration. +- `execution/`: main-context bindings, isolated new contexts, persistent context + mutation/lexicals, receiver identity, repeated Script execution, and + `createScript()` construction. +- `script/` and `metadata/`: constructor/run option validation, stable error + class/code checks, filename/offset presence, `displayErrors`, source-map URL, + cached-data acceptance/rejection, and deprecated produced-cache properties. +- `compile-function/`: parameters, parsing contexts, context extensions, + arguments, receiver behavior, validation, and portable cache observations. +- `cross-context/`: built-in/prototype identity, structured values, descriptors, + errors, and promises without raw inspector or engine-specific stack output. +- `modules/`: the separately gated, documented `Module`, `SourceTextModule`, and + `SyntheticModule` lifecycle, validation, namespace, linking, evaluation, and + cached-data subset. These fixtures use `--experimental-vm-modules` and + `PERRY_EXPERIMENTAL_VM_MODULES=1` explicitly. -- Full VM module parsing/evaluation beyond deterministic lifecycle fixtures: - #3132, #3133 -- vm.constants deeper context-loader behavior: #3283 -- Exact V8-backed heap accounting for `measureMemory()` values +## Upstream selection evidence + +The selection was reviewed on 2026-07-16 against these primary repositories: + +- Node.js `v26.5.0` (`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`): + `test/parallel/test-vm-*`, `test/es-module/test-vm-*`, + `test/sequential/test-vm-*`, and `doc/api/vm.md`. +- Deno main (`c99e6904d8e297712ba859a64bbe848532d8f90f`): + `tests/unit_node/vm_test.ts`, `tests/specs/node/vm_*`, and + `ext/node/polyfills/vm.js`. +- Bun main (`0ecd508247c7e99477717389a6cad44552cac023`): `test/js/node/vm/`, its + vendored `test/js/node/test/parallel/test-vm-*` selection, and + `src/js/node/vm.ts`. + +The fixtures intentionally reduce those upstream assertions to stable semantic +output: booleans, values, property flags, error names, and error codes. They do +not compare absolute paths, raw error messages, full stacks, cache bytes, or +object inspection order. + +## AOT boundary and stopping evidence + +Perry documents a V8-free, narrowed constant-source VM evaluator. Consequently: + +- `code-generation-strings.ts` and `run-new-code-generation.ts` record + `eval`/`new Function` policy behavior, but Perry mismatches there are + deliberate runtime-generated-code AOT exclusions. They are kept separate from + literal-source context behavior. +- Literal-source mutation, isolation, receiver, lexical, option-validation, + error, compile-function, and cross-context failures are genuine gaps in the + currently claimed VM subset, not dynamic-source discovery failures. +- Dynamic import callbacks, `importModuleDynamically`, default/custom loaders, + network imports, and import-meta initialization require separate loader work. +- Inspector/debug breaks, signals, `breakOnSigint`, timeout races/infinite + loops, escaped promise timeouts, and parse-abort behavior are excluded because + they need process control or can hang the granular runner. +- Context `name` and `origin` type validation and acceptance are covered, but + their deeper metadata effects are inspector-facing and stay with inspector + integration work. +- `measureMemory()` exact values, GC/weak-reference/leak/stress tests, and code + cache byte identity are engine- or environment-specific. Only portable shapes + and acceptance state are covered. +- WebAssembly code-generation execution, code-cache corruption fuzzing, exact + syntax/stack strings, and source-map path formatting remain separate risky + categories. +- Proxy-backed sandboxes were evaluated from Node/Bun's selection. Node 26.5.0 + completes deterministically, but Perry currently exits with `SIGSEGV`; the + crashing probe is excluded from this print-and-diff suite and requires + separate runtime crash coverage. +- Node 26.5.0 aborts the process for the invalid `compileFunction()` parameter + name `"a-b"` on the tested macOS build, so that unsafe oracle case is + excluded; type validation around it remains covered. + +The remaining Node/Deno/Bun cases are therefore redundant with these semantic +contracts, outside Perry's documented AOT model, experimental loader work, or +belong to one of the unreliable categories above. diff --git a/test-parity/node-suite/vm/api/descriptors.ts b/test-parity/node-suite/vm/api/descriptors.ts new file mode 100644 index 0000000000..1c5d01060e --- /dev/null +++ b/test-parity/node-suite/vm/api/descriptors.ts @@ -0,0 +1,30 @@ +import * as vm from "node:vm"; + +function descriptor(target: object, key: string) { + let owner: any = target; + let depth = 0; + while (owner && !Object.prototype.hasOwnProperty.call(owner, key)) { + owner = Object.getPrototypeOf(owner); + depth++; + } + const value = owner && Object.getOwnPropertyDescriptor(owner, key); + console.log( + key + ":", + depth, + value?.enumerable, + value?.configurable, + value?.writable, + typeof value?.value, + ); +} + +descriptor(vm, "Script"); +descriptor(vm, "createContext"); +descriptor(vm, "constants"); +const script = new vm.Script(""); +descriptor(script, "runInContext"); +descriptor(script, "createCachedData"); +console.log( + "prototype constructor:", + vm.Script.prototype.constructor === vm.Script, +); diff --git a/test-parity/node-suite/vm/api/exports-and-constants.ts b/test-parity/node-suite/vm/api/exports-and-constants.ts new file mode 100644 index 0000000000..71eaa3e62d --- /dev/null +++ b/test-parity/node-suite/vm/api/exports-and-constants.ts @@ -0,0 +1,31 @@ +import vm, * as namespace from "node:vm"; + +const expected = [ + "Script", + "compileFunction", + "constants", + "createContext", + "createScript", + "isContext", + "measureMemory", + "runInContext", + "runInNewContext", + "runInThisContext", +]; + +console.log( + "exports:", + expected.map((key) => key + ":" + typeof (vm as any)[key]).join(","), +); +console.log("namespace default:", namespace.default === vm); +console.log( + "script class:", + typeof vm.Script, + vm.Script.name, + vm.Script.length, +); +console.log("constants frozen:", Object.isFrozen(vm.constants)); +console.log( + "constants null prototype:", + Object.getPrototypeOf(vm.constants) === null, +); diff --git a/test-parity/node-suite/vm/compile-function/arguments-and-receiver.ts b/test-parity/node-suite/vm/compile-function/arguments-and-receiver.ts new file mode 100644 index 0000000000..2943b4212e --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/arguments-and-receiver.ts @@ -0,0 +1,10 @@ +import * as vm from "node:vm"; + +const fn: any = vm.compileFunction( + "return arguments.length + ':' + a + ':' + b + ':' + this.marker", + ["a", "b"], +); + +console.log("length:", fn.length); +console.log("missing:", fn.call({ marker: "m" }, 1)); +console.log("extra:", fn.call({ marker: "e" }, 1, 2, 3)); diff --git a/test-parity/node-suite/vm/compile-function/context-extensions.ts b/test-parity/node-suite/vm/compile-function/context-extensions.ts new file mode 100644 index 0000000000..5bc4109472 --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/context-extensions.ts @@ -0,0 +1,13 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({ shared: 10 }); +const extension = { shared: 20, extra: 3 }; +const fn: any = vm.compileFunction("return shared + extra + arg", ["arg"], { + parsingContext: context, + contextExtensions: [extension], +}); + +console.log("result:", fn(4)); +extension.extra = 5; +console.log("live extension:", fn(4)); +console.log("length name:", fn.length, typeof fn.name, fn.name); diff --git a/test-parity/node-suite/vm/compile-function/extension-precedence.ts b/test-parity/node-suite/vm/compile-function/extension-precedence.ts new file mode 100644 index 0000000000..a201745c23 --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/extension-precedence.ts @@ -0,0 +1,15 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({ fallback: "context" }); +const first: any = { left: "left", shared: "first" }; +const second: any = { right: "right", shared: "second" }; +const fn: any = vm.compileFunction( + "return left + ':' + shared + ':' + right + ':' + fallback", + [], + { parsingContext: context, contextExtensions: [first, second] }, +); + +console.log("precedence:", fn()); +first.shared = "changed-first"; +second.shared = "changed-second"; +console.log("live precedence:", fn()); diff --git a/test-parity/node-suite/vm/compile-function/syntax-errors.ts b/test-parity/node-suite/vm/compile-function/syntax-errors.ts new file mode 100644 index 0000000000..1e558d015f --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/syntax-errors.ts @@ -0,0 +1,13 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("unexpected token", () => vm.compileFunction("return )")); +shape("unterminated body", () => vm.compileFunction("if (true) {")); diff --git a/test-parity/node-suite/vm/compile-function/to-string.ts b/test-parity/node-suite/vm/compile-function/to-string.ts new file mode 100644 index 0000000000..88d052275f --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/to-string.ts @@ -0,0 +1,8 @@ +import * as vm from "node:vm"; + +const fn: any = vm.compileFunction("return left + right", ["left", "right"]); +const source = Function.prototype.toString.call(fn); + +console.log("shape:", source.startsWith("function (left, right)")); +console.log("body:", source.includes("return left + right")); +console.log("metadata:", fn.name, fn.length, Object.hasOwn(fn, "prototype")); diff --git a/test-parity/node-suite/vm/compile-function/validation.ts b/test-parity/node-suite/vm/compile-function/validation.ts new file mode 100644 index 0000000000..2fd66f4f8d --- /dev/null +++ b/test-parity/node-suite/vm/compile-function/validation.ts @@ -0,0 +1,32 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("missing code", () => (vm.compileFunction as any)()); +shape("params null", () => vm.compileFunction("", null as any)); +shape("param type", () => vm.compileFunction("", [1 as any])); +shape("options null", () => vm.compileFunction("", [], null as any)); +shape("filename", () => vm.compileFunction("", [], { filename: null as any })); +shape( + "lineOffset", + () => vm.compileFunction("", [], { lineOffset: "1" as any }), +); +shape( + "columnOffset", + () => vm.compileFunction("", [], { columnOffset: null as any }), +); +shape( + "extensions null", + () => vm.compileFunction("", [], { contextExtensions: null as any }), +); +shape( + "extensions member", + () => vm.compileFunction("", [], { contextExtensions: [1 as any] }), +); diff --git a/test-parity/node-suite/vm/context/code-generation-strings.ts b/test-parity/node-suite/vm/context/code-generation-strings.ts new file mode 100644 index 0000000000..a703131054 --- /dev/null +++ b/test-parity/node-suite/vm/context/code-generation-strings.ts @@ -0,0 +1,21 @@ +import * as vm from "node:vm"; + +const disabled = vm.createContext({}, { + codeGeneration: { strings: false, wasm: true }, +}); +try { + console.log("eval:", vm.runInContext("eval('1')", disabled)); +} catch (error: any) { + console.log("eval:", error.name, error.code || "-"); +} +try { + console.log( + "Function:", + vm.runInContext("new Function('return 1')()", disabled), + ); +} catch (error: any) { + console.log("Function:", error.name, error.code || "-"); +} + +const enabled = vm.createContext({}, { codeGeneration: { strings: true } }); +console.log("enabled eval:", vm.runInContext("eval('2 + 3')", enabled)); diff --git a/test-parity/node-suite/vm/context/create-context-array.ts b/test-parity/node-suite/vm/context/create-context-array.ts new file mode 100644 index 0000000000..9d7c255e43 --- /dev/null +++ b/test-parity/node-suite/vm/context/create-context-array.ts @@ -0,0 +1,11 @@ +import * as vm from "node:vm"; + +const array: any[] = [1, 2]; +console.log("identity:", vm.createContext(array) === array); +console.log("marker:", vm.isContext(array)); +console.log("length:", array.length); +console.log( + "execution:", + vm.runInContext("length = length + 1; length", array), + array.length, +); diff --git a/test-parity/node-suite/vm/context/create-context-shapes.ts b/test-parity/node-suite/vm/context/create-context-shapes.ts new file mode 100644 index 0000000000..b0ec8db100 --- /dev/null +++ b/test-parity/node-suite/vm/context/create-context-shapes.ts @@ -0,0 +1,17 @@ +import * as vm from "node:vm"; + +const omitted: any = vm.createContext(); +const object: any = { value: 1 }; + +console.log("omitted:", typeof omitted, vm.isContext(omitted)); +console.log( + "object identity:", + vm.createContext(object) === object, + vm.isContext(object), +); +console.log( + "repeat identity:", + vm.createContext(object) === object, + vm.isContext(object), +); +console.log("plain markers:", vm.isContext({}), vm.isContext([])); diff --git a/test-parity/node-suite/vm/context/dont-contextify.ts b/test-parity/node-suite/vm/context/dont-contextify.ts new file mode 100644 index 0000000000..fcb2afc595 --- /dev/null +++ b/test-parity/node-suite/vm/context/dont-contextify.ts @@ -0,0 +1,15 @@ +import * as vm from "node:vm"; + +const context: any = vm.createContext(vm.constants.DONT_CONTEXTIFY); +console.log("marker:", vm.isContext(context)); +console.log("identity:", context === globalThis); +console.log("receiver exposed:", vm.runInContext("this", context) === context); +console.log( + "global identity:", + vm.runInContext("this === globalThis", context) === true, +); +console.log( + "mutation:", + vm.runInContext("value = 3; value", context), + context.value, +); diff --git a/test-parity/node-suite/vm/context/function-bindings.ts b/test-parity/node-suite/vm/context/function-bindings.ts new file mode 100644 index 0000000000..70c86a0394 --- /dev/null +++ b/test-parity/node-suite/vm/context/function-bindings.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; + +const sandbox: any = {}; +const context = vm.createContext(sandbox); +console.log( + "declare:", + vm.runInContext( + "function declared() { return 4; } var expression = function () { return 5; }; let lexical = () => 6; declared() + expression() + lexical()", + context, + ), +); +console.log( + "properties:", + typeof sandbox.declared, + typeof sandbox.expression, + typeof sandbox.lexical, +); +console.log("repeat:", vm.runInContext("declared() + expression()", context)); diff --git a/test-parity/node-suite/vm/context/global-object-forwarding.ts b/test-parity/node-suite/vm/context/global-object-forwarding.ts new file mode 100644 index 0000000000..05cb5122a9 --- /dev/null +++ b/test-parity/node-suite/vm/context/global-object-forwarding.ts @@ -0,0 +1,17 @@ +import * as vm from "node:vm"; + +const sandbox: any = { value: 1 }; +const context = vm.createContext(sandbox); +const contextGlobal: any = vm.runInContext("this", context); + +console.log("identity:", contextGlobal === sandbox); +contextGlobal.value = 4; +contextGlobal.added = 5; +console.log("outer write:", sandbox.value, sandbox.added); +sandbox.value = 6; +console.log( + "inner read:", + contextGlobal.value, + vm.runInContext("value", context), +); +console.log("own:", Object.hasOwn(contextGlobal, "added")); diff --git a/test-parity/node-suite/vm/context/microtask-mode.ts b/test-parity/node-suite/vm/context/microtask-mode.ts new file mode 100644 index 0000000000..8669c58e50 --- /dev/null +++ b/test-parity/node-suite/vm/context/microtask-mode.ts @@ -0,0 +1,13 @@ +import * as vm from "node:vm"; + +const sandbox: any = { value: 1 }; +vm.runInNewContext("Promise.resolve().then(() => value = 2)", sandbox, { + microtaskMode: "afterEvaluate", +}); +console.log("after evaluate:", sandbox.value); + +const context: any = vm.createContext({ value: 3 }, { + microtaskMode: "afterEvaluate", +}); +vm.runInContext("Promise.resolve().then(() => value = 4)", context); +console.log("context option:", context.value); diff --git a/test-parity/node-suite/vm/context/options-validation.ts b/test-parity/node-suite/vm/context/options-validation.ts new file mode 100644 index 0000000000..14782109e2 --- /dev/null +++ b/test-parity/node-suite/vm/context/options-validation.ts @@ -0,0 +1,39 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape( + "codeGeneration null", + () => vm.createContext({}, { codeGeneration: null as any }), +); +shape( + "strings type", + () => vm.createContext({}, { codeGeneration: { strings: 1 as any } }), +); +shape( + "wasm type", + () => vm.createContext({}, { codeGeneration: { wasm: "no" as any } }), +); +shape( + "microtask bad", + () => vm.createContext({}, { microtaskMode: "bad" as any }), +); +shape( + "microtask type", + () => vm.createContext({}, { microtaskMode: 1 as any }), +); +shape( + "name valid", + () => vm.createContext({}, { name: "fixture" }), +); +shape( + "origin valid", + () => vm.createContext({}, { origin: "vm://fixture" }), +); diff --git a/test-parity/node-suite/vm/context/properties/accessors.ts b/test-parity/node-suite/vm/context/properties/accessors.ts new file mode 100644 index 0000000000..e562d43be3 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/accessors.ts @@ -0,0 +1,25 @@ +import * as vm from "node:vm"; + +let stored = "unset"; +const sandbox: any = {}; +Object.defineProperties(sandbox, { + getter: { + configurable: true, + get: () => "read", + }, + setter: { + configurable: true, + get: () => stored, + set: (value) => { + stored = String(value); + }, + }, +}); + +const context = vm.createContext(sandbox); +console.log("initial:", vm.runInContext("getter + ':' + setter", context)); +console.log( + "write:", + vm.runInContext("setter = 'written'; getter + ':' + setter", context), +); +console.log("outer:", stored, sandbox.getter, sandbox.setter); diff --git a/test-parity/node-suite/vm/context/properties/circular-reference.ts b/test-parity/node-suite/vm/context/properties/circular-reference.ts new file mode 100644 index 0000000000..8e98748789 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/circular-reference.ts @@ -0,0 +1,10 @@ +import * as vm from "node:vm"; + +const sandbox: any = { value: 1 }; +sandbox.self = sandbox; +const context = vm.createContext(sandbox); + +console.log("outer chain:", sandbox.self.self.self === sandbox); +console.log("context read:", vm.runInContext("self.self.value", context)); +vm.runInContext("self.value = 4", context); +console.log("context write:", sandbox.value, sandbox.self.value); diff --git a/test-parity/node-suite/vm/context/properties/define-property.ts b/test-parity/node-suite/vm/context/properties/define-property.ts new file mode 100644 index 0000000000..91b43b8125 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/define-property.ts @@ -0,0 +1,28 @@ +import * as vm from "node:vm"; + +const sandbox: any = {}; +const context = vm.createContext(sandbox); +vm.runInContext( + "Object.defineProperty(globalThis, 'named', { value: 8, writable: false, enumerable: true, configurable: true })", + context, +); +vm.runInContext( + "Object.defineProperty(globalThis, 99, { value: 20, enumerable: true })", + context, +); + +const named = Object.getOwnPropertyDescriptor(sandbox, "named"); +const indexed = Object.getOwnPropertyDescriptor(sandbox, "99"); +console.log("values:", sandbox.named, sandbox[99]); +console.log( + "named flags:", + named?.writable, + named?.enumerable, + named?.configurable, +); +console.log( + "indexed flags:", + indexed?.writable, + indexed?.enumerable, + indexed?.configurable, +); diff --git a/test-parity/node-suite/vm/context/properties/deletion.ts b/test-parity/node-suite/vm/context/properties/deletion.ts new file mode 100644 index 0000000000..f3dae55b1e --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/deletion.ts @@ -0,0 +1,11 @@ +import * as vm from "node:vm"; + +const sandbox: any = { removable: 3, retained: 4 }; +const context = vm.createContext(sandbox); + +console.log( + "delete result:", + vm.runInContext("delete globalThis.removable", context), +); +console.log("outer:", "removable" in sandbox, sandbox.retained); +console.log("context:", vm.runInContext("typeof removable", context)); diff --git a/test-parity/node-suite/vm/context/properties/descriptors.ts b/test-parity/node-suite/vm/context/properties/descriptors.ts new file mode 100644 index 0000000000..2e7559e766 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/descriptors.ts @@ -0,0 +1,23 @@ +import * as vm from "node:vm"; + +const sandbox: any = {}; +Object.defineProperty(sandbox, "fixed", { + value: 7, + writable: false, + enumerable: false, + configurable: false, +}); +const context = vm.createContext(sandbox); +const descriptor: any = vm.runInContext( + "Object.getOwnPropertyDescriptor(globalThis, 'fixed')", + context, +); + +console.log("value:", descriptor?.value); +console.log( + "flags:", + descriptor?.writable, + descriptor?.enumerable, + descriptor?.configurable, +); +console.log("outer keys:", Object.keys(sandbox).includes("fixed")); diff --git a/test-parity/node-suite/vm/context/properties/inheritance.ts b/test-parity/node-suite/vm/context/properties/inheritance.ts new file mode 100644 index 0000000000..ed8a2313e7 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/inheritance.ts @@ -0,0 +1,15 @@ +import * as vm from "node:vm"; + +const base: any = { inherited: 2 }; +const sandbox: any = Object.create(base); +sandbox.own = 3; +const context = vm.createContext(sandbox); + +console.log("read:", vm.runInContext("inherited + own", context)); +console.log("write:", vm.runInContext("inherited = 5; inherited", context)); +console.log( + "ownership:", + Object.prototype.hasOwnProperty.call(sandbox, "inherited"), + sandbox.inherited, + base.inherited, +); diff --git a/test-parity/node-suite/vm/context/properties/non-writable.ts b/test-parity/node-suite/vm/context/properties/non-writable.ts new file mode 100644 index 0000000000..25bc25fe78 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/non-writable.ts @@ -0,0 +1,23 @@ +import * as vm from "node:vm"; + +const sandbox: any = {}; +Object.defineProperty(sandbox, "locked", { + value: 11, + writable: false, + configurable: true, +}); +const context = vm.createContext(sandbox); + +try { + console.log("sloppy:", vm.runInContext("locked = 12; locked", context)); +} catch (error: any) { + console.log("sloppy:", error.name, error.code || "-"); +} +console.log("outer:", sandbox.locked); +try { + vm.runInContext("'use strict'; locked = 13", context); + console.log("strict: ok"); +} catch (error: any) { + console.log("strict:", error.name, error.code || "-"); +} +console.log("unchanged:", sandbox.locked); diff --git a/test-parity/node-suite/vm/context/properties/own-key-enumeration.ts b/test-parity/node-suite/vm/context/properties/own-key-enumeration.ts new file mode 100644 index 0000000000..d1b212c6c0 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/own-key-enumeration.ts @@ -0,0 +1,30 @@ +import * as vm from "node:vm"; + +const symbol = Symbol("symbol"); +const sandbox: any = { visible: 1, symbol, [symbol]: 3 }; +Object.defineProperty(sandbox, "hidden", { value: 2 }); +const context = vm.createContext(sandbox); + +console.log( + "keys:", + vm.runInContext("Object.keys(globalThis).includes('visible')", context), + vm.runInContext("Object.keys(globalThis).includes('hidden')", context), +); +console.log( + "names:", + vm.runInContext( + "Object.getOwnPropertyNames(globalThis).includes('visible')", + context, + ), + vm.runInContext( + "Object.getOwnPropertyNames(globalThis).includes('hidden')", + context, + ), +); +console.log( + "symbols:", + vm.runInContext( + "Object.getOwnPropertySymbols(globalThis).includes(symbol)", + context, + ), +); diff --git a/test-parity/node-suite/vm/context/properties/symbol-keys.ts b/test-parity/node-suite/vm/context/properties/symbol-keys.ts new file mode 100644 index 0000000000..3c824ac0b4 --- /dev/null +++ b/test-parity/node-suite/vm/context/properties/symbol-keys.ts @@ -0,0 +1,31 @@ +import * as vm from "node:vm"; + +const marker = Symbol("marker"); +const hidden = Symbol("hidden"); +const sandbox: any = { marker, hidden, [marker]: "visible" }; +Object.defineProperty(sandbox, hidden, { value: "hidden" }); +const context = vm.createContext(sandbox); + +console.log( + "forwarded symbols:", + vm.runInContext( + "Object.getOwnPropertySymbols(globalThis).includes(marker)", + context, + ), + vm.runInContext( + "Object.getOwnPropertySymbols(globalThis).includes(hidden)", + context, + ), +); +console.log( + "values:", + vm.runInContext("globalThis[marker]", context), + vm.runInContext("globalThis[hidden]", context), +); +console.log( + "hidden enumerable:", + vm.runInContext( + "Object.getOwnPropertyDescriptor(globalThis, hidden).enumerable", + context, + ), +); diff --git a/test-parity/node-suite/vm/context/run-new-code-generation.ts b/test-parity/node-suite/vm/context/run-new-code-generation.ts new file mode 100644 index 0000000000..e58fa77132 --- /dev/null +++ b/test-parity/node-suite/vm/context/run-new-code-generation.ts @@ -0,0 +1,22 @@ +import * as vm from "node:vm"; + +try { + vm.runInNewContext("eval('1')", {}, { + contextCodeGeneration: { strings: false, wasm: true }, + }); + console.log("disabled: ok"); +} catch (error: any) { + console.log("disabled:", error.name, error.code || "-"); +} +console.log( + "enabled:", + vm.runInNewContext("eval('2 + 3')", {}, { + contextCodeGeneration: { strings: true }, + }), +); +try { + vm.runInNewContext("1", {}, { contextCodeGeneration: null as any }); + console.log("validation: ok"); +} catch (error: any) { + console.log("validation:", error.name, error.code || "-"); +} diff --git a/test-parity/node-suite/vm/context/strict-writable-assignment.ts b/test-parity/node-suite/vm/context/strict-writable-assignment.ts new file mode 100644 index 0000000000..56ca426cc2 --- /dev/null +++ b/test-parity/node-suite/vm/context/strict-writable-assignment.ts @@ -0,0 +1,15 @@ +import * as vm from "node:vm"; + +const sandbox: any = { value: 2 }; +const context = vm.createContext(sandbox); + +console.log( + "strict existing:", + vm.runInContext("'use strict'; value = value + 3; value", context), + sandbox.value, +); +console.log( + "strict explicit global:", + vm.runInContext("'use strict'; globalThis.added = 7; added", context), + sandbox.added, +); diff --git a/test-parity/node-suite/vm/cross-context/builtin-identity.ts b/test-parity/node-suite/vm/cross-context/builtin-identity.ts new file mode 100644 index 0000000000..803ac4d8e0 --- /dev/null +++ b/test-parity/node-suite/vm/cross-context/builtin-identity.ts @@ -0,0 +1,21 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({}); +const names = [ + "Object", + "Array", + "Error", + "TypeError", + "Promise", + "Map", + "Set", +]; +for (const name of names) { + const value = vm.runInContext(name, context); + console.log(name + ":", typeof value, value === (globalThis as any)[name]); +} +console.log( + "prototype identity:", + vm.runInContext("Object.prototype", context) === Object.prototype, + vm.runInContext("Array.prototype", context) === Array.prototype, +); diff --git a/test-parity/node-suite/vm/cross-context/descriptors.ts b/test-parity/node-suite/vm/cross-context/descriptors.ts new file mode 100644 index 0000000000..b5ac6fd0c6 --- /dev/null +++ b/test-parity/node-suite/vm/cross-context/descriptors.ts @@ -0,0 +1,21 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({}); +const descriptor: any = vm.runInContext( + "Object.getOwnPropertyDescriptor({ value: 1 }, 'value')", + context, +); + +console.log( + "flags:", + descriptor?.enumerable, + descriptor?.configurable, + descriptor?.writable, +); +console.log("value:", typeof descriptor?.value, descriptor?.value); +console.log( + "prototype:", + descriptor === undefined + ? false + : Object.getPrototypeOf(descriptor) === Object.prototype, +); diff --git a/test-parity/node-suite/vm/cross-context/promises.ts b/test-parity/node-suite/vm/cross-context/promises.ts new file mode 100644 index 0000000000..a337301d37 --- /dev/null +++ b/test-parity/node-suite/vm/cross-context/promises.ts @@ -0,0 +1,13 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({}); +const promise: any = vm.runInContext("Promise.resolve({ value: 7 })", context); +console.log("thenable:", typeof promise?.then, promise instanceof Promise); +const value = await promise; +console.log( + "resolved:", + value?.value, + value === undefined + ? false + : Object.getPrototypeOf(value) === Object.prototype, +); diff --git a/test-parity/node-suite/vm/cross-context/structured-values.ts b/test-parity/node-suite/vm/cross-context/structured-values.ts new file mode 100644 index 0000000000..08a8a9e8d6 --- /dev/null +++ b/test-parity/node-suite/vm/cross-context/structured-values.ts @@ -0,0 +1,28 @@ +import * as vm from "node:vm"; + +const context = vm.createContext({}); +const object: any = vm.runInContext("({ value: 1 })", context); +const array: any = vm.runInContext("[1, 2, 3]", context); +const error: any = vm.runInContext("new TypeError('boom')", context); + +console.log( + "object:", + object.value, + Object.getPrototypeOf(object) === Object.prototype, +); +console.log( + "array:", + Array.isArray(array), + array.length, + Object.getPrototypeOf(array) === Array.prototype, +); +console.log( + "error:", + typeof error, + error?.name, + error instanceof TypeError, +); +console.log( + "context brands:", + vm.runInContext("Object.prototype.toString.call([])", context), +); diff --git a/test-parity/node-suite/vm/execution/compile-function.ts b/test-parity/node-suite/vm/execution/compile-function.ts index 7aa9d309cc..d98b431752 100644 --- a/test-parity/node-suite/vm/execution/compile-function.ts +++ b/test-parity/node-suite/vm/execution/compile-function.ts @@ -16,15 +16,24 @@ const fn: any = vm.compileFunction("return a + b + globalThis.z", ["a", "b"], { parsingContext: context, }); -console.log("compile bound:", fn.length, fn(3, 4), typeof (globalThis as any).z); +console.log( + "compile bound:", + fn.length, + fn(3, 4), + typeof (globalThis as any).z, +); const mainFn: any = vm.compileFunction("return a + b", ["a", "b"]); console.log("compile main:", mainFn.length, mainFn(10, 20)); errorShape("compile code validation", () => vm.compileFunction(1 as any)); -errorShape("compile params validation", () => vm.compileFunction("return 1", "x" as any)); -errorShape("compile context validation", () => - vm.compileFunction("return 1", [], { parsingContext: {} as any }), +errorShape( + "compile params validation", + () => vm.compileFunction("return 1", "x" as any), +); +errorShape( + "compile context validation", + () => vm.compileFunction("return 1", [], { parsingContext: {} as any }), ); console.log( diff --git a/test-parity/node-suite/vm/execution/context-lexicals.ts b/test-parity/node-suite/vm/execution/context-lexicals.ts new file mode 100644 index 0000000000..b097e4c6e1 --- /dev/null +++ b/test-parity/node-suite/vm/execution/context-lexicals.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; + +const first: any = vm.createContext({}); +const second: any = vm.createContext({}); + +console.log( + "declare:", + vm.runInContext("let lexical = 3; const fixed = 4; lexical + fixed", first), +); +console.log( + "repeat:", + vm.runInContext("lexical = lexical + 2; lexical + fixed", first), +); +console.log( + "not property:", + Object.prototype.hasOwnProperty.call(first, "lexical"), +); +console.log("other context:", vm.runInContext("typeof lexical", second)); diff --git a/test-parity/node-suite/vm/execution/create-script-alias.ts b/test-parity/node-suite/vm/execution/create-script-alias.ts new file mode 100644 index 0000000000..a380d214b0 --- /dev/null +++ b/test-parity/node-suite/vm/execution/create-script-alias.ts @@ -0,0 +1,19 @@ +import * as vm from "node:vm"; + +const direct = new vm.Script("value = value + 1; value"); +const factory = vm.createScript("value = value + 1; value"); +const one: any = vm.createContext({ value: 1 }); +const two: any = vm.createContext({ value: 10 }); + +console.log( + "instances:", + direct instanceof vm.Script, + factory instanceof vm.Script, +); +console.log( + "constructor:", + direct.constructor === vm.Script, + factory.constructor === vm.Script, +); +console.log("direct:", direct.runInContext(one), one.value); +console.log("factory:", factory.runInContext(two), two.value); diff --git a/test-parity/node-suite/vm/execution/function-receiver.ts b/test-parity/node-suite/vm/execution/function-receiver.ts new file mode 100644 index 0000000000..c801fb3030 --- /dev/null +++ b/test-parity/node-suite/vm/execution/function-receiver.ts @@ -0,0 +1,15 @@ +import * as vm from "node:vm"; + +const main: any = vm.compileFunction("return this"); +const context: any = vm.createContext({ marker: 7 }); +const bound: any = vm.compileFunction("return this", [], { + parsingContext: context, +}); + +console.log("main undefined call:", main() === globalThis); +console.log("main receiver:", main.call({ marker: 1 }).marker); +console.log( + "bound undefined call:", + bound() === vm.runInContext("globalThis", context), +); +console.log("bound receiver:", bound.call({ marker: 2 }).marker); diff --git a/test-parity/node-suite/vm/execution/module-scope-isolation.ts b/test-parity/node-suite/vm/execution/module-scope-isolation.ts new file mode 100644 index 0000000000..d50d551a92 --- /dev/null +++ b/test-parity/node-suite/vm/execution/module-scope-isolation.ts @@ -0,0 +1,17 @@ +import * as vm from "node:vm"; + +const moduleOnly = 7; +console.log( + "module local:", + moduleOnly, + vm.runInThisContext("typeof moduleOnly"), +); +try { + console.log( + "explicit global:", + vm.runInThisContext("globalThis.vmScoped = 3; vmScoped"), + (globalThis as any).vmScoped, + ); +} finally { + delete (globalThis as any).vmScoped; +} diff --git a/test-parity/node-suite/vm/execution/run-in-context-mutations.ts b/test-parity/node-suite/vm/execution/run-in-context-mutations.ts new file mode 100644 index 0000000000..52570be4c7 --- /dev/null +++ b/test-parity/node-suite/vm/execution/run-in-context-mutations.ts @@ -0,0 +1,23 @@ +import * as vm from "node:vm"; + +const sandbox: any = { count: 1, nested: { value: 3 } }; +const context = vm.createContext(sandbox); + +console.log( + "first:", + vm.runInContext( + "count = count + 1; nested.value = nested.value + 2; count", + context, + ), +); +console.log("after first:", sandbox.count, sandbox.nested.value); +console.log( + "second:", + vm.runInContext("count += 4; added = count; added", context), +); +console.log("after second:", sandbox.count, sandbox.added); +console.log( + "same receiver:", + vm.runInContext("this", context) === vm.runInContext("globalThis", context), +); +console.log("sandbox receiver:", vm.runInContext("this", context) === sandbox); diff --git a/test-parity/node-suite/vm/execution/run-in-new-isolation.ts b/test-parity/node-suite/vm/execution/run-in-new-isolation.ts new file mode 100644 index 0000000000..7f57ecf66b --- /dev/null +++ b/test-parity/node-suite/vm/execution/run-in-new-isolation.ts @@ -0,0 +1,23 @@ +import * as vm from "node:vm"; + +(globalThis as any).__outside = 9; +try { + const sandbox: any = { seed: 2 }; + const result = vm.runInNewContext( + "seed = seed + 5; created = seed + 1; typeof process + ':' + typeof __outside", + sandbox, + ); + console.log("result:", result); + console.log("sandbox:", sandbox.seed, sandbox.created); + console.log("outer created:", typeof (globalThis as any).created); + console.log( + "receiver:", + vm.runInNewContext("this === globalThis", sandbox) === true, + ); + console.log( + "sandbox identity:", + vm.runInNewContext("this", sandbox) === sandbox, + ); +} finally { + delete (globalThis as any).__outside; +} diff --git a/test-parity/node-suite/vm/execution/run-in-this-bindings.ts b/test-parity/node-suite/vm/execution/run-in-this-bindings.ts new file mode 100644 index 0000000000..0630ce590a --- /dev/null +++ b/test-parity/node-suite/vm/execution/run-in-this-bindings.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; + +(globalThis as any).__vmValue = 4; +try { + console.log("read:", vm.runInThisContext("__vmValue")); + console.log( + "write result:", + vm.runInThisContext("__vmValue = __vmValue + 3; __vmValue"), + ); + console.log("write global:", (globalThis as any).__vmValue); + console.log( + "global identity:", + vm.runInThisContext("this === globalThis") === true, + ); + console.log("process visibility:", vm.runInThisContext("typeof process")); +} finally { + delete (globalThis as any).__vmValue; +} diff --git a/test-parity/node-suite/vm/execution/script-context.ts b/test-parity/node-suite/vm/execution/script-context.ts index 7b70c535dc..86d4917d2c 100644 --- a/test-parity/node-suite/vm/execution/script-context.ts +++ b/test-parity/node-suite/vm/execution/script-context.ts @@ -25,7 +25,10 @@ console.log( sandbox.y, typeof (globalThis as any).y, ); -errorShape("run plain object validation", () => vm.runInContext("1", {} as any)); +errorShape( + "run plain object validation", + () => vm.runInContext("1", {} as any), +); try { (vm as any).Script("1"); @@ -51,5 +54,13 @@ console.log( typeof (globalThis as any).result, ); -console.log("runInContext:", vm.runInContext("x = x + 1; x", context), sandbox.x); -console.log("createScript:", vm.createScript("x = x + 1; x").runInContext(context), sandbox.x); +console.log( + "runInContext:", + vm.runInContext("x = x + 1; x", context), + sandbox.x, +); +console.log( + "createScript:", + vm.createScript("x = x + 1; x").runInContext(context), + sandbox.x, +); diff --git a/test-parity/node-suite/vm/execution/script-run-methods.ts b/test-parity/node-suite/vm/execution/script-run-methods.ts new file mode 100644 index 0000000000..a8bfec6332 --- /dev/null +++ b/test-parity/node-suite/vm/execution/script-run-methods.ts @@ -0,0 +1,28 @@ +import * as vm from "node:vm"; + +(globalThis as any).__scriptCount = 0; +try { + const script = new vm.Script( + "__scriptCount = __scriptCount + 1; __scriptCount", + ); + console.log( + "this repeat:", + script.runInThisContext(), + script.runInThisContext(), + ); + const sandbox: any = { __scriptCount: 10 }; + const context = vm.createContext(sandbox); + console.log( + "context repeat:", + script.runInContext(context), + script.runInContext(context), + sandbox.__scriptCount, + ); + console.log( + "new repeat:", + script.runInNewContext({ __scriptCount: 20 }), + script.runInNewContext({ __scriptCount: 30 }), + ); +} finally { + delete (globalThis as any).__scriptCount; +} diff --git a/test-parity/node-suite/vm/imports/named-and-namespace.ts b/test-parity/node-suite/vm/imports/named-and-namespace.ts index e33056aaf6..192d8dd8c0 100644 --- a/test-parity/node-suite/vm/imports/named-and-namespace.ts +++ b/test-parity/node-suite/vm/imports/named-and-namespace.ts @@ -1,7 +1,6 @@ // node:vm no-flag ESM import surface. import vmDefault, * as vm from "node:vm"; import { - Script, compileFunction, constants, createContext, @@ -11,19 +10,22 @@ import { runInContext, runInNewContext, runInThisContext, + Script, } from "node:vm"; -for (const [name, value] of [ - ["Script", Script], - ["createContext", createContext], - ["createScript", createScript], - ["runInContext", runInContext], - ["runInNewContext", runInNewContext], - ["runInThisContext", runInThisContext], - ["isContext", isContext], - ["compileFunction", compileFunction], - ["measureMemory", measureMemory], -] as const) { +for ( + const [name, value] of [ + ["Script", Script], + ["createContext", createContext], + ["createScript", createScript], + ["runInContext", runInContext], + ["runInNewContext", runInNewContext], + ["runInThisContext", runInThisContext], + ["isContext", isContext], + ["compileFunction", compileFunction], + ["measureMemory", measureMemory], + ] as const +) { console.log( name, typeof value, diff --git a/test-parity/node-suite/vm/metadata/cached-data-after-run.ts b/test-parity/node-suite/vm/metadata/cached-data-after-run.ts new file mode 100644 index 0000000000..6f62be4f9c --- /dev/null +++ b/test-parity/node-suite/vm/metadata/cached-data-after-run.ts @@ -0,0 +1,21 @@ +import * as vm from "node:vm"; + +const source = "value = value + 1; value"; +const script: any = new vm.Script(source); +const before = script.createCachedData(); +const sandbox: any = { value: 1 }; + +console.log("run:", script.runInNewContext(sandbox), sandbox.value); +const after = script.createCachedData(); +console.log( + "cache shape:", + Buffer.isBuffer(before), + before.length > 0, + Buffer.isBuffer(after), + after.length > 0, +); +console.log( + "accepted:", + new vm.Script(source, { cachedData: before }).cachedDataRejected, + new vm.Script(source, { cachedData: after }).cachedDataRejected, +); diff --git a/test-parity/node-suite/vm/metadata/cached-data-views.ts b/test-parity/node-suite/vm/metadata/cached-data-views.ts new file mode 100644 index 0000000000..d707467e56 --- /dev/null +++ b/test-parity/node-suite/vm/metadata/cached-data-views.ts @@ -0,0 +1,41 @@ +import * as vm from "node:vm"; + +const scriptSource = "value + 1"; +const scriptCache = new vm.Script(scriptSource).createCachedData(); +const scriptBytes = new Uint8Array( + scriptCache.buffer, + scriptCache.byteOffset, + scriptCache.byteLength, +); +const scriptView = new DataView( + scriptCache.buffer, + scriptCache.byteOffset, + scriptCache.byteLength, +); +console.log( + "Script views:", + new vm.Script(scriptSource, { cachedData: scriptBytes }).cachedDataRejected, + new vm.Script(scriptSource, { cachedData: scriptView }).cachedDataRejected, +); + +const functionSource = "return value + 1"; +const produced: any = vm.compileFunction(functionSource, ["value"], { + produceCachedData: true, +}); +const functionBytes = new Uint8Array( + produced.cachedData.buffer, + produced.cachedData.byteOffset, + produced.cachedData.byteLength, +); +const functionView = new DataView( + produced.cachedData.buffer, + produced.cachedData.byteOffset, + produced.cachedData.byteLength, +); +console.log( + "function views:", + vm.compileFunction(functionSource, ["value"], { cachedData: functionBytes }) + .cachedDataRejected, + vm.compileFunction(functionSource, ["value"], { cachedData: functionView }) + .cachedDataRejected, +); diff --git a/test-parity/node-suite/vm/metadata/measure-memory.ts b/test-parity/node-suite/vm/metadata/measure-memory.ts index 3f74d62072..7478fd6100 100644 --- a/test-parity/node-suite/vm/metadata/measure-memory.ts +++ b/test-parity/node-suite/vm/metadata/measure-memory.ts @@ -21,7 +21,11 @@ function entryShape(label: string, entry: any) { ); } -console.log("measure function:", typeof vm.measureMemory, vm.measureMemory.length); +console.log( + "measure function:", + typeof vm.measureMemory, + vm.measureMemory.length, +); console.log("measure promise:", typeof vm.measureMemory().then); const summary: any = await vm.measureMemory({ execution: "eager" }); @@ -34,11 +38,18 @@ console.log( typeof summary.WebAssembly.metadata, ); -const detailed: any = await vm.measureMemory({ mode: "detailed", execution: "eager" }); +const detailed: any = await vm.measureMemory({ + mode: "detailed", + execution: "eager", +}); console.log("detailed keys:", Object.keys(detailed).join(",")); entryShape("detailed total", detailed.total); entryShape("detailed current", detailed.current); -console.log("detailed other:", Array.isArray(detailed.other), detailed.other.length); +console.log( + "detailed other:", + Array.isArray(detailed.other), + detailed.other.length, +); errorShape("mode validation", () => { vm.measureMemory({ mode: "bad" as any }); diff --git a/test-parity/node-suite/vm/metadata/script-cached-data.ts b/test-parity/node-suite/vm/metadata/script-cached-data.ts index 0f6bd13486..8f0085330c 100644 --- a/test-parity/node-suite/vm/metadata/script-cached-data.ts +++ b/test-parity/node-suite/vm/metadata/script-cached-data.ts @@ -15,8 +15,15 @@ const script: any = new vm.Script(source); const cachedData = script.createCachedData(); console.log("source map:", script.sourceMapURL); -console.log("script cache shape:", Buffer.isBuffer(cachedData), cachedData.length > 0); -console.log("script no cache rejected:", script.cachedDataRejected === undefined); +console.log( + "script cache shape:", + Buffer.isBuffer(cachedData), + cachedData.length > 0, +); +console.log( + "script no cache rejected:", + script.cachedDataRejected === undefined, +); const accepted: any = new vm.Script(source, { cachedData }); console.log("script cache accepted:", accepted.cachedDataRejected); diff --git a/test-parity/node-suite/vm/modules/constructor-validation.ts b/test-parity/node-suite/vm/modules/constructor-validation.ts new file mode 100644 index 0000000000..f49258921b --- /dev/null +++ b/test-parity/node-suite/vm/modules/constructor-validation.ts @@ -0,0 +1,34 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("source call", () => (vm.SourceTextModule as any)("")); +shape("source code", () => new vm.SourceTextModule(1 as any)); +shape("source options", () => new vm.SourceTextModule("", null as any)); +shape( + "source context", + () => new vm.SourceTextModule("", { context: {} as any }), +); +shape( + "identifier", + () => new vm.SourceTextModule("", { identifier: 1 as any }), +); +shape("synthetic call", () => (vm.SyntheticModule as any)([], () => {})); +shape( + "synthetic exports", + () => new vm.SyntheticModule("value" as any, () => {}), +); +shape("synthetic callback", () => new vm.SyntheticModule([], null as any)); +shape( + "synthetic context", + () => new vm.SyntheticModule([], () => {}, { context: {} as any }), +); diff --git a/test-parity/node-suite/vm/modules/context-and-namespace.ts b/test-parity/node-suite/vm/modules/context-and-namespace.ts new file mode 100644 index 0000000000..227421fe15 --- /dev/null +++ b/test-parity/node-suite/vm/modules/context-and-namespace.ts @@ -0,0 +1,32 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +const context: any = vm.createContext({ seed: 4 }); +const module = new vm.SourceTextModule( + "export const answer = seed + 1; globalThis.created = answer + 1", + { context, identifier: "context-module" }, +); + +console.log("initial:", module.status, module.identifier); +try { + console.log("initial namespace:", module.namespace.answer); +} catch (error: any) { + console.log("initial namespace:", error.name, error.code || "-"); +} +await module.link(() => { + throw new Error("unexpected link"); +}); +console.log("linked:", module.status); +await module.evaluate(); +console.log( + "evaluated:", + module.status, + module.namespace.answer, + context.created, +); +console.log("namespace tag:", Object.prototype.toString.call(module.namespace)); +console.log( + "namespace prototype:", + Object.getPrototypeOf(module.namespace) === null, +); diff --git a/test-parity/node-suite/vm/modules/evaluation-error.ts b/test-parity/node-suite/vm/modules/evaluation-error.ts new file mode 100644 index 0000000000..ad5d6defd5 --- /dev/null +++ b/test-parity/node-suite/vm/modules/evaluation-error.ts @@ -0,0 +1,29 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +const module = new vm.SourceTextModule("throw new TypeError('boom')", { + identifier: "error.vm", +}); +await module.link(() => { + throw new Error("unexpected link"); +}); +let caught: any; +try { + await module.evaluate(); + console.log("evaluate: ok"); +} catch (error: any) { + caught = error; + console.log("evaluate:", error.name, error.code || "-"); +} +console.log("status:", module.status); +try { + console.log( + "stored error:", + module.error.name, + module.error.code || "-", + module.error === caught, + ); +} catch (error: any) { + console.log("stored error:", error.name, error.code || "-"); +} diff --git a/test-parity/node-suite/vm/modules/lifecycle.ts b/test-parity/node-suite/vm/modules/lifecycle.ts index b81c33b7dc..4aeeaefefa 100644 --- a/test-parity/node-suite/vm/modules/lifecycle.ts +++ b/test-parity/node-suite/vm/modules/lifecycle.ts @@ -3,51 +3,58 @@ import * as vm from "node:vm"; function logError(label: string, fn: () => unknown): void { - try { - fn(); - console.log(label, "ok"); - } catch (error) { - console.log( - label, - error.constructor.name, - error.name, - error.code ?? "-", - error.message, - ); - } + try { + fn(); + console.log(label, "ok"); + } catch (error) { + console.log( + label, + error.constructor.name, + error.name, + error.code ?? "-", + ); + } } console.log( - "module keys", - Object.keys(vm).filter((key) => key.includes("Module")).join(","), + "module keys", + Object.keys(vm).filter((key) => key.includes("Module")).join(","), ); console.log( - "module types", - typeof vm.Module, - typeof vm.SourceTextModule, - typeof vm.SyntheticModule, - (vm as any).Module.length, - vm.SourceTextModule.length, - vm.SyntheticModule.length, + "module types", + typeof vm.Module, + typeof vm.SourceTextModule, + typeof vm.SyntheticModule, + (vm as any).Module.length, + vm.SourceTextModule.length, + vm.SyntheticModule.length, ); logError("module call", () => (vm as any).Module()); logError("module construct", () => new (vm as any).Module()); let moduleCallArgOrder = 0; -logError("module call arg", () => (vm as any).Module((moduleCallArgOrder = 7))); +logError("module call arg", () => (vm as any).Module(moduleCallArgOrder = 7)); console.log("module call arg side effect", moduleCallArgOrder); let moduleConstructArgOrder = 0; -logError("module construct arg", () => new (vm as any).Module((moduleConstructArgOrder = 9))); +logError( + "module construct arg", + () => new (vm as any).Module(moduleConstructArgOrder = 9), +); console.log("module construct arg side effect", moduleConstructArgOrder); const dep = new vm.SyntheticModule( - ["value", "label"], - () => { - console.log("synthetic callback"); - }, - { identifier: "dep" }, + ["value", "label"], + () => { + console.log("synthetic callback"); + }, + { identifier: "dep" }, ); -console.log("dep initial", dep.status, dep.identifier, dep.namespace.value === undefined); +console.log( + "dep initial", + dep.status, + dep.identifier, + dep.namespace.value === undefined, +); logError("dep missing before link", () => dep.setExport("missing", 1)); dep.setExport("value", 1); console.log("dep preset", dep.namespace.value); @@ -57,57 +64,71 @@ logError("dep missing linked", () => dep.setExport("missing", 2)); dep.setExport("value", 41); dep.setExport("label", "dep"); await dep.evaluate(); -console.log("dep evaluated", dep.status, dep.namespace.value, dep.namespace.label); +console.log( + "dep evaluated", + dep.status, + dep.namespace.value, + dep.namespace.label, +); logError("dep missing evaluated", () => dep.setExport("missing", 3)); const source = new vm.SourceTextModule( - [ - 'import { value as depValue, label } from "dep";', - "export const answer = depValue + 1;", - "export const message = label + ':' + answer;", - ].join("\n"), - { identifier: "main" }, + [ + 'import { value as depValue, label } from "dep";', + "export const answer = depValue + 1;", + "export const message = label + ':' + answer;", + ].join("\n"), + { identifier: "main" }, ); console.log("source initial", source.status, source.identifier); console.log("source deps", source.dependencySpecifiers.join(",")); console.log( - "source requests", - source.moduleRequests - .map((request) => `${request.specifier}:${JSON.stringify(request.attributes)}:${request.phase}`) - .join("|"), + "source requests", + source.moduleRequests + .map((request) => + `${request.specifier}:${ + JSON.stringify(request.attributes) + }:${request.phase}` + ) + .join("|"), ); console.log("source tla", source.hasTopLevelAwait()); try { - source.hasAsyncGraph(); + source.hasAsyncGraph(); } catch (error) { - console.log("source async precondition", error.code, error.name); + console.log("source async precondition", error.code, error.name); } await source.link((specifier, referencingModule, extra) => { - console.log( - "linker", - specifier, - referencingModule.identifier, - JSON.stringify(extra.attributes), - ); - return dep; + console.log( + "linker", + specifier, + referencingModule.identifier, + JSON.stringify(extra.attributes), + ); + return dep; }); console.log("source linked", source.status, source.hasAsyncGraph()); await source.evaluate(); -console.log("source evaluated", source.status, source.namespace.answer, source.namespace.message); +console.log( + "source evaluated", + source.status, + source.namespace.answer, + source.namespace.message, +); try { - source.error; + source.error; } catch (error) { - console.log("source error precondition", error.code, error.name); + console.log("source error precondition", error.code, error.name); } const viaRequests = new vm.SourceTextModule( - [ - 'import { value } from "dep";', - "export const doubled = value + value;", - ].join("\n"), - { identifier: "req" }, + [ + 'import { value } from "dep";', + "export const doubled = value + value;", + ].join("\n"), + { identifier: "req" }, ); console.log("req before", viaRequests.status); diff --git a/test-parity/node-suite/vm/modules/repeated-evaluate.ts b/test-parity/node-suite/vm/modules/repeated-evaluate.ts new file mode 100644 index 0000000000..80c76bc919 --- /dev/null +++ b/test-parity/node-suite/vm/modules/repeated-evaluate.ts @@ -0,0 +1,29 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +const context: any = vm.createContext({ evaluations: 0 }); +const module = new vm.SourceTextModule( + "evaluations = evaluations + 1; export const value = evaluations", + { context, identifier: "repeat.vm" }, +); + +await module.link(() => { + throw new Error("unexpected link"); +}); +const first = module.evaluate(); +console.log("first promise:", typeof first?.then); +await first; +console.log( + "first:", + module.status, + module.namespace.value, + context.evaluations, +); +await module.evaluate(); +console.log( + "second:", + module.status, + module.namespace.value, + context.evaluations, +); diff --git a/test-parity/node-suite/vm/modules/source-syntax-errors.ts b/test-parity/node-suite/vm/modules/source-syntax-errors.ts new file mode 100644 index 0000000000..b9f62886c0 --- /dev/null +++ b/test-parity/node-suite/vm/modules/source-syntax-errors.ts @@ -0,0 +1,25 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape( + "export", + () => + new vm.SourceTextModule("export const = 1", { identifier: "export.vm" }), +); +shape( + "import", + () => + new vm.SourceTextModule("import { value from 'dep'", { + identifier: "import.vm", + }), +); diff --git a/test-parity/node-suite/vm/modules/top-level-await-metadata.ts b/test-parity/node-suite/vm/modules/top-level-await-metadata.ts new file mode 100644 index 0000000000..1a96145fe5 --- /dev/null +++ b/test-parity/node-suite/vm/modules/top-level-await-metadata.ts @@ -0,0 +1,25 @@ +// parity-node-argv: --experimental-vm-modules --no-warnings +// parity-env: PERRY_EXPERIMENTAL_VM_MODULES=1 +import * as vm from "node:vm"; + +const sync = new vm.SourceTextModule("export const value = 1"); +const asyncModule = new vm.SourceTextModule( + "await Promise.resolve(); export const value = 2", +); + +console.log( + "before link:", + sync.hasTopLevelAwait(), + asyncModule.hasTopLevelAwait(), +); +await sync.link(() => { + throw new Error("unexpected link"); +}); +await asyncModule.link(() => { + throw new Error("unexpected link"); +}); +console.log( + "after link:", + sync.hasTopLevelAwait(), + asyncModule.hasTopLevelAwait(), +); diff --git a/test-parity/node-suite/vm/require/require-shape.ts b/test-parity/node-suite/vm/require/require-shape.ts index abd1221516..f59bfe16f3 100644 --- a/test-parity/node-suite/vm/require/require-shape.ts +++ b/test-parity/node-suite/vm/require/require-shape.ts @@ -5,19 +5,25 @@ const vmBare = require("vm"); console.log("node prefix identity:", vm === vmBare); console.log("require keys:", Object.keys(vm).join(",")); -for (const name of [ - "Script", - "createContext", - "createScript", - "runInContext", - "runInNewContext", - "runInThisContext", - "isContext", - "compileFunction", - "measureMemory", -] as const) { +for ( + const name of [ + "Script", + "createContext", + "createScript", + "runInContext", + "runInNewContext", + "runInThisContext", + "isContext", + "compileFunction", + "measureMemory", + ] as const +) { const value = vm[name]; - console.log(name, typeof value, typeof value === "function" ? value.length : "-"); + console.log( + name, + typeof value, + typeof value === "function" ? value.length : "-", + ); } const builtin = process.getBuiltinModule("vm"); diff --git a/test-parity/node-suite/vm/script/constructor-validation.ts b/test-parity/node-suite/vm/script/constructor-validation.ts new file mode 100644 index 0000000000..e04b014cc6 --- /dev/null +++ b/test-parity/node-suite/vm/script/constructor-validation.ts @@ -0,0 +1,24 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("call", () => (vm.Script as any)("1")); +shape("options", () => new vm.Script("1", 42 as any)); +shape("filename", () => new vm.Script("1", { filename: 1 as any })); +shape("lineOffset type", () => new vm.Script("1", { lineOffset: "1" as any })); +shape("lineOffset range", () => new vm.Script("1", { lineOffset: 0.5 })); +shape( + "columnOffset type", + () => new vm.Script("1", { columnOffset: null as any }), +); +shape( + "columnOffset range", + () => new vm.Script("1", { columnOffset: 2 ** 32 }), +); diff --git a/test-parity/node-suite/vm/script/display-errors.ts b/test-parity/node-suite/vm/script/display-errors.ts new file mode 100644 index 0000000000..042ce8d2e8 --- /dev/null +++ b/test-parity/node-suite/vm/script/display-errors.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; + +for (const displayErrors of [true, false]) { + try { + vm.runInThisContext("throw new RangeError('stable')", { + filename: "display-errors.vm", + displayErrors, + }); + console.log("display " + displayErrors + ": ok"); + } catch (error: any) { + console.log( + "display " + displayErrors + ":", + error.name, + error.code || "-", + String(error.stack).includes("display-errors.vm"), + ); + } +} diff --git a/test-parity/node-suite/vm/script/error-metadata.ts b/test-parity/node-suite/vm/script/error-metadata.ts new file mode 100644 index 0000000000..19ff611b0d --- /dev/null +++ b/test-parity/node-suite/vm/script/error-metadata.ts @@ -0,0 +1,48 @@ +import * as vm from "node:vm"; + +function capture(label: string, fn: () => unknown, filename: string) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log( + label + ":", + error.name, + error.code || "-", + String(error.stack).includes(filename), + ); + } +} + +capture( + "syntax filename", + () => new vm.Script(")", { filename: "syntax-fixture.vm" }), + "syntax-fixture.vm", +); +capture( + "syntax offsets", + () => + new vm.Script(")", { + filename: "offset-fixture.vm", + lineOffset: 4, + columnOffset: 3, + }), + "offset-fixture.vm", +); +const offsetStack = new vm.Script("new Error().stack", { + filename: "offset-fixture.vm", + lineOffset: 4, + columnOffset: 3, +}).runInThisContext(); +console.log( + "offset position:", + String(offsetStack).includes("offset-fixture.vm:5:4"), +); +const runtime = new vm.Script("throw new TypeError('boom')", { + filename: "runtime-fixture.vm", +}); +capture( + "runtime filename", + () => runtime.runInThisContext(), + "runtime-fixture.vm", +); diff --git a/test-parity/node-suite/vm/script/fresh-contexts.ts b/test-parity/node-suite/vm/script/fresh-contexts.ts new file mode 100644 index 0000000000..d22b103ea8 --- /dev/null +++ b/test-parity/node-suite/vm/script/fresh-contexts.ts @@ -0,0 +1,14 @@ +import * as vm from "node:vm"; + +const script = new vm.Script( + "counter = typeof counter === 'undefined' ? 1 : counter + 1; counter", +); + +console.log("fresh:", script.runInNewContext(), script.runInNewContext()); +const sandbox: any = {}; +console.log( + "reused object:", + script.runInNewContext(sandbox), + script.runInNewContext(sandbox), + sandbox.counter, +); diff --git a/test-parity/node-suite/vm/script/run-options-validation.ts b/test-parity/node-suite/vm/script/run-options-validation.ts new file mode 100644 index 0000000000..36f5575575 --- /dev/null +++ b/test-parity/node-suite/vm/script/run-options-validation.ts @@ -0,0 +1,27 @@ +import * as vm from "node:vm"; + +const script = new vm.Script("1"); +const context = vm.createContext(); + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("this options", () => script.runInThisContext(null as any)); +shape("context options", () => script.runInContext(context, "bad" as any)); +shape("new options", () => script.runInNewContext({}, 1 as any)); +shape("timeout type", () => script.runInThisContext({ timeout: "1" as any })); +shape("timeout range", () => script.runInThisContext({ timeout: 0 })); +shape( + "displayErrors", + () => script.runInThisContext({ displayErrors: 1 as any }), +); +shape( + "breakOnSigint", + () => script.runInThisContext({ breakOnSigint: null as any }), +); diff --git a/test-parity/node-suite/vm/script/string-filename-option.ts b/test-parity/node-suite/vm/script/string-filename-option.ts new file mode 100644 index 0000000000..ebef88c9df --- /dev/null +++ b/test-parity/node-suite/vm/script/string-filename-option.ts @@ -0,0 +1,35 @@ +import * as vm from "node:vm"; + +function containsFilename(value: unknown, filename: string) { + return String(value).includes(filename); +} + +console.log( + "Script:", + containsFilename( + new vm.Script("new Error().stack", "script-string.vm").runInThisContext(), + "script-string.vm", + ), +); +console.log( + "runThis:", + containsFilename( + vm.runInThisContext("new Error().stack", "this-string.vm"), + "this-string.vm", + ), +); +console.log( + "runNew:", + containsFilename( + vm.runInNewContext("new Error().stack", {}, "new-string.vm"), + "new-string.vm", + ), +); +const context = vm.createContext({}); +console.log( + "runContext:", + containsFilename( + vm.runInContext("new Error().stack", context, "context-string.vm"), + "context-string.vm", + ), +); diff --git a/test-parity/node-suite/vm/validation/code-and-options.ts b/test-parity/node-suite/vm/validation/code-and-options.ts new file mode 100644 index 0000000000..47a79c0ee9 --- /dev/null +++ b/test-parity/node-suite/vm/validation/code-and-options.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +shape("Script code", () => new vm.Script(1 as any)); +shape("Script options", () => new vm.Script("1", 1 as any)); +shape("createScript code", () => vm.createScript(null as any)); +shape("runThis code", () => vm.runInThisContext({} as any)); +shape("runNew code", () => vm.runInNewContext(undefined as any)); +shape("runContext code", () => vm.runInContext(1 as any, vm.createContext())); +shape("runContext plain", () => vm.runInContext("1", {} as any)); diff --git a/test-parity/node-suite/vm/validation/create-context.ts b/test-parity/node-suite/vm/validation/create-context.ts new file mode 100644 index 0000000000..6bdf4eae41 --- /dev/null +++ b/test-parity/node-suite/vm/validation/create-context.ts @@ -0,0 +1,23 @@ +import * as vm from "node:vm"; + +function shape(label: string, fn: () => unknown) { + try { + fn(); + console.log(label + ": ok"); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} + +for ( + const [label, value] of [["null", null], ["string", "x"], ["number", 1], [ + "boolean", + true, + ]] +) { + shape("sandbox " + label, () => vm.createContext(value as any)); +} +shape("options null", () => vm.createContext({}, null as any)); +shape("options string", () => vm.createContext({}, "x" as any)); +shape("name null", () => vm.createContext({}, { name: null as any })); +shape("origin number", () => vm.createContext({}, { origin: 1 as any })); diff --git a/test-parity/node-suite/vm/validation/is-context.ts b/test-parity/node-suite/vm/validation/is-context.ts new file mode 100644 index 0000000000..327fbb6e6e --- /dev/null +++ b/test-parity/node-suite/vm/validation/is-context.ts @@ -0,0 +1,17 @@ +import * as vm from "node:vm"; + +for ( + const [label, value] of [ + ["undefined", undefined], + ["null", null], + ["string", "x"], + ["number", 1], + ["boolean", false], + ] as const +) { + try { + console.log(label + ":", vm.isContext(value as any)); + } catch (error: any) { + console.log(label + ":", error.name, error.code || "-"); + } +} diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 922c40b052..84ee161f5e 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -5,9 +5,9 @@ "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Floors were refreshed from a clean node-26 run at 2810/2863 (98.1%), then the deterministic child_process floor was measured independently at 43/53 on Node 26.5.0. The overall summary is the sum of the committed per-module floors." }, "overall": { - "pass": 2818, - "total": 2890, - "pct": 97.5 + "pass": 2831, + "total": 2946, + "pct": 96.1 }, "modules": { "assert": { @@ -207,8 +207,8 @@ "total": 4 }, "vm": { - "pass": 8, - "total": 8 + "pass": 21, + "total": 64 }, "wasi": { "pass": 3,