1'use strict'; 2const { 3 ArrayPrototypeForEach, 4 ArrayPrototypeJoin, 5 ArrayPrototypePush, 6 ObjectEntries, 7 RegExpPrototypeSymbolReplace, 8 RegExpPrototypeSymbolSplit, 9 SafeMap, 10 SafeSet, 11 StringPrototypeReplaceAll, 12 StringPrototypeRepeat, 13} = primordials; 14const { inspectWithNoCustomRetry } = require('internal/errors'); 15const { isError, kEmptyObject } = require('internal/util'); 16const { getCoverageReport } = require('internal/test_runner/utils'); 17const kDefaultIndent = ' '; // 4 spaces 18const kFrameStartRegExp = /^ {4}at /; 19const kLineBreakRegExp = /\n|\r\n/; 20const kDefaultTAPVersion = 13; 21const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity }; 22let testModule; // Lazy loaded due to circular dependency. 23 24function lazyLoadTest() { 25 testModule ??= require('internal/test_runner/test'); 26 return testModule; 27} 28 29 30async function * tapReporter(source) { 31 yield `TAP version ${kDefaultTAPVersion}\n`; 32 for await (const { type, data } of source) { 33 switch (type) { 34 case 'test:fail': { 35 yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo); 36 const location = `${data.file}:${data.line}:${data.column}`; 37 yield reportDetails(data.nesting, data.details, location); 38 break; 39 } case 'test:pass': 40 yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo); 41 yield reportDetails(data.nesting, data.details, null); 42 break; 43 case 'test:plan': 44 yield `${indent(data.nesting)}1..${data.count}\n`; 45 break; 46 case 'test:start': 47 yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`; 48 break; 49 case 'test:stderr': 50 case 'test:stdout': { 51 const lines = RegExpPrototypeSymbolSplit(kLineBreakRegExp, data.message); 52 for (let i = 0; i < lines.length; i++) { 53 if (lines[i].length === 0) continue; 54 yield `# ${tapEscape(lines[i])}\n`; 55 } 56 break; 57 } case 'test:diagnostic': 58 yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; 59 break; 60 case 'test:coverage': 61 yield getCoverageReport(indent(data.nesting), data.summary, '# ', ''); 62 break; 63 } 64 } 65} 66 67function reportTest(nesting, testNumber, status, name, skip, todo) { 68 let line = `${indent(nesting)}${status} ${testNumber}`; 69 70 if (name) { 71 line += ` ${tapEscape(`- ${name}`)}`; 72 } 73 74 if (skip !== undefined) { 75 line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}`; 76 } else if (todo !== undefined) { 77 line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`; 78 } 79 80 line += '\n'; 81 82 return line; 83} 84 85function reportDetails(nesting, data = kEmptyObject, location) { 86 const { error, duration_ms } = data; 87 const _indent = indent(nesting); 88 let details = `${_indent} ---\n`; 89 90 details += jsToYaml(_indent, 'duration_ms', duration_ms); 91 details += jsToYaml(_indent, 'type', data.type); 92 93 if (location) { 94 details += jsToYaml(_indent, 'location', location); 95 } 96 97 details += jsToYaml(_indent, null, error, new SafeSet()); 98 details += `${_indent} ...\n`; 99 return details; 100} 101 102const memo = new SafeMap(); 103function indent(nesting) { 104 let value = memo.get(nesting); 105 if (value === undefined) { 106 value = StringPrototypeRepeat(kDefaultIndent, nesting); 107 memo.set(nesting, value); 108 } 109 110 return value; 111} 112 113 114// In certain places, # and \ need to be escaped as \# and \\. 115function tapEscape(input) { 116 let result = StringPrototypeReplaceAll(input, '\b', '\\b'); 117 result = StringPrototypeReplaceAll(result, '\f', '\\f'); 118 result = StringPrototypeReplaceAll(result, '\t', '\\t'); 119 result = StringPrototypeReplaceAll(result, '\n', '\\n'); 120 result = StringPrototypeReplaceAll(result, '\r', '\\r'); 121 result = StringPrototypeReplaceAll(result, '\v', '\\v'); 122 result = StringPrototypeReplaceAll(result, '\\', '\\\\'); 123 result = StringPrototypeReplaceAll(result, '#', '\\#'); 124 return result; 125} 126 127function jsToYaml(indent, name, value, seen) { 128 if (value === null || value === undefined) { 129 return ''; 130 } 131 132 if (typeof value !== 'object') { 133 const prefix = `${indent} ${name}: `; 134 135 if (typeof value !== 'string') { 136 return `${prefix}${inspectWithNoCustomRetry(value, inspectOptions)}\n`; 137 } 138 139 const lines = RegExpPrototypeSymbolSplit(kLineBreakRegExp, value); 140 141 if (lines.length === 1) { 142 return `${prefix}${inspectWithNoCustomRetry(value, inspectOptions)}\n`; 143 } 144 145 let str = `${prefix}|-\n`; 146 147 for (let i = 0; i < lines.length; i++) { 148 str += `${indent} ${lines[i]}\n`; 149 } 150 151 return str; 152 } 153 154 seen.add(value); 155 const entries = ObjectEntries(value); 156 const isErrorObj = isError(value); 157 let result = ''; 158 let propsIndent = indent; 159 160 if (name != null) { 161 result += `${indent} ${name}:\n`; 162 propsIndent += ' '; 163 } 164 165 for (let i = 0; i < entries.length; i++) { 166 const { 0: key, 1: value } = entries[i]; 167 168 if (isErrorObj && (key === 'cause' || key === 'code')) { 169 continue; 170 } 171 if (seen.has(value)) { 172 result += `${propsIndent} ${key}: <Circular>\n`; 173 continue; 174 } 175 176 result += jsToYaml(propsIndent, key, value, seen); 177 } 178 179 if (isErrorObj) { 180 const { kUnwrapErrors } = lazyLoadTest(); 181 const { 182 cause, 183 code, 184 failureType, 185 message, 186 expected, 187 actual, 188 operator, 189 stack, 190 name, 191 } = value; 192 let errMsg = message ?? '<unknown error>'; 193 let errName = name; 194 let errStack = stack; 195 let errCode = code; 196 let errExpected = expected; 197 let errActual = actual; 198 let errOperator = operator; 199 let errIsAssertion = isAssertionLike(value); 200 201 // If the ERR_TEST_FAILURE came from an error provided by user code, 202 // then try to unwrap the original error message and stack. 203 if (code === 'ERR_TEST_FAILURE' && kUnwrapErrors.has(failureType)) { 204 errStack = cause?.stack ?? errStack; 205 errCode = cause?.code ?? errCode; 206 errName = cause?.name ?? errName; 207 errMsg = cause?.message ?? errMsg; 208 209 if (isAssertionLike(cause)) { 210 errExpected = cause.expected; 211 errActual = cause.actual; 212 errOperator = cause.operator ?? errOperator; 213 errIsAssertion = true; 214 } 215 } 216 217 result += jsToYaml(indent, 'error', errMsg, seen); 218 219 if (errCode) { 220 result += jsToYaml(indent, 'code', errCode, seen); 221 } 222 if (errName && errName !== 'Error') { 223 result += jsToYaml(indent, 'name', errName, seen); 224 } 225 226 if (errIsAssertion) { 227 result += jsToYaml(indent, 'expected', errExpected, seen); 228 result += jsToYaml(indent, 'actual', errActual, seen); 229 if (errOperator) { 230 result += jsToYaml(indent, 'operator', errOperator, seen); 231 } 232 } 233 234 if (typeof errStack === 'string') { 235 const frames = []; 236 237 ArrayPrototypeForEach( 238 RegExpPrototypeSymbolSplit(kLineBreakRegExp, errStack), 239 (frame) => { 240 const processed = RegExpPrototypeSymbolReplace( 241 kFrameStartRegExp, 242 frame, 243 '', 244 ); 245 246 if (processed.length > 0 && processed.length !== frame.length) { 247 ArrayPrototypePush(frames, processed); 248 } 249 }, 250 ); 251 252 if (frames.length > 0) { 253 const frameDelimiter = `\n${indent} `; 254 255 result += `${indent} stack: |-${frameDelimiter}`; 256 result += `${ArrayPrototypeJoin(frames, frameDelimiter)}\n`; 257 } 258 } 259 } 260 261 return result; 262} 263 264function isAssertionLike(value) { 265 return value && typeof value === 'object' && 'expected' in value && 'actual' in value; 266} 267 268module.exports = tapReporter; 269