diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 4a0bae09b9..65918772bb 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -83,16 +83,32 @@ class Base { }); runner.on(EVENT_TEST_FAIL, function (test, err) { - if (showDiff(err)) { - stringifyDiffObjs(err); + try { + if (showDiff(err)) { + stringifyDiffObjs(err); + } + // more than one error per test + if (test.err && err instanceof Error) { + test.err.multiple = (test.err.multiple || []).concat(err); + } else { + test.err = err; + } + failures.push(test); + } catch (listenerErr) { + try { + process.stderr.write( + "\n[mocha] reporter error while handling test failure: " + + (listenerErr && listenerErr.stack + ? listenerErr.stack + : String(listenerErr)) + + "\n", + ); + } catch {} + try { + if (!test.err) test.err = err; + failures.push(test); + } catch {} } - // more than one error per test - if (test.err && err instanceof Error) { - test.err.multiple = (test.err.multiple || []).concat(err); - } else { - test.err = err; - } - failures.push(test); }); } @@ -331,23 +347,28 @@ function stringifyDiffObjs(err) { if (!utils.isString(err.actual) || !utils.isString(err.expected)) { // Estimate size before stringifying to avoid hangs const maxSafeSize = exports.maxDiffSize || 8192; - const actualSize = estimateSize(err.actual, 10); - const expectedSize = estimateSize(err.expected, 10); - - if ( - actualSize === -1 || - expectedSize === -1 || - actualSize > maxSafeSize || - expectedSize > maxSafeSize - ) { - // Values too large/complex - provide safe fallback - err.actual = "[object too large to diff]"; - err.expected = "[object too large to diff]"; - return; - } + try { + const actualSize = estimateSize(err.actual, 10); + const expectedSize = estimateSize(err.expected, 10); + + if ( + actualSize === -1 || + expectedSize === -1 || + actualSize > maxSafeSize || + expectedSize > maxSafeSize + ) { + // Values too large/complex - provide safe fallback + err.actual = "[object too large to diff]"; + err.expected = "[object too large to diff]"; + return; + } - err.actual = utils.stringify(err.actual); - err.expected = utils.stringify(err.expected); + err.actual = utils.stringify(err.actual); + err.expected = utils.stringify(err.expected); + } catch { + err.actual = "[object could not be stringified]"; + err.expected = "[object could not be stringified]"; + } } } @@ -456,6 +477,21 @@ exports.list = function (failures) { var multipleErr, multipleTest; Base.consoleLog(); failures.forEach(function (test, i) { + try { + renderOneFailure(test, i); + } catch (renderErr) { + try { + Base.consoleLog( + " %s) %s:\n [mocha] failed to render error: %s", + i + 1, + test && test.titlePath ? test.titlePath().join(" ") : "", + renderErr && renderErr.message ? renderErr.message : renderErr, + ); + } catch {} + } + }); + + function renderOneFailure(test, i) { // format var fmt = color("error title", " %s) %s:\n") + @@ -507,7 +543,7 @@ exports.list = function (failures) { }); Base.consoleLog(fmt, i + 1, testTitle, msg, stack); - }); + } }; /** diff --git a/test/integration/fixtures/reporters/listener-throws.fixture.js b/test/integration/fixtures/reporters/listener-throws.fixture.js new file mode 100644 index 0000000000..155a740cea --- /dev/null +++ b/test/integration/fixtures/reporters/listener-throws.fixture.js @@ -0,0 +1,25 @@ +"use strict"; + +const assert = require("node:assert"); + +it("listener-throws", () => { + const poison = new Proxy( + {}, + { + ownKeys() { + throw new TypeError("poisoned ownKeys"); + }, + getOwnPropertyDescriptor() { + throw new TypeError("poisoned descriptor"); + }, + get() { + throw new TypeError("poisoned get"); + }, + }, + ); + throw new assert.AssertionError({ + actual: poison, + expected: { a: 1 }, + message: "boom", + }); +}); diff --git a/test/integration/reporter-crash.spec.js b/test/integration/reporter-crash.spec.js new file mode 100644 index 0000000000..b489e749ba --- /dev/null +++ b/test/integration/reporter-crash.spec.js @@ -0,0 +1,34 @@ +"use strict"; + +const { invokeMochaAsync, resolveFixturePath } = require("./helpers"); + +describe("reporter resilience", function () { + this.timeout(10000); + + async function runFixture() { + const [, promise] = invokeMochaAsync( + [resolveFixturePath("reporters/listener-throws")], + { stdio: "pipe" }, + ); + return promise; + } + + it("should exit non-zero when a Base listener throws during EVENT_TEST_FAIL", async function () { + const res = await runFixture(); + expect(res, "to have failed"); + }); + + it("should log a reporter-error message to stderr", async function () { + const res = await runFixture(); + expect( + res.output, + "to match", + /\[mocha\] (reporter error|failed to render)/, + ); + }); + + it("should still report the failing test in the epilougue", async function () { + const res = await runFixture(); + expect(res.output, "to match", /1 failing/); + }); +}); diff --git a/test/reporters/base.spec.js b/test/reporters/base.spec.js index d74e009e54..4a89085586 100644 --- a/test/reporters/base.spec.js +++ b/test/reporters/base.spec.js @@ -593,6 +593,57 @@ describe("Base reporter", function () { }); }); + describe("reporter resilience", function () { + // "resilience" sounds dramatic or....dramatic. + it("should not throw out of Base.list when err.actual triggers a throw in showDiff", function () { + var poison = new Proxy( + {}, + { + ownKeys: function () { + throw new TypeError("poisoned ownKeys"); + }, + getOwnPropertyDescriptor: function () { + throw new TypeError("poisoned descriptor"); + }, + get: function () { + throw new TypeError("poisoned get"); + }, + }, + ); + var err = new AssertionError({ + actual: poison, + expected: { a: 1 }, + message: "boom", + }); + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join("\n"); + expect(errOut, "to contain", "test title"); + }); + + it("should swallow errors thrown by stringifyDiffObjs and substitute a placeholder", function () { + var utils = require("../../lib/utils"); + sinon + .stub(utils, "stringify") + .throws(new Error("simulated stringify failure")); + try { + var err = new AssertionError({ + actual: { a: 1 }, + expected: { a: 2 }, + message: "boom", + }); + var test = makeTest(err); + list([test]); + var errOut = stdout.join("\n"); + expect(errOut, "to contain", "test title"); + } finally { + sinon.restore(); + } + }); + }); + it("should list multiple Errors per test", function () { var err = new Error("First Error"); err.multiple = [new Error("Second Error - same test")];