Skip to content

Commit

Permalink
Add support for LCOV report format
Browse files Browse the repository at this point in the history
  • Loading branch information
GaelGirodon committed May 28, 2024
1 parent 0a7a5de commit 0da419d
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 4 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
![tests](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Fci-badges-action-junit-tests.json)
![coverage](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Fci-badges-action-cobertura-coverage.json)

This action generates badges (as JSON files) from Go, JUnit, Cobertura and
JaCoCo test and coverage reports (most test runners and code coverage tools,
This action generates badges (as JSON files) from Go, JUnit, Cobertura, JaCoCo
and LCOV test and coverage reports (most test runners and code coverage tools,
including Mocha, Jest, PHPUnit, c8, Istanbul/nyc, and more, support at least
one of these formats) and upload them to a Gist to make them available to
Shields through the endpoint feature with (almost) zero configuration.
Expand Down Expand Up @@ -69,6 +69,7 @@ from test report(s).
![coverage](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-go-coverage.json)
![coverage](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-cobertura-coverage.json)
![coverage](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-jacoco-coverage.json)
![coverage](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-lcov-coverage.json)

This badge displays the percentage of covered lines extracted from a coverage
report.
Expand Down Expand Up @@ -157,6 +158,24 @@ from the first matching and valid report file.

➡️ `{repo}-[{ref}-]jacoco-coverage.json`

### LCOV

Write the coverage report to a file matching:

- `**/lcov.*`
- `**/*.lcov`

This is the default format and location with LCOV, but some code coverage
tools support this format too, natively or using an additional reporter:

- **c8**: `c8 --reporter lcov [...]``coverage/lcov.info`
- **Deno**: `deno test --coverage=cov_profile && deno coverage cov_profile --lcov --output=cov_profile.lcov`

The coverage will be computed using `LF` and `LH` keys, from the first
matching and valid report file.

➡️ `{repo}-[{ref}-]lcov-coverage.json`

## Notes

Storing badge JSON files on a Gist may seem tedious, but:
Expand Down
3 changes: 2 additions & 1 deletion src/reports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as go from './go.js';
import * as junit from './junit.js';
import * as cobertura from './cobertura.js';
import * as jacoco from './jacoco.js';
import * as lcov from './lcov.js';

/**
* Available report loaders
* @type {{ [key: string]: { getReports: ReportsLoader } }}
*/
const loaders = { go, junit, cobertura, jacoco };
const loaders = { go, junit, cobertura, jacoco, lcov };

/**
* Load all available reports in the current workspace.
Expand Down
64 changes: 64 additions & 0 deletions src/reports/lcov.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as core from '@actions/core';
import { promises as fs } from 'fs';
import { join } from 'path';
import { globNearest } from '../util/index.js';

/**
* Load coverage reports using LCOV format.
* @param {string} root Root search directory
* @returns LCOV coverage report
*/
export async function getReports(root) {
core.info('Load LCOV coverage report');
const patterns = [
join(root, '**/lcov.*'),
join(root, '**/*.lcov')
];
const files = await globNearest(patterns);
/** @type {Omit<CoverageReport, 'format'>[]} */
const reports = [];
for (const f of files) {
core.info(`Load LCOV report '${f}'`);
const coverage = await getCoverage(f);
if (coverage < 0) {
core.info('Report is not a valid LCOV report');
continue; // Invalid report file, trying the next one
}
reports.push({ type: 'coverage', data: { coverage } });
break; // Successfully loaded a report file, can return now
}
core.info(`Loaded ${reports.length} LCOV report(s)`);
return reports;
}

/**
* Compute the total line coverage rate from the given LCOV report file,
* i.e., the ratio of all hit (LH) to found (LF) lines, deduplicated by source
* file (SF) (only the best coverage for each source file is considered).
* @param {string} path Path to the LCOV coverage report file
* @returns {Promise<number>} The total line coverage rate (%),
* or -1 if no coverage data can be read from this file
*/
async function getCoverage(path) {
const contents = await fs.readFile(path, { encoding: 'utf8' });
/** @type {{ [sf: string]: { lh: number, lf: number } }} */
const sourceFiles = {};
let from = 0, to = 0;
while ((to = contents.indexOf('end_of_record', from)) > 0) {
const record = contents.slice(from, to);
const sf = record.match(/^SF:(.+)$/m)?.[1] ?? '';
const lh = parseInt(record.match(/^LH:([0-9]+)$/m)?.[1] ?? '0');
const lf = parseInt(record.match(/^LF:([0-9]+)$/m)?.[1] ?? '0');
const value = sourceFiles[sf];
if (sf && (!value || (lf >= value.lf && lh / lf > value.lh / value.lf))) {
sourceFiles[sf] = { lh, lf };
}
from = to + 13;
}
const { lh, lf } = Object.values(sourceFiles)
.reduce((sum, val) => ({
lh: sum.lh + val.lh,
lf: sum.lf + val.lf
}), { lh: 0, lf: 0 });
return lf > 0 ? (lh / lf) * 100 : -1;
}
15 changes: 15 additions & 0 deletions src/reports/lcov.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from 'assert/strict';
import { join } from 'path';
import { getReports } from './lcov.js';

describe('reports/lcov', () => {
describe('#getReports()', () => {
it('should return a coverage report', async () => {
const reports = await getReports(join(process.cwd(), 'test/data/lcov'));
assert.equal(reports.length, 1);
assert.deepEqual(reports, [
{ type: 'coverage', data: { coverage: 47 } }
]);
});
});
});
22 changes: 22 additions & 0 deletions test/data/lcov/lcov.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
TN:
SF:a.js
LH:20
LF:40
end_of_record
SF:b.js
LH:30
LF:70
end_of_record
SF:c.js
LH:30
LF:90
end_of_record
TN:
SF:a.js
LH:34
LF:40
end_of_record
SF:b.js
LH:20
LF:70
end_of_record
3 changes: 2 additions & 1 deletion test/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ describe('CI Badges action', function () {
'repo-go-coverage.json',
'repo-go-tests.json',
'repo-jacoco-coverage.json',
'repo-junit-tests.json'
'repo-junit-tests.json',
'repo-lcov-coverage.json'
].forEach(f => assert.ok(files.includes(f)));
});
});
Expand Down

0 comments on commit 0da419d

Please sign in to comment.