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