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
22 changes: 12 additions & 10 deletions src/js/node/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,12 @@ async function runFiles(opts: ReturnType<typeof validateRunOptions>, reporter: T
let i = 0;
for (; i < files.length; i++) {
if (opts.signal?.aborted) break;
await runOneFile(files[i], opts, reporter, counts);
await runOneFile(files[i], i + 1, opts, reporter, counts);
}
// Node cancels each not-yet-started FileTest with cancelledByParent rather
// than silently dropping it; an aborted run must not report success:true.
for (; i < files.length; i++) {
reportCancelledFile(files[i], opts, reporter, counts);
reportCancelledFile(files[i], i + 1, opts, reporter, counts);
}

reporter.plan({ __proto__: null, nesting: 0, count: counts.topLevel });
Expand All @@ -408,6 +408,7 @@ async function runFiles(opts: ReturnType<typeof validateRunOptions>, reporter: T

function reportCancelledFile(
file: string,
ordinal: number,
opts: ReturnType<typeof validateRunOptions>,
reporter: TestsStream,
counts: Record<string, number>,
Expand All @@ -418,7 +419,7 @@ function reportCancelledFile(
nesting: 0,
name: file,
type: "test",
testId: 1,
testId: ordinal,
parentId: 0,
tags: [],
line: 1,
Expand All @@ -433,17 +434,18 @@ function reportCancelledFile(
__proto__: null,
...fileNode,
type: undefined,
testNumber: 1,
testNumber: ordinal,
details: { ...details, passed: false },
});
reporter.fail({ __proto__: null, ...fileNode, type: undefined, testNumber: 1, details });
reporter.fail({ __proto__: null, ...fileNode, type: undefined, testNumber: ordinal, details });
counts.tests++;
counts.cancelled++;
counts.topLevel++;
}

async function runOneFile(
file: string,
ordinal: number,
opts: ReturnType<typeof validateRunOptions>,
reporter: TestsStream,
counts: Record<string, number>,
Expand All @@ -463,7 +465,7 @@ async function runOneFile(
nesting: 0,
name: file,
type: "test",
testId: 1,
testId: ordinal,
parentId: 0,
tags: [],
line: 1,
Expand Down Expand Up @@ -584,7 +586,7 @@ async function runOneFile(
__proto__: null,
...fileNode,
type: undefined,
testNumber: 1,
testNumber: ordinal,
details: {
__proto__: null,
duration_ms: fileDuration,
Expand All @@ -595,9 +597,9 @@ async function runOneFile(
});
const details = { __proto__: null, duration_ms: fileDuration, type: "test", error };
if (fileFailed) {
reporter.fail({ __proto__: null, ...fileNode, type: undefined, testNumber: 1, details });
reporter.fail({ __proto__: null, ...fileNode, type: undefined, testNumber: ordinal, details });
} else {
reporter.pass({ __proto__: null, ...fileNode, type: undefined, testNumber: 1, details });
reporter.pass({ __proto__: null, ...fileNode, type: undefined, testNumber: ordinal, details });
}
}
addRunCounts(counts, fileCounts);
Expand All @@ -618,6 +620,7 @@ function rebuildError(serialized: any, depth = 0): Error {
return error;
}

// `nesting` is forwarded as-is: the file node is not the parent of the file's tests.
function republishChildEvent(
event: { type: string; data: any },
file: string,
Expand All @@ -627,7 +630,6 @@ function republishChildEvent(
const { type, data } = event;
Object.setPrototypeOf(data, null);
data.file = file;
data.nesting = (data.nesting ?? 0) + 1;
if (type === "test:pass" || type === "test:fail") {
const isSuite = data.type === "suite";
// node counts a suite in `suites` and stops there: a skipped or todo suite
Expand Down
93 changes: 92 additions & 1 deletion test/js/node/test_runner/node-test.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "node:path";

describe("node:test", () => {
Expand Down Expand Up @@ -344,6 +344,97 @@ async function runTests(filenames: string[], env: Record<string, string> = {}, a
return { exitCode, stdout, stderr };
}

describe("node:test run()", () => {
// Expected values are what node v26.3.0 reports for the same files.
// run() spawns one debug+ASAN `bun test` child per file, sequentially, hence
// the same headroom as the multi-file tests above.
test.concurrent(
"republishes a file's events at the child's nesting and numbers the file node by its position in the run",
async () => {
using dir = tempDir("node-test-run-nesting", {
"nested.js": `
const { test } = require("node:test");
test("top", async t => {
await t.test("sub", () => {});
});
test("failing", () => {
throw new Error("boom");
});
test("skipped", { skip: true }, () => {});
`,
"empty.js": "",
"driver.mjs": `
import { run } from "node:test";

const verdicts = [];
for await (const { type, data } of run({ files: ["nested.js", "empty.js"] })) {
if (type === "test:pass" || type === "test:fail") {
const { name, nesting, testNumber, testId } = data;
verdicts.push({ type, name, nesting, testNumber, testId });
}
}
console.log(JSON.stringify(verdicts));
`,
});

await using proc = spawn({
cmd: [bunExe(), "driver.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const verdicts = JSON.parse(stdout);

// A file's top-level tests are nesting 0 under run(), exactly as when the
// file runs on its own; only a file with nothing to report (empty.js) is
// reported through its own file-level node.
expect(verdicts.map(({ type, name, nesting }) => [type, name, nesting])).toEqual([
["test:pass", "sub", 1],
["test:pass", "top", 0],
["test:fail", "failing", 0],
["test:pass", "skipped", 0],
["test:pass", "empty.js", 0],
]);
// The file node is the run's second file, not file number 1 every time.
expect(verdicts.at(-1)).toEqual({ type: "test:pass", name: "empty.js", nesting: 0, testNumber: 2, testId: 2 });
},
30_000,
);

test.concurrent("numbers the file nodes of an aborted run by their position too", async () => {
// Nothing is spawned: the run is aborted before its first file starts, so
// every file is reported as cancelled (the files need not even exist).
await using proc = spawn({
cmd: [
bunExe(),
"-e",
`
const { run } = require("node:test");
const verdicts = [];
for await (const { type, data } of run({ files: ["a.js", "b.js"], signal: AbortSignal.abort() })) {
if (type === "test:fail") verdicts.push([data.name, data.testNumber, data.testId]);
}
console.log(JSON.stringify(verdicts));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
expect(JSON.parse(stdout)).toEqual([
["a.js", 1, 1],
["b.js", 2, 2],
]);
});
});

describe("node:test mock", () => {
const { mock } = require("node:test");

Expand Down
Loading