Files
gpt-load/web/e2e/run-e2e.mjs
T

125 lines
4.3 KiB
JavaScript

import { randomBytes } from 'node:crypto'
import { spawn } from 'node:child_process'
import { access, mkdtemp, readFile, realpath, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { resolveSafeRunDirectory } from './run-directory-safety.mjs'
if (process.platform === 'win32') {
console.error('E2E harness requires a POSIX platform')
process.exit(1)
}
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const repositoryRoot = resolve(webRoot, '..')
const binary = join(repositoryRoot, 'gpt-load')
await access(binary)
const playwrightArgs = process.argv.slice(2)
const projectArgument = playwrightArgs.find((value) => value.startsWith('--project='))
const project = projectArgument?.slice('--project='.length) ?? 'chromium'
if (!(project === 'chromium' || project === 'webkit')) {
console.error(`E2E harness does not support project ${project}`)
process.exit(1)
}
const effectivePlaywrightArgs =
projectArgument === undefined ? [...playwrightArgs, '--project=chromium'] : playwrightArgs
const scenario = (playwrightArgs.find((value) => value.endsWith('.spec.ts')) ?? 'full')
.replace(/^.*\//, '')
.replace(/\.spec\.ts$/, '')
.replace(/[^a-z0-9-]/gi, '-')
const artifactStem = `${project}-serial-${scenario}`
const visualAppPort =
scenario === 'visual-scenarios' ? (process.env.GPT_LOAD_E2E_APP_PORT ?? '43101') : undefined
const runDirectory = await mkdtemp(join(tmpdir(), 'gpt-load-e2e-run-'))
const readyFile = join(runDirectory, `${artifactStem}-ready.json`)
const authKey = `e2e-auth-${randomBytes(24).toString('hex')}`
const server = spawn(process.execPath, [join(webRoot, 'e2e/start-e2e-server.mjs')], {
cwd: webRoot,
env: {
...process.env,
GPT_LOAD_E2E_READY_FILE: readyFile,
GPT_LOAD_E2E_AUTH_KEY: authKey,
GPT_LOAD_E2E_PROJECT: project,
GPT_LOAD_E2E_SCENARIO: scenario,
...(visualAppPort === undefined ? {} : { GPT_LOAD_E2E_APP_PORT: visualAppPort }),
},
stdio: ['ignore', 'inherit', 'inherit'],
})
function waitForExit(child) {
return new Promise((resolveExit) =>
child.once('exit', (code, signal) => resolveExit({ code, signal })),
)
}
const serverExit = waitForExit(server)
let interrupted = false
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
interrupted = true
server.kill(signal)
})
}
async function waitForReady() {
const deadline = Date.now() + 30_000
while (Date.now() < deadline) {
if (server.exitCode !== null || server.signalCode !== null) {
throw new Error('E2E harness exited before readiness')
}
try {
const ready = JSON.parse(await readFile(readyFile, 'utf8'))
for (const field of ['base_url', 'upstream_url']) {
const url = new URL(ready[field])
if (url.hostname !== '127.0.0.1' || Number(url.port) <= 0) {
throw new Error('E2E harness returned an unsafe endpoint')
}
}
return ready
} catch (error) {
if (error instanceof SyntaxError || error?.code === 'ENOENT') {
await new Promise((resolveWait) => setTimeout(resolveWait, 100))
continue
}
throw error
}
}
throw new Error('E2E harness ready artifact timed out')
}
let exitCode = 1
try {
const ready = await waitForReady()
const cli = join(webRoot, 'node_modules/@playwright/test/cli.js')
const playwright = spawn(
process.execPath,
[cli, 'test', ...effectivePlaywrightArgs, '--config', 'playwright.config.ts'],
{
cwd: webRoot,
env: {
...process.env,
GPT_LOAD_E2E_BASE_URL: ready.base_url,
GPT_LOAD_E2E_UPSTREAM_URL: ready.upstream_url,
GPT_LOAD_E2E_AUTH_KEY: authKey,
GPT_LOAD_E2E_ARTIFACT_DIR: `test-results/${artifactStem}`,
},
stdio: 'inherit',
},
)
const result = await waitForExit(playwright)
exitCode = result.code ?? 1
} finally {
if (server.exitCode === null && server.signalCode === null) server.kill('SIGTERM')
await serverExit
const safeRunDirectory = await resolveSafeRunDirectory(readyFile)
if (safeRunDirectory !== (await realpath(runDirectory))) {
throw new Error('Refusing to remove an unsafe E2E run directory')
}
await rm(safeRunDirectory, { recursive: true, force: true })
}
process.exit(interrupted ? 130 : exitCode)