Skip to content

Commit a2a064c

Browse files
authored
test: split FFI call and callback coverage
Isolate each callback abort scenario so timeouts identify the failing case. Consolidate callback GC coverage in the weakref test and disable core dumps for intentional aborts on POSIX. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: #66287 Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent 5137638 commit a2a064c

10 files changed

Lines changed: 217 additions & 246 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const { spawnSync } = require('node:child_process');
5+
6+
function spawnAbortingChild(source) {
7+
const args = ['-e', source];
8+
if (common.isWindows) {
9+
return spawnSync(process.execPath, args, { encoding: 'utf8' });
10+
}
11+
12+
// Avoid writing core files for these intentional aborts.
13+
return spawnSync('/bin/sh', [
14+
'-c', 'ulimit -c 0 && exec "$@"',
15+
'sh', process.execPath, ...args,
16+
], { encoding: 'utf8' });
17+
}
18+
19+
function assertAborts(source, message) {
20+
const { stderr, status, signal } = spawnAbortingChild(source);
21+
assert.ok(common.nodeProcessAborted(status, signal),
22+
`status: ${status}, signal: ${signal}
23+
stderr: ${stderr}`);
24+
assert.match(stderr, message);
25+
}
26+
27+
function assertCallbackAborts(callbackBody, message) {
28+
assertAborts(
29+
`'use strict';
30+
const ffi = require('node:ffi');
31+
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
32+
const { lib, functions } = ffi.dlopen(libraryPath, fixtureSymbols);
33+
const callback = lib.registerCallback(
34+
{ arguments: ['i32'], return: 'i32' },
35+
() => { ${callbackBody} },
36+
);
37+
functions.call_int_callback(callback, 21);`,
38+
message,
39+
);
40+
}
41+
42+
module.exports = {
43+
assertAborts,
44+
assertCallbackAborts,
45+
};

‎test/ffi/ffi.status‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ prefix ffi
44

55
[$system==solaris] # Also applies to SmartOS
66
# Bundled libffi callbacks crash on SmartOS.
7-
test-ffi-calls: SKIP
7+
test-ffi-callback*: SKIP
88
test-ffi-shared-buffer: SKIP
99
test-ffi-weakref-calls: SKIP
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const { test } = require('node:test');
5+
const { assertAborts } = require('./ffi-callback-test-common');
6+
7+
test('ffi aborts on cross-thread callback invocation', () => {
8+
const workerSource = `
9+
const { workerData } = require('node:worker_threads');
10+
const ffi = require('node:ffi');
11+
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
12+
const { functions } = ffi.dlopen(libraryPath, fixtureSymbols);
13+
functions.call_int_callback(workerData, 21);
14+
`;
15+
assertAborts(
16+
`'use strict';
17+
const { Worker } = require('node:worker_threads');
18+
const ffi = require('node:ffi');
19+
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
20+
const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
21+
const callback = lib.registerCallback(
22+
{ arguments: ['i32'], return: 'i32' },
23+
(value) => value * 2,
24+
);
25+
new Worker(${JSON.stringify(workerSource)}, { eval: true, workerData: callback });`,
26+
/Callbacks can only be invoked on the system thread they were created on/,
27+
);
28+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const { test } = require('node:test');
5+
const { assertCallbackAborts } = require('./ffi-callback-test-common');
6+
7+
test('ffi aborts on fractional callback return values', () => {
8+
assertCallbackAborts('return 1.5;', /Callback returned invalid value for declared FFI type/);
9+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const { test } = require('node:test');
5+
const { assertCallbackAborts } = require('./ffi-callback-test-common');
6+
7+
test('ffi aborts on out-of-range callback return values', () => {
8+
assertCallbackAborts('return 2 ** 40;', /Callback returned invalid value for declared FFI type/);
9+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const { test } = require('node:test');
5+
const { assertCallbackAborts } = require('./ffi-callback-test-common');
6+
7+
test('ffi aborts when a callback returns a promise', () => {
8+
assertCallbackAborts('return Promise.resolve(1);', /Callbacks cannot return promises/);
9+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const { test } = require('node:test');
5+
const { assertCallbackAborts } = require('./ffi-callback-test-common');
6+
7+
test('ffi aborts when a callback throws', () => {
8+
assertCallbackAborts('throw new Error("boom");', /Callbacks cannot throw an exception/);
9+
});

‎test/ffi/test-ffi-callbacks.js‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfFFIMissing();
4+
const assert = require('node:assert');
5+
const { test } = require('node:test');
6+
const ffi = require('node:ffi');
7+
const { cString, fixtureSymbols, libraryPath } = require('./ffi-test-common');
8+
9+
function getLibrary() {
10+
return ffi.dlopen(libraryPath, fixtureSymbols);
11+
}
12+
13+
test('ffi callbacks can be registered and invoked', () => {
14+
const { lib, functions: symbols } = getLibrary();
15+
const seen = [];
16+
const intCallback = lib.registerCallback(
17+
{ arguments: ['i32'], return: 'i32' },
18+
(value) => value * 2,
19+
);
20+
const stringCallback = lib.registerCallback(
21+
{ arguments: ['pointer'], return: 'void' },
22+
(ptr) => seen.push(ffi.toString(ptr)),
23+
);
24+
const binaryCallback = lib.registerCallback(
25+
{ arguments: ['i32', 'i32'], return: 'i32' },
26+
(a, b) => a + b,
27+
);
28+
29+
try {
30+
assert.strictEqual(symbols.call_int_callback(intCallback, 21), 42);
31+
symbols.call_string_callback(stringCallback, cString('hello callback'));
32+
assert.deepStrictEqual(seen, ['hello callback']);
33+
assert.strictEqual(symbols.call_binary_int_callback(binaryCallback, 19, 23), 42);
34+
35+
const nullPointerCallback = lib.registerCallback({ return: 'pointer' }, () => null);
36+
const undefinedPointerCallback = lib.registerCallback({ return: 'pointer' }, () => undefined);
37+
try {
38+
assert.strictEqual(symbols.call_pointer_callback_is_null(nullPointerCallback), 1);
39+
assert.strictEqual(symbols.call_pointer_callback_is_null(undefinedPointerCallback), 1);
40+
} finally {
41+
lib.unregisterCallback(nullPointerCallback);
42+
lib.unregisterCallback(undefinedPointerCallback);
43+
}
44+
} finally {
45+
lib.unregisterCallback(intCallback);
46+
lib.unregisterCallback(stringCallback);
47+
lib.unregisterCallback(binaryCallback);
48+
lib.close();
49+
}
50+
});
51+
52+
test('ffi callback ref and unref APIs work', () => {
53+
const { lib, functions: symbols } = getLibrary();
54+
let called = false;
55+
const values = [];
56+
const voidCallback = lib.registerCallback(() => {
57+
called = true;
58+
});
59+
const countingCallback = lib.registerCallback(
60+
{ arguments: ['i32'], return: 'i32' },
61+
(value) => {
62+
values.push(value);
63+
return 0;
64+
},
65+
);
66+
67+
try {
68+
lib.unrefCallback(voidCallback);
69+
lib.refCallback(voidCallback);
70+
symbols.call_void_callback(voidCallback);
71+
symbols.call_callback_multiple_times(countingCallback, 5);
72+
73+
assert.strictEqual(called, true);
74+
assert.deepStrictEqual(values, [0, 1, 2, 3, 4]);
75+
76+
lib.unregisterCallback(voidCallback);
77+
lib.unregisterCallback(countingCallback);
78+
79+
assert.throws(() => lib.refCallback(voidCallback), /Callback not found/);
80+
assert.throws(() => lib.unregisterCallback(-1n), /The first argument must be a non-negative bigint/);
81+
} finally {
82+
lib.close();
83+
}
84+
});

0 commit comments

Comments
 (0)