Skip to content

Commit

Permalink
feat(vitest): filter stacktraces (fix vitest-dev#1999) (vitest-dev#4338)
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkf authored and LorenzoBloedow committed Dec 19, 2023
1 parent 76012ac commit d0ebfa8
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 2 deletions.
28 changes: 28 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,34 @@ export default defineConfig({
})
```

### onStackTrace

- **Type**: `(error: Error, frame: ParsedStack) => boolean | void`
- **Version**: Since Vitest 1.0.0-beta.3

Apply a filtering function to each frame of each stacktrace when handling errors. The first argument, `error`, is an object with the same properties as a standard `Error`, but it is not an actual instance.

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(error: Error, { file }: ParsedStack): boolean | void {
// If we've encountered a ReferenceError, show the whole stack.
if (error.name === 'ReferenceError')
return
// Reject all frames from third party libraries.
if (file.includes('node_modules'))
return false
},
},
})
```

### diff

- **Type:** `string`
Expand Down
6 changes: 5 additions & 1 deletion packages/utils/src/source-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type { SourceMapInput } from '@jridgewell/trace-mapping'
export interface StackTraceParserOptions {
ignoreStackEntries?: (RegExp | string)[]
getSourceMap?: (file: string) => unknown
frameFilter?: (error: Error, frame: ParsedStack) => boolean | void
}

const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m
Expand Down Expand Up @@ -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(f => options.frameFilter!(e, f) !== false)

e.stacks = stackFrames
return stackFrames
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/reporters/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/reporters/junit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/reporters/tap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export class WorkspaceProject {
resolveSnapshotPath: undefined,
},
onConsoleLog: undefined!,
onStackTrace: undefined!,
sequence: {
...this.ctx.config.sequence,
sequencer: undefined!,
Expand Down
11 changes: 10 additions & 1 deletion packages/vitest/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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?: (error: Error, frame: ParsedStack) => boolean | void

/**
* Indicates if CSS files should be processed.
*
Expand Down Expand Up @@ -788,6 +796,7 @@ export type ProjectConfig = Omit<
| 'resolveSnapshotPath'
| 'passWithNoTests'
| 'onConsoleLog'
| 'onStackTrace'
| 'dangerouslyIgnoreUnhandledErrors'
| 'slowTestThreshold'
| 'inspect'
Expand Down
21 changes: 21 additions & 0 deletions test/stacktraces/fixtures/error-with-stack.test.js
Original file line number Diff line number Diff line change
@@ -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!')
}
32 changes: 32 additions & 0 deletions test/stacktraces/test/__snapshots__/runner.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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:9
18|
19| function d() {
20| throw new Error('Something truly horrible has happened!')
| ^
21| }
22|
❯ c error-with-stack.test.js:16:3
❯ a error-with-stack.test.js:8:3
❯ error-with-stack.test.js:4:3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
"
`;

exports[`stacktrace should print error frame source file correctly > error-in-deps > error-in-deps 1`] = `
"⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
Expand Down Expand Up @@ -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:9
18|
19| function d() {
20| throw new Error('Something truly horrible has happened!')
| ^
21| }
22|
❯ c error-with-stack.test.js:16:3"
`;
exports[`stacktraces should respect sourcemaps > mocked-global.test.js > mocked-global.test.js 1`] = `
" ❯ mocked-global.test.js:6:13
4|
Expand Down
14 changes: 14 additions & 0 deletions test/stacktraces/test/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: (_error, { method }) => method !== 'b',
}, [testFile])

expect(stderr).toMatchSnapshot('stacktrace-filtering')
}, 30000)
})

0 comments on commit d0ebfa8

Please sign in to comment.