mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2025-12-31 20:36:24 +00:00
fix: impl quic listener
This commit is contained in:
119
tests/core/transport/quic/test_connection.py
Normal file
119
tests/core/transport/quic/test_connection.py
Normal 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()
|
||||
171
tests/core/transport/quic/test_listener.py
Normal file
171
tests/core/transport/quic/test_listener.py
Normal 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
|
||||
@ -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."""
|
||||
|
||||
94
tests/core/transport/quic/test_utils.py
Normal file
94
tests/core/transport/quic/test_utils.py
Normal 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)
|
||||
Reference in New Issue
Block a user