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 all 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
@@ -1,6 +1,6 @@
import { promises as fs } from 'fs'
import type MagicString from 'magic-string'
import { lineSplitRE, numberToPos, posToNumber } from '../../../utils/source-map'
import { lineSplitRE, offsetToLineNumber, positionToOffset } from '../../../utils/source-map'
import { getCallLastIndex } from '../../../utils'

export interface InlineSnapshot {
Expand All @@ -21,7 +21,7 @@ export async function saveInlineSnapshots(
const s = new MagicString(code)

for (const snap of snaps) {
const index = posToNumber(code, snap)
const index = positionToOffset(code, snap.line, snap.column)
replaceInlineSnap(code, s, index, snap.snapshot)
}

Expand Down Expand Up @@ -50,8 +50,8 @@ function replaceObjectSnap(code: string, s: MagicString, index: number, newSnap:
}

function prepareSnapString(snap: string, source: string, index: number) {
const lineIndex = numberToPos(source, index).line
const line = source.split(lineSplitRE)[lineIndex - 1]
const lineNumber = offsetToLineNumber(source, index)
const line = source.split(lineSplitRE)[lineNumber - 1]
const indent = line.match(/^\s*/)![0] || ''
const indentNext = indent.includes('\t') ? `${indent}\t` : `${indent} `

Expand Down
41 changes: 14 additions & 27 deletions packages/vitest/src/node/error.ts
@@ -1,10 +1,10 @@
/* 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'
import { lineSplitRE, parseStacktrace, posToNumber } from '../utils/source-map'
import type { ErrorWithDiff, ParsedStack } from '../types'
import { lineSplitRE, parseStacktrace, positionToOffset } from '../utils/source-map'
import { F_POINTER } from '../utils/figures'
import { stringify } from '../integrations/chai/jest-matcher-utils'
import { TypeCheckError } from '../typecheck/typechecker'
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.line, s.column)))
}
})
}
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,12 @@ function printStack(
export function generateCodeFrame(
source: string,
indent = 0,
start: number | Position = 0,
end?: number,
lineNumber: number,
columnNumber: number,
range = 2,
): string {
start = posToNumber(source, start)
end = end || start
const start = positionToOffset(source, lineNumber, columnNumber)
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
7 changes: 0 additions & 7 deletions packages/vitest/src/types/general.ts
Expand Up @@ -47,18 +47,11 @@ export interface UserConsoleLog {
size: number
}

export interface Position {
source?: string
line: number
column: number
}

export interface ParsedStack {
method: string
file: string
line: number
column: number
sourcePos?: Position
}

export interface ErrorWithDiff extends Error {
Expand Down
32 changes: 13 additions & 19 deletions packages/vitest/src/utils/source-map.ts
@@ -1,5 +1,5 @@
import { resolve } from 'pathe'
import type { ErrorWithDiff, ParsedStack, Position } from '../types'
import type { ErrorWithDiff, ParsedStack } from '../types'
import { notNullish } from './base'

export const lineSplitRE = /\r?\n/
Expand Down Expand Up @@ -91,31 +91,27 @@ export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] {
return stackFrames
}

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

if (line > lines.length)
if (lineNumber > lines.length)
return source.length

for (let i = 0; i < line - 1; i++)
for (let i = 0; i < lineNumber - 1; i++)
start += lines[i].length + 1

return start + column
return start + columnNumber
}

export function numberToPos(
export function offsetToLineNumber(
source: string,
offset: number | Position,
): Position {
if (typeof offset !== 'number')
return offset
offset: number,
): number {
if (offset > source.length) {
throw new Error(
`offset is longer than source length! offset ${offset} > length ${source.length}`,
Expand All @@ -124,14 +120,12 @@ export function numberToPos(
const lines = source.split(lineSplitRE)
let counted = 0
let line = 0
let column = 0
for (; line < lines.length; line++) {
const lineLength = lines[line].length + 1
if (counted + lineLength >= offset) {
column = offset - counted + 1
if (counted + lineLength >= offset)
break
}

counted += lineLength
}
return { line: line + 1, column }
return line + 1
}