Skip to content

Commit

Permalink
fix(browser): print correct stack trace in source files (#6003)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jun 30, 2024
1 parent 6d8848e commit 62aa720
Show file tree
Hide file tree
Showing 19 changed files with 147 additions and 48 deletions.
43 changes: 43 additions & 0 deletions packages/browser/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import type {
WorkspaceProject,
} from 'vitest/node'
import { join, resolve } from 'pathe'
import type { ErrorWithDiff } from '@vitest/utils'
import { slash } from '@vitest/utils'
import type { ResolvedConfig } from 'vitest'
import { type StackTraceParserOptions, parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map'
import { BrowserServerState } from './state'
import { getBrowserProvider } from './utils'
import { BrowserServerCDPHandler } from './cdp'
Expand All @@ -33,10 +35,31 @@ export class BrowserServer implements IBrowserServer {

public vite!: Vite.ViteDevServer

private stackTraceOptions: StackTraceParserOptions

constructor(
public project: WorkspaceProject,
public base: string,
) {
this.stackTraceOptions = {
frameFilter: project.config.onStackTrace,
getSourceMap: (id) => {
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult
return result?.map
},
getFileName: (id) => {
const mod = this.vite.moduleGraph.getModuleById(id)
if (mod?.file) {
return mod.file
}
const modUrl = this.vite.moduleGraph.urlToModuleMap.get(id)
if (modUrl?.file) {
return modUrl.file
}
return id
},
}

this.state = new BrowserServerState()

const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
Expand Down Expand Up @@ -139,6 +162,26 @@ export class BrowserServer implements IBrowserServer {
})
}

public parseErrorStacktrace(
e: ErrorWithDiff,
options: StackTraceParserOptions = {},
) {
return parseErrorStacktrace(e, {
...this.stackTraceOptions,
...options,
})
}

public parseStacktrace(
trace: string,
options: StackTraceParserOptions = {},
) {
return parseStacktrace(trace, {
...this.stackTraceOptions,
...options,
})
}

private cdpSessions = new Map<string, Promise<CDPSession>>()

async ensureCDPHandler(contextId: string, sessionId: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/runner/src/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function collectTests(
const config = runner.config

for (const filepath of paths) {
const file = createFileTask(filepath, config.root, config.name)
const file = createFileTask(filepath, config.root, config.name, runner.pool)

runner.onCollectStart?.(file)

Expand Down
4 changes: 4 additions & 0 deletions packages/runner/src/types/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,8 @@ export interface VitestRunner {
* Publicly available configuration.
*/
config: VitestRunnerConfig
/**
* The name of the current pool. Can affect how stack trace is inferred on the server side.
*/
pool?: string
}
1 change: 1 addition & 0 deletions packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface Suite extends TaskBase {
}

export interface File extends Suite {
pool?: string
filepath: string
projectName: string | undefined
collectDuration?: number
Expand Down
2 changes: 2 additions & 0 deletions packages/runner/src/utils/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function createFileTask(
filepath: string,
root: string,
projectName: string,
pool?: string,
) {
const path = relative(root, filepath)
const file: File = {
Expand All @@ -130,6 +131,7 @@ export function createFileTask(
meta: Object.create(null),
projectName,
file: undefined!,
pool,
}
file.file = file
return file
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/src/source-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type { SourceMapInput } from '@jridgewell/trace-mapping'
export interface StackTraceParserOptions {
ignoreStackEntries?: (RegExp | string)[]
getSourceMap?: (file: string) => unknown
getFileName?: (id: string) => string
frameFilter?: (error: Error, frame: ParsedStack) => boolean | void
}

Expand Down Expand Up @@ -192,6 +193,10 @@ export function parseStacktrace(
)
}
return stacks.map((stack) => {
if (options.getFileName) {
stack.file = options.getFileName(stack.file)
}

const map = options.getSourceMap?.(stack.file) as
| SourceMapInput
| null
Expand Down
16 changes: 9 additions & 7 deletions packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws'
import type { StackTraceParserOptions } from '@vitest/utils/source-map'
import type { ViteDevServer } from 'vite'
import { API_PATH } from '../constants'
import type { Vitest } from '../node'
Expand Down Expand Up @@ -182,15 +181,18 @@ export class WebSocketReporter implements Reporter {

packs.forEach(([taskId, result]) => {
const project = this.ctx.getProjectByTaskId(taskId)

const parserOptions: StackTraceParserOptions = {
getSourceMap: file => project.getBrowserSourceMapModuleById(file),
}
const task = this.ctx.state.idMap.get(taskId)
const isBrowser = task && task.file.pool === 'browser'

result?.errors?.forEach((error) => {
if (!isPrimitive(error)) {
error.stacks = parseErrorStacktrace(error, parserOptions)
if (isPrimitive(error)) {
return
}

const stacks = isBrowser
? project.browser?.parseErrorStacktrace(error)
: parseErrorStacktrace(error)
error.stacks = stacks
})
})

Expand Down
29 changes: 10 additions & 19 deletions packages/vitest/src/node/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,40 @@ import { Writable } from 'node:stream'
import { normalize, relative } from 'pathe'
import c from 'picocolors'
import cliTruncate from 'cli-truncate'
import type { StackTraceParserOptions } from '@vitest/utils/source-map'
import { inspect } from '@vitest/utils'
import stripAnsi from 'strip-ansi'
import type { ErrorWithDiff, ParsedStack } from '../types'
import {
lineSplitRE,
parseErrorStacktrace,
positionToOffset,
} from '../utils/source-map'
import { F_POINTER } from '../utils/figures'
import { TypeCheckError } from '../typecheck/typechecker'
import { isPrimitive } from '../utils'
import type { Vitest } from './core'
import { divider } from './reporters/renderers/utils'
import type { ErrorOptions } from './logger'
import { Logger } from './logger'
import type { WorkspaceProject } from './workspace'

interface PrintErrorOptions {
type?: string
logger: Logger
fullStack?: boolean
showCodeFrame?: boolean
printProperties?: boolean
screenshotPaths?: string[]
parseErrorStacktrace: (error: ErrorWithDiff) => ParsedStack[]
}

interface PrintErrorResult {
export interface PrintErrorResult {
nearest?: ParsedStack
}

// use Logger with custom Console to capture entire error printing
export function capturePrintError(
error: unknown,
ctx: Vitest,
project: WorkspaceProject,
options: ErrorOptions,
) {
let output = ''
const writable = new Writable({
Expand All @@ -47,9 +46,10 @@ export function capturePrintError(
callback()
},
})
const result = printError(error, project, {
const logger = new Logger(ctx, writable, writable)
const result = logger.printError(error, {
showCodeFrame: false,
logger: new Logger(ctx, writable, writable),
...options,
})
return { nearest: result?.nearest, output }
}
Expand All @@ -59,7 +59,7 @@ export function printError(
project: WorkspaceProject | undefined,
options: PrintErrorOptions,
): PrintErrorResult | undefined {
const { showCodeFrame = true, fullStack = false, type, printProperties = true } = options
const { showCodeFrame = true, type, printProperties = true } = options
const logger = options.logger
let e = error as ErrorWithDiff

Expand All @@ -84,16 +84,7 @@ export function printError(
return
}

const parserOptions: StackTraceParserOptions = {
// only browser stack traces require remapping
getSourceMap: file => project.getBrowserSourceMapModuleById(file),
frameFilter: project.config.onStackTrace,
}

if (fullStack) {
parserOptions.ignoreStackEntries = []
}
const stacks = parseErrorStacktrace(e, parserOptions)
const stacks = options.parseErrorStacktrace(e)

const nearest
= error instanceof TypeCheckError
Expand Down Expand Up @@ -195,9 +186,9 @@ export function printError(
if (typeof e.cause === 'object' && e.cause && 'name' in e.cause) {
(e.cause as any).name = `Caused by: ${(e.cause as any).name}`
printError(e.cause, project, {
fullStack,
showCodeFrame: false,
logger: options.logger,
parseErrorStacktrace: options.parseErrorStacktrace,
})
}

Expand Down
30 changes: 24 additions & 6 deletions packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ import { Console } from 'node:console'
import type { Writable } from 'node:stream'
import { createLogUpdate } from 'log-update'
import c from 'picocolors'
import type { ErrorWithDiff } from '../types'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import type { ErrorWithDiff, Task } from '../types'
import type { TypeCheckError } from '../typecheck/typechecker'
import { toArray } from '../utils'
import { highlightCode } from '../utils/colors'
import { divider } from './reporters/renderers/utils'
import { RandomSequencer } from './sequencers/RandomSequencer'
import type { Vitest } from './core'
import type { PrintErrorResult } from './error'
import { printError } from './error'
import type { WorkspaceProject } from './workspace'

interface ErrorOptions {
export interface ErrorOptions {
type?: string
fullStack?: boolean
project?: WorkspaceProject
verbose?: boolean
screenshotPaths?: string[]
task?: Task
showCodeFrame?: boolean
}

const ESC = '\x1B['
Expand Down Expand Up @@ -89,18 +93,32 @@ export class Logger {
this.console.log(`${CURSOR_TO_START}${ERASE_DOWN}${log}`)
}

printError(err: unknown, options: ErrorOptions = {}) {
printError(err: unknown, options: ErrorOptions = {}): PrintErrorResult | undefined {
const { fullStack = false, type } = options
const project = options.project
?? this.ctx.getCoreWorkspaceProject()
?? this.ctx.projects[0]
printError(err, project, {
fullStack,
return printError(err, project, {
type,
showCodeFrame: true,
showCodeFrame: options.showCodeFrame ?? true,
logger: this,
printProperties: options.verbose,
screenshotPaths: options.screenshotPaths,
parseErrorStacktrace: (error) => {
// browser stack trace needs to be processed differently,
// so there is a separate method for that
if (options.task?.file.pool === 'browser' && project.browser) {
return project.browser.parseErrorStacktrace(error, {
ignoreStackEntries: fullStack ? [] : undefined,
})
}

// node.js stack trace already has correct source map locations
return parseErrorStacktrace(error, {
frameFilter: project.config.onStackTrace,
ignoreStackEntries: fullStack ? [] : undefined,
})
},
})
}

Expand Down
17 changes: 12 additions & 5 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,15 @@ export abstract class BaseReporter implements Reporter {
if (log.browser) {
write('\n')
}

const project = log.taskId
? this.ctx.getProjectByTaskId(log.taskId)
: this.ctx.getCoreWorkspaceProject()
const stack = parseStacktrace(log.origin, {
getSourceMap: file => project.getBrowserSourceMapModuleById(file),
frameFilter: project.config.onStackTrace,
})

const stack = log.browser
? (project.browser?.parseStacktrace(log.origin) || [])
: parseStacktrace(log.origin)

const highlight = task
? stack.find(i => i.file === task.file.filepath)
: null
Expand Down Expand Up @@ -605,7 +607,12 @@ export abstract class BaseReporter implements Reporter {
}
const screenshots = tasks.filter(t => t.meta?.failScreenshotPath).map(t => t.meta?.failScreenshotPath as string)
const project = this.ctx.getProjectByTaskId(tasks[0].id)
this.ctx.logger.printError(error, { project, verbose: this.verbose, screenshotPaths: screenshots })
this.ctx.logger.printError(error, {
project,
verbose: this.verbose,
screenshotPaths: screenshots,
task: tasks[0],
})
errorDivider()
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/vitest/src/node/reporters/github-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class GithubActionsReporter implements Reporter {
project: WorkspaceProject
title: string
error: unknown
file?: File
}>()
for (const error of errors) {
projectErrors.push({
Expand All @@ -40,14 +41,15 @@ export class GithubActionsReporter implements Reporter {
project,
title,
error,
file,
})
}
}
}

// format errors via `printError`
for (const { project, title, error } of projectErrors) {
const result = capturePrintError(error, this.ctx, project)
for (const { project, title, error, file } of projectErrors) {
const result = capturePrintError(error, this.ctx, { project, task: file })
const stack = result?.nearest
if (!stack) {
continue
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/reporters/junit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export class JUnitReporter implements Reporter {
const result = capturePrintError(
error,
this.ctx,
this.ctx.getProjectByTaskId(task.id),
{ project: this.ctx.getProjectByTaskId(task.id), task },
)
await this.baseLog(
escapeXML(stripAnsi(result.output.trim())),
Expand Down
Loading

0 comments on commit 62aa720

Please sign in to comment.