From 4eb5030b86da04efa662b26e4a15b60d8a9ac379 Mon Sep 17 00:00:00 2001 From: AriPerkkio Date: Thu, 12 Jan 2023 21:19:57 +0200 Subject: [PATCH] feat(coverage): add support for coverage reporter options --- docs/config/index.md | 19 ++++++- package.json | 1 - packages/coverage-c8/package.json | 2 +- packages/coverage-c8/src/provider.ts | 33 +++++++++++- packages/coverage-istanbul/src/provider.ts | 27 ++++++++-- packages/vitest/package.json | 1 + packages/vitest/src/defaults.ts | 2 +- packages/vitest/src/types/coverage.ts | 28 ++++------ pnpm-lock.yaml | 29 ++++++++-- .../coverage-report-tests/utils.ts | 2 +- .../test/configuration-options.test-d.ts | 54 ++++++++++++++++++- test/coverage-test/vitest.config.ts | 7 ++- 12 files changed, 170 insertions(+), 35 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index 4a3c717a301d..cb62351448a5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -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=`, `--coverage.reporter= --coverage.reporter=` -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: + + ```ts + { + reporter: [ + ['lcov', { 'projectRoot': './src' }], + ['json', { 'file': 'coverage.json' }], + ['text'] + ] + } + ``` #### skipFull diff --git a/package.json b/package.json index 9a6270c9999e..b5c251937ce9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/coverage-c8/package.json b/packages/coverage-c8/package.json index 3b1073566a5e..8e9d65ca1e92 100644 --- a/packages/coverage-c8/package.json +++ b/packages/coverage-c8/package.json @@ -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" }, diff --git a/packages/coverage-c8/src/provider.ts b/packages/coverage-c8/src/provider.ts index a1a64d6df6ee..6573f873bfdd 100644 --- a/packages/coverage-c8/src/provider.ts +++ b/packages/coverage-c8/src/provider.ts @@ -51,6 +51,15 @@ export class C8CoverageProvider implements CoverageProvider { const options: ConstructorParameters[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) @@ -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, @@ -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, } @@ -179,3 +187,24 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options { return resolved } + +function resolveReporters(configReporters: NonNullable): 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 +} diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 9f019d40dfba..5abc0058a52b 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -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) } @@ -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, @@ -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 @@ -287,3 +287,24 @@ function isEmptyCoverageRange(range: libCoverage.Range) { || range.end.column === undefined ) } + +function resolveReporters(configReporters: NonNullable): 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 +} diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 3e328f345ace..0af61735b1f7 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -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", diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 6d6ec909d483..f6ed961c2e82 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -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'], diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts index 13c306b7f8c1..efa648a82667 100644 --- a/packages/vitest/src/types/coverage.ts +++ b/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' @@ -48,20 +49,14 @@ export interface CoverageProviderModule { stopCoverage?(): unknown | Promise } -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 + ? ReportOptions[ReporterName] extends never + ? [ReporterName, {}] // E.g. the "none" reporter + : [ReporterName, Partial] + : never type Provider = 'c8' | 'istanbul' | 'custom' | undefined @@ -79,14 +74,13 @@ type FieldsWithDefaultValues = | 'reportsDirectory' | 'exclude' | 'extension' - | 'reporter' export type ResolvedCoverageOptions = & CoverageOptions & Required, FieldsWithDefaultValues>> // Resolved fields which may have different typings as public configuration API has & { - reporter: CoverageReporter[] + reporter: CoverageReporterWithOptions[] } export interface BaseCoverageOptions { @@ -148,7 +142,7 @@ export interface BaseCoverageOptions { * * @default ['text', 'html', 'clover', 'json'] */ - reporter?: Arrayable + reporter?: Arrayable | (CoverageReporter | [CoverageReporter] | CoverageReporterWithOptions)[] /** * Do not show files with 100% statement, branch, and function coverage diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f878a07e6798..295195e1b3b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,6 @@ importers: '@vitest/coverage-istanbul': workspace:* '@vitest/ui': workspace:* bumpp: ^8.2.1 - c8: ^7.12.0 esbuild: ^0.16.3 eslint: ^8.31.0 esno: ^0.16.3 @@ -65,7 +64,6 @@ importers: '@vitest/coverage-istanbul': link:packages/coverage-istanbul '@vitest/ui': link:packages/ui bumpp: 8.2.1 - c8: 7.12.0 esbuild: 0.16.3 eslint: 8.31.0 esno: 0.16.3 @@ -650,14 +648,14 @@ importers: packages/coverage-c8: specifiers: - c8: ^7.12.0 + c8: ^7.13.0 pathe: ^1.1.0 picocolors: ^1.0.0 std-env: ^3.3.1 vite-node: workspace:* vitest: workspace:* dependencies: - c8: 7.12.0 + c8: 7.13.0 picocolors: 1.0.0 std-env: 3.3.1 devDependencies: @@ -852,6 +850,7 @@ importers: '@types/chai': ^4.3.4 '@types/chai-subset': ^1.3.3 '@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 @@ -935,6 +934,7 @@ importers: '@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 @@ -10300,6 +10300,25 @@ packages: v8-to-istanbul: 9.0.1 yargs: 16.2.0 yargs-parser: 20.2.9 + dev: true + + /c8/7.13.0: + resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.0 + istanbul-reports: 3.1.5 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.0.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 /cac/6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} @@ -12654,7 +12673,7 @@ packages: dependencies: '@babel/traverse': 7.20.12 '@babel/types': 7.20.7 - c8: 7.12.0 + c8: 7.13.0 transitivePeerDependencies: - supports-color dev: true diff --git a/test/coverage-test/coverage-report-tests/utils.ts b/test/coverage-test/coverage-report-tests/utils.ts index 6cc7600a3178..69fc835a3973 100644 --- a/test/coverage-test/coverage-report-tests/utils.ts +++ b/test/coverage-test/coverage-report-tests/utils.ts @@ -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'] = {} diff --git a/test/coverage-test/test/configuration-options.test-d.ts b/test/coverage-test/test/configuration-options.test-d.ts index 98389e84b1ae..867c970a9470 100644 --- a/test/coverage-test/test/configuration-options.test-d.ts +++ b/test/coverage-test/test/configuration-options.test-d.ts @@ -102,7 +102,7 @@ test('provider module', () => { enabled: true, exclude: ['string'], extension: ['string'], - reporter: ['html', 'json'], + reporter: [['html', {}], ['json', { file: 'string' }]], reportsDirectory: 'string', } }, @@ -165,3 +165,55 @@ test('reporters, multiple', () => { // @ts-expect-error -- ... and all reporters must be known assertType({ reporter: ['html', 'json', 'unknown-reporter'] }) }) + +test('reporters, with options', () => { + assertType({ + 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({ + reporter: [ + ['html', { subdir: 'string' }], + ['json'], + ['lcov', { projectRoot: 'string' }], + ], + }) + + assertType({ + 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({ + reporter: [ + 'clover', + ['cobertura'], + ['html-spa', {}], + ['html', { verbose: true, subdir: 'string' }], + ], + }) +}) diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts index 325f715f4112..559f7e9e34e1 100644 --- a/test/coverage-test/vitest.config.ts +++ b/test/coverage-test/vitest.config.ts @@ -19,7 +19,12 @@ export default defineConfig({ include: ['src/**'], 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'),