Skip to content

Commit

Permalink
feat(coverage): add support for coverage reporter options
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Jan 21, 2023
1 parent 703aab4 commit 7cf61b7
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 33 deletions.
21 changes: 18 additions & 3 deletions docs/config/index.md
Expand Up @@ -684,13 +684,28 @@ When using `c8` provider a temporary `/tmp` directory is created for [V8 coverag

#### reporter

- **Type:** `string | string[]`
- **Type:** `string | string[] | [string, {}][]`
- **Default:** `['text', 'html', 'clover', 'json']`
- **Available for providers:** `'c8' | 'istanbul'`
- **CLI:** `--coverage.reporter=<reporter>`, `--coverage.reporter=<reporter1> --coverage.reporter=<reporter2>`

Coverage reporters to use. See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters.

Coverage reporters to use. See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters. See [`@types/istanbul-reporter`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/276d95e4304b3670eaf6e8e5a7ea9e265a14e338/types/istanbul-reports/index.d.ts) for details about reporter specific options.

The reporter has three different types:

- A single reporter: `{ reporter: 'html' }`
- Multiple reporters without options: `{ reporter: ['html', 'json'] }`
- A single or multiple reporters with reporter options:
<!-- eslint-skip -->
```ts
{
reporter: [
['lcov', { 'projectRoot': './src' }],
['json', { 'file': 'coverage.json' }],
['text']
]
}
```

#### skipFull

Expand Down
39 changes: 36 additions & 3 deletions packages/coverage-c8/src/provider.ts
Expand Up @@ -6,7 +6,7 @@ import { extname, resolve } from 'pathe'
import type { RawSourceMap } from 'vite-node'
import { coverageConfigDefaults } from 'vitest/config'
// eslint-disable-next-line no-restricted-imports
import type { CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest'
import type { CoverageC8Options, CoverageOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest'
import type { Vitest } from 'vitest/node'
import type { Report } from 'c8'
// @ts-expect-error missing types
Expand Down Expand Up @@ -55,6 +55,18 @@ export class C8CoverageProvider implements CoverageProvider {
const options: ConstructorParameters<typeof Report>[0] = {
...this.options,
all: this.options.all && allTestsRun,
reporter: this.options.reporter.map(([reporterName]) => reporterName),

// TODO: This requires unreleased c8@7.13.0
// @ts-expect-error -- untyped
reporterOptions: this.options.reporter.reduce((all, [name, options]) => ({
...all,
[name]: {
skipFull: this.options.skipFull,
projectRoot: this.ctx.config.root,
...options,
},
}), {}),
}

const report = createReport(options)
Expand Down Expand Up @@ -159,7 +171,6 @@ export class C8CoverageProvider implements CoverageProvider {

function resolveC8Options(options: CoverageC8Options, root: string): Options {
const reportsDirectory = resolve(root, options.reportsDirectory || coverageConfigDefaults.reportsDirectory)
const reporter = options.reporter || coverageConfigDefaults.reporter

const resolved: Options = {
...coverageConfigDefaults,
Expand All @@ -174,7 +185,7 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {
// Resolved fields
provider: 'c8',
tempDirectory: process.env.NODE_V8_COVERAGE || resolve(reportsDirectory, 'tmp'),
reporter: Array.isArray(reporter) ? reporter : [reporter],
reporter: resolveReporters(options.reporter || coverageConfigDefaults.reporter),
reportsDirectory,
}

Expand All @@ -187,3 +198,25 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {

return resolved
}

// TODO: Move to some generic utility for sharing with istanbul-provider
function resolveReporters(configReporters: NonNullable<CoverageOptions['reporter']>): Options['reporter'] {
// E.g. { reporter: "html" }
if (!Array.isArray(configReporters))
return [[configReporters, {}]]

const resolvedReporters: Options['reporter'] = []

for (const reporter of configReporters) {
if (Array.isArray(reporter)) {
// E.g. { reporter: [ ["html", { skipEmpty: true }], ["lcov"], ["json", { file: "map.json" }] ]}
resolvedReporters.push([reporter[0], reporter[1] || {}])
}
else {
// E.g. { reporter: ["html", "json"]}
resolvedReporters.push([reporter, {}])
}
}

return resolvedReporters
}
30 changes: 26 additions & 4 deletions packages/coverage-istanbul/src/provider.ts
Expand Up @@ -2,7 +2,7 @@
import { existsSync, promises as fs } from 'fs'
import { relative, resolve } from 'pathe'
import type { TransformPluginContext } from 'rollup'
import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest'
import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest'
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'
import libReport from 'istanbul-lib-report'
import reports from 'istanbul-reports'
Expand Down Expand Up @@ -123,9 +123,10 @@ export class IstanbulCoverageProvider implements CoverageProvider {
})

for (const reporter of this.options.reporter) {
reports.create(reporter, {
reports.create(reporter[0], {
skipFull: this.options.skipFull,
projectRoot: this.ctx.config.root,
...reporter[1],
}).execute(context)
}

Expand Down Expand Up @@ -221,7 +222,6 @@ export class IstanbulCoverageProvider implements CoverageProvider {

function resolveIstanbulOptions(options: CoverageIstanbulOptions, root: string): Options {
const reportsDirectory = resolve(root, options.reportsDirectory || coverageConfigDefaults.reportsDirectory)
const reporter = options.reporter || coverageConfigDefaults.reporter

const resolved: Options = {
...coverageConfigDefaults,
Expand All @@ -232,7 +232,7 @@ function resolveIstanbulOptions(options: CoverageIstanbulOptions, root: string):
// Resolved fields
provider: 'istanbul',
reportsDirectory,
reporter: Array.isArray(reporter) ? reporter : [reporter],
reporter: resolveReporters(options.reporter || coverageConfigDefaults.reporter),
}

return resolved
Expand Down Expand Up @@ -287,3 +287,25 @@ function isEmptyCoverageRange(range: libCoverage.Range) {
|| range.end.column === undefined
)
}

// TODO: Move to some generic utility for sharing with c8-provider
function resolveReporters(configReporters: NonNullable<CoverageOptions['reporter']>): Options['reporter'] {
// E.g. { reporter: "html" }
if (!Array.isArray(configReporters))
return [[configReporters, {}]]

const resolvedReporters: Options['reporter'] = []

for (const reporter of configReporters) {
if (Array.isArray(reporter)) {
// E.g. { reporter: [ ["html", { skipEmpty: true }], ["lcov"], ["json", { file: "map.json" }] ]}
resolvedReporters.push([reporter[0], reporter[1] || {}])
}
else {
// E.g. { reporter: ["html", "json"]}
resolvedReporters.push([reporter, {}])
}
}

return resolvedReporters
}
1 change: 1 addition & 0 deletions packages/vitest/package.json
Expand Up @@ -108,6 +108,7 @@
"dependencies": {
"@types/chai": "^4.3.4",
"@types/chai-subset": "^1.3.3",
"@types/istanbul-reports": "^3.0.1",
"@types/node": "*",
"acorn": "^8.8.1",
"acorn-walk": "^8.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/defaults.ts
Expand Up @@ -32,7 +32,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
cleanOnRerun: true,
reportsDirectory: './coverage',
exclude: defaultCoverageExcludes,
reporter: ['text', 'html', 'clover', 'json'],
reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]],
// default extensions used by c8, plus '.vue' and '.svelte'
// see https://github.com/istanbuljs/schema/blob/master/default-extension.js
extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte'],
Expand Down
28 changes: 11 additions & 17 deletions packages/vitest/src/types/coverage.ts
@@ -1,4 +1,5 @@
import type { TransformPluginContext, TransformResult } from 'rollup'
import type { ReportOptions } from 'istanbul-reports'
import type { Vitest } from '../node'
import type { Arrayable } from './general'
import type { AfterSuiteRunMeta } from './worker'
Expand Down Expand Up @@ -38,20 +39,14 @@ export interface CoverageProviderModule {
takeCoverage?(): unknown | Promise<unknown>
}

export type CoverageReporter =
| 'clover'
| 'cobertura'
| 'html-spa'
| 'html'
| 'json-summary'
| 'json'
| 'lcov'
| 'lcovonly'
| 'none'
| 'teamcity'
| 'text-lcov'
| 'text-summary'
| 'text'
export type CoverageReporter = keyof ReportOptions

type CoverageReporterWithOptions<ReporterName extends CoverageReporter = CoverageReporter> =
ReporterName extends CoverageReporter
? ReportOptions[ReporterName] extends never
? [ReporterName, {}] // E.g. the "none" reporter
: [ReporterName, Partial<ReportOptions[ReporterName]>]
: never

type Provider = 'c8' | 'istanbul' | CoverageProviderModule | undefined

Expand All @@ -68,14 +63,13 @@ type FieldsWithDefaultValues =
| 'reportsDirectory'
| 'exclude'
| 'extension'
| 'reporter'

export type ResolvedCoverageOptions<T extends Provider = Provider> =
& CoverageOptions<T>
& Required<Pick<CoverageOptions<T>, FieldsWithDefaultValues>>
// Resolved fields which may have different typings as public configuration API has
& {
reporter: CoverageReporter[]
reporter: CoverageReporterWithOptions[]
}

export interface BaseCoverageOptions {
Expand Down Expand Up @@ -137,7 +131,7 @@ export interface BaseCoverageOptions {
*
* @default ['text', 'html', 'clover', 'json']
*/
reporter?: Arrayable<CoverageReporter>
reporter?: Arrayable<CoverageReporter> | (CoverageReporter | [CoverageReporter] | CoverageReporterWithOptions)[]

/**
* Do not show files with 100% statement, branch, and function coverage
Expand Down
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/coverage-test/coverage-report-tests/utils.ts
Expand Up @@ -17,7 +17,7 @@ interface CoverageFinalJson {
*/
export async function readCoverageJson() {
// @ts-expect-error -- generated file
const { default: jsonReport } = await import('./coverage/coverage-final.json') as CoverageFinalJson
const { default: jsonReport } = await import('./coverage/custom-json-report-name.json') as CoverageFinalJson

const normalizedReport: CoverageFinalJson['default'] = {}

Expand Down
54 changes: 53 additions & 1 deletion test/coverage-test/test/configuration-options.test-d.ts
Expand Up @@ -28,7 +28,7 @@ test('providers, custom', () => {
enabled: true,
exclude: ['string'],
extension: ['string'],
reporter: ['html', 'json'],
reporter: [['html', {}], ['json', { file: 'string' }]],
reportsDirectory: 'string',
}
},
Expand Down Expand Up @@ -139,3 +139,55 @@ test('reporters, multiple', () => {
// @ts-expect-error -- ... and all reporters must be known
assertType<Coverage>({ reporter: ['html', 'json', 'unknown-reporter'] })
})

test('reporters, with options', () => {
assertType<Coverage>({
reporter: [
['clover', { projectRoot: 'string', file: 'string' }],
['cobertura', { projectRoot: 'string', file: 'string' }],
['html-spa', { metricsToShow: ['branches', 'functions'], verbose: true, subdir: 'string' }],
['html', { verbose: true, subdir: 'string' }],
['json-summary', { file: 'string' }],
['json', { file: 'string' }],
['lcov', { projectRoot: 'string', file: 'string' }],
['lcovonly', { projectRoot: 'string', file: 'string' }],
['none'],
['teamcity', { blockName: 'string' }],
['text-lcov', { projectRoot: 'string' }],
['text-summary', { file: 'string' }],
['text', { skipEmpty: true, skipFull: true, maxCols: 1 }],
],
})

assertType<Coverage>({
reporter: [
['html', { subdir: 'string' }],
['json'],
['lcov', { projectRoot: 'string' }],
],
})

assertType<Coverage>({
reporter: [
// @ts-expect-error -- teamcity report option on html reporter
['html', { blockName: 'string' }],

// @ts-expect-error -- html-spa report option on json reporter
['json', { metricsToShow: ['branches'] }],

// @ts-expect-error -- second value should be object even though TS intellisense prompts types of reporters
['lcov', 'html-spa'],
],
})
})

test('reporters, mixed variations', () => {
assertType<Coverage>({
reporter: [
'clover',
['cobertura'],
['html-spa', {}],
['html', { verbose: true, subdir: 'string' }],
],
})
})
7 changes: 6 additions & 1 deletion test/coverage-test/vitest.config.ts
Expand Up @@ -15,7 +15,12 @@ export default defineConfig({
enabled: true,
clean: true,
all: true,
reporter: ['html', 'text', 'lcov', 'json'],
reporter: [
'text',
['html'],
['lcov', {}],
['json', { file: 'custom-json-report-name.json' }],
],
},
setupFiles: [
resolve(__dirname, './setup.ts'),
Expand Down

0 comments on commit 7cf61b7

Please sign in to comment.