mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 01:17:31 +00:00
test(e2e): fix supersync test flakiness from parallel worker overload
Two sources of flakiness when running the full supersync suite:
1. Health check stampede: with many workers starting simultaneously,
all workers called isServerHealthy() concurrently (up to 24 HTTP
requests at once). This overloaded the server, causing false
negatives — workers got serverHealthyCache=false and skipped all
their tests (up to 192/198 tests). Fix: run the health check once
in globalSetup before workers are forked and store the result in
process.env.SUPERSYNC_SERVER_HEALTHY; workers read the env var
instead of making HTTP requests.
2. ERR_CONNECTION_REFUSED in createSimulatedClient: each supersync
test creates 2-3 browser contexts. Under parallel load the Angular
dev server temporarily refuses connections. Fix: retry page.goto('/')
up to 3 times with 1-2s backoff on ERR_CONNECTION_REFUSED.
This commit is contained in:
parent
af7c7687e2
commit
8865dc0a50
3 changed files with 60 additions and 6 deletions
|
|
@ -47,9 +47,18 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Also checks server health and skips test if server unavailable.
|
||||
*/
|
||||
testRunId: async ({}, use, testInfo) => {
|
||||
// Check server health once per worker
|
||||
// Check server health once per worker.
|
||||
// Prefer the result pre-computed in globalSetup (stored as an env var before workers
|
||||
// were forked) to avoid a stampede of concurrent HTTP requests from all workers
|
||||
// simultaneously — which can overload the server and produce false negatives.
|
||||
if (serverHealthyCache === null) {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
// Fallback for when tests are run directly without going through globalSetup
|
||||
// (e.g., `npx playwright test --config ... e2e/tests/sync/foo.spec.ts`)
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
@ -69,9 +78,13 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Tests are automatically skipped if the server is not healthy.
|
||||
*/
|
||||
serverHealthy: async ({}, use, testInfo) => {
|
||||
// Only check once per worker
|
||||
// Only check once per worker — use globalSetup env var if available (see testRunId above)
|
||||
if (serverHealthyCache === null) {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FullConfig } from '@playwright/test';
|
|||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isServerHealthy } from './utils/supersync-helpers';
|
||||
|
||||
/**
|
||||
* Warm up the dev server by fetching the app once before any tests start.
|
||||
|
|
@ -69,6 +70,20 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
|
|||
config.projects[0]?.use?.baseURL ||
|
||||
'http://localhost:4242';
|
||||
await warmUpDevServer(baseURL);
|
||||
|
||||
// Check SuperSync server health ONCE here, before workers start.
|
||||
// Without this, each worker checks independently on startup — with many workers
|
||||
// running simultaneously, the concurrent health-check requests can overload the
|
||||
// supersync server and cause false negatives, making workers skip all their tests.
|
||||
// By storing the result in an env var set before workers are forked, every worker
|
||||
// reads the cached result instantly instead of making HTTP requests.
|
||||
const healthy = await isServerHealthy().catch(() => false);
|
||||
process.env.SUPERSYNC_SERVER_HEALTHY = healthy ? 'true' : 'false';
|
||||
if (healthy) {
|
||||
console.log('SuperSync server healthy — supersync tests will run');
|
||||
} else {
|
||||
console.log('SuperSync server not available — supersync tests will be skipped');
|
||||
}
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
|
|
|
|||
|
|
@ -239,8 +239,34 @@ export const createSimulatedClient = async (
|
|||
}
|
||||
});
|
||||
|
||||
// Navigate to app and wait for ready
|
||||
await page.goto('/');
|
||||
// Navigate to app with retry for transient ERR_CONNECTION_REFUSED.
|
||||
// Under parallel load (many workers × 2-3 browser contexts each), the Angular
|
||||
// dev server can temporarily refuse connections. Retrying recovers from this
|
||||
// without failing the test outright.
|
||||
let lastGotoError: Error | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
await page.goto('/');
|
||||
lastGotoError = null;
|
||||
break;
|
||||
} catch (e) {
|
||||
lastGotoError = e as Error;
|
||||
const isConnectionRefused = lastGotoError.message.includes(
|
||||
'ERR_CONNECTION_REFUSED',
|
||||
);
|
||||
if (attempt < 2 && isConnectionRefused) {
|
||||
const delay = 1000 * (attempt + 1);
|
||||
console.log(
|
||||
`[Client ${clientName}] page.goto('/') failed (attempt ${attempt + 1}/3): ERR_CONNECTION_REFUSED — retrying in ${delay}ms`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
} else {
|
||||
break; // Non-connection error or last attempt — let it throw below
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastGotoError) throw lastGotoError;
|
||||
|
||||
await waitForAppReady(page);
|
||||
|
||||
const workView = new WorkViewPage(page, `${clientName}-${testPrefix}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue