Ctrl K
ComunidadGuías…

scripts/run_e2e.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Run Playwright against a temporary, deterministic DiceFrame instance."""

from __future__ import annotations

from contextlib import nullcontext
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from scripts.e2e_fake_hub import running_fake_hub
from scripts.prepare_e2e_data import prepare_e2e_data


FRONTEND = ROOT / "frontend-v2"
HOST = "127.0.0.1"
# 默认 18000;可用 TRPG_E2E_PORT 覆盖,避免与用户正在运行的实例端口冲突。
PORT = int(os.getenv("TRPG_E2E_PORT") or 18000)


def _port_is_open() -> bool:
    with socket.socket() as sock:
        sock.settimeout(0.2)
        return sock.connect_ex((HOST, PORT)) == 0


def _wait_until_ready(process: subprocess.Popen, log_file: Path) -> None:
    health_url = f"http://{HOST}:{PORT}/api/system/update/health"
    for _ in range(60):
        if process.poll() is not None:
            raise RuntimeError(f"DiceFrame exited before E2E startup:\n{log_file.read_text(encoding='utf-8', errors='replace')}")
        try:
            with urllib.request.urlopen(health_url, timeout=1) as response:
                if response.status == 200:
                    return
        except Exception:
            time.sleep(0.25)
    raise RuntimeError(f"DiceFrame did not become ready:\n{log_file.read_text(encoding='utf-8', errors='replace')}")


def _keep_server_log(log_file: Path) -> None:
    # The data dir is temporary; CI sets this so a failing run keeps the log.
    target = os.getenv("DICEFRAME_E2E_SERVER_LOG")
    if target and log_file.is_file():
        Path(target).parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(log_file, target)


def main() -> int:
    if _port_is_open():
        print(f"Port {PORT} is already in use; stop the existing DiceFrame server before E2E.", file=sys.stderr)
        return 2
    npm = shutil.which("npm.cmd") or shutil.which("npm")
    playwright = FRONTEND / "node_modules" / ".bin" / ("playwright.cmd" if os.name == "nt" else "playwright")
    if not npm or not playwright.is_file():
        print("npm or the local Playwright binary was not found; run npm ci first.", file=sys.stderr)
        return 2
    built = subprocess.run([npm, "run", "build"], cwd=FRONTEND, check=False)
    if built.returncode:
        return built.returncode

    # Manual opt-out preserves DICEFRAME_HUB_URL (or the production default).
    hub_context = nullcontext() if os.getenv("DICEFRAME_E2E_REAL_HUB") == "1" else running_fake_hub()
    with hub_context as hub, tempfile.TemporaryDirectory(prefix="diceframe-e2e-") as temp_name:
        data_dir = Path(temp_name) / "data"
        prepare_e2e_data(data_dir)
        log_file = Path(temp_name) / "server.log"
        env = os.environ.copy()
        if hub is not None:
            env["DICEFRAME_HUB_URL"] = hub.base_url
            print(f"E2E Hub: {hub.base_url} (local fake)", flush=True)
        env["TRPG_DATA_DIR"] = str(data_dir)
        env["DICEFRAME_E2E_DATA_DIR"] = str(data_dir)
        env["TRPG_WEB_PORT"] = str(PORT)
        with log_file.open("w", encoding="utf-8") as output:
            server = subprocess.Popen(
                [sys.executable, str(ROOT / "web_server.py")],
                cwd=ROOT,
                env=env,
                stdout=output,
                stderr=subprocess.STDOUT,
            )
            try:
                _wait_until_ready(server, log_file)
                completed = subprocess.run(
                    [str(playwright), "test", *sys.argv[1:]],
                    cwd=FRONTEND,
                    env=env,
                    check=False,
                )
                return completed.returncode
            finally:
                if server.poll() is None:
                    server.terminate()
                    try:
                        server.wait(timeout=10)
                    except subprocess.TimeoutExpired:
                        server.kill()
                        server.wait(timeout=5)
                _keep_server_log(log_file)


if __name__ == "__main__":
    raise SystemExit(main())