Skip to content

Commit 0342744

Browse files
Han5991richardlau
authored andcommitted
test_runner: add timestamp to JUnit reporter testsuites
Emit the standard JUnit timestamp (ISO 8601) on <testsuite> 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: #64028 Signed-off-by: sangwook <rewq5991@gmail.com> PR-URL: #64029 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent e470c74 commit 0342744

4 files changed

Lines changed: 21 additions & 6 deletions

File tree

lib/internal/test_runner/reporter/junit.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const {
55
ArrayPrototypeMap,
66
ArrayPrototypePush,
77
ArrayPrototypeSome,
8+
Date,
9+
DateNow,
10+
DatePrototypeToISOString,
811
NumberPrototypeToFixed,
912
ObjectEntries,
1013
RegExpPrototypeSymbolReplace,
@@ -112,6 +115,11 @@ module.exports = async function* junitReporter(source) {
112115
currentTest.attrs.tests = nonCommentChildren.length;
113116
currentTest.attrs.failures = ArrayPrototypeFilter(currentTest.children, isFailure).length;
114117
currentTest.attrs.skipped = ArrayPrototypeFilter(currentTest.children, isSkipped).length;
118+
// A suite's `test:start` is emitted lazily (when its first subtest
119+
// reports), so derive the start time from the end minus the measured
120+
// duration rather than stamping the (late) test:start moment.
121+
currentTest.attrs.timestamp =
122+
DatePrototypeToISOString(new Date(DateNow() - event.data.details.duration_ms));
115123
currentTest.attrs.hostname = HOSTNAME;
116124
} else {
117125
currentTest.tag = 'testcase';

test/common/assertSnapshot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ function replaceJunitDuration(str) {
226226
.replaceAll(/time="[0-9.]+"/g, 'time="*"')
227227
.replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *')
228228
.replaceAll(`hostname="${hostname()}"`, 'hostname="HOSTNAME"')
229+
.replaceAll(/timestamp="[^"]*"/g, 'timestamp="*"')
229230
.replaceAll(/file="[^"]*"/g, 'file="*"');
230231
}
231232

test/fixtures/test-runner/output/junit_reporter.snapshot

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ true !== false
128128
<testcase name="immediate throw - passes but warns" time="*" classname="test" file="*"/>
129129
<testcase name="immediate reject - passes but warns" time="*" classname="test" file="*"/>
130130
<testcase name="immediate resolve pass" time="*" classname="test" file="*"/>
131-
<testsuite name="subtest sync throw fail" time="*" disabled="0" errors="0" tests="1" failures="1" skipped="0" hostname="HOSTNAME">
131+
<testsuite name="subtest sync throw fail" time="*" disabled="0" errors="0" tests="1" failures="1" skipped="0" timestamp="*" hostname="HOSTNAME">
132132
<testcase name="+sync throw fail" time="*" classname="test" file="*" failure="thrown from subtest sync throw fail">
133133
<failure type="testCodeFailure" message="thrown from subtest sync throw fail">
134134
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
@@ -151,15 +151,15 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
151151
[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) }
152152
</failure>
153153
</testcase>
154-
<testsuite name="level 0a" time="*" disabled="0" errors="0" tests="4" failures="0" skipped="0" hostname="HOSTNAME">
154+
<testsuite name="level 0a" time="*" disabled="0" errors="0" tests="4" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
155155
<testcase name="level 1a" time="*" classname="test" file="*"/>
156156
<testcase name="level 1b" time="*" classname="test" file="*"/>
157157
<testcase name="level 1c" time="*" classname="test" file="*"/>
158158
<testcase name="level 1d" time="*" classname="test" file="*"/>
159159
</testsuite>
160-
<testsuite name="top level" time="*" disabled="0" errors="0" tests="2" failures="0" skipped="0" hostname="HOSTNAME">
160+
<testsuite name="top level" time="*" disabled="0" errors="0" tests="2" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
161161
<testcase name="+long running" time="*" classname="test" file="*"/>
162-
<testsuite name="+short running" time="*" disabled="0" errors="0" tests="1" failures="0" skipped="0" hostname="HOSTNAME">
162+
<testsuite name="+short running" time="*" disabled="0" errors="0" tests="1" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
163163
<testcase name="++short running" time="*" classname="test" file="*"/>
164164
</testsuite>
165165
</testsuite>
@@ -266,7 +266,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
266266
</failure>
267267
</testcase>
268268
<testcase name="callback async throw after done" time="*" classname="test" file="*"/>
269-
<testsuite name="only is set on subtests but not in only mode" time="*" disabled="0" errors="0" tests="3" failures="0" skipped="0" hostname="HOSTNAME">
269+
<testsuite name="only is set on subtests but not in only mode" time="*" disabled="0" errors="0" tests="3" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
270270
<testcase name="running subtest 1" time="*" classname="test" file="*"/>
271271
<testcase name="running subtest 3" time="*" classname="test" file="*"/>
272272
<testcase name="running subtest 4" time="*" classname="test" file="*"/>
@@ -288,7 +288,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
288288
}
289289
</failure>
290290
</testcase>
291-
<testsuite name="subtest sync throw fails" time="*" disabled="0" errors="0" tests="2" failures="2" skipped="0" hostname="HOSTNAME">
291+
<testsuite name="subtest sync throw fails" time="*" disabled="0" errors="0" tests="2" failures="2" skipped="0" timestamp="*" hostname="HOSTNAME">
292292
<testcase name="sync throw fails at first" time="*" classname="test" file="*" failure="thrown from subtest sync throw fails at first">
293293
<failure type="testCodeFailure" message="thrown from subtest sync throw fails at first">
294294
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first

test/parallel/test-runner-reporters.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ describe('node:test reporters', { concurrency: true }, () => {
200200
assert.strictEqual(child.stdout.toString(), '');
201201
const fileContents = fs.readFileSync(file, 'utf8');
202202
assert.match(fileContents, /<testsuite .*name="nested".*tests="2".*failures="1".*skipped="0".*>/);
203+
// The exact timestamp format is intentionally not pinned here (still under
204+
// discussion); assert only that the value is present and a real date.
205+
const { 1: timestamp } = fileContents.match(/<testsuite [^>]*timestamp="([^"]+)"/) ?? [];
206+
assert.ok(timestamp, 'testsuite should have a timestamp attribute');
207+
assert.match(timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
208+
assert.ok(!Number.isNaN(Date.parse(timestamp)), `expected a valid date, got ${timestamp}`);
203209
assert.match(fileContents, /<testcase .*name="failing".*>\s*<failure .*type="testCodeFailure".*message="error".*>/);
204210
assert.match(fileContents, /<testcase .*name="ok".*classname="test".*\/>/);
205211
assert.match(fileContents, /<testcase .*name="top level".*classname="test".*\/>/);

0 commit comments

Comments
 (0)