Skip to content

Commit 26bba60

Browse files
committed
test_runner: add junit reporter
1 parent 48fcb20 commit 26bba60

File tree

6 files changed

+645
-0
lines changed

6 files changed

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

lib/internal/test_runner/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const kBuiltinReporters = new SafeMap([
110110
['spec', 'internal/test_runner/reporter/spec'],
111111
['dot', 'internal/test_runner/reporter/dot'],
112112
['tap', 'internal/test_runner/reporter/tap'],
113+
['junit', 'internal/test_runner/reporter/junit'],
113114
]);
114115

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

lib/test/reporters.js

Lines changed: 10 additions & 0 deletions
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,
Lines changed: 7 additions & 0 deletions
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)