Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions scripts/node_suite_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,22 @@
_DUR_MS = re.compile(r"(?<=: )\d+(?:\.\d+)?(ms|s)\b")
_DUR_CLOCK = re.compile(r"\b\d+:\d{2}\.\d{3} \((h:mm|m):ss\.mmm\)")
_FRAME = re.compile(r"^\s{4,}(at\s|\d+:\s)|… \d+ more identical frames")
_TEST_DURATION = re.compile(r"\(\d+(?:\.\d+)?ms\)")
_TEST_LOCATION = re.compile(r"^test at .+:\d+:\d+$")


def normalize(text: str) -> str:
out = []
for line in text.split("\n"):
if _FRAME.search(line):
continue
if _TEST_LOCATION.match(line):
line = "test at <location>"
line = _DUR_MS.sub(lambda m: "<dur>" + m.group(1), line)
line = _DUR_CLOCK.sub(lambda m: "<dur:" + m.group(1) + ">", line)
line = _TEST_DURATION.sub("(<test-duration>)", line)
if line.startswith("ℹ duration_ms "):
line = "ℹ duration_ms <test-duration>"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
out.append(line)
return "\n".join(out)

Expand Down
61 changes: 61 additions & 0 deletions test-parity/node-suite/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# `node:test` granular parity coverage

This directory compares deterministic `node:test` semantics through controlled
console markers. The differential runner still compares the test runner's own
stdout and exit status, but canonicalizes test durations and source locations;
fixtures do not assert paths, stacks, process IDs, or terminal formatting.

## Upstream selection

The expansion was selected on 2026-07-16 from these primary repository
snapshots:

- Node.js [`34c28d5a69f4f00cd599adcbe57834435d3a683b`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b), especially
[`test-runner-mocking.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-mocking.js),
[`test-runner-plan.mjs`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-plan.mjs),
[`test-runner-aftereach-runtime-skip.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-aftereach-runtime-skip.js),
[`test-runner-subtest-after-hook.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-subtest-after-hook.js), and the deterministic parts of the run and mock-timer tests.
- Deno [`f8a17c8171569fa2870d740030aaa59c91fdf9ee`](https://github.com/denoland/deno/tree/f8a17c8171569fa2870d740030aaa59c91fdf9ee). Deno's current Node-compat selection does not carry a dedicated `node:test` file under `tests/unit_node`; its runner, context, hooks, mocks, timer, snapshot, and reporter compatibility lives in
[`ext/node/polyfills/testing.ts`](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/ext/node/polyfills/testing.ts).
- Bun [`6173d6431ee8ad086bf79d1d5354080cfe937964`](https://github.com/oven-sh/bun/tree/6173d6431ee8ad086bf79d1d5354080cfe937964), especially its
[`node:test` selection](https://github.com/oven-sh/bun/blob/6173d6431ee8ad086bf79d1d5354080cfe937964/test/js/node/test_runner/node-test.test.ts) and
[hook-order fixture](https://github.com/oven-sh/bun/blob/6173d6431ee8ad086bf79d1d5354080cfe937964/test/js/node/test_runner/fixtures/02-hooks.js).

## Added diagnostic categories

- `runner/registration`: deferred registration, nested suites/subtests, and
parent-child completion.
- `runner/context` and `runner/api`: assertion surface, plans, runtime
skip/todo/only, failure propagation, names, the existing diagnostic case, and
the claimed `run()` surface.
- `runner/hooks`: global and nested ordering plus cleanup after body or setup
throws.
- `mock-fn`: successful and throwing call records, implementation queues,
reset/restore behavior, nested method restoration, property/accessor contexts,
`times`, and validation.
- `mock-timers`: boundary ordering, nested `runAll()`, and deterministic Date
`setTime()` behavior.

The existing snapshot fixtures already use fixed local files, and the existing
reporter fixture feeds synthetic events. Additional snapshot/reporter cases were
not added because the remaining upstream coverage is primarily path, stack,
duration, coverage, and process-output formatting.

## Stopping judgment

The remaining Node runner corpus is intentionally left for separate work:

- CLI discovery, watch mode, coverage, source maps, process isolation, worker
IDs, global setup, rerun state, and force-exit behavior require subprocess or
multi-file harness support rather than this in-process granular suite.
- concurrency, randomization, timeouts, abort scheduling, refed handles, and
scheduler-sensitive timer APIs are not deterministic enough for byte-for-byte
stdout comparison here.
- colors/TTY, absolute locations, stacks, durations, and reporter formatting
tied to those values are environment-specific.
- `TestContext.waitFor()`, `runOnly()`, tags, full names, signals, and custom
assertions are not listed in Perry's current `node:test` manifest; testing
those as claimed compatibility would overstate the supported surface.
- module mocking and snapshot update/CLI behavior are separate runtime and CLI
projects. Further core cases are redundant with the focused fixtures above or
depend on one of these excluded surfaces.
18 changes: 18 additions & 0 deletions test-parity/node-suite/test/mock-fn/accessor-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mock } from "node:test";

let stored = "initial";
const target = {
get value() {
return stored;
},
set value(value: string) {
stored = value;
},
};

const getter = mock.getter(target, "value", {});
const setter = mock.setter(target, "value", {});
console.log("getter options:", target.value, getter.mock.callCount());
target.value = "changed";
console.log("setter options:", stored, setter.mock.callCount());
mock.restoreAll();
33 changes: 33 additions & 0 deletions test-parity/node-suite/test/mock-fn/call-record-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { mock } from "node:test";

const expected = new Error("controlled mock failure");
const fn = mock.fn(function (this: any, value: number) {
if (value < 0) throw expected;
return this.offset + value;
});

console.log("success:", fn.call({ label: "receiver", offset: 4 }, 3));
try {
fn(-1);
} catch (error) {
console.log("caught:", error === expected);
}

const success = fn.mock.calls[0];
const failure = fn.mock.calls[1];
console.log(
"success record:",
JSON.stringify(success.arguments),
success.this.label,
success.result,
success.error === undefined,
success.target === undefined,
);
console.log(
"failure record:",
JSON.stringify(failure.arguments),
failure.result === undefined,
failure.error === expected,
typeof failure.stack,
);
mock.restoreAll();
13 changes: 13 additions & 0 deletions test-parity/node-suite/test/mock-fn/implementations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { mock } from "node:test";

const fn = mock.fn((value: number) => `original:${value}`);
fn.mock.mockImplementation((value: number) => `replacement:${value}`);
fn.mock.mockImplementationOnce((value: number) => `once-a:${value}`);
fn.mock.mockImplementationOnce((value: number) => `once-b:${value}`);

console.log("sequence:", fn(1), fn(2), fn(3));
console.log("count:", fn.mock.callCount(), fn.mock.calls.length);
fn.mock.resetCalls();
console.log("reset calls:", fn.mock.callCount(), fn(4));
fn.mock.restore();
console.log("restore:", fn(5), fn.mock.callCount());
16 changes: 16 additions & 0 deletions test-parity/node-suite/test/mock-fn/method-restore-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { mock } from "node:test";

const target = {
value: 10,
read() {
return this.value;
},
};

const first = mock.method(target, "read", () => 20);
const second = mock.method(target, "read", () => 30);
console.log("nested:", target.read(), first === second);
second.mock.restore();
console.log("restore second:", target.read(), target.read === first);
first.mock.restore();
console.log("restore first:", target.read(), (target.read as any).mock === undefined);
11 changes: 11 additions & 0 deletions test-parity/node-suite/test/mock-fn/property-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mock } from "node:test";

const target = { value: 1 };
const property = mock.property(target, "value", 2);
console.log("initial:", target.value, typeof property.mock);
property.mock.mockImplementation(3);
console.log("replacement:", target.value);
property.mock.mockImplementationOnce(4);
console.log("once:", target.value, target.value);
property.mock.restore();
console.log("restored:", target.value);
18 changes: 18 additions & 0 deletions test-parity/node-suite/test/mock-fn/times-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mock } from "node:test";

const fn = mock.fn(
() => "original",
() => "temporary",
{ times: 2 },
);
console.log("fn times:", fn(), fn(), fn(), fn.mock.callCount());

const target = {
value: 5,
read() {
return this.value;
},
};
mock.method(target, "read", () => 99, { times: 1 });
console.log("method times:", target.read(), target.read());
mock.restoreAll();
18 changes: 18 additions & 0 deletions test-parity/node-suite/test/mock-fn/tracker-reset-restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mock } from "node:test";

const fn = mock.fn(
() => "original",
() => "replacement",
);

console.log("initial:", fn());
mock.restoreAll();
console.log("after restoreAll:", fn());
fn.mock.mockImplementation(() => "again");
console.log("replaced again:", fn());
mock.reset();
console.log("after reset:", fn(), fn.mock.callCount());
fn.mock.mockImplementation(() => "post-reset");
mock.restoreAll();
console.log("disassociated:", fn());
mock.reset();
23 changes: 23 additions & 0 deletions test-parity/node-suite/test/mock-fn/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { mock } from "node:test";

function codeOf(fn: () => void): string {
try {
fn();
return "NO_THROW";
} catch (error) {
return (error as any).code ?? (error as Error).name;
}
}

console.log("fn original:", codeOf(() => mock.fn(1 as any)));
console.log("method target:", codeOf(() => mock.method(null as any, "x")));
console.log("method missing:", codeOf(() => mock.method({}, "x")));
console.log("getter missing:", codeOf(() => mock.getter({}, "x")));
console.log(
"times zero:",
codeOf(() => mock.fn(() => undefined, { times: 0 })),
);
console.log(
"times fraction:",
codeOf(() => mock.fn(() => undefined, { times: 1.5 })),
);
9 changes: 9 additions & 0 deletions test-parity/node-suite/test/mock-timers/date-set-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { mock } from "node:test";

mock.timers.enable({ apis: ["Date"], now: 1_000 });
console.log("initial:", Date.now(), new Date().toISOString());
mock.timers.setTime(2_500);
console.log("advanced:", Date.now(), new Date().toISOString());
mock.timers.setTime(500);
console.log("rewound:", Date.now(), new Date().toISOString());
mock.timers.reset();
12 changes: 12 additions & 0 deletions test-parity/node-suite/test/mock-timers/run-all-nested.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { mock } from "node:test";

mock.timers.enable({ apis: ["Date", "setTimeout"], now: 100 });
const events: string[] = [];
setTimeout(() => {
events.push(`outer:${Date.now()}`);
setTimeout(() => events.push(`inner:${Date.now()}`), 5);
}, 10);

mock.timers.runAll();
console.log("runAll:", JSON.stringify(events));
mock.timers.reset();
15 changes: 15 additions & 0 deletions test-parity/node-suite/test/mock-timers/tick-boundary-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { mock } from "node:test";

mock.timers.enable({ apis: ["setTimeout"], now: 0 });
const events: string[] = [];
setTimeout(() => events.push("late"), 10);
setTimeout(() => events.push("first-at-five"), 5);
setTimeout(() => events.push("second-at-five"), 5);

mock.timers.tick(4);
console.log("tick4:", JSON.stringify(events));
mock.timers.tick(1);
console.log("tick5:", JSON.stringify(events));
mock.timers.tick(5);
console.log("tick10:", JSON.stringify(events));
mock.timers.reset();
8 changes: 8 additions & 0 deletions test-parity/node-suite/test/runner/api/run-empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { run } from "node:test";

console.log("run:before");
let events = 0;
for await (const _event of run({ files: [] })) {
events++;
}
console.log("run:after", events);
12 changes: 12 additions & 0 deletions test-parity/node-suite/test/runner/context/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import test from "node:test";

test("context assertions", (t) => {
console.log(
"assertions:",
["ok", "strictEqual", "deepStrictEqual", "throws"]
.map((name) => `${name}:${typeof (t.assert as any)[name]}`)
.join(","),
);
t.assert.strictEqual(2 + 2, 4);
console.log("assertions:passed");
});
10 changes: 10 additions & 0 deletions test-parity/node-suite/test/runner/context/failure-propagation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import test from "node:test";

test("parent observes child failure", async (t) => {
console.log("parent:before-failure");
await t.test("failing child", () => {
console.log("child:throwing");
throw new Error("controlled child failure");
});
console.log("parent:after-failure");
});
8 changes: 8 additions & 0 deletions test-parity/node-suite/test/runner/context/metadata-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import test from "node:test";

test("named parent", async (t) => {
console.log("context name:", t.name);
await t.test("named child", (child) => {
console.log("child context name:", child.name);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import test from "node:test";

test("regular sibling", () => {
console.log("only:regular");
});

test.only("marked only", () => {
console.log("only:marked");
});
10 changes: 10 additions & 0 deletions test-parity/node-suite/test/runner/context/plan-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import test from "node:test";

test("matched assertion plan", (t) => {
t.plan(1);
t.assert.fileSnapshot(
{ ok: true },
"test-parity/node-suite/test/snapshots/file-object.json",
);
console.log("plan:matched-one");
});
6 changes: 6 additions & 0 deletions test-parity/node-suite/test/runner/context/plan-mismatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import test from "node:test";

test("unmet assertion plan", (t) => {
t.plan(1);
console.log("plan:expected-one");
});
11 changes: 11 additions & 0 deletions test-parity/node-suite/test/runner/context/plan-overrun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import test from "node:test";

test("exceeded assertion plan", (t) => {
t.plan(0);
console.log("plan:before-extra");
t.assert.fileSnapshot(
{ ok: true },
"test-parity/node-suite/test/snapshots/file-object.json",
);
console.log("plan:after-extra");
});
6 changes: 6 additions & 0 deletions test-parity/node-suite/test/runner/context/plan-zero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import test from "node:test";

test("zero assertion plan", (t) => {
t.plan(0);
console.log("plan:zero");
});
13 changes: 13 additions & 0 deletions test-parity/node-suite/test/runner/context/runtime-directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import test from "node:test";

test("runtime skip", (t) => {
console.log("runtime-skip:before");
t.skip("not applicable");
console.log("runtime-skip:after");
});

test("runtime todo", (t) => {
console.log("runtime-todo:before");
t.todo("not implemented");
console.log("runtime-todo:after");
});
10 changes: 10 additions & 0 deletions test-parity/node-suite/test/runner/hooks/after-each-after-throw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { afterEach, test } from "node:test";

afterEach(() => {
console.log("hook:afterEach-cleanup");
});

test("body throws", () => {
console.log("body:before-throw");
throw new Error("controlled body failure");
});
Loading
Loading