chore: auto-commit (43 archivos)

- .mcp.json
- bash/functions/infra/write_mcp_jupyter_config.md
- bash/functions/infra/write_mcp_jupyter_config.sh
- cpp/CMakeLists.txt
- cpp/apps/chart_demo
- cpp/apps/shaders_lab
- cpp/functions/gfx/gl_framebuffer.cpp
- cpp/functions/gfx/gl_framebuffer.h
- cpp/functions/gfx/gl_framebuffer.md
- cpp/functions/gfx/mesh_gpu.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:28:47 +02:00
parent fec8ebd4ec
commit fce88032ca
44 changed files with 3924 additions and 64 deletions
@@ -0,0 +1,319 @@
"""Tests para jupyter_run_cells.
Cubre:
- Validacion de indices (rango y tipo de celda).
- Comportamiento stop_on_error (True/False).
- E2E contra un Jupyter Lab vivo si esta disponible (skip si no).
"""
from __future__ import annotations
import json
import os
import socket
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
from python.functions.notebook import jupyter_exec as jx
from python.functions.notebook import jupyter_run_cells as jrc
# ---------------------------------------------------------------------------
# Helpers para mocks
# ---------------------------------------------------------------------------
def _make_nb(cells: list[dict]) -> dict:
"""Construye un dict de notebook minimo con las celdas dadas."""
return {
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {},
"cells": cells,
}
def _code_cell(source: str) -> dict:
return {
"cell_type": "code",
"source": source,
"outputs": [],
"execution_count": None,
"metadata": {},
"id": "test-id",
}
def _markdown_cell(source: str) -> dict:
return {
"cell_type": "markdown",
"source": source,
"metadata": {},
"id": "md-id",
}
def _kernel_result(outputs: list[dict], execution_count: int = 1) -> dict:
return {"outputs": outputs, "execution_count": execution_count, "status": "ok"}
def _stream_output(text: str) -> dict:
return {"output_type": "stream", "name": "stdout", "text": text}
def _error_output(ename: str = "ValueError", traceback: list[str] | None = None) -> dict:
return {
"output_type": "error",
"ename": ename,
"evalue": "bad value",
"traceback": traceback or [f"{ename}: bad value"],
}
class _FakeKernel:
"""Simula KernelClient con una lista de resultados predefinidos."""
def __init__(self, results: list[dict]):
self._results = iter(results)
def execute(self, source: str, **kwargs) -> dict:
return next(self._results)
def __enter__(self):
return self
def __exit__(self, *args):
pass
# ---------------------------------------------------------------------------
# Tests unitarios
# ---------------------------------------------------------------------------
def _patch_infra(nb: dict, kernel: _FakeKernel):
"""Context managers que parchean _ensure_session, _get/put_notebook y KernelClient."""
file_node = {"content": nb}
return [
patch.object(jrc, "_ensure_session", return_value="kernel-abc"),
patch.object(jrc, "_get_notebook_content", return_value=file_node),
patch.object(jrc, "_put_notebook_content"),
patch("python.functions.notebook.jupyter_run_cells.KernelClient", return_value=kernel),
]
def test_run_cells_single_cell_returns_output():
nb = _make_nb([_code_cell("print(42)")])
fake_kernel = _FakeKernel([_kernel_result([_stream_output("42\n")], execution_count=1)])
with (
patch.object(jrc, "_ensure_session", return_value="k1"),
patch.object(jrc, "_get_notebook_content", return_value={"content": nb}),
patch.object(jrc, "_put_notebook_content") as mock_put,
patch("python.functions.notebook.jupyter_run_cells.KernelClient", return_value=fake_kernel),
):
result = jrc.jupyter_run_cells(
"nb.ipynb", [0], server_url="http://localhost:8888", token=""
)
assert result["kernel_id"] == "k1"
assert result["stopped_at"] is None
assert len(result["executed"]) == 1
entry = result["executed"][0]
assert entry["cell_index"] == 0
assert entry["execution_count"] == 1
assert entry["outputs"] == ["42"]
assert entry["error"] is None
assert entry["duration_s"] >= 0
mock_put.assert_called_once()
def test_run_cells_stops_on_error_by_default():
nb = _make_nb([_code_cell("x=1"), _code_cell("bad"), _code_cell("ok")])
fake_kernel = _FakeKernel([
_kernel_result([_stream_output("1")], 1),
_kernel_result([_error_output("RuntimeError", ["RuntimeError: bad"])], 2),
# la celda 2 nunca deberia ejecutarse
])
with (
patch.object(jrc, "_ensure_session", return_value="k2"),
patch.object(jrc, "_get_notebook_content", return_value={"content": nb}),
patch.object(jrc, "_put_notebook_content"),
patch("python.functions.notebook.jupyter_run_cells.KernelClient", return_value=fake_kernel),
):
result = jrc.jupyter_run_cells("nb.ipynb", [0, 1, 2], stop_on_error=True)
assert result["stopped_at"] == 1
assert len(result["executed"]) == 2
assert result["executed"][1]["error"] is not None
assert "RuntimeError" in result["executed"][1]["error"]
def test_run_cells_no_stop_on_error_continues():
nb = _make_nb([_code_cell("bad"), _code_cell("print('after')")])
fake_kernel = _FakeKernel([
_kernel_result([_error_output()], 1),
_kernel_result([_stream_output("after")], 2),
])
with (
patch.object(jrc, "_ensure_session", return_value="k3"),
patch.object(jrc, "_get_notebook_content", return_value={"content": nb}),
patch.object(jrc, "_put_notebook_content"),
patch("python.functions.notebook.jupyter_run_cells.KernelClient", return_value=fake_kernel),
):
result = jrc.jupyter_run_cells("nb.ipynb", [0, 1], stop_on_error=False)
assert result["stopped_at"] is None
assert len(result["executed"]) == 2
assert result["executed"][0]["error"] is not None
assert result["executed"][1]["outputs"] == ["after"]
def test_run_cells_invalid_index_raises():
nb = _make_nb([_code_cell("x=1")])
with (
patch.object(jrc, "_ensure_session", return_value="k4"),
patch.object(jrc, "_get_notebook_content", return_value={"content": nb}),
patch.object(jrc, "_put_notebook_content"),
patch("python.functions.notebook.jupyter_run_cells.KernelClient"),
):
with pytest.raises(IndexError, match="fuera de rango"):
jrc.jupyter_run_cells("nb.ipynb", [5])
def test_run_cells_non_code_cell_raises():
nb = _make_nb([_markdown_cell("# titulo")])
with (
patch.object(jrc, "_ensure_session", return_value="k5"),
patch.object(jrc, "_get_notebook_content", return_value={"content": nb}),
patch.object(jrc, "_put_notebook_content"),
patch("python.functions.notebook.jupyter_run_cells.KernelClient"),
):
with pytest.raises(ValueError, match="no es de codigo"):
jrc.jupyter_run_cells("nb.ipynb", [0])
# ---------------------------------------------------------------------------
# E2E (requiere Jupyter Lab corriendo)
# ---------------------------------------------------------------------------
JUPYTER_VENV_BIN = Path("/home/lucas/fn_registry/analysis/pruebas_jupyter/.venv/bin")
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def _wait_http(url: str, timeout: float = 10.0) -> bool:
end = time.time() + timeout
while time.time() < end:
try:
with urllib.request.urlopen(url, timeout=1):
return True
except OSError:
time.sleep(0.3)
return False
@pytest.fixture(scope="module")
def jupyter_server(tmp_path_factory):
"""Arranca un Jupyter Lab en puerto libre. Skip si las deps no estan."""
if not (JUPYTER_VENV_BIN / "jupyter-lab").exists():
pytest.skip("Jupyter Lab no disponible en pruebas_jupyter venv")
workdir = tmp_path_factory.mktemp("jupyter_run_cells_e2e")
(workdir / "notebooks").mkdir()
port = _free_port()
proc = subprocess.Popen(
[
str(JUPYTER_VENV_BIN / "jupyter-lab"),
f"--port={port}",
"--no-browser",
"--ServerApp.token=",
"--ServerApp.password=",
"--ServerApp.disable_check_xsrf=True",
f"--ServerApp.root_dir={workdir}",
"--collaborative",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
server_url = f"http://localhost:{port}"
if not _wait_http(f"{server_url}/api"):
proc.terminate()
pytest.skip("Jupyter Lab no levanto a tiempo")
yield server_url, workdir
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
def test_e2e_run_cells_batch(jupyter_server):
"""Ejecuta 3 celdas en lote y verifica outputs y persistence."""
server_url, workdir = jupyter_server
# Prepara el notebook con 3 celdas via jupyter_exec
jx.jupyter_append_execute("notebooks/batch.ipynb", "x = 10", server_url=server_url)
jx.jupyter_append_execute("notebooks/batch.ipynb", "y = x * 3", server_url=server_url)
jx.jupyter_append_execute("notebooks/batch.ipynb", "print(x + y)", server_url=server_url)
# Ejecuta el lote
result = jrc.jupyter_run_cells(
"notebooks/batch.ipynb",
[0, 1, 2],
server_url=server_url,
)
assert result["stopped_at"] is None
assert len(result["executed"]) == 3
assert result["executed"][2]["outputs"] == ["40"]
assert result["total_duration_s"] > 0
assert result["kernel_id"] != ""
# Verifica persistencia en disco
nb = json.loads((workdir / "notebooks" / "batch.ipynb").read_text())
assert nb["cells"][2]["execution_count"] is not None
def test_e2e_run_cells_stop_on_error(jupyter_server):
"""Verifica que stop_on_error detiene el lote en la celda con error."""
server_url, workdir = jupyter_server
jx.jupyter_append_execute("notebooks/stopper.ipynb", "a = 1", server_url=server_url)
jx.jupyter_append_execute("notebooks/stopper.ipynb", "raise ValueError('boom')", server_url=server_url)
jx.jupyter_append_execute("notebooks/stopper.ipynb", "print('no llego')", server_url=server_url)
result = jrc.jupyter_run_cells(
"notebooks/stopper.ipynb",
[0, 1, 2],
server_url=server_url,
stop_on_error=True,
)
assert result["stopped_at"] == 1
assert len(result["executed"]) == 2
assert result["executed"][1]["error"] is not None
assert "ValueError" in result["executed"][1]["error"]
# La celda 2 no debe aparecer en executed
assert all(e["cell_index"] != 2 for e in result["executed"])