161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
"""
|
|||
|
|
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")
|