Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/internal/test_runner/reporter/junit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const {
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSome,
Date,
DateNow,
DatePrototypeToISOString,
NumberPrototypeToFixed,
ObjectEntries,
RegExpPrototypeSymbolReplace,
Expand Down Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions test/common/assertSnapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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="*"');
}

Expand Down
12 changes: 6 additions & 6 deletions test/fixtures/test-runner/output/junit_reporter.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ true !== false
<testcase name="immediate throw - passes but warns" time="*" classname="test" file="*"/>
<testcase name="immediate reject - passes but warns" time="*" classname="test" file="*"/>
<testcase name="immediate resolve pass" time="*" classname="test" file="*"/>
<testsuite name="subtest sync throw fail" time="*" disabled="0" errors="0" tests="1" failures="1" skipped="0" hostname="HOSTNAME">
<testsuite name="subtest sync throw fail" time="*" disabled="0" errors="0" tests="1" failures="1" skipped="0" timestamp="*" hostname="HOSTNAME">
<testcase name="+sync throw fail" time="*" classname="test" file="*" failure="thrown from subtest sync throw fail">
<failure type="testCodeFailure" message="thrown from subtest sync throw fail">
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
Expand All @@ -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) }
</failure>
</testcase>
<testsuite name="level 0a" time="*" disabled="0" errors="0" tests="4" failures="0" skipped="0" hostname="HOSTNAME">
<testsuite name="level 0a" time="*" disabled="0" errors="0" tests="4" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
<testcase name="level 1a" time="*" classname="test" file="*"/>
<testcase name="level 1b" time="*" classname="test" file="*"/>
<testcase name="level 1c" time="*" classname="test" file="*"/>
<testcase name="level 1d" time="*" classname="test" file="*"/>
</testsuite>
<testsuite name="top level" time="*" disabled="0" errors="0" tests="2" failures="0" skipped="0" hostname="HOSTNAME">
<testsuite name="top level" time="*" disabled="0" errors="0" tests="2" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
<testcase name="+long running" time="*" classname="test" file="*"/>
<testsuite name="+short running" time="*" disabled="0" errors="0" tests="1" failures="0" skipped="0" hostname="HOSTNAME">
<testsuite name="+short running" time="*" disabled="0" errors="0" tests="1" failures="0" skipped="0" timestamp="*" hostname="HOSTNAME">
<testcase name="++short running" time="*" classname="test" file="*"/>
</testsuite>
</testsuite>
Expand Down Expand Up @@ -266,7 +266,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
</failure>
</testcase>
<testcase name="callback async throw after done" time="*" classname="test" file="*"/>
<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">
<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">
<testcase name="running subtest 1" time="*" classname="test" file="*"/>
<testcase name="running subtest 3" time="*" classname="test" file="*"/>
<testcase name="running subtest 4" time="*" classname="test" file="*"/>
Expand All @@ -288,7 +288,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
}
</failure>
</testcase>
<testsuite name="subtest sync throw fails" time="*" disabled="0" errors="0" tests="2" failures="2" skipped="0" hostname="HOSTNAME">
<testsuite name="subtest sync throw fails" time="*" disabled="0" errors="0" tests="2" failures="2" skipped="0" timestamp="*" hostname="HOSTNAME">
<testcase name="sync throw fails at first" time="*" classname="test" file="*" failure="thrown from subtest sync throw fails at first">
<failure type="testCodeFailure" message="thrown from subtest sync throw fails at first">
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first
Expand Down
6 changes: 6 additions & 0 deletions test/parallel/test-runner-reporters.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ describe('node:test reporters', { concurrency: true }, () => {
assert.strictEqual(child.stdout.toString(), '');
const fileContents = fs.readFileSync(file, 'utf8');
assert.match(fileContents, /<testsuite .*name="nested".*tests="2".*failures="1".*skipped="0".*>/);
// 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(/<testsuite [^>]*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, /<testcase .*name="failing".*>\s*<failure .*type="testCodeFailure".*message="error".*>/);
assert.match(fileContents, /<testcase .*name="ok".*classname="test".*\/>/);
assert.match(fileContents, /<testcase .*name="top level".*classname="test".*\/>/);
Expand Down
Loading