Skip to content

Commit 1b51488

Browse files
authored
Harness scaffold and Node.js implementor (#11)
1 parent b5f94ae commit 1b51488

File tree

11 files changed

+238
-2
lines changed

11 files changed

+238
-2
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Node-API Conformance Test Suite: A pure ECMAScript test suite for Node-API imple
77
## General Principles
88
- **Keep it minimal**: Avoid unnecessary dependencies, configuration, or complexity
99
- **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).
10-
- **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.
10+
- **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.
1111
- **Implementor Flexibility**: Native code building and loading is delegated to implementors, with the test-suite providing defaults that work for Node.js.
1212
- **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.
1313
- **Process Isolation**: The built-in runner for Node.js, run each test in isolation to prevent crashes from aborting entire test suite.

.github/workflows/test.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Test Node.js implementation
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
test:
7+
timeout-minutes: 10
8+
strategy:
9+
fail-fast: false
10+
matrix:
11+
node-version:
12+
- 20.x
13+
- 22.x
14+
- 24.x
15+
- 25.x
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Harden Runner
19+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
20+
with:
21+
egress-policy: audit
22+
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
25+
with:
26+
node-version: ${{ matrix.node-version }}
27+
- name: Check Node.js installation
28+
run: |
29+
node --version
30+
npm --version
31+
- name: Install dependencies
32+
run: npm ci
33+
- name: npm test
34+
run: npm run node:test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ Written in ECMAScript & C/C++ with an implementor customizable harness.
88
99
## Overview
1010

11-
The tests are divided into two buckets, based on the two header files declaring the Node-API functions:
11+
### Tests
12+
13+
The tests are divided into three buckets:
14+
15+
- `tests/harness/*` exercising the implementor's test harness.
16+
17+
and two parts based on the two header files declaring the Node-API functions:
1218

1319
- `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.
1420
- `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.
21+
22+
### Implementors
23+
24+
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.
25+
26+
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.

implementors/node/assert.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
import { ok } from "node:assert/strict";
3+
4+
const assert = (value, message) => {
5+
ok(value, message);
6+
};
7+
8+
Object.assign(globalThis, { assert });

implementors/node/run-tests.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { spawn } from "node:child_process";
2+
import { promises as fs } from "node:fs";
3+
import path from "node:path";
4+
import { test, type TestContext } from "node:test";
5+
6+
const ROOT_PATH = path.resolve(import.meta.dirname, "..", "..");
7+
const TESTS_ROOT_PATH = path.join(ROOT_PATH, "tests");
8+
const ASSERT_MODULE_PATH = path.join(
9+
ROOT_PATH,
10+
"implementors",
11+
"node",
12+
"assert.js"
13+
);
14+
15+
async function listDirectoryEntries(dir: string) {
16+
const entries = await fs.readdir(dir, { withFileTypes: true });
17+
const directories: string[] = [];
18+
const files: string[] = [];
19+
20+
for (const entry of entries) {
21+
if (entry.isDirectory()) {
22+
directories.push(entry.name);
23+
} else if (entry.isFile() && entry.name.endsWith(".js")) {
24+
files.push(entry.name);
25+
}
26+
}
27+
28+
directories.sort();
29+
files.sort();
30+
31+
return { directories, files };
32+
}
33+
34+
function runFileInSubprocess(filePath: string): Promise<void> {
35+
return new Promise((resolve, reject) => {
36+
const child = spawn(process.execPath, [
37+
"--import",
38+
ASSERT_MODULE_PATH,
39+
filePath,
40+
]);
41+
42+
let stderrOutput = "";
43+
child.stderr.setEncoding("utf8");
44+
child.stderr.on("data", (chunk) => {
45+
stderrOutput += chunk;
46+
});
47+
48+
child.stdout.pipe(process.stdout);
49+
50+
child.on("error", reject);
51+
52+
child.on("close", (code, signal) => {
53+
if (code === 0) {
54+
resolve();
55+
return;
56+
}
57+
58+
const reason =
59+
code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
60+
const trimmedStderr = stderrOutput.trim();
61+
const stderrSuffix = trimmedStderr
62+
? `\n--- stderr ---\n${trimmedStderr}\n--- end stderr ---`
63+
: "";
64+
reject(
65+
new Error(
66+
`Test file ${path.relative(
67+
TESTS_ROOT_PATH,
68+
filePath
69+
)} failed (${reason})${stderrSuffix}`
70+
)
71+
);
72+
});
73+
});
74+
}
75+
76+
async function populateSuite(
77+
testContext: TestContext,
78+
dir: string
79+
): Promise<void> {
80+
const { directories, files } = await listDirectoryEntries(dir);
81+
82+
for (const file of files) {
83+
const filePath = path.join(dir, file);
84+
await testContext.test(file, () => runFileInSubprocess(filePath));
85+
}
86+
87+
for (const directory of directories) {
88+
await testContext.test(directory, async (subTest) => {
89+
await populateSuite(subTest, path.join(dir, directory));
90+
});
91+
}
92+
}
93+
94+
test("harness", async (t) => {
95+
await populateSuite(t, path.join(TESTS_ROOT_PATH, "harness"));
96+
});
97+
98+
test("js-native-api", async (t) => {
99+
await populateSuite(t, path.join(TESTS_ROOT_PATH, "js-native-api"));
100+
});
101+
102+
test("node-api", async (t) => {
103+
await populateSuite(t, path.join(TESTS_ROOT_PATH, "node-api"));
104+
});

package-lock.json

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "node-api-cts",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"node:test": "node --test ./implementors/node/run-tests.ts"
8+
},
9+
"devDependencies": {
10+
"@types/node": "^24.10.1"
11+
}
12+
}

tests/harness/assert.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
if (typeof assert !== 'function') {
2+
throw new Error('Expected a global assert function');
3+
}
4+
5+
try {
6+
assert(true, 'assert(true, message) should not throw');
7+
} catch (error) {
8+
throw new Error(`Global assert(true, message) must not throw: ${String(error)}`);
9+
}
10+
11+
const failureMessage = 'assert(false, message) should throw this message';
12+
let threw = false;
13+
14+
try {
15+
assert(false, failureMessage);
16+
} catch (error) {
17+
threw = true;
18+
19+
if (!(error instanceof Error)) {
20+
throw new Error(`Global assert(false, message) must throw an Error instance but got: ${String(error)}`);
21+
}
22+
23+
const actualMessage = error.message;
24+
if (actualMessage !== failureMessage) {
25+
throw new Error(
26+
`Global assert(false, message) must throw message "${failureMessage}" but got "${actualMessage}"`,
27+
);
28+
}
29+
}
30+
31+
if (!threw) {
32+
throw new Error('Global assert(false, message) must throw');
33+
}

tests/js-native-api/tumbleweed

Whitespace-only changes.

0 commit comments

Comments
 (0)