mirror of
https://github.com/vitest-dev/vitest.git
synced 2026-02-01 17:36:51 +00:00
feat: embeded c8, close #321
This commit is contained in:
parent
7db9bcc86b
commit
d0ce8d0cb4
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,3 +79,4 @@ dist
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
@ -204,25 +204,26 @@ export default defineConfig({
|
||||
|
||||
Vitest supports Native code coverage via [c8](https://github.com/bcoe/c8)
|
||||
|
||||
```bash
|
||||
$ npm i -D c8
|
||||
$ c8 vitest
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"coverage": "c8 vitest"
|
||||
"coverage": "vitest --coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For convenience, we also provide a shorthand for passing `--coverage` option to CLI, which will wrap the process with `c8` for you. Note when using the shorthand, you will lose the ability to pass additional options to `c8`.
|
||||
To configure it, set `test.coverage` options in your config file:
|
||||
|
||||
```bash
|
||||
$ vitest --coverage
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
reporter: ['text', 'json', 'html']
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
For more configuration available, please refer to [c8](https://github.com/bcoe/c8)'s documentation.
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@
|
||||
"chai-subset": "^1.6.0",
|
||||
"cli-truncate": "^3.1.0",
|
||||
"diff": "^5.0.0",
|
||||
"execa": "^6.0.0",
|
||||
"fast-glob": "^3.2.7",
|
||||
"find-up": "^6.2.0",
|
||||
"flatted": "^3.2.4",
|
||||
|
||||
169
packages/vitest/src/coverage.ts
Normal file
169
packages/vitest/src/coverage.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { existsSync, promises as fs } from 'fs'
|
||||
import { createRequire } from 'module'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { resolve } from 'pathe'
|
||||
import type { Arrayable } from 'vitest'
|
||||
import type { Vitest } from './node'
|
||||
|
||||
const defaultExcludes = [
|
||||
'coverage/**',
|
||||
'packages/*/test{,s}/**',
|
||||
'**/*.d.ts',
|
||||
'test{,s}/**',
|
||||
'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}',
|
||||
'**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}',
|
||||
'**/__tests__/**',
|
||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc}.config.{js,cjs,mjs,ts}',
|
||||
'**/.{eslint,mocha}rc.{js,cjs}',
|
||||
]
|
||||
|
||||
export type Reporter =
|
||||
| 'clover'
|
||||
| 'cobertura'
|
||||
| 'html-spa'
|
||||
| 'html'
|
||||
| 'json-summary'
|
||||
| 'json'
|
||||
| 'lcov'
|
||||
| 'lcovonly'
|
||||
| 'none'
|
||||
| 'teamcity'
|
||||
| 'text-lcov'
|
||||
| 'text-summary'
|
||||
| 'text'
|
||||
|
||||
export interface C8Options {
|
||||
/**
|
||||
* Enable coverage, pass `--coverage` to enable
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
enabled?: boolean
|
||||
/**
|
||||
* Directory to write coverage report to
|
||||
*/
|
||||
reportsDirectory?: string
|
||||
/**
|
||||
* Clean coverage before running tests
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
clean?: boolean
|
||||
/**
|
||||
* Clean coverage report on watch rerun
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
cleanOnRerun?: boolean
|
||||
/**
|
||||
* Allow files from outside of your cwd.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
allowExternal?: any
|
||||
/**
|
||||
* Reporters
|
||||
*
|
||||
* @default 'text'
|
||||
*/
|
||||
reporter?: Arrayable<Reporter>
|
||||
/**
|
||||
* Exclude coverage under /node_modules/
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
excludeNodeModules?: boolean
|
||||
exclude?: string[]
|
||||
include?: string[]
|
||||
skipFull?: boolean
|
||||
|
||||
// c8 options, not sure if we should expose them
|
||||
/**
|
||||
* Directory to store V8 coverage reports
|
||||
*/
|
||||
// tempDirectory?: string
|
||||
// watermarks?: any
|
||||
// excludeAfterRemap?: any
|
||||
// omitRelative?: any
|
||||
// wrapperLength?: any
|
||||
// resolve?: any
|
||||
// all?: any
|
||||
// src?: any
|
||||
}
|
||||
|
||||
export interface ResolvedC8Options extends Required<C8Options> {
|
||||
tempDirectory: string
|
||||
}
|
||||
|
||||
export function resolveC8Options(options: C8Options, root: string): ResolvedC8Options {
|
||||
const resolved: ResolvedC8Options = {
|
||||
enabled: false,
|
||||
clean: true,
|
||||
cleanOnRerun: false,
|
||||
reportsDirectory: './coverage',
|
||||
excludeNodeModules: true,
|
||||
exclude: defaultExcludes,
|
||||
reporter: 'text',
|
||||
allowExternal: false,
|
||||
...options as any,
|
||||
}
|
||||
|
||||
resolved.reportsDirectory = resolve(root, resolved.reportsDirectory)
|
||||
resolved.tempDirectory = process.env.NODE_V8_COVERAGE || resolve(resolved.reportsDirectory, 'tmp')
|
||||
|
||||
return resolved as ResolvedC8Options
|
||||
}
|
||||
|
||||
export async function cleanCoverage(options: ResolvedC8Options, clean = true) {
|
||||
if (clean && existsSync(options.reportsDirectory))
|
||||
await fs.rmdir(options.reportsDirectory, { recursive: true })
|
||||
|
||||
if (!existsSync(options.tempDirectory))
|
||||
await fs.mkdir(options.tempDirectory, { recursive: true })
|
||||
}
|
||||
|
||||
export async function prepareCoverage(options: ResolvedC8Options) {
|
||||
if (options.enabled)
|
||||
return false
|
||||
|
||||
await cleanCoverage(options, options.clean)
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export async function reportCoverage(ctx: Vitest) {
|
||||
// await writeC8Sourcemap(ctx)
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const createReport = require('c8/lib/report')
|
||||
const report = createReport(ctx.config.coverage)
|
||||
|
||||
const files = Array.from(ctx.visitedFilesMap.entries()).filter(i => !i[0].includes('/node_modules/'))
|
||||
|
||||
files.forEach(([file, map]) => {
|
||||
if (!existsSync(file))
|
||||
return
|
||||
const url = pathToFileURL(file).href
|
||||
report.sourceMapCache[url] = {
|
||||
data: {
|
||||
...map,
|
||||
sources: map.sources.map(i => pathToFileURL(i).href) || [url],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
await report.run()
|
||||
}
|
||||
|
||||
// export async function writeC8Sourcemap(ctx: Vitest) {
|
||||
// const cache: Record<string, any> = {}
|
||||
|
||||
// // write a fake coverage report with source map for c8 to consume
|
||||
// await fs.writeFile(
|
||||
// join(ctx.config.coverage.tempDirectory, 'vitest-source-map.json'),
|
||||
// JSON.stringify({
|
||||
// 'result': [],
|
||||
// 'timestamp': performance.now(),
|
||||
// 'source-map-cache': cache,
|
||||
// }), 'utf-8',
|
||||
// )
|
||||
// }
|
||||
@ -22,10 +22,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
import { isObject } from '../../utils'
|
||||
import type { Tester } from './types'
|
||||
|
||||
export const isObject = (val: any): val is object => toString.call(val) === '[object Object]'
|
||||
|
||||
// Extracted out of jasmine 2.5.2
|
||||
export function equals(
|
||||
a: unknown,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import readline from 'readline'
|
||||
import cac from 'cac'
|
||||
import { execa } from 'execa'
|
||||
import type { UserConfig } from '../types'
|
||||
import { version } from '../../package.json'
|
||||
import { ensurePackageInstalled } from '../utils'
|
||||
@ -47,7 +48,7 @@ cli.parse()
|
||||
|
||||
async function dev(cliFilters: string[], argv: UserConfig) {
|
||||
if (argv.watch == null)
|
||||
argv.watch = !process.env.CI && !process.env.NODE_V8_COVERAGE && !argv.run
|
||||
argv.watch = !process.env.CI && !argv.run
|
||||
await run(cliFilters, argv)
|
||||
}
|
||||
|
||||
@ -55,17 +56,35 @@ async function run(cliFilters: string[], options: UserConfig) {
|
||||
process.env.VITEST = 'true'
|
||||
process.env.NODE_ENV = 'test'
|
||||
|
||||
if (!await ensurePackageInstalled('vite'))
|
||||
process.exit(1)
|
||||
|
||||
if (typeof options.coverage === 'boolean')
|
||||
options.coverage = { enabled: options.coverage }
|
||||
|
||||
const ctx = await createVitest(options)
|
||||
|
||||
process.chdir(ctx.config.root)
|
||||
if (ctx.config.coverage.enabled) {
|
||||
if (!await ensurePackageInstalled('c8'))
|
||||
process.exit(1)
|
||||
|
||||
registerConsoleShortcuts(ctx)
|
||||
if (!process.env.NODE_V8_COVERAGE) {
|
||||
process.env.NODE_V8_COVERAGE = ctx.config.coverage.tempDirectory
|
||||
const { exitCode } = await execa(process.argv0, process.argv.slice(1), { stdio: 'inherit' })
|
||||
process.exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.config.environment && ctx.config.environment !== 'node') {
|
||||
if (!await ensurePackageInstalled(ctx.config.environment))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (process.stdin.isTTY && ctx.config.watch)
|
||||
registerConsoleShortcuts(ctx)
|
||||
|
||||
process.chdir(ctx.config.root)
|
||||
|
||||
ctx.onServerRestarted(() => {
|
||||
// TODO: re-consider how to re-run the tests the server smartly
|
||||
ctx.start(cliFilters)
|
||||
@ -88,23 +107,20 @@ async function run(cliFilters: string[], options: UserConfig) {
|
||||
}
|
||||
|
||||
function registerConsoleShortcuts(ctx: Vitest) {
|
||||
// listen to keyboard input
|
||||
if (process.stdin.isTTY) {
|
||||
readline.emitKeypressEvents(process.stdin)
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.on('keypress', (str: string, key: any) => {
|
||||
if (str === '\x03' || str === '\x1B' || (key && key.ctrl && key.name === 'c')) // ctrl-c or esc
|
||||
process.exit()
|
||||
readline.emitKeypressEvents(process.stdin)
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.on('keypress', (str: string, key: any) => {
|
||||
if (str === '\x03' || str === '\x1B' || (key && key.ctrl && key.name === 'c')) // ctrl-c or esc
|
||||
process.exit()
|
||||
|
||||
// is running, ignore keypress
|
||||
if (ctx.runningPromise)
|
||||
return
|
||||
// is running, ignore keypress
|
||||
if (ctx.runningPromise)
|
||||
return
|
||||
|
||||
// press any key to exit on first run
|
||||
if (ctx.isFirstRun)
|
||||
process.exit()
|
||||
// press any key to exit on first run
|
||||
if (ctx.isFirstRun)
|
||||
process.exit()
|
||||
|
||||
// TODO: add more commands
|
||||
})
|
||||
}
|
||||
// TODO: add more commands
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { resolve } from 'pathe'
|
||||
import type { ResolvedConfig as ResolvedViteConfig } from 'vite'
|
||||
import type { ResolvedConfig, UserConfig } from '../types'
|
||||
import { defaultExclude, defaultInclude, defaultPort } from '../constants'
|
||||
import { resolveC8Options } from '../coverage'
|
||||
import { deepMerge } from '../utils'
|
||||
|
||||
export function resolveConfig(
|
||||
options: UserConfig,
|
||||
@ -11,11 +13,12 @@ export function resolveConfig(
|
||||
options.environment = 'happy-dom'
|
||||
|
||||
const resolved = {
|
||||
...options,
|
||||
...viteConfig.test,
|
||||
...deepMerge(options, viteConfig.test),
|
||||
root: viteConfig.root,
|
||||
} as ResolvedConfig
|
||||
|
||||
resolved.coverage = resolveC8Options(resolved.coverage, resolved.root)
|
||||
|
||||
resolved.depsInline = [...resolved.deps?.inline || []]
|
||||
resolved.depsExternal = [...resolved.deps?.external || []]
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { existsSync, promises as fs } from 'fs'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { join, resolve } from 'pathe'
|
||||
import { resolve } from 'pathe'
|
||||
import type { ViteDevServer, InlineConfig as ViteInlineConfig, Plugin as VitePlugin, UserConfig as ViteUserConfig } from 'vite'
|
||||
import { createServer, mergeConfig } from 'vite'
|
||||
import { findUp } from 'find-up'
|
||||
@ -15,6 +13,7 @@ import { hasFailed, noop, slash, toArray } from '../utils'
|
||||
import { MocksPlugin } from '../plugins/mock'
|
||||
import { DefaultReporter } from '../reporters/default'
|
||||
import { ReportersMap } from '../reporters'
|
||||
import { cleanCoverage, prepareCoverage, reportCoverage } from '../coverage'
|
||||
import type { WorkerPool } from './pool'
|
||||
import { StateManager } from './state'
|
||||
import { resolveConfig } from './config'
|
||||
@ -45,7 +44,7 @@ class Vitest {
|
||||
this.console = globalThis.console
|
||||
}
|
||||
|
||||
setServer(options: UserConfig, server: ViteDevServer) {
|
||||
async setServer(options: UserConfig, server: ViteDevServer) {
|
||||
this.unregisterWatcher?.()
|
||||
clearTimeout(this._rerunTimer)
|
||||
this.restartsCount += 1
|
||||
@ -53,6 +52,7 @@ class Vitest {
|
||||
this.pool = undefined
|
||||
|
||||
const resolved = resolveConfig(options, server.config)
|
||||
|
||||
this.server = server
|
||||
this.config = resolved
|
||||
this.state = new StateManager()
|
||||
@ -78,6 +78,9 @@ class Vitest {
|
||||
this.runningPromise = undefined
|
||||
|
||||
this._onRestartListeners.forEach(fn => fn())
|
||||
|
||||
if (resolved.coverage.enabled)
|
||||
await prepareCoverage(resolved.coverage)
|
||||
}
|
||||
|
||||
async start(filters?: string[]) {
|
||||
@ -96,7 +99,8 @@ class Vitest {
|
||||
if (this.config.watch)
|
||||
await this.report('onWatcherStart')
|
||||
|
||||
await this.writeC8Sourcemap()
|
||||
if (this.config.coverage.enabled)
|
||||
await reportCoverage(this)
|
||||
}
|
||||
|
||||
async runFiles(files: string[]) {
|
||||
@ -165,11 +169,18 @@ class Vitest {
|
||||
const files = Array.from(this.changedTests)
|
||||
this.changedTests.clear()
|
||||
|
||||
this.console.log('return')
|
||||
if (this.config.coverage.enabled && this.config.coverage.cleanOnRerun)
|
||||
await cleanCoverage(this.config.coverage)
|
||||
|
||||
await this.report('onWatcherRerun', files, triggerId)
|
||||
|
||||
await this.runFiles(files)
|
||||
|
||||
await this.report('onWatcherStart')
|
||||
|
||||
if (this.config.coverage.enabled)
|
||||
await reportCoverage(this)
|
||||
}, WATCHER_DEBOUNCE)
|
||||
}
|
||||
|
||||
@ -259,38 +270,6 @@ class Vitest {
|
||||
return files
|
||||
}
|
||||
|
||||
async writeC8Sourcemap() {
|
||||
const coverageDir = process.env.NODE_V8_COVERAGE
|
||||
if (!coverageDir)
|
||||
return
|
||||
|
||||
const cache: Record<string, any> = {}
|
||||
|
||||
const files = Array.from(this.visitedFilesMap.entries()).filter(i => !i[0].includes('/node_modules/'))
|
||||
|
||||
files.forEach(([file, map]) => {
|
||||
if (!existsSync(file))
|
||||
return
|
||||
const url = pathToFileURL(file).href
|
||||
cache[url] = {
|
||||
data: {
|
||||
...map,
|
||||
sources: map.sources.map(i => pathToFileURL(i).href) || [url],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// write a fake coverage report with source map for c8 to consume
|
||||
await fs.writeFile(
|
||||
join(coverageDir, 'vitest-source-map.json'),
|
||||
JSON.stringify({
|
||||
'result': [],
|
||||
'timestamp': performance.now(),
|
||||
'source-map-cache': cache,
|
||||
}), 'utf-8',
|
||||
)
|
||||
}
|
||||
|
||||
isTargetFile(id: string): boolean {
|
||||
if (mm.isMatch(id, this.config.exclude))
|
||||
return false
|
||||
@ -326,7 +305,7 @@ export async function createVitest(options: UserConfig, viteOverrides: ViteUserC
|
||||
async configureServer(server) {
|
||||
if (haveStarted)
|
||||
await ctx.report('onServerRestart')
|
||||
ctx.setServer(options, server)
|
||||
await ctx.setServer(options, server)
|
||||
haveStarted = true
|
||||
if (options.api)
|
||||
server.middlewares.use((await import('../api/middleware')).default(ctx))
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { BuiltinReporters } from '../reporters'
|
||||
import type { C8Options, ResolvedC8Options } from '../coverage'
|
||||
import type { Reporter } from './reporter'
|
||||
import type { SnapshotStateOptions } from './snapshot'
|
||||
import type { Arrayable } from './general'
|
||||
@ -148,6 +149,11 @@ export interface InlineConfig {
|
||||
*/
|
||||
isolate?: boolean
|
||||
|
||||
/**
|
||||
* Coverage options
|
||||
*/
|
||||
coverage?: C8Options
|
||||
|
||||
/**
|
||||
* Open Vitest UI
|
||||
* @internal WIP
|
||||
@ -211,12 +217,13 @@ export interface UserConfig extends InlineConfig {
|
||||
passWithNoTests?: boolean
|
||||
}
|
||||
|
||||
export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters'> {
|
||||
export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters' | 'coverage'> {
|
||||
config?: string
|
||||
filters?: string[]
|
||||
|
||||
depsInline: (string | RegExp)[]
|
||||
depsExternal: (string | RegExp)[]
|
||||
|
||||
coverage: ResolvedC8Options
|
||||
snapshotOptions: SnapshotStateOptions
|
||||
}
|
||||
|
||||
@ -140,6 +140,46 @@ export async function ensurePackageInstalled(
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge :P
|
||||
*/
|
||||
export function deepMerge<T extends object = object>(target: T, ...sources: any[]): T {
|
||||
if (!sources.length)
|
||||
return target as any
|
||||
|
||||
const source = sources.shift()
|
||||
if (source === undefined)
|
||||
return target as any
|
||||
|
||||
if (isMergableObject(target) && isMergableObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (isMergableObject(source[key])) {
|
||||
// @ts-expect-error
|
||||
if (!target[key])
|
||||
// @ts-expect-error
|
||||
target[key] = {}
|
||||
|
||||
// @ts-expect-error
|
||||
deepMerge(target[key], source[key])
|
||||
}
|
||||
else {
|
||||
// @ts-expect-error
|
||||
target[key] = source[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return deepMerge(target, ...sources)
|
||||
}
|
||||
|
||||
function isMergableObject(item: any): item is Object {
|
||||
return isObject(item) && !Array.isArray(item)
|
||||
}
|
||||
|
||||
export function isObject(val: any): val is object {
|
||||
return toString.call(val) === '[object Object]'
|
||||
}
|
||||
|
||||
export function toFilePath(id: string, root: string): string {
|
||||
let absolute = slash(id).startsWith('/@fs/')
|
||||
? id.slice(4)
|
||||
|
||||
@ -1,21 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { fileURLToPath } from 'url'
|
||||
import { ensurePackageInstalled, resolvePath } from './dist/utils.js'
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
|
||||
if (!await ensurePackageInstalled('vite'))
|
||||
process.exit(1)
|
||||
|
||||
if (argv.includes('--coverage')) {
|
||||
if (!await ensurePackageInstalled('c8'))
|
||||
process.exit(1)
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const entry = resolvePath(filename, '../dist/cli.js')
|
||||
process.argv.splice(2, 0, process.argv[0], entry)
|
||||
await import('c8/bin/c8.js')
|
||||
}
|
||||
else {
|
||||
await import('./dist/cli.js')
|
||||
}
|
||||
import('./dist/cli.js')
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -138,6 +138,7 @@ importers:
|
||||
chai-subset: ^1.6.0
|
||||
cli-truncate: ^3.1.0
|
||||
diff: ^5.0.0
|
||||
execa: ^6.0.0
|
||||
fast-glob: ^3.2.7
|
||||
find-up: ^6.2.0
|
||||
flatted: ^3.2.4
|
||||
@ -183,6 +184,7 @@ importers:
|
||||
chai-subset: 1.6.0
|
||||
cli-truncate: 3.1.0
|
||||
diff: 5.0.0
|
||||
execa: 6.0.0
|
||||
fast-glob: 3.2.7
|
||||
find-up: 6.2.0
|
||||
flatted: 3.2.4
|
||||
|
||||
@ -7,5 +7,8 @@ export default defineConfig({
|
||||
setupFiles: [
|
||||
'./test/setup.ts',
|
||||
],
|
||||
coverage: {
|
||||
reporter: ['text', 'html'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user