feat(lcov) use really lcov.info file

This commit is contained in:
Julien Catania 2024-01-12 16:18:04 +01:00
parent 712fabc3c8
commit f19a7213c3
5 changed files with 261 additions and 138 deletions

View file

@ -2,136 +2,58 @@
exports[`lcov report coverage report from facebook/jest test results matches snapshot 1`] = `
TestRunResult {
"path": "fixtures/lcov.json",
"path": "fixtures/lcov.info",
"suites": Array [
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": "src/core/dao.service.ts",
"name": "src/services/notifier/NotifierService.js",
"tests": Array [
Object {
"name": "statement",
"name": "lines 100% (21/21)",
"result": "success",
"time": 0,
},
Object {
"name": "fonction",
"name": "functions 100% (10/10)",
"result": "success",
"time": 0,
},
Object {
"name": "branche",
"result": "success",
"name": "branches 50% (3/6)",
"result": "failed",
"time": 0,
},
],
},
],
"name": "src/core/dao.service.ts",
"name": "src/services/notifier/NotifierService.js",
"totalTime": undefined,
},
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": "src/domains/auth/auth.controller.ts",
"name": "src/services/notifier/providers/DiscordNotifierProvider.js",
"tests": Array [
Object {
"name": "statement",
"name": "lines 100% (17/17)",
"result": "success",
"time": 0,
},
Object {
"name": "fonction",
"result": "failed",
"time": 0,
},
Object {
"name": "branche",
"name": "functions 100% (3/3)",
"result": "success",
"time": 0,
},
],
},
],
"name": "src/domains/auth/auth.controller.ts",
"totalTime": undefined,
},
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": "src/shared/notif/providers/fcm/fcm.service.spec.ts",
"tests": Array [
Object {
"name": "statement",
"result": "failed",
"time": 0,
},
Object {
"name": "fonction",
"result": "failed",
"time": 0,
},
Object {
"name": "branche",
"name": "branches 75% (3/4)",
"result": "failed",
"time": 0,
},
],
},
],
"name": "src/shared/notif/providers/fcm/fcm.service.spec.ts",
"totalTime": undefined,
},
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": "src/shared/notif/providers/fcm/fcm.service.ts",
"tests": Array [
Object {
"name": "statement",
"result": "failed",
"time": 0,
},
Object {
"name": "fonction",
"result": "failed",
"time": 0,
},
Object {
"name": "branche",
"result": "success",
"time": 0,
},
],
},
],
"name": "src/shared/notif/providers/fcm/fcm.service.ts",
"totalTime": undefined,
},
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": "src/shared/notif/providers/mail/mail-service.ts",
"tests": Array [
Object {
"name": "statement",
"result": "failed",
"time": 0,
},
Object {
"name": "fonction",
"result": "failed",
"time": 0,
},
Object {
"name": "branche",
"result": "success",
"time": 0,
},
],
},
],
"name": "src/shared/notif/providers/mail/mail-service.ts",
"name": "src/services/notifier/providers/DiscordNotifierProvider.js",
"totalTime": undefined,
},
],

View file

@ -3,17 +3,15 @@ import * as path from 'path'
import {getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/path-utils'
import { LcovParser } from "../src/parsers/lcov/lcov-parser";
import {LcovParser} from '../src/parsers/lcov/lcov-parser'
describe('lcov report coverage', () => {
it('report from facebook/jest test results matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'lcov.info')
const outputPath = path.join(__dirname, '__outputs__', 'lcov-report-results.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new LcovParser({parseErrors: true, trackedFiles: []})
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()

247
dist/index.js generated vendored
View file

@ -265,10 +265,10 @@ const dotnet_trx_parser_1 = __nccwpck_require__(2664);
const java_junit_parser_1 = __nccwpck_require__(676);
const jest_junit_parser_1 = __nccwpck_require__(1113);
const mocha_json_parser_1 = __nccwpck_require__(6043);
const swift_xunit_parser_1 = __nccwpck_require__(5366);
const path_utils_1 = __nccwpck_require__(4070);
const github_utils_1 = __nccwpck_require__(3522);
const markdown_utils_1 = __nccwpck_require__(6482);
const lcov_parser_1 = __nccwpck_require__(5804);
const lcov_parser_1 = __nccwpck_require__(5698);
function main() {
return __awaiter(this, void 0, void 0, function* () {
try {
@ -294,6 +294,7 @@ class TestReporter {
this.listTests = core.getInput('list-tests', { required: true });
this.maxAnnotations = parseInt(core.getInput('max-annotations', { required: true }));
this.failOnError = core.getInput('fail-on-error', { required: true }) === 'true';
this.failOnEmpty = core.getInput('fail-on-empty', { required: true }) === 'true';
this.workDirInput = core.getInput('working-directory', { required: false });
this.onlySummary = core.getInput('only-summary', { required: false }) === 'true';
this.token = core.getInput('token', { required: true });
@ -365,7 +366,7 @@ class TestReporter {
core.setFailed(`Failed test were found and 'fail-on-error' option is set to ${this.failOnError}`);
return;
}
if (results.length === 0) {
if (results.length === 0 && this.failOnEmpty) {
core.setFailed(`No test report files were found`);
return;
}
@ -402,16 +403,21 @@ class TestReporter {
const annotations = (0, get_annotations_1.getAnnotations)(results, this.maxAnnotations);
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed');
const conclusion = isFailed ? 'failure' : 'success';
const icon = isFailed ? markdown_utils_1.Icon.fail : markdown_utils_1.Icon.success;
const passed = results.reduce((sum, tr) => sum + tr.passed, 0);
const failed = results.reduce((sum, tr) => sum + tr.failed, 0);
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0);
const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `;
core.info(`Updating check run conclusion (${conclusion}) and output`);
const resp = yield this.octokit.rest.checks.update(Object.assign({ check_run_id: createResp.data.id, conclusion, status: 'completed', output: {
title: `${name} ${icon}`,
title: shortSummary,
summary,
annotations
} }, github.context.repo));
core.info(`Check run create response: ${resp.status}`);
core.info(`Check run URL: ${resp.data.url}`);
core.info(`Check run HTML: ${resp.data.html_url}`);
core.setOutput('url', resp.data.url);
core.setOutput('url_html', resp.data.html_url);
return results;
});
}
@ -429,6 +435,8 @@ class TestReporter {
return new jest_junit_parser_1.JestJunitParser(options);
case 'mocha-json':
return new mocha_json_parser_1.MochaJsonParser(options);
case 'swift-xunit':
return new swift_xunit_parser_1.SwiftXunitParser(options);
case 'lcov':
return new lcov_parser_1.LcovParser(options);
default:
@ -1209,7 +1217,7 @@ class JestJunitParser {
const suites = junit.testsuites.testsuite === undefined
? []
: junit.testsuites.testsuite.map(ts => {
const name = ts.$.name.trim();
const name = this.escapeCharacters(ts.$.name.trim());
const time = parseFloat(ts.$.time) * 1000;
const sr = new test_results_1.TestSuiteResult(name, this.getGroups(ts), time);
return sr;
@ -1278,13 +1286,16 @@ class JestJunitParser {
var _a, _b;
return ((_b = (_a = this.options.workDir) !== null && _a !== void 0 ? _a : this.assumedWorkDir) !== null && _b !== void 0 ? _b : (this.assumedWorkDir = (0, path_utils_1.getBasePath)(path, this.options.trackedFiles)));
}
escapeCharacters(s) {
return s.replace(/([<>])/g, '\\$1');
}
}
exports.JestJunitParser = JestJunitParser;
/***/ }),
/***/ 5804:
/***/ 5698:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
@ -1301,19 +1312,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.LcovParser = void 0;
const test_results_1 = __nccwpck_require__(2768);
const lcov_utils_1 = __nccwpck_require__(4750);
class LcovParser {
constructor(options) {
this.options = options;
}
parse(path, content) {
return __awaiter(this, void 0, void 0, function* () {
const report = this.parseFile(path, content);
const report = yield this.parseFile(path, content);
return this.getTestRunResult(path, report);
});
}
parseFile(path, content) {
try {
return JSON.parse(content);
return (0, lcov_utils_1.parseProm)(content);
//return JSON.parse(content) as LcovReport
}
catch (e) {
throw new Error(`Invalid JSON at ${path}\n\n${e}`);
@ -1322,47 +1335,67 @@ class LcovParser {
getTestRunResult(path, report) {
return __awaiter(this, void 0, void 0, function* () {
const suites = [];
for (const key of Object.keys(report)) {
const s = this.getParsedStat(report[key].s);
const f = this.getParsedStat(report[key].f);
const b = this.getParsedStat(report[key].b);
for (let reportElement of report) {
const fileName = reportElement.file;
const statementCaseResult = {
name: 'statement',
name: `lines ${this.getPartInfo(reportElement.lines)}`,
time: 0,
result: s.percentage >= 80 ? 'success' : 'failed'
result: this.getPercentage(reportElement.lines) >= 80 ? 'success' : 'failed'
};
const fonctionCaseResult = {
name: 'fonction',
name: `functions ${this.getPartInfo(reportElement.functions)}`,
time: 0,
result: f.percentage >= 80 ? 'success' : 'failed'
result: this.getPercentage(reportElement.functions) >= 80 ? 'success' : 'failed'
};
const brancheCaseResult = {
name: 'branche',
name: `branches ${this.getPartInfo(reportElement.branches)}`,
time: 0,
result: b.percentage >= 80 ? 'success' : 'failed'
result: this.getPercentage(reportElement.branches) >= 80 ? 'success' : 'failed'
};
const testCases = [statementCaseResult, fonctionCaseResult, brancheCaseResult];
const goups = [new test_results_1.TestGroupResult(key, testCases)];
const suite = new test_results_1.TestSuiteResult(key, goups);
const groups = [new test_results_1.TestGroupResult(fileName, testCases)];
const suite = new test_results_1.TestSuiteResult(fileName, groups);
suites.push(suite);
console.log({ key, s, f, b });
}
return new test_results_1.TestRunResult(path, suites);
});
}
getParsedStat(stat) {
const max = Object.keys(stat).length;
const nonCovered = this.zeroLength(stat);
const percentage = ((max - nonCovered) / max) * 100;
return { max, nonCovered, percentage };
getPercentage(stat) {
return stat ? stat.hit / stat.found * 100 : 100;
}
zeroLength(report) {
return Object.keys(report).filter(key => report[key] === 0).length;
getPartInfo(stat) {
return `${this.getPercentage(stat)}% (${stat.hit}/${stat.found})`;
}
}
exports.LcovParser = LcovParser;
/***/ }),
/***/ 4750:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.parseProm = void 0;
const lcov_parse_1 = __importDefault(__nccwpck_require__(7454));
const parseProm = (pathOrStr) => {
return new Promise((resolve, reject) => {
(0, lcov_parse_1.default)(pathOrStr, (err, data) => {
if (err) {
reject(err);
}
resolve(data !== null && data !== void 0 ? data : []);
});
});
};
exports.parseProm = parseProm;
/***/ }),
/***/ 6043:
@ -1476,6 +1509,25 @@ class MochaJsonParser {
exports.MochaJsonParser = MochaJsonParser;
/***/ }),
/***/ 5366:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.SwiftXunitParser = void 0;
const java_junit_parser_1 = __nccwpck_require__(676);
class SwiftXunitParser extends java_junit_parser_1.JavaJunitParser {
constructor(options) {
super(options);
this.options = options;
}
}
exports.SwiftXunitParser = SwiftXunitParser;
/***/ }),
/***/ 5867:
@ -2279,8 +2331,8 @@ function parseIsoDate(str) {
}
exports.parseIsoDate = parseIsoDate;
function getFirstNonEmptyLine(stackTrace) {
const lines = stackTrace.split(/\r?\n/g);
return lines.find(str => !/^\s*$/.test(str));
const lines = stackTrace === null || stackTrace === void 0 ? void 0 : stackTrace.split(/\r?\n/g);
return lines === null || lines === void 0 ? void 0 : lines.find(str => !/^\s*$/.test(str));
}
exports.getFirstNonEmptyLine = getFirstNonEmptyLine;
@ -22198,6 +22250,139 @@ class Keyv extends EventEmitter {
module.exports = Keyv;
/***/ }),
/***/ 7454:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://yuilibrary.com/license/
*/
var fs = __nccwpck_require__(7147),
path = __nccwpck_require__(1017);
/* istanbul ignore next */
var exists = fs.exists || path.exists;
var walkFile = function(str, cb) {
var data = [], item;
[ 'end_of_record' ].concat(str.split('\n')).forEach(function(line) {
line = line.trim();
var allparts = line.split(':'),
parts = [allparts.shift(), allparts.join(':')],
lines, fn;
switch (parts[0].toUpperCase()) {
case 'TN':
item.title = parts[1].trim();
break;
case 'SF':
item.file = parts.slice(1).join(':').trim();
break;
case 'FNF':
item.functions.found = Number(parts[1].trim());
break;
case 'FNH':
item.functions.hit = Number(parts[1].trim());
break;
case 'LF':
item.lines.found = Number(parts[1].trim());
break;
case 'LH':
item.lines.hit = Number(parts[1].trim());
break;
case 'DA':
lines = parts[1].split(',');
item.lines.details.push({
line: Number(lines[0]),
hit: Number(lines[1])
});
break;
case 'FN':
fn = parts[1].split(',');
item.functions.details.push({
name: fn[1],
line: Number(fn[0])
});
break;
case 'FNDA':
fn = parts[1].split(',');
item.functions.details.some(function(i, k) {
if (i.name === fn[1] && i.hit === undefined) {
item.functions.details[k].hit = Number(fn[0]);
return true;
}
});
break;
case 'BRDA':
fn = parts[1].split(',');
item.branches.details.push({
line: Number(fn[0]),
block: Number(fn[1]),
branch: Number(fn[2]),
taken: ((fn[3] === '-') ? 0 : Number(fn[3]))
});
break;
case 'BRF':
item.branches.found = Number(parts[1]);
break;
case 'BRH':
item.branches.hit = Number(parts[1]);
break;
}
if (line.indexOf('end_of_record') > -1) {
data.push(item);
item = {
lines: {
found: 0,
hit: 0,
details: []
},
functions: {
hit: 0,
found: 0,
details: []
},
branches: {
hit: 0,
found: 0,
details: []
}
};
}
});
data.shift();
if (data.length) {
cb(null, data);
} else {
cb('Failed to parse string');
}
};
var parse = function(file, cb) {
exists(file, function(x) {
if (!x) {
return walkFile(file, cb);
}
fs.readFile(file, 'utf8', function(err, str) {
walkFile(str, cb);
});
});
};
module.exports = parse;
module.exports.source = walkFile;
/***/ }),
/***/ 9662:

29
dist/licenses.txt generated vendored
View file

@ -1056,6 +1056,35 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keyv
MIT
lcov-parse
BSD-3-Clause
Copyright 2012 Yahoo! Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Yahoo! Inc. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lowercase-keys
MIT
MIT License

View file

@ -1,11 +1,7 @@
import {DEFAULT_LOCALE} from './utils/node-utils'
export class TestRunResult {
constructor(
readonly path: string,
readonly suites: TestSuiteResult[],
private totalTime?: number
) {}
constructor(readonly path: string, readonly suites: TestSuiteResult[], private totalTime?: number) {}
get tests(): number {
return this.suites.reduce((sum, g) => sum + g.tests, 0)
@ -44,11 +40,7 @@ export class TestRunResult {
}
export class TestSuiteResult {
constructor(
readonly name: string,
readonly groups: TestGroupResult[],
private totalTime?: number
) {}
constructor(readonly name: string, readonly groups: TestGroupResult[], private totalTime?: number) {}
get tests(): number {
return this.groups.reduce((sum, g) => sum + g.tests.length, 0)
@ -86,10 +78,7 @@ export class TestSuiteResult {
}
export class TestGroupResult {
constructor(
readonly name: string | undefined | null,
readonly tests: TestCaseResult[]
) {}
constructor(readonly name: string | undefined | null, readonly tests: TestCaseResult[]) {}
get passed(): number {
return this.tests.reduce((sum, t) => (t.result === 'success' ? sum + 1 : sum), 0)