PAMPAr-Coder / scripts /test_inference_integration.py
lucas-mella's picture
feat: upload PAMPAr-Coder code and documentation
a2d6a0d verified
Raw
History Blame Contribute Delete
13 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: BUSL-1.1
"""
test_inference_integration.py — Prueba fehaciente del servidor pampar.inference.
Lanza el servidor como subprocess real, carga el checkpoint, y verifica que:
1. El proceso arranca y emite READY
2. Responde a una petición de inferencia con código Python real
3. El código generado al menos tiene sintaxis válida
4. Responde a una petición de boot con AGENTS.md bien formado
5. Maneja JSON inválido sin caer
Uso:
python scripts/test_inference_integration.py
python scripts/test_inference_integration.py --checkpoint checkpoints/v3_sft_v8.pt
python scripts/test_inference_integration.py --device cpu --timeout 60
"""
from __future__ import annotations
import argparse
import ast
import json
import subprocess
import sys
import time
from pathlib import Path
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).parent.parent
DEFAULT_CHECKPOINT = PROJECT_ROOT / "checkpoints" / "v3_sft_v8.pt"
VENV_PYTHON = PROJECT_ROOT.parent / ".venv" / "Scripts" / "python.exe"
def _python_has_torch(python_path: str) -> bool:
"""Verifica rápido si ese intérprete tiene torch disponible."""
try:
result = subprocess.run(
[python_path, "-c", "import torch"],
capture_output=True, timeout=10,
)
return result.returncode == 0
except Exception:
return False
def find_python() -> str:
"""Devuelve el primer intérprete Python que tenga torch instalado."""
candidates = [
str(VENV_PYTHON) if VENV_PYTHON.exists() else None,
sys.executable,
]
for candidate in candidates:
if candidate and _python_has_torch(candidate):
return candidate
# Último recurso: sys.executable aunque no tenga torch (el servidor fallará con mensaje claro)
return sys.executable
# ---------------------------------------------------------------------------
# Color helpers
# ---------------------------------------------------------------------------
def ok(msg: str) -> None:
print(f" ✅ {msg}")
def fail(msg: str) -> None:
print(f" ❌ {msg}")
def section(title: str) -> None:
print(f"\n{'─'*50}")
print(f" {title}")
print(f"{'─'*50}")
# ---------------------------------------------------------------------------
# Server wrapper
# ---------------------------------------------------------------------------
class InferenceServer:
"""Prozess-wrapper para pampar.inference."""
def __init__(self, checkpoint: str, device: str, timeout_ready: int = 90):
python = find_python()
cmd = [python, "-m", "pampar.inference", "--checkpoint", checkpoint, "--device", device]
import os as _os
_env = _os.environ.copy()
_env["PYTHONIOENCODING"] = "utf-8"
self.proc = subprocess.Popen(
cmd,
cwd=str(PROJECT_ROOT),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
bufsize=1,
env=_env,
)
self._wait_ready(timeout_ready)
def _wait_ready(self, timeout: int) -> None:
print(f" Esperando READY (timeout={timeout}s)…", end="", flush=True)
deadline = time.time() + timeout
stderr_lines: list[str] = []
while time.time() < deadline:
line = self.proc.stderr.readline()
if not line:
if self.proc.poll() is not None:
raise RuntimeError(
f"Proceso terminó antes de READY (código {self.proc.returncode}).\n"
+ "".join(stderr_lines)
)
time.sleep(0.1)
continue
stderr_lines.append(line)
print(".", end="", flush=True)
if "READY" in line:
print(" listo.")
# Leer también la línea {"type":"ready"} de stdout
self.proc.stdout.readline()
return
raise TimeoutError("El servidor no emitió READY a tiempo.\n" + "".join(stderr_lines[-20:]))
def send(self, msg: dict) -> dict:
line = json.dumps(msg, ensure_ascii=False) + "\n"
self.proc.stdin.write(line)
self.proc.stdin.flush()
resp_line = self.proc.stdout.readline()
if not resp_line:
raise EOFError("El servidor cerró stdout inesperadamente.")
return json.loads(resp_line)
def send_raw(self, raw: str) -> str:
self.proc.stdin.write(raw + "\n")
self.proc.stdin.flush()
return self.proc.stdout.readline()
def close(self) -> None:
try:
self.proc.stdin.close()
except Exception:
pass
self.proc.wait(timeout=5)
# ---------------------------------------------------------------------------
# Casos de prueba
# ---------------------------------------------------------------------------
PRUEBA_INFER = {
"type": "infer",
"prompt": (
"### Problem:\n"
"Write a Python function `suma(a, b)` that returns the sum of two numbers.\n"
"### Solution:\n"
),
"max_tokens": 80,
"temperature": 0.1,
}
PRUEBA_INFER_ALGO_REAL = {
"type": "infer",
"prompt": (
"### Problem:\n"
"Write a Python function `es_par(n)` that returns True if n is even, False otherwise.\n"
"### Solution:\n"
),
"max_tokens": 256,
"temperature": 0.1,
}
def run_tests(checkpoint: str, device: str, timeout: int) -> int:
"""Ejecuta todos los tests. Devuelve número de fallos."""
fallos = 0
section("Iniciando servidor de inferencia")
try:
server = InferenceServer(checkpoint=checkpoint, device=device, timeout_ready=timeout)
except Exception as exc:
fail(f"No se pudo iniciar el servidor: {exc}")
return 1
# ------------------------------------------------------------------
# Test 1: responde a inferencia básica
# ------------------------------------------------------------------
section("Test 1: Respuesta a petición de inferencia")
try:
resp = server.send(PRUEBA_INFER)
if resp.get("type") == "infer_ok":
texto = resp.get("text", "").strip()
if texto:
ok(f"Texto generado ({len(texto)} chars): {repr(texto[:80])}")
else:
fail("infer_ok pero text está vacío")
fallos += 1
else:
fail(f"Respuesta inesperada: {resp}")
fallos += 1
except Exception as exc:
fail(f"Excepción: {exc}")
fallos += 1
# ------------------------------------------------------------------
# Test 2: sintaxis válida en el código generado
# ------------------------------------------------------------------
section("Test 2: Código generado tiene sintaxis Python válida")
try:
resp = server.send(PRUEBA_INFER_ALGO_REAL)
texto = resp.get("text", "")
# Limpiamos markdown y truncamos en stop markers
texto_limpio = texto.replace("```python", "").replace("```", "").strip()
if "###" in texto_limpio:
texto_limpio = texto_limpio[:texto_limpio.index("###")].rstrip()
# Intentar parsear progresivamente — quitar líneas del final hasta que sea válido
lines = texto_limpio.split("\n")
parsed = False
while lines:
candidate = "\n".join(lines).rstrip()
if candidate:
try:
ast.parse(candidate)
texto_limpio = candidate
parsed = True
break
except SyntaxError:
lines.pop()
else:
break
if parsed:
ok(f"Sintaxis válida: {repr(texto_limpio[:100])}")
else:
try:
ast.parse(texto_limpio)
ok(f"Sintaxis válida: {repr(texto_limpio[:100])}")
except SyntaxError as se:
fail(f"SyntaxError en código generado: {se}\nCódigo: {repr(texto_limpio[:200])}")
fallos += 1
except Exception as exc:
fail(f"Excepción: {exc}")
fallos += 1
# ------------------------------------------------------------------
# Test 3: boot genera AGENTS.md
# ------------------------------------------------------------------
section("Test 3: Boot genera AGENTS.md")
try:
resp = server.send({"type": "boot", "workspace": str(PROJECT_ROOT)})
if resp.get("type") == "boot_ok":
md = resp.get("agents_md", "")
checks = [
("## Quick Reference" in md, "Sección Quick Reference"),
("## Boot protocol" in md, "Sección Boot protocol"),
(len(md) > 200, "Contenido suficiente (>200 chars)"),
]
for passed, label in checks:
if passed:
ok(label)
else:
fail(label)
fallos += 1
else:
fail(f"Respuesta inesperada: {resp}")
fallos += 1
except Exception as exc:
fail(f"Excepción: {exc}")
fallos += 1
# ------------------------------------------------------------------
# Test 4: prompt vacío → error (no crash)
# ------------------------------------------------------------------
section("Test 4: Prompt vacío → error controlado")
try:
resp = server.send({"type": "infer", "prompt": ""})
if resp.get("type") == "error":
ok(f"Error controlado: {resp.get('message', '')[:60]}")
else:
fail(f"Se esperaba error, se recibió: {resp}")
fallos += 1
except Exception as exc:
fail(f"Excepción: {exc}")
fallos += 1
# ------------------------------------------------------------------
# Test 5: JSON inválido no baja el servidor
# ------------------------------------------------------------------
section("Test 5: JSON inválido no mata el servidor")
try:
raw = server.send_raw("esto no es json {{{{")
if raw.strip():
resp = json.loads(raw)
if resp.get("type") == "error":
ok("El servidor respondió error y sigue vivo")
else:
ok(f"El servidor respondió (tipo={resp.get('type')}) y sigue vivo")
else:
fail("No hubo respuesta al JSON inválido")
fallos += 1
# Verificar que el servidor sigue respondiendo
resp2 = server.send({"type": "infer", "prompt": "### Problem:\nhi\n### Solution:\n", "max_tokens": 20})
if resp2.get("type") == "infer_ok":
ok("Servidor sigue respondiendo después del JSON inválido")
else:
fail(f"El servidor dejó de responder: {resp2}")
fallos += 1
except Exception as exc:
fail(f"Excepción: {exc}")
fallos += 1
# ------------------------------------------------------------------
# Resumen
# ------------------------------------------------------------------
server.close()
section("RESUMEN")
if fallos == 0:
print(f" ✅ TODOS LOS TESTS PASARON (5/5)")
else:
print(f" ❌ {fallos} test(s) fallaron")
return fallos
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Test de integración del servidor pampar.inference")
parser.add_argument(
"--checkpoint",
default=str(DEFAULT_CHECKPOINT),
help=f"Ruta al checkpoint .pt (default: {DEFAULT_CHECKPOINT})",
)
parser.add_argument(
"--device", default="auto", choices=["auto", "cpu", "cuda"],
help="Dispositivo de inferencia"
)
parser.add_argument(
"--timeout", type=int, default=90,
help="Segundos máximos para esperar READY del servidor"
)
args = parser.parse_args()
if not Path(args.checkpoint).exists():
print(f"ERROR: Checkpoint no encontrado: {args.checkpoint}")
print(f" Checkpoints disponibles:")
for pt in sorted((PROJECT_ROOT / "checkpoints").glob("*.pt")):
print(f" {pt.name}")
sys.exit(1)
fallos = run_tests(args.checkpoint, args.device, args.timeout)
sys.exit(0 if fallos == 0 else 1)
if __name__ == "__main__":
main()