From eeeb20fe01c9bb19789e25a99c773d52f00098ca Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 20 Jun 2026 23:47:25 +0900 Subject: [PATCH 1/2] test_runner: add timestamp to JUnit reporter testsuites Emit the standard JUnit timestamp (ISO 8601) on elements, which the reporter was omitting. The suite start time is reconstructed as end-minus-duration, because the runner reports a suite's test:start lazily (when its first subtest reports), which would otherwise record a time close to the suite's end. Fixes: https://github.com/nodejs/node/issues/64028 Signed-off-by: sangwook --- lib/internal/test_runner/reporter/junit.js | 8 ++++++++ test/common/assertSnapshot.js | 1 + .../test-runner/output/junit_reporter.snapshot | 12 ++++++------ test/parallel/test-runner-reporters.js | 6 ++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/internal/test_runner/reporter/junit.js b/lib/internal/test_runner/reporter/junit.js index 25f5667c59e273..130ff60af405ad 100644 --- a/lib/internal/test_runner/reporter/junit.js +++ b/lib/internal/test_runner/reporter/junit.js @@ -5,6 +5,9 @@ const { ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypeSome, + Date, + DateNow, + DatePrototypeToISOString, NumberPrototypeToFixed, ObjectEntries, RegExpPrototypeSymbolReplace, @@ -112,6 +115,11 @@ module.exports = async function* junitReporter(source) { currentTest.attrs.tests = nonCommentChildren.length; currentTest.attrs.failures = ArrayPrototypeFilter(currentTest.children, isFailure).length; currentTest.attrs.skipped = ArrayPrototypeFilter(currentTest.children, isSkipped).length; + // A suite's `test:start` is emitted lazily (when its first subtest + // reports), so derive the start time from the end minus the measured + // duration rather than stamping the (late) test:start moment. + currentTest.attrs.timestamp = + DatePrototypeToISOString(new Date(DateNow() - event.data.details.duration_ms)); currentTest.attrs.hostname = HOSTNAME; } else { currentTest.tag = 'testcase'; diff --git a/test/common/assertSnapshot.js b/test/common/assertSnapshot.js index 9a7482020e098a..4fdd0be0ee32ae 100644 --- a/test/common/assertSnapshot.js +++ b/test/common/assertSnapshot.js @@ -226,6 +226,7 @@ function replaceJunitDuration(str) { .replaceAll(/time="[0-9.]+"/g, 'time="*"') .replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *') .replaceAll(`hostname="${hostname()}"`, 'hostname="HOSTNAME"') + .replaceAll(/timestamp="[^"]*"/g, 'timestamp="*"') .replaceAll(/file="[^"]*"/g, 'file="*"'); } diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot index 1142b5b31ff2e7..cef5f0b52da186 100644 --- a/test/fixtures/test-runner/output/junit_reporter.snapshot +++ b/test/fixtures/test-runner/output/junit_reporter.snapshot @@ -128,7 +128,7 @@ true !== false - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail @@ -151,15 +151,15 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail [Error [ERR_TEST_FAILURE]: Symbol(thrown symbol from sync throw non-error fail)] { code: 'ERR_TEST_FAILURE', failureType: 'testCodeFailure', cause: Symbol(thrown symbol from sync throw non-error fail) } - + - + - + @@ -266,7 +266,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw - + @@ -288,7 +288,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw } - + Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js index 50a47578a1da7e..7fed79d45b48fd 100644 --- a/test/parallel/test-runner-reporters.js +++ b/test/parallel/test-runner-reporters.js @@ -200,6 +200,12 @@ describe('node:test reporters', { concurrency: true }, () => { assert.strictEqual(child.stdout.toString(), ''); const fileContents = fs.readFileSync(file, 'utf8'); assert.match(fileContents, //); + // The exact timestamp format is intentionally not pinned here (still under + // discussion); assert only that the value is present and a real date. + const { 1: timestamp } = fileContents.match(/]*timestamp="([^"]+)"/) ?? []; + assert.ok(timestamp, 'testsuite should have a timestamp attribute'); + assert.match(timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + assert.ok(!Number.isNaN(Date.parse(timestamp)), `expected a valid date, got ${timestamp}`); assert.match(fileContents, /\s*/); assert.match(fileContents, //); assert.match(fileContents, //); From c41e207313f7ac23a31dc223b503069ebdfffbb7 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 21 Jun 2026 00:35:07 +0900 Subject: [PATCH 2/2] build: trigger ci rerun Signed-off-by: sangwook