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

feat: error log improvements #1202

Merged
merged 8 commits into from Apr 28, 2022
11 changes: 9 additions & 2 deletions packages/vitest/src/integrations/vi.ts
Expand Up @@ -21,8 +21,15 @@ class VitestUtils {
this._mocker = typeof __vitest_mocker__ !== 'undefined' ? __vitest_mocker__ : null
this._mockedDate = null

if (!this._mocker)
throw new Error('Vitest was initialized with native Node instead of Vite Node')
if (!this._mocker) {
const errorMsg = 'Vitest was initialized with native Node instead of Vite Node.'
+ '\n\nOne of the following is possible:'
+ '\n- "vitest" is imported outside of your tests (in that case, use "vitest/node" or import.meta.vitest)'
+ '\n- "vitest" is imported inside "globalSetup" (use "setupFiles", because "globalSetup" runs in a different context)'
+ '\n- Your dependency inside "node_modules" imports "vitest" directly (in that case, inline that dependency, using "deps.inline" config)'
+ '\n- Otherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n'
throw new Error(errorMsg)
}
}

// timers
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/cli-api.ts
Expand Up @@ -76,7 +76,7 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit
catch (e) {
process.exitCode = 1
ctx.error(`\n${c.red(divider(c.bold(c.inverse(' Unhandled Error '))))}`)
await ctx.printError(e)
await ctx.printError(e, true)
ctx.error('\n\n')
return false
}
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/core.ts
Expand Up @@ -493,8 +493,8 @@ export class Vitest {
return code.includes('import.meta.vitest')
}

printError(err: unknown) {
return printError(err, this)
printError(err: unknown, fullStack = false) {
return printError(err, fullStack, this)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should show all stack trace for some errors (like unhandled ones), bacause it's really hard to debug otherwise.

}

onServerRestarted(fn: () => void) {
Expand Down
55 changes: 45 additions & 10 deletions packages/vitest/src/node/error.ts
@@ -1,6 +1,6 @@
/* eslint-disable prefer-template */
/* eslint-disable no-template-curly-in-string */
import { existsSync, promises as fs } from 'fs'
import { existsSync, readFileSync } from 'fs'
import { join, relative } from 'pathe'
import c from 'picocolors'
import cliTruncate from 'cli-truncate'
Expand All @@ -17,7 +17,7 @@ export function fileFromParsedStack(stack: ParsedStack) {
return stack.file
}

export async function printError(error: unknown, ctx: Vitest) {
export async function printError(error: unknown, fullStack: boolean, ctx: Vitest) {
let e = error as ErrorWithDiff

if (typeof error === 'string') {
Expand All @@ -27,7 +27,7 @@ export async function printError(error: unknown, ctx: Vitest) {
} as any
}

const stacks = parseStacktrace(e)
const stacks = parseStacktrace(e, fullStack)

await interpretSourcePos(stacks, ctx)

Expand All @@ -36,10 +36,12 @@ export async function printError(error: unknown, ctx: Vitest) {
&& existsSync(stack.file),
)

const errorProperties = getErrorProperties(e)

printErrorMessage(e, ctx.console)
await printStack(ctx, stacks, nearest, async (s, pos) => {
printStack(ctx, stacks, nearest, errorProperties, (s, pos) => {
if (s === nearest && nearest) {
const sourceCode = await fs.readFile(fileFromParsedStack(nearest), 'utf-8')
const sourceCode = readFileSync(fileFromParsedStack(nearest), 'utf-8')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a bug where fs.readFile stalls the terminal, and breaks for of loop - because of that some stack trace might be missing.

ctx.log(c.yellow(generateCodeFrame(sourceCode, 4, pos)))
}
})
Expand All @@ -50,6 +52,27 @@ export async function printError(error: unknown, ctx: Vitest) {
displayDiff(stringify(e.actual), stringify(e.expected), ctx.console, ctx.config.outputTruncateLength)
}

function getErrorProperties(e: ErrorWithDiff) {
const skip = [
'message',
'name',
'nameStr',
'stack',
'stacks',
'stackStr',
'showDiff',
'actual',
'expected',
]
const errorObject = Object.create(null)
for (const key in e) {
if (!skip.includes(key))
errorObject[key] = e[key as keyof ErrorWithDiff]
}

return errorObject
}

const esmErrors = [
'Cannot use import statement outside a module',
'Unexpected token \'export\'',
Expand Down Expand Up @@ -95,29 +118,41 @@ function printErrorMessage(error: ErrorWithDiff, console: Console) {
console.error(c.red(`${c.bold(errorName)}: ${error.message}`))
}

async function printStack(
function printStack(
ctx: Vitest,
stack: ParsedStack[],
highlight?: ParsedStack,
highlight: ParsedStack | undefined,
errorProperties: Record<string, unknown>,
onStack?: ((stack: ParsedStack, pos: Position) => void),
) {
if (!stack.length)
return

const hasProperties = Object.keys(errorProperties).length > 0

let stackNumber = 0

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)

ctx.log(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`))
await onStack?.(frame, pos)
const isLastStack = stackNumber === stack.length - 1 || frame.file in ctx.state.filesMap

ctx.log(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`), isLastStack && hasProperties ? '{' : '')
onStack?.(frame, pos)

// reached at test file, skip the follow stack
if (frame.file in ctx.state.filesMap)
break

stackNumber++
}
if (hasProperties) {
const propertiesString = stringify(errorProperties, 10, { printBasicPrototype: false }).substring(2)
ctx.log(propertiesString)
}
ctx.log()
}

export function generateCodeFrame(
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/reporters/base.ts
Expand Up @@ -210,7 +210,7 @@ export abstract class BaseReporter implements Reporter {
process.on('unhandledRejection', async (err) => {
process.exitCode = 1
this.ctx.error(`\n${c.red(divider(c.bold(c.inverse(' Unhandled Rejection '))))}`)
await this.ctx.printError(err)
await this.ctx.printError(err, true)
this.ctx.error('\n\n')
process.exit(1)
})
Expand Down