• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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