Skip to content

Commit

Permalink
fix(api): make api parse error stacks and return sourcePos in onTaskU…
Browse files Browse the repository at this point in the history
…pdate (#2563)

* fix(api): make api parse error stacks on onTaskUpdate

* chore(node): remove all references to sourcePos

* chore(node): remove Position type
  • Loading branch information
adrienbaron committed Dec 28, 2022
1 parent b5dd8bc commit ef77dcc
Show file tree
Hide file tree
Showing 11 changed files with 49 additions and 87 deletions.
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)
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
}

0 comments on commit ef77dcc

Please sign in to comment.