diff --git a/__tests__/__snapshots__/lcov.test.ts.snap b/__tests__/__snapshots__/lcov.test.ts.snap index 3fa6990..8125e21 100644 --- a/__tests__/__snapshots__/lcov.test.ts.snap +++ b/__tests__/__snapshots__/lcov.test.ts.snap @@ -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, }, ], diff --git a/__tests__/lcov.test.ts b/__tests__/lcov.test.ts index 60dd616..4678329 100644 --- a/__tests__/lcov.test.ts +++ b/__tests__/lcov.test.ts @@ -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() diff --git a/dist/index.js b/dist/index.js index 3158810..19bbbdf 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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: diff --git a/dist/licenses.txt b/dist/licenses.txt index 452b9cb..47780e4 100644 --- a/dist/licenses.txt +++ b/dist/licenses.txt @@ -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 diff --git a/src/test-results.ts b/src/test-results.ts index bca8c41..97cf3b4 100644 --- a/src/test-results.ts +++ b/src/test-results.ts @@ -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)