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(reporter)!: move ctx.log to ctx.logger.log, improve log flicking #1166

Merged
merged 20 commits into from Jul 9, 2022
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
4 changes: 2 additions & 2 deletions packages/vitest/src/node/cli-api.ts
Expand Up @@ -58,8 +58,8 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit
}
catch (e) {
process.exitCode = 1
await ctx.printError(e, true, 'Unhandled Error')
ctx.error('\n\n')
await ctx.logger.printError(e, true, 'Unhandled Error')
ctx.logger.error('\n\n')
return false
}

Expand Down
62 changes: 9 additions & 53 deletions packages/vitest/src/node/core.ts
@@ -1,5 +1,4 @@
import { existsSync, promises as fs } from 'fs'
import readline from 'readline'
import type { ViteDevServer } from 'vite'
import { relative, toNamespacedPath } from 'pathe'
import fg from 'fast-glob'
Expand All @@ -16,8 +15,8 @@ import type { WorkerPool } from './pool'
import { createReporters } from './reporters/utils'
import { StateManager } from './state'
import { resolveConfig } from './config'
import { printError } from './error'
import { VitestGit } from './git'
import { Logger } from './logger'
import { VitestCache } from './cache'

const WATCHER_DEBOUNCE = 100
Expand All @@ -32,12 +31,9 @@ export class Vitest {
snapshot: SnapshotManager = undefined!
cache: VitestCache = undefined!
reporters: Reporter[] = undefined!
console: Console
logger: Logger
pool: WorkerPool | undefined

outputStream = process.stdout
errorStream = process.stderr

vitenode: ViteNodeServer = undefined!

invalidates: Set<string> = new Set()
Expand All @@ -49,12 +45,12 @@ export class Vitest {
restartsCount = 0
runner: ViteNodeRunner = undefined!

private _onRestartListeners: Array<() => void> = []

constructor() {
this.console = globalThis.console
this.logger = new Logger(this)
}

private _onRestartListeners: Array<() => void> = []

async setServer(options: UserConfig, server: ViteDevServer) {
this.unregisterWatcher?.()
clearTimeout(this._rerunTimer)
Expand Down Expand Up @@ -100,7 +96,7 @@ export class Vitest {
await this.cache.results.readFromCache()
}
catch (err) {
this.error(`[vitest] Error, while trying to parse cache in ${this.cache.results.getCachePath()}:`, err)
this.logger.error(`[vitest] Error, while trying to parse cache in ${this.cache.results.getCachePath()}:`, err)
}
}

Expand Down Expand Up @@ -132,20 +128,7 @@ export class Vitest {
if (!files.length) {
const exitCode = this.config.passWithNoTests ? 0 : 1

const comma = c.dim(', ')
if (filters?.length)
this.console.error(c.dim('filter: ') + c.yellow(filters.join(comma)))
if (this.config.include)
this.console.error(c.dim('include: ') + c.yellow(this.config.include.join(comma)))
if (this.config.exclude)
this.console.error(c.dim('exclude: ') + c.yellow(this.config.exclude.join(comma)))
if (this.config.watchExclude)
this.console.error(c.dim('watch exclude: ') + c.yellow(this.config.watchExclude.join(comma)))

if (this.config.passWithNoTests)
this.log('No test files found, exiting with code 0\n')
else
this.error(c.red('\nNo test files found, exiting with code 1'))
this.logger.printNoTestFound(filters)

process.exit(exitCode)
}
Expand Down Expand Up @@ -193,7 +176,7 @@ export class Vitest {
changedSince: this.config.changed,
})
if (!related) {
this.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n'))
this.logger.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n'))
process.exit(1)
}
this.config.related = Array.from(new Set(related))
Expand Down Expand Up @@ -302,25 +285,6 @@ export class Vitest {
}
}

log(...args: any[]) {
this.console.log(...args)
}

error(...args: any[]) {
this.console.error(...args)
}

clearScreen() {
if (this.server.config.clearScreen === false)
return

const repeatCount = (process.stdout?.rows ?? 0) - 2
const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : ''
this.console.log(blank)
readline.cursorTo(process.stdout, 0, 0)
readline.clearScreenDown(process.stdout)
}

private _rerunTimer: any
private async scheduleRerun(triggerId: string) {
const currentCount = this.restartsCount
Expand Down Expand Up @@ -459,7 +423,7 @@ export class Vitest {
this.server.close(),
].filter(Boolean)).then((results) => {
results.filter(r => r.status === 'rejected').forEach((err) => {
this.error('error during close', (err as PromiseRejectedResult).reason)
this.logger.error('error during close', (err as PromiseRejectedResult).reason)
})
})
}
Expand Down Expand Up @@ -536,14 +500,6 @@ export class Vitest {
return code.includes('import.meta.vitest')
}

printError(err: unknown, fullStack = false, type?: string) {
return printError(err, this, {
fullStack,
type,
showCodeFrame: true,
})
}

onServerRestarted(fn: () => void) {
this._onRestartListeners.push(fn)
}
Expand Down
26 changes: 15 additions & 11 deletions packages/vitest/src/node/error.ts
Expand Up @@ -10,6 +10,7 @@ import { stringify } from '../integrations/chai/jest-matcher-utils'
import type { Vitest } from './core'
import { type DiffOptions, unifiedDiff } from './diff'
import { divider } from './reporters/renderers/utils'
import type { Logger } from './logger'

export function fileFromParsedStack(stack: ParsedStack) {
if (stack?.sourcePos?.source?.startsWith('..'))
Expand Down Expand Up @@ -47,15 +48,16 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro

if (type)
printErrorType(type, ctx)
printErrorMessage(e, ctx.console)

printErrorMessage(e, ctx.logger)
printStack(ctx, stacks, nearest, errorProperties, (s, pos) => {
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 (existsSync(file)) {
const sourceCode = readFileSync(file, 'utf-8')
ctx.log(c.yellow(generateCodeFrame(sourceCode, 4, pos)))
ctx.logger.log(c.yellow(generateCodeFrame(sourceCode, 4, pos)))
}
}
})
Expand All @@ -68,15 +70,15 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro
handleImportOutsideModuleError(e.stack || e.stackStr || '', ctx)

if (e.showDiff) {
displayDiff(stringify(e.actual), stringify(e.expected), ctx.console, {
displayDiff(stringify(e.actual), stringify(e.expected), ctx.logger.console, {
outputTruncateLength: ctx.config.outputTruncateLength,
outputDiffLines: ctx.config.outputDiffLines,
})
}
}

function printErrorType(type: string, ctx: Vitest) {
ctx.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`)
ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`)
}

const skipErrorProperties = [
Expand Down Expand Up @@ -124,7 +126,7 @@ function handleImportOutsideModuleError(stack: string, ctx: Vitest) {
else
name = name.split('/')[0]

ctx.error(c.yellow(
ctx.logger.error(c.yellow(
`Module ${path} seems to be an ES Module but shipped in a CommonJS package. `
+ `You might want to create an issue to the package ${c.bold(`"${name}"`)} asking `
+ 'them to ship the file in .mjs extension or add "type": "module" in their package.json.'
Expand All @@ -148,9 +150,9 @@ function displayDiff(actual: string, expected: string, console: Console, options
console.error(c.gray(unifiedDiff(actual, expected, options)) + '\n')
}

function printErrorMessage(error: ErrorWithDiff, console: Console) {
function printErrorMessage(error: ErrorWithDiff, logger: Logger) {
const errorName = error.name || error.nameStr || 'Unknown Error'
console.error(c.red(`${c.bold(errorName)}: ${error.message}`))
logger.error(c.red(`${c.bold(errorName)}: ${error.message}`))
}

function printStack(
Expand All @@ -163,25 +165,27 @@ function printStack(
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)

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

// reached at test file, skip the follow stack
if (frame.file in ctx.state.filesMap)
break
}
ctx.log()
logger.log()
const hasProperties = Object.keys(errorProperties).length > 0
if (hasProperties) {
ctx.log(c.red(c.dim(divider())))
logger.log(c.red(c.dim(divider())))
const propertiesString = stringify(errorProperties, 10, { printBasicPrototype: false })
ctx.log(c.red(c.bold('Serialized Error:')), c.gray(propertiesString))
logger.log(c.red(c.bold('Serialized Error:')), c.gray(propertiesString))
}
}

Expand Down
118 changes: 118 additions & 0 deletions packages/vitest/src/node/logger.ts
@@ -0,0 +1,118 @@
import { createLogUpdate } from 'log-update'
import c from 'picocolors'
import { version } from '../../../../package.json'
import type { ErrorWithDiff } from '../types'
import { divider } from './reporters/renderers/utils'
import type { Vitest } from './core'
import { printError } from './error'

export class Logger {
outputStream = process.stdout
errorStream = process.stderr
logUpdate = createLogUpdate(process.stdout)

private _clearScreenPending: string | undefined

constructor(
public ctx: Vitest,
public console = globalThis.console,
) {

}

log(...args: any[]) {
this._clearScreen()
this.console.log(...args)
}

error(...args: any[]) {
this._clearScreen()
this.console.error(...args)
}

warn(...args: any[]) {
this._clearScreen()
this.console.warn(...args)
}

clearScreen(message: string, force = false) {
if (this.ctx.server.config.clearScreen === false) {
this.console.log(message)
return
}

this._clearScreenPending = message
if (force)
this._clearScreen()
}

private _clearScreen() {
if (!this._clearScreenPending)
return

const log = this._clearScreenPending
this._clearScreenPending = undefined
// equivalent to ansi-escapes:
// stdout.write(ansiEscapes.cursorTo(0, 0) + ansiEscapes.eraseDown + log)
this.console.log(`\u001B[1;1H\u001B[J${log}`)
}

printError(err: unknown, fullStack = false, type?: string) {
return printError(err, this.ctx, {
fullStack,
type,
showCodeFrame: true,
})
}

printNoTestFound(filters?: string[]) {
const config = this.ctx.config
const comma = c.dim(', ')
if (filters?.length)
this.console.error(c.dim('filter: ') + c.yellow(filters.join(comma)))
if (config.include)
this.console.error(c.dim('include: ') + c.yellow(config.include.join(comma)))
if (config.exclude)
this.console.error(c.dim('exclude: ') + c.yellow(config.exclude.join(comma)))
if (config.watchExclude)
this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma)))

if (config.passWithNoTests)
this.log('No test files found, exiting with code 0\n')
else
this.error(c.red('\nNo test files found, exiting with code 1'))
}

printBanner() {
this.log()

const versionTest = this.ctx.config.watch
? c.blue(`v${version}`)
: c.cyan(`v${version}`)
const mode = this.ctx.config.watch
? c.blue(' DEV ')
: c.cyan(' RUN ')

this.log(`${c.inverse(c.bold(mode))} ${versionTest} ${c.gray(this.ctx.config.root)}`)

if (this.ctx.config.ui)
this.log(c.dim(c.green(` UI started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.server.config.server.port}`)}${this.ctx.config.uiBase}`)))
else if (this.ctx.config.api)
this.log(c.dim(c.green(` API started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.config.api.port}`)}`)))

this.log()
}

async printUnhandledErrors(errors: unknown[]) {
const errorMessage = c.red(c.bold(
`\nVitest caught ${errors.length} unhandled error${errors.length > 1 ? 's' : ''} during the test run. This might cause false positive tests.`
+ '\nPlease, resolve all the errors to make sure your tests are not affected.',
))
this.log(c.red(divider(c.bold(c.inverse(' Unhandled Errors ')))))
this.log(errorMessage)
await Promise.all(errors.map(async (err) => {
await this.printError(err, true, (err as ErrorWithDiff).type || 'Unhandled Error')
}))
this.log(c.red(divider()))
}
}
6 changes: 3 additions & 3 deletions packages/vitest/src/node/plugins/globalSetup.ts
Expand Up @@ -65,8 +65,8 @@ export const GlobalSetupPlugin = (ctx: Vitest): Plugin => {
}
}
catch (e) {
ctx.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`)
await ctx.printError(e)
ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`)
await ctx.logger.printError(e)
process.exit(1)
}
},
Expand All @@ -78,7 +78,7 @@ export const GlobalSetupPlugin = (ctx: Vitest): Plugin => {
await globalSetupFile.teardown?.()
}
catch (error) {
console.error(`error during global teardown of ${globalSetupFile.file}`, error)
ctx.logger.error(`error during global teardown of ${globalSetupFile.file}`, error)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/plugins/index.ts
Expand Up @@ -133,7 +133,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest())
(await import('../../api/setup')).setup(ctx)
}
catch (err) {
ctx.printError(err, true)
ctx.logger.printError(err, true)
process.exit(1)
}

Expand Down