diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 40b5996461..5af6e7bd8a 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -221,6 +221,56 @@ var generateDiff = (exports.generateDiff = function (actual, expected) { } }); +/** + * Traverses err.cause and returns all stack traces + * + * @private + * @param {Error} err + * @param {Set} [seen] + * @return {{ message: string, msg: string, stack: string }} + */ +var getFullErrorStack = function (err, seen) { + if (seen && seen.has(err)) { + return { message: '', msg: '', stack: '' }; + } + + var message; + + if (typeof err.inspect === 'function') { + message = err.inspect() + ''; + } else if (err.message && typeof err.message.toString === 'function') { + message = err.message + ''; + } else { + message = ''; + } + + var msg; + var stack = err.stack || message; + var index = message ? stack.indexOf(message) : -1; + + if (index === -1) { + msg = message; + } else { + index += message.length; + msg = stack.slice(0, index); + // remove msg from stack + stack = stack.slice(index + 1); + + if (err.cause) { + seen = seen || new Set(); + seen.add(err); + const causeStack = getFullErrorStack(err.cause, seen) + stack += '\n Caused by: ' + causeStack.msg + (causeStack.stack ? '\n' + causeStack.stack : ''); + } + } + + return { + message, + msg, + stack + }; +}; + /** * Outputs the given `failures` as a list. * @@ -241,7 +291,6 @@ exports.list = function (failures) { color('error stack', '\n%s\n'); // msg - var msg; var err; if (test.err && test.err.multiple) { if (multipleTest !== test) { @@ -252,25 +301,8 @@ exports.list = function (failures) { } else { err = test.err; } - var message; - if (typeof err.inspect === 'function') { - message = err.inspect() + ''; - } else if (err.message && typeof err.message.toString === 'function') { - message = err.message + ''; - } else { - message = ''; - } - var stack = err.stack || message; - var index = message ? stack.indexOf(message) : -1; - if (index === -1) { - msg = message; - } else { - index += message.length; - msg = stack.slice(0, index); - // remove msg from stack - stack = stack.slice(index + 1); - } + var { message, msg, stack } = getFullErrorStack(err); // uncaught if (err.uncaught) { diff --git a/lib/runner.js b/lib/runner.js index 12807725fb..60a19f0e3f 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -443,11 +443,22 @@ Runner.prototype.fail = function (test, err, force) { err = thrown2Error(err); } - try { - err.stack = - this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack); - } catch (ignore) { - // some environments do not take kindly to monkeying with the stack + // Filter the stack traces + if (!this.fullStackTrace) { + const alreadyFiltered = new Set(); + let currentErr = err; + + while (currentErr && currentErr.stack && !alreadyFiltered.has(currentErr)) { + alreadyFiltered.add(currentErr); + + try { + currentErr.stack = stackFilter(currentErr.stack); + } catch (ignore) { + // some environments do not take kindly to monkeying with the stack + } + + currentErr = currentErr.cause; + } } this.emit(constants.EVENT_TEST_FAIL, test, err); diff --git a/test/reporters/base.spec.js b/test/reporters/base.spec.js index 6b30b2ccc7..84778f3693 100644 --- a/test/reporters/base.spec.js +++ b/test/reporters/base.spec.js @@ -491,6 +491,108 @@ describe('Base reporter', function () { expect(errOut, 'to be', '1) test title:\n Error\n foo\n bar'); }); + describe('error causes', function () { + it('should append any error cause trail to stack traces', function () { + var err = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + cause: { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: { + message: 'Cause2', + stack: 'Cause2\nabc\nxyz', + showDiff: false + } + } + }; + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by: Cause1\n bar\n foo\n Caused by: Cause2\n abc\n xyz' + ); + }); + + it('should not get stuck in a hypothetical circular error cause trail', function () { + var err1 = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + }; + var err2 = { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: err1 + } + err1.cause = err2; + + var test = makeTest(err1); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by: Cause1\n bar\n foo\n Caused by: ' + ); + }); + + it("should set an empty cause if neither 'inspect' nor 'message' is set", function () { + var err = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + cause: { + showDiff: false, + } + }; + + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by:' + ); + }); + + it('should not add cause trail if error does not contain message', function () { + var err = { + message: 'Error', + stack: 'foo\nbar', + showDiff: false, + cause: { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: { + message: 'Cause2', + stack: 'Cause2\nabc\nxyz', + showDiff: false + } + } + }; + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect(errOut, 'to be', '1) test title:\n Error\n foo\n bar'); + }); + }); + it('should list multiple Errors per test', function () { var err = new Error('First Error'); err.multiple = [new Error('Second Error - same test')]; diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js index dd96558017..66ca6a0532 100644 --- a/test/unit/runner.spec.js +++ b/test/unit/runner.spec.js @@ -629,6 +629,22 @@ describe('Runner', function () { }); runner.fail(hook, err); }); + + it('should prettify stack-traces in error cause trail', function (done) { + var hook = new Hook(); + hook.parent = suite; + var causeErr = new Error(); + // Fake stack-trace + causeErr.stack = stack.join('\n'); + var err = new Error(); + err.cause = causeErr; + + runner.on(EVENT_TEST_FAIL, function (_hook, _err) { + expect(_err.cause.stack, 'to be', stack.slice(0, 3).join('\n')); + done(); + }); + runner.fail(hook, err); + }); }); describe('long', function () { @@ -647,6 +663,24 @@ describe('Runner', function () { }); runner.fail(hook, err); }); + + it('should display full stack-traces in error cause trail', function (done) { + var hook = new Hook(); + hook.parent = suite; + var causeErr = new Error(); + // Fake stack-trace + causeErr.stack = stack.join('\n'); + var err = new Error(); + err.cause = causeErr; + // Add --stack-trace option + runner.fullStackTrace = true; + + runner.on(EVENT_TEST_FAIL, function (_hook, _err) { + expect(_err.cause.stack, 'to be', stack.join('\n')); + done(); + }); + runner.fail(hook, err); + }); }); describe('ginormous', function () {