From 9454e9685282899a1ff5e3779d708a05136a7150 Mon Sep 17 00:00:00 2001 From: Clark Fischer Date: Thu, 19 Oct 2023 09:22:52 -0700 Subject: [PATCH] feat: filter stacktraces (fix #1999) This change introduces a new optional configuration parameter, `onStackTrace`. If included, each frame of each error stacktrace encountered during the test run will be tested by the provided function. If the test fails, the frame will be omitted from the displayed trace. --- docs/config/index.md | 22 +++++++++++++ packages/utils/src/source-map.ts | 6 +++- packages/vitest/src/node/error.ts | 1 + packages/vitest/src/node/reporters/json.ts | 1 + packages/vitest/src/node/reporters/junit.ts | 1 + packages/vitest/src/node/reporters/tap.ts | 1 + packages/vitest/src/node/workspace.ts | 1 + packages/vitest/src/types/config.ts | 11 ++++++- .../fixtures/error-with-stack.test.js | 21 ++++++++++++ .../test/__snapshots__/runner.test.ts.snap | 32 +++++++++++++++++++ test/stacktraces/test/runner.test.ts | 14 ++++++++ 11 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 test/stacktraces/fixtures/error-with-stack.test.js diff --git a/docs/config/index.md b/docs/config/index.md index 841e1e8b26d27..91bd286fbcefb 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1785,6 +1785,28 @@ export default defineConfig({ }) ``` +### onStackTrace + +- **Type**: `(frame: ParsedStack) => boolean` + +Apply a filtering function to each frame of each stacktrace when handling errors. + +Can be useful for filtering out stacktrace frames from third-party libraries. + +```ts +import type { ParsedStack } from 'vitest' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + onStackTrace({ file }: ParsedStack): boolean { + // Reject all frames from third party libraries. + return !file.includes('node_modules') + }, + }, +}) +``` + ### diff - **Type:** `string` diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index c9c8959b0d946..be5cfb48c5f9f 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -10,6 +10,7 @@ export type { SourceMapInput } from '@jridgewell/trace-mapping' export interface StackTraceParserOptions { ignoreStackEntries?: (RegExp | string)[] getSourceMap?: (file: string) => unknown + frameFilter?: (frame: ParsedStack) => boolean } const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m @@ -179,7 +180,10 @@ export function parseErrorStacktrace(e: ErrorWithDiff, options: StackTraceParser return e.stacks const stackStr = e.stack || e.stackStr || '' - const stackFrames = parseStacktrace(stackStr, options) + let stackFrames = parseStacktrace(stackStr, options) + + if (options.frameFilter) + stackFrames = stackFrames.filter(options.frameFilter) e.stacks = stackFrames return stackFrames diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts index ba01b68bebb61..7a4a0eb885558 100644 --- a/packages/vitest/src/node/error.ts +++ b/packages/vitest/src/node/error.ts @@ -49,6 +49,7 @@ export async function printError(error: unknown, project: WorkspaceProject | und const parserOptions: StackTraceParserOptions = { // only browser stack traces require remapping getSourceMap: file => project.getBrowserSourceMapModuleById(file), + frameFilter: project.config.onStackTrace, } if (fullStack) diff --git a/packages/vitest/src/node/reporters/json.ts b/packages/vitest/src/node/reporters/json.ts index 5405122a1fe1b..00c24888a572d 100644 --- a/packages/vitest/src/node/reporters/json.ts +++ b/packages/vitest/src/node/reporters/json.ts @@ -186,6 +186,7 @@ export class JsonReporter implements Reporter { const project = this.ctx.getProjectByTaskId(test.id) const stack = parseErrorStacktrace(error, { getSourceMap: file => project.getBrowserSourceMapModuleById(file), + frameFilter: this.ctx.config.onStackTrace, }) const frame = stack[0] if (!frame) diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 1a34c7b34ecac..c66cb30feba52 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -135,6 +135,7 @@ export class JUnitReporter implements Reporter { const project = this.ctx.getProjectByTaskId(task.id) const stack = parseErrorStacktrace(error, { getSourceMap: file => project.getBrowserSourceMapModuleById(file), + frameFilter: this.ctx.config.onStackTrace, }) // TODO: This is same as printStack but without colors. Find a way to reuse code. diff --git a/packages/vitest/src/node/reporters/tap.ts b/packages/vitest/src/node/reporters/tap.ts index 50fcf288bf840..0800d9ad5c14c 100644 --- a/packages/vitest/src/node/reporters/tap.ts +++ b/packages/vitest/src/node/reporters/tap.ts @@ -76,6 +76,7 @@ export class TapReporter implements Reporter { task.result.errors.forEach((error) => { const stacks = parseErrorStacktrace(error, { getSourceMap: file => project.getBrowserSourceMapModuleById(file), + frameFilter: this.ctx.config.onStackTrace, }) const stack = stacks[0] diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 8f459bfce0d4d..65d62c372f125 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -315,6 +315,7 @@ export class WorkspaceProject { resolveSnapshotPath: undefined, }, onConsoleLog: undefined!, + onStackTrace: undefined!, sequence: { ...this.ctx.config.sequence, sequencer: undefined!, diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index b5f9e83e20044..90d96da0b74d4 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -11,7 +11,7 @@ import type { JSDOMOptions } from './jsdom-options' import type { HappyDOMOptions } from './happy-dom-options' import type { Reporter } from './reporter' import type { SnapshotStateOptions } from './snapshot' -import type { Arrayable } from './general' +import type { Arrayable, ParsedStack } from './general' import type { BenchmarkUserOptions } from './benchmark' import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser' import type { Pool, PoolOptions } from './pool-options' @@ -537,6 +537,14 @@ export interface InlineConfig { */ onConsoleLog?: (log: string, type: 'stdout' | 'stderr') => false | void + /** + * Enable stack trace filtering. If absent, all stack trace frames + * will be shown. + * + * Return `false` to omit the frame. + */ + onStackTrace?: (frame: ParsedStack) => boolean + /** * Indicates if CSS files should be processed. * @@ -788,6 +796,7 @@ export type ProjectConfig = Omit< | 'resolveSnapshotPath' | 'passWithNoTests' | 'onConsoleLog' + | 'onStackTrace' | 'dangerouslyIgnoreUnhandledErrors' | 'slowTestThreshold' | 'inspect' diff --git a/test/stacktraces/fixtures/error-with-stack.test.js b/test/stacktraces/fixtures/error-with-stack.test.js new file mode 100644 index 0000000000000..7786fa9e90b1b --- /dev/null +++ b/test/stacktraces/fixtures/error-with-stack.test.js @@ -0,0 +1,21 @@ +import { test } from 'vitest' + +test('error in deps', () => { + a() +}) + +function a() { + b() +} + +function b() { + c() +} + +function c() { + d() +} + +function d() { + throw new Error('Something truly horrible has happened!') +} diff --git a/test/stacktraces/test/__snapshots__/runner.test.ts.snap b/test/stacktraces/test/__snapshots__/runner.test.ts.snap index 77080e9151588..4f5c23a909bb4 100644 --- a/test/stacktraces/test/__snapshots__/runner.test.ts.snap +++ b/test/stacktraces/test/__snapshots__/runner.test.ts.snap @@ -1,5 +1,26 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`stacktrace filtering > filters stacktraces > stacktrace-filtering 1`] = ` +"⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL error-with-stack.test.js > error in deps +Error: Something truly horrible has happened! + ❯ d error-with-stack.test.js:20:11 + 18| + 19| function d() { + 20| throw new Error("Something truly horrible has happened!") + | ^ + 21| } + 22| + ❯ c error-with-stack.test.js:16:5 + ❯ a error-with-stack.test.js:8:5 + ❯ error-with-stack.test.js:4:5 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + +" +`; + exports[`stacktrace should print error frame source file correctly > error-in-deps > error-in-deps 1`] = ` "⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ @@ -71,6 +92,17 @@ exports[`stacktraces should respect sourcemaps > error-in-deps.test.js > error-i " `; +exports[`stacktraces should respect sourcemaps > error-with-stack.test.js > error-with-stack.test.js 1`] = ` +" ❯ d error-with-stack.test.js:20:11 + 18| + 19| function d() { + 20| throw new Error("Something truly horrible has happened!") + | ^ + 21| } + 22| + ❯ c error-with-stack.test.js:16:5" +`; + exports[`stacktraces should respect sourcemaps > mocked-global.test.js > mocked-global.test.js 1`] = ` " ❯ mocked-global.test.js:6:13 4| diff --git a/test/stacktraces/test/runner.test.ts b/test/stacktraces/test/runner.test.ts index 69feb285138a6..fd25c2594f5a3 100644 --- a/test/stacktraces/test/runner.test.ts +++ b/test/stacktraces/test/runner.test.ts @@ -52,3 +52,17 @@ describe('stacktrace should print error frame source file correctly', async () = expect(stderr).toMatchSnapshot('error-in-deps') }, 30000) }) + +describe('stacktrace filtering', async () => { + const root = resolve(__dirname, '../fixtures') + const testFile = resolve(root, './error-with-stack.test.js') + + it('filters stacktraces', async () => { + const { stderr } = await runVitest({ + root, + onStackTrace: ({ method }) => method !== 'b', + }, [testFile]) + + expect(stderr).toMatchSnapshot('stacktrace-filtering') + }, 30000) +})