From e2455848bd1dfe48ab320c3dfdd37f136de8ec07 Mon Sep 17 00:00:00 2001 From: mag123c Date: Sun, 12 Oct 2025 09:50:56 +0900 Subject: [PATCH] test_runner: add classname hierarchy for JUnit reporter --- lib/internal/test_runner/test.js | 12 ++++++++++ lib/internal/test_runner/tests_stream.js | 2 ++ .../output/junit_classname_hierarchy.js | 19 +++++++++++++++ .../output/junit_classname_hierarchy.snapshot | 23 ++++++++++++++++++ .../output/junit_reporter.snapshot | 24 +++++++++---------- test/parallel/test-runner-reporters.js | 2 +- .../test-output-junit-classname-hierarchy.mjs | 12 ++++++++++ 7 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/test-runner/output/junit_classname_hierarchy.js create mode 100644 test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot create mode 100644 test/test-runner/test-output-junit-classname-hierarchy.mjs diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index de2d4c7b34dc4e..8b8e4dce39b7fd 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,6 +1,7 @@ 'use strict'; const { ArrayPrototypeEvery, + ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, @@ -1532,6 +1533,17 @@ class Test extends AsyncResource { details.passed_on_attempt = this.passedAttempt; } + // Generate classname from suite hierarchy for JUnit reporter + if (this.parent && this.parent !== this.root) { + const parts = []; + for (let t = this.parent; t !== t.root; t = t.parent) { + ArrayPrototypeUnshift(parts, t.name); + } + if (parts.length > 0) { + details.classname = ArrayPrototypeJoin(parts, '.'); + } + } + return { __proto__: null, details, directive }; } diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index 17b6890b5fc5df..bd38e60dfb125a 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -41,6 +41,7 @@ class TestsStream extends Readable { nesting, testNumber, details, + ...(details.classname && { __proto__: null, classname: details.classname }), ...loc, ...directive, }); @@ -53,6 +54,7 @@ class TestsStream extends Readable { nesting, testNumber, details, + ...(details.classname && { __proto__: null, classname: details.classname }), ...loc, ...directive, }); diff --git a/test/fixtures/test-runner/output/junit_classname_hierarchy.js b/test/fixtures/test-runner/output/junit_classname_hierarchy.js new file mode 100644 index 00000000000000..8b634be39380e7 --- /dev/null +++ b/test/fixtures/test-runner/output/junit_classname_hierarchy.js @@ -0,0 +1,19 @@ +'use strict'; +require('../../../common'); +const { suite, test } = require('node:test'); + +suite('Math', () => { + suite('Addition', () => { + test('adds positive numbers', () => {}); + }); + + suite('Multiplication', () => { + test('multiplies positive numbers', () => {}); + }); +}); + +suite('String', () => { + test('concatenates strings', () => {}); +}); + +test('standalone test', () => {}); diff --git a/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot b/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot new file mode 100644 index 00000000000000..8b645b067d2dc5 --- /dev/null +++ b/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index 1142b5b31ff2e7..a4522714838c09 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -129,7 +129,7 @@ true !== false - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:125:11) @@ -152,15 +152,15 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail - - - - + + + + - + - + @@ -267,9 +267,9 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw - - - + + + @@ -289,7 +289,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:334:11) @@ -304,7 +304,7 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first } - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at second at TestContext.<anonymous> (/test/fixtures/test-runner/output/output.js:337:11) { diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js index 50a47578a1da7e..63dac1346c90ef 100644 --- a/test/parallel/test-runner-reporters.js +++ b/test/parallel/test-runner-reporters.js @@ -201,7 +201,7 @@ describe('node:test reporters', { concurrency: true }, () => { const fileContents = fs.readFileSync(file, 'utf8'); assert.match(fileContents, //); assert.match(fileContents, /\s*/); - assert.match(fileContents, //); + assert.match(fileContents, //); assert.match(fileContents, //); }); }); diff --git a/test/test-runner/test-output-junit-classname-hierarchy.mjs b/test/test-runner/test-output-junit-classname-hierarchy.mjs new file mode 100644 index 00000000000000..737cefd89ecae2 --- /dev/null +++ b/test/test-runner/test-output-junit-classname-hierarchy.mjs @@ -0,0 +1,12 @@ +// Test that the output of test-runner/output/junit_classname_hierarchy.js matches +// test-runner/output/junit_classname_hierarchy.snapshot +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, junitTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/junit_classname_hierarchy.js'), + junitTransform, + { flags: ['--test-reporter=junit'] }, +);