"""Tests para ping_host (CLI `ping`, estilo dict sin excepciones). Sin red: se monkeypatchea ``subprocess.run`` en el namespace del modulo ``ping_host`` para devolver salidas fijas o lanzar excepciones controladas. """ import os import subprocess import sys sys.path.insert(0, os.path.dirname(__file__)) import ping_host as ping_mod from ping_host import ping_host # Salida real de un ping con exito (4/4, 0% loss, linea rtt). RAW_OK = """\ PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. 64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=1.10 ms 64 bytes from 1.1.1.1: icmp_seq=2 ttl=58 time=2.20 ms 64 bytes from 1.1.1.1: icmp_seq=3 ttl=58 time=2.50 ms 64 bytes from 1.1.1.1: icmp_seq=4 ttl=58 time=3.00 ms --- 1.1.1.1 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3004ms rtt min/avg/max/mdev = 1.1/2.2/3.3/0.4 ms """ # Salida real de un host con ICMP filtrado: todo perdido, sin linea rtt. RAW_FILTERED = """\ PING blackhole.example (10.255.255.1) 56(84) bytes of data. --- blackhole.example ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 3070ms """ class _FakeProc: """Stand-in de CompletedProcess: solo expone stdout y returncode.""" def __init__(self, stdout: str, returncode: int = 0): self.stdout = stdout self.returncode = returncode def _patch_run(monkeypatch, *, stdout=None, raises=None): """Sustituye subprocess.run en el modulo ping_host. Si ``raises`` es una excepcion, la lanza al invocarse; en otro caso devuelve un _FakeProc con el stdout dado. """ def fake_run(*args, **kwargs): if raises is not None: raise raises return _FakeProc(stdout) monkeypatch.setattr(ping_mod.subprocess, "run", fake_run) def test_golden_ping_con_exito(monkeypatch): """Ping exitoso: parsea paquetes, perdida 0% y rtt min/avg/max.""" _patch_run(monkeypatch, stdout=RAW_OK) result = ping_host("1.1.1.1") assert result["status"] == "ok" assert result["host"] == "1.1.1.1" assert result["packets_sent"] == 4 assert result["packets_recv"] == 4 assert result["loss_pct"] == 0.0 assert result["rtt_min_ms"] == 1.1 assert result["rtt_avg_ms"] == 2.2 assert result["rtt_max_ms"] == 3.3 assert result["raw"] == RAW_OK def test_edge_host_filtrado(monkeypatch): """Host inalcanzable/filtrado: status ok, 100% loss, rtt None.""" _patch_run(monkeypatch, stdout=RAW_FILTERED) result = ping_host("blackhole.example") assert result["status"] == "ok" assert result["packets_sent"] == 4 assert result["packets_recv"] == 0 assert result["loss_pct"] == 100.0 assert result["rtt_avg_ms"] is None assert result["rtt_min_ms"] is None assert result["rtt_max_ms"] is None def test_error_host_vacio(monkeypatch): """Host en blanco: status error sin invocar subprocess.""" def boom(*args, **kwargs): raise AssertionError("subprocess.run no debe llamarse con host vacio") monkeypatch.setattr(ping_mod.subprocess, "run", boom) result = ping_host(" ") assert result["status"] == "error" assert "vacio" in result["error"] def test_error_binario_ausente(monkeypatch): """ping no en PATH: status error y el mensaje menciona ping.""" _patch_run(monkeypatch, raises=FileNotFoundError()) result = ping_host("1.1.1.1") assert result["status"] == "error" assert "ping" in result["error"] assert result["host"] == "1.1.1.1" def test_error_timeout(monkeypatch): """Timeout duro del subprocess: status error.""" _patch_run( monkeypatch, raises=subprocess.TimeoutExpired(cmd=["ping"], timeout=1), ) result = ping_host("1.1.1.1") assert result["status"] == "error" assert "timeout" in result["error"]