"""E2E tests for terminal_panel demos in primitives_gallery. Lanza primitives_gallery en modo --capture, captura el demo "terminal_panel" como PNG y verifica que la region del terminal tiene fondo oscuro (fix del issue 0132: fondo negro + prompt input). Uso desde la raiz del registry: python/.venv/bin/python3 -m pytest cpp/tests/e2e/test_terminal_panel_e2e.py -v Requisitos: - primitives_gallery compilado (Linux o Windows .exe). - WSL2 con interop habilitado para el path Windows. - Pillow instalado en el venv del registry (python/.venv). En entornos sin GL (CI headless), el binario sale != 0 y el test se skipea automaticamente (SKIP, no FAIL). """ import os import subprocess import sys import tempfile from pathlib import Path import pytest # --------------------------------------------------------------------------- # Helpers de localizacion del binario # --------------------------------------------------------------------------- REGISTRY_ROOT = Path(__file__).resolve().parents[3] # fn_registry/ def _find_binary() -> Path | None: """Devuelve el primer primitives_gallery encontrado (Linux o Windows).""" # Paths fijos conocidos primero. candidates = [ REGISTRY_ROOT / "cpp" / "build" / "apps" / "primitives_gallery" / "primitives_gallery", REGISTRY_ROOT / "cpp" / "build" / "linux" / "apps" / "primitives_gallery" / "primitives_gallery", REGISTRY_ROOT / "cpp" / "build" / "windows" / "apps" / "primitives_gallery" / "primitives_gallery.exe", # Desktop de Windows (deploy anterior) Path("/mnt/c/Users/lucas/Desktop/apps/primitives_gallery/primitives_gallery.exe"), ] for p in candidates: if p.exists(): return p # Busqueda amplia como fallback. for pattern in ("primitives_gallery", "primitives_gallery.exe"): for found in (REGISTRY_ROOT / "cpp" / "build").rglob(pattern): if found.is_file(): return found return None # --------------------------------------------------------------------------- # Fixture: captura PNG del demo terminal_panel # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def terminal_png(tmp_path_factory) -> Path: """Lanza primitives_gallery --capture y devuelve el PNG generado.""" binary = _find_binary() if binary is None: pytest.skip("primitives_gallery binary not found — build it first") out_dir = tmp_path_factory.mktemp("terminal_capture") # En WSL, un .exe Windows necesita invocarse como proceso Windows. # En Linux, se invoca directamente con LIBGL_ALWAYS_SOFTWARE=1. env = os.environ.copy() is_windows_exe = binary.suffix == ".exe" if is_windows_exe: # Convertir el out_dir a path Windows via wslpath. wslpath_result = subprocess.run( ["wslpath", "-w", str(out_dir)], capture_output=True, text=True ) if wslpath_result.returncode != 0: pytest.skip("wslpath not available — can't convert path for Windows exe") win_out_dir = wslpath_result.stdout.strip() cmd = [str(binary), "--capture", win_out_dir] else: env["LIBGL_ALWAYS_SOFTWARE"] = "1" cmd = [str(binary), "--capture", str(out_dir)] result = subprocess.run( cmd, capture_output=True, text=True, env=env, cwd=str(REGISTRY_ROOT), timeout=60, ) if result.returncode != 0: # Sin GL o sin display — skip en lugar de FAIL. pytest.skip( f"primitives_gallery --capture exited {result.returncode} " f"(no GL context?). stdout: {result.stdout[-200:]} " f"stderr: {result.stderr[-200:]}" ) png_path = out_dir / "terminal_panel.png" if not png_path.exists(): pytest.skip(f"terminal_panel.png not generated in {out_dir}. " f"stdout: {result.stdout[-300:]}") return png_path # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- def test_terminal_panel_png_exists(terminal_png: Path): """El PNG del demo terminal_panel debe existir despues del capture.""" assert terminal_png.exists(), f"PNG not found: {terminal_png}" assert terminal_png.stat().st_size > 1000, "PNG sospechosamente pequeño" def test_terminal_panel_not_all_white(terminal_png: Path): """La imagen no debe ser completamente blanca (render vacio).""" try: from PIL import Image except ImportError: pytest.skip("Pillow not installed — run: pip install Pillow") img = Image.open(terminal_png).convert("RGB") px = img.load() w, h = img.size total = w * h white_count = sum( 1 for y in range(h) for x in range(w) if px[x, y][0] > 240 and px[x, y][1] > 240 and px[x, y][2] > 240 # type: ignore[index] ) white_ratio = white_count / total assert white_ratio < 0.95, ( f"Image is {white_ratio:.1%} white — terminal render likely failed. " f"({terminal_png})" ) def test_terminal_panel_dark_background(terminal_png: Path): """La region central del terminal debe ser mayormente oscura (fondo negro fix 0132).""" try: from PIL import Image except ImportError: pytest.skip("Pillow not installed — run: pip install Pillow") img = Image.open(terminal_png).convert("RGB") w, h = img.size # Recortar la region central-inferior (donde vive el scrollback del terminal). # El demo header ocupa ~15% superior; el resto deberia ser el area del terminal. # Ajustar: top=20%, bottom=85%, left=10%, right=90%. left = int(w * 0.10) right = int(w * 0.90) top = int(h * 0.20) bottom = int(h * 0.85) region = img.crop((left, top, right, bottom)) rw, rh = region.size total = rw * rh if total == 0: pytest.skip("Crop region empty — image too small?") rpx = region.load() # Pixel oscuro: todos los canales RGB < 60. dark_count = sum( 1 for y in range(rh) for x in range(rw) if rpx[x, y][0] < 60 and rpx[x, y][1] < 60 and rpx[x, y][2] < 60 # type: ignore[index] ) dark_ratio = dark_count / total assert dark_ratio >= 0.30, ( f"Terminal region has only {dark_ratio:.1%} dark pixels (expected >= 30%). " f"The black background fix (issue 0132) may not be active. " f"Region: ({left},{top})-({right},{bottom}) in {w}x{h} image. " f"({terminal_png})" ) def test_terminal_panel_has_light_text_on_dark(terminal_png: Path): """Debe haber pixels claros (texto/toolbar) sobre fondo oscuro — render activo. En modo --capture el PTY reader es async y puede no entregar output en los primeros frames. Verificamos que al menos la toolbar (Clear/Copy/Reset) y el borde del child tienen pixels no-negros (> 0.3% de la imagen total), lo que confirma que el panel se renderizo. """ try: from PIL import Image except ImportError: pytest.skip("Pillow not installed — run: pip install Pillow") img = Image.open(terminal_png).convert("RGB") pixels = img.load() w, h = img.size total = w * h # Contar pixels con al menos un canal > 60 en toda la imagen. # Incluye la toolbar (botones), bordes, prompt "$ " y cualquier output. light_count = sum( 1 for y in range(h) for x in range(w) if max(pixels[x, y]) > 60 # type: ignore[index] ) light_ratio = light_count / total # Umbral conservador: > 0.3% — basta con que la toolbar sea visible. # En modo interactivo con PTY output el ratio sera mucho mayor (> 5%). assert light_ratio >= 0.003, ( f"Image has only {light_ratio:.2%} non-dark pixels — " f"terminal panel may not be rendering at all. " f"Check that fn_term::render is called and ImGui window is visible. " f"({terminal_png})" )