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.
* `message` {string} The diagnostic message.
* `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.
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:stdout':
return data.message;
case 'test:diagnostic':
return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
case 'test:diagnostic':{
const diagnosticColor = reporterColorMap[data.level] || reporterColorMap['test:diagnostic'];
return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
}
case 'test:coverage':
return getCoverageReport(indent(data.nesting), data.summary,
reporterUnicodeSymbolMap['test:coverage'], colors.blue, true);

View File

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

View File

@ -1235,7 +1235,7 @@ class Test extends AsyncResource {
if (actual < threshold) {
harness.success = false;
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', {
__proto__: null,
nesting,
message,
level,
...loc,
});
}

View File

@ -90,6 +90,26 @@ for (const coverage of coverages) {
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}`, () => {
const result = spawnSync(process.execPath, [
'--test',

View File

@ -33,6 +33,24 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
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');
it('should allow custom arguments via execArgv', async () => {
const result = await run({ files: [argPrintingFile], execArgv: ['-p', '"Printed"'] }).compose(spec).toArray();