"""The syslog listener (#5999): lines queued as they arrive, bounded, read in rounds; the listener bound as the settings say, and a port it can't open said, not raised.""" import asyncio import socket from datetime import datetime, timedelta, timezone from types import SimpleNamespace import pytest from plugins.syslog import receiver @pytest.fixture(autouse=True) def fresh(monkeypatch): monkeypatch.setattr(receiver, "LISTENER", receiver.Listener()) monkeypatch.setattr(receiver, "QUEUE", receiver.deque()) def test_the_queue_is_bounded_and_says_what_it_lost(monkeypatch): monkeypatch.setattr(receiver, "MAX_QUEUED", 3) for i in range(5): receiver.take("10.0.0.5", f"<13>line {i}".encode()) assert receiver.LISTENER.received == 5 and receiver.LISTENER.dropped == 2 assert [r.message.text for r in receiver.read_round()] == ["line 2", "line 3", "line 4"] def test_a_round_takes_at_most_its_share_and_leaves_the_rest(): for i in range(5): receiver.take("10.0.0.5", f"<13>line {i}".encode()) first = receiver.read_round(limit=2) assert [r.message.text for r in first] == ["line 0", "line 1"] and len(receiver.QUEUE) == 3 assert first[0].address == "10.0.0.5" def _app(**cfg): return SimpleNamespace(config={"PLUGINS": {"syslog": cfg}}) def _free_port() -> int: with socket.socket() as s: s.bind(("0.0.0.0", 0)) return s.getsockname()[1] def test_it_listens_on_udp_and_tcp_and_takes_what_arrives(): async def go(): port = _free_port() await receiver.ensure_listening(_app(port=port, udp=True, tcp=True)) assert receiver.LISTENER.listening and receiver.LISTENER.error is None udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp.sendto(b"<13>over udp", ("127.0.0.1", port)) udp.close() reader, writer = await asyncio.wait_for(asyncio.open_connection("127.0.0.1", port), 5) writer.write(b"12 <13>over tcp<14>and a line\n") await writer.drain() writer.close() for _ in range(50): if len(receiver.QUEUE) >= 3: break await asyncio.sleep(0.05) texts = sorted(r.message.text for r in receiver.read_round()) await receiver.close() return texts assert asyncio.run(go()) == ["and a line", "over tcp", "over udp"] def test_a_port_it_cannot_open_is_said_and_tried_again_later(): async def go(): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as held: held.bind(("0.0.0.0", 0)) port = held.getsockname()[1] now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc) await receiver.ensure_listening(_app(port=port, udp=True, tcp=False), now) said = receiver.LISTENER.error tried = receiver.LISTENER.tried_at await receiver.ensure_listening(_app(port=port, udp=True, tcp=False), now + timedelta(seconds=10)) assert receiver.LISTENER.tried_at == tried # not retried within the minute return said said = asyncio.run(go()) assert said and said.startswith("Port ") and "UDP" in said def test_a_change_of_port_moves_the_listener(): async def go(): a, b = _free_port(), _free_port() await receiver.ensure_listening(_app(port=a, udp=True, tcp=False)) await receiver.ensure_listening(_app(port=b, udp=True, tcp=False)) port = receiver.LISTENER._udp_transport.get_extra_info("sockname")[1] await receiver.close() return port, b port, b = asyncio.run(go()) assert port == b