Skip to content
Merged
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 .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Node-API Conformance Test Suite: A pure ECMAScript test suite for Node-API imple
## General Principles
- **Keep it minimal**: Avoid unnecessary dependencies, configuration, or complexity
- **Pure ECMAScript tests**: To lower the barrier for implementors to run the tests, all tests are written as single files of pure ECMAScript, with no import / export statements and no use of Node.js runtime APIs outside of the language standard (such as `process`, `require`, `node:*` modules).
- **TypeScript tooling**: The tooling around the tests are written in TypeScript and expects to be ran in a Node.js compatible runtime with type-stripping enabled.
- **TypeScript tooling**: The tooling around the tests are written in TypeScript and expects to be ran in a Node.js compatible runtime with type-stripping enabled. Never rely on `ts-node`, `node --loader ts-node/esm`, or `--experimental-strip-types`; use only stable, built-in Node.js capabilities.
- **Implementor Flexibility**: Native code building and loading is delegated to implementors, with the test-suite providing defaults that work for Node.js.
- **Extra convenience**: Extra (generated) code is provided to make it easier for implementors to load and run tests, such as extra package exports exposing test functions that implementors can integrate with their preferred test frameworks.
- **Process Isolation**: The built-in runner for Node.js, run each test in isolation to prevent crashes from aborting entire test suite.
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test Node.js implementation

on: [push, pull_request]

jobs:
test:
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version:
- 20.x
- 22.x
- 24.x
- 25.x
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ matrix.node-version }}
- name: Check Node.js installation
run: |
node --version
npm --version
- name: Install dependencies
run: npm ci
- name: npm test
run: npm run node:test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ Written in ECMAScript & C/C++ with an implementor customizable harness.

## Overview

The tests are divided into two buckets, based on the two header files declaring the Node-API functions:
### Tests

The tests are divided into three buckets:

- `tests/harness/*` exercising the implementor's test harness.

and two parts based on the two header files declaring the Node-API functions:

- `tests/js-native-api/*` testing the engine-specific part of Node-API defined in the [`js_native_api.h`](https://github.com/nodejs/node-api-headers/blob/main/include/js_native_api.h) header.
- `tests/node-api/*` testing the runtime-specific part of Node-API defined in the [`node_api.h`](https://github.com/nodejs/node-api-headers/blob/main/include/node_api.h) header.

### Implementors

This repository offers an opportunity for implementors of Node-API to maintain (parts of) their implementor-specific harness inside this repository, in a sub-directory of the `implementors` directory. We do this in hope of increased velocity from faster iteration and potentials for reuse of code across the harnesses.

We maintain a list of other runtimes implementing Node-API in [doc/node-api-engine-bindings.md](https://github.com/nodejs/abi-stable-node/blob/doc/node-api-engine-bindings.md#node-api-bindings-in-other-runtimes) of the `nodejs/abi-stable-node` repository.
8 changes: 8 additions & 0 deletions implementors/node/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import { ok } from "node:assert/strict";

const assert = (value, message) => {
ok(value, message);
};

Object.assign(globalThis, { assert });
104 changes: 104 additions & 0 deletions implementors/node/run-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { spawn } from "node:child_process";
import { promises as fs } from "node:fs";
import path from "node:path";
import { test, type TestContext } from "node:test";

const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
const TESTS_ROOT_PATH = path.join(ROOT_PATH, "tests");
const ASSERT_MODULE_PATH = path.join(
ROOT_PATH,
"implementors",
"node",
"assert.js"
);

async function listDirectoryEntries(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const directories: string[] = [];
const files: string[] = [];

for (const entry of entries) {
if (entry.isDirectory()) {
directories.push(entry.name);
} else if (entry.isFile() && entry.name.endsWith(".js")) {
files.push(entry.name);
}
}

directories.sort();
files.sort();

return { directories, files };
}

function runFileInSubprocess(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [
"--import",
ASSERT_MODULE_PATH,
filePath,
]);

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}`
)
);
});
});
}

async function populateSuite(
testContext: TestContext,
dir: string
): Promise<void> {
const { directories, files } = await listDirectoryEntries(dir);

for (const file of files) {
const filePath = path.join(dir, file);
await testContext.test(file, () => runFileInSubprocess(filePath));
}

for (const directory of directories) {
await testContext.test(directory, async (subTest) => {
await populateSuite(subTest, path.join(dir, directory));
});
}
}

test("harness", async (t) => {
await populateSuite(t, path.join(TESTS_ROOT_PATH, "harness"));
});

test("js-native-api", async (t) => {
await populateSuite(t, path.join(TESTS_ROOT_PATH, "js-native-api"));
});

test("node-api", async (t) => {
await populateSuite(t, path.join(TESTS_ROOT_PATH, "node-api"));
});
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "node-api-cts",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"node:test": "node --test ./implementors/node/run-tests.ts"
},
"devDependencies": {
"@types/node": "^24.10.1"
}
}
33 changes: 33 additions & 0 deletions tests/harness/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
if (typeof assert !== 'function') {
throw new Error('Expected a global assert function');
}

try {
assert(true, 'assert(true, message) should not throw');
} catch (error) {
throw new Error(`Global assert(true, message) must not throw: ${String(error)}`);
}

const failureMessage = 'assert(false, message) should throw this message';
let threw = false;

try {
assert(false, failureMessage);
} catch (error) {
threw = true;

if (!(error instanceof Error)) {
throw new Error(`Global assert(false, message) must throw an Error instance but got: ${String(error)}`);
}

const actualMessage = error.message;
if (actualMessage !== failureMessage) {
throw new Error(
`Global assert(false, message) must throw message "${failureMessage}" but got "${actualMessage}"`,
);
}
}

if (!threw) {
throw new Error('Global assert(false, message) must throw');
}
Empty file added tests/js-native-api/tumbleweed
Empty file.
Empty file added tests/node-api/tumbleweed
Empty file.