1'use strict';
2const {
3  ArrayPrototypeFilter,
4  ArrayPrototypeMap,
5  ArrayPrototypeJoin,
6  ArrayPrototypePush,
7  ArrayPrototypeSome,
8  NumberPrototypeToFixed,
9  ObjectEntries,
10  RegExpPrototypeSymbolReplace,
11  String,
12  StringPrototypeRepeat,
13} = primordials;
14
15const { inspectWithNoCustomRetry } = require('internal/errors');
16const { hostname } = require('os');
17
18const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
19const HOSTNAME = hostname();
20
21function escapeAttribute(s = '') {
22  return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, ''), '"'));
23}
24
25function escapeContent(s = '') {
26  return RegExpPrototypeSymbolReplace(/</g, RegExpPrototypeSymbolReplace(/&/g, s, '&amp;'), '&lt;');
27}
28
29function escapeComment(s = '') {
30  return RegExpPrototypeSymbolReplace(/--/g, s, '&#45;&#45;');
31}
32
33function 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
54function isFailure(node) {
55  return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'failure')) || node?.attrs?.failures;
56}
57
58function isSkipped(node) {
59  return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'skipped')) || node?.attrs?.failures;
60}
61
62module.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};
159