Skip to content

Commit

Permalink
Report skipped tests and clean code
Browse files Browse the repository at this point in the history
- Report skipped tests for JUnit & Go formats
- Generate a JUnit badge only if at least one file matches
- Improve tests, types and naming
  • Loading branch information
GaelGirodon committed May 24, 2024
1 parent a3b1d30 commit 9002ebb
Show file tree
Hide file tree
Showing 25 changed files with 190 additions and 109 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ jobs:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm run build
- run: npm run test:ci
env:
GIST_TOKEN: ${{ secrets.GIST_TOKEN }}
- run: npm run build
- run: sed -i '0,/index.js/s//dist\/index.js/' action.yml package.json
- run: rm -rf test/data
- uses: ./
with:
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ advantage of Shields customization features (through the query string).
![tests](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-go-tests.json)
![tests](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fgist.githubusercontent.com%2FGaelGirodon%2F715c62717519f634185af0ebde234992%2Fraw%2Frepo-junit-tests.json)

This badge displays the number of passed and failed tests extracted from a test
report.
This badge displays the number of passed, failed and skipped tests extracted
from test report(s).

```json
{"schemaVersion":1,"label":"tests","message":"3 passed","color":"brightgreen"}
Expand Down Expand Up @@ -89,7 +89,7 @@ Only matched report formats will get a file uploaded to the Gist.
Write the verbose test output (`>` or `tee`) with coverage enabled to a single
`test*.{out,txt}` file next to the `go.mod` file:

- `RUN`, `PASS` and `FAIL` flags will be used to count tests
- `RUN`, `PASS`, `FAIL` and `SKIP` flags will be used to count tests
- The last percentage will be used as the coverage value

`go tool cover -func=cover.out` output may be appended to the above file to make
Expand Down Expand Up @@ -118,8 +118,8 @@ support this format too, natively or using an additional reporter:
- **Deno**: `deno test --junit-path=report.xml`
- **PHPUnit**: `phpunit --log-junit report.xml`

The number of tests and failures will be extracted from top-level `<testsuite>`
tags, from all matching and valid report files.
The number of tests (total, failed and skipped) will be extracted from
top-level `<testsuite>` tags, from all matching and valid report files.

➡️ `{repo}-[{ref}-]junit-tests.json`

Expand Down
11 changes: 9 additions & 2 deletions jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"checkJs": true
"checkJs": true,
"lib": [
"esnext"
]
},
"include": [ "*.js", "src/**/*.js" ]
"include": [
"*.js",
"src/**/*.js",
"test/**/*.js"
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint",
"test": "mocha src",
"test:e2e": "mocha test",
"test:ci": "c8 --reporter=cobertura --all --include \"src/**/*.js\" mocha --reporter mocha-junit-reporter src test || exit 0"
"test:ci": "c8 --reporter=cobertura mocha --reporter mocha-junit-reporter src test || exit 0"
},
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions src/badges/coverage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Build a coverage badge.
* @param {*} data Badge data
* @returns {import('./index.js').Badge} Badge content
* @param {CoverageReportData} data Coverage report data
* @returns {BadgeContent} Badge content
*/
export function buildBadge(data) {
const content = {};
Expand Down
15 changes: 5 additions & 10 deletions src/badges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ import * as core from '@actions/core';
import * as tests from './tests.js';
import * as coverage from './coverage.js';

/**
* @typedef {{schemaVersion?: number, label?: string, message?: string, color?: string}} Badge A generated badge
* @typedef {(data: any) => Badge} BadgeGenerator A badge generator
*/

/**
* Available badge generators
* @type {{[key: string]: {buildBadge: BadgeGenerator}}}
* @type {{ [key: string]: { buildBadge: BadgeGenerator } }}
*/
const generators = { tests, coverage };

/**
* Build a badge file from a report.
* @param {import('../reports/index.js').Report} report Input report
* @returns {{name: string, content: Badge}} Badge file name and content
* @param {Report} report Input report
* @returns {NamedBadge} Badge name and content
*/
export function buildBadge(report) {
let name = `${report.format}-${report.type}.json`;
Expand All @@ -32,10 +27,10 @@ export function buildBadge(report) {
name = `${prefix}-${name}`;
}
core.info(`Build badge ${name}`);
const content = {
const badge = {
schemaVersion: 1,
label: report.type,
...generators[report.type].buildBadge(report.data)
};
return { name, content };
return { name, badge };
}
2 changes: 1 addition & 1 deletion src/badges/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('badges/index', () => {
const actual = buildBadge({ type: 'coverage', format: 'go', data: { coverage: 0 } });
assert.deepEqual(actual, {
name: expected.name,
content: { schemaVersion: 1, label: 'coverage', message: '0%', color: 'red' }
badge: { schemaVersion: 1, label: 'coverage', message: '0%', color: 'red' }
});
});
}
Expand Down
10 changes: 7 additions & 3 deletions src/badges/tests.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/**
* Build a tests badge.
* @param {*} data Badge data
* @returns {import('./index.js').Badge} Badge content
* @param {TestReportData} data Test report data
* @returns {BadgeContent} Badge content
*/
export function buildBadge(data) {
const content = {};
content.message = `${data.passed} passed`;
content.color = data.passed > 0 ? 'brightgreen' : 'lightgrey';
if (data.failed > 0) {
content.message += `, ${data.failed} failed`;
content.color = 'red';
}
if (data.skipped > 0) {
content.message += `, ${data.skipped} skipped`;
}
content.color = data.failed === 0 ? 'brightgreen' : 'red';
return content;
}
10 changes: 7 additions & 3 deletions src/badges/tests.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import { buildBadge } from './tests.js';
describe('badges/tests', () => {
describe('#buildBadge()', () => {
const tests = [
{ data: { passed: 5, failed: 0 }, expected: { message: '5 passed', color: 'brightgreen' } },
{ data: { passed: 4, failed: 1 }, expected: { message: '4 passed, 1 failed', color: 'red' } }
{ data: { passed: 0, failed: 0, skipped: 0 }, expected: { message: '0 passed', color: 'lightgrey' } },
{ data: { passed: 0, failed: 0, skipped: 1 }, expected: { message: '0 passed, 1 skipped', color: 'lightgrey' } },
{ data: { passed: 5, failed: 0, skipped: 0 }, expected: { message: '5 passed', color: 'brightgreen' } },
{ data: { passed: 4, failed: 0, skipped: 1 }, expected: { message: '4 passed, 1 skipped', color: 'brightgreen' } },
{ data: { passed: 4, failed: 1, skipped: 0 }, expected: { message: '4 passed, 1 failed', color: 'red' } },
{ data: { passed: 3, failed: 1, skipped: 1 }, expected: { message: '3 passed, 1 failed, 1 skipped', color: 'red' } }
];
for (const { data, expected } of tests) {
it(`should return a ${expected.color} "${expected.message}" badge for ${data.passed}/${data.failed} tests`, () => {
it(`should return a ${expected.color} "${expected.message}" badge for ${data.passed}/${data.failed}/${data.skipped} tests`, () => {
const actual = buildBadge(data);
assert.deepEqual(actual, expected);
});
Expand Down
10 changes: 4 additions & 6 deletions src/gist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import * as github from '@actions/github';

/**
* Update the Gist.
* @param {{name: string, content: import('../badges/index.js').Badge}[]} badges Badges to add to the Gist
* @param {NamedBadge[]} badges Badges to add to the Gist
*/
export async function update(badges) {
core.info(`Update Gist with ${badges.length} file(s)`);
const octokit = github.getOctokit(core.getInput('token'));
const files = badges
.reduce((result, b) => {
result[b.name] = { content: JSON.stringify(b.content) };
return result;
}, {});
const files = Object.fromEntries(
badges.map((b) => [b.name, { content: JSON.stringify(b.badge) }])
);
await octokit.rest.gists.update({
gist_id: core.getInput('gist-id'),
files
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export async function main() {
.map(b => buildBadge(b));
await gist.update(badges);
} catch (error) {
core.warning(`An error occurred: ${error.message}`);
const msg = error instanceof Error ? error.message : error;
core.warning(`An error occurred: ${msg}`);
}
}
19 changes: 10 additions & 9 deletions src/reports/cobertura.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@ export async function getReports(root) {
join(root, '**/*cobertura*.xml'),
join(root, '**/*coverage*.xml')
];
const reports = await globNearest(patterns);
const badges = [];
for (const r of reports) {
core.info(`Load Cobertura report '${r}'`);
const report = await fs.readFile(r, { encoding: 'utf8' });
const coverageMatches = report
const files = await globNearest(patterns);
/** @type {Omit<CoverageReport, 'format'>[]} */
const reports = [];
for (const f of files) {
core.info(`Load Cobertura report '${f}'`);
const contents = await fs.readFile(f, { encoding: 'utf8' });
const coverageMatches = contents
.match(/(?<=<coverage[^>]+line-rate=")[0-9.]+(?=")/);
if (coverageMatches?.length !== 1) {
core.info('Report is not a valid Cobertura report');
continue; // Invalid report file, trying the next one
}
const coverage = parseFloat(coverageMatches[0]) * 100;
badges.push({ type: 'coverage', data: { coverage } });
reports.push({ type: 'coverage', data: { coverage } });
break; // Successfully loaded a report file, can return now
}
core.info(`Loaded ${badges.length} Cobertura report(s)`);
return badges;
core.info(`Loaded ${reports.length} Cobertura report(s)`);
return reports;
}
2 changes: 1 addition & 1 deletion src/reports/cobertura.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getReports } from './cobertura.js';

describe('reports/cobertura', () => {
describe('#getReports()', () => {
it('should return coverage report', async () => {
it('should return a coverage report', async () => {
const reports = await getReports(join(process.cwd(), 'test/data/cobertura'));
assert.equal(reports.length, 1);
assert.deepEqual(reports, [
Expand Down
30 changes: 16 additions & 14 deletions src/reports/go.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { globNearest } from '../util/index.js';
/**
* Load reports using Go tests and coverage formats.
* @param {string} root Root search directory
* @returns {Promise<import('./index.js').Report[]>} Go tests and coverage reports
* @returns Go tests and coverage reports
*/
export async function getReports(root) {
core.info('Load Go tests and coverage report');
Expand All @@ -17,26 +17,28 @@ export async function getReports(root) {
}
const dir = dirname(goMods[0]);
core.info(`Search Go reports in '${dir}'`);
const badges = [];
/** @type {Omit<TestReport | CoverageReport, 'format'>[]} */
const reports = [];
const patterns = ['test*.out', 'test*.txt'].map(t => join(dir, t));
const reports = await globNearest(patterns);
for (const r of reports) {
core.info(`Load Go report '${r}'`);
const report = await fs.readFile(r, { encoding: 'utf8' });
const tests = (report.match(/=== RUN/g) || []).length;
const files = await globNearest(patterns);
for (const f of files) {
core.info(`Load Go report '${f}'`);
const contents = await fs.readFile(f, { encoding: 'utf8' });
const tests = (contents.match(/=== RUN/g) || []).length;
if (tests === 0) {
continue; // Invalid report file, trying the next one
}
const passed = (report.match(/--- PASS/g) || []).length;
const failed = (report.match(/--- FAIL/g) || []).length;
badges.push({ type: 'tests', data: { passed, failed, tests } });
const percentages = report.match(/(?<=\s)[0-9.]+(?=%)/g);
const passed = (contents.match(/--- PASS/g) || []).length;
const failed = (contents.match(/--- FAIL/g) || []).length;
const skipped = (contents.match(/--- SKIP/g) || []).length;
reports.push({ type: 'tests', data: { tests, passed, failed, skipped } });
const percentages = contents.match(/(?<=\s)[0-9.]+(?=%)/g);
if (percentages && percentages.length >= 1) {
const coverage = parseFloat(percentages.slice(-1)[0]);
badges.push({ type: 'coverage', data: { coverage } });
reports.push({ type: 'coverage', data: { coverage } });
}
break; // Successfully loaded a report file, can return now
}
core.info(`Loaded ${badges.length} Go report(s)`);
return badges;
core.info(`Loaded ${reports.length} Go report(s)`);
return reports;
}
35 changes: 28 additions & 7 deletions src/reports/go.test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import assert from 'assert/strict';
import { copyFile, mkdir, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { getReports } from './go.js';

describe('reports/go', () => {
describe('#getReports()', () => {
it('should return tests and coverage reports', async () => {
const reports = await getReports(join(process.cwd(), 'test/data/go'));
assert.equal(reports.length, 2);
assert.deepEqual(reports, [
{ type: 'tests', data: { passed: 3, failed: 0, tests: 3 } },
{ type: 'coverage', data: { coverage: 96.5 } }
]);
before(async () => {
await mkdir('test/data/go/project');
await writeFile('test/data/go/project/go.mod', '');
});
const coverage = { type: 'coverage', data: { coverage: 96.5 } };
const tests = [
{ report: 'test.out', expected: [{ type: 'tests', data: { tests: 3, passed: 3, failed: 0, skipped: 0 } }, coverage] },
{ report: 'only-test.out', expected: [{ type: 'tests', data: { tests: 2, passed: 2, failed: 0, skipped: 0 } }] },
{ report: 'failed-test.out', expected: [{ type: 'tests', data: { tests: 6, passed: 3, failed: 2, skipped: 1 } }, coverage] },
{ report: 'go.mod', expected: [] }
];
for (const { report, expected } of tests) {
it(`should return expected report(s) for file ${report}`, async () => {
await copyFile(join('test/data/go', report), 'test/data/go/project/test.out');
const reports = await getReports(join(process.cwd(), 'test/data/go/project'));
assert.equal(reports.length, expected.length);
assert.deepEqual(reports, expected);
});
}
after(async () => {
await rm('test/data/go/project', { recursive: true });
});

it('should not return any reports when go.mod file is missing', async () => {
const reports = await getReports(join(process.cwd(), 'test/data/junit'));
assert.equal(reports.length, 0);
assert.deepEqual(reports, []);
});
});
});
11 changes: 3 additions & 8 deletions src/reports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,23 @@ import * as junit from './junit.js';
import * as cobertura from './cobertura.js';
import * as jacoco from './jacoco.js';

/**
* @typedef {{ type: string, format?: string, data: any }} Report A loaded report
* @typedef {(root: string) => Promise<Report[]>} ReportsLoader A reports loader
*/

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

/**
* Load all available reports in the current workspace.
* @returns Loaded reports
* @returns {Promise<Report[]>} Loaded reports
*/
export async function getReports() {
core.info('Load reports');
const all = [];
for (const id of Object.keys(loaders)) {
try {
const reports = await loaders[id].getReports(process.cwd());
all.push(...reports.map(r => ({ format: id, ...r })));
all.push(...reports.map(r => ({ ...r, format: id })));
} catch (error) {
core.warning(`Skipping ${id} report format: ${error}`);
}
Expand Down
Loading

0 comments on commit 9002ebb

Please sign in to comment.