"""Tests para jupyter_exec. Cubre: - Que `_notebook_exists` usa GET (regresion del bug 0050: HEAD daba 405). - Que `_create_notebook` no toca el servidor si el notebook ya existe. - 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, patch import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) from python.functions.notebook import jupyter_exec as jx # --------------------------------------------------------------------------- # Tests unitarios (regresion del bug HEAD/GET) # --------------------------------------------------------------------------- def _http_response_mock(body: bytes = b"{}", status: int = 200) -> MagicMock: resp = MagicMock() resp.read.return_value = body resp.__enter__ = lambda self: self resp.__exit__ = lambda self, *a: False resp.status = status return resp def test_notebook_exists_uses_get_not_head(): """Regresion 0050: HEAD devuelve 405 en /api/contents; debe usar GET.""" captured = {} def fake_urlopen(req, timeout): captured["method"] = req.get_method() captured["url"] = req.full_url return _http_response_mock(b'{"name":"x.ipynb"}') with patch.object(jx, "urlopen", side_effect=fake_urlopen): ok = jx._notebook_exists("x.ipynb", "http://srv", "") assert ok is True assert captured["method"] == "GET" assert "content=0" in captured["url"] def test_notebook_exists_returns_false_on_404(): err = urllib.request.HTTPError(url="x", code=404, msg="nope", hdrs=None, fp=None) with patch.object(jx, "urlopen", side_effect=err): assert jx._notebook_exists("x.ipynb", "http://srv", "") is False def test_create_notebook_skips_when_exists(): with patch.object(jx, "_notebook_exists", return_value=True), \ patch.object(jx, "urlopen") as mock_open: jx._create_notebook("x.ipynb", "http://srv", "") mock_open.assert_not_called() def test_new_code_cell_has_required_fields(): cell = jx._new_code_cell("print(42)") assert cell["cell_type"] == "code" assert cell["source"] == "print(42)" assert cell["outputs"] == [] assert cell["execution_count"] is None assert isinstance(cell["id"], str) and len(cell["id"]) > 0 assert cell["metadata"] == {} def test_extract_outputs_handles_streams_and_results(): raw = [ {"output_type": "stream", "name": "stdout", "text": "hola\n"}, {"output_type": "execute_result", "data": {"text/plain": "42"}}, {"output_type": "error", "traceback": ["E1", "E2"]}, ] out = jx._extract_outputs(raw) assert out == ["hola", "42", "E1\nE2"] # --------------------------------------------------------------------------- # E2E (requiere Jupyter Lab corriendo) # --------------------------------------------------------------------------- JUPYTER_VENV_BIN = Path(__file__).resolve().parents[4] / "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_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 levantó a tiempo") yield server_url, workdir proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() def test_e2e_append_executes_and_persists(jupyter_server): server_url, workdir = jupyter_server result = jx.jupyter_append_execute( "notebooks/test.ipynb", "z = 21 * 2; print(z)", server_url=server_url, ) assert result["cell_index"] == 0 assert result["outputs"] == ["42"] nb = json.loads((workdir / "notebooks" / "test.ipynb").read_text()) assert len(nb["cells"]) == 1 assert nb["cells"][0]["execution_count"] == 1 def test_e2e_append_twice_increments_index(jupyter_server): server_url, _ = jupyter_server jx.jupyter_append_execute("notebooks/twice.ipynb", "a = 1", server_url=server_url) r2 = jx.jupyter_append_execute("notebooks/twice.ipynb", "print(a + 1)", server_url=server_url) assert r2["cell_index"] == 1 assert r2["outputs"] == ["2"] def test_e2e_cell_executes_existing(jupyter_server): server_url, _ = jupyter_server jx.jupyter_append_execute("notebooks/cell.ipynb", "v = 10", server_url=server_url) jx.jupyter_append_execute("notebooks/cell.ipynb", "print(v * 5)", server_url=server_url) r = jx.jupyter_execute_cell("notebooks/cell.ipynb", 1, server_url=server_url) assert r["outputs"] == ["50"] def test_e2e_kernel_mode(jupyter_server): server_url, _ = jupyter_server r = jx.jupyter_kernel_execute("print('hello kernel')", server_url=server_url) assert r["status"] == "ok" assert r["outputs"] == ["hello kernel"]