test_runner: add level parameter to reporter.diagnostic

Added a parameter to allow severity-based formatting for
diagnostic messages. Defaults to 'info'.
This update enables better control over message presentation
(e.g., coloring) based on severity levels such as 'info', 'warn',
and 'error'.

Refs: https://github.com/nodejs/node/issues/55922
PR-URL: https://github.com/nodejs/node/pull/57923
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Jacopo Martinelli 2025-05-19 10:28:05 +02:00 committed by GitHub
parent 7c74205aa7
commit 38757c906d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 59 additions and 4 deletions

View File

@ -3016,6 +3016,11 @@ defined. The corresponding declaration ordered event is `'test:start'`.
`undefined` if the test was run through the REPL. `undefined` if the test was run through the REPL.
* `message` {string} The diagnostic message. * `message` {string} The diagnostic message.
* `nesting` {number} The nesting level of the test. * `nesting` {number} The nesting level of the test.
* `level` {string} The severity level of the diagnostic message.
Possible values are:
* `'info'`: Informational messages.
* `'warn'`: Warnings.
* `'error'`: Errors.
Emitted when [`context.diagnostic`][] is called. Emitted when [`context.diagnostic`][] is called.
This event is guaranteed to be emitted in the same order as the tests are This event is guaranteed to be emitted in the same order as the tests are

View File

@ -94,8 +94,10 @@ class SpecReporter extends Transform {
case 'test:stderr': case 'test:stderr':
case 'test:stdout': case 'test:stdout':
return data.message; return data.message;
case 'test:diagnostic': case 'test:diagnostic':{
return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; const diagnosticColor = reporterColorMap[data.level] || reporterColorMap['test:diagnostic'];
return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
}
case 'test:coverage': case 'test:coverage':
return getCoverageReport(indent(data.nesting), data.summary, return getCoverageReport(indent(data.nesting), data.summary,
reporterUnicodeSymbolMap['test:coverage'], colors.blue, true); reporterUnicodeSymbolMap['test:coverage'], colors.blue, true);

View File

@ -37,6 +37,15 @@ const reporterColorMap = {
get 'test:diagnostic'() { get 'test:diagnostic'() {
return colors.blue; return colors.blue;
}, },
get 'info'() {
return colors.blue;
},
get 'warn'() {
return colors.yellow;
},
get 'error'() {
return colors.red;
},
}; };
function indent(nesting) { function indent(nesting) {

View File

@ -1235,7 +1235,7 @@ class Test extends AsyncResource {
if (actual < threshold) { if (actual < threshold) {
harness.success = false; harness.success = false;
process.exitCode = kGenericUserError; process.exitCode = kGenericUserError;
reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`); reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`, 'error');
} }
} }

View File

@ -116,11 +116,12 @@ class TestsStream extends Readable {
}); });
} }
diagnostic(nesting, loc, message) { diagnostic(nesting, loc, message, level = 'info') {
this[kEmitMessage]('test:diagnostic', { this[kEmitMessage]('test:diagnostic', {
__proto__: null, __proto__: null,
nesting, nesting,
message, message,
level,
...loc, ...loc,
}); });
} }

View File

@ -90,6 +90,26 @@ for (const coverage of coverages) {
assert(!findCoverageFileForPid(result.pid)); assert(!findCoverageFileForPid(result.pid));
}); });
test(`test failing ${coverage.flag} with red color`, () => {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--test-coverage-exclude=!test/**',
`${coverage.flag}=99`,
'--test-reporter', 'spec',
fixture,
], {
env: { ...process.env, FORCE_COLOR: '3' },
});
const stdout = result.stdout.toString();
// eslint-disable-next-line no-control-regex
const redColorRegex = /\u001b\[31m Error: \d{2}\.\d{2}% \w+ coverage does not meet threshold of 99%/;
assert.match(stdout, redColorRegex, 'Expected red color code not found in diagnostic message');
assert.strictEqual(result.status, 1);
assert(!findCoverageFileForPid(result.pid));
});
test(`test failing ${coverage.flag}`, () => { test(`test failing ${coverage.flag}`, () => {
const result = spawnSync(process.execPath, [ const result = spawnSync(process.execPath, [
'--test', '--test',

View File

@ -33,6 +33,24 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
for await (const _ of stream); for await (const _ of stream);
}); });
it('should emit diagnostic events with level parameter', async () => {
const diagnosticEvents = [];
const stream = run({
files: [join(testFixtures, 'coverage.js')],
reporter: 'spec',
});
stream.on('test:diagnostic', (event) => {
diagnosticEvents.push(event);
});
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
assert(diagnosticEvents.length > 0, 'No diagnostic events were emitted');
const infoEvent = diagnosticEvents.find((e) => e.level === 'info');
assert(infoEvent, 'No diagnostic events with level "info" were emitted');
});
const argPrintingFile = join(testFixtures, 'print-arguments.js'); const argPrintingFile = join(testFixtures, 'print-arguments.js');
it('should allow custom arguments via execArgv', async () => { it('should allow custom arguments via execArgv', async () => {
const result = await run({ files: [argPrintingFile], execArgv: ['-p', '"Printed"'] }).compose(spec).toArray(); const result = await run({ files: [argPrintingFile], execArgv: ['-p', '"Printed"'] }).compose(spec).toArray();