From d5239a541eb0d285b67d170f4b13c907c6c75321 Mon Sep 17 00:00:00 2001 From: AS Date: Fri, 8 Aug 2025 17:16:20 +0300 Subject: [PATCH 1/3] test: improve test coverage for plugin-coverage --- .../nx-workspace/code-pushup.config.ts | 23 + .../fixtures/nx-workspace/coverage/lcov.info | 39 ++ .../mocks/fixtures/nx-workspace/nx.json | 11 + .../mocks/fixtures/nx-workspace/package.json | 10 + .../fixtures/nx-workspace/vitest.config.ts | 10 + .../coverage-paths.e2e.test.ts.snap | 629 ++++++++++++++++++ .../tests/coverage-paths.e2e.test.ts | 74 +++ .../mocks/duplicate-record-lcov.info | 33 + .../lcov-runner.int.test.ts.snap | 188 ++++++ .../lib/runner/lcov/lcov-runner.int.test.ts | 33 + .../lib/runner/lcov/lcov-runner.unit.test.ts | 276 +++++++- .../lib/runner/lcov/merge-lcov.unit.test.ts | 258 +++++++ packages/plugin-coverage/vitest.int.config.ts | 2 +- .../plugin-coverage/vitest.unit.config.ts | 2 +- 14 files changed, 1581 insertions(+), 7 deletions(-) create mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts create mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info create mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json create mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json create mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts create mode 100644 e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap create mode 100644 e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts create mode 100644 packages/plugin-coverage/mocks/duplicate-record-lcov.info diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts new file mode 100644 index 000000000..8567072cf --- /dev/null +++ b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts @@ -0,0 +1,23 @@ +import coveragePlugin from '@code-pushup/coverage-plugin'; + +export default { + plugins: [ + await coveragePlugin({ + reports: ['coverage/lcov.info'], + }), + ], + categories: [ + { + slug: 'code-coverage', + title: 'Code coverage', + refs: [ + { + type: 'group', + plugin: 'coverage', + slug: 'coverage', + weight: 1, + }, + ], + }, + ], +}; diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info new file mode 100644 index 000000000..56f86e380 --- /dev/null +++ b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info @@ -0,0 +1,39 @@ +TN: +SF:src/utils.ts +FN:1,add +FN:5,subtract +FN:9,multiply +FN:13,divide +FNF:4 +FNH:4 +FNDA:1,add +FNDA:5,subtract +FNDA:9,multiply +FNDA:13,divide +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:15,1 +DA:16,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +LF:20 +LH:20 +BRF:2 +BRH:2 +BRDA:13,0,0,1 +BRDA:13,1,0,1 +end_of_record diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json new file mode 100644 index 000000000..5dae72f27 --- /dev/null +++ b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json @@ -0,0 +1,11 @@ +{ + "extends": "nx/presets/npm.json", + "targets": { + "test": { + "executor": "@nx/vite:test", + "options": { + "configFile": "vitest.config.ts" + } + } + } +} diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json new file mode 100644 index 000000000..44d3bbede --- /dev/null +++ b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-nx-workspace", + "version": "1.0.0", + "scripts": { + "build": "echo \"build\"" + }, + "devDependencies": { + "nx": "^17.0.0" + } +} diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts new file mode 100644 index 000000000..470d418e7 --- /dev/null +++ b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: 'coverage', + }, + }, +}); diff --git a/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap b/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap new file mode 100644 index 000000000..9d0c42166 --- /dev/null +++ b/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap @@ -0,0 +1,629 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PLUGIN collect report with coverage-paths functionality > should handle multiple coverage reports from different projects 1`] = ` +{ + "categories": [ + { + "refs": [ + { + "plugin": "coverage", + "slug": "coverage", + "type": "group", + "weight": 1, + }, + ], + "slug": "code-coverage", + "title": "Code coverage", + }, + ], + "packageName": "@code-pushup/core", + "plugins": [ + { + "audits": [ + { + "description": "Measures how many functions were called in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Function coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "function-coverage", + "title": "Function coverage", + "value": 100, + }, + { + "description": "Measures how many branches were executed after conditional statements in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Branch coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "branch-coverage", + "title": "Branch coverage", + "value": 100, + }, + { + "description": "Measures how many lines of code were executed in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Line coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "line-coverage", + "title": "Line coverage", + "value": 100, + }, + ], + "description": "Official Code PushUp code coverage plugin.", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/coverage-plugin/", + "groups": [ + { + "description": "Group containing all defined coverage types as audits.", + "refs": [ + { + "slug": "function-coverage", + "weight": 6, + }, + { + "slug": "branch-coverage", + "weight": 3, + }, + { + "slug": "line-coverage", + "weight": 1, + }, + ], + "slug": "coverage", + "title": "Code coverage metrics", + }, + ], + "icon": "folder-coverage-open", + "packageName": "@code-pushup/coverage-plugin", + "slug": "coverage", + "title": "Code coverage", + }, + ], +} +`; + +exports[`PLUGIN collect report with coverage-paths functionality > should run coverage plugin with Nx workspace and create report.json 1`] = ` +{ + "categories": [ + { + "refs": [ + { + "plugin": "coverage", + "slug": "coverage", + "type": "group", + "weight": 1, + }, + ], + "slug": "code-coverage", + "title": "Code coverage", + }, + ], + "packageName": "@code-pushup/core", + "plugins": [ + { + "audits": [ + { + "description": "Measures how many functions were called in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Function coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "function-coverage", + "title": "Function coverage", + "value": 100, + }, + { + "description": "Measures how many branches were executed after conditional statements in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Branch coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "branch-coverage", + "title": "Branch coverage", + "value": 100, + }, + { + "description": "Measures how many lines of code were executed in at least one test.", + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "nx-workspace", + "values": { + "coverage": 1, + }, + }, + ], + "name": "coverage-paths", + "values": { + "coverage": 1, + }, + }, + ], + "name": "__test__", + "values": { + "coverage": 1, + }, + }, + ], + "name": "plugin-coverage-e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "e2e", + "values": { + "coverage": 1, + }, + }, + ], + "name": "tmp", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Line coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "line-coverage", + "title": "Line coverage", + "value": 100, + }, + ], + "description": "Official Code PushUp code coverage plugin.", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/coverage-plugin/", + "groups": [ + { + "description": "Group containing all defined coverage types as audits.", + "refs": [ + { + "slug": "function-coverage", + "weight": 6, + }, + { + "slug": "branch-coverage", + "weight": 3, + }, + { + "slug": "line-coverage", + "weight": 1, + }, + ], + "slug": "coverage", + "title": "Code coverage metrics", + }, + ], + "icon": "folder-coverage-open", + "packageName": "@code-pushup/coverage-plugin", + "slug": "coverage", + "title": "Code coverage", + }, + ], +} +`; diff --git a/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts b/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts new file mode 100644 index 000000000..9446a17de --- /dev/null +++ b/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts @@ -0,0 +1,74 @@ +import { cp } from 'node:fs/promises'; +import path from 'node:path'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { type Report, reportSchema } from '@code-pushup/models'; +import { nxTargetProject } from '@code-pushup/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + omitVariableReportData, + teardownTestFolder, +} from '@code-pushup/test-utils'; +import { executeProcess, readJsonFile } from '@code-pushup/utils'; + +describe('PLUGIN collect report with coverage-paths functionality', () => { + const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); + const testFileDir = path.join(envRoot, TEST_OUTPUT_DIR, 'coverage-paths'); + + const nxWorkspaceDir = path.join(testFileDir, 'nx-workspace'); + const nxWorkspaceOutputDir = path.join(nxWorkspaceDir, '.code-pushup'); + + const fixtureDir = path.join( + 'e2e', + nxTargetProject(), + 'mocks', + 'fixtures', + 'nx-workspace', + ); + + beforeAll(async () => { + await cp(fixtureDir, nxWorkspaceDir, { recursive: true }); + }); + + afterAll(async () => { + await teardownTestFolder(nxWorkspaceDir); + }); + + afterEach(async () => { + await teardownTestFolder(nxWorkspaceOutputDir); + }); + + it('should run coverage plugin with Nx workspace and create report.json', async () => { + const { code } = await executeProcess({ + command: 'npx', + args: ['code-pushup', 'collect', '--no-progress'], + cwd: nxWorkspaceDir, + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(nxWorkspaceOutputDir, 'report.json'), + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + expect(omitVariableReportData(report)).toMatchSnapshot(); + }); + + it('should handle multiple coverage reports from different projects', async () => { + const { code } = await executeProcess({ + command: 'npx', + args: ['code-pushup', 'collect', '--no-progress'], + cwd: nxWorkspaceDir, + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(nxWorkspaceOutputDir, 'report.json'), + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + expect(omitVariableReportData(report)).toMatchSnapshot(); + }); +}); diff --git a/packages/plugin-coverage/mocks/duplicate-record-lcov.info b/packages/plugin-coverage/mocks/duplicate-record-lcov.info new file mode 100644 index 000000000..b7dc2ee45 --- /dev/null +++ b/packages/plugin-coverage/mocks/duplicate-record-lcov.info @@ -0,0 +1,33 @@ +TN: +SF:src\lib\utils.ts +FN:2,formatReportScore +FN:6,calcDuration +FNF:2 +FNH:1 +FNDA:1,formatReportScore +FNDA:6,calcDuration +DA:1,1 +DA:2,1 +DA:3,0 +DA:4,0 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +LF:10 +LH:8 +BRDA:1,0,0,6 +BRDA:1,1,0,5 +BRDA:2,4,0,1 +BRDA:4,5,0,17 +BRDA:5,6,0,4 +BRDA:6,7,0,13 +BRDA:6,10,0,1 +BRDA:7,11,0,3 +BRDA:10,12,0,12 +BRDA:10,13,0,1 +BRF:10 +BRH:9 +end_of_record diff --git a/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.int.test.ts.snap b/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.int.test.ts.snap index e062ef1c2..8447688b0 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.int.test.ts.snap +++ b/packages/plugin-coverage/src/lib/runner/lcov/__snapshots__/lcov-runner.int.test.ts.snap @@ -274,3 +274,191 @@ exports[`lcovResultsToAuditOutputs > should correctly merge all lines for covera }, ] `; + +exports[`lcovResultsToAuditOutputs > should correctly merge duplicate LCOV records from multiple files 1`] = ` +[ + { + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "lib", + "values": { + "coverage": 1, + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "cli", + "values": { + "coverage": 1, + }, + }, + ], + "name": "packages", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Branch coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "branch-coverage", + "value": 100, + }, + { + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "lib", + "values": { + "coverage": 1, + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "cli", + "values": { + "coverage": 1, + }, + }, + ], + "name": "packages", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Function coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "function-coverage", + "value": 100, + }, + { + "details": { + "trees": [ + { + "root": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "name": "utils.ts", + "values": { + "coverage": 1, + "missing": [], + }, + }, + ], + "name": "lib", + "values": { + "coverage": 1, + }, + }, + ], + "name": "src", + "values": { + "coverage": 1, + }, + }, + ], + "name": "cli", + "values": { + "coverage": 1, + }, + }, + ], + "name": "packages", + "values": { + "coverage": 1, + }, + }, + ], + "name": ".", + "values": { + "coverage": 1, + }, + }, + "title": "Line coverage", + "type": "coverage", + }, + ], + }, + "displayValue": "100 %", + "score": 1, + "slug": "line-coverage", + "value": 100, + }, +] +`; diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.int.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.int.test.ts index a151c3534..0478e1043 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.int.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.int.test.ts @@ -52,4 +52,37 @@ describe('lcovResultsToAuditOutputs', () => { ); expect(osAgnosticAuditOutputs(results)).toMatchSnapshot(); }); + + it('should correctly merge duplicate LCOV records from multiple files', async () => { + const results = await lcovResultsToAuditOutputs( + [ + { + resultsPath: path.join( + fileURLToPath(path.dirname(import.meta.url)), + '..', + '..', + '..', + '..', + 'mocks', + 'single-record-lcov.info', + ), + pathToProject: 'packages/cli', + }, + { + resultsPath: path.join( + fileURLToPath(path.dirname(import.meta.url)), + '..', + '..', + '..', + '..', + 'mocks', + 'duplicate-record-lcov.info', + ), + pathToProject: 'packages/cli', + }, + ], + ['branch', 'function', 'line'], + ); + expect(osAgnosticAuditOutputs(results)).toMatchSnapshot(); + }); }); diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts index 14da90d7e..3ead63bc0 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts @@ -1,8 +1,18 @@ import { vol } from 'memfs'; import path from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { ui } from '@code-pushup/utils'; -import { parseLcovFiles } from './lcov-runner.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getGitRoot, ui } from '@code-pushup/utils'; +import type { CoverageResult, CoverageType } from '../../config.js'; +import { lcovResultsToAuditOutputs, parseLcovFiles } from './lcov-runner.js'; + +// Mock getGitRoot +vi.mock('@code-pushup/utils', async () => { + const actual = await vi.importActual('@code-pushup/utils'); + return { + ...actual, + getGitRoot: vi.fn(), + }; +}); describe('parseLcovFiles', () => { const UTILS_REPORT = ` @@ -43,6 +53,29 @@ LH:3 BRF:0 BRH:0 end_of_record +`; + + const MULTI_FILE_REPORT = ` +TN: +SF:${path.join('file1', 'test.ts')} +FNF:1 +FNH:1 +DA:1,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:${path.join('file2', 'test.ts')} +FNF:0 +FNH:0 +DA:1,0 +LF:1 +LH:0 +BRF:1 +BRH:0 +end_of_record `; beforeEach(() => { @@ -51,6 +84,7 @@ end_of_record [path.join('integration-tests', 'lcov.info')]: UTILS_REPORT, // file name value under SF used in tests [path.join('unit-tests', 'lcov.info')]: CONSTANTS_REPORT, // file name value under SF used in tests [path.join('pytest', 'lcov.info')]: PYTEST_REPORT, + [path.join('multi', 'lcov.info')]: MULTI_FILE_REPORT, 'lcov.info': '', // empty report file }, 'coverage', @@ -106,13 +140,14 @@ end_of_record }); it('should warn about an empty lcov file', async () => { + const warningSpy = vi.spyOn(ui().logger, 'warning'); + await parseLcovFiles([ path.join('coverage', 'integration-tests', 'lcov.info'), path.join('coverage', 'lcov.info'), ]); - expect(ui()).toHaveLogged( - 'warn', + expect(warningSpy).toHaveBeenCalledWith( `Coverage plugin: Empty lcov report file detected at ${path.join( 'coverage', 'lcov.info', @@ -136,4 +171,235 @@ end_of_record }), ]); }); + + it('should handle invalid stats where hit > found', async () => { + const invalidReport = ` +TN: +SF:${path.join('invalid', 'file.ts')} +FNF:2 +FNH:3 +DA:1,1 +DA:2,1 +LF:2 +LH:3 +BRF:1 +BRH:2 +end_of_record +`; + + vol.fromJSON( + { + [path.join('invalid', 'lcov.info')]: invalidReport, + }, + 'coverage', + ); + + const result = await parseLcovFiles([ + path.join('coverage', 'invalid', 'lcov.info'), + ]); + + expect(result[0]?.functions.hit).toBe(2); + expect(result[0]?.lines.hit).toBe(2); + expect(result[0]?.branches.hit).toBe(1); + }); + + it('should handle multiple files with different coverage types', async () => { + const result = await parseLcovFiles([ + path.join('coverage', 'multi', 'lcov.info'), + ]); + + expect(result).toHaveLength(2); + expect(result[0]?.file).toBe(path.join('file1', 'test.ts')); + expect(result[1]?.file).toBe(path.join('file2', 'test.ts')); + expect(result[0]?.functions.hit).toBe(1); + expect(result[1]?.functions.hit).toBe(0); + }); + + it('should handle edge case with no branches or functions', async () => { + const edgeCaseReport = ` +TN: +SF:${path.join('edge', 'case.ts')} +FNF:0 +FNH:0 +DA:1,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +`; + + vol.fromJSON( + { + [path.join('edge', 'lcov.info')]: edgeCaseReport, + }, + 'coverage', + ); + + const result = await parseLcovFiles([ + path.join('coverage', 'edge', 'lcov.info'), + ]); + + expect(result[0]?.functions.hit).toBe(0); + expect(result[0]?.functions.found).toBe(0); + expect(result[0]?.branches.hit).toBe(0); + expect(result[0]?.branches.found).toBe(0); + expect(result[0]?.lines.hit).toBe(1); + expect(result[0]?.lines.found).toBe(1); + }); +}); + +describe('lcovResultsToAuditOutputs', () => { + const mockResults: CoverageResult[] = [ + { + resultsPath: path.join('coverage', 'test', 'lcov.info'), + pathToProject: 'packages/cli', + }, + ]; + + const mockCoverageTypes: CoverageType[] = ['function', 'branch', 'line']; + + beforeEach(() => { + vi.clearAllMocks(); + (getGitRoot as any).mockResolvedValue('/mock/git/root'); + + // Setup mock LCOV file for testing + const testReport = ` +TN: +SF:${path.join('src', 'test.ts')} +FNF:1 +FNH:1 +DA:1,1 +LF:1 +LH:1 +BRF:1 +BRH:1 +end_of_record +`; + + vol.fromJSON( + { + [path.join('test', 'lcov.info')]: testReport, + }, + 'coverage', + ); + }); + + it('should return audit outputs for all coverage types', async () => { + const result = await lcovResultsToAuditOutputs( + mockResults, + mockCoverageTypes, + ); + + expect(result).toHaveLength(3); + expect(result[0]).toHaveProperty('slug', 'function-coverage'); + expect(result[1]).toHaveProperty('slug', 'branch-coverage'); + expect(result[2]).toHaveProperty('slug', 'line-coverage'); + }); + + it('should handle single coverage type', async () => { + const result = await lcovResultsToAuditOutputs(mockResults, ['function']); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('slug', 'function-coverage'); + }); + + it('should handle empty coverage types array', async () => { + const result = await lcovResultsToAuditOutputs(mockResults, []); + + expect(result).toHaveLength(0); + }); + + it('should handle getGitRoot failure gracefully', async () => { + (getGitRoot as any).mockRejectedValue(new Error('Git root not found')); + + await expect( + lcovResultsToAuditOutputs(mockResults, mockCoverageTypes), + ).rejects.toThrow('Git root not found'); + }); + + it('should handle multiple results with different project paths', async () => { + const multiResults: CoverageResult[] = [ + { + resultsPath: path.join('coverage', 'test', 'lcov.info'), + pathToProject: 'packages/cli', + }, + { + resultsPath: path.join('coverage', 'test2', 'lcov.info'), + pathToProject: 'packages/utils', + }, + ]; + + const testReport2 = ` +TN: +SF:${path.join('src', 'utils.ts')} +FNF:2 +FNH:1 +DA:1,1 +DA:2,0 +LF:2 +LH:1 +BRF:1 +BRH:0 +end_of_record +`; + + vol.fromJSON( + { + [path.join('test2', 'lcov.info')]: testReport2, + }, + 'coverage', + ); + + const result = await lcovResultsToAuditOutputs(multiResults, ['function']); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('slug', 'function-coverage'); + }); + + it('should handle string results path', async () => { + const stringResults: CoverageResult[] = [ + path.join('coverage', 'test', 'lcov.info'), + ]; + + const result = await lcovResultsToAuditOutputs(stringResults, ['line']); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('slug', 'line-coverage'); + }); + + it('should handle mixed results format', async () => { + const mixedResults: CoverageResult[] = [ + path.join('coverage', 'test', 'lcov.info'), + { + resultsPath: path.join('coverage', 'test2', 'lcov.info'), + pathToProject: 'packages/utils', + }, + ]; + + const testReport2 = ` +TN: +SF:${path.join('src', 'utils.ts')} +FNF:1 +FNH:1 +DA:1,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +`; + + vol.fromJSON( + { + [path.join('test2', 'lcov.info')]: testReport2, + }, + 'coverage', + ); + + const result = await lcovResultsToAuditOutputs(mixedResults, ['function']); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('slug', 'function-coverage'); + }); }); diff --git a/packages/plugin-coverage/src/lib/runner/lcov/merge-lcov.unit.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/merge-lcov.unit.test.ts index 8ea4f4ee9..6bdf1fa01 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/merge-lcov.unit.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/merge-lcov.unit.test.ts @@ -90,6 +90,80 @@ describe('mergeLcovResults', () => { UNIQUE_REPORT, ]); }); + + it('should return records unchanged when no duplicates exist', () => { + const records = [ + { + title: '', + file: 'src/file1.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { found: 1, hit: 1, details: [{ line: 1, hit: 1 }] }, + functions: { found: 0, hit: 0, details: [] }, + }, + { + title: '', + file: 'src/file2.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { found: 1, hit: 0, details: [{ line: 1, hit: 0 }] }, + functions: { found: 0, hit: 0, details: [] }, + }, + ]; + + expect(mergeLcovResults(records)).toStrictEqual(records); + }); + + it('should handle empty records array', () => { + expect(mergeLcovResults([])).toStrictEqual([]); + }); + + it('should handle single record', () => { + const singleRecord = { + title: '', + file: 'src/single.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { found: 1, hit: 1, details: [{ line: 1, hit: 1 }] }, + functions: { found: 0, hit: 0, details: [] }, + }; + + expect(mergeLcovResults([singleRecord])).toStrictEqual([singleRecord]); + }); + + it('should handle duplicates with different line counts', () => { + const records = [ + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { + found: 2, + hit: 1, + details: [ + { line: 1, hit: 1 }, + { line: 2, hit: 0 }, + ], + }, + functions: { found: 0, hit: 0, details: [] }, + }, + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { + found: 2, + hit: 1, + details: [ + { line: 1, hit: 1 }, + { line: 2, hit: 0 }, + ], + }, + functions: { found: 0, hit: 0, details: [] }, + }, + ]; + + const result = mergeLcovResults(records); + expect(result).toHaveLength(1); + expect(result[0]?.file).toBe('src/file.ts'); + }); }); describe('mergeDuplicateLcovRecords', () => { @@ -176,6 +250,82 @@ describe('mergeDuplicateLcovRecords', () => { }, }); }); + + it('should handle records with zero hit counts', () => { + const records = [ + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { + found: 2, + hit: 0, + details: [ + { line: 1, hit: 0 }, + { line: 2, hit: 0 }, + ], + }, + functions: { + found: 1, + hit: 0, + details: [{ line: 1, name: 'func', hit: 0 }], + }, + }, + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { + found: 2, + hit: 0, + details: [ + { line: 1, hit: 0 }, + { line: 2, hit: 0 }, + ], + }, + functions: { + found: 1, + hit: 0, + details: [{ line: 1, name: 'func', hit: 0 }], + }, + }, + ]; + + const result = mergeDuplicateLcovRecords(records); + expect(result.lines.hit).toBe(0); + expect(result.functions.hit).toBe(0); + expect(result.branches.hit).toBe(0); + }); + + it('should handle records with null function hit values', () => { + const records = [ + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { found: 1, hit: 1, details: [{ line: 1, hit: 1 }] }, + functions: { + found: 1, + hit: 0, + details: [{ line: 1, name: 'func', hit: undefined }], + }, + }, + { + title: '', + file: 'src/file.ts', + branches: { found: 0, hit: 0, details: [] }, + lines: { found: 1, hit: 1, details: [{ line: 1, hit: 1 }] }, + functions: { + found: 1, + hit: 0, + details: [{ line: 1, name: 'func', hit: 2 }], + }, + }, + ]; + + const result = mergeDuplicateLcovRecords(records); + expect(result.functions.hit).toBe(1); // Only the second record has hit > 0 + }); }); describe('mergeLcovLineDetails', () => { @@ -218,6 +368,24 @@ describe('mergeLcovLineDetails', () => { { line: 3, hit: 0 }, ]); }); + + it('should handle empty details arrays', () => { + expect(mergeLcovLineDetails([])).toStrictEqual([]); + expect(mergeLcovLineDetails([[]])).toStrictEqual([]); + expect(mergeLcovLineDetails([[], []])).toStrictEqual([]); + }); + + it('should handle single line details', () => { + expect( + mergeLcovLineDetails([[{ line: 1, hit: 5 }], [{ line: 1, hit: 3 }]]), + ).toStrictEqual([{ line: 1, hit: 8 }]); + }); + + it('should handle negative hit values', () => { + expect( + mergeLcovLineDetails([[{ line: 1, hit: -1 }], [{ line: 1, hit: 2 }]]), + ).toStrictEqual([{ line: 1, hit: 1 }]); + }); }); describe('mergeLcovBranchDetails', () => { @@ -257,6 +425,45 @@ describe('mergeLcovBranchDetails', () => { { line: 3, block: 0, branch: 0, taken: 0 }, ]); }); + + it('should handle empty details arrays', () => { + expect(mergeLcovBranchesDetails([])).toStrictEqual([]); + expect(mergeLcovBranchesDetails([[]])).toStrictEqual([]); + expect(mergeLcovBranchesDetails([[], []])).toStrictEqual([]); + }); + + it('should handle complex branch structures', () => { + expect( + mergeLcovBranchesDetails([ + [ + { line: 1, block: 1, branch: 0, taken: 1 }, + { line: 1, block: 1, branch: 1, taken: 0 }, + { line: 2, block: 2, branch: 0, taken: 1 }, + ], + [ + { line: 1, block: 1, branch: 0, taken: 1 }, + { line: 1, block: 1, branch: 1, taken: 1 }, + { line: 3, block: 3, branch: 0, taken: 0 }, + ], + ]), + ).toStrictEqual([ + { line: 1, block: 1, branch: 0, taken: 2 }, + { line: 1, block: 1, branch: 1, taken: 1 }, + { line: 2, block: 2, branch: 0, taken: 1 }, + { line: 3, block: 3, branch: 0, taken: 0 }, + ]); + }); + + it('should handle negative taken values', () => { + expect( + mergeLcovBranchesDetails([ + [{ line: 1, block: 0, branch: 0, taken: -1 }], + [{ line: 1, block: 0, branch: 0, taken: 2 }], + ]), + ).toStrictEqual([ + { line: 1, block: 0, branch: 0, taken: 1 }, + ]); + }); }); describe('mergeLcovFunctionsDetails', () => { @@ -296,4 +503,55 @@ describe('mergeLcovFunctionsDetails', () => { { line: 7, name: 'div', hit: 0 }, ]); }); + + it('should handle empty details arrays', () => { + expect(mergeLcovFunctionsDetails([])).toStrictEqual([]); + expect(mergeLcovFunctionsDetails([[]])).toStrictEqual([]); + expect(mergeLcovFunctionsDetails([[], []])).toStrictEqual([]); + }); + + it('should handle undefined hit values', () => { + expect( + mergeLcovFunctionsDetails([ + [{ line: 1, name: 'func', hit: undefined }], + [{ line: 1, name: 'func', hit: 2 }], + ]), + ).toStrictEqual([{ line: 1, name: 'func', hit: 2 }]); + }); + + it('should handle multiple undefined hit values', () => { + expect( + mergeLcovFunctionsDetails([ + [{ line: 1, name: 'func', hit: undefined }], + [{ line: 1, name: 'func', hit: undefined }], + ]), + ).toStrictEqual([{ line: 1, name: 'func', hit: 0 }]); + }); + + it('should handle functions with same name but different lines', () => { + expect( + mergeLcovFunctionsDetails([ + [ + { line: 1, name: 'func', hit: 1 }, + { line: 5, name: 'func', hit: 2 }, + ], + [ + { line: 1, name: 'func', hit: 3 }, + { line: 5, name: 'func', hit: 1 }, + ], + ]), + ).toStrictEqual([ + { line: 1, name: 'func', hit: 4 }, + { line: 5, name: 'func', hit: 3 }, + ]); + }); + + it('should handle negative hit values', () => { + expect( + mergeLcovFunctionsDetails([ + [{ line: 1, name: 'func', hit: -1 }], + [{ line: 1, name: 'func', hit: 3 }], + ]), + ).toStrictEqual([{ line: 1, name: 'func', hit: 2 }]); + }); }); diff --git a/packages/plugin-coverage/vitest.int.config.ts b/packages/plugin-coverage/vitest.int.config.ts index a105197ee..1386d8f90 100644 --- a/packages/plugin-coverage/vitest.int.config.ts +++ b/packages/plugin-coverage/vitest.int.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ coverage: { reporter: ['text', 'lcov'], reportsDirectory: '../../coverage/plugin-coverage/int-tests', - exclude: ['mocks/**', '**/types.ts'], + exclude: ['mocks/**', '**/types.ts', '**/vitest.*.config.ts'], }, environment: 'node', include: ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], diff --git a/packages/plugin-coverage/vitest.unit.config.ts b/packages/plugin-coverage/vitest.unit.config.ts index e86d3f2e5..e0bd6113e 100644 --- a/packages/plugin-coverage/vitest.unit.config.ts +++ b/packages/plugin-coverage/vitest.unit.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ coverage: { reporter: ['text', 'lcov'], reportsDirectory: '../../coverage/plugin-coverage/unit-tests', - exclude: ['mocks/**', '**/types.ts'], + exclude: ['mocks/**', '**/types.ts', '**/vitest.*.config.ts'], }, environment: 'node', include: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], From c65bca0a64417790277b96475e13f6cdc899d4e1 Mon Sep 17 00:00:00 2001 From: AS Date: Fri, 15 Aug 2025 11:40:57 +0300 Subject: [PATCH 2/3] test: remove e2e and fix mr comment --- .../nx-workspace/code-pushup.config.ts | 23 - .../fixtures/nx-workspace/coverage/lcov.info | 39 -- .../mocks/fixtures/nx-workspace/nx.json | 11 - .../mocks/fixtures/nx-workspace/package.json | 10 - .../fixtures/nx-workspace/vitest.config.ts | 10 - .../coverage-paths.e2e.test.ts.snap | 629 ------------------ .../tests/coverage-paths.e2e.test.ts | 74 --- 7 files changed, 796 deletions(-) delete mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts delete mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info delete mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json delete mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json delete mode 100644 e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts delete mode 100644 e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap delete mode 100644 e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts deleted file mode 100644 index 8567072cf..000000000 --- a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/code-pushup.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import coveragePlugin from '@code-pushup/coverage-plugin'; - -export default { - plugins: [ - await coveragePlugin({ - reports: ['coverage/lcov.info'], - }), - ], - categories: [ - { - slug: 'code-coverage', - title: 'Code coverage', - refs: [ - { - type: 'group', - plugin: 'coverage', - slug: 'coverage', - weight: 1, - }, - ], - }, - ], -}; diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info deleted file mode 100644 index 56f86e380..000000000 --- a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/coverage/lcov.info +++ /dev/null @@ -1,39 +0,0 @@ -TN: -SF:src/utils.ts -FN:1,add -FN:5,subtract -FN:9,multiply -FN:13,divide -FNF:4 -FNH:4 -FNDA:1,add -FNDA:5,subtract -FNDA:9,multiply -FNDA:13,divide -DA:1,1 -DA:2,1 -DA:3,1 -DA:4,1 -DA:5,1 -DA:6,1 -DA:7,1 -DA:8,1 -DA:9,1 -DA:10,1 -DA:11,1 -DA:12,1 -DA:13,1 -DA:14,1 -DA:15,1 -DA:16,1 -DA:17,1 -DA:18,1 -DA:19,1 -DA:20,1 -LF:20 -LH:20 -BRF:2 -BRH:2 -BRDA:13,0,0,1 -BRDA:13,1,0,1 -end_of_record diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json deleted file mode 100644 index 5dae72f27..000000000 --- a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/nx.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "nx/presets/npm.json", - "targets": { - "test": { - "executor": "@nx/vite:test", - "options": { - "configFile": "vitest.config.ts" - } - } - } -} diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json deleted file mode 100644 index 44d3bbede..000000000 --- a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "test-nx-workspace", - "version": "1.0.0", - "scripts": { - "build": "echo \"build\"" - }, - "devDependencies": { - "nx": "^17.0.0" - } -} diff --git a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts b/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts deleted file mode 100644 index 470d418e7..000000000 --- a/e2e/plugin-coverage-e2e/mocks/fixtures/nx-workspace/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - coverage: { - reporter: ['text', 'lcov'], - reportsDirectory: 'coverage', - }, - }, -}); diff --git a/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap b/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap deleted file mode 100644 index 9d0c42166..000000000 --- a/e2e/plugin-coverage-e2e/tests/__snapshots__/coverage-paths.e2e.test.ts.snap +++ /dev/null @@ -1,629 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`PLUGIN collect report with coverage-paths functionality > should handle multiple coverage reports from different projects 1`] = ` -{ - "categories": [ - { - "refs": [ - { - "plugin": "coverage", - "slug": "coverage", - "type": "group", - "weight": 1, - }, - ], - "slug": "code-coverage", - "title": "Code coverage", - }, - ], - "packageName": "@code-pushup/core", - "plugins": [ - { - "audits": [ - { - "description": "Measures how many functions were called in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Function coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "function-coverage", - "title": "Function coverage", - "value": 100, - }, - { - "description": "Measures how many branches were executed after conditional statements in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Branch coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "branch-coverage", - "title": "Branch coverage", - "value": 100, - }, - { - "description": "Measures how many lines of code were executed in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Line coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "line-coverage", - "title": "Line coverage", - "value": 100, - }, - ], - "description": "Official Code PushUp code coverage plugin.", - "docsUrl": "https://www.npmjs.com/package/@code-pushup/coverage-plugin/", - "groups": [ - { - "description": "Group containing all defined coverage types as audits.", - "refs": [ - { - "slug": "function-coverage", - "weight": 6, - }, - { - "slug": "branch-coverage", - "weight": 3, - }, - { - "slug": "line-coverage", - "weight": 1, - }, - ], - "slug": "coverage", - "title": "Code coverage metrics", - }, - ], - "icon": "folder-coverage-open", - "packageName": "@code-pushup/coverage-plugin", - "slug": "coverage", - "title": "Code coverage", - }, - ], -} -`; - -exports[`PLUGIN collect report with coverage-paths functionality > should run coverage plugin with Nx workspace and create report.json 1`] = ` -{ - "categories": [ - { - "refs": [ - { - "plugin": "coverage", - "slug": "coverage", - "type": "group", - "weight": 1, - }, - ], - "slug": "code-coverage", - "title": "Code coverage", - }, - ], - "packageName": "@code-pushup/core", - "plugins": [ - { - "audits": [ - { - "description": "Measures how many functions were called in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Function coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "function-coverage", - "title": "Function coverage", - "value": 100, - }, - { - "description": "Measures how many branches were executed after conditional statements in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Branch coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "branch-coverage", - "title": "Branch coverage", - "value": 100, - }, - { - "description": "Measures how many lines of code were executed in at least one test.", - "details": { - "trees": [ - { - "root": { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "name": "utils.ts", - "values": { - "coverage": 1, - "missing": [], - }, - }, - ], - "name": "src", - "values": { - "coverage": 1, - }, - }, - ], - "name": "nx-workspace", - "values": { - "coverage": 1, - }, - }, - ], - "name": "coverage-paths", - "values": { - "coverage": 1, - }, - }, - ], - "name": "__test__", - "values": { - "coverage": 1, - }, - }, - ], - "name": "plugin-coverage-e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "e2e", - "values": { - "coverage": 1, - }, - }, - ], - "name": "tmp", - "values": { - "coverage": 1, - }, - }, - ], - "name": ".", - "values": { - "coverage": 1, - }, - }, - "title": "Line coverage", - "type": "coverage", - }, - ], - }, - "displayValue": "100 %", - "score": 1, - "slug": "line-coverage", - "title": "Line coverage", - "value": 100, - }, - ], - "description": "Official Code PushUp code coverage plugin.", - "docsUrl": "https://www.npmjs.com/package/@code-pushup/coverage-plugin/", - "groups": [ - { - "description": "Group containing all defined coverage types as audits.", - "refs": [ - { - "slug": "function-coverage", - "weight": 6, - }, - { - "slug": "branch-coverage", - "weight": 3, - }, - { - "slug": "line-coverage", - "weight": 1, - }, - ], - "slug": "coverage", - "title": "Code coverage metrics", - }, - ], - "icon": "folder-coverage-open", - "packageName": "@code-pushup/coverage-plugin", - "slug": "coverage", - "title": "Code coverage", - }, - ], -} -`; diff --git a/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts b/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts deleted file mode 100644 index 9446a17de..000000000 --- a/e2e/plugin-coverage-e2e/tests/coverage-paths.e2e.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { cp } from 'node:fs/promises'; -import path from 'node:path'; -import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; -import { type Report, reportSchema } from '@code-pushup/models'; -import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { - E2E_ENVIRONMENTS_DIR, - TEST_OUTPUT_DIR, - omitVariableReportData, - teardownTestFolder, -} from '@code-pushup/test-utils'; -import { executeProcess, readJsonFile } from '@code-pushup/utils'; - -describe('PLUGIN collect report with coverage-paths functionality', () => { - const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); - const testFileDir = path.join(envRoot, TEST_OUTPUT_DIR, 'coverage-paths'); - - const nxWorkspaceDir = path.join(testFileDir, 'nx-workspace'); - const nxWorkspaceOutputDir = path.join(nxWorkspaceDir, '.code-pushup'); - - const fixtureDir = path.join( - 'e2e', - nxTargetProject(), - 'mocks', - 'fixtures', - 'nx-workspace', - ); - - beforeAll(async () => { - await cp(fixtureDir, nxWorkspaceDir, { recursive: true }); - }); - - afterAll(async () => { - await teardownTestFolder(nxWorkspaceDir); - }); - - afterEach(async () => { - await teardownTestFolder(nxWorkspaceOutputDir); - }); - - it('should run coverage plugin with Nx workspace and create report.json', async () => { - const { code } = await executeProcess({ - command: 'npx', - args: ['code-pushup', 'collect', '--no-progress'], - cwd: nxWorkspaceDir, - }); - - expect(code).toBe(0); - - const report = await readJsonFile( - path.join(nxWorkspaceOutputDir, 'report.json'), - ); - - expect(() => reportSchema.parse(report)).not.toThrow(); - expect(omitVariableReportData(report)).toMatchSnapshot(); - }); - - it('should handle multiple coverage reports from different projects', async () => { - const { code } = await executeProcess({ - command: 'npx', - args: ['code-pushup', 'collect', '--no-progress'], - cwd: nxWorkspaceDir, - }); - - expect(code).toBe(0); - - const report = await readJsonFile( - path.join(nxWorkspaceOutputDir, 'report.json'), - ); - - expect(() => reportSchema.parse(report)).not.toThrow(); - expect(omitVariableReportData(report)).toMatchSnapshot(); - }); -}); From b29bb399f520980593e23a73c1e1cd22ccbd2c6c Mon Sep 17 00:00:00 2001 From: AS Date: Fri, 22 Aug 2025 11:43:00 +0300 Subject: [PATCH 3/3] test: fix mr comments --- .../src/lib/runner/lcov/lcov-runner.unit.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts index 3ead63bc0..b943b04f0 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts @@ -5,7 +5,6 @@ import { getGitRoot, ui } from '@code-pushup/utils'; import type { CoverageResult, CoverageType } from '../../config.js'; import { lcovResultsToAuditOutputs, parseLcovFiles } from './lcov-runner.js'; -// Mock getGitRoot vi.mock('@code-pushup/utils', async () => { const actual = await vi.importActual('@code-pushup/utils'); return { @@ -140,14 +139,13 @@ end_of_record }); it('should warn about an empty lcov file', async () => { - const warningSpy = vi.spyOn(ui().logger, 'warning'); - await parseLcovFiles([ path.join('coverage', 'integration-tests', 'lcov.info'), path.join('coverage', 'lcov.info'), ]); - expect(warningSpy).toHaveBeenCalledWith( + expect(ui()).toHaveLogged( + 'warn', `Coverage plugin: Empty lcov report file detected at ${path.join( 'coverage', 'lcov.info', @@ -172,7 +170,7 @@ end_of_record ]); }); - it('should handle invalid stats where hit > found', async () => { + it('should sanitize hit values to not exceed found values when invalid stats are encountered', async () => { const invalidReport = ` TN: SF:${path.join('invalid', 'file.ts')} @@ -261,9 +259,8 @@ describe('lcovResultsToAuditOutputs', () => { beforeEach(() => { vi.clearAllMocks(); - (getGitRoot as any).mockResolvedValue('/mock/git/root'); + vi.mocked(getGitRoot).mockResolvedValue('/mock/git/root'); - // Setup mock LCOV file for testing const testReport = ` TN: SF:${path.join('src', 'test.ts')} @@ -311,7 +308,7 @@ end_of_record }); it('should handle getGitRoot failure gracefully', async () => { - (getGitRoot as any).mockRejectedValue(new Error('Git root not found')); + vi.mocked(getGitRoot).mockRejectedValue(new Error('Git root not found')); await expect( lcovResultsToAuditOutputs(mockResults, mockCoverageTypes),