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(coverage): add support for coverage reporter options #2690

Merged
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
19 changes: 17 additions & 2 deletions docs/config/index.md
Expand Up @@ -725,13 +725,28 @@ Directory to write coverage report to.

#### 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
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -46,7 +46,6 @@
"@vitest/coverage-istanbul": "workspace:*",
"@vitest/ui": "workspace:*",
"bumpp": "^8.2.1",
"c8": "^7.12.0",
"esbuild": "^0.16.16",
"eslint": "^8.31.0",
"esno": "^0.16.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/coverage-c8/package.json
Expand Up @@ -45,7 +45,7 @@
"vitest": ">=0.29.0 <1"
},
"dependencies": {
"c8": "^7.12.0",
"c8": "^7.13.0",
"picocolors": "^1.0.0",
"std-env": "^3.3.1"
},
Expand Down
33 changes: 31 additions & 2 deletions packages/coverage-c8/src/provider.ts
Expand Up @@ -51,6 +51,15 @@ 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),
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 @@ -152,7 +161,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 @@ -166,7 +174,7 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {

// Resolved fields
provider: 'c8',
reporter: Array.isArray(reporter) ? reporter : [reporter],
reporter: resolveReporters(options.reporter || coverageConfigDefaults.reporter),
reportsDirectory,
}

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

return resolved
}

function resolveReporters(configReporters: NonNullable<CoverageC8Options['reporter']>): Options['reporter'] {
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved
// 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
}
27 changes: 24 additions & 3 deletions packages/coverage-istanbul/src/provider.ts
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,24 @@ function isEmptyCoverageRange(range: libCoverage.Range) {
|| range.end.column === undefined
)
}

function resolveReporters(configReporters: NonNullable<CoverageIstanbulOptions['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 @@ -144,6 +144,7 @@
"@edge-runtime/vm": "2.0.2",
"@sinonjs/fake-timers": "^10.0.2",
"@types/diff": "^5.0.2",
"@types/istanbul-reports": "^3.0.1",
"@types/jsdom": "^21.1.0",
"@types/micromatch": "^4.0.2",
"@types/natural-compare": "^1.4.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/defaults.ts
Expand Up @@ -33,7 +33,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 @@ -48,20 +49,14 @@ export interface CoverageProviderModule {
stopCoverage?(): 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' | 'custom' | undefined

Expand All @@ -79,14 +74,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 @@ -148,7 +142,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
29 changes: 24 additions & 5 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 @@ -18,7 +18,7 @@ interface CoverageFinalJson {
* Normalizes paths to keep contents consistent between OS's
*/
export async function readCoverageJson() {
const jsonReport = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf8')) as CoverageFinalJson
const jsonReport = JSON.parse(readFileSync('./coverage/custom-json-report-name.json', 'utf8')) 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 @@ -102,7 +102,7 @@ test('provider module', () => {
enabled: true,
exclude: ['string'],
extension: ['string'],
reporter: ['html', 'json'],
reporter: [['html', {}], ['json', { file: 'string' }]],
reportsDirectory: 'string',
}
},
Expand Down Expand Up @@ -165,3 +165,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' }],
],
})
})