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 631d839c9..a9dd4e5e4 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts @@ -77,13 +77,18 @@ export async function parseLcovFiles( ); } const parsedRecords = parseLcov(toUnixNewlines(lcovFileContent)); - return parsedRecords.map(record => ({ - ...record, - file: - typeof result === 'string' || result.pathToProject == null - ? record.file - : path.join(result.pathToProject, record.file), - })); + return parsedRecords.map( + (record): LCOVRecord => ({ + title: record.title, + file: + typeof result === 'string' || result.pathToProject == null + ? record.file + : path.join(result.pathToProject, record.file), + functions: filterOutInvalidLines(record, 'functions'), + branches: filterOutInvalidLines(record, 'branches'), + lines: filterOutInvalidLines(record, 'lines'), + }), + ); }), ) ).flat(); @@ -95,6 +100,26 @@ export async function parseLcovFiles( return parsedResults; } +/** + * Filters out invalid line numbers. + * + * Some tools like pytest-cov emit line number 0. https://github.com/nedbat/coveragepy/issues/1846 + * + * @param record LCOV record + * @param type Coverage type + * @returns Coverage output from record without invalid line numbers + */ +function filterOutInvalidLines( + record: LCOVRecord, + type: T, +): LCOVRecord[T] { + const stats = record[type]; + return { + ...stats, + details: stats.details.filter(detail => detail.line > 0), + }; +} + /** * This function aggregates coverage stats from all coverage files * @param records LCOV record for each file 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 e5d8f2108..bde457156 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 @@ -31,6 +31,18 @@ LH:1 BRF:0 BRH:0 end_of_record +`; + + const PYTEST_REPORT = ` +TN: +SF:kw/__init__.py +DA:1,1,gG9L/J2A/IwO9tZM1raZxQ +DA:0,0,gG9L/J2A/IwO9tZM1raZxQ +LF:2 +LH:1 +BRF:0 +BRH:0 +end_of_record `; beforeEach(() => { @@ -38,6 +50,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, 'lcov.info': '', // empty report file }, 'coverage', @@ -106,4 +119,19 @@ end_of_record )}.`, ); }); + + it('should skip lines numbered 0', async () => { + await expect( + parseLcovFiles([path.join('coverage', 'pytest', 'lcov.info')]), + ).resolves.toEqual([ + expect.objectContaining({ + lines: expect.objectContaining({ + details: [ + { hit: 1, line: 1 }, + // no { hit: 0, line: 0 }, + ], + }), + }), + ]); + }); });