Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): make api parse error stacks and return sourcePos in onTaskUpdate #2563

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/ui/client/components/views/ViewEditor.vue
Expand Up @@ -79,12 +79,12 @@ watch([cm, failed], ([cmValue]) => {
const e = i.result?.error
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
if (stacks.length) {
const pos = stacks[0].sourcePos || stacks[0]
const stack = stacks[0]
const div = document.createElement('div')
div.className = 'op80 flex gap-x-2 items-center'
const pre = document.createElement('pre')
pre.className = 'c-red-600 dark:c-red-400'
pre.textContent = `${' '.repeat(pos.column)}^ ${e?.nameStr}: ${e?.message}`
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}`
div.appendChild(pre)
const span = document.createElement('span')
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
Expand All @@ -95,12 +95,12 @@ watch([cm, failed], ([cmValue]) => {
placement: 'bottom',
}, false)
const el: EventListener = async () => {
await openInEditor(stacks[0].file, pos.line, pos.column)
await openInEditor(stacks[0].file, stack.line, stack.column)
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(cm.value!.addLineClass(pos.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(pos.line - 1, div))
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
}
})
if (!hasBeenEdited.value)
Expand Down
9 changes: 2 additions & 7 deletions packages/ui/client/components/views/ViewReport.cy.tsx
Expand Up @@ -9,10 +9,6 @@ const stackRowSelector = '[data-testid=stack]'
const makeTextStack = () => ({
line: faker.datatype.number({ min: 0, max: 120 }),
column: faker.datatype.number({ min: 0, max: 5000 }),
sourcePos: {
line: faker.datatype.number({ min: 121, max: 240 }),
column: faker.datatype.number({ min: 5001, max: 10000 }),
},
// Absolute file paths
file: faker.system.filePath(),
method: faker.hacker.verb(),
Expand Down Expand Up @@ -49,9 +45,8 @@ describe('ViewReport', () => {
cy.get(stackRowSelector).should('have.length', stacks.length)
.get(stackRowSelector)
.each(($stack, idx) => {
const { column, line, file: fileName, sourcePos } = stacks[idx]
expect($stack).not.to.contain.text(`${line}:${column}`)
expect($stack).to.contain.text(`${sourcePos.line}:${sourcePos.column}`)
const { column, line, file: fileName } = stacks[idx]
expect($stack).to.contain.text(`${line}:${column}`)
expect($stack).to.contain.text(`- ${fileName}`)
})
})
Expand Down
12 changes: 2 additions & 10 deletions packages/ui/client/components/views/ViewReport.vue
Expand Up @@ -88,14 +88,6 @@ function relative(p: string) {
return p
}

function line(stack: ParsedStack) {
return stack.sourcePos?.line ?? stack.line
}

function column(stack: ParsedStack) {
return stack.sourcePos?.column ?? stack.column
}

interface Diff { error: NonNullable<Pick<ErrorWithDiff, 'expected' | 'actual'>> }
type ResultWithDiff = Task['result'] & Diff
function isDiffShowable(result?: Task['result']): result is ResultWithDiff {
Expand Down Expand Up @@ -128,14 +120,14 @@ function diff(result: ResultWithDiff): string {
<div v-else-if="task.result?.error" class="scrolls scrolls-rounded task-error">
<pre><b>{{ task.result.error.name || task.result.error.nameStr }}</b>: {{ task.result.error.message }}</pre>
<div v-for="(stack, i) of task.result.error.stacks" :key="i" class="op80 flex gap-x-2 items-center" data-testid="stack">
<pre> - {{ relative(stack.file) }}:{{ line(stack) }}:{{ column(stack) }}</pre>
<pre> - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}</pre>
<div
v-if="shouldOpenInEditor(stack.file, props.file?.name)"
v-tooltip.bottom="'Open in Editor'"
class="i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em"
tabindex="0"
aria-label="Open in Editor"
@click.passive="openInEditor(stack.file, line(stack), column(stack))"
@click.passive="openInEditor(stack.file, stack.line, stack.column)"
/>
</div>
<pre v-if="isDiffShowable(task.result)">
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/api/setup.ts
@@ -1,4 +1,5 @@
import { promises as fs } from 'node:fs'

import type { BirpcReturn } from 'birpc'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
Expand All @@ -8,6 +9,7 @@ import { API_PATH } from '../constants'
import type { Vitest } from '../node'
import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types'
import { getModuleGraph } from '../utils'
import { parseStacktrace } from '../utils/source-map'
import type { TransformResultWithSource, WebSocketEvents, WebSocketHandlers } from './types'

export function setup(ctx: Vitest) {
Expand Down Expand Up @@ -121,6 +123,11 @@ class WebSocketReporter implements Reporter {
if (this.clients.size === 0)
return

packs.forEach(([, result]) => {
if (result?.error)
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
result.error.stacks = parseStacktrace(result.error)
})

this.clients.forEach((client) => {
client.onTaskUpdate?.(packs)
})
Expand Down
36 changes: 11 additions & 25 deletions packages/vitest/src/node/error.ts
@@ -1,6 +1,6 @@
/* eslint-disable prefer-template */
import { existsSync, readFileSync } from 'fs'
import { join, normalize, relative } from 'pathe'
import { normalize, relative } from 'pathe'
import c from 'picocolors'
import cliTruncate from 'cli-truncate'
import type { ErrorWithDiff, ParsedStack, Position } from '../types'
Expand All @@ -13,12 +13,6 @@ import type { Vitest } from './core'
import { divider } from './reporters/renderers/utils'
import type { Logger } from './logger'

export function fileFromParsedStack(stack: ParsedStack) {
if (stack?.sourcePos?.source?.startsWith('..'))
return join(stack.file, '../', stack.sourcePos.source)
return stack.file
}

interface PrintErrorOptions {
type?: string
fullStack?: boolean
Expand Down Expand Up @@ -64,15 +58,10 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro
ctx.logger.error(c.yellow(e.frame))
}
else {
printStack(ctx, stacks, nearest, errorProperties, (s, pos) => {
printStack(ctx, stacks, nearest, errorProperties, (s) => {
if (showCodeFrame && s === nearest && nearest) {
const file = fileFromParsedStack(nearest)
// could point to non-existing original file
// for example, when there is a source map file, but no source in node_modules
if (nearest.file === file || existsSync(file)) {
const sourceCode = readFileSync(file, 'utf-8')
ctx.logger.error(c.yellow(generateCodeFrame(sourceCode, 4, pos)))
}
const sourceCode = readFileSync(nearest.file, 'utf-8')
ctx.logger.error(c.yellow(generateCodeFrame(sourceCode, 4, s)))
}
})
}
Expand Down Expand Up @@ -181,21 +170,19 @@ function printStack(
stack: ParsedStack[],
highlight: ParsedStack | undefined,
errorProperties: Record<string, unknown>,
onStack?: ((stack: ParsedStack, pos: Position) => void),
onStack?: ((stack: ParsedStack) => void),
) {
if (!stack.length)
return

const logger = ctx.logger

for (const frame of stack) {
const pos = frame.sourcePos || frame
const color = frame === highlight ? c.yellow : c.gray
const file = fileFromParsedStack(frame)
const path = relative(ctx.config.root, file)
const path = relative(ctx.config.root, frame.file)

logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`))
onStack?.(frame, pos)
logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${frame.line}:${frame.column}`)].filter(Boolean).join(' ')}`))
onStack?.(frame)

// reached at test file, skip the follow stack
if (frame.file in ctx.state.filesMap)
Expand All @@ -213,12 +200,11 @@ function printStack(
export function generateCodeFrame(
source: string,
indent = 0,
start: number | Position = 0,
end?: number,
pos: Position,
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
range = 2,
): string {
start = posToNumber(source, start)
end = end || start
const start = posToNumber(source, pos)
const end = start
const lines = source.split(lineSplitRE)
let count = 0
let res: string[] = []
Expand Down
3 changes: 1 addition & 2 deletions packages/vitest/src/node/reporters/json.ts
Expand Up @@ -188,7 +188,6 @@ export class JsonReporter implements Reporter {
if (!frame)
return

const pos = frame.sourcePos || frame
return { line: pos.line, column: pos.column }
return { line: frame.line, column: frame.column }
}
}
3 changes: 1 addition & 2 deletions packages/vitest/src/node/reporters/junit.ts
Expand Up @@ -127,10 +127,9 @@ export class JUnitReporter implements Reporter {

// TODO: This is same as printStack but without colors. Find a way to reuse code.
for (const frame of stack) {
const pos = frame.sourcePos ?? frame
const path = relative(this.ctx.config.root, frame.file)

await this.baseLog(` ${F_POINTER} ${[frame.method, `${path}:${pos.line}:${pos.column}`].filter(Boolean).join(' ')}`)
await this.baseLog(` ${F_POINTER} ${[frame.method, `${path}:${frame.line}:${frame.column}`].filter(Boolean).join(' ')}`)

// reached at test file, skip the follow stack
if (frame.file in this.ctx.state.filesMap)
Expand Down
4 changes: 0 additions & 4 deletions packages/vitest/src/typecheck/typechecker.ts
Expand Up @@ -149,10 +149,6 @@ export class Typechecker {
line: info.line,
column: info.column,
method: '',
sourcePos: {
line: info.line,
column: info.column,
},
},
])
Error.stackTraceLimit = limit
Expand Down
1 change: 0 additions & 1 deletion packages/vitest/src/types/general.ts
Expand Up @@ -58,7 +58,6 @@ export interface ParsedStack {
file: string
line: number
column: number
sourcePos?: Position
}

export interface ErrorWithDiff extends Error {
Expand Down
4 changes: 1 addition & 3 deletions packages/vitest/src/utils/source-map.ts
Expand Up @@ -93,10 +93,8 @@ export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] {

export function posToNumber(
source: string,
pos: number | Position,
pos: Position,
): number {
if (typeof pos === 'number')
return pos
const lines = source.split(lineSplitRE)
const { line, column } = pos
let start = 0
Expand Down