Skip to content

Commit

Permalink
test_runner: display failed test stack trace with dot reporter
Browse files Browse the repository at this point in the history
PR-URL: #52655
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
  • Loading branch information
mihir254 authored and targos committed May 8, 2024
1 parent 7d7a762 commit 3267b3c
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 84 deletions.
14 changes: 12 additions & 2 deletions lib/internal/test_runner/reporter/dot.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
'use strict';

const {
ArrayPrototypePush,
MathMax,
} = primordials;
const colors = require('internal/util/colors');
const { formatTestReport } = require('internal/test_runner/reporter/utils');

module.exports = async function* dot(source) {
let count = 0;
let columns = getLineLength();
for await (const { type } of source) {
const failedTests = [];
for await (const { type, data } of source) {
if (type === 'test:pass') {
yield '.';
}
if (type === 'test:fail') {
yield 'X';
ArrayPrototypePush(failedTests, data);
}
if ((type === 'test:fail' || type === 'test:pass') && ++count === columns) {
yield '\n';
Expand All @@ -23,6 +27,12 @@ module.exports = async function* dot(source) {
}
}
yield '\n';
if (failedTests.length > 0) {
yield `\n${colors.red}Failed tests:${colors.white}\n\n`;
for (const test of failedTests) {
yield formatTestReport('test:fail', test);
}
}
};

function getLineLength() {
Expand Down
89 changes: 14 additions & 75 deletions lib/internal/test_runner/reporter/spec.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,35 @@
'use strict';

const {
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeUnshift,
RegExpPrototypeSymbolSplit,
SafeMap,
StringPrototypeRepeat,
hardenRegExp,
} = primordials;
const assert = require('assert');
const Transform = require('internal/streams/transform');
const { inspectWithNoCustomRetry } = require('internal/errors');
const colors = require('internal/util/colors');
const { kSubtestsFailed } = require('internal/test_runner/test');
const { getCoverageReport } = require('internal/test_runner/utils');
const { relative } = require('path');
const {
formatTestReport,
indent,
reporterColorMap,
reporterUnicodeSymbolMap,
} = require('internal/test_runner/reporter/utils');

const symbols = {
'__proto__': null,
'test:fail': '\u2716 ',
'test:pass': '\u2714 ',
'test:diagnostic': '\u2139 ',
'test:coverage': '\u2139 ',
'arrow:right': '\u25B6 ',
'hyphen:minus': '\uFE63 ',
};
class SpecReporter extends Transform {
#stack = [];
#reported = [];
#indentMemo = new SafeMap();
#failedTests = [];
#cwd = process.cwd();
#inspectOptions;
#colors;

constructor() {
super({ __proto__: null, writableObjectMode: true });
colors.refresh();
this.#inspectOptions = { __proto__: null, colors: colors.shouldColorize(process.stdout), breakLength: Infinity };
this.#colors = {
'__proto__': null,
'test:fail': colors.red,
'test:pass': colors.green,
'test:diagnostic': colors.blue,
};
}

#indent(nesting) {
let value = this.#indentMemo.get(nesting);
if (value === undefined) {
value = StringPrototypeRepeat(' ', nesting);
this.#indentMemo.set(nesting, value);
}

return value;
}
#formatError(error, indent) {
if (!error) return '';
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
const message = ArrayPrototypeJoin(
RegExpPrototypeSymbolSplit(
hardenRegExp(/\r?\n/),
inspectWithNoCustomRetry(err, this.#inspectOptions),
), `\n${indent} `);
return `\n${indent} ${message}\n`;
}
#formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) {
let color = this.#colors[type] ?? colors.white;
let symbol = symbols[type] ?? ' ';
const { skip, todo } = data;
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : '';
let title = `${data.name}${duration_ms}`;

if (skip !== undefined) {
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
} else if (todo !== undefined) {
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
}
const error = this.#formatError(data.details?.error, indent);
if (hasChildren) {
// If this test has had children - it was already reported, so slightly modify the output
const err = !error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`;
return `${prefix}${indent}${color}${symbols['arrow:right']}${colors.white}${title}${err}`;
}
if (skip !== undefined) {
color = colors.gray;
symbol = symbols['hyphen:minus'];
}
return `${prefix}${indent}${color}${symbol}${title}${colors.white}${error}`;
}
#handleTestReportEvent(type, data) {
const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event
if (subtest) {
Expand All @@ -106,15 +44,15 @@ class SpecReporter extends Transform {
assert(parent.type === 'test:start');
const msg = parent.data;
ArrayPrototypeUnshift(this.#reported, msg);
prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`;
prefix += `${indent(msg.nesting)}${reporterUnicodeSymbolMap['arrow:right']}${msg.name}\n`;
}
let hasChildren = false;
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
ArrayPrototypeShift(this.#reported);
hasChildren = true;
}
const indent = this.#indent(data.nesting);
return `${this.#formatTestReport(type, data, prefix, indent, hasChildren)}\n`;
const indentation = indent(data.nesting);
return `${formatTestReport(type, data, prefix, indentation, hasChildren)}\n`;
}
#handleEvent({ type, data }) {
switch (type) {
Expand All @@ -132,9 +70,10 @@ class SpecReporter extends Transform {
case 'test:stdout':
return data.message;
case 'test:diagnostic':
return `${this.#colors[type]}${this.#indent(data.nesting)}${symbols[type]}${data.message}${colors.white}\n`;
return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
case 'test:coverage':
return getCoverageReport(this.#indent(data.nesting), data.summary, symbols['test:coverage'], colors.blue, true);
return getCoverageReport(indent(data.nesting), data.summary,
reporterUnicodeSymbolMap['test:coverage'], colors.blue, true);
}
}
_transform({ type, data }, encoding, callback) {
Expand All @@ -145,10 +84,10 @@ class SpecReporter extends Transform {
callback(null, '');
return;
}
const results = [`\n${this.#colors['test:fail']}${symbols['test:fail']}failing tests:${colors.white}\n`];
const results = [`\n${reporterColorMap['test:fail']}${reporterUnicodeSymbolMap['test:fail']}failing tests:${colors.white}\n`];
for (let i = 0; i < this.#failedTests.length; i++) {
const test = this.#failedTests[i];
const formattedErr = this.#formatTestReport('test:fail', test);
const formattedErr = formatTestReport('test:fail', test);

if (test.file) {
const relPath = relative(this.#cwd, test.file);
Expand Down
93 changes: 93 additions & 0 deletions lib/internal/test_runner/reporter/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';
const {
ArrayPrototypeJoin,
RegExpPrototypeSymbolSplit,
SafeMap,
StringPrototypeRepeat,
hardenRegExp,
} = primordials;
const colors = require('internal/util/colors');
const { inspectWithNoCustomRetry } = require('internal/errors');
const indentMemo = new SafeMap();

const inspectOptions = {
__proto__: null,
colors: colors.shouldColorize(process.stdout),
breakLength: Infinity,
};

const reporterUnicodeSymbolMap = {
'__proto__': null,
'test:fail': '\u2716 ',
'test:pass': '\u2714 ',
'test:diagnostic': '\u2139 ',
'test:coverage': '\u2139 ',
'arrow:right': '\u25B6 ',
'hyphen:minus': '\uFE63 ',
};

const reporterColorMap = {
'__proto__': null,
get 'test:fail'() {
return colors.red;
},
get 'test:pass'() {
return colors.green;
},
get 'test:diagnostic'() {
return colors.blue;
},
};

function indent(nesting) {
let value = indentMemo.get(nesting);
if (value === undefined) {
value = StringPrototypeRepeat(' ', nesting);
indentMemo.set(nesting, value);
}
return value;
}

function formatError(error, indent) {
if (!error) return '';
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
const message = ArrayPrototypeJoin(
RegExpPrototypeSymbolSplit(
hardenRegExp(/\r?\n/),
inspectWithNoCustomRetry(err, inspectOptions),
), `\n${indent} `);
return `\n${indent} ${message}\n`;
}

function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) {
let color = reporterColorMap[type] ?? colors.white;
let symbol = reporterUnicodeSymbolMap[type] ?? ' ';
const { skip, todo } = data;
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : '';
let title = `${data.name}${duration_ms}`;

if (skip !== undefined) {
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
} else if (todo !== undefined) {
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
}
const error = formatError(data.details?.error, indent);
if (hasChildren) {
// If this test has had children - it was already reported, so slightly modify the output
const err = !error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`;
return `${prefix}${indent}${color}${reporterUnicodeSymbolMap['arrow:right']}${colors.white}${title}${err}`;
}
if (skip !== undefined) {
color = colors.gray;
symbol = reporterUnicodeSymbolMap['hyphen:minus'];
}
return `${prefix}${indent}${color}${symbol}${title}${colors.white}${error}`;
}

module.exports = {
__proto__: null,
reporterUnicodeSymbolMap,
reporterColorMap,
formatTestReport,
indent,
};
Loading

0 comments on commit 3267b3c

Please sign in to comment.