Run 525 was the first with #174's graceful failure, and it worked: the integration suite failed, the end-to-end suite ran again, and `SonarQube Scan` succeeded for the first time since #154 started. But `Summarize integration tests` failed, and that step should not be able to. Both scripts already carried the principle in a comment — report plainly and exit 0, because the job fails on the real step and a stack trace here would only bury it — and both only implemented it for the file being absent. A file that exists and cannot be read crashed them. Two ways to reach that, both reproduced. `--forceExit`, which the integration script passes to paper over a post-run hang, can end the process around the write and leave partial JSON. And a suite that fails to *run* rather than to assert arrives without the array the failure renderer walks, which is exactly the shape this suite has been producing under #154. Reading is now guarded as thoroughly as `summarize-playwright.js` already guarded its traversal, and that traversal's `|| []` discipline is extended to the jest renderer. `summarize-playwright.js` had the same hole by the narrower path of an unguarded `JSON.parse`. The reason reaches the log rather than being swallowed. "Could not read the results file" with the parse error is diagnostic; a silent empty summary is not. This matters beyond tidiness because of where the failure lands. A crash here reports the job as failing at a step named for summarising rather than for testing, which is the misdirection #142 fixed once already — and a summariser whose job is to make a failing run readable should not crash on the output of the worst failures, which is the moment it is most needed. Verified against a truncated file, a suite entry with no `testResults`, an absent file, and a real 278-test run: the first three now exit 0 naming the reason, the absent case is unchanged, and the happy path still reports its counts. Closes #178
105 lines
3.8 KiB
JavaScript
105 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
const fs = require('fs');
|
|
|
|
const [, , resultsPath] = process.argv;
|
|
|
|
// This runs with `if: always()`, so it also fires when an earlier step failed and
|
|
// no results file was ever written. Report that plainly and exit 0 — the job still
|
|
// fails on the real step, and a stack trace here would only bury it.
|
|
if (!fs.existsSync(resultsPath)) {
|
|
report([`## Playwright E2E Results`, '', `No results file at \`${resultsPath}\` — the test step failed before writing one.`, '']);
|
|
console.log(`Playwright: no results file at ${resultsPath} — the test step failed before writing one`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// The traversal below is defensive at every level; reading was not. A run
|
|
// killed part way through leaves a file that exists and does not parse, and
|
|
// crashing on it fails a step named for summarising rather than for testing.
|
|
// See #178.
|
|
let data;
|
|
try {
|
|
data = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
|
|
} catch (err) {
|
|
const why = `Could not read \`${resultsPath}\` — ${err.message}`;
|
|
report(['## Playwright E2E Results', '', why, '']);
|
|
console.log(`Playwright: ${why}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const stats = data.stats || {};
|
|
|
|
const lines = [];
|
|
lines.push('## Playwright E2E Results');
|
|
lines.push('');
|
|
lines.push('| | Count |');
|
|
lines.push('|---|---|');
|
|
lines.push(`| ✅ Passed | ${stats.expected || 0} |`);
|
|
lines.push(`| ❌ Failed | ${stats.unexpected || 0} |`);
|
|
lines.push(`| ⚠️ Flaky | ${stats.flaky || 0} |`);
|
|
lines.push(`| ⏭️ Skipped | ${stats.skipped || 0} |`);
|
|
lines.push('');
|
|
|
|
function collectFailures(suites, path) {
|
|
path = path || [];
|
|
let failures = [];
|
|
for (const suite of suites || []) {
|
|
const currentPath = path.concat(suite.title).filter(Boolean);
|
|
for (const spec of suite.specs || []) {
|
|
for (const test of spec.tests || []) {
|
|
for (const result of test.results || []) {
|
|
if (result.status !== 'passed' && result.status !== 'skipped') {
|
|
failures.push(`${currentPath.join(' > ')} > ${spec.title}: ${result.status}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (suite.suites) failures = failures.concat(collectFailures(suite.suites, currentPath));
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
const failures = collectFailures(data.suites);
|
|
if (failures.length) {
|
|
lines.push('### Failures');
|
|
lines.push('');
|
|
for (const f of failures) lines.push(`- ${f}`);
|
|
lines.push('');
|
|
}
|
|
|
|
report(lines);
|
|
|
|
headline();
|
|
|
|
// Exits 0 whatever it found. Summarising and gating are two jobs, and this
|
|
// script doing both meant the workflow failed at a step named "Summarize
|
|
// end-to-end tests", which reads as a broken summary script rather than as
|
|
// failing tests. The workflow's own `Fail if either suite failed` step fails the
|
|
// job now. It was written for exactly that and was being skipped, because a step
|
|
// whose `if:` omits always() still requires its predecessors to have succeeded,
|
|
// and this script had already failed the job before it could run.
|
|
process.exit(0);
|
|
|
|
/**
|
|
* One line of counts on stdout, alongside the markdown.
|
|
*
|
|
* `report` writes to GITEA_STEP_SUMMARY when it is set, which under Actions is
|
|
* always — so the table and the failure list landed in the Summary tab while the
|
|
* log showed a bare exit with no output at all. This puts the numbers in the log
|
|
* too, at the point the run stops.
|
|
*/
|
|
function headline() {
|
|
const failed = stats.unexpected || 0;
|
|
const counts =
|
|
`Playwright: ${stats.expected || 0} passed, ${failed} failed, ` +
|
|
`${stats.flaky || 0} flaky, ${stats.skipped || 0} skipped`;
|
|
console.log(failed > 0 ? `${counts} — see the job summary for which` : counts);
|
|
}
|
|
|
|
function report(out) {
|
|
const summaryPath = process.env.GITEA_STEP_SUMMARY || process.env.GITHUB_STEP_SUMMARY;
|
|
if (summaryPath) {
|
|
fs.appendFileSync(summaryPath, out.join('\n') + '\n');
|
|
} else {
|
|
console.log(out.join('\n'));
|
|
}
|
|
} |