reduce e2e test flakiness
ci/woodpecker/push/e2e Pipeline was successful
ci/woodpecker/push/fmt Pipeline was successful
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
2026-08-02 00:18:22 -04:00
parent a2134b93f1
commit 04c9724c76
2 changed files with 59 additions and 34 deletions
+43 -18
View File
@@ -12,13 +12,36 @@ import * as path from "path";
export const test = base.extend<{ server: { baseURL: string }; page: Page }>({ export const test = base.extend<{ server: { baseURL: string }; page: Page }>({
server: [ server: [
async ({}, use) => { async ({}, use) => {
const server = await startServer();
await use({ baseURL: server.baseURL });
await killTree(server.child);
// Clean up the DB files (including -wal / -shm).
for (const suffix of ["", "-wal", "-shm"]) {
fs.rmSync(server.dbPath + suffix, { force: true });
}
},
{ scope: "test", auto: true },
],
// Provide a page whose baseURL points at this test's server.
page: async ({ browser, server }, use) => {
const context = await browser.newContext({ baseURL: server.baseURL });
const page = await context.newPage();
await use(page);
await context.close();
},
});
/** Starts a server, retrying on a fresh port if the first attempt fails to bind. */
async function startServer() {
for (let attempt = 0; attempt < 5; attempt++) {
const dbPath = path.join( const dbPath = path.join(
os.tmpdir(), os.tmpdir(),
`sustenance-e2e-${process.pid}-${Date.now()}-${Math.random() `sustenance-e2e-${process.pid}-${Date.now()}-${Math.random()
.toString(36) .toString(36)
.slice(2)}.db`, .slice(2)}.db`,
); );
const port = 3200 + Math.floor(Math.random() * 2000); const port = 20000 + Math.floor(Math.random() * 30000);
const baseURL = `http://127.0.0.1:${port}`; const baseURL = `http://127.0.0.1:${port}`;
const child = spawn( const child = spawn(
@@ -33,33 +56,35 @@ export const test = base.extend<{ server: { baseURL: string }; page: Page }>({
// Point SEED_CONFIG at a nonexistent file so no default user is created. // Point SEED_CONFIG at a nonexistent file so no default user is created.
SEED_CONFIG: path.join(os.tmpdir(), "sustenance-e2e-no-seed.json"), SEED_CONFIG: path.join(os.tmpdir(), "sustenance-e2e-no-seed.json"),
}, },
stdio: "ignore", stdio: ["ignore", "ignore", "pipe"],
// Run in its own process group so we can kill the whole tree. // Run in its own process group so we can kill the whole tree.
detached: true, detached: true,
}, },
); );
let stderr = "";
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
try {
await waitForServer(baseURL, child); await waitForServer(baseURL, child);
return { baseURL, child, dbPath };
await use({ baseURL }); } catch (error) {
// The server may have failed to bind (port collision). Clean up and retry.
await killTree(child); await killTree(child);
// Clean up the DB files (including -wal / -shm).
for (const suffix of ["", "-wal", "-shm"]) { for (const suffix of ["", "-wal", "-shm"]) {
fs.rmSync(dbPath + suffix, { force: true }); fs.rmSync(dbPath + suffix, { force: true });
} }
}, if (attempt === 4) {
{ scope: "test", auto: true }, throw new Error(
], `server failed to start after retries; last stderr:\n${stderr}\n${error}`,
);
// Provide a page whose baseURL points at this test's server. }
page: async ({ browser, server }, use) => { }
const context = await browser.newContext({ baseURL: server.baseURL }); }
const page = await context.newPage(); throw new Error("unreachable");
await use(page); }
await context.close();
},
});
async function waitForServer(baseURL: string, child: ChildProcess) { async function waitForServer(baseURL: string, child: ChildProcess) {
const deadline = Date.now() + 60_000; const deadline = Date.now() + 60_000;
+1 -1
View File
@@ -3,7 +3,7 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({ export default defineConfig({
testDir: "./tests", testDir: "./tests",
timeout: 30_000, timeout: 30_000,
retries: 0, retries: 2,
use: { use: {
trace: "on-first-retry", trace: "on-first-retry",
}, },