Files
bermudalamb 5d427bc1a1
Linting / lint (pull_request) Successful in 2m0s
SonarQube Analysis / sonarqube (pull_request) Failing after 21m21s
fix(ci): stop the test summarisers failing the job on an unreadable results file (#178)
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
2026-08-25 09:20:34 -05:00

88 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const [, , resultsPath, label] = 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([`## ${label || 'Test'} Results`, '', `No results file at \`${resultsPath}\` — the test step failed before writing one.`, '']);
console.log(`${label || 'Test'}: no results file at ${resultsPath} — the test step failed before writing one`);
process.exit(0);
}
// Existing but unreadable is a separate case from absent, and it was the one
// that could still fail the job. `--forceExit` can end the process around the
// write and leave a partial file, and a suite that fails to *run* rather than
// to assert produces a shape the renderer below did not expect. Either crashed
// the script, which failed a step named for summarising and buried the real
// failure — the exact misdirection #142 fixed once already. See #178.
//
// The reason is reported rather than swallowed: "could not read the results
// file" plus the parse error is diagnostic, where a silent empty summary is not.
let data;
try {
data = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
} catch (err) {
const why = `Could not read \`${resultsPath}\` — ${err.message}`;
report([`## ${label || 'Test'} Results`, '', why, '']);
console.log(`${label || 'Test'}: ${why}`);
process.exit(0);
}
const lines = [];
lines.push(`## ${label || 'Test'} Results`);
lines.push('');
lines.push('| | Count |');
lines.push('|---|---|');
lines.push(`| ✅ Passed | ${data.numPassedTests} |`);
lines.push(`| ❌ Failed | ${data.numFailedTests} |`);
lines.push(`| ⏭️ Skipped | ${data.numPendingTests} |`);
lines.push(`| **Total** | **${data.numTotalTests}** |`);
lines.push('');
if (data.numFailedTests > 0) {
lines.push('### Failures');
lines.push('');
// Defensive at every level, like summarize-playwright.js's traversal: a suite
// that failed to run can arrive without the array this walks.
for (const suite of data.testResults || []) {
for (const t of suite.testResults || []) {
if (t.status === 'failed') {
lines.push(`- **${t.fullName}**`);
const msg = t.failureMessages && t.failureMessages[0]
? t.failureMessages[0].split('\n')[0]
: 'see job log for details';
lines.push(` \`${msg}\``);
}
}
}
lines.push('');
}
report(lines);
headline();
// Exits 0 whatever it found, for the same reason as summarize-playwright.js:
// summarising and gating are two jobs, and the workflow's `Fail if either suite
// failed` step is the one that should fail a run.
process.exit(0);
/** One line of counts on stdout, so the log says what happened, not only the Summary tab. */
function headline() {
const counts =
`${label || 'Test'}: ${data.numPassedTests} passed, ${data.numFailedTests} failed, ` +
`${data.numPendingTests} skipped`;
console.log(data.numFailedTests > 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'));
}
}