From d259c14814d256bb1ad3dfb749a7c8ac25640d2b Mon Sep 17 00:00:00 2001 From: Katerina Pilatova Date: Mon, 12 Feb 2024 14:54:07 +0100 Subject: [PATCH] feat(plugin-coverage): add coverage tool run option, convert to runnerConfig --- .github/workflows/ci.yml | 2 - .github/workflows/release.yml | 2 - code-pushup.config.ts | 6 +- .../fixtures/code-pushup.config.coverage.ts | 19 +---- .../__snapshots__/collect.e2e.test.ts.snap | 84 ++++++++----------- packages/plugin-coverage/project.json | 1 + packages/plugin-coverage/src/bin.ts | 3 + packages/plugin-coverage/src/lib/config.ts | 9 +- .../src/lib/coverage-plugin.ts | 56 ++++--------- ...n.test.ts => coverage-plugin.unit.test.ts} | 69 +++++---------- .../src/lib/runner/constants.ts | 10 +++ .../plugin-coverage/src/lib/runner/index.ts | 50 +++++++++++ ...p => lcov-runner.integration.test.ts.snap} | 0 ...est.ts => lcov-runner.integration.test.ts} | 2 +- .../runner/lcov/{runner.ts => lcov-runner.ts} | 72 +++++++++------- .../src/lib/runner/runner.integration.test.ts | 82 ++++++++++++++++++ packages/utils/src/index.ts | 1 + packages/utils/src/lib/file-system.ts | 8 +- 18 files changed, 285 insertions(+), 191 deletions(-) create mode 100644 packages/plugin-coverage/src/bin.ts rename packages/plugin-coverage/src/lib/{coverage-plugin.integration.test.ts => coverage-plugin.unit.test.ts} (52%) create mode 100644 packages/plugin-coverage/src/lib/runner/constants.ts create mode 100644 packages/plugin-coverage/src/lib/runner/index.ts rename packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/{runner.integration.test.ts.snap => lcov-runner.integration.test.ts.snap} (100%) rename packages/plugin-coverage/src/lib/runner/lcov/{runner.integration.test.ts => lcov-runner.integration.test.ts} (94%) rename packages/plugin-coverage/src/lib/runner/lcov/{runner.ts => lcov-runner.ts} (69%) create mode 100644 packages/plugin-coverage/src/lib/runner/runner.integration.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 044929ef5..1d5886739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,8 +173,6 @@ jobs: cache: npm - name: Install dependencies run: npm ci - - name: Collect unit test coverage (temporary due to coverage plugin) - run: npx nx run-many -t unit-test --coverage - name: Collect Code PushUp report run: npx nx run-collect - name: Upload Code PushUp report to portal diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bed1f33c7..aecc69130 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,8 +90,6 @@ jobs: run: | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc npx nx run-many -t=deploy --exclude=plugin-lighthouse,nx-plugin - - name: Collect unit test coverage (temporary due to coverage plugin) - run: npx nx run-many -t unit-test --coverage - name: Collect and upload Code PushUp report run: npx nx run-autorun env: diff --git a/code-pushup.config.ts b/code-pushup.config.ts index 03b535320..d25c5e217 100644 --- a/code-pushup.config.ts +++ b/code-pushup.config.ts @@ -49,7 +49,11 @@ const config: CoreConfig = { plugins: [ await eslintPlugin(await eslintConfigFromNxProjects()), - coveragePlugin({ + await coveragePlugin({ + coverageToolCommand: { + command: 'npx', + args: ['nx', 'run-many', '-t', 'unit-test', '--coverage'], + }, reports: [ { resultsPath: 'coverage/cli/unit-tests/lcov.info', 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 99cf2f19c..1b211f2ba 100644 --- a/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts +++ b/e2e/cli-e2e/mocks/fixtures/code-pushup.config.coverage.ts @@ -15,29 +15,16 @@ export default { title: 'Code coverage', refs: [ { - type: 'audit', + type: 'group', plugin: 'coverage', - slug: 'function-coverage', - weight: 1, - }, - { - type: 'audit', - plugin: 'coverage', - slug: 'branch-coverage', - weight: 1, - }, - { - type: 'audit', - plugin: 'coverage', - slug: 'line-coverage', + slug: 'coverage', weight: 1, }, ], }, ], plugins: [ - coveragePlugin({ - coverageTypes: ['branch', 'function', 'line'], + await coveragePlugin({ reports: [ { resultsPath: join('e2e', 'cli-e2e', 'mocks', 'fixtures', 'lcov.info'), 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 2e0fac472..b8e0164d6 100644 --- a/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/cli-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -7,20 +7,8 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "refs": [ { "plugin": "coverage", - "slug": "function-coverage", - "type": "audit", - "weight": 1, - }, - { - "plugin": "coverage", - "slug": "branch-coverage", - "type": "audit", - "weight": 1, - }, - { - "plugin": "coverage", - "slug": "line-coverage", - "type": "audit", + "slug": "coverage", + "type": "group", "weight": 1, }, ], @@ -32,6 +20,38 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "plugins": [ { "audits": [ + { + "description": "Measures how many functions were called in at least one test.", + "details": { + "issues": [ + { + "message": "Function formatReportScore is not called in any test case.", + "severity": "error", + "source": { + "file": "packages/cli/src/lib/partly-covered/utils.ts", + "position": { + "startLine": 2, + }, + }, + }, + { + "message": "Function sortReport is not called in any test case.", + "severity": "error", + "source": { + "file": "packages/cli/src/lib/not-covered/sorting.ts", + "position": { + "startLine": 1, + }, + }, + }, + ], + }, + "displayValue": "60 %", + "score": 0.6, + "slug": "function-coverage", + "title": "Function coverage", + "value": 60, + }, { "description": "Measures how many branches were executed after conditional statements in at least one test.", "details": { @@ -84,38 +104,6 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "title": "Branch coverage", "value": 76, }, - { - "description": "Measures how many functions were called in at least one test.", - "details": { - "issues": [ - { - "message": "Function formatReportScore is not called in any test case.", - "severity": "error", - "source": { - "file": "packages/cli/src/lib/partly-covered/utils.ts", - "position": { - "startLine": 2, - }, - }, - }, - { - "message": "Function sortReport is not called in any test case.", - "severity": "error", - "source": { - "file": "packages/cli/src/lib/not-covered/sorting.ts", - "position": { - "startLine": 1, - }, - }, - }, - ], - }, - "displayValue": "60 %", - "score": 0.6, - "slug": "function-coverage", - "title": "Function coverage", - "value": 60, - }, { "description": "Measures how many lines of code were executed in at least one test.", "details": { @@ -158,11 +146,11 @@ exports[`CLI collect > should run Code coverage plugin and create report.json 1` "description": "Group containing all defined coverage types as audits.", "refs": [ { - "slug": "branch-coverage", + "slug": "function-coverage", "weight": 1, }, { - "slug": "function-coverage", + "slug": "branch-coverage", "weight": 1, }, { diff --git a/packages/plugin-coverage/project.json b/packages/plugin-coverage/project.json index 2b325a66f..ed08d37de 100644 --- a/packages/plugin-coverage/project.json +++ b/packages/plugin-coverage/project.json @@ -11,6 +11,7 @@ "outputPath": "dist/packages/plugin-coverage", "main": "packages/plugin-coverage/src/index.ts", "tsConfig": "packages/plugin-coverage/tsconfig.lib.json", + "additionalEntryPoints": ["packages/plugin-coverage/src/bin.ts"], "assets": ["packages/plugin-coverage/*.md"], "esbuildConfig": "esbuild.config.js" } diff --git a/packages/plugin-coverage/src/bin.ts b/packages/plugin-coverage/src/bin.ts new file mode 100644 index 000000000..9121d88b2 --- /dev/null +++ b/packages/plugin-coverage/src/bin.ts @@ -0,0 +1,3 @@ +import { executeRunner } from './lib/runner'; + +await executeRunner().catch(console.error); diff --git a/packages/plugin-coverage/src/lib/config.ts b/packages/plugin-coverage/src/lib/config.ts index bd7a6f4bf..ef2c5d336 100644 --- a/packages/plugin-coverage/src/lib/config.ts +++ b/packages/plugin-coverage/src/lib/config.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const coverageTypeSchema = z.enum(['function', 'branch', 'line']); export type CoverageType = z.infer; -export const coverageReportSchema = z.object({ +export const coverageResultSchema = z.object({ resultsPath: z.string().includes('lcov'), pathToProject: z .string({ @@ -12,7 +12,7 @@ export const coverageReportSchema = z.object({ }) .optional(), }); -export type CoverageReport = z.infer; +export type CoverageResult = z.infer; export const coveragePluginConfigSchema = z.object({ coverageToolCommand: z @@ -34,7 +34,7 @@ export const coveragePluginConfigSchema = z.object({ .min(1) .default(['function', 'branch', 'line']), reports: z - .array(coverageReportSchema, { + .array(coverageResultSchema, { description: 'Path to all code coverage report files. Only LCOV format is supported for now.', }) @@ -49,3 +49,6 @@ export const coveragePluginConfigSchema = z.object({ .optional(), }); export type CoveragePluginConfig = z.input; +export type FinalCoveragePluginConfig = z.infer< + typeof coveragePluginConfigSchema +>; diff --git a/packages/plugin-coverage/src/lib/coverage-plugin.ts b/packages/plugin-coverage/src/lib/coverage-plugin.ts index f769f101a..986a03abb 100644 --- a/packages/plugin-coverage/src/lib/coverage-plugin.ts +++ b/packages/plugin-coverage/src/lib/coverage-plugin.ts @@ -1,21 +1,11 @@ -import { join } from 'node:path'; -import type { - Audit, - Group, - PluginConfig, - RunnerConfig, - RunnerFunction, -} from '@code-pushup/models'; -import { capitalize, pluginWorkDir } from '@code-pushup/utils'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { Audit, Group, PluginConfig } from '@code-pushup/models'; +import { capitalize } from '@code-pushup/utils'; import { name, version } from '../../package.json'; import { CoveragePluginConfig, coveragePluginConfigSchema } from './config'; -import { lcovResultsToAuditOutputs } from './runner/lcov/runner'; -import { applyMaxScoreAboveThreshold, coverageDescription } from './utils'; - -export const RUNNER_OUTPUT_PATH = join( - pluginWorkDir('coverage'), - 'runner-output.json', -); +import { createRunnerConfig } from './runner'; +import { coverageDescription } from './utils'; /** * Instantiates Code PushUp code coverage plugin for core config. @@ -35,11 +25,12 @@ export const RUNNER_OUTPUT_PATH = join( * * @returns Plugin configuration. */ -export function coveragePlugin(config: CoveragePluginConfig): PluginConfig { - const { reports, perfectScoreThreshold, coverageTypes, coverageToolCommand } = - coveragePluginConfigSchema.parse(config); +export async function coveragePlugin( + config: CoveragePluginConfig, +): Promise { + const coverageConfig = coveragePluginConfigSchema.parse(config); - const audits = coverageTypes.map( + const audits = coverageConfig.coverageTypes.map( (type): Audit => ({ slug: `${type}-coverage`, title: `${capitalize(type)} coverage`, @@ -54,25 +45,10 @@ export function coveragePlugin(config: CoveragePluginConfig): PluginConfig { refs: audits.map(audit => ({ ...audit, weight: 1 })), }; - const getAuditOutputs = async () => - perfectScoreThreshold - ? applyMaxScoreAboveThreshold( - await lcovResultsToAuditOutputs(reports, coverageTypes), - perfectScoreThreshold, - ) - : await lcovResultsToAuditOutputs(reports, coverageTypes); - - // if coverage results are provided, only convert them to AuditOutputs - // if not, run coverage command and then run result conversion - const runner: RunnerConfig | RunnerFunction = - coverageToolCommand == null - ? getAuditOutputs - : { - command: coverageToolCommand.command, - args: coverageToolCommand.args, - outputFile: RUNNER_OUTPUT_PATH, - outputTransform: getAuditOutputs, - }; + const runnerScriptPath = join( + fileURLToPath(dirname(import.meta.url)), + 'bin.js', + ); return { slug: 'coverage', @@ -84,6 +60,6 @@ export function coveragePlugin(config: CoveragePluginConfig): PluginConfig { version, audits, groups: [group], - runner, + runner: await createRunnerConfig(runnerScriptPath, coverageConfig), }; } diff --git a/packages/plugin-coverage/src/lib/coverage-plugin.integration.test.ts b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts similarity index 52% rename from packages/plugin-coverage/src/lib/coverage-plugin.integration.test.ts rename to packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts index f39b545cc..a3879c198 100644 --- a/packages/plugin-coverage/src/lib/coverage-plugin.integration.test.ts +++ b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts @@ -1,8 +1,15 @@ import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { CoveragePluginConfig } from './config'; +import { RunnerConfig } from '@code-pushup/models'; import { coveragePlugin } from './coverage-plugin'; +vi.mock('./runner/index.ts', () => ({ + createRunnerConfig: vi.fn().mockReturnValue({ + command: 'node', + outputFile: 'runner-output.json', + } satisfies RunnerConfig), +})); + describe('coveragePlugin', () => { const LCOV_PATH = join( 'packages', @@ -11,35 +18,38 @@ describe('coveragePlugin', () => { 'single-record-lcov.info', ); - it('should initialise a Code coverage plugin', () => { - expect( + it('should initialise a Code coverage plugin', async () => { + await expect( coveragePlugin({ coverageTypes: ['function'], reports: [{ resultsPath: LCOV_PATH }], }), - ).toStrictEqual( + ).resolves.toStrictEqual( expect.objectContaining({ slug: 'coverage', title: 'Code coverage', audits: expect.any(Array), groups: expect.any(Array), + runner: expect.any(Object), }), ); }); - it('should generate audits from coverage types', () => { - expect( + it('should generate audits from coverage types', async () => { + await expect( coveragePlugin({ coverageTypes: ['function', 'branch'], reports: [{ resultsPath: LCOV_PATH }], }), - ).toStrictEqual( + ).resolves.toStrictEqual( expect.objectContaining({ audits: [ { slug: 'function-coverage', title: 'Function coverage', - description: expect.stringContaining('Function coverage'), + description: expect.stringContaining( + 'how many functions were called', + ), }, expect.objectContaining({ slug: 'branch-coverage' }), ], @@ -47,13 +57,13 @@ describe('coveragePlugin', () => { ); }); - it('should provide a group from defined coverage types', () => { - expect( + it('should provide a group from defined coverage types', async () => { + await expect( coveragePlugin({ coverageTypes: ['branch', 'line'], reports: [{ resultsPath: LCOV_PATH }], }), - ).toStrictEqual( + ).resolves.toStrictEqual( expect.objectContaining({ audits: [ expect.objectContaining({ slug: 'branch-coverage' }), @@ -71,41 +81,4 @@ describe('coveragePlugin', () => { }), ); }); - - it('should assign RunnerConfig when a command is passed', () => { - expect( - coveragePlugin({ - coverageTypes: ['line'], - reports: [{ resultsPath: LCOV_PATH }], - coverageToolCommand: { - command: 'npm run-many', - args: ['-t', 'test', '--coverage'], - }, - } satisfies CoveragePluginConfig), - ).toStrictEqual( - expect.objectContaining({ - slug: 'coverage', - runner: { - command: 'npm run-many', - args: ['-t', 'test', '--coverage'], - outputFile: expect.stringContaining('runner-output.json'), - outputTransform: expect.any(Function), - }, - }), - ); - }); - - it('should assign a RunnerFunction when only reports are passed', () => { - expect( - coveragePlugin({ - coverageTypes: ['line'], - reports: [{ resultsPath: LCOV_PATH }], - } satisfies CoveragePluginConfig), - ).toStrictEqual( - expect.objectContaining({ - slug: 'coverage', - runner: expect.any(Function), - }), - ); - }); }); diff --git a/packages/plugin-coverage/src/lib/runner/constants.ts b/packages/plugin-coverage/src/lib/runner/constants.ts new file mode 100644 index 000000000..45490e44a --- /dev/null +++ b/packages/plugin-coverage/src/lib/runner/constants.ts @@ -0,0 +1,10 @@ +import { join } from 'node:path'; +import { pluginWorkDir } from '@code-pushup/utils'; + +export const WORKDIR = pluginWorkDir('coverage'); +export const RUNNER_OUTPUT_PATH = join(WORKDIR, 'runner-output.json'); +export const PLUGIN_CONFIG_PATH = join( + process.cwd(), + WORKDIR, + 'plugin-config.json', +); diff --git a/packages/plugin-coverage/src/lib/runner/index.ts b/packages/plugin-coverage/src/lib/runner/index.ts new file mode 100644 index 000000000..42f931d34 --- /dev/null +++ b/packages/plugin-coverage/src/lib/runner/index.ts @@ -0,0 +1,50 @@ +import { writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { AuditOutputs, RunnerConfig } from '@code-pushup/models'; +import { + ensureDirectoryExists, + executeProcess, + readJsonFile, +} from '@code-pushup/utils'; +import { FinalCoveragePluginConfig } from '../config'; +import { applyMaxScoreAboveThreshold } from '../utils'; +import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH, WORKDIR } from './constants'; +import { lcovResultsToAuditOutputs } from './lcov/lcov-runner'; + +export async function executeRunner(): Promise { + const { reports, coverageToolCommand, coverageTypes } = + await readJsonFile(PLUGIN_CONFIG_PATH); + + // Run coverage tool if provided + if (coverageToolCommand != null) { + const { command, args } = coverageToolCommand; + await executeProcess({ command, args }); + } + + // Caculate coverage from LCOV results + const auditOutputs = await lcovResultsToAuditOutputs(reports, coverageTypes); + + await ensureDirectoryExists(dirname(RUNNER_OUTPUT_PATH)); + await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(auditOutputs)); +} + +export async function createRunnerConfig( + scriptPath: string, + config: FinalCoveragePluginConfig, +): Promise { + // Create JSON config for executeRunner + await ensureDirectoryExists(WORKDIR); + await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + + const threshold = config.perfectScoreThreshold; + + return { + command: 'node', + args: [scriptPath], + outputFile: RUNNER_OUTPUT_PATH, + ...(threshold != null && { + outputTransform: outputs => + applyMaxScoreAboveThreshold(outputs as AuditOutputs, threshold), + }), + }; +} diff --git a/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/runner.integration.test.ts.snap b/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap similarity index 100% rename from packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/runner.integration.test.ts.snap rename to packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.integration.test.ts.snap diff --git a/packages/plugin-coverage/src/lib/runner/lcov/runner.integration.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts similarity index 94% rename from packages/plugin-coverage/src/lib/runner/lcov/runner.integration.test.ts rename to packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts index 626867bfa..58bea1618 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/runner.integration.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.integration.test.ts @@ -1,7 +1,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, it } from 'vitest'; -import { lcovResultsToAuditOutputs } from './runner'; +import { lcovResultsToAuditOutputs } from './lcov-runner'; describe('lcovResultsToAuditOutputs', () => { it('should correctly convert lcov results to AuditOutputs', async () => { diff --git a/packages/plugin-coverage/src/lib/runner/lcov/runner.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts similarity index 69% rename from packages/plugin-coverage/src/lib/runner/lcov/runner.ts rename to packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts index 1888c265b..7f4023ac8 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/runner.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts @@ -2,7 +2,7 @@ import { join } from 'node:path'; import type { LCOVRecord } from 'parse-lcov'; import { AuditOutputs } from '@code-pushup/models'; import { exists, readTextFile, toUnixNewlines } from '@code-pushup/utils'; -import { CoverageReport, CoverageType } from '../../config'; +import { CoverageResult, CoverageType } from '../../config'; import { parseLcov } from './parse-lcov'; import { lcovCoverageToAuditOutput, @@ -15,40 +15,20 @@ import { LCOVStat, LCOVStats } from './types'; /** * - * @param reports report files + * @param results Paths to LCOV results * @param coverageTypes types of coverage to be considered * @returns Audit outputs with complete coverage data. */ export async function lcovResultsToAuditOutputs( - reports: CoverageReport[], + results: CoverageResult[], coverageTypes: CoverageType[], ): Promise { - const parsedReports = await Promise.all( - reports.map(async report => { - const reportContent = await readTextFile(report.resultsPath); - const parsedRecords = parseLcov(toUnixNewlines(reportContent)); - return parsedRecords.map(record => ({ - ...record, - file: - report.pathToProject == null - ? record.file - : join(report.pathToProject, record.file), - })); - }), - ); - if (parsedReports.length !== reports.length) { - throw new Error('Some provided LCOV reports were not valid.'); - } - - const flatReports = parsedReports.flat(); - - if (flatReports.length === 0) { - throw new Error('All provided reports are empty.'); - } + // Parse lcov files + const lcovResults = await parseLcovFiles(results); - // Accumulate code coverage from all coverage result files - const totalCoverageStats = getTotalCoverageFromLcovReports( - flatReports, + // Calculate code coverage from all coverage results + const totalCoverageStats = getTotalCoverageFromLcovRecords( + lcovResults, coverageTypes, ); @@ -63,13 +43,47 @@ export async function lcovResultsToAuditOutputs( .filter(exists); } +/** + * + * @param results Paths to LCOV results + * @returns Array of parsed LCOVRecords. + */ +async function parseLcovFiles( + results: CoverageResult[], +): Promise { + const parsedResults = await Promise.all( + results.map(async result => { + const lcovFileContent = await readTextFile(result.resultsPath); + const parsedRecords = parseLcov(toUnixNewlines(lcovFileContent)); + return parsedRecords.map(record => ({ + ...record, + file: + result.pathToProject == null + ? record.file + : join(result.pathToProject, record.file), + })); + }), + ); + if (parsedResults.length !== results.length) { + throw new Error('Some provided LCOV results were not valid.'); + } + + const flatResults = parsedResults.flat(); + + if (flatResults.length === 0) { + throw new Error('All provided results are empty.'); + } + + return flatResults; +} + /** * * @param records This function aggregates coverage stats from all coverage files * @param coverageTypes Types of coverage to be gathered * @returns Complete coverage stats for all defined types of coverage. */ -function getTotalCoverageFromLcovReports( +function getTotalCoverageFromLcovRecords( records: LCOVRecord[], coverageTypes: CoverageType[], ): LCOVStats { diff --git a/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts new file mode 100644 index 000000000..81f7a521b --- /dev/null +++ b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts @@ -0,0 +1,82 @@ +import { writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, it } from 'vitest'; +import { AuditOutput, AuditOutputs, RunnerConfig } from '@code-pushup/models'; +import { readJsonFile, removeDirectoryIfExists } from '@code-pushup/utils'; +import { createRunnerConfig, executeRunner } from '.'; +import { FinalCoveragePluginConfig } from '../config'; +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' }], + coverageTypes: ['branch'], + perfectScoreThreshold: 85, + }); + expect(runnerConfig).toStrictEqual({ + command: 'node', + args: ['executeRunner.ts'], + outputTransform: expect.any(Function), + outputFile: expect.stringContaining('runner-output.json'), + } satisfies RunnerConfig); + }); + + it('should provide plugin config to runner in JSON file', async () => { + await removeDirectoryIfExists(WORKDIR); + + const pluginConfig: FinalCoveragePluginConfig = { + coverageTypes: ['line'], + reports: [{ resultsPath: 'coverage/lcov.info' }], + coverageToolCommand: { command: 'npm', args: ['run', 'test'] }, + perfectScoreThreshold: 85, + }; + + await createRunnerConfig('executeRunner.ts', pluginConfig); + + const config = await readJsonFile( + PLUGIN_CONFIG_PATH, + ); + expect(config).toStrictEqual(pluginConfig); + }); +}); + +describe('executeRunner', () => { + it('should successfully execute runner', async () => { + const config = { + reports: [ + { + resultsPath: join( + fileURLToPath(dirname(import.meta.url)), + '..', + '..', + '..', + 'mocks', + 'single-record-lcov.info', + ), + }, + ], + coverageTypes: ['line'], + }; + + await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + await executeRunner(); + + const results = await readJsonFile(RUNNER_OUTPUT_PATH); + expect(results).toStrictEqual([ + expect.objectContaining({ + slug: 'line-coverage', + score: 0.7, + value: 70, + details: { + issues: [ + expect.objectContaining({ + message: 'Lines 7-9 are not covered in any test case.', + }), + ], + }, + } satisfies AuditOutput), + ]); + }); +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 932cde2d4..cec3a39b8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -20,6 +20,7 @@ export { pluginWorkDir, readJsonFile, readTextFile, + removeDirectoryIfExists, } from './lib/file-system'; export { formatBytes, diff --git a/packages/utils/src/lib/file-system.ts b/packages/utils/src/lib/file-system.ts index 3df771856..256768d49 100644 --- a/packages/utils/src/lib/file-system.ts +++ b/packages/utils/src/lib/file-system.ts @@ -1,6 +1,6 @@ import { type Options, bundleRequire } from 'bundle-require'; import chalk from 'chalk'; -import { mkdir, readFile, readdir, stat } from 'node:fs/promises'; +import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises'; import { join } from 'node:path'; import { formatBytes } from './formatting'; import { logMultipleResults } from './log-results'; @@ -45,6 +45,12 @@ export async function ensureDirectoryExists(baseDir: string) { } } +export async function removeDirectoryIfExists(dir: string) { + if (await directoryExists(dir)) { + await rm(dir, { recursive: true, force: true }); + } +} + export type FileResult = readonly [string] | readonly [string, number]; export type MultipleFileResults = PromiseSettledResult[];