fix: impl quic listener

This commit is contained in:
Akash Mondal
2025-06-10 21:40:21 +00:00
committed by lla-dane
parent 446a22b0f0
commit 54b3055eaa
13 changed files with 1687 additions and 150 deletions

View File

@ -0,0 +1,119 @@
from unittest.mock import (
Mock,
)
import pytest
from multiaddr.multiaddr import Multiaddr
from libp2p.crypto.ed25519 import (
create_new_key_pair,
)
from libp2p.peer.id import ID
from libp2p.transport.quic.connection import QUICConnection
from libp2p.transport.quic.exceptions import QUICStreamError
class TestQUICConnection:
"""Test suite for QUIC connection functionality."""
@pytest.fixture
def mock_quic_connection(self):
"""Create mock aioquic QuicConnection."""
mock = Mock()
mock.next_event.return_value = None
mock.datagrams_to_send.return_value = []
mock.get_timer.return_value = None
return mock
@pytest.fixture
def quic_connection(self, mock_quic_connection):
"""Create test QUIC connection."""
private_key = create_new_key_pair().private_key
peer_id = ID.from_pubkey(private_key.get_public_key())
return QUICConnection(
quic_connection=mock_quic_connection,
remote_addr=("127.0.0.1", 4001),
peer_id=peer_id,
local_peer_id=peer_id,
is_initiator=True,
maddr=Multiaddr("/ip4/127.0.0.1/udp/4001/quic"),
transport=Mock(),
)
def test_connection_initialization(self, quic_connection):
"""Test connection initialization."""
assert quic_connection._remote_addr == ("127.0.0.1", 4001)
assert quic_connection.is_initiator is True
assert not quic_connection.is_closed
assert not quic_connection.is_established
assert len(quic_connection._streams) == 0
def test_stream_id_calculation(self):
"""Test stream ID calculation for client/server."""
# Client connection (initiator)
client_conn = QUICConnection(
quic_connection=Mock(),
remote_addr=("127.0.0.1", 4001),
peer_id=None,
local_peer_id=Mock(),
is_initiator=True,
maddr=Multiaddr("/ip4/127.0.0.1/udp/4001/quic"),
transport=Mock(),
)
assert client_conn._next_stream_id == 0 # Client starts with 0
# Server connection (not initiator)
server_conn = QUICConnection(
quic_connection=Mock(),
remote_addr=("127.0.0.1", 4001),
peer_id=None,
local_peer_id=Mock(),
is_initiator=False,
maddr=Multiaddr("/ip4/127.0.0.1/udp/4001/quic"),
transport=Mock(),
)
assert server_conn._next_stream_id == 1 # Server starts with 1
def test_incoming_stream_detection(self, quic_connection):
"""Test incoming stream detection logic."""
# For client (initiator), odd stream IDs are incoming
assert quic_connection._is_incoming_stream(1) is True # Server-initiated
assert quic_connection._is_incoming_stream(0) is False # Client-initiated
assert quic_connection._is_incoming_stream(5) is True # Server-initiated
assert quic_connection._is_incoming_stream(4) is False # Client-initiated
@pytest.mark.trio
async def test_connection_stats(self, quic_connection):
"""Test connection statistics."""
stats = quic_connection.get_stats()
expected_keys = [
"peer_id",
"remote_addr",
"is_initiator",
"is_established",
"is_closed",
"active_streams",
"next_stream_id",
]
for key in expected_keys:
assert key in stats
@pytest.mark.trio
async def test_connection_close(self, quic_connection):
"""Test connection close functionality."""
assert not quic_connection.is_closed
await quic_connection.close()
assert quic_connection.is_closed
@pytest.mark.trio
async def test_stream_operations_on_closed_connection(self, quic_connection):
"""Test stream operations on closed connection."""
await quic_connection.close()
with pytest.raises(QUICStreamError, match="Connection is closed"):
await quic_connection.open_stream()

View File

@ -0,0 +1,171 @@
from unittest.mock import AsyncMock
import pytest
from multiaddr.multiaddr import Multiaddr
import trio
from libp2p.crypto.ed25519 import (
create_new_key_pair,
)
from libp2p.transport.quic.exceptions import (
QUICListenError,
)
from libp2p.transport.quic.listener import QUICListener
from libp2p.transport.quic.transport import (
QUICTransport,
QUICTransportConfig,
)
from libp2p.transport.quic.utils import (
create_quic_multiaddr,
quic_multiaddr_to_endpoint,
)
class TestQUICListener:
"""Test suite for QUIC listener functionality."""
@pytest.fixture
def private_key(self):
"""Generate test private key."""
return create_new_key_pair().private_key
@pytest.fixture
def transport_config(self):
"""Generate test transport configuration."""
return QUICTransportConfig(idle_timeout=10.0)
@pytest.fixture
def transport(self, private_key, transport_config):
"""Create test transport instance."""
return QUICTransport(private_key, transport_config)
@pytest.fixture
def connection_handler(self):
"""Mock connection handler."""
return AsyncMock()
@pytest.fixture
def listener(self, transport, connection_handler):
"""Create test listener."""
return transport.create_listener(connection_handler)
def test_listener_creation(self, transport, connection_handler):
"""Test listener creation."""
listener = transport.create_listener(connection_handler)
assert isinstance(listener, QUICListener)
assert listener._transport == transport
assert listener._handler == connection_handler
assert not listener._listening
assert not listener._closed
@pytest.mark.trio
async def test_listener_invalid_multiaddr(self, listener: QUICListener):
"""Test listener with invalid multiaddr."""
async with trio.open_nursery() as nursery:
invalid_addr = Multiaddr("/ip4/127.0.0.1/tcp/4001")
with pytest.raises(QUICListenError, match="Invalid QUIC multiaddr"):
await listener.listen(invalid_addr, nursery)
@pytest.mark.trio
async def test_listener_basic_lifecycle(self, listener: QUICListener):
"""Test basic listener lifecycle."""
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic") # Port 0 = random
async with trio.open_nursery() as nursery:
# Start listening
success = await listener.listen(listen_addr, nursery)
assert success
assert listener.is_listening()
# Check bound addresses
addrs = listener.get_addrs()
assert len(addrs) == 1
# Check stats
stats = listener.get_stats()
assert stats["is_listening"] is True
assert stats["active_connections"] == 0
assert stats["pending_connections"] == 0
# Close listener
await listener.close()
assert not listener.is_listening()
@pytest.mark.trio
async def test_listener_double_listen(self, listener: QUICListener):
"""Test that double listen raises error."""
listen_addr = create_quic_multiaddr("127.0.0.1", 9001, "/quic")
# The nursery is the outer context
async with trio.open_nursery() as nursery:
# The try/finally is now INSIDE the nursery scope
try:
# The listen method creates the socket and starts background tasks
success = await listener.listen(listen_addr, nursery)
assert success
await trio.sleep(0.01)
addrs = listener.get_addrs()
assert len(addrs) > 0
print("ADDRS 1: ", len(addrs))
print("TEST LOGIC FINISHED")
async with trio.open_nursery() as nursery2:
with pytest.raises(QUICListenError, match="Already listening"):
await listener.listen(listen_addr, nursery2)
finally:
# This block runs BEFORE the 'async with nursery' exits.
print("INNER FINALLY: Closing listener to release socket...")
# This closes the socket and sets self._listening = False,
# which helps the background tasks terminate cleanly.
await listener.close()
print("INNER FINALLY: Listener closed.")
# By the time we get here, the listener and its tasks have been fully
# shut down, allowing the nursery to exit without hanging.
print("TEST COMPLETED SUCCESSFULLY.")
@pytest.mark.trio
async def test_listener_port_binding(self, listener: QUICListener):
"""Test listener port binding and cleanup."""
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
# The nursery is the outer context
async with trio.open_nursery() as nursery:
# The try/finally is now INSIDE the nursery scope
try:
# The listen method creates the socket and starts background tasks
success = await listener.listen(listen_addr, nursery)
assert success
await trio.sleep(0.5)
addrs = listener.get_addrs()
assert len(addrs) > 0
print("TEST LOGIC FINISHED")
finally:
# This block runs BEFORE the 'async with nursery' exits.
print("INNER FINALLY: Closing listener to release socket...")
# This closes the socket and sets self._listening = False,
# which helps the background tasks terminate cleanly.
await listener.close()
print("INNER FINALLY: Listener closed.")
# By the time we get here, the listener and its tasks have been fully
# shut down, allowing the nursery to exit without hanging.
print("TEST COMPLETED SUCCESSFULLY.")
@pytest.mark.trio
async def test_listener_stats_tracking(self, listener):
"""Test listener statistics tracking."""
initial_stats = listener.get_stats()
# All counters should start at 0
assert initial_stats["connections_accepted"] == 0
assert initial_stats["connections_rejected"] == 0
assert initial_stats["bytes_received"] == 0
assert initial_stats["packets_processed"] == 0

View File

@ -7,6 +7,7 @@ import pytest
from libp2p.crypto.ed25519 import (
create_new_key_pair,
)
from libp2p.crypto.keys import PrivateKey
from libp2p.transport.quic.exceptions import (
QUICDialError,
QUICListenError,
@ -23,7 +24,7 @@ class TestQUICTransport:
@pytest.fixture
def private_key(self):
"""Generate test private key."""
return create_new_key_pair()
return create_new_key_pair().private_key
@pytest.fixture
def transport_config(self):
@ -33,7 +34,7 @@ class TestQUICTransport:
)
@pytest.fixture
def transport(self, private_key, transport_config):
def transport(self, private_key: PrivateKey, transport_config: QUICTransportConfig):
"""Create test transport instance."""
return QUICTransport(private_key, transport_config)
@ -47,18 +48,35 @@ class TestQUICTransport:
def test_supported_protocols(self, transport):
"""Test supported protocol identifiers."""
protocols = transport.protocols()
assert "/quic-v1" in protocols
assert "/quic" in protocols # draft-29
# TODO: Update when quic-v1 compatible
# assert "quic-v1" in protocols
assert "quic" in protocols # draft-29
def test_can_dial_quic_addresses(self, transport):
def test_can_dial_quic_addresses(self, transport: QUICTransport):
"""Test multiaddr compatibility checking."""
import multiaddr
# Valid QUIC addresses
valid_addrs = [
multiaddr.Multiaddr("/ip4/127.0.0.1/udp/4001/quic-v1"),
multiaddr.Multiaddr("/ip4/192.168.1.1/udp/8080/quic"),
multiaddr.Multiaddr("/ip6/::1/udp/4001/quic-v1"),
# TODO: Update Multiaddr package to accept quic-v1
multiaddr.Multiaddr(
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
multiaddr.Multiaddr(
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
multiaddr.Multiaddr(
f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
multiaddr.Multiaddr(
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
multiaddr.Multiaddr(
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
multiaddr.Multiaddr(
f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
]
for addr in valid_addrs:
@ -93,7 +111,7 @@ class TestQUICTransport:
await transport.close()
with pytest.raises(QUICDialError, match="Transport is closed"):
await transport.dial(multiaddr.Multiaddr("/ip4/127.0.0.1/udp/4001/quic-v1"))
await transport.dial(multiaddr.Multiaddr("/ip4/127.0.0.1/udp/4001/quic"))
def test_create_listener_closed_transport(self, transport):
"""Test creating listener with closed transport raises error."""

View File

@ -0,0 +1,94 @@
import pytest
from multiaddr.multiaddr import Multiaddr
from libp2p.transport.quic.config import QUICTransportConfig
from libp2p.transport.quic.utils import (
create_quic_multiaddr,
is_quic_multiaddr,
multiaddr_to_quic_version,
quic_multiaddr_to_endpoint,
)
class TestQUICUtils:
"""Test suite for QUIC utility functions."""
def test_is_quic_multiaddr(self):
"""Test QUIC multiaddr validation."""
# Valid QUIC multiaddrs
valid = [
# TODO: Update Multiaddr package to accept quic-v1
Multiaddr(
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
Multiaddr(
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
Multiaddr(
f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_DRAFT29}"
),
Multiaddr(
f"/ip4/127.0.0.1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
Multiaddr(
f"/ip4/192.168.1.1/udp/8080/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
Multiaddr(
f"/ip6/::1/udp/4001/{QUICTransportConfig.PROTOCOL_QUIC_V1}"
),
]
for addr in valid:
assert is_quic_multiaddr(addr)
# Invalid multiaddrs
invalid = [
Multiaddr("/ip4/127.0.0.1/tcp/4001"),
Multiaddr("/ip4/127.0.0.1/udp/4001"),
Multiaddr("/ip4/127.0.0.1/udp/4001/ws"),
]
for addr in invalid:
assert not is_quic_multiaddr(addr)
def test_quic_multiaddr_to_endpoint(self):
"""Test multiaddr to endpoint conversion."""
addr = Multiaddr("/ip4/192.168.1.100/udp/4001/quic")
host, port = quic_multiaddr_to_endpoint(addr)
assert host == "192.168.1.100"
assert port == 4001
# Test IPv6
# TODO: Update Multiaddr project to handle ip6
# addr6 = Multiaddr("/ip6/::1/udp/8080/quic")
# host6, port6 = quic_multiaddr_to_endpoint(addr6)
# assert host6 == "::1"
# assert port6 == 8080
def test_create_quic_multiaddr(self):
"""Test QUIC multiaddr creation."""
# IPv4
addr = create_quic_multiaddr("127.0.0.1", 4001, "/quic")
assert str(addr) == "/ip4/127.0.0.1/udp/4001/quic"
# IPv6
addr6 = create_quic_multiaddr("::1", 8080, "/quic")
assert str(addr6) == "/ip6/::1/udp/8080/quic"
def test_multiaddr_to_quic_version(self):
"""Test QUIC version extraction."""
addr = Multiaddr("/ip4/127.0.0.1/udp/4001/quic")
version = multiaddr_to_quic_version(addr)
assert version in ["quic", "quic-v1"] # Depending on implementation
def test_invalid_multiaddr_operations(self):
"""Test error handling for invalid multiaddrs."""
invalid_addr = Multiaddr("/ip4/127.0.0.1/tcp/4001")
with pytest.raises(ValueError):
quic_multiaddr_to_endpoint(invalid_addr)
with pytest.raises(ValueError):
multiaddr_to_quic_version(invalid_addr)