Skip to content

Commit 17a05b1

Browse files
MoLowruyadorno
authored andcommitted
test_runner: add junit reporter
PR-URL: #49614 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 5672e38 commit 17a05b1

File tree

7 files changed

+685
-2
lines changed

7 files changed

+685
-2
lines changed

doc/api/test.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,9 @@ The following built-reporters are supported:
653653
where each passing test is represented by a `.`,
654654
and each failing test is represented by a `X`.
655655

656+
* `junit`
657+
The junit reporter outputs test results in a jUnit XML format
658+
656659
When `stdout` is a [TTY][], the `spec` reporter is used by default.
657660
Otherwise, the `tap` reporter is used by default.
658661

@@ -664,11 +667,11 @@ to the test runner's output is required, use the events emitted by the
664667
The reporters are available via the `node:test/reporters` module:
665668

666669
```mjs
667-
import { tap, spec, dot } from 'node:test/reporters';
670+
import { tap, spec, dot, junit } from 'node:test/reporters';
668671
```
669672

670673
```cjs
671-
const { tap, spec, dot } = require('node:test/reporters');
674+
const { tap, spec, dot, junit } = require('node:test/reporters');
672675
```
673676

674677
### Custom reporters
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeFilter,
4+
ArrayPrototypeMap,
5+
ArrayPrototypeJoin,
6+
ArrayPrototypePush,
7+
ArrayPrototypeSome,
8+
NumberPrototypeToFixed,
9+
ObjectEntries,
10+
RegExpPrototypeSymbolReplace,
11+
String,
12+
StringPrototypeRepeat,
13+
} = primordials;
14+
15+
const { inspectWithNoCustomRetry } = require('internal/errors');
16+
const { hostname } = require('os');
17+
18+
const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
19+
const HOSTNAME = hostname();
20+
21+
function escapeAttribute(s = '') {
22+
return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, ''), '&quot;'));
23+
}
24+
25+
function escapeContent(s = '') {
26+
return RegExpPrototypeSymbolReplace(/</g, RegExpPrototypeSymbolReplace(/&/g, s, '&amp;'), '&lt;');
27+
}
28+
29+
function escapeComment(s = '') {
30+
return RegExpPrototypeSymbolReplace(/--/g, s, '&#45;&#45;');
31+
}
32+
33+
function treeToXML(tree) {
34+
if (typeof tree === 'string') {
35+
return `${escapeContent(tree)}\n`;
36+
}
37+
const {
38+
tag, attrs, nesting, children, comment,
39+
} = tree;
40+
const indent = StringPrototypeRepeat('\t', nesting + 1);
41+
if (comment) {
42+
return `${indent}<!-- ${escapeComment(comment)} -->\n`;
43+
}
44+
const attrsString = ArrayPrototypeJoin(ArrayPrototypeMap(ObjectEntries(attrs)
45+
, ({ 0: key, 1: value }) => `${key}="${escapeAttribute(String(value))}"`)
46+
, ' ');
47+
if (!children?.length) {
48+
return `${indent}<${tag} ${attrsString}/>\n`;
49+
}
50+
const childrenString = ArrayPrototypeJoin(ArrayPrototypeMap(children ?? [], treeToXML), '');
51+
return `${indent}<${tag} ${attrsString}>\n${childrenString}${indent}</${tag}>\n`;
52+
}
53+
54+
function isFailure(node) {
55+
return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'failure')) || node?.attrs?.failures;
56+
}
57+
58+
function isSkipped(node) {
59+
return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'skipped')) || node?.attrs?.failures;
60+
}
61+
62+
module.exports = async function* junitReporter(source) {
63+
yield '<?xml version="1.0" encoding="utf-8"?>\n';
64+
yield '<testsuites>\n';
65+
let currentSuite = null;
66+
const roots = [];
67+
68+
function startTest(event) {
69+
const originalSuite = currentSuite;
70+
currentSuite = {
71+
__proto__: null,
72+
attrs: { __proto__: null, name: event.data.name },
73+
nesting: event.data.nesting,
74+
parent: currentSuite,
75+
children: [],
76+
};
77+
if (originalSuite?.children) {
78+
ArrayPrototypePush(originalSuite.children, currentSuite);
79+
}
80+
if (!currentSuite.parent) {
81+
ArrayPrototypePush(roots, currentSuite);
82+
}
83+
}
84+
85+
for await (const event of source) {
86+
switch (event.type) {
87+
case 'test:start': {
88+
startTest(event);
89+
break;
90+
}
91+
case 'test:pass':
92+
case 'test:fail': {
93+
if (!currentSuite) {
94+
startTest({ __proto__: null, data: { __proto__: null, name: 'root', nesting: 0 } });
95+
}
96+
if (currentSuite.attrs.name !== event.data.name ||
97+
currentSuite.nesting !== event.data.nesting) {
98+
startTest(event);
99+
}
100+
const currentTest = currentSuite;
101+
if (currentSuite?.nesting === event.data.nesting) {
102+
currentSuite = currentSuite.parent;
103+
}
104+
currentTest.attrs.time = NumberPrototypeToFixed(event.data.details.duration_ms / 1000, 6);
105+
const nonCommentChildren = ArrayPrototypeFilter(currentTest.children, (c) => c.comment == null);
106+
if (nonCommentChildren.length > 0) {
107+
currentTest.tag = 'testsuite';
108+
currentTest.attrs.disabled = 0;
109+
currentTest.attrs.errors = 0;
110+
currentTest.attrs.tests = nonCommentChildren.length;
111+
currentTest.attrs.failures = ArrayPrototypeFilter(currentTest.children, isFailure).length;
112+
currentTest.attrs.skipped = ArrayPrototypeFilter(currentTest.children, isSkipped).length;
113+
currentTest.attrs.hostname = HOSTNAME;
114+
} else {
115+
currentTest.tag = 'testcase';
116+
currentTest.attrs.classname = event.data.classname ?? 'test';
117+
if (event.data.skip) {
118+
ArrayPrototypePush(currentTest.children, {
119+
__proto__: null, nesting: event.data.nesting + 1, tag: 'skipped',
120+
attrs: { __proto__: null, type: 'skipped', message: event.data.skip },
121+
});
122+
}
123+
if (event.data.todo) {
124+
ArrayPrototypePush(currentTest.children, {
125+
__proto__: null, nesting: event.data.nesting + 1, tag: 'skipped',
126+
attrs: { __proto__: null, type: 'todo', message: event.data.todo },
127+
});
128+
}
129+
if (event.type === 'test:fail') {
130+
const error = event.data.details?.error;
131+
currentTest.children.push({
132+
__proto__: null,
133+
nesting: event.data.nesting + 1,
134+
tag: 'failure',
135+
attrs: { __proto__: null, type: error?.failureType || error?.code, message: error?.message ?? '' },
136+
children: [inspectWithNoCustomRetry(error, inspectOptions)],
137+
});
138+
currentTest.failures = 1;
139+
currentTest.attrs.failure = error?.message ?? '';
140+
}
141+
}
142+
break;
143+
}
144+
case 'test:diagnostic': {
145+
const parent = currentSuite?.children ?? roots;
146+
ArrayPrototypePush(parent, {
147+
__proto__: null, nesting: event.data.nesting, comment: event.data.message,
148+
});
149+
break;
150+
} default:
151+
break;
152+
}
153+
}
154+
for (const suite of roots) {
155+
yield treeToXML(suite);
156+
}
157+
yield '</testsuites>\n';
158+
};

lib/internal/test_runner/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const kBuiltinReporters = new SafeMap([
116116
['spec', 'internal/test_runner/reporter/spec'],
117117
['dot', 'internal/test_runner/reporter/dot'],
118118
['tap', 'internal/test_runner/reporter/tap'],
119+
['junit', 'internal/test_runner/reporter/junit'],
119120
]);
120121

121122
const kDefaultReporter = process.stdout.isTTY ? 'spec' : 'tap';

lib/test/reporters.js

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { ObjectDefineProperties, ReflectConstruct } = primordials;
44

55
let dot;
6+
let junit;
67
let spec;
78
let tap;
89

@@ -17,6 +18,15 @@ ObjectDefineProperties(module.exports, {
1718
return dot;
1819
},
1920
},
21+
junit: {
22+
__proto__: null,
23+
configurable: true,
24+
enumerable: true,
25+
get() {
26+
junit ??= require('internal/test_runner/reporter/junit');
27+
return junit;
28+
},
29+
},
2030
spec: {
2131
__proto__: null,
2232
configurable: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
require('../../../common');
3+
const fixtures = require('../../../common/fixtures');
4+
const spawn = require('node:child_process').spawn;
5+
6+
spawn(process.execPath,
7+
['--no-warnings', '--test-reporter', 'junit', fixtures.path('test-runner/output/output.js')], { stdio: 'inherit' });

0 commit comments

Comments
 (0)