""" Fixtures pytest pour les tests E2E : serveur PHP local + page Playwright. Le serveur de développement PHP (`php -S`) est démarré une fois par session de test sur 127.0.0.1:8000 (ou sur un port libre si 8000 est déjà pris). Les appels aux API distantes (PeerTube, Castopod) passent par le vrai réseau : le premier chargement remplit le cache du site (cache/api), ce qui peut prendre un certain temps. Lancement : /tmp/test-venv/bin/python -m pytest tests/e2e/ """ import os import re import socket import subprocess import tempfile import time import urllib.request from pathlib import Path import pytest from playwright.sync_api import sync_playwright PROJECT_ROOT = Path(__file__).resolve().parents[2] SERVER_HOST = "127.0.0.1" DEFAULT_PORT = 8000 STARTUP_TIMEOUT = 30 # secondes pour que php -S accepte des connexions WARMUP_TIMEOUT = 90 # premier hit : remplit les caches API (Castopod/PeerTube) PAGE_TIMEOUT_MS = 60000 def _port_libre(port): """Retourne True si aucun service n'écoute sur le port donné.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex((SERVER_HOST, port)) != 0 def _trouver_port(): """Utilise le port 8000 si possible, sinon un port libre quelconque.""" if _port_libre(DEFAULT_PORT): return DEFAULT_PORT with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((SERVER_HOST, 0)) return s.getsockname()[1] def lire_constante_config(nom, defaut=None): """ Lit une constante define('NOM', 'valeur') dans la configuration PHP. Cherche dans config.default.php puis dans config.local.php (qui a le dernier mot, comme dans includes/config.php). Ne gère que les valeurs chaînes simples — suffisant pour SITE_NAME, PEERTUBE_URL, etc. """ valeur = defaut for fichier in ("config.default.php", "config.local.php"): chemin = PROJECT_ROOT / "includes" / fichier if not chemin.is_file(): continue texte = chemin.read_text(encoding="utf-8") motif = r"define\(\s*'" + re.escape(nom) + r"'\s*,\s*'((?:[^'\\]|\\.)*)'\s*\)" m = re.search(motif, texte) if m: valeur = m.group(1).replace("\\'", "'").replace("\\\\", "\\") return valeur @pytest.fixture(scope="session") def base_url(): """Démarre `php -S` à la racine du projet et fournit l'URL de base.""" port = _trouver_port() url = f"http://{SERVER_HOST}:{port}" log = tempfile.NamedTemporaryFile( mode="w+", prefix="php-e2e-", suffix=".log", delete=False ) # PHP_CLI_SERVER_WORKERS : le serveur intégré est mono-thread par défaut, # ce qui bloque quand le navigateur demande plusieurs PHP en parallèle. env = dict(os.environ, PHP_CLI_SERVER_WORKERS="4") proc = subprocess.Popen( ["php", "-S", f"{SERVER_HOST}:{port}", "-t", str(PROJECT_ROOT)], cwd=str(PROJECT_ROOT), stdout=log, stderr=subprocess.STDOUT, env=env, ) # Attendre que le serveur accepte des connexions deadline = time.monotonic() + STARTUP_TIMEOUT pret = False while time.monotonic() < deadline: if proc.poll() is not None: log.seek(0) raise RuntimeError( "Le serveur PHP s'est arrêté au démarrage :\n" + log.read() ) try: with socket.create_connection((SERVER_HOST, port), timeout=1): pret = True break except OSError: time.sleep(0.2) if not pret: proc.terminate() raise RuntimeError( f"Le serveur PHP ne répond pas après {STARTUP_TIMEOUT} s" ) # Pré-chauffe les caches (initCategories + appels API distants) try: with urllib.request.urlopen(url + "/index.php", timeout=WARMUP_TIMEOUT) as rep: if rep.status != 200: raise RuntimeError(f"index.php a répondu HTTP {rep.status}") except Exception: proc.terminate() log.seek(0) raise RuntimeError( "Impossible de charger la page d'accueil :\n" + log.read() ) yield url proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() log.close() @pytest.fixture(scope="session") def navigateur(): """Instance Chromium partagée pour toute la session de test.""" with sync_playwright() as p: browser = p.chromium.launch() yield browser browser.close() @pytest.fixture() def page(navigateur): """Nouvelle page vierge pour chaque test (contexte isolé).""" context = navigateur.new_context() context.set_default_timeout(PAGE_TIMEOUT_MS) pg = context.new_page() yield pg context.close() @pytest.fixture(scope="session") def site_name(): """Valeur de la constante SITE_NAME de la configuration du site.""" return lire_constante_config("SITE_NAME") @pytest.fixture(scope="session") def peertube_url(): """Valeur de la constante PEERTUBE_URL de la configuration du site.""" return lire_constante_config("PEERTUBE_URL")