test: ensure assertions are reachable in more folders

PR-URL: https://github.com/nodejs/node/pull/60411
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Antoine du Hamel 2025-10-29 16:16:59 +01:00 committed by GitHub
parent 66f19ddd18
commit 53e325ffd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 147 additions and 77 deletions

View File

@ -1,4 +1,4 @@
import '../common/index.mjs';
import { mustCallAtLeast } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import tmpdir from '../common/tmpdir.js';
@ -14,7 +14,7 @@ const apilinks = fixtures.path('apilinks');
tmpdir.refresh();
fs.readdirSync(apilinks).forEach((fixture) => {
fs.readdirSync(apilinks).forEach(mustCallAtLeast((fixture) => {
if (!fixture.endsWith('.js')) return;
const input = path.join(apilinks, fixture);
@ -40,4 +40,4 @@ fs.readdirSync(apilinks).forEach((fixture) => {
Object.keys(actualLinks).length, 0,
`unexpected links returned ${JSON.stringify(actualLinks)}`,
);
});
}));

View File

@ -163,13 +163,31 @@ export default [
'benchmark',
'cctest',
'client-proxy',
'doctool',
'embedding',
'fixtures',
'fuzzers',
'js-native-api',
'known_issues',
'message',
'module-hooks',
'node-api',
'pummel',
'nop',
'overlapped-checker',
'pseudo-tty',
'pummel',
'report',
'sea',
'sqlite',
'system-ca',
'test426',
'testpy',
'tick-processor',
'tools',
'v8-updates',
'wasi',
'wasm-allocation',
'wpt',
].join(',')}}/**/*.{js,mjs,cjs}`,
],
rules: {

View File

@ -3,14 +3,14 @@
const common = require('../../../common');
const { suite, test } = require('node:test');
suite('async suite', async () => {
suite('async suite', common.mustCall(async () => {
await 1;
test('enabled 1', common.mustCall());
await 1;
test('not run', common.mustNotCall());
await 1;
});
}));
suite('sync suite', () => {
suite('sync suite', common.mustCall(() => {
test('enabled 2', common.mustCall());
});
}));

View File

@ -9,6 +9,7 @@ before(() => testArr.push('global before'));
after(() => {
testArr.push('global after');
// eslint-disable-next-line node-core/must-call-assert
assert.deepStrictEqual(testArr, [
'global before',
'describe before',

View File

@ -7,7 +7,7 @@ const { setTimeout } = require('node:timers/promises');
before((t) => t.diagnostic('before 1 called'));
after((t) => t.diagnostic('after 1 called'));
describe('describe hooks', () => {
describe('describe hooks', common.mustCall(() => {
const testArr = [];
before(function() {
testArr.push('before ' + this.name);
@ -51,9 +51,9 @@ describe('describe hooks', () => {
it('nested 1', () => testArr.push('nested 1'));
test('nested 2', () => testArr.push('nested 2'));
});
});
}));
describe('describe hooks - no subtests', () => {
describe('describe hooks - no subtests', common.mustCall(() => {
const testArr = [];
before(function() {
testArr.push('before ' + this.name);
@ -67,7 +67,7 @@ describe('describe hooks - no subtests', () => {
}));
beforeEach(common.mustNotCall());
afterEach(common.mustNotCall());
});
}));
describe('before throws', () => {
before(() => { throw new Error('before'); });
@ -75,10 +75,10 @@ describe('before throws', () => {
test('2', () => {});
});
describe('before throws - no subtests', () => {
describe('before throws - no subtests', common.mustCall(() => {
before(() => { throw new Error('before'); });
after(common.mustCall());
});
}));
describe('after throws', () => {
after(() => { throw new Error('after'); });
@ -102,11 +102,11 @@ describe('afterEach throws', () => {
test('2', () => {});
});
describe('afterEach when test fails', () => {
describe('afterEach when test fails', common.mustCall(() => {
afterEach(common.mustCall(2));
it('1', () => { throw new Error('test'); });
test('2', () => {});
});
}));
describe('afterEach throws and test fails', () => {
afterEach(() => { throw new Error('afterEach'); });
@ -246,13 +246,13 @@ test('t.after() is called if test body throws', (t) => {
throw new Error('bye');
});
describe('run after when before throws', () => {
describe('run after when before throws', common.mustCall(() => {
after(common.mustCall(() => {
console.log('- after() called');
}));
before(() => { throw new Error('before'); });
it('1', () => {});
});
}));
test('test hooks - async', async (t) => {

View File

@ -32,7 +32,7 @@ test('top level test enabled', common.mustCall(async (t) => {
);
}));
describe('top level describe enabled', () => {
describe('top level describe enabled', common.mustCall(() => {
before(common.mustCall());
beforeEach(common.mustCall(3));
afterEach(common.mustCall(3));
@ -44,7 +44,7 @@ describe('top level describe enabled', () => {
describe('nested describe enabled', common.mustCall(() => {
it('is enabled', common.mustCall());
}));
});
}));
describe('yes', function() {
it('no', () => {});
@ -76,13 +76,13 @@ describe('no with todo', { todo: true }, () => {
});
});
describe('DescribeForMatchWithAncestors', () => {
describe('DescribeForMatchWithAncestors', common.mustCall(() => {
it('NestedTest', () => common.mustNotCall());
describe('NestedDescribeForMatchWithAncestors', () => {
describe('NestedDescribeForMatchWithAncestors', common.mustCall(() => {
it('NestedTest', common.mustCall());
});
});
}));
}));
describe('DescribeForMatchWithAncestors', () => {
it('NestedTest', () => common.mustNotCall());

View File

@ -3,14 +3,14 @@ const common = require('../../common');
const assert = require('assert');
const addon = require(`./build/${common.buildType}/3_callbacks`);
addon.RunCallback(function(msg) {
addon.RunCallback(common.mustCall((msg) => {
assert.strictEqual(msg, 'hello world');
});
}));
function testRecv(desiredRecv) {
addon.RunCallbackWithRecv(function() {
addon.RunCallbackWithRecv(common.mustCall(function() {
assert.strictEqual(this, desiredRecv);
}, desiredRecv);
}), desiredRecv);
}
testRecv(undefined);

View File

@ -50,9 +50,9 @@ test_promise.concludeCurrentPromise(undefined, true);
const rejectPromise = Promise.reject(-1);
const expected_reason = -1;
assert.strictEqual(test_promise.isPromise(rejectPromise), true);
rejectPromise.catch((reason) => {
rejectPromise.catch(common.mustCall((reason) => {
assert.strictEqual(reason, expected_reason);
});
}));
assert.strictEqual(test_promise.isPromise(2.4), false);
assert.strictEqual(test_promise.isPromise('I promise!'), false);

View File

@ -21,25 +21,25 @@ if (cluster.isPrimary) {
if (err.code === 'ENOTSUP') throw err;
});
worker1.on('message', (msg) => {
worker1.on('message', common.mustCall((msg) => {
if (typeof msg !== 'object') process.exit(0);
if (msg.message !== 'success') process.exit(0);
if (typeof msg.port1 !== 'number') process.exit(0);
const worker2 = cluster.fork({ PRT1: msg.port1 });
worker2.on('message', () => process.exit(0));
worker2.on('exit', (code, signal) => {
worker2.on('exit', common.mustCall((code, signal) => {
// This is the droid we are looking for
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
}));
// cleanup anyway
process.on('exit', () => {
worker1.send(BYE);
worker2.send(BYE);
});
});
}));
// end primary code
} else {
// worker code

View File

@ -9,7 +9,7 @@ if (!common.isLinux) {
common.skip('This test is only applicable to Linux');
}
const { ok, strictEqual } = require('assert');
const assert = require('assert');
const { join } = require('path');
const path = require('path');
const tmpdir = require('../common/tmpdir');
@ -42,8 +42,8 @@ const name = Buffer.from([
const testPath = Buffer.concat([tmpdirPath, sepBuf, name]);
writeFileSync(testPath, 'test content');
ok(existsSync(testPath));
strictEqual(readFileSync(testPath, 'utf8'), 'test content');
assert.ok(existsSync(testPath));
assert.strictEqual(readFileSync(testPath, 'utf8'), 'test content');
// The cpSync is expected to fail because the implementation does not
// properly handle non-UTF8 names in the path.

View File

@ -16,9 +16,9 @@ const response = 'content-length: 19\r\n';
const methods = [ 'GET', 'HEAD', 'DELETE', 'POST', 'PATCH', 'PUT', 'OPTIONS' ];
const server = http.createServer(common.mustCall(function(req, res) {
req.on('data', function(chunk) {
req.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, Buffer.from(upload));
});
}));
res.setHeader('Content-Type', 'text/plain');
let payload = `${req.method}\r\n`;
for (let i = 0; i < req.rawHeaders.length; i += 2) {
@ -30,22 +30,22 @@ const server = http.createServer(common.mustCall(function(req, res) {
res.end(payload);
}), methods.length);
server.listen(0, function tryNextRequest() {
server.listen(0, common.mustCall(function tryNextRequest() {
const method = methods.pop();
if (method === undefined) return;
const port = server.address().port;
const req = http.request({ method, port }, function(res) {
const req = http.request({ method, port }, common.mustCall((res) => {
const chunks = [];
res.on('data', function(chunk) {
chunks.push(chunk);
});
res.on('end', function() {
res.on('end', common.mustCall(() => {
const received = Buffer.concat(chunks).toString();
const expected = method.toLowerCase() + '\r\n' + response;
assert.strictEqual(received.toLowerCase(), expected);
tryNextRequest();
});
});
}));
}));
req.end(upload);
}).unref();
})).unref();

View File

@ -17,9 +17,9 @@ const response = '';
const methods = [ 'GET', 'HEAD', 'DELETE', 'POST', 'PATCH', 'PUT', 'OPTIONS' ];
const server = http.createServer(common.mustCall(function(req, res) {
req.on('data', function(chunk) {
req.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk.toString(), upload);
});
}));
res.setHeader('Content-Type', 'text/plain');
res.write(`${req.method}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) {
@ -31,22 +31,22 @@ const server = http.createServer(common.mustCall(function(req, res) {
res.end();
}), methods.length);
server.listen(0, function tryNextRequest() {
server.listen(0, common.mustCall(function tryNextRequest() {
const method = methods.pop();
if (method === undefined) return;
const port = server.address().port;
const req = http.request({ method, port }, function(res) {
const req = http.request({ method, port }, common.mustCall((res) => {
const chunks = [];
res.on('data', function(chunk) {
chunks.push(chunk);
});
res.on('end', function() {
res.on('end', common.mustCall(() => {
const received = Buffer.concat(chunks).toString();
const expected = method.toLowerCase() + '\r\n' + response;
assert.strictEqual(received.toLowerCase(), expected);
tryNextRequest();
});
});
}));
}));
req.end();
}).unref();
})).unref();

View File

@ -16,9 +16,9 @@ const response = 'transfer-encoding: chunked\r\n';
const methods = [ 'GET', 'HEAD', 'DELETE', 'POST', 'PATCH', 'PUT', 'OPTIONS' ];
const server = http.createServer(common.mustCall(function(req, res) {
req.on('data', function(chunk) {
req.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk.toString(), upload);
});
}));
res.setHeader('Content-Type', 'text/plain');
res.write(`${req.method}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) {
@ -30,23 +30,23 @@ const server = http.createServer(common.mustCall(function(req, res) {
res.end();
}), methods.length);
server.listen(0, function tryNextRequest() {
server.listen(0, common.mustCall(function tryNextRequest() {
const method = methods.pop();
if (method === undefined) return;
const port = server.address().port;
const req = http.request({ method, port }, function(res) {
const req = http.request({ method, port }, common.mustCall((res) => {
const chunks = [];
res.on('data', function(chunk) {
chunks.push(chunk);
});
res.on('end', function() {
res.on('end', common.mustCall(() => {
const received = Buffer.concat(chunks).toString();
const expected = method.toLowerCase() + '\r\n' + response;
assert.strictEqual(received.toLowerCase(), expected);
tryNextRequest();
});
});
}));
}));
req.write(upload);
req.end();
}).unref();
})).unref();

View File

@ -22,7 +22,7 @@ const server = http.createServer(common.mustCall(function(req, res) {
}));
server.listen(0, () => {
server.listen(0, common.mustCall(() => {
http.request({
port: server.address().port,
path: expected,
@ -33,4 +33,4 @@ server.listen(0, () => {
console.log(e.message);
process.exit(1);
}).end();
});
}));

View File

@ -30,11 +30,11 @@ function serialFork() {
const worker = cluster.fork();
worker.on('error', (err) => assert.fail(err));
// No common.mustCall since 1 out of 3 should fail.
worker.on('online', () => {
worker.on('online', common.mustCall(() => {
worker.on('message', common.mustCall((message) => {
ports.push(message.debugPort);
}));
});
}));
worker.on('exit', common.mustCall((code, signal) => {
assert.strictEqual(signal, null);
// worker 2 should fail because of port clash with `server`

View File

@ -8,7 +8,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/must-call-assert');
const message = 'Assertions must be wrapped into `common.mustCall` or `common.mustCallAtLeast`';
const message = 'Assertions must be wrapped into `common.mustSucceed`, `common.mustCall` or `common.mustCallAtLeast`';
const tester = new RuleTester();
tester.run('must-call-assert', rule, {
@ -18,6 +18,8 @@ tester.run('must-call-assert', rule, {
'process.once("message", common.mustCall((code) => {assert.strictEqual(code, 0)}));',
'process.once("message", common.mustCall((code) => {if(2+2 === 5) { assert.strictEqual(code, 0)} }));',
'process.once("message", common.mustCall((code) => { (() => assert.strictEqual(code, 0))(); }));',
'someAsyncTask(common.mustSucceed((code) => { (() => assert.strictEqual(code, 0))(); }));',
'someAsyncTask(mustSucceed((code) => { (() => assert.strictEqual(code, 0))(); }));',
'(async () => {await assert.rejects(fun())})().then()',
'[1, true].forEach((val) => assert.strictEqual(fun(val), 0));',
'const assert = require("node:assert")',
@ -68,10 +70,41 @@ tester.run('must-call-assert', rule, {
import assert from 'node:assert';
describe("whatever", () => {
it("should not be reported", async () => {
it("should not be reported", async (t) => {
assert.strictEqual(2+2, 5);
await t.test("name", () => {
assert.ok(global.test);
});
});
});
`,
`
process.on("message", common.mustCall(() => {
Promise.all([].map(async (val) => {
val = await asyncTask(val);
assert.strictEqual(val, 3);
})).then(common.mustCall());
}));
`,
`
spawnSyncAndAssert(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA,MKSNAPSHOT',
...process.env,
},
},
{
trim: true,
stdout: 'Hello from snapshot',
stderr(output) {
assert.doesNotMatch(
output,
/Single executable application is an experimental feature/);
},
},
);
`,
],
invalid: [

View File

@ -3,7 +3,7 @@
// This tests the creation of a single executable application that can be
// debugged using the inspector protocol with NODE_OPTIONS=--inspect-brk=0
require('../common');
const common = require('../common');
const assert = require('assert');
const { writeFileSync, existsSync } = require('fs');
const { spawn } = require('child_process');
@ -64,7 +64,7 @@ seaProcess.stdout.on('data', (data) => {
console.log(`[SEA][STDOUT] ${data}`);
});
seaProcess.stderr.on('data', (data) => {
seaProcess.stderr.on('data', common.mustCallAtLeast((data) => {
console.log(`[SEA][STDERR] ${data}`);
seaStderr += data;
@ -103,19 +103,19 @@ seaProcess.stderr.on('data', (data) => {
console.log(`[INSPECT][STDERR] ${data}`);
});
inspectorProcess.on('close', (code) => {
inspectorProcess.on('close', common.mustCall((code) => {
assert.strictEqual(code, 0, `Inspector process exited with code ${code}.`);
});
}));
inspectorProcess.on('error', (err) => {
throw err;
});
}
});
}));
seaProcess.on('close', (code) => {
seaProcess.on('close', common.mustCall((code) => {
assert.strictEqual(code, 0, `SEA process exited with code ${code}.`);
});
}));
seaProcess.on('error', (err) => {
throw err;

View File

@ -1,12 +1,19 @@
'use strict';
const message =
'Assertions must be wrapped into `common.mustCall` or `common.mustCallAtLeast`';
'Assertions must be wrapped into `common.mustSucceed`, `common.mustCall` or `common.mustCallAtLeast`';
const requireCall = 'CallExpression[callee.name="require"]';
const assertModuleSpecifier = '/^(node:)?assert(.strict)?$/';
const isPromiseAllCallArg = (node) =>
node.parent?.type === 'CallExpression' &&
node.parent.callee.type === 'MemberExpression' &&
node.parent.callee.object.type === 'Identifier' && node.parent.callee.object.name === 'Promise' &&
node.parent.callee.property.type === 'Identifier' && node.parent.callee.property.name === 'all' &&
node.parent.arguments.length === 1 && node.parent.arguments[0] === node;
function findEnclosingFunction(node) {
while (true) {
node = node.parent;
@ -20,10 +27,20 @@ function findEnclosingFunction(node) {
if (
node.parent.callee.type === 'MemberExpression' &&
(node.parent.callee.object.type === 'ArrayExpression' || node.parent.callee.object.type === 'Identifier') &&
node.parent.callee.property.name === 'forEach'
(
node.parent.callee.property.name === 'forEach' ||
(node.parent.callee.property.name === 'map' && isPromiseAllCallArg(node.parent))
)
) continue; // `[].forEach()` call
} else if (node.parent?.type === 'NewExpression') {
if (node.parent.callee.type === 'Identifier' && node.parent.callee.name === 'Promise') continue;
} else if (node.parent?.type === 'Property') {
const ancestor = node.parent.parent?.parent;
if (ancestor?.type === 'CallExpression' &&
ancestor.callee.type === 'Identifier' &&
/^spawnSyncAnd(Exit(WithoutError)?|Assert)$/.test(ancestor.callee.name)) {
continue;
}
}
break;
}
@ -31,7 +48,7 @@ function findEnclosingFunction(node) {
}
function isMustCallOrMustCallAtLeast(str) {
return str === 'mustCall' || str === 'mustCallAtLeast';
return str === 'mustCall' || str === 'mustCallAtLeast' || str === 'mustSucceed';
}
function isMustCallOrTest(str) {
@ -68,6 +85,7 @@ module.exports = {
parent.arguments[0].type === 'Literal' &&
parent.arguments[0].value === 'exit'
),
t: (name) => name === 'test', // t.test
}[parent.callee.object.name]?.(parent.callee.property.name)
) {
return;