diff --git a/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts b/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts index 1b211f2ba..df76233b8 100644 --- a/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts +++ b/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts @@ -25,12 +25,7 @@ export default { ], plugins: [ await coveragePlugin({ - reports: [ - { - resultsPath: join('e2e', 'cli-e2e', 'mocks', 'fixtures', 'lcov.info'), - pathToProject: join('packages', 'cli'), - }, - ], + reports: [join('e2e', 'cli-e2e', 'mocks', 'fixtures', 'lcov.info')], }), ], } satisfies CoreConfig; diff --git a/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap b/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap index 78376699e..3e3c88591 100644 --- a/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -28,7 +28,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "Function formatReportScore is not called in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/partly-covered/utils.ts", + "file": "src/lib/partly-covered/utils.ts", "position": { "startLine": 2, }, @@ -38,7 +38,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "Function sortReport is not called in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/not-covered/sorting.ts", + "file": "src/lib/not-covered/sorting.ts", "position": { "startLine": 1, }, @@ -60,7 +60,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "2nd branch is not taken in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/partly-covered/utils.ts", + "file": "src/lib/partly-covered/utils.ts", "position": { "startLine": 6, }, @@ -70,7 +70,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "2nd branch is not taken in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/partly-covered/utils.ts", + "file": "src/lib/partly-covered/utils.ts", "position": { "startLine": 10, }, @@ -80,7 +80,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "1st branch is not taken in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/not-covered/sorting.ts", + "file": "src/lib/not-covered/sorting.ts", "position": { "startLine": 7, }, @@ -90,7 +90,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "2nd branch is not taken in any test case.", "severity": "error", "source": { - "file": "packages/cli/src/lib/not-covered/sorting.ts", + "file": "src/lib/not-covered/sorting.ts", "position": { "startLine": 7, }, @@ -112,7 +112,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "Lines 7-9 are not covered in any test case.", "severity": "warning", "source": { - "file": "packages/cli/src/lib/partly-covered/utils.ts", + "file": "src/lib/partly-covered/utils.ts", "position": { "endLine": 9, "startLine": 7, @@ -123,7 +123,7 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "message": "Lines 1-5 are not covered in any test case.", "severity": "warning", "source": { - "file": "packages/cli/src/lib/not-covered/sorting.ts", + "file": "src/lib/not-covered/sorting.ts", "position": { "endLine": 5, "startLine": 1, diff --git a/packages/plugin-coverage/README.md b/packages/plugin-coverage/README.md index aede2bb20..ac7cf1650 100644 --- a/packages/plugin-coverage/README.md +++ b/packages/plugin-coverage/README.md @@ -36,7 +36,7 @@ Measured coverage types are mapped to Code PushUp audits in the following way plugins: [ // ... await coveragePlugin({ - reports: [{ resultsPath: 'coverage/lcov.info' }], + reports: ['coverage/lcov.info'], coverageToolCommand: { command: 'npx', args: ['jest', '--coverage', '--coverageReporters=lcov'], @@ -119,8 +119,9 @@ It recognises the following entities: The plugin accepts the following parameters: - `coverageTypes`: An array of types of coverage that you wish to track. Supported values: `function`, `branch`, `line`. Defaults to all available types. -- `reports`: Array of information about files with code coverage results - paths to results, path to project root the results belong to. LCOV format is supported for now. - - If you have an Nx monorepo, you can adjust our helper function `getNxCoveragePaths` to get the path information automatically. +- `reports`: Array of information about files with code coverage results. LCOV format is supported for now. + - For a single project, providing paths to results as strings is enough. + - If you have a monorepo, both path to results (`resultsPath`) and path from the root to project the results belong to (`pathToProject`) need to be provided for the LCOV format. For Nx monorepos, you can use our helper function `getNxCoveragePaths` to get the path information automatically. - (optional) `coverageToolCommand`: If you wish to run your coverage tool to generate the results first, you may define it here. - (optional) `perfectScoreThreshold`: If your coverage goal is not 100%, you may define it here in range 0-1. Any score above the defined threshold will be given the perfect score. The value will stay unaffected. diff --git a/packages/plugin-coverage/src/lib/config.ts b/packages/plugin-coverage/src/lib/config.ts index ef2c5d336..02ed5a3d8 100644 --- a/packages/plugin-coverage/src/lib/config.ts +++ b/packages/plugin-coverage/src/lib/config.ts @@ -3,15 +3,26 @@ import { z } from 'zod'; export const coverageTypeSchema = z.enum(['function', 'branch', 'line']); export type CoverageType = z.infer; -export const coverageResultSchema = z.object({ - resultsPath: z.string().includes('lcov'), - pathToProject: z +export const coverageResultSchema = z.union([ + z.object({ + resultsPath: z + .string({ + description: 'Path to coverage results for Nx setup.', + }) + .includes('lcov'), + pathToProject: z + .string({ + description: + 'Path from workspace root to project root. Necessary for LCOV reports which provide a relative path.', + }) + .optional(), + }), + z .string({ - description: - 'Path from workspace root to project root. Necessary for LCOV reports.', + description: 'Path to coverage results for a single project setup.', }) - .optional(), -}); + .includes('lcov'), +]); export type CoverageResult = z.infer; export const coveragePluginConfigSchema = z.object({ diff --git a/packages/plugin-coverage/src/lib/config.unit.test.ts b/packages/plugin-coverage/src/lib/config.unit.test.ts index 48140248c..2a6d510d8 100644 --- a/packages/plugin-coverage/src/lib/config.unit.test.ts +++ b/packages/plugin-coverage/src/lib/config.unit.test.ts @@ -28,14 +28,14 @@ describe('coveragePluginConfigSchema', () => { it('accepts a minimal code coverage configuration', () => { expect(() => coveragePluginConfigSchema.parse({ - reports: [{ resultsPath: 'coverage/cli/lcov.info' }], + reports: ['coverage/cli/lcov.info'], } satisfies CoveragePluginConfig), ).not.toThrow(); }); it('replaces undefined coverage with all available types', () => { const config = { - reports: [{ resultsPath: 'coverage/cli/lcov.info' }], + reports: ['coverage/cli/lcov.info'], } satisfies CoveragePluginConfig; expect(() => coveragePluginConfigSchema.parse(config)).not.toThrow(); @@ -51,7 +51,7 @@ describe('coveragePluginConfigSchema', () => { expect(() => coveragePluginConfigSchema.parse({ coverageTypes: [], - reports: [{ resultsPath: 'coverage/cli/lcov.info' }], + reports: ['coverage/cli/lcov.info'], } satisfies CoveragePluginConfig), ).toThrow('too_small'); }); @@ -69,7 +69,7 @@ describe('coveragePluginConfigSchema', () => { expect(() => coveragePluginConfigSchema.parse({ coverageTypes: ['line'], - reports: [{ resultsPath: 'coverage/cli/coverage-final.json' }], + reports: ['coverage/cli/coverage-final.json'], } satisfies CoveragePluginConfig), ).toThrow(/Invalid input: must include.+lcov/); }); @@ -90,7 +90,7 @@ describe('coveragePluginConfigSchema', () => { expect(() => coveragePluginConfigSchema.parse({ coverageTypes: ['line'], - reports: [{ resultsPath: 'coverage/cli/lcov.info' }], + reports: ['coverage/cli/lcov.info'], perfectScoreThreshold: 1.1, } satisfies CoveragePluginConfig), ).toThrow('too_big'); diff --git a/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts index a3879c198..89308056c 100644 --- a/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts +++ b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts @@ -22,7 +22,7 @@ describe('coveragePlugin', () => { await expect( coveragePlugin({ coverageTypes: ['function'], - reports: [{ resultsPath: LCOV_PATH }], + reports: [LCOV_PATH], }), ).resolves.toStrictEqual( expect.objectContaining({ @@ -39,7 +39,7 @@ describe('coveragePlugin', () => { await expect( coveragePlugin({ coverageTypes: ['function', 'branch'], - reports: [{ resultsPath: LCOV_PATH }], + reports: [LCOV_PATH], }), ).resolves.toStrictEqual( expect.objectContaining({ diff --git a/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap b/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap index d9d95ca0f..d5f456560 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap +++ b/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`lcovResultsToAuditOutputs > should correctly convert lcov results to AuditOutputs 1`] = ` +exports[`lcovResultsToAuditOutputs > should correctly convert lcov results to AuditOutputs and prepend project paths 1`] = ` [ { "details": { diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts index 58bea1618..160dfddcb 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts @@ -4,7 +4,7 @@ import { describe, it } from 'vitest'; import { lcovResultsToAuditOutputs } from './lcov-runner'; describe('lcovResultsToAuditOutputs', () => { - it('should correctly convert lcov results to AuditOutputs', async () => { + it('should correctly convert lcov results to AuditOutputs and prepend project paths', async () => { /** * The stats passed in the fixture are as follows * Functions: 2 found, 2 covered (100% coverage) diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts index 7f4023ac8..25af2b1ec 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts @@ -53,12 +53,14 @@ async function parseLcovFiles( ): Promise { const parsedResults = await Promise.all( results.map(async result => { - const lcovFileContent = await readTextFile(result.resultsPath); + const resultsPath = + typeof result === 'string' ? result : result.resultsPath; + const lcovFileContent = await readTextFile(resultsPath); const parsedRecords = parseLcov(toUnixNewlines(lcovFileContent)); return parsedRecords.map(record => ({ ...record, file: - result.pathToProject == null + typeof result === 'string' || result.pathToProject == null ? record.file : join(result.pathToProject, record.file), })); diff --git a/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts index aa02ed30e..4bdd7ee32 100644 --- a/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts +++ b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts @@ -11,7 +11,7 @@ import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH, WORKDIR } from './constants'; describe('createRunnerConfig', () => { it('should create a valid runner config', async () => { const runnerConfig = await createRunnerConfig('executeRunner.ts', { - reports: [{ resultsPath: 'coverage/lcov.info' }], + reports: ['coverage/lcov.info'], coverageTypes: ['branch'], perfectScoreThreshold: 85, }); @@ -28,7 +28,7 @@ describe('createRunnerConfig', () => { const pluginConfig: FinalCoveragePluginConfig = { coverageTypes: ['line'], - reports: [{ resultsPath: 'coverage/lcov.info' }], + reports: ['coverage/lcov.info'], coverageToolCommand: { command: 'npm', args: ['run', 'test'] }, perfectScoreThreshold: 85, }; @@ -46,16 +46,14 @@ describe('executeRunner', () => { it('should successfully execute runner', async () => { const config: FinalCoveragePluginConfig = { reports: [ - { - resultsPath: join( - fileURLToPath(dirname(import.meta.url)), - '..', - '..', - '..', - 'mocks', - 'single-record-lcov.info', - ), - }, + join( + fileURLToPath(dirname(import.meta.url)), + '..', + '..', + '..', + 'mocks', + 'single-record-lcov.info', + ), ], coverageTypes: ['line'], };