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 Feb 16, 2023
1 parent b67a5fb commit a5e7847
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 35 deletions.
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
34 changes: 32 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,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<CoverageC8Options['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
}
28 changes: 25 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,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<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' }],
],
})
})

0 comments on commit a5e7847

Please sign in to comment.