• 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 = { __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