Skip to content
Draft
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
2 changes: 1 addition & 1 deletion PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h
| `test_dataview` | Not ported | Medium |
| `test_date` | Ported ✅ | Easy |
| `test_error` | Ported ✅ | Medium |
| `test_exception` | Not ported | Medium |
| `test_exception` | Ported ✅ | Medium |
| `test_finalizer` | Not ported | Medium |
| `test_function` | Not ported | Medium |
| `test_general` | Not ported | Hard |
Expand Down
38 changes: 23 additions & 15 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default defineConfig([
globalIgnores(["**/CMakeFiles/**"]),
eslint.configs.recommended,
tseslint.configs.recommended,
{
files: [
"tests/**/*.js",
],
files: ["tests/**/*.{mjs,js}"],
languageOptions: {
// Only allow ECMAScript built-ins and CTS harness globals.
// This causes no-undef to flag any runtime-specific API (setTimeout, process, Buffer, etc.).
Expand All @@ -22,27 +20,37 @@ export default defineConfig([
mustCall: "readonly",
mustNotCall: "readonly",
gcUntil: "readonly",
spawnTest: "readonly",
experimentalFeatures: "readonly",
napiVersion: "readonly",
skipTest: "readonly",
},
},
rules: {
"no-undef": "error",
"no-restricted-imports": ["error", {
patterns: ["*"],
}],
"no-restricted-syntax": ["error",
{ selector: "MemberExpression[object.name='globalThis']", message: "Avoid globalThis access in test files — use CTS harness globals instead" },
{ selector: "MemberExpression[object.name='global']", message: "Avoid global access in test files — use CTS harness globals instead" }
"no-restricted-imports": [
"error",
{
patterns: ["*"],
},
],
"no-restricted-syntax": [
"error",
{
selector: "MemberExpression[object.name='globalThis']",
message:
"Avoid globalThis access in test files — use CTS harness globals instead",
},
{
selector: "MemberExpression[object.name='global']",
message:
"Avoid global access in test files — use CTS harness globals instead",
},
],
},
},
{
files: [
"implementors/**/*.{js,ts}",
"scripts/**/*.{js,mjs}",
],
files: ["implementors/**/*.{js,ts}", "scripts/**/*.{js,mjs}"],
languageOptions: {
globals: {
...globals.es2025,
Expand Down
26 changes: 12 additions & 14 deletions implementors/node/assert.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@

import {
ok,
strictEqual,
notStrictEqual,
deepStrictEqual,
throws,
match,
} from "node:assert/strict";

const assert = Object.assign(
(value, message) => ok(value, message),
{
ok: (value, message) => ok(value, message),
strictEqual: (actual, expected, message) =>
strictEqual(actual, expected, message),
notStrictEqual: (actual, expected, message) =>
notStrictEqual(actual, expected, message),
deepStrictEqual: (actual, expected, message) =>
deepStrictEqual(actual, expected, message),
throws: (fn, error, message) => throws(fn, error, message),
},
);
const assert = Object.assign((value, message) => ok(value, message), {
ok: (value, message) => ok(value, message),
strictEqual: (actual, expected, message) =>
strictEqual(actual, expected, message),
notStrictEqual: (actual, expected, message) =>
notStrictEqual(actual, expected, message),
deepStrictEqual: (actual, expected, message) =>
deepStrictEqual(actual, expected, message),
throws: (fn, error, message) => throws(fn, error, message),
match: (string, regex, message) => match(string, regex, message),
});

Object.assign(globalThis, { assert });
16 changes: 16 additions & 0 deletions implementors/node/child_process.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface SpawnTestOptions {
cwd?: string;
nodeFlags?: string[];
}

export interface SpawnTestResult {
status: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}

export function spawnTest(
filePath: string,
options?: SpawnTestOptions
): SpawnTestResult;
49 changes: 49 additions & 0 deletions implementors/node/child_process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { spawnSync } from "node:child_process";
import path from "node:path";

const HARNESS_MODULE_PATHS = [
"features.js",
"assert.js",
"load-addon.js",
"gc.js",
"must-call.js",
"child_process.js",
].map((file) => path.join(import.meta.dirname, file));

/**
* Runs a test file in a fresh Node.js subprocess with the CTS harness globals
* pre-loaded, and returns its exit status, signal, and captured output.
*
* @param {string} filePath - Path to the JS/MJS file to execute. Resolved
* against `options.cwd` if relative.
* @param {{ cwd?: string, nodeFlags?: string[] }} [options]
* - `cwd`: working directory for the child; defaults to `process.cwd()`.
* - `nodeFlags`: CLI flags passed to `node` before the `--import` chain
* (e.g., `["--expose-gc"]`). Defaults to no flags so each caller declares
* what its child needs.
* @returns {{ status: number | null, signal: NodeJS.Signals | null, stdout: string, stderr: string }}
*/
export const spawnTest = (filePath, options = {}) => {
const args = [...(options.nodeFlags ?? [])];
for (const modulePath of HARNESS_MODULE_PATHS) {
args.push("--import", "file://" + modulePath);
}
args.push(filePath);

const result = spawnSync(process.execPath, args, {
cwd: options.cwd ?? process.cwd(),
maxBuffer: 100 * 1024 * 1024,
});
if (result.error) throw result.error;
return {
status: result.status,
signal: result.signal,
stderr: result.stderr?.toString() ?? "",
stdout: result.stdout?.toString() ?? "",
};
};

// This module is loaded in both contexts: imported by the parent test runner
// (tests.ts) and `--import`ed into every spawned child. The side effect below
// installs `spawnTest` on the child's globalThis so tests can call it directly.
Object.assign(globalThis, { spawnTest });
112 changes: 24 additions & 88 deletions implementors/node/tests.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
import assert from "node:assert";
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

import { spawnTest } from "./child_process.js";

assert(
typeof import.meta.dirname === "string",
"Expecting a recent Node.js runtime API version"
"Expecting a recent Node.js runtime API version",
);

const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
const TESTS_ROOT_PATH = path.join(ROOT_PATH, "tests");
const FEATURES_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"features.js"
);
const ASSERT_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"assert.js"
);
const LOAD_ADDON_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"load-addon.js"
);
const GC_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"gc.js"
);
const MUST_CALL_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"must-call.js"
);

export function listDirectoryEntries(dir: string) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
Expand All @@ -60,61 +31,26 @@ export function listDirectoryEntries(dir: string) {
return { directories, files };
}

export function runFileInSubprocess(
cwd: string,
filePath: string
): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[
// Using file scheme prefix when to enable imports on Windows
"--expose-gc",
"--import",
"file://" + FEATURES_MODULE_PATH,
"--import",
"file://" + ASSERT_MODULE_PATH,
"--import",
"file://" + LOAD_ADDON_MODULE_PATH,
"--import",
"file://" + GC_MODULE_PATH,
"--import",
"file://" + MUST_CALL_MODULE_PATH,
filePath,
],
{ cwd }
);

let stderrOutput = "";
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
stderrOutput += chunk;
});

child.stdout.pipe(process.stdout);

child.on("error", reject);

child.on("close", (code, signal) => {
if (code === 0) {
resolve();
return;
}

const reason =
code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
const trimmedStderr = stderrOutput.trim();
const stderrSuffix = trimmedStderr
? `\n--- stderr ---\n${trimmedStderr}\n--- end stderr ---`
: "";
reject(
new Error(
`Test file ${path.relative(
TESTS_ROOT_PATH,
filePath
)} failed (${reason})${stderrSuffix}`
)
);
});
export function runFileInSubprocess(cwd: string, filePath: string): void {
const { status, signal, stdout, stderr } = spawnTest(filePath, {
cwd,
nodeFlags: ["--expose-gc"],
});

if (stdout) process.stdout.write(stdout);

if (status === 0) return;

const reason =
status !== null ? `exit code ${status}` : `signal ${signal ?? "unknown"}`;
const trimmedStderr = stderr.trim();
const stderrSuffix = trimmedStderr
? `\n--- stderr ---\n${trimmedStderr}\n--- end stderr ---`
: "";
throw new Error(
`Test file ${path.relative(
TESTS_ROOT_PATH,
path.join(cwd, filePath),
)} failed (${reason})${stderrSuffix}`,
);
}
1 change: 1 addition & 0 deletions tests/js-native-api/test_exception/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_node_api_cts_addon(test_exception test_exception.c)
Loading
Loading