From 9fed286c494d46d9c2d5a9349a9675655ac5dbd9 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Sat, 27 Jul 2024 22:05:59 +0100 Subject: [PATCH] Core: Exclude or grey internal frames from stack traces in TAP reporter Internal frames are those from qunit.js, or Node.js runtime. * Remove any internal frames from the top of the stack. * Grey out later internal frames anywhere in the stack. This change is applied to the TAP reporter, which the QUnit CLI uses by default. Closes https://github.com/qunitjs/qunit/pull/1789. --- src/core/reporters/TapReporter.js | 3 +- src/core/stacktrace.js | 34 +++++++++++++++++++ .../async-module-error-promise.tap.txt | 1 - .../async-module-error-thenable.tap.txt | 1 - test/cli/fixtures/async-module-error.tap.txt | 1 - 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/core/reporters/TapReporter.js b/src/core/reporters/TapReporter.js index 326d784ff..4960760b7 100644 --- a/src/core/reporters/TapReporter.js +++ b/src/core/reporters/TapReporter.js @@ -1,6 +1,7 @@ import kleur from 'kleur'; import { errorString } from '../utilities.js'; import { console } from '../globals.js'; +import { annotateStacktrace } from '../stacktrace.js'; const hasOwn = Object.prototype.hasOwnProperty; @@ -276,7 +277,7 @@ export default class TapReporter { out += `\n message: ${prettyYamlValue(errorString(error))}`; out += `\n severity: ${prettyYamlValue('failed')}`; if (error && error.stack) { - out += `\n stack: ${prettyYamlValue(error.stack + '\n')}`; + out += `\n stack: ${prettyYamlValue(annotateStacktrace(error, kleur.grey) + '\n')}`; } out += '\n ...'; this.log(out); diff --git a/src/core/stacktrace.js b/src/core/stacktrace.js index 92e85d010..588a8c5cb 100644 --- a/src/core/stacktrace.js +++ b/src/core/stacktrace.js @@ -59,6 +59,40 @@ function qunitFileName () { const fileName = qunitFileName(); +/** + * - For internal errors from QUnit itself, remove the first qunit.js frames. + * - For errors in Node.js, format any remaining qunit.js and node:internal + * frames as internal (i.e. grey out). + */ +export function annotateStacktrace (e, formatInternal) { + if (!e || !e.stack) { + return String(e); + } + const frames = e.stack.split('\n'); + const annotated = []; + if (e.toString().indexOf(frames[0]) !== -1) { + // In Firefox and Safari e.stack starts with frame 0, but in V8 (Chrome/Node.js), + // e.stack starts first stringified message. Preserve this separately, + // so that, below, we can distinguish between internal frames on top + // (to remove) vs later internal frames (to format differently). + annotated.push(frames.shift()); + } + let initialInternal = true; + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]; + const isInternal = (frame.indexOf(fileName) !== -1 || frame.indexOf('node:internal/') !== -1); + if (!isInternal) { + initialInternal = false; + } + // Remove initial internal frames entirely. + if (!initialInternal) { + annotated.push(isInternal ? formatInternal(frame) : frame); + } + } + + return annotated.join('\n'); +} + export function extractStacktrace (e, offset) { offset = offset === undefined ? 4 : offset; diff --git a/test/cli/fixtures/async-module-error-promise.tap.txt b/test/cli/fixtures/async-module-error-promise.tap.txt index a98ee29fd..1d602ac36 100644 --- a/test/cli/fixtures/async-module-error-promise.tap.txt +++ b/test/cli/fixtures/async-module-error-promise.tap.txt @@ -9,7 +9,6 @@ not ok 1 global failure severity: failed stack: | TypeError: QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks - at qunit.js at /qunit/test/cli/fixtures/async-module-error-promise.js:1:7 at internal ... diff --git a/test/cli/fixtures/async-module-error-thenable.tap.txt b/test/cli/fixtures/async-module-error-thenable.tap.txt index 89cbbdac8..76df6e5cf 100644 --- a/test/cli/fixtures/async-module-error-thenable.tap.txt +++ b/test/cli/fixtures/async-module-error-thenable.tap.txt @@ -9,7 +9,6 @@ not ok 1 global failure severity: failed stack: | TypeError: QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks - at qunit.js at /qunit/test/cli/fixtures/async-module-error-thenable.js:1:7 at internal ... diff --git a/test/cli/fixtures/async-module-error.tap.txt b/test/cli/fixtures/async-module-error.tap.txt index 4b97c716a..ee5855bcf 100644 --- a/test/cli/fixtures/async-module-error.tap.txt +++ b/test/cli/fixtures/async-module-error.tap.txt @@ -9,7 +9,6 @@ not ok 1 global failure severity: failed stack: | TypeError: QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks - at qunit.js at /qunit/test/cli/fixtures/async-module-error.js:2:7 at internal ...