Skip to content

Commit

Permalink
chore: improve test summary, skip only/skip tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Oct 2, 2022
1 parent 7d1f3ec commit 689361d
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 198 deletions.
2 changes: 1 addition & 1 deletion packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class Vitest {
const checker = new Typechecker(this, testsFilesList)
checker.onParseEnd(async ({ files, sourceErrors }) => {
await this.report('onFinished', files)
if (sourceErrors.length) {
if (sourceErrors.length && !this.config.typecheck.ignoreSourceErrors) {
process.exitCode = 1
await this.logger.printSourceTypeErrors(sourceErrors)
}
Expand Down
14 changes: 10 additions & 4 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { performance } from 'perf_hooks'
import c from 'picocolors'
import type { ErrorWithDiff, File, Reporter, Task, TaskResultPack, UserConsoleLog } from '../../types'
import { clearInterval, getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode, relativePath, setInterval } from '../../utils'
import { clearInterval, getFullName, getSuites, getTests, getTypecheckTests, hasFailed, hasFailedSnapshot, isNode, relativePath, setInterval } from '../../utils'
import type { Vitest } from '../../node'
import { F_RIGHT } from '../../utils/figures'
import { divider, formatTimeString, getStateString, getStateSymbol, pointer, renderSnapshotSummary } from './renderers/utils'
Expand Down Expand Up @@ -202,7 +202,7 @@ export abstract class BaseReporter implements Reporter {
}

async reportTestSummary(files: File[]) {
const tests = getTests(files)
const tests = this.mode === 'typecheck' ? getTypecheckTests(files) : getTests(files)
const logger = this.ctx.logger

const executionTime = this.end - this.start
Expand All @@ -212,7 +212,7 @@ export abstract class BaseReporter implements Reporter {
const transformTime = Array.from(this.ctx.vitenode.fetchCache.values()).reduce((a, b) => a + (b?.duration || 0), 0)
const threadTime = collectTime + testsTime + setupTime

const padTitle = (str: string) => c.dim(`${str.padStart(10)} `)
const padTitle = (str: string) => c.dim(`${str.padStart(11)} `)
const time = (time: number) => {
if (time > 1000)
return `${(time / 1000).toFixed(2)}s`
Expand All @@ -239,6 +239,11 @@ export abstract class BaseReporter implements Reporter {

logger.log(padTitle('Test Files'), getStateString(files))
logger.log(padTitle('Tests'), getStateString(tests))
if (this.mode === 'typecheck') {
// has only failed types
const typechecks = getTests(tests).filter(t => t.type === 'typecheck')
logger.log(padTitle('Type Errors'), getStateString(typechecks, 'typechecks', false))
}
logger.log(padTitle('Start at'), formatTimeString(this._timeStart))
if (this.watchFilters)
logger.log(padTitle('Duration'), time(threadTime))
Expand Down Expand Up @@ -270,7 +275,8 @@ export abstract class BaseReporter implements Reporter {
}

if (failedTests.length) {
logger.error(c.red(divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `)))))
const type = this.mode === 'typecheck' ? 'Typechecks' : 'Tests'
logger.error(c.red(divider(c.bold(c.inverse(` Failed ${type} ${failedTests.length} `)))))
logger.error()

await this.printTaskErrors(failedTests, errorDivider)
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/reporters/renderers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function renderSnapshotSummary(rootDir: string, snapshots: SnapshotSummar
return summary
}

export function getStateString(tasks: Task[], name = 'tests') {
export function getStateString(tasks: Task[], name = 'tests', showTotal = true) {
if (tasks.length === 0)
return c.dim(`no ${name}`)

Expand All @@ -105,7 +105,7 @@ export function getStateString(tasks: Task[], name = 'tests') {
passed.length ? c.bold(c.green(`${passed.length} passed`)) : null,
skipped.length ? c.yellow(`${skipped.length} skipped`) : null,
todo.length ? c.gray(`${todo.length} todo`) : null,
].filter(Boolean).join(c.dim(' | ')) + c.gray(` (${tasks.length})`)
].filter(Boolean).join(c.dim(' | ')) + (showTotal ? c.gray(` (${tasks.length})`) : '')
}

export function getStateSymbol(task: Task) {
Expand Down
20 changes: 20 additions & 0 deletions packages/vitest/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,34 @@ export interface InlineConfig {
*/
dangerouslyIgnoreUnhandledErrors?: boolean

/**
* Options for configuring typechecking test environment.
*/
typecheck?: Partial<TypecheckConfig>
}

export interface TypecheckConfig {
/**
* What tool to use for type checking.
*/
checker: 'tsc' | 'vue-tsc'
/**
* Pattern for files that should be treated as test files
*/
include: string[]
/**
* Pattern for files that should not be treated as test files
*/
exclude: string[]
/**
* Check JS files that have `@ts-check` comment.
* If you have it enabled in tsconfig, this will not overwrite it.
*/
allowJs?: boolean
/**
* Do not fail, if Vitest found errors not inside the test files.
*/
ignoreSourceErrors?: boolean
}

export interface UserConfig extends InlineConfig {
Expand Down
151 changes: 151 additions & 0 deletions packages/vitest/src/typescript/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { relative } from 'pathe'
import { parse as parseAst } from 'acorn'
import { ancestor as walkAst } from 'acorn-walk'
import type { RawSourceMap } from 'vite-node'

import type { File, Suite, Vitest } from '../types'
import { TYPECHECK_SUITE } from './constants'

interface ParsedFile extends File {
start: number
end: number
}

interface ParsedSuite extends Suite {
start: number
end: number
}

interface LocalCallDefinition {
start: number
end: number
name: string
type: string
mode: 'run' | 'skip' | 'only' | 'todo'
task: ParsedSuite | ParsedFile
}

export interface FileInformation {
file: ParsedFile
filepath: string
parsed: string
map: RawSourceMap | null
definitions: LocalCallDefinition[]
}

export async function collectTests(ctx: Vitest, filepath: string): Promise<null | FileInformation> {
const request = await ctx.vitenode.transformRequest(filepath)
if (!request)
return null
const ast = parseAst(request.code, {
ecmaVersion: 'latest',
allowAwaitOutsideFunction: true,
})
const file: ParsedFile = {
filepath,
type: 'suite',
id: '-1',
name: relative(ctx.config.root, filepath),
mode: 'run',
tasks: [],
start: ast.start,
end: ast.end,
result: {
state: 'pass',
},
}
const definitions: LocalCallDefinition[] = []
const getName = (callee: any): string | null => {
if (!callee)
return null
if (callee.type === 'Identifier')
return callee.name
if (callee.type === 'MemberExpression') {
// direct call as `__vite_ssr__.test()`
if (callee.object?.name?.startsWith('__vite_ssr_'))
return getName(callee.property)
return getName(callee.object?.property)
}
return null
}
walkAst(ast, {
CallExpression(node) {
const { callee } = node as any
const name = getName(callee)
if (!name)
return
if (!['it', 'test', 'describe', 'suite'].includes(name))
return
const { arguments: [{ value: message }] } = node as any
const property = callee?.property?.name
const mode = !property || property === name ? 'run' : property
if (mode === 'each')
throw new Error(`${name}.each syntax is not supported when testing types`)
definitions.push({
start: node.start,
end: node.end,
name: message,
type: name,
mode,
} as LocalCallDefinition)
},
})
let lastSuite: ParsedSuite = file
const getLatestSuite = (index: number) => {
const suite = lastSuite
while (lastSuite !== file && lastSuite.end < index)
lastSuite = suite.suite as ParsedSuite
return lastSuite
}
definitions.sort((a, b) => a.start - b.start).forEach((definition, idx) => {
const latestSuite = getLatestSuite(definition.start)
let mode = definition.mode
if (latestSuite.mode !== 'run') // inherit suite mode, if it's set
mode = latestSuite.mode
const state = mode === 'run' ? 'pass' : mode
// expectTypeOf and any type error is actually a "test" ("typecheck"),
// and all "test"s should be inside a "suite"
const task: ParsedSuite = {
type: 'suite',
id: idx.toString(),
suite: latestSuite,
file,
tasks: [],
mode,
name: definition.name,
end: definition.end,
start: definition.start,
result: {
state,
},
}
definition.task = task
latestSuite.tasks.push(task)
if (definition.type === 'describe' || definition.type === 'suite')
lastSuite = task
else
// to show correct amount of "tests" in summary, we mark this with a special symbol
Object.defineProperty(task, TYPECHECK_SUITE, { value: true })
})
const markSkippedTests = (suite: Suite) => {
const hasOnly = suite.tasks.some(task => task.mode === 'only')
suite.tasks.forEach((task) => {
if ((hasOnly && task.mode !== 'only') || suite.mode === 'skip') {
task.mode = 'skip'
task.result = {
state: 'skip',
}
}
if (task.type === 'suite')
markSkippedTests(task)
})
}
markSkippedTests(file)
return {
file,
parsed: request.code,
filepath,
map: request.map as RawSourceMap | null,
definitions,
}
}
2 changes: 1 addition & 1 deletion packages/vitest/src/typescript/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const EXPECT_TYPEOF_MATCHERS = Symbol('vitest:expect-typeof-matchers')
export const TYPECHECK_ERROR = Symbol('vitest:typecheck-error')
export const TYPECHECK_SUITE = Symbol('vitest:typecheck-suite')

0 comments on commit 689361d

Please sign in to comment.