fix: accept stream on server side

This commit is contained in:
Akash Mondal
2025-07-02 12:40:21 +00:00
committed by lla-dane
parent 6c45862fe9
commit c15c317514
9 changed files with 1444 additions and 1743 deletions

View File

@ -0,0 +1,415 @@
"""
Basic QUIC Echo Test
Simple test to verify the basic QUIC flow:
1. Client connects to server
2. Client sends data
3. Server receives data and echoes back
4. Client receives the echo
This test focuses on identifying where the accept_stream issue occurs.
"""
import logging
import pytest
import trio
from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.peer.id import ID
from libp2p.transport.quic.config import QUICTransportConfig
from libp2p.transport.quic.connection import QUICConnection
from libp2p.transport.quic.transport import QUICTransport
from libp2p.transport.quic.utils import create_quic_multiaddr
# Set up logging to see what's happening
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TestBasicQUICFlow:
"""Test basic QUIC client-server communication flow."""
@pytest.fixture
def server_key(self):
"""Generate server key pair."""
return create_new_key_pair()
@pytest.fixture
def client_key(self):
"""Generate client key pair."""
return create_new_key_pair()
@pytest.fixture
def server_config(self):
"""Simple server configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
max_concurrent_streams=10,
max_connections=5,
)
@pytest.fixture
def client_config(self):
"""Simple client configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
max_concurrent_streams=5,
)
@pytest.mark.trio
async def test_basic_echo_flow(
self, server_key, client_key, server_config, client_config
):
"""Test basic client-server echo flow with detailed logging."""
print("\n=== BASIC QUIC ECHO TEST ===")
# Create server components
server_transport = QUICTransport(server_key.private_key, server_config)
server_peer_id = ID.from_pubkey(server_key.public_key)
# Track test state
server_received_data = None
server_connection_established = False
echo_sent = False
async def echo_server_handler(connection: QUICConnection) -> None:
"""Simple echo server handler with detailed logging."""
nonlocal server_received_data, server_connection_established, echo_sent
print("🔗 SERVER: Connection handler called")
server_connection_established = True
try:
print("📡 SERVER: Waiting for incoming stream...")
# Accept stream with timeout and detailed logging
print("📡 SERVER: Calling accept_stream...")
stream = await connection.accept_stream(timeout=5.0)
if stream is None:
print("❌ SERVER: accept_stream returned None")
return
print(f"✅ SERVER: Stream accepted! Stream ID: {stream.stream_id}")
# Read data from the stream
print("📖 SERVER: Reading data from stream...")
server_data = await stream.read(1024)
if not server_data:
print("❌ SERVER: No data received from stream")
return
server_received_data = server_data.decode("utf-8", errors="ignore")
print(f"📨 SERVER: Received data: '{server_received_data}'")
# Echo the data back
echo_message = f"ECHO: {server_received_data}"
print(f"📤 SERVER: Sending echo: '{echo_message}'")
await stream.write(echo_message.encode())
echo_sent = True
print("✅ SERVER: Echo sent successfully")
# Close the stream
await stream.close()
print("🔒 SERVER: Stream closed")
except Exception as e:
print(f"❌ SERVER: Error in handler: {e}")
import traceback
traceback.print_exc()
# Create listener
listener = server_transport.create_listener(echo_server_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
# Variables to track client state
client_connected = False
client_sent_data = False
client_received_echo = None
try:
print("🚀 Starting server...")
async with trio.open_nursery() as nursery:
# Start server listener
success = await listener.listen(listen_addr, nursery)
assert success, "Failed to start server listener"
# Get server address
server_addrs = listener.get_addrs()
server_addr = server_addrs[0]
print(f"🔧 SERVER: Listening on {server_addr}")
# Give server a moment to be ready
await trio.sleep(0.1)
print("🚀 Starting client...")
# Create client transport
client_transport = QUICTransport(client_key.private_key, client_config)
try:
# Connect to server
print(f"📞 CLIENT: Connecting to {server_addr}")
connection = await client_transport.dial(
server_addr, peer_id=server_peer_id, nursery=nursery
)
client_connected = True
print("✅ CLIENT: Connected to server")
# Open a stream
print("📤 CLIENT: Opening stream...")
stream = await connection.open_stream()
print(f"✅ CLIENT: Stream opened with ID: {stream.stream_id}")
# Send test data
test_message = "Hello QUIC Server!"
print(f"📨 CLIENT: Sending message: '{test_message}'")
await stream.write(test_message.encode())
client_sent_data = True
print("✅ CLIENT: Message sent")
# Read echo response
print("📖 CLIENT: Waiting for echo response...")
response_data = await stream.read(1024)
if response_data:
client_received_echo = response_data.decode(
"utf-8", errors="ignore"
)
print(f"📬 CLIENT: Received echo: '{client_received_echo}'")
else:
print("❌ CLIENT: No echo response received")
print("🔒 CLIENT: Closing connection")
await connection.close()
print("🔒 CLIENT: Connection closed")
print("🔒 CLIENT: Closing transport")
await client_transport.close()
print("🔒 CLIENT: Transport closed")
except Exception as e:
print(f"❌ CLIENT: Error: {e}")
import traceback
traceback.print_exc()
finally:
await client_transport.close()
print("🔒 CLIENT: Transport closed")
# Give everything time to complete
await trio.sleep(0.5)
# Cancel nursery to stop server
nursery.cancel_scope.cancel()
finally:
# Cleanup
if not listener._closed:
await listener.close()
await server_transport.close()
# Verify the flow worked
print("\n📊 TEST RESULTS:")
print(f" Server connection established: {server_connection_established}")
print(f" Client connected: {client_connected}")
print(f" Client sent data: {client_sent_data}")
print(f" Server received data: '{server_received_data}'")
print(f" Echo sent by server: {echo_sent}")
print(f" Client received echo: '{client_received_echo}'")
# Test assertions
assert server_connection_established, "Server connection handler was not called"
assert client_connected, "Client failed to connect"
assert client_sent_data, "Client failed to send data"
assert server_received_data == "Hello QUIC Server!", (
f"Server received wrong data: '{server_received_data}'"
)
assert echo_sent, "Server failed to send echo"
assert client_received_echo == "ECHO: Hello QUIC Server!", (
f"Client received wrong echo: '{client_received_echo}'"
)
print("✅ BASIC ECHO TEST PASSED!")
@pytest.mark.trio
async def test_server_accept_stream_timeout(
self, server_key, client_key, server_config, client_config
):
"""Test what happens when server accept_stream times out."""
print("\n=== TESTING SERVER ACCEPT_STREAM TIMEOUT ===")
server_transport = QUICTransport(server_key.private_key, server_config)
server_peer_id = ID.from_pubkey(server_key.public_key)
accept_stream_called = False
accept_stream_timeout = False
async def timeout_test_handler(connection: QUICConnection) -> None:
"""Handler that tests accept_stream timeout."""
nonlocal accept_stream_called, accept_stream_timeout
print("🔗 SERVER: Connection established, testing accept_stream timeout")
accept_stream_called = True
try:
print("📡 SERVER: Calling accept_stream with 2 second timeout...")
stream = await connection.accept_stream(timeout=2.0)
print(f"✅ SERVER: accept_stream returned: {stream}")
except Exception as e:
print(f"⏰ SERVER: accept_stream timed out or failed: {e}")
accept_stream_timeout = True
listener = server_transport.create_listener(timeout_test_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
client_connected = False
try:
async with trio.open_nursery() as nursery:
# Start server
success = await listener.listen(listen_addr, nursery)
assert success
server_addr = listener.get_addrs()[0]
print(f"🔧 SERVER: Listening on {server_addr}")
# Create client but DON'T open a stream
client_transport = QUICTransport(client_key.private_key, client_config)
try:
print("📞 CLIENT: Connecting (but NOT opening stream)...")
connection = await client_transport.dial(
server_addr, peer_id=server_peer_id, nursery=nursery
)
client_connected = True
print("✅ CLIENT: Connected (no stream opened)")
# Wait for server timeout
await trio.sleep(3.0)
await connection.close()
print("🔒 CLIENT: Connection closed")
finally:
await client_transport.close()
nursery.cancel_scope.cancel()
finally:
await listener.close()
await server_transport.close()
print("\n📊 TIMEOUT TEST RESULTS:")
print(f" Client connected: {client_connected}")
print(f" accept_stream called: {accept_stream_called}")
print(f" accept_stream timeout: {accept_stream_timeout}")
assert client_connected, "Client should have connected"
assert accept_stream_called, "accept_stream should have been called"
assert accept_stream_timeout, (
"accept_stream should have timed out when no stream was opened"
)
print("✅ TIMEOUT TEST PASSED!")
@pytest.mark.trio
async def test_debug_accept_stream_hanging(
self, server_key, client_key, server_config, client_config
):
"""Debug test to see exactly where accept_stream might be hanging."""
print("\n=== DEBUGGING ACCEPT_STREAM HANGING ===")
server_transport = QUICTransport(server_key.private_key, server_config)
server_peer_id = ID.from_pubkey(server_key.public_key)
async def debug_handler(connection: QUICConnection) -> None:
"""Handler with extensive debugging."""
print(f"🔗 SERVER: Handler called for connection {id(connection)} ")
print(f" Connection closed: {connection.is_closed}")
print(f" Connection started: {connection._started}")
print(f" Connection established: {connection._established}")
try:
print("📡 SERVER: About to call accept_stream...")
print(f" Accept queue length: {len(connection._stream_accept_queue)}")
print(
f" Accept event set: {connection._stream_accept_event.is_set()}"
)
# Use a short timeout to avoid hanging the test
with trio.move_on_after(3.0) as cancel_scope:
stream = await connection.accept_stream()
if stream:
print(f"✅ SERVER: Got stream {stream.stream_id}")
else:
print("❌ SERVER: accept_stream returned None")
if cancel_scope.cancelled_caught:
print("⏰ SERVER: accept_stream cancelled due to timeout")
except Exception as e:
print(f"❌ SERVER: Exception in accept_stream: {e}")
import traceback
traceback.print_exc()
listener = server_transport.create_listener(debug_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
try:
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
server_addr = listener.get_addrs()[0]
print(f"🔧 SERVER: Listening on {server_addr}")
# Create client and connect
client_transport = QUICTransport(client_key.private_key, client_config)
try:
print("📞 CLIENT: Connecting...")
connection = await client_transport.dial(
server_addr, peer_id=server_peer_id, nursery=nursery
)
print("✅ CLIENT: Connected")
# Open stream after a short delay
await trio.sleep(0.1)
print("📤 CLIENT: Opening stream...")
stream = await connection.open_stream()
print(f"📤 CLIENT: Stream {stream.stream_id} opened")
# Send some data
await stream.write(b"test data")
print("📨 CLIENT: Data sent")
# Give server time to process
await trio.sleep(1.0)
# Cleanup
await stream.close()
await connection.close()
print("🔒 CLIENT: Cleaned up")
finally:
await client_transport.close()
await trio.sleep(0.5)
nursery.cancel_scope.cancel()
finally:
await listener.close()
await server_transport.close()
print("✅ DEBUG TEST COMPLETED!")

View File

@ -295,7 +295,10 @@ class TestQUICConnection:
mock_verify.assert_called_once()
@pytest.mark.trio
async def test_connection_connect_timeout(self, quic_connection: QUICConnection):
@pytest.mark.slow
async def test_connection_connect_timeout(
self, quic_connection: QUICConnection
) -> None:
"""Test connection establishment timeout."""
quic_connection._started = True
# Don't set connected event to simulate timeout
@ -330,7 +333,7 @@ class TestQUICConnection:
# Error handling tests
@pytest.mark.trio
async def test_connection_error_handling(self, quic_connection):
async def test_connection_error_handling(self, quic_connection) -> None:
"""Test connection error handling."""
error = Exception("Test error")
@ -343,7 +346,7 @@ class TestQUICConnection:
# Statistics and monitoring tests
@pytest.mark.trio
async def test_connection_stats_enhanced(self, quic_connection):
async def test_connection_stats_enhanced(self, quic_connection) -> None:
"""Test enhanced connection statistics."""
quic_connection._started = True
@ -370,7 +373,7 @@ class TestQUICConnection:
assert stats["inbound_streams"] == 0
@pytest.mark.trio
async def test_get_active_streams(self, quic_connection):
async def test_get_active_streams(self, quic_connection) -> None:
"""Test getting active streams."""
quic_connection._started = True
@ -385,7 +388,7 @@ class TestQUICConnection:
assert stream2 in active_streams
@pytest.mark.trio
async def test_get_streams_by_protocol(self, quic_connection):
async def test_get_streams_by_protocol(self, quic_connection) -> None:
"""Test getting streams by protocol."""
quic_connection._started = True
@ -407,7 +410,9 @@ class TestQUICConnection:
# Enhanced close tests
@pytest.mark.trio
async def test_connection_close_enhanced(self, quic_connection: QUICConnection):
async def test_connection_close_enhanced(
self, quic_connection: QUICConnection
) -> None:
"""Test enhanced connection close with stream cleanup."""
quic_connection._started = True
@ -423,7 +428,9 @@ class TestQUICConnection:
# Concurrent operations tests
@pytest.mark.trio
async def test_concurrent_stream_operations(self, quic_connection):
async def test_concurrent_stream_operations(
self, quic_connection: QUICConnection
) -> None:
"""Test concurrent stream operations."""
quic_connection._started = True
@ -444,16 +451,16 @@ class TestQUICConnection:
# Connection properties tests
def test_connection_properties(self, quic_connection):
def test_connection_properties(self, quic_connection: QUICConnection) -> None:
"""Test connection property accessors."""
assert quic_connection.multiaddr() == quic_connection._maddr
assert quic_connection.local_peer_id() == quic_connection._local_peer_id
assert quic_connection.remote_peer_id() == quic_connection._peer_id
assert quic_connection.remote_peer_id() == quic_connection._remote_peer_id
# IRawConnection interface tests
@pytest.mark.trio
async def test_raw_connection_write(self, quic_connection):
async def test_raw_connection_write(self, quic_connection: QUICConnection) -> None:
"""Test raw connection write interface."""
quic_connection._started = True
@ -468,26 +475,16 @@ class TestQUICConnection:
mock_stream.close_write.assert_called_once()
@pytest.mark.trio
async def test_raw_connection_read_not_implemented(self, quic_connection):
async def test_raw_connection_read_not_implemented(
self, quic_connection: QUICConnection
) -> None:
"""Test raw connection read raises NotImplementedError."""
with pytest.raises(NotImplementedError, match="Use muxed connection interface"):
with pytest.raises(NotImplementedError):
await quic_connection.read()
# String representation tests
def test_connection_string_representation(self, quic_connection):
"""Test connection string representations."""
repr_str = repr(quic_connection)
str_str = str(quic_connection)
assert "QUICConnection" in repr_str
assert str(quic_connection._peer_id) in repr_str
assert str(quic_connection._remote_addr) in repr_str
assert str(quic_connection._peer_id) in str_str
# Mock verification helpers
def test_mock_resource_scope_functionality(self, mock_resource_scope):
def test_mock_resource_scope_functionality(self, mock_resource_scope) -> None:
"""Test mock resource scope works correctly."""
assert mock_resource_scope.memory_reserved == 0

File diff suppressed because it is too large Load Diff

View File

@ -1,765 +1,323 @@
"""
Integration tests for QUIC transport that test actual networking.
These tests require network access and test real socket operations.
Basic QUIC Echo Test
Simple test to verify the basic QUIC flow:
1. Client connects to server
2. Client sends data
3. Server receives data and echoes back
4. Client receives the echo
This test focuses on identifying where the accept_stream issue occurs.
"""
import logging
import random
import socket
import time
import pytest
import trio
from libp2p.crypto.ed25519 import create_new_key_pair
from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.peer.id import ID
from libp2p.transport.quic.config import QUICTransportConfig
from libp2p.transport.quic.connection import QUICConnection
from libp2p.transport.quic.transport import QUICTransport
from libp2p.transport.quic.utils import create_quic_multiaddr
# Set up logging to see what's happening
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TestQUICNetworking:
"""Integration tests that use actual networking."""
@pytest.fixture
def server_config(self):
"""Server configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
max_concurrent_streams=100,
)
@pytest.fixture
def client_config(self):
"""Client configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
)
class TestBasicQUICFlow:
"""Test basic QUIC client-server communication flow."""
@pytest.fixture
def server_key(self):
"""Generate server key pair."""
return create_new_key_pair().private_key
return create_new_key_pair()
@pytest.fixture
def client_key(self):
"""Generate client key pair."""
return create_new_key_pair().private_key
@pytest.mark.trio
async def test_listener_binding_real_socket(self, server_key, server_config):
"""Test that listener can bind to real socket."""
transport = QUICTransport(server_key, server_config)
async def connection_handler(connection):
logger.info(f"Received connection: {connection}")
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async with trio.open_nursery() as nursery:
try:
success = await listener.listen(listen_addr, nursery)
assert success
# Verify we got a real port
addrs = listener.get_addrs()
assert len(addrs) == 1
# Port should be non-zero (was assigned)
from libp2p.transport.quic.utils import quic_multiaddr_to_endpoint
host, port = quic_multiaddr_to_endpoint(addrs[0])
assert host == "127.0.0.1"
assert port > 0
logger.info(f"Listener bound to {host}:{port}")
# Listener should be active
assert listener.is_listening()
# Test basic stats
stats = listener.get_stats()
assert stats["active_connections"] == 0
assert stats["pending_connections"] == 0
# Close listener
await listener.close()
assert not listener.is_listening()
finally:
await transport.close()
@pytest.mark.trio
async def test_multiple_listeners_different_ports(self, server_key, server_config):
"""Test multiple listeners on different ports."""
transport = QUICTransport(server_key, server_config)
async def connection_handler(connection):
pass
listeners = []
bound_ports = []
# Create multiple listeners
for i in range(3):
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
try:
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
# Get bound port
addrs = listener.get_addrs()
from libp2p.transport.quic.utils import quic_multiaddr_to_endpoint
host, port = quic_multiaddr_to_endpoint(addrs[0])
bound_ports.append(port)
listeners.append(listener)
logger.info(f"Listener {i} bound to port {port}")
nursery.cancel_scope.cancel()
finally:
await listener.close()
# All ports should be different
assert len(set(bound_ports)) == len(bound_ports)
@pytest.mark.trio
async def test_port_already_in_use(self, server_key, server_config):
"""Test handling of port already in use."""
transport1 = QUICTransport(server_key, server_config)
transport2 = QUICTransport(server_key, server_config)
async def connection_handler(connection):
pass
listener1 = transport1.create_listener(connection_handler)
listener2 = transport2.create_listener(connection_handler)
# Bind first listener to a specific port
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async with trio.open_nursery() as nursery:
success1 = await listener1.listen(listen_addr, nursery)
assert success1
# Get the actual bound port
addrs = listener1.get_addrs()
from libp2p.transport.quic.utils import quic_multiaddr_to_endpoint
host, port = quic_multiaddr_to_endpoint(addrs[0])
# Try to bind second listener to same port
# Should fail or get different port
same_port_addr = create_quic_multiaddr("127.0.0.1", port, "/quic")
# This might either fail or succeed with SO_REUSEPORT
# The exact behavior depends on the system
try:
success2 = await listener2.listen(same_port_addr, nursery)
if success2:
# If it succeeds, verify different behavior
logger.info("Second listener bound successfully (SO_REUSEPORT)")
except Exception as e:
logger.info(f"Second listener failed as expected: {e}")
await listener1.close()
await listener2.close()
await transport1.close()
await transport2.close()
@pytest.mark.trio
async def test_listener_connection_tracking(self, server_key, server_config):
"""Test that listener properly tracks connection state."""
transport = QUICTransport(server_key, server_config)
received_connections = []
async def connection_handler(connection):
received_connections.append(connection)
logger.info(f"Handler received connection: {connection}")
# Keep connection alive briefly
await trio.sleep(0.1)
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
# Initially no connections
stats = listener.get_stats()
assert stats["active_connections"] == 0
assert stats["pending_connections"] == 0
# Simulate some packet processing
await trio.sleep(0.1)
# Verify listener is still healthy
assert listener.is_listening()
await listener.close()
await transport.close()
@pytest.mark.trio
async def test_listener_error_recovery(self, server_key, server_config):
"""Test listener error handling and recovery."""
transport = QUICTransport(server_key, server_config)
# Handler that raises an exception
async def failing_handler(connection):
raise ValueError("Simulated handler error")
listener = transport.create_listener(failing_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
try:
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
# Even with failing handler, listener should remain stable
await trio.sleep(0.1)
assert listener.is_listening()
# Test complete, stop listening
nursery.cancel_scope.cancel()
finally:
await listener.close()
await transport.close()
@pytest.mark.trio
async def test_transport_resource_cleanup_v1(self, server_key, server_config):
"""Test with single parent nursery managing all listeners."""
transport = QUICTransport(server_key, server_config)
async def connection_handler(connection):
pass
listeners = []
try:
async with trio.open_nursery() as parent_nursery:
# Start all listeners in parallel within the same nursery
for i in range(3):
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
listeners.append(listener)
parent_nursery.start_soon(
listener.listen, listen_addr, parent_nursery
)
# Give listeners time to start
await trio.sleep(0.2)
# Verify all listeners are active
for i, listener in enumerate(listeners):
assert listener.is_listening()
# Close transport should close all listeners
await transport.close()
# The nursery will exit cleanly because listeners are closed
finally:
# Cleanup verification outside nursery
assert transport._closed
assert len(transport._listeners) == 0
# All listeners should be closed
for listener in listeners:
assert not listener.is_listening()
@pytest.mark.trio
async def test_concurrent_listener_operations(self, server_key, server_config):
"""Test concurrent listener operations."""
transport = QUICTransport(server_key, server_config)
async def connection_handler(connection):
await trio.sleep(0.01) # Simulate some work
async def create_and_run_listener(listener_id):
"""Create, run, and close a listener."""
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
logger.info(f"Listener {listener_id} started")
# Run for a short time
await trio.sleep(0.1)
await listener.close()
logger.info(f"Listener {listener_id} closed")
try:
# Run multiple listeners concurrently
async with trio.open_nursery() as nursery:
for i in range(5):
nursery.start_soon(create_and_run_listener, i)
finally:
await transport.close()
class TestQUICConcurrency:
"""Fixed tests with proper nursery management."""
@pytest.fixture
def server_key(self):
"""Generate server key pair."""
return create_new_key_pair().private_key
return create_new_key_pair()
@pytest.fixture
def server_config(self):
"""Server configuration."""
"""Simple server configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
max_concurrent_streams=100,
max_concurrent_streams=10,
max_connections=5,
)
@pytest.fixture
def client_config(self):
"""Simple client configuration."""
return QUICTransportConfig(
idle_timeout=10.0,
connection_timeout=5.0,
max_concurrent_streams=5,
)
@pytest.mark.trio
async def test_concurrent_listener_operations(self, server_key, server_config):
"""Test concurrent listener operations - FIXED VERSION."""
transport = QUICTransport(server_key, server_config)
async def test_basic_echo_flow(
self, server_key, client_key, server_config, client_config
):
"""Test basic client-server echo flow with detailed logging."""
print("\n=== BASIC QUIC ECHO TEST ===")
async def connection_handler(connection):
await trio.sleep(0.01) # Simulate some work
# Create server components
server_transport = QUICTransport(server_key.private_key, server_config)
server_peer_id = ID.from_pubkey(server_key.public_key)
listeners = []
# Track test state
server_received_data = None
server_connection_established = False
echo_sent = False
async def create_and_run_listener(listener_id):
"""Create and run a listener - fixed to avoid deadlock."""
listener = transport.create_listener(connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
listeners.append(listener)
async def echo_server_handler(connection: QUICConnection) -> None:
"""Simple echo server handler with detailed logging."""
nonlocal server_received_data, server_connection_established, echo_sent
print("🔗 SERVER: Connection handler called")
server_connection_established = True
try:
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
print("📡 SERVER: Waiting for incoming stream...")
logger.info(f"Listener {listener_id} started")
# Accept stream with timeout and detailed logging
print("📡 SERVER: Calling accept_stream...")
stream = await connection.accept_stream(timeout=5.0)
# Run for a short time
await trio.sleep(0.1)
if stream is None:
print("❌ SERVER: accept_stream returned None")
return
# Close INSIDE the nursery scope to allow clean exit
await listener.close()
logger.info(f"Listener {listener_id} closed")
print(f"✅ SERVER: Stream accepted! Stream ID: {stream.stream_id}")
# Read data from the stream
print("📖 SERVER: Reading data from stream...")
server_data = await stream.read(1024)
if not server_data:
print("❌ SERVER: No data received from stream")
return
server_received_data = server_data.decode("utf-8", errors="ignore")
print(f"📨 SERVER: Received data: '{server_received_data}'")
# Echo the data back
echo_message = f"ECHO: {server_received_data}"
print(f"📤 SERVER: Sending echo: '{echo_message}'")
await stream.write(echo_message.encode())
echo_sent = True
print("✅ SERVER: Echo sent successfully")
# Close the stream
await stream.close()
print("🔒 SERVER: Stream closed")
except Exception as e:
logger.error(f"Listener {listener_id} error: {e}")
if not listener._closed:
await listener.close()
raise
print(f"❌ SERVER: Error in handler: {e}")
import traceback
try:
# Run multiple listeners concurrently
async with trio.open_nursery() as nursery:
for i in range(5):
nursery.start_soon(create_and_run_listener, i)
traceback.print_exc()
# Verify all listeners were created and closed properly
assert len(listeners) == 5
for listener in listeners:
assert not listener.is_listening() # Should all be closed
finally:
await transport.close()
@pytest.mark.trio
@pytest.mark.slow
async def test_listener_under_simulated_load(self, server_key, server_config):
"""REAL load test with actual packet simulation."""
print("=== REAL LOAD TEST ===")
config = QUICTransportConfig(
idle_timeout=30.0,
connection_timeout=10.0,
max_concurrent_streams=1000,
max_connections=500,
)
transport = QUICTransport(server_key, config)
connection_count = 0
async def connection_handler(connection):
nonlocal connection_count
# TODO: Remove type ignore when pyrefly fixes nonlocal bug
connection_count += 1 # type: ignore
print(f"Real connection established: {connection_count}")
# Simulate connection work
await trio.sleep(0.01)
listener = transport.create_listener(connection_handler)
# Create listener
listener = server_transport.create_listener(echo_server_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async def generate_udp_traffic(target_host, target_port, num_packets=100):
"""Generate fake UDP traffic to simulate load."""
print(
f"Generating {num_packets} UDP packets to {target_host}:{target_port}"
)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
for i in range(num_packets):
# Send random UDP packets
# (Won't be valid QUIC, but will exercise packet handler)
fake_packet = (
f"FAKE_PACKET_{i}_{random.randint(1000, 9999)}".encode()
)
sock.sendto(fake_packet, (target_host, int(target_port)))
# Small delay between packets
await trio.sleep(0.001)
if i % 20 == 0:
print(f"Sent {i + 1}/{num_packets} packets")
except Exception as e:
print(f"Error sending packets: {e}")
finally:
sock.close()
print(f"Finished sending {num_packets} packets")
# Variables to track client state
client_connected = False
client_sent_data = False
client_received_echo = None
try:
print("🚀 Starting server...")
async with trio.open_nursery() as nursery:
# Start server listener
success = await listener.listen(listen_addr, nursery)
assert success
assert success, "Failed to start server listener"
# Get the actual bound port
bound_addrs = listener.get_addrs()
bound_addr = bound_addrs[0]
print(bound_addr)
host, port = (
bound_addr.value_for_protocol("ip4"),
bound_addr.value_for_protocol("udp"),
)
# Get server address
server_addrs = listener.get_addrs()
server_addr = server_addrs[0]
print(f"🔧 SERVER: Listening on {server_addr}")
print(f"Listener bound to {host}:{port}")
# Give server a moment to be ready
await trio.sleep(0.1)
# Start load generation
nursery.start_soon(generate_udp_traffic, host, port, 50)
print("🚀 Starting client...")
# Let the load test run
start_time = time.time()
await trio.sleep(2.0) # Let traffic flow for 2 seconds
end_time = time.time()
# Create client transport
client_transport = QUICTransport(client_key.private_key, client_config)
# Check that listener handled the load
stats = listener.get_stats()
print(f"Final stats: {stats}")
# Should have received packets (even if they're invalid QUIC)
assert stats["packets_processed"] > 0
assert stats["bytes_received"] > 0
duration = end_time - start_time
print(f"Load test ran for {duration:.2f}s")
print(f"Processed {stats['packets_processed']} packets")
print(f"Received {stats['bytes_received']} bytes")
await listener.close()
finally:
if not listener._closed:
await listener.close()
await transport.close()
class TestQUICRealWorldScenarios:
"""Test real-world usage scenarios - FIXED VERSIONS."""
@pytest.mark.trio
async def test_echo_server_pattern(self):
"""Test a basic echo server pattern - FIXED VERSION."""
server_key = create_new_key_pair().private_key
config = QUICTransportConfig(idle_timeout=5.0)
transport = QUICTransport(server_key, config)
echo_data = []
async def echo_connection_handler(connection):
"""Echo server that handles one connection."""
logger.info(f"Echo server got connection: {connection}")
async def stream_handler(stream):
try:
# Read data and echo it back
while True:
data = await stream.read(1024)
if not data:
break
# Connect to server
print(f"📞 CLIENT: Connecting to {server_addr}")
connection = await client_transport.dial(
server_addr, peer_id=server_peer_id, nursery=nursery
)
client_connected = True
print("✅ CLIENT: Connected to server")
echo_data.append(data)
await stream.write(b"ECHO: " + data)
# Open a stream
print("📤 CLIENT: Opening stream...")
stream = await connection.open_stream()
print(f"✅ CLIENT: Stream opened with ID: {stream.stream_id}")
# Send test data
test_message = "Hello QUIC Server!"
print(f"📨 CLIENT: Sending message: '{test_message}'")
await stream.write(test_message.encode())
client_sent_data = True
print("✅ CLIENT: Message sent")
# Read echo response
print("📖 CLIENT: Waiting for echo response...")
response_data = await stream.read(1024)
if response_data:
client_received_echo = response_data.decode(
"utf-8", errors="ignore"
)
print(f"📬 CLIENT: Received echo: '{client_received_echo}'")
else:
print("❌ CLIENT: No echo response received")
print("🔒 CLIENT: Closing connection")
await connection.close()
print("🔒 CLIENT: Connection closed")
print("🔒 CLIENT: Closing transport")
await client_transport.close()
print("🔒 CLIENT: Transport closed")
except Exception as e:
logger.error(f"Stream error: {e}")
print(f"❌ CLIENT: Error: {e}")
import traceback
traceback.print_exc()
finally:
await stream.close()
await client_transport.close()
print("🔒 CLIENT: Transport closed")
connection.set_stream_handler(stream_handler)
# Keep connection alive until closed
while not connection.is_closed:
await trio.sleep(0.1)
listener = transport.create_listener(echo_connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
try:
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
# Let server initialize
await trio.sleep(0.1)
# Verify server is ready
assert listener.is_listening()
# Run server for a bit
# Give everything time to complete
await trio.sleep(0.5)
# Close inside nursery for clean exit
await listener.close()
# Cancel nursery to stop server
nursery.cancel_scope.cancel()
finally:
# Ensure cleanup
# Cleanup
if not listener._closed:
await listener.close()
await transport.close()
await server_transport.close()
# Verify the flow worked
print("\n📊 TEST RESULTS:")
print(f" Server connection established: {server_connection_established}")
print(f" Client connected: {client_connected}")
print(f" Client sent data: {client_sent_data}")
print(f" Server received data: '{server_received_data}'")
print(f" Echo sent by server: {echo_sent}")
print(f" Client received echo: '{client_received_echo}'")
# Test assertions
assert server_connection_established, "Server connection handler was not called"
assert client_connected, "Client failed to connect"
assert client_sent_data, "Client failed to send data"
assert server_received_data == "Hello QUIC Server!", (
f"Server received wrong data: '{server_received_data}'"
)
assert echo_sent, "Server failed to send echo"
assert client_received_echo == "ECHO: Hello QUIC Server!", (
f"Client received wrong echo: '{client_received_echo}'"
)
print("✅ BASIC ECHO TEST PASSED!")
@pytest.mark.trio
async def test_connection_lifecycle_monitoring(self):
"""Test monitoring connection lifecycle events - FIXED VERSION."""
server_key = create_new_key_pair().private_key
config = QUICTransportConfig(idle_timeout=5.0)
transport = QUICTransport(server_key, config)
async def test_server_accept_stream_timeout(
self, server_key, client_key, server_config, client_config
):
"""Test what happens when server accept_stream times out."""
print("\n=== TESTING SERVER ACCEPT_STREAM TIMEOUT ===")
lifecycle_events = []
server_transport = QUICTransport(server_key.private_key, server_config)
server_peer_id = ID.from_pubkey(server_key.public_key)
async def monitoring_handler(connection):
lifecycle_events.append(("connection_started", connection.get_stats()))
accept_stream_called = False
accept_stream_timeout = False
async def timeout_test_handler(connection: QUICConnection) -> None:
"""Handler that tests accept_stream timeout."""
nonlocal accept_stream_called, accept_stream_timeout
print("🔗 SERVER: Connection established, testing accept_stream timeout")
accept_stream_called = True
try:
# Monitor connection
while not connection.is_closed:
stats = connection.get_stats()
lifecycle_events.append(("connection_stats", stats))
await trio.sleep(0.1)
print("📡 SERVER: Calling accept_stream with 2 second timeout...")
stream = await connection.accept_stream(timeout=2.0)
print(f"✅ SERVER: accept_stream returned: {stream}")
except Exception as e:
lifecycle_events.append(("connection_error", str(e)))
finally:
lifecycle_events.append(("connection_ended", connection.get_stats()))
print(f"⏰ SERVER: accept_stream timed out or failed: {e}")
accept_stream_timeout = True
listener = transport.create_listener(monitoring_handler)
listener = server_transport.create_listener(timeout_test_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
client_connected = False
try:
async with trio.open_nursery() as nursery:
# Start server
success = await listener.listen(listen_addr, nursery)
assert success
# Run monitoring for a bit
await trio.sleep(0.5)
server_addr = listener.get_addrs()[0]
print(f"🔧 SERVER: Listening on {server_addr}")
# Check that monitoring infrastructure is working
assert listener.is_listening()
# Create client but DON'T open a stream
client_transport = QUICTransport(client_key.private_key, client_config)
# Close inside nursery
await listener.close()
try:
print("📞 CLIENT: Connecting (but NOT opening stream)...")
connection = await client_transport.dial(
server_addr, peer_id=server_peer_id, nursery=nursery
)
client_connected = True
print("✅ CLIENT: Connected (no stream opened)")
# Wait for server timeout
await trio.sleep(3.0)
await connection.close()
print("🔒 CLIENT: Connection closed")
finally:
await client_transport.close()
nursery.cancel_scope.cancel()
finally:
# Ensure cleanup
if not listener._closed:
await listener.close()
await transport.close()
await listener.close()
await server_transport.close()
# Should have some lifecycle events from setup
logger.info(f"Recorded {len(lifecycle_events)} lifecycle events")
print("\n📊 TIMEOUT TEST RESULTS:")
print(f" Client connected: {client_connected}")
print(f" accept_stream called: {accept_stream_called}")
print(f" accept_stream timeout: {accept_stream_timeout}")
@pytest.mark.trio
async def test_multi_listener_echo_servers(self):
"""Test multiple echo servers running in parallel."""
server_key = create_new_key_pair().private_key
config = QUICTransportConfig(idle_timeout=5.0)
transport = QUICTransport(server_key, config)
assert client_connected, "Client should have connected"
assert accept_stream_called, "accept_stream should have been called"
assert accept_stream_timeout, (
"accept_stream should have timed out when no stream was opened"
)
all_echo_data = {}
listeners = []
async def create_echo_server(server_id):
"""Create and run one echo server."""
echo_data = []
all_echo_data[server_id] = echo_data
async def echo_handler(connection):
logger.info(f"Echo server {server_id} got connection")
async def stream_handler(stream):
try:
while True:
data = await stream.read(1024)
if not data:
break
echo_data.append(data)
await stream.write(f"ECHO-{server_id}: ".encode() + data)
except Exception as e:
logger.error(f"Stream error in server {server_id}: {e}")
finally:
await stream.close()
connection.set_stream_handler(stream_handler)
while not connection.is_closed:
await trio.sleep(0.1)
listener = transport.create_listener(echo_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
listeners.append(listener)
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
logger.info(f"Echo server {server_id} started")
# Run for a bit
await trio.sleep(0.3)
# Close this server
await listener.close()
logger.info(f"Echo server {server_id} closed")
try:
# Run multiple echo servers in parallel
async with trio.open_nursery() as nursery:
for i in range(3):
nursery.start_soon(create_echo_server, i)
# Verify all servers ran
assert len(listeners) == 3
assert len(all_echo_data) == 3
for listener in listeners:
assert not listener.is_listening() # Should all be closed
finally:
await transport.close()
@pytest.mark.trio
async def test_graceful_shutdown_sequence(self):
"""Test graceful shutdown of multiple components."""
server_key = create_new_key_pair().private_key
config = QUICTransportConfig(idle_timeout=5.0)
transport = QUICTransport(server_key, config)
shutdown_events = []
listeners = []
async def tracked_connection_handler(connection):
"""Connection handler that tracks shutdown."""
try:
while not connection.is_closed:
await trio.sleep(0.1)
finally:
shutdown_events.append(f"connection_closed_{id(connection)}")
async def create_tracked_listener(listener_id):
"""Create a listener that tracks its lifecycle."""
try:
listener = transport.create_listener(tracked_connection_handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
listeners.append(listener)
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
shutdown_events.append(f"listener_{listener_id}_started")
# Run for a bit
await trio.sleep(0.2)
# Graceful close
await listener.close()
shutdown_events.append(f"listener_{listener_id}_closed")
except Exception as e:
shutdown_events.append(f"listener_{listener_id}_error_{e}")
raise
try:
# Start multiple listeners
async with trio.open_nursery() as nursery:
for i in range(3):
nursery.start_soon(create_tracked_listener, i)
# Verify shutdown sequence
start_events = [e for e in shutdown_events if "started" in e]
close_events = [e for e in shutdown_events if "closed" in e]
assert len(start_events) == 3
assert len(close_events) == 3
logger.info(f"Shutdown sequence: {shutdown_events}")
finally:
shutdown_events.append("transport_closing")
await transport.close()
shutdown_events.append("transport_closed")
# HELPER FUNCTIONS FOR CLEANER TESTS
async def run_listener_for_duration(transport, handler, duration=0.5):
"""Helper to run a single listener for a specific duration."""
listener = transport.create_listener(handler)
listen_addr = create_quic_multiaddr("127.0.0.1", 0, "/quic")
async with trio.open_nursery() as nursery:
success = await listener.listen(listen_addr, nursery)
assert success
# Run for specified duration
await trio.sleep(duration)
# Clean close
await listener.close()
return listener
async def run_multiple_listeners_parallel(transport, handler, count=3, duration=0.5):
"""Helper to run multiple listeners in parallel."""
listeners = []
async def single_listener_task(listener_id):
listener = await run_listener_for_duration(transport, handler, duration)
listeners.append(listener)
logger.info(f"Listener {listener_id} completed")
async with trio.open_nursery() as nursery:
for i in range(count):
nursery.start_soon(single_listener_task, i)
return listeners
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
print("✅ TIMEOUT TEST PASSED!")

View File

@ -8,6 +8,7 @@ from libp2p.crypto.ed25519 import (
create_new_key_pair,
)
from libp2p.crypto.keys import PrivateKey
from libp2p.peer.id import ID
from libp2p.transport.quic.exceptions import (
QUICDialError,
QUICListenError,
@ -111,7 +112,10 @@ 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"))
await transport.dial(
multiaddr.Multiaddr("/ip4/127.0.0.1/udp/4001/quic"),
ID.from_pubkey(create_new_key_pair().public_key),
)
def test_create_listener_closed_transport(self, transport):
"""Test creating listener with closed transport raises error."""