mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2026-02-12 16:10:57 +00:00
Merge remote-tracking branch 'acul71/feat/804-add-thin-waist-address' into feat/804-add-thin-waist-address
This commit is contained in:
@ -11,8 +11,8 @@ from multiaddr import Multiaddr
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from libp2p.utils.address_validation import (
|
from libp2p.utils.address_validation import (
|
||||||
get_available_interfaces,
|
|
||||||
expand_wildcard_address,
|
expand_wildcard_address,
|
||||||
|
get_available_interfaces,
|
||||||
get_optimal_binding_address,
|
get_optimal_binding_address,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -21,7 +21,10 @@ except ImportError:
|
|||||||
return [Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")]
|
return [Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")]
|
||||||
|
|
||||||
def expand_wildcard_address(addr: Multiaddr, port: int | None = None):
|
def expand_wildcard_address(addr: Multiaddr, port: int | None = None):
|
||||||
return [addr if port is None else Multiaddr(str(addr).rsplit("/", 1)[0] + f"/{port}")]
|
if port is None:
|
||||||
|
return [addr]
|
||||||
|
addr_str = str(addr).rsplit("/", 1)[0]
|
||||||
|
return [Multiaddr(addr_str + f"/{port}")]
|
||||||
|
|
||||||
def get_optimal_binding_address(port: int, protocol: str = "tcp"):
|
def get_optimal_binding_address(port: int, protocol: str = "tcp"):
|
||||||
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")
|
return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")
|
||||||
@ -57,4 +60,4 @@ def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -18,10 +18,8 @@ from libp2p.network.stream.net_stream import (
|
|||||||
from libp2p.peer.peerinfo import (
|
from libp2p.peer.peerinfo import (
|
||||||
info_from_p2p_addr,
|
info_from_p2p_addr,
|
||||||
)
|
)
|
||||||
|
|
||||||
from libp2p.utils.address_validation import (
|
from libp2p.utils.address_validation import (
|
||||||
get_optimal_binding_address,
|
get_optimal_binding_address,
|
||||||
get_available_interfaces,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
PROTOCOL_ID = TProtocol("/echo/1.0.0")
|
PROTOCOL_ID = TProtocol("/echo/1.0.0")
|
||||||
@ -36,8 +34,8 @@ async def _echo_stream_handler(stream: INetStream) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def run(port: int, destination: str, seed: int | None = None) -> None:
|
async def run(port: int, destination: str, seed: int | None = None) -> None:
|
||||||
# Use all available interfaces for listening (JS parity)
|
# CHANGED: previously hardcoded 0.0.0.0
|
||||||
listen_addrs = get_available_interfaces(port)
|
listen_addr = get_optimal_binding_address(port)
|
||||||
|
|
||||||
if seed:
|
if seed:
|
||||||
import random
|
import random
|
||||||
|
|||||||
@ -330,8 +330,16 @@ class Swarm(Service, INetworkService):
|
|||||||
|
|
||||||
# Close all listeners
|
# Close all listeners
|
||||||
if hasattr(self, "listeners"):
|
if hasattr(self, "listeners"):
|
||||||
for listener in self.listeners.values():
|
for maddr_str, listener in self.listeners.items():
|
||||||
await listener.close()
|
await listener.close()
|
||||||
|
# Notify about listener closure
|
||||||
|
try:
|
||||||
|
multiaddr = Multiaddr(maddr_str)
|
||||||
|
await self.notify_listen_close(multiaddr)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to notify listen_close for {maddr_str}: {e}"
|
||||||
|
)
|
||||||
self.listeners.clear()
|
self.listeners.clear()
|
||||||
|
|
||||||
# Close the transport if it exists and has a close method
|
# Close the transport if it exists and has a close method
|
||||||
@ -420,7 +428,9 @@ class Swarm(Service, INetworkService):
|
|||||||
nursery.start_soon(notifee.closed_stream, self, stream)
|
nursery.start_soon(notifee.closed_stream, self, stream)
|
||||||
|
|
||||||
async def notify_listen_close(self, multiaddr: Multiaddr) -> None:
|
async def notify_listen_close(self, multiaddr: Multiaddr) -> None:
|
||||||
raise NotImplementedError
|
async with trio.open_nursery() as nursery:
|
||||||
|
for notifee in self.notifees:
|
||||||
|
nursery.start_soon(notifee.listen_close, self, multiaddr)
|
||||||
|
|
||||||
# Generic notifier used by NetStream._notify_closed
|
# Generic notifier used by NetStream._notify_closed
|
||||||
async def notify_all(self, notifier: Callable[[INotifee], Awaitable[None]]) -> None:
|
async def notify_all(self, notifier: Callable[[INotifee], Awaitable[None]]) -> None:
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import List, Optional
|
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from multiaddr.utils import get_thin_waist_addresses, get_network_addrs # type: ignore
|
from multiaddr.utils import ( # type: ignore
|
||||||
|
get_network_addrs,
|
||||||
|
get_thin_waist_addresses,
|
||||||
|
)
|
||||||
|
|
||||||
_HAS_THIN_WAIST = True
|
_HAS_THIN_WAIST = True
|
||||||
except ImportError: # pragma: no cover - only executed in older environments
|
except ImportError: # pragma: no cover - only executed in older environments
|
||||||
_HAS_THIN_WAIST = False
|
_HAS_THIN_WAIST = False
|
||||||
@ -11,7 +15,7 @@ except ImportError: # pragma: no cover - only executed in older environments
|
|||||||
get_network_addrs = None # type: ignore
|
get_network_addrs = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def _safe_get_network_addrs(ip_version: int) -> List[str]:
|
def _safe_get_network_addrs(ip_version: int) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Internal safe wrapper. Returns a list of IP addresses for the requested IP version.
|
Internal safe wrapper. Returns a list of IP addresses for the requested IP version.
|
||||||
Falls back to minimal defaults when Thin Waist helpers are missing.
|
Falls back to minimal defaults when Thin Waist helpers are missing.
|
||||||
@ -31,7 +35,7 @@ def _safe_get_network_addrs(ip_version: int) -> List[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _safe_expand(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]:
|
def _safe_expand(addr: Multiaddr, port: int | None = None) -> list[Multiaddr]:
|
||||||
"""
|
"""
|
||||||
Internal safe expansion wrapper. Returns a list of Multiaddr objects.
|
Internal safe expansion wrapper. Returns a list of Multiaddr objects.
|
||||||
If Thin Waist isn't available, returns [addr] (identity).
|
If Thin Waist isn't available, returns [addr] (identity).
|
||||||
@ -46,7 +50,7 @@ def _safe_expand(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]
|
|||||||
return [addr]
|
return [addr]
|
||||||
|
|
||||||
|
|
||||||
def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr]:
|
def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr]:
|
||||||
"""
|
"""
|
||||||
Discover available network interfaces (IPv4 + IPv6 if supported) for binding.
|
Discover available network interfaces (IPv4 + IPv6 if supported) for binding.
|
||||||
|
|
||||||
@ -54,7 +58,7 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr
|
|||||||
:param protocol: Transport protocol (e.g., "tcp" or "udp").
|
:param protocol: Transport protocol (e.g., "tcp" or "udp").
|
||||||
:return: List of Multiaddr objects representing candidate interface addresses.
|
:return: List of Multiaddr objects representing candidate interface addresses.
|
||||||
"""
|
"""
|
||||||
addrs: List[Multiaddr] = []
|
addrs: list[Multiaddr] = []
|
||||||
|
|
||||||
# IPv4 enumeration
|
# IPv4 enumeration
|
||||||
seen_v4: set[str] = set()
|
seen_v4: set[str] = set()
|
||||||
@ -62,14 +66,11 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr
|
|||||||
seen_v4.add(ip)
|
seen_v4.add(ip)
|
||||||
addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}"))
|
addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}"))
|
||||||
|
|
||||||
# Ensure loopback IPv4 explicitly present (JS echo parity) even if not returned
|
# IPv6 enumeration (optional: only include if we have at least one global or
|
||||||
if "127.0.0.1" not in seen_v4:
|
# loopback)
|
||||||
addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}"))
|
|
||||||
|
|
||||||
# IPv6 enumeration (optional: only include if we have at least one global or loopback)
|
|
||||||
seen_v6: set[str] = set()
|
|
||||||
for ip in _safe_get_network_addrs(6):
|
for ip in _safe_get_network_addrs(6):
|
||||||
seen_v6.add(ip)
|
# Avoid returning unusable wildcard expansions if the environment does not
|
||||||
|
# support IPv6
|
||||||
addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}"))
|
addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}"))
|
||||||
# Optionally ensure IPv6 loopback when any IPv6 present but loopback missing
|
# Optionally ensure IPv6 loopback when any IPv6 present but loopback missing
|
||||||
if seen_v6 and "::1" not in seen_v6:
|
if seen_v6 and "::1" not in seen_v6:
|
||||||
@ -82,7 +83,9 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr
|
|||||||
return addrs
|
return addrs
|
||||||
|
|
||||||
|
|
||||||
def expand_wildcard_address(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]:
|
def expand_wildcard_address(
|
||||||
|
addr: Multiaddr, port: int | None = None
|
||||||
|
) -> list[Multiaddr]:
|
||||||
"""
|
"""
|
||||||
Expand a wildcard (e.g. /ip4/0.0.0.0/tcp/0) into all concrete interfaces.
|
Expand a wildcard (e.g. /ip4/0.0.0.0/tcp/0) into all concrete interfaces.
|
||||||
|
|
||||||
@ -132,4 +135,4 @@ __all__ = [
|
|||||||
"get_available_interfaces",
|
"get_available_interfaces",
|
||||||
"get_optimal_binding_address",
|
"get_optimal_binding_address",
|
||||||
"expand_wildcard_address",
|
"expand_wildcard_address",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,11 +5,12 @@ the stream passed into opened_stream is correct.
|
|||||||
Note: Listen event does not get hit because MyNotifee is passed
|
Note: Listen event does not get hit because MyNotifee is passed
|
||||||
into network after network has already started listening
|
into network after network has already started listening
|
||||||
|
|
||||||
TODO: Add tests for closed_stream, listen_close when those
|
Note: ClosedStream events are processed asynchronously and may not be
|
||||||
features are implemented in swarm
|
immediately available due to the rapid nature of operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
@ -29,11 +30,11 @@ from tests.utils.factories import (
|
|||||||
|
|
||||||
class Event(enum.Enum):
|
class Event(enum.Enum):
|
||||||
OpenedStream = 0
|
OpenedStream = 0
|
||||||
ClosedStream = 1 # Not implemented
|
ClosedStream = 1
|
||||||
Connected = 2
|
Connected = 2
|
||||||
Disconnected = 3
|
Disconnected = 3
|
||||||
Listen = 4
|
Listen = 4
|
||||||
ListenClose = 5 # Not implemented
|
ListenClose = 5
|
||||||
|
|
||||||
|
|
||||||
class MyNotifee(INotifee):
|
class MyNotifee(INotifee):
|
||||||
@ -60,8 +61,11 @@ class MyNotifee(INotifee):
|
|||||||
self.events.append(Event.Listen)
|
self.events.append(Event.Listen)
|
||||||
|
|
||||||
async def listen_close(self, network: INetwork, multiaddr: Multiaddr) -> None:
|
async def listen_close(self, network: INetwork, multiaddr: Multiaddr) -> None:
|
||||||
# TODO: It is not implemented yet.
|
if network is None:
|
||||||
pass
|
raise ValueError("network parameter cannot be None")
|
||||||
|
if multiaddr is None:
|
||||||
|
raise ValueError("multiaddr parameter cannot be None")
|
||||||
|
self.events.append(Event.ListenClose)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.trio
|
@pytest.mark.trio
|
||||||
@ -123,3 +127,171 @@ async def test_notify(security_protocol):
|
|||||||
assert await wait_for_event(events_1_1, Event.OpenedStream, 1.0)
|
assert await wait_for_event(events_1_1, Event.OpenedStream, 1.0)
|
||||||
assert await wait_for_event(events_1_1, Event.ClosedStream, 1.0)
|
assert await wait_for_event(events_1_1, Event.ClosedStream, 1.0)
|
||||||
assert await wait_for_event(events_1_1, Event.Disconnected, 1.0)
|
assert await wait_for_event(events_1_1, Event.Disconnected, 1.0)
|
||||||
|
|
||||||
|
# Note: ListenClose events are triggered when swarm closes during cleanup
|
||||||
|
# The test framework automatically closes listeners, triggering ListenClose
|
||||||
|
# notifications
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_event(events_list, event, timeout=1.0):
|
||||||
|
"""Helper to wait for a specific event to appear in the events list."""
|
||||||
|
with trio.move_on_after(timeout):
|
||||||
|
while event not in events_list:
|
||||||
|
await trio.sleep(0.01)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_notify_with_closed_stream_and_listen_close():
|
||||||
|
"""Test that closed_stream and listen_close events are properly triggered."""
|
||||||
|
# Event lists for notifees
|
||||||
|
events_0 = []
|
||||||
|
events_1 = []
|
||||||
|
|
||||||
|
# Create two swarms
|
||||||
|
async with SwarmFactory.create_batch_and_listen(2) as swarms:
|
||||||
|
# Register notifees
|
||||||
|
notifee_0 = MyNotifee(events_0)
|
||||||
|
notifee_1 = MyNotifee(events_1)
|
||||||
|
|
||||||
|
swarms[0].register_notifee(notifee_0)
|
||||||
|
swarms[1].register_notifee(notifee_1)
|
||||||
|
|
||||||
|
# Connect swarms
|
||||||
|
await connect_swarm(swarms[0], swarms[1])
|
||||||
|
|
||||||
|
# Create and close a stream to trigger closed_stream event
|
||||||
|
stream = await swarms[0].new_stream(swarms[1].get_peer_id())
|
||||||
|
await stream.close()
|
||||||
|
|
||||||
|
# Note: Events are processed asynchronously and may not be immediately available
|
||||||
|
# due to the rapid nature of operations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_notify_edge_cases():
|
||||||
|
"""Test edge cases for notify system."""
|
||||||
|
events = []
|
||||||
|
|
||||||
|
async with SwarmFactory.create_batch_and_listen(2) as swarms:
|
||||||
|
notifee = MyNotifee(events)
|
||||||
|
swarms[0].register_notifee(notifee)
|
||||||
|
|
||||||
|
# Connect swarms first
|
||||||
|
await connect_swarm(swarms[0], swarms[1])
|
||||||
|
|
||||||
|
# Test 1: Multiple rapid stream operations
|
||||||
|
streams = []
|
||||||
|
for _ in range(5):
|
||||||
|
stream = await swarms[0].new_stream(swarms[1].get_peer_id())
|
||||||
|
streams.append(stream)
|
||||||
|
|
||||||
|
# Close all streams rapidly
|
||||||
|
for stream in streams:
|
||||||
|
await stream.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_my_notifee_error_handling():
|
||||||
|
"""Test error handling for invalid parameters in MyNotifee methods."""
|
||||||
|
events = []
|
||||||
|
notifee = MyNotifee(events)
|
||||||
|
|
||||||
|
# Mock objects for testing
|
||||||
|
mock_network = Mock(spec=INetwork)
|
||||||
|
mock_stream = Mock(spec=INetStream)
|
||||||
|
mock_multiaddr = Mock(spec=Multiaddr)
|
||||||
|
|
||||||
|
# Test closed_stream with None parameters
|
||||||
|
with pytest.raises(ValueError, match="network parameter cannot be None"):
|
||||||
|
await notifee.closed_stream(None, mock_stream) # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="stream parameter cannot be None"):
|
||||||
|
await notifee.closed_stream(mock_network, None) # type: ignore
|
||||||
|
|
||||||
|
# Test listen_close with None parameters
|
||||||
|
with pytest.raises(ValueError, match="network parameter cannot be None"):
|
||||||
|
await notifee.listen_close(None, mock_multiaddr) # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="multiaddr parameter cannot be None"):
|
||||||
|
await notifee.listen_close(mock_network, None) # type: ignore
|
||||||
|
|
||||||
|
# Verify no events were recorded due to errors
|
||||||
|
assert len(events) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_rapid_stream_operations():
|
||||||
|
"""Test rapid stream open/close operations."""
|
||||||
|
events_0 = []
|
||||||
|
events_1 = []
|
||||||
|
|
||||||
|
async with SwarmFactory.create_batch_and_listen(2) as swarms:
|
||||||
|
notifee_0 = MyNotifee(events_0)
|
||||||
|
notifee_1 = MyNotifee(events_1)
|
||||||
|
|
||||||
|
swarms[0].register_notifee(notifee_0)
|
||||||
|
swarms[1].register_notifee(notifee_1)
|
||||||
|
|
||||||
|
# Connect swarms
|
||||||
|
await connect_swarm(swarms[0], swarms[1])
|
||||||
|
|
||||||
|
# Rapidly create and close multiple streams
|
||||||
|
streams = []
|
||||||
|
for _ in range(3):
|
||||||
|
stream = await swarms[0].new_stream(swarms[1].get_peer_id())
|
||||||
|
streams.append(stream)
|
||||||
|
|
||||||
|
# Close all streams immediately
|
||||||
|
for stream in streams:
|
||||||
|
await stream.close()
|
||||||
|
|
||||||
|
# Verify OpenedStream events are recorded
|
||||||
|
assert events_0.count(Event.OpenedStream) == 3
|
||||||
|
assert events_1.count(Event.OpenedStream) == 3
|
||||||
|
|
||||||
|
# Close peer to trigger disconnection events
|
||||||
|
await swarms[0].close_peer(swarms[1].get_peer_id())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_concurrent_stream_operations():
|
||||||
|
"""Test concurrent stream operations using trio nursery."""
|
||||||
|
events_0 = []
|
||||||
|
events_1 = []
|
||||||
|
|
||||||
|
async with SwarmFactory.create_batch_and_listen(2) as swarms:
|
||||||
|
notifee_0 = MyNotifee(events_0)
|
||||||
|
notifee_1 = MyNotifee(events_1)
|
||||||
|
|
||||||
|
swarms[0].register_notifee(notifee_0)
|
||||||
|
swarms[1].register_notifee(notifee_1)
|
||||||
|
|
||||||
|
# Connect swarms
|
||||||
|
await connect_swarm(swarms[0], swarms[1])
|
||||||
|
|
||||||
|
async def create_and_close_stream():
|
||||||
|
"""Create and immediately close a stream."""
|
||||||
|
stream = await swarms[0].new_stream(swarms[1].get_peer_id())
|
||||||
|
await stream.close()
|
||||||
|
|
||||||
|
# Run multiple stream operations concurrently
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
for _ in range(4):
|
||||||
|
nursery.start_soon(create_and_close_stream)
|
||||||
|
|
||||||
|
# Verify some OpenedStream events are recorded
|
||||||
|
# (concurrent operations may not all succeed)
|
||||||
|
opened_count_0 = events_0.count(Event.OpenedStream)
|
||||||
|
opened_count_1 = events_1.count(Event.OpenedStream)
|
||||||
|
|
||||||
|
assert opened_count_0 > 0, (
|
||||||
|
f"Expected some OpenedStream events, got {opened_count_0}"
|
||||||
|
)
|
||||||
|
assert opened_count_1 > 0, (
|
||||||
|
f"Expected some OpenedStream events, got {opened_count_1}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Close peer to trigger disconnection events
|
||||||
|
await swarms[0].close_peer(swarms[1].get_peer_id())
|
||||||
|
|||||||
@ -1,65 +1,108 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import sys
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
from multiaddr import Multiaddr
|
||||||
import trio
|
from multiaddr.protocols import P_IP4, P_IP6, P_P2P, P_TCP
|
||||||
|
|
||||||
|
# pytestmark = pytest.mark.timeout(20) # Temporarily disabled for debugging
|
||||||
|
|
||||||
# This test is intentionally lightweight and can be marked as 'integration'.
|
# This test is intentionally lightweight and can be marked as 'integration'.
|
||||||
# It ensures the echo example runs and prints the new Thin Waist lines using Trio primitives.
|
# It ensures the echo example runs and prints the new Thin Waist lines using Trio primitives.
|
||||||
|
|
||||||
EXAMPLES_DIR = Path(__file__).parent.parent.parent / "examples" / "echo"
|
current_file = Path(__file__)
|
||||||
|
project_root = current_file.parent.parent.parent
|
||||||
|
EXAMPLES_DIR: Path = project_root / "examples" / "echo"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.trio
|
def test_echo_example_starts_and_prints_thin_waist(monkeypatch, tmp_path):
|
||||||
async def test_echo_example_starts_and_prints_thin_waist() -> None:
|
"""Run echo server and validate printed multiaddr and peer id."""
|
||||||
cmd = [sys.executable, str(EXAMPLES_DIR / "echo.py"), "-p", "0"]
|
# Run echo example as server
|
||||||
|
cmd = [sys.executable, "-u", str(EXAMPLES_DIR / "echo.py"), "-p", "0"]
|
||||||
|
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
||||||
|
proc: subprocess.Popen[str] = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
found_selected = False
|
if proc.stdout is None:
|
||||||
found_interfaces = False
|
proc.terminate()
|
||||||
|
raise RuntimeError("Process stdout is None")
|
||||||
|
out_stream = proc.stdout
|
||||||
|
|
||||||
# Use a cancellation scope as timeout (similar to previous 10s loop)
|
peer_id: str | None = None
|
||||||
with trio.move_on_after(10) as cancel_scope:
|
printed_multiaddr: str | None = None
|
||||||
# Start process streaming stdout
|
saw_waiting = False
|
||||||
proc = await trio.open_process(
|
|
||||||
cmd,
|
start = time.time()
|
||||||
stdout=trio.SUBPROCESS_PIPE,
|
timeout_s = 8.0
|
||||||
stderr=trio.STDOUT,
|
try:
|
||||||
|
while time.time() - start < timeout_s:
|
||||||
|
line = out_stream.readline()
|
||||||
|
if not line:
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
|
s = line.strip()
|
||||||
|
if s.startswith("I am "):
|
||||||
|
peer_id = s.partition("I am ")[2]
|
||||||
|
if s.startswith("echo-demo -d "):
|
||||||
|
printed_multiaddr = s.partition("echo-demo -d ")[2]
|
||||||
|
if "Waiting for incoming connections..." in s:
|
||||||
|
saw_waiting = True
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
proc.terminate()
|
||||||
|
with contextlib.suppress(ProcessLookupError):
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
assert peer_id, "Did not capture peer ID line"
|
||||||
|
assert printed_multiaddr, "Did not capture multiaddr line"
|
||||||
|
assert saw_waiting, "Did not capture waiting-for-connections line"
|
||||||
|
|
||||||
|
# Validate multiaddr structure using py-multiaddr protocol methods
|
||||||
|
ma = Multiaddr(printed_multiaddr) # should parse without error
|
||||||
|
|
||||||
|
# Check that the multiaddr contains the p2p protocol
|
||||||
|
try:
|
||||||
|
peer_id_from_multiaddr = ma.value_for_protocol("p2p")
|
||||||
|
assert peer_id_from_multiaddr is not None, (
|
||||||
|
"Multiaddr missing p2p protocol value"
|
||||||
)
|
)
|
||||||
|
assert peer_id_from_multiaddr == peer_id, (
|
||||||
|
f"Peer ID mismatch: {peer_id_from_multiaddr} != {peer_id}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(f"Failed to extract p2p protocol value: {e}")
|
||||||
|
|
||||||
assert proc.stdout is not None # for type checkers
|
# Validate the multiaddr structure by checking protocols
|
||||||
buffer = b""
|
protocols = ma.protocols()
|
||||||
|
|
||||||
try:
|
# Should have at least IP, TCP, and P2P protocols
|
||||||
while not (found_selected and found_interfaces):
|
assert any(p.code == P_IP4 or p.code == P_IP6 for p in protocols), (
|
||||||
# Read some bytes (non-blocking with timeout scope)
|
"Missing IP protocol"
|
||||||
data = await proc.stdout.receive_some(1024)
|
)
|
||||||
if not data:
|
assert any(p.code == P_TCP for p in protocols), "Missing TCP protocol"
|
||||||
# Process might still be starting; yield control
|
assert any(p.code == P_P2P for p in protocols), "Missing P2P protocol"
|
||||||
await trio.sleep(0.05)
|
|
||||||
continue
|
|
||||||
buffer += data
|
|
||||||
# Process complete lines
|
|
||||||
*lines, buffer = buffer.split(b"\n") if b"\n" in buffer else ([], buffer)
|
|
||||||
for raw in lines:
|
|
||||||
line = raw.decode(errors="ignore")
|
|
||||||
if "Selected binding address:" in line:
|
|
||||||
found_selected = True
|
|
||||||
if "Available candidate interfaces:" in line:
|
|
||||||
found_interfaces = True
|
|
||||||
if "Waiting for incoming connections..." in line:
|
|
||||||
# We have reached steady state; can stop reading further
|
|
||||||
if found_selected and found_interfaces:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
# Terminate the long-running echo example
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
proc.terminate()
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
await trio.move_on_after(2)(proc.wait) # best-effort wait
|
|
||||||
if cancel_scope.cancelled_caught:
|
|
||||||
# Timeout occurred
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert found_selected, "Did not capture Thin Waist binding log line"
|
# Extract the p2p part and validate it matches the captured peer ID
|
||||||
assert found_interfaces, "Did not capture Thin Waist interfaces log line"
|
p2p_part = Multiaddr(f"/p2p/{peer_id}")
|
||||||
|
try:
|
||||||
|
# Decapsulate the p2p part to get the transport address
|
||||||
|
transport_addr = ma.decapsulate(p2p_part)
|
||||||
|
# Verify the decapsulated address doesn't contain p2p
|
||||||
|
transport_protocols = transport_addr.protocols()
|
||||||
|
assert not any(p.code == P_P2P for p in transport_protocols), (
|
||||||
|
"Decapsulation failed - still contains p2p"
|
||||||
|
)
|
||||||
|
# Verify the original multiaddr can be reconstructed
|
||||||
|
reconstructed = transport_addr.encapsulate(p2p_part)
|
||||||
|
assert str(reconstructed) == str(ma), "Reconstruction failed"
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(f"Multiaddr decapsulation failed: {e}")
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import pytest
|
|||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
|
|
||||||
from libp2p.utils.address_validation import (
|
from libp2p.utils.address_validation import (
|
||||||
|
expand_wildcard_address,
|
||||||
get_available_interfaces,
|
get_available_interfaces,
|
||||||
get_optimal_binding_address,
|
get_optimal_binding_address,
|
||||||
expand_wildcard_address,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -53,4 +53,4 @@ def test_expand_wildcard_address_ipv6() -> None:
|
|||||||
expanded = expand_wildcard_address(wildcard)
|
expanded = expand_wildcard_address(wildcard)
|
||||||
assert len(expanded) > 0
|
assert len(expanded) > 0
|
||||||
for e in expanded:
|
for e in expanded:
|
||||||
assert "/ip6/" in str(e)
|
assert "/ip6/" in str(e)
|
||||||
|
|||||||
Reference in New Issue
Block a user