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, '&'), '<'); 27} 28 29function escapeComment(s = '') { 30 return RegExpPrototypeSymbolReplace(/--/g, s, '--'); 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