"""EPMD client tests. Unit tests run against recorded byte streams captured from a real EPMD talking to a real named Erlang node (``erl -sname testnode``). The integration test only runs if an EPMD is reachable on localhost and a node is registered under the name we ask for. """ from __future__ import annotations import shutil import socket import subprocess import time import pytest from erldistpy.epmd import ( DEFAULT_PORT, EpmdError, EpmdInfo, build_port_please2_req, lookup, parse_port2_resp, ) # Captured from a real EPMD answering for `erl -sname testnode`. # port = 36205, node_type = 77 (normal), proto = 0 (tcp/ipv4), # hi_ver = 6, lo_ver = 5, name = "testnode", extra = empty. PORT2_RESP_OK = bytes.fromhex("77008d6d4d00000600050008746573746e6f64650000") # Captured from EPMD when the asked-for node is not registered. PORT2_RESP_NOT_FOUND = bytes.fromhex("7701") def test_build_request_known_shape(): req = build_port_please2_req("testnode") # 2-byte length prefix (BE) + tag 122 + name assert req[:2] == b"\x00\x09" # 9 bytes follow assert req[2] == 122 # PORT_PLEASE2_REQ assert req[3:] == b"testnode" def test_build_request_rejects_fully_qualified_name(): with pytest.raises(ValueError): build_port_please2_req("testnode@somehost") def test_parse_port2_resp_ok(): info = parse_port2_resp(PORT2_RESP_OK) assert info == EpmdInfo( name="testnode", port=36205, node_type=77, protocol=0, highest_version=6, lowest_version=5, extra=b"", ) def test_parse_port2_resp_not_found_returns_none(): assert parse_port2_resp(PORT2_RESP_NOT_FOUND) is None def test_parse_port2_resp_wrong_tag(): with pytest.raises(EpmdError, match="unexpected response tag"): parse_port2_resp(b"\x00\x00") def test_parse_port2_resp_truncated(): with pytest.raises(EpmdError, match="truncated|too short"): parse_port2_resp(b"\x77\x00\x00") def test_lookup_rejects_fully_qualified_name(): with pytest.raises(ValueError): lookup("testnode@somehost") def test_lookup_unreachable_host(): # Reserved port unlikely to be open with pytest.raises(EpmdError, match="unreachable"): lookup("any", host="127.0.0.1", port=1, timeout=0.5) # -------------------------------------------------------------------------- # Integration — requires a live EPMD + a named Erlang node # -------------------------------------------------------------------------- def _epmd_running() -> bool: try: with socket.create_connection(("localhost", DEFAULT_PORT), timeout=0.5): return True except OSError: return False @pytest.fixture(scope="module") def named_erlang_node(): """Spawn `erl -sname testnode`, register with EPMD, tear down after.""" if not shutil.which("erl"): pytest.skip("erl not installed") if not _epmd_running(): pytest.skip("EPMD not running on localhost") proc = subprocess.Popen( ["erl", "-sname", "erldistpy_itest", "-noshell", "-eval", "timer:sleep(infinity)."], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # Wait for the node to register with EPMD for _ in range(40): time.sleep(0.1) info = lookup("erldistpy_itest", timeout=0.5) if info is not None: break else: proc.terminate() proc.wait(timeout=2) pytest.skip("erl node failed to register with EPMD") yield "erldistpy_itest" proc.terminate() try: proc.wait(timeout=2) except subprocess.TimeoutExpired: proc.kill() def test_live_lookup_returns_info(named_erlang_node): info = lookup(named_erlang_node) assert info is not None assert info.name == named_erlang_node assert info.port > 0 assert info.node_type == 77 assert info.protocol == 0 # OTP 23+ speaks at least version 6 assert info.highest_version >= 5 def test_live_lookup_unknown_node_returns_none(named_erlang_node): assert lookup("definitely_not_a_real_node_qzxw") is None