Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/node_suite_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def normalize(text: str) -> str:
"http", "http2", "https", "net", "dgram", "tls", "cluster", "dns",
"stream", "child_process", "worker_threads", "inspector",
"inspector-promises", "repl", "diagnostics_channel", "timers", "fetch",
"v8",
}

tests = []
Expand Down
113 changes: 113 additions & 0 deletions test-parity/node-suite/v8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# node:v8 granular parity suite

Deterministic Node.js compatibility coverage for Perry's `node:v8` surface. The
fixtures compare semantic contracts rather than V8-internal byte streams,
volatile heap sizes, engine addresses, stacks, timestamps, or GC timing.

## Pinned primary evidence

Node 26.5.0 is the oracle. Its tag resolves to commit
[`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`](https://github.com/nodejs/node/tree/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb).
The audit used:

- [`lib/v8.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/lib/v8.js)
- [`doc/api/v8.md`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/doc/api/v8.md)
- [`test-v8-serdes.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-v8-serdes.js)
- [`test-v8-stats.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-v8-stats.js)
- [`test-v8-version-tag.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-v8-version-tag.js)
- [`test-v8-flag-type-check.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-v8-flag-type-check.js)
- [`test-v8-getheapsnapshot-twice.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-v8-getheapsnapshot-twice.js)
- the `test-promise-hook-*` and `test-v8-startup-snapshot-api.js` families at
the same commit

The cross-runtime audit used Deno 2.9.3 stable commit
[`f39575ecd50602a5b42b1ba8e93849460de9fcf4`](https://github.com/denoland/deno/blob/f39575ecd50602a5b42b1ba8e93849460de9fcf4/ext/node/polyfills/v8.ts)
and current source commit
[`803a3c933e1e23e0972445293ec0b34b8da96ccc`](https://github.com/denoland/deno/blob/803a3c933e1e23e0972445293ec0b34b8da96ccc/ext/node/polyfills/v8.ts),
plus Bun 1.3.14 stable commit
[`0d9b296af33f2b851fcbf4df3e9ec89751734ba4`](https://github.com/oven-sh/bun/blob/0d9b296af33f2b851fcbf4df3e9ec89751734ba4/src/js/node/v8.ts)
and current source commit
[`aca54d5c2b874ac304a3bbe1d67630e4daf17b43`](https://github.com/oven-sh/bun/blob/aca54d5c2b874ac304a3bbe1d67630e4daf17b43/src/js/node/v8.ts).

## Coverage (46 fixtures)

- **Exports and classes (8):** exact ESM surface, descriptors, import identity,
constructor/prototype metadata, inheritance, instance identity, and receiver
validation.
- **Structured clone helpers (12):** primitives and special numbers, sparse
arrays, objects, Map/Set identity, typed arrays, Buffer/DataView/ArrayBuffer,
Date/RegExp, Error cause, Float16Array, shared/cyclic graphs, corrupt wire
data, invalid sources, and invalid values.
- **Serializer classes (8):** headers and sequential values, raw integer/double
methods, raw Buffer/TypedArray/DataView input, release reuse, wire version,
ArrayBuffer transfer identity, validation, and every Node 26 ArrayBufferView
input family.
- **Heap diagnostics (7):** stable heap/code/space/C++ statistic shapes and
types, parsed heap-snapshot schema, two consecutive streams, isolated file
output, and cleanup.
- **Promise hooks (5):** namespace metadata, validation, stopper behavior,
parent identity, and filtered init/before/settled/after ordering.
- **GCProfiler (2):** lifecycle/report shape and `Symbol.dispose` behavior.
- **Version flags (2):** repeatable uint32 tag and isolated flag/type behavior.
- **String representation (1):** stable Latin-1/BMP/astral cases and validation.
- **Startup snapshots (1):** ordinary-process surface and
`ERR_NOT_BUILDING_SNAPSHOT` boundary.

The module runs in the sequential lane because heap snapshots are native,
memory-heavy work and `setFlagsFromString()` mutates process-global V8 state.
Every parity fixture still executes in a fresh process.

## Cross-runtime divergences

- Deno backs the serializer classes with V8 and rewrites wire version 16 to 15
for Node interoperability. It omits several modern exports and its
`startupSnapshot` callbacks are surface-only rather than Node's ordinary-mode
errors.
- Bun uses JavaScriptCore serialization, so its wire bytes intentionally do not
interoperate with Node/Deno. Most serializer lifecycle, promise-hook, flag,
and profiler APIs are stubs. Bun's heap statistics are explicitly approximate.
- Node remains the assertion oracle. These engine-design differences are
documented rather than hidden by weakening Node contracts.

## Deliberate stopping boundary

Excluded from this deterministic lane:

- exact serialized bytes or cross-engine wire compatibility;
- Node-internal host objects and private `_writeHostObject`, `_readHostObject`,
or `_getDataCloneError` overrides; these require internal bindings and are not
public `node:v8` contracts;
- pinned legacy wire blobs and unaligned legacy typed-array bytes, which test
exact engine wire compatibility rather than semantic round trips;
- exact heap sizes, addresses, space population, GC/finalization timing,
retention and `queryObjects()` counts;
- raw CPU/heap profile and coverage payloads, inspector coupling, code-cache
bytes, memory pressure, leak/stress, crash/fault injection, and concurrency;
- `setHeapSnapshotNearHeapLimit()` and other near-OOM behavior;
- startup snapshot building, which requires process flags and generated
artifacts. Ordinary-process behavior is already exhausted by the focused
startup fixture.

Heap snapshot fixtures parse only stable schema relationships, consume or
destroy streams, write inside an isolated temporary directory, and remove all
files. The valid flag fixture is process-isolated and cannot leak into another
fixture.

## Measured Perry classification

Repeated Node 26.5.0 oracle runs produced identical output for all 46 fixtures.
Repeated focused Perry differential runs produced the same **14/46** result: 14
passes, 30 output mismatches, one stable SIGSEGV (`serialize/builtins` after
printing its semantic observations), one stable timeout
(`serialize/references-cycles`), and zero compile failures. The compact runner
classifies the signal mismatch in its `diff` bucket, yielding
`pass=14, diff=31, perry_err=1`.

Validation also includes Node syntax checks for every fixture,
`deno fmt
--check`, `cargo fmt --all -- --check`, the Rust file-size gate, JSON
parsing and baseline/count invariants, release builds for the compiler/runtime
packages, `git diff --check`, and staged-scope/artifact checks.

These failures are intentional parity targets. This tests-only suite does not
change compiler, linker, runtime, or build behavior to conceal them.
32 changes: 32 additions & 0 deletions test-parity/node-suite/v8/classes/constructor-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as v8 from "node:v8";

for (
const name of [
"Serializer",
"Deserializer",
"DefaultSerializer",
"DefaultDeserializer",
"GCProfiler",
] as const
) {
try {
(v8[name] as any)();
console.log(name + ": no throw");
} catch (error: any) {
console.log(name + ":", error.name, error.code);
}
}

for (
const [label, value] of [["undefined", undefined], ["string", "bad"], [
"object",
{},
]] as const
) {
try {
new v8.Deserializer(value as any);
console.log("Deserializer " + label + ": no throw");
} catch (error: any) {
console.log("Deserializer " + label + ":", error.name, error.code);
}
}
25 changes: 25 additions & 0 deletions test-parity/node-suite/v8/classes/instances-inheritance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as v8 from "node:v8";

const serializer = new v8.DefaultSerializer();
const serialized = v8.serialize({ ok: true });
const deserializer = new v8.DefaultDeserializer(serialized);
const profiler = new v8.GCProfiler();

console.log(
"serializer:",
serializer instanceof v8.DefaultSerializer,
serializer instanceof v8.Serializer,
);
console.log(
"deserializer:",
deserializer instanceof v8.DefaultDeserializer,
deserializer instanceof v8.Deserializer,
);
console.log("profiler:", profiler instanceof v8.GCProfiler);
console.log(
"constructors:",
serializer.constructor.name,
deserializer.constructor.name,
profiler.constructor.name,
);
console.log("independent:", new v8.Serializer() !== new v8.Serializer());
36 changes: 36 additions & 0 deletions test-parity/node-suite/v8/classes/prototypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as v8 from "node:v8";

for (
const name of [
"Serializer",
"Deserializer",
"DefaultSerializer",
"DefaultDeserializer",
"GCProfiler",
] as const
) {
const Constructor = v8[name];
console.log(name, typeof Constructor, Constructor.length, Constructor.name);
console.log(
name + " prototype:",
Object.getOwnPropertyNames(Constructor.prototype).join(","),
);
const descriptor = Object.getOwnPropertyDescriptor(Constructor, "prototype")!;
console.log(
name + " prototype descriptor:",
descriptor.writable,
descriptor.enumerable,
descriptor.configurable,
);
}

console.log(
"serializer inheritance:",
Object.getPrototypeOf(v8.DefaultSerializer.prototype) ===
v8.Serializer.prototype,
);
console.log(
"deserializer inheritance:",
Object.getPrototypeOf(v8.DefaultDeserializer.prototype) ===
v8.Deserializer.prototype,
);
28 changes: 28 additions & 0 deletions test-parity/node-suite/v8/classes/receiver-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as v8 from "node:v8";

for (
const [label, fn] of [
["Serializer.writeHeader", v8.Serializer.prototype.writeHeader],
["Serializer.releaseBuffer", v8.Serializer.prototype.releaseBuffer],
["Deserializer.readHeader", v8.Deserializer.prototype.readHeader],
["GCProfiler.start", v8.GCProfiler.prototype.start],
] as const
) {
for (
const [receiverLabel, receiver] of [["undefined", undefined], [
"object",
{},
], ["null", null]] as const
) {
try {
fn.call(receiver);
console.log(label + " " + receiverLabel + ": no throw");
} catch (error: any) {
console.log(
label + " " + receiverLabel + ":",
error.name,
error.code ?? "no-code",
);
}
}
}
23 changes: 23 additions & 0 deletions test-parity/node-suite/v8/exports/descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as v8 from "node:v8";

for (
const key of [
"serialize",
"Serializer",
"promiseHooks",
"startupSnapshot",
"GCProfiler",
] as const
) {
const descriptor = Object.getOwnPropertyDescriptor(v8, key)!;
console.log(
key,
descriptor.enumerable,
descriptor.configurable,
"writable" in descriptor ? descriptor.writable : "accessor",
typeof descriptor.value,
);
}

console.log("tag:", Object.prototype.toString.call(v8));
console.log("extensible:", Object.isExtensible(v8));
18 changes: 10 additions & 8 deletions test-parity/node-suite/v8/exports/diagnostics-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
writeHeapSnapshot,
} from "node:v8";

for (const [name, value] of [
["getCppHeapStatistics", getCppHeapStatistics],
["getHeapSnapshot", getHeapSnapshot],
["isStringOneByteRepresentation", isStringOneByteRepresentation],
["queryObjects", queryObjects],
["startCpuProfile", startCpuProfile],
["writeHeapSnapshot", writeHeapSnapshot],
] as const) {
for (
const [name, value] of [
["getCppHeapStatistics", getCppHeapStatistics],
["getHeapSnapshot", getHeapSnapshot],
["isStringOneByteRepresentation", isStringOneByteRepresentation],
["queryObjects", queryObjects],
["startCpuProfile", startCpuProfile],
["writeHeapSnapshot", writeHeapSnapshot],
] as const
) {
console.log(name, typeof value, value.length, typeof (v8 as any)[name]);
}
15 changes: 15 additions & 0 deletions test-parity/node-suite/v8/exports/import-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as prefixed from "node:v8";
import legacy from "v8";
import { deserialize, serialize } from "node:v8";

console.log("module identity:", prefixed.serialize === legacy.serialize);
console.log(
"named identity:",
serialize === prefixed.serialize,
deserialize === prefixed.deserialize,
);
console.log(
"default keys:",
Object.keys(legacy).length === Object.keys(prefixed).length,
);
console.log("roundtrip:", deserialize(serialize("identity")));
33 changes: 33 additions & 0 deletions test-parity/node-suite/v8/exports/public-surface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as v8 from "node:v8";

const expected = [
"DefaultDeserializer",
"DefaultSerializer",
"Deserializer",
"GCProfiler",
"Serializer",
"cachedDataVersionTag",
"default",
"deserialize",
"getCppHeapStatistics",
"getHeapCodeStatistics",
"getHeapSnapshot",
"getHeapSpaceStatistics",
"getHeapStatistics",
"isStringOneByteRepresentation",
"promiseHooks",
"queryObjects",
"serialize",
"setFlagsFromString",
"setHeapSnapshotNearHeapLimit",
"startCpuProfile",
"startHeapProfile",
"startupSnapshot",
"stopCoverage",
"takeCoverage",
"writeHeapSnapshot",
];

console.log("keys:", Object.keys(v8).join(","));
console.log("exact:", expected.every((key, i) => Object.keys(v8)[i] === key));
console.log("count:", Object.keys(v8).length);
13 changes: 13 additions & 0 deletions test-parity/node-suite/v8/gc-profiler/dispose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { GCProfiler } from "node:v8";

const profiler = new GCProfiler();
console.log("dispose method:", typeof profiler[Symbol.dispose]);
console.log("start twice:", profiler.start(), profiler.start());
console.log("first stop report:", typeof profiler.stop() === "object");
console.log("second stop:", profiler.stop());
console.log("dispose return:", profiler[Symbol.dispose]());

const active = new GCProfiler();
active.start();
console.log("active dispose:", active[Symbol.dispose]());
console.log("stop after dispose:", active.stop());
Loading
Loading