Co-Study4Grid / scripts /parity_e2e /playwright.global-setup.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
5.95 kB
/**
* Playwright global setup — wires the real-backend mode for the demo
* replay specs.
*
* Activated when `COSTUDY4GRID_REAL_BACKEND=1` is set. The setup
* either spawns `uvicorn expert_backend.main:app --port 8000`
* (when `COSTUDY4GRID_SPAWN_BACKEND=1`) or assumes an externally-
* managed backend is already listening on `127.0.0.1:8000`. In both
* cases it then POSTs `/api/config` with the small_grid paths so the
* subsequent specs hit a pre-loaded state.
*
* When `COSTUDY4GRID_REAL_BACKEND` is unset (the default), this setup
* is a no-op and the specs run against their `page.route` mocks as
* before.
*
* Why a globalSetup instead of a per-spec `beforeAll`:
* - The backend load takes ~5-10 s on small_grid and would be paid
* by every spec under per-spec setup (currently 3 demo_*.spec.ts).
* - Pytest's `setup_class` equivalent is one POST /api/config per
* test class; here we want ONE POST per test session.
*
* The matching teardown (`playwright.global-teardown.ts`) only kills
* the uvicorn process when we spawned it; an externally-managed
* backend is left untouched.
*
* Limitations / prereqs documented in `DEMO_REPLAY_README.md`.
*/
import { spawn, type ChildProcess } from 'node:child_process';
import * as path from 'node:path';
import type { FullConfig } from '@playwright/test';
const BACKEND_URL = process.env.COSTUDY4GRID_BACKEND_URL ?? 'http://127.0.0.1:8000';
const REPO_ROOT = path.resolve(__dirname, '../..');
// Resolved small_grid paths — must match what `test_demo_scenario_small_grid.py`
// uses on the pytest side so the two test layers agree on the fixture data.
const SMALL_GRID_CONFIG = {
network_path: path.join(REPO_ROOT, 'data', 'bare_env_small_grid_test', 'grid.xiidm'),
action_file_path: path.join(REPO_ROOT, 'data', 'action_space', 'reduced_model_actions_test.json'),
layout_path: path.join(REPO_ROOT, 'data', 'bare_env_small_grid_test', 'grid_layout.json'),
min_line_reconnections: 2.0,
min_close_coupling: 3.0,
min_open_coupling: 2.0,
min_line_disconnections: 3.0,
min_pst: 1.0,
min_load_shedding: 2.0,
min_renewable_curtailment_actions: 0,
n_prioritized_actions: 10,
lines_monitoring_path: '',
monitoring_factor: 0.95,
pre_existing_overload_threshold: 0.02,
ignore_reconnections: false,
pypowsybl_fast_mode: true,
model: 'expert',
compute_overflow_graph: true,
};
let spawnedBackend: ChildProcess | null = null;
async function waitForBackend(url: string, timeoutMs = 60_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
let lastError: unknown = null;
while (Date.now() < deadline) {
try {
const res = await fetch(`${url}/api/user-config`);
if (res.ok) return;
lastError = new Error(`status ${res.status}`);
} catch (err) {
lastError = err;
}
await new Promise(r => setTimeout(r, 500));
}
throw new Error(
`Backend did not become ready at ${url} within ${timeoutMs}ms. Last error: ${String(lastError)}`,
);
}
async function spawnUvicorn(): Promise<ChildProcess> {
const proc = spawn(
'uvicorn',
['expert_backend.main:app', '--host', '127.0.0.1', '--port', '8000'],
{
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env },
},
);
// Surface backend stderr in the Playwright run so a failed boot
// is debuggable from the test output.
proc.stderr?.on('data', (chunk: Buffer) => {
process.stderr.write(`[uvicorn] ${chunk.toString()}`);
});
proc.on('exit', (code) => {
if (code !== null && code !== 0) {
process.stderr.write(`[uvicorn] exited with code ${code}\n`);
}
});
return proc;
}
export default async function globalSetup(_config: FullConfig): Promise<void> {
if (process.env.COSTUDY4GRID_REAL_BACKEND !== '1') {
// Mock-backend mode: the specs' own `page.route` calls handle
// everything. Nothing to do here.
return;
}
if (process.env.COSTUDY4GRID_SPAWN_BACKEND === '1') {
// eslint-disable-next-line no-console
console.log('[globalSetup] spawning uvicorn for real-backend mode…');
spawnedBackend = await spawnUvicorn();
// Stash the pid on the env so teardown can find it without
// sharing module state (Playwright re-imports the teardown
// file in a fresh module context).
process.env.COSTUDY4GRID_SPAWNED_PID = String(spawnedBackend.pid);
} else {
// eslint-disable-next-line no-console
console.log(
`[globalSetup] real-backend mode — expecting an externally-managed `
+ `backend at ${BACKEND_URL}. Set COSTUDY4GRID_SPAWN_BACKEND=1 to `
+ `auto-spawn uvicorn instead.`,
);
}
await waitForBackend(BACKEND_URL);
// POST /api/config with the small_grid paths. Subsequent specs
// then run against this pre-loaded state.
const configResp = await fetch(`${BACKEND_URL}/api/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(SMALL_GRID_CONFIG),
});
if (!configResp.ok) {
throw new Error(
`POST /api/config failed: ${configResp.status} ${configResp.statusText} — `
+ `${await configResp.text()}`,
);
}
const configBody = await configResp.json() as { status?: string; total_lines_count?: number };
if (configBody.status !== 'success' || (configBody.total_lines_count ?? 0) === 0) {
throw new Error(`POST /api/config returned unexpected body: ${JSON.stringify(configBody)}`);
}
// eslint-disable-next-line no-console
console.log(
`[globalSetup] backend configured against small_grid `
+ `(${configBody.total_lines_count} lines).`,
);
}