* ci(browser): stop a hung browser run before the job limit and name it A browser job killed at its 20-minute limit keeps no log and skips its upload steps (merge-queue run 40723857746, firefox shard 2/2), so nothing showed which test hung. - Each browser test step gets timeout-minutes 18, below the job's 20, so the step fails and the always() uploads still run. - Under CI, Playwright's globalTimeout stops the run first: PM_TEST_GLOBAL_TIMEOUT_MINUTES 17 for page workflows, 15 for coverage workflows, a fixed 3 for the viewer suite. Locally there is no limit unless the variable is set. Each test keeps the 30 s default. - running-tests-reporter.mjs prints the tests still running when the run ends; Playwright's own reporters cannot, since those tests never end. - A blob report goes to tools/vendor/test-results/blob/, inside the uploaded traces path. - ci-workflow.test.mjs pins globalTimeout < step limit < job limit and the always() uploads; no existing limit is raised. Closes #425 * ci(browser): rely on the step limit, report the running tests on SIGTERM Maintainer ruling on #425: no Playwright globalTimeout. The 18-minute step limit under the unchanged 20-minute job limit stays, as do the always() uploads and the CI blob report. - Drop globalTimeout and PM_TEST_GLOBAL_TIMEOUT_MINUTES with its helper and tests. - A step that reaches its limit gets SIGTERM, which ended Playwright with no report and no blob. The running-tests reporter now catches the first SIGTERM, prints the tests still running and sends Playwright its own SIGINT, so the run ends interrupted and the blob report is written. A second SIGTERM kills as usual. - Test paths are relative to the suite's rootDir instead of the cwd, so `npm run --prefix` no longer prints ../../ prefixes. - The workflow comment and TESTING.md say the guarantee assumes an npm cache hit and that coverage shard 1 is tight in its 18-minute step. * ci(browser): forward SIGTERM and SIGINT from frontend-coverage.mjs to its child In the coverage job npm signals only frontend-coverage.mjs. It blocked in spawnSync with no listener, so Playwright never saw a step timeout. It now runs each step's child with async spawn, in its own process group, and forwards SIGTERM and SIGINT to the running child. Step order, exit codes and the "Command failed (N)" message are unchanged. Locally, in the pinned image, `timeout -s TERM` on test:coverage printed the still-running tests and wrote both blob reports. TESTING.md records that a SIGTERM to npm alone stops at npm's `sh -c`, and that delivery through `docker exec` in CI is not yet observed. Refs #425 * ci(browser): load the running-tests reporter by absolute path A config imported from another directory (the workflow-artifacts test copies it to a temp root) resolved './running-tests-reporter.mjs' against that root under CI and failed to load it. Resolve it from this module instead.
59 lines
2.2 KiB
JavaScript
59 lines
2.2 KiB
JavaScript
import { writeSync } from 'node:fs';
|
||
import { relative } from 'node:path';
|
||
|
||
// Names the tests still running when a run is cut off (#425). A browser test step that reaches its
|
||
// timeout-minutes gets SIGTERM, which ends Playwright at once: no reporter's onEnd runs, so neither
|
||
// `list` nor the blob report says what was running. On the first SIGTERM this reporter prints every
|
||
// started test that has not ended, then turns the signal into SIGINT, Playwright's own interrupt,
|
||
// which stops the workers, ends the run as interrupted (non-zero exit) and writes the blob report.
|
||
// A second SIGTERM gets the default behaviour: the process dies.
|
||
export default class RunningTestsReporter {
|
||
constructor({ now = () => Date.now(), write = text => writeSync(1, text), host = process } = {}) {
|
||
this.now = now;
|
||
this.write = write;
|
||
this.host = host;
|
||
this.rootDir = host.cwd();
|
||
this.running = new Map();
|
||
this.onSigterm = () => {
|
||
this.host.removeListener('SIGTERM', this.onSigterm);
|
||
this.report('SIGTERM');
|
||
this.host.kill(this.host.pid, 'SIGINT');
|
||
};
|
||
}
|
||
|
||
printsToStdio() {
|
||
return false;
|
||
}
|
||
|
||
onBegin(config) {
|
||
this.rootDir = config.rootDir;
|
||
this.host.on('SIGTERM', this.onSigterm);
|
||
}
|
||
|
||
onTestBegin(test, result) {
|
||
this.running.set(`${test.id}#${result.retry}`, { test, startedAt: this.now() });
|
||
}
|
||
|
||
onTestEnd(test, result) {
|
||
this.running.delete(`${test.id}#${result.retry}`);
|
||
}
|
||
|
||
describe({ test, startedAt }) {
|
||
const project = test.parent?.project()?.name;
|
||
const where = `${relative(this.rootDir, test.location.file)}:${test.location.line}`;
|
||
const seconds = ((this.now() - startedAt) / 1000).toFixed(1);
|
||
return `${project ? `[${project}] › ` : ''}${where} › ${test.title} (running ${seconds}s)`;
|
||
}
|
||
|
||
report(reason) {
|
||
if (!this.running.size) return;
|
||
const lines = [...this.running.values()].map(entry => ` ${this.describe(entry)}`);
|
||
this.write(`\nStill running when the run ended (${reason}): ${this.running.size}\n${lines.join('\n')}\n`);
|
||
}
|
||
|
||
onEnd(result) {
|
||
this.host.removeListener('SIGTERM', this.onSigterm);
|
||
this.report(result.status);
|
||
}
|
||
}
|