From 7f2c6355d6fcc7cd6630129154d52372ec7dc75a Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 14:13:56 +0100 Subject: [PATCH 1/8] REFACTOR: fix arrangement of the classes --- libp2p/abc.py | 195 ++++++++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/libp2p/abc.py b/libp2p/abc.py index dc941c43d..ecc4d0f16 100644 --- a/libp2p/abc.py +++ b/libp2p/abc.py @@ -1068,6 +1068,104 @@ async def listen_close(self, network: "INetwork", multiaddr: Multiaddr) -> None: """ +class IMultiselectCommunicator(ABC): + """ + Communicator helper for multiselect. + + Ensures that both the client and multistream module follow the same + multistream protocol. + """ + + @abstractmethod + async def write(self, msg_str: str) -> None: + """ + Write a message to the stream. + + Parameters + ---------- + msg_str : str + The message string to write. + + """ + + @abstractmethod + async def read(self) -> str: + """ + Read a message from the stream until EOF. + + Returns + ------- + str + The message read from the stream. + + """ + + +# -------------------------- multiselect_muxer interface.py -------------------------- + + +class IMultiselectMuxer(ABC): + """ + Multiselect module for protocol negotiation. + + Responsible for responding to a multiselect client by selecting a protocol + and its corresponding handler for communication. + """ + + handlers: dict[TProtocol | None, StreamHandlerFn | None] + + @abstractmethod + def add_handler(self, protocol: TProtocol, handler: StreamHandlerFn) -> None: + """ + Store a handler for the specified protocol. + + Parameters + ---------- + protocol : TProtocol + The protocol name. + handler : StreamHandlerFn + The handler function associated with the protocol. + + """ + + def get_protocols(self) -> tuple[TProtocol | None, ...]: + """ + Retrieve the protocols for which handlers have been registered. + + Returns + ------- + tuple[TProtocol, ...] + A tuple of registered protocol names. + + """ + return tuple(self.handlers.keys()) + + @abstractmethod + async def negotiate( + self, communicator: IMultiselectCommunicator + ) -> tuple[TProtocol | None, StreamHandlerFn | None]: + """ + Negotiate a protocol selection with a multiselect client. + + Parameters + ---------- + communicator : IMultiselectCommunicator + The communicator used to negotiate the protocol. + + Returns + ------- + tuple[TProtocol, StreamHandlerFn] + A tuple containing the selected protocol and its handler. + + Raises + ------ + Exception + If negotiation fails. + + """ + + + # -------------------------- host interface.py -------------------------- @@ -1128,15 +1226,14 @@ def get_network(self) -> INetworkService: """ - # FIXME: Replace with correct return type @abstractmethod - def get_mux(self) -> Any: + def get_mux(self) -> IMultiselectMuxer: """ Retrieve the muxer instance for the host. Returns ------- - Any + IMultiselectMuxer The muxer instance of the host. """ @@ -1498,37 +1595,7 @@ def is_expired(self) -> bool: # ------------------ multiselect_communicator interface.py ------------------ -class IMultiselectCommunicator(ABC): - """ - Communicator helper for multiselect. - - Ensures that both the client and multistream module follow the same - multistream protocol. - """ - - @abstractmethod - async def write(self, msg_str: str) -> None: - """ - Write a message to the stream. - - Parameters - ---------- - msg_str : str - The message string to write. - - """ - - @abstractmethod - async def read(self) -> str: - """ - Read a message from the stream until EOF. - - Returns - ------- - str - The message read from the stream. - """ # -------------------------- multiselect_client interface.py -------------------------- @@ -1613,68 +1680,6 @@ async def try_select( """ -# -------------------------- multiselect_muxer interface.py -------------------------- - - -class IMultiselectMuxer(ABC): - """ - Multiselect module for protocol negotiation. - - Responsible for responding to a multiselect client by selecting a protocol - and its corresponding handler for communication. - """ - - handlers: dict[TProtocol | None, StreamHandlerFn | None] - - @abstractmethod - def add_handler(self, protocol: TProtocol, handler: StreamHandlerFn) -> None: - """ - Store a handler for the specified protocol. - - Parameters - ---------- - protocol : TProtocol - The protocol name. - handler : StreamHandlerFn - The handler function associated with the protocol. - - """ - - def get_protocols(self) -> tuple[TProtocol | None, ...]: - """ - Retrieve the protocols for which handlers have been registered. - - Returns - ------- - tuple[TProtocol, ...] - A tuple of registered protocol names. - - """ - return tuple(self.handlers.keys()) - - @abstractmethod - async def negotiate( - self, communicator: IMultiselectCommunicator - ) -> tuple[TProtocol | None, StreamHandlerFn | None]: - """ - Negotiate a protocol selection with a multiselect client. - - Parameters - ---------- - communicator : IMultiselectCommunicator - The communicator used to negotiate the protocol. - - Returns - ------- - tuple[TProtocol, StreamHandlerFn] - A tuple containing the selected protocol and its handler. - - Raises - ------ - Exception - If negotiation fails. - - """ # -------------------------- routing interface.py -------------------------- From ca9bed4738c8d74e79bf1618c13cd5022c0f134a Mon Sep 17 00:00:00 2001 From: Emmanuel Appah Date: Wed, 2 Jul 2025 14:55:02 +0100 Subject: [PATCH 2/8] Fix(tests): Resolve Muxed Connection Type Assertion Error --- libp2p/host/basic_host.py | 3 +- pyproject.toml | 4 + tests/core/host/conftest.py | 1 + tests/core/host/test_mux_type_compliance.py | 258 ++++++++++++++++++++ 4 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 tests/core/host/conftest.py create mode 100644 tests/core/host/test_mux_type_compliance.py diff --git a/libp2p/host/basic_host.py b/libp2p/host/basic_host.py index 798186cfe..44b05cdfa 100644 --- a/libp2p/host/basic_host.py +++ b/libp2p/host/basic_host.py @@ -16,6 +16,7 @@ from libp2p.abc import ( IHost, + IMultiselectMuxer, INetConn, INetStream, INetworkService, @@ -127,7 +128,7 @@ def get_peerstore(self) -> IPeerStore: """ return self.peerstore - def get_mux(self) -> Multiselect: + def get_mux(self) -> IMultiselectMuxer: """ :return: mux instance of host """ diff --git a/pyproject.toml b/pyproject.toml index cf0001562..30c8013b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -281,3 +281,7 @@ project_excludes = [ "**/*.pyi", ".venv/**", ] + +[tool.poetry.group.dev.dependencies] +pytest-trio = "^0.8.0" + diff --git a/tests/core/host/conftest.py b/tests/core/host/conftest.py new file mode 100644 index 000000000..dee6ee6d5 --- /dev/null +++ b/tests/core/host/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["trio"] diff --git a/tests/core/host/test_mux_type_compliance.py b/tests/core/host/test_mux_type_compliance.py new file mode 100644 index 000000000..6618468b7 --- /dev/null +++ b/tests/core/host/test_mux_type_compliance.py @@ -0,0 +1,258 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest + +# Import the interfaces +from libp2p.abc import ( + IMultiselectMuxer, + INetworkService, + IPeerStore, +) +from libp2p.custom_types import StreamHandlerFn, TProtocol + +# Import the concrete classes for instantiation and specific type checks +from libp2p.host.basic_host import BasicHost + +# For expected errors in negotiation tests +from libp2p.protocol_muxer.exceptions import MultiselectError +from libp2p.protocol_muxer.multiselect import Multiselect +from libp2p.protocol_muxer.multiselect_client import ( + MultiselectClient, +) # Needed for mock calls +from libp2p.protocol_muxer.multiselect_communicator import ( + MultiselectCommunicator, +) # Needed for mock calls + +# --- Fixtures for setting up the test environment --- + + +@pytest.fixture +def mock_peer_id(): + """Provides a mock PeerID for testing purposes.""" + mock = MagicMock() + mock.__str__.return_value = "QmMockPeerId" + return mock + + +@pytest.fixture +def mock_peerstore(): + """Provides a mocked IPeerStore instance.""" + mock = MagicMock(spec=IPeerStore) + mock.pubkey.return_value = MagicMock() # Mock PublicKey + mock.privkey.return_value = MagicMock() # Mock PrivateKey + mock.add_addrs = AsyncMock() # Ensure add_addrs is an AsyncMock if called + mock.peer_info.return_value = MagicMock() # Mock PeerInfo + return mock + + +@pytest.fixture +def mock_network_service(mock_peer_id, mock_peerstore): + """ + Provides a mocked INetworkService instance with necessary sub-mocks. + This simulates the network environment for the BasicHost. + """ + mock_network = AsyncMock(spec=INetworkService) + mock_network.peerstore = mock_peerstore + mock_network.get_peer_id.return_value = mock_peer_id + mock_network.connections = {} # Simulate no active connections initially + mock_network.listeners = {} # Simulate no active listeners initially + mock_network.set_stream_handler = ( + MagicMock() + ) # Mock setting stream handler if called during init + mock_network.new_stream = AsyncMock() # Mock for new_stream calls in BasicHost + + return mock_network + + +@pytest.fixture +def basic_host(mock_network_service): + """ + Provides an instance of BasicHost initialized with mocked dependencies. + """ + # BasicHost.__init__ calls set_stream_handler, so mock_network_service needs it. + # It also initializes self.multiselect and self.multiselect_client internally. + return BasicHost(network=mock_network_service, enable_mDNS=False) + + +@pytest.fixture +def mock_communicator(): + """ + Provides a mock for IMultiselectCommunicator for negotiation tests. + By default, it will provide responses for a successful handshake and a protocol proposal. + Reset side_effect in specific tests if different behavior is needed. + """ + mock = AsyncMock( + spec=MultiselectCommunicator + ) # Use concrete spec for more accurate method mocks + mock.read = AsyncMock() + mock.write = AsyncMock() + return mock + + +# --- Runtime Type Checking Tests --- + + +def test_get_mux_return_type_runtime(basic_host): + """ + Verifies at runtime that BasicHost.get_mux() returns an object + that is an instance of both the IMultiselectMuxer interface and + the concrete Multiselect class. + """ + mux = basic_host.get_mux() + + # 1. Assert it's an instance of the interface + assert isinstance(mux, IMultiselectMuxer), ( + f"Expected mux to be an instance of IMultiselectMuxer, but got {type(mux)}" + ) + + # 2. Assert it's an instance of the concrete implementation + assert isinstance(mux, Multiselect), ( + f"Expected mux to be an instance of Multiselect, but got {type(mux)}" + ) + + # Optional: Verify that the object returned is the one stored internally + assert mux is basic_host.multiselect, ( + "The returned muxer should be the internal multiselect instance" + ) + + +def test_get_mux_interface_compliance(basic_host): + """ + Ensures that the object returned by BasicHost.get_mux() has all + the expected attributes and methods defined by IMultiselectMuxer. + """ + mux = basic_host.get_mux() + + # Check presence of required attributes/methods + assert hasattr(mux, "handlers"), "IMultiselectMuxer must have 'handlers' attribute" + assert isinstance(mux.handlers, dict), "'handlers' attribute must be a dictionary" + + assert hasattr(mux, "add_handler"), ( + "IMultiselectMuxer must have 'add_handler' method" + ) + assert callable(mux.add_handler), "'add_handler' must be callable" + + assert hasattr(mux, "get_protocols"), ( + "IMultiselectMuxer must have 'get_protocols' method" + ) + assert callable(mux.get_protocols), "'get_protocols' must be callable" + + assert hasattr(mux, "negotiate"), "IMultiselectMuxer must have 'negotiate' method" + assert callable(mux.negotiate), "'negotiate' must be callable" + + +# --- Functionality / Integration Tests --- + + +async def test_get_mux_add_handler_and_get_protocols(basic_host): + """ + Tests the functional behavior of add_handler and get_protocols methods + on the muxer returned by get_mux(). + """ + mux = basic_host.get_mux() + + # Initial state check - ensure default protocols are present + initial_protocols = mux.get_protocols() + assert ( + TProtocol("/multistream/1.0.0") in initial_protocols + ) # BasicHost adds default + + # Ensure our test protocols aren't there yet + assert TProtocol("/test/1.0.0") not in initial_protocols + assert TProtocol("/another/protocol/1.0.0") not in initial_protocols + + # Define a dummy handler + def dummy_handler(stream: AsyncMock) -> None: + pass + + # Add first protocol + protocol_a = TProtocol("/test/1.0.0") + mux.add_handler(protocol_a, dummy_handler) + + # Verify first protocol was added + updated_protocols_a = mux.get_protocols() + assert protocol_a in updated_protocols_a + assert mux.handlers[protocol_a] is dummy_handler + + # Add second protocol + protocol_b = TProtocol("/another/protocol/1.0.0") + mux.add_handler(protocol_b, lambda s: None) # Another dummy handler + + # Verify second protocol was added + updated_protocols_b = mux.get_protocols() + assert protocol_b in updated_protocols_b + assert ( + len(updated_protocols_b) >= len(initial_protocols) + 2 + ) # Should have added two new custom ones + + +async def test_get_mux_negotiate_success(basic_host, mock_communicator): + """ + Tests the successful negotiation flow using the muxer's negotiate method. + """ + mux = basic_host.get_mux() + + # Define a protocol and its handler that `negotiate` should successfully find + selected_protocol_str = "/app/my-protocol/1.0.0" + selected_protocol = TProtocol(selected_protocol_str) + dummy_negotiate_handler = AsyncMock( + spec=StreamHandlerFn + ) # Handler for the selected protocol + mux.add_handler(selected_protocol, dummy_negotiate_handler) + + # Configure the mock_communicator to simulate a successful negotiation sequence + mock_communicator.read.side_effect = [ + "/multistream/1.0.0", # First read: Client sends its multistream protocol (handshake) + selected_protocol_str, # Second read: Client proposes the app protocol + ] + + # Perform the negotiation + protocol, handler = await mux.negotiate(mock_communicator) + + # Assert the returned protocol and handler are correct + assert protocol == selected_protocol + assert handler is dummy_negotiate_handler + + # Verify calls to the mock communicator (handshake and protocol acceptance) + mock_communicator.write.assert_has_calls( + [ + # Handshake response + pytest.call("/multistream/1.0.0"), + # Protocol acceptance + pytest.call(selected_protocol_str), + ] + ) + # Ensure no other writes occurred + assert mock_communicator.write.call_count == 2 + assert mock_communicator.read.call_count == 2 + + +async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicator): + """ + Tests the negotiation flow when the proposed protocol is not found. + """ + mux = basic_host.get_mux() + + # Ensure the protocol we propose isn't actually registered (beyond defaults) + non_existent_protocol = TProtocol("/non-existent/protocol") + assert non_existent_protocol not in mux.get_protocols() # Ensure it's not present + + # Configure the mock_communicator to simulate a handshake followed by a non-existent protocol + mock_communicator.read.side_effect = [ + "/multistream/1.0.0", # Handshake response + str(non_existent_protocol), # Client proposes a non-existent protocol + ] + + # Expect a MultiselectError as the protocol won't be found + with pytest.raises(MultiselectError): + await mux.negotiate(mock_communicator) + + # Verify handshake write and "na" (not available) write + mock_communicator.write.assert_has_calls( + [ + pytest.call("/multistream/1.0.0"), + pytest.call("na"), # Muxer should respond with "na" + ] + ) + assert mock_communicator.write.call_count == 2 + assert mock_communicator.read.call_count == 2 From 8fd2d65def1c038886fa0355f50cbbc1dd01e415 Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 15:26:38 +0100 Subject: [PATCH 3/8] FIX-TESTS: all test cases for test_mux_type compliance passed --- pyproject.toml | 1 + tests/core/host/test_mux_type_compliance.py | 28 ++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 30c8013b9..822c7973f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "trio>=0.26.0", "fastecdsa==2.3.2; sys_platform != 'win32'", "zeroconf (>=0.147.0,<0.148.0)", + "pytest-trio (>=0.8.0,<0.9.0)", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/tests/core/host/test_mux_type_compliance.py b/tests/core/host/test_mux_type_compliance.py index 6618468b7..134295625 100644 --- a/tests/core/host/test_mux_type_compliance.py +++ b/tests/core/host/test_mux_type_compliance.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, call import pytest @@ -14,7 +14,10 @@ from libp2p.host.basic_host import BasicHost # For expected errors in negotiation tests -from libp2p.protocol_muxer.exceptions import MultiselectError +from libp2p.protocol_muxer.exceptions import ( + MultiselectError, + MultiselectCommunicatorError, +) from libp2p.protocol_muxer.multiselect import Multiselect from libp2p.protocol_muxer.multiselect_client import ( MultiselectClient, @@ -143,7 +146,7 @@ def test_get_mux_interface_compliance(basic_host): # --- Functionality / Integration Tests --- - +@pytest.mark.trio async def test_get_mux_add_handler_and_get_protocols(basic_host): """ Tests the functional behavior of add_handler and get_protocols methods @@ -153,10 +156,7 @@ async def test_get_mux_add_handler_and_get_protocols(basic_host): # Initial state check - ensure default protocols are present initial_protocols = mux.get_protocols() - assert ( - TProtocol("/multistream/1.0.0") in initial_protocols - ) # BasicHost adds default - + # The multistream protocol is part of the handshake, not a default handler. # Ensure our test protocols aren't there yet assert TProtocol("/test/1.0.0") not in initial_protocols assert TProtocol("/another/protocol/1.0.0") not in initial_protocols @@ -186,6 +186,7 @@ def dummy_handler(stream: AsyncMock) -> None: ) # Should have added two new custom ones +@pytest.mark.trio async def test_get_mux_negotiate_success(basic_host, mock_communicator): """ Tests the successful negotiation flow using the muxer's negotiate method. @@ -217,9 +218,9 @@ async def test_get_mux_negotiate_success(basic_host, mock_communicator): mock_communicator.write.assert_has_calls( [ # Handshake response - pytest.call("/multistream/1.0.0"), + call("/multistream/1.0.0"), # Protocol acceptance - pytest.call(selected_protocol_str), + call(selected_protocol_str), ] ) # Ensure no other writes occurred @@ -227,6 +228,7 @@ async def test_get_mux_negotiate_success(basic_host, mock_communicator): assert mock_communicator.read.call_count == 2 +@pytest.mark.trio async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicator): """ Tests the negotiation flow when the proposed protocol is not found. @@ -241,6 +243,7 @@ async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicato mock_communicator.read.side_effect = [ "/multistream/1.0.0", # Handshake response str(non_existent_protocol), # Client proposes a non-existent protocol + MultiselectCommunicatorError("Mock is exhausted") ] # Expect a MultiselectError as the protocol won't be found @@ -250,9 +253,10 @@ async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicato # Verify handshake write and "na" (not available) write mock_communicator.write.assert_has_calls( [ - pytest.call("/multistream/1.0.0"), - pytest.call("na"), # Muxer should respond with "na" + call("/multistream/1.0.0"), + call("na"), # Muxer should respond with "na" ] ) assert mock_communicator.write.call_count == 2 - assert mock_communicator.read.call_count == 2 + # The read call count should be 3 due to the final loop attempt. + assert mock_communicator.read.call_count == 3 From bb8c6611f826c8b6999a9a1353c82904d24ad2eb Mon Sep 17 00:00:00 2001 From: Emmanuel Appah Date: Wed, 2 Jul 2025 15:38:13 +0100 Subject: [PATCH 4/8] Fix: Fix multiselect type consistency --- libp2p/abc.py | 14 +++-- libp2p/protocol_muxer/multiselect.py | 13 +++++ tests/conftest.py | 3 ++ tests/core/host/test_mux_type_compliance.py | 57 ++++++++++++++++----- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/libp2p/abc.py b/libp2p/abc.py index ecc4d0f16..a2ca1791c 100644 --- a/libp2p/abc.py +++ b/libp2p/abc.py @@ -1128,7 +1128,8 @@ def add_handler(self, protocol: TProtocol, handler: StreamHandlerFn) -> None: """ - def get_protocols(self) -> tuple[TProtocol | None, ...]: + @abstractmethod # Ensure this is present if it was implicitly removed + def get_protocols(self) -> tuple[TProtocol, ...]: """ Retrieve the protocols for which handlers have been registered. @@ -1138,7 +1139,10 @@ def get_protocols(self) -> tuple[TProtocol | None, ...]: A tuple of registered protocol names. """ - return tuple(self.handlers.keys()) + # For an abstract method, the body might be empty or a simple `pass`. + # If it was `return tuple(self.handlers.keys())`, it should remain unchanged. + # The key is the type annotation above. + pass # Or whatever the abstract method's body was @abstractmethod async def negotiate( @@ -1165,7 +1169,6 @@ async def negotiate( """ - # -------------------------- host interface.py -------------------------- @@ -1595,9 +1598,6 @@ def is_expired(self) -> bool: # ------------------ multiselect_communicator interface.py ------------------ - - - # -------------------------- multiselect_client interface.py -------------------------- @@ -1680,8 +1680,6 @@ async def try_select( """ - - # -------------------------- routing interface.py -------------------------- diff --git a/libp2p/protocol_muxer/multiselect.py b/libp2p/protocol_muxer/multiselect.py index 8f6e0e749..2692276d3 100644 --- a/libp2p/protocol_muxer/multiselect.py +++ b/libp2p/protocol_muxer/multiselect.py @@ -45,6 +45,19 @@ def add_handler( """ self.handlers[protocol] = handler + def get_protocols(self) -> tuple[TProtocol, ...]: + """ + Retrieve the protocols for which handlers have been registered. + + Returns + ------- + tuple[TProtocol, ...] + A tuple of registered protocol names. + + """ + # Filter out None values, as they are not considered valid "protocols" + return tuple(p for p in self.handlers.keys() if p is not None) + # FIXME: Make TProtocol Optional[TProtocol] to keep types consistent async def negotiate( self, communicator: IMultiselectCommunicator diff --git a/tests/conftest.py b/tests/conftest.py index ba3b7da0c..6ccaf9973 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,3 +4,6 @@ @pytest.fixture def security_protocol(): return None + + +pytest_plugins = ["trio"] diff --git a/tests/core/host/test_mux_type_compliance.py b/tests/core/host/test_mux_type_compliance.py index 134295625..605ec76bf 100644 --- a/tests/core/host/test_mux_type_compliance.py +++ b/tests/core/host/test_mux_type_compliance.py @@ -15,25 +15,20 @@ # For expected errors in negotiation tests from libp2p.protocol_muxer.exceptions import ( - MultiselectError, MultiselectCommunicatorError, + MultiselectError, ) from libp2p.protocol_muxer.multiselect import Multiselect -from libp2p.protocol_muxer.multiselect_client import ( - MultiselectClient, -) # Needed for mock calls from libp2p.protocol_muxer.multiselect_communicator import ( MultiselectCommunicator, -) # Needed for mock calls - -# --- Fixtures for setting up the test environment --- +) @pytest.fixture def mock_peer_id(): """Provides a mock PeerID for testing purposes.""" mock = MagicMock() - mock.__str__.return_value = "QmMockPeerId" + mock.__str__ = lambda: "QmMockPeerId" return mock @@ -81,7 +76,8 @@ def basic_host(mock_network_service): def mock_communicator(): """ Provides a mock for IMultiselectCommunicator for negotiation tests. - By default, it will provide responses for a successful handshake and a protocol proposal. + By default, it will provide responses for a successful handshake and a protocol + proposal. Reset side_effect in specific tests if different behavior is needed. """ mock = AsyncMock( @@ -146,6 +142,7 @@ def test_get_mux_interface_compliance(basic_host): # --- Functionality / Integration Tests --- + @pytest.mark.trio async def test_get_mux_add_handler_and_get_protocols(basic_host): """ @@ -203,7 +200,8 @@ async def test_get_mux_negotiate_success(basic_host, mock_communicator): # Configure the mock_communicator to simulate a successful negotiation sequence mock_communicator.read.side_effect = [ - "/multistream/1.0.0", # First read: Client sends its multistream protocol (handshake) + "/multistream/1.0.0", # First read: Client sends its multistream + # protocol (handshake) selected_protocol_str, # Second read: Client proposes the app protocol ] @@ -239,11 +237,12 @@ async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicato non_existent_protocol = TProtocol("/non-existent/protocol") assert non_existent_protocol not in mux.get_protocols() # Ensure it's not present - # Configure the mock_communicator to simulate a handshake followed by a non-existent protocol + # Configure the mock_communicator to simulate a handshake followed by a non-existent + # protocol mock_communicator.read.side_effect = [ "/multistream/1.0.0", # Handshake response str(non_existent_protocol), # Client proposes a non-existent protocol - MultiselectCommunicatorError("Mock is exhausted") + MultiselectCommunicatorError("Mock is exhausted"), ] # Expect a MultiselectError as the protocol won't be found @@ -260,3 +259,37 @@ async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicato assert mock_communicator.write.call_count == 2 # The read call count should be 3 due to the final loop attempt. assert mock_communicator.read.call_count == 3 + + +@pytest.mark.trio +async def test_mux_get_protocols_excludes_none(basic_host): + """ + Tests that get_protocols() method on the muxer returned by get_mux() + correctly excludes None from the returned list of protocols, + even if a handler was internally associated with a None protocol. + """ + mux = basic_host.get_mux() + + # Ensure no None protocol initially (assuming default setup doesn't add None) + assert None not in mux.get_protocols() + + # Artificially add a handler with None as the protocol. + # This simulates a scenario where None might exist as a key internally. + def dummy_none_handler(stream): + pass + + mux.add_handler(None, dummy_none_handler) + + # Now, retrieve the protocols and assert that None is NOT included + # in the list returned by get_protocols(), due to our fix. + retrieved_protocols = mux.get_protocols() + assert None not in retrieved_protocols + + # Also, ensure that other valid protocols are still present + # (optional, but good check) + # Add a valid protocol to ensure the filter doesn't remove everything + test_protocol = TProtocol("/test/valid-protocol/1.0.0") + mux.add_handler(test_protocol, lambda s: None) + retrieved_protocols_after_valid = mux.get_protocols() + assert test_protocol in retrieved_protocols_after_valid + assert None not in retrieved_protocols_after_valid From 050e20b06e4a7dde152de651de94f5c93d75cafb Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 15:49:14 +0100 Subject: [PATCH 5/8] FIX: conftest error --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index ba3b7da0c..ea271100e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ import pytest +pytest_plugins = ["trio"] + @pytest.fixture def security_protocol(): From 3496dd6644d6cdf8555ab98a948ee2a38d354eb2 Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 16:02:19 +0100 Subject: [PATCH 6/8] REFACTOR: linting checks and resolved --- libp2p/abc.py | 6 --- pyproject.toml | 1 - tests/core/host/test_mux_type_compliance.py | 46 ++++++++++----------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/libp2p/abc.py b/libp2p/abc.py index ecc4d0f16..100f47e46 100644 --- a/libp2p/abc.py +++ b/libp2p/abc.py @@ -1165,7 +1165,6 @@ async def negotiate( """ - # -------------------------- host interface.py -------------------------- @@ -1595,9 +1594,6 @@ def is_expired(self) -> bool: # ------------------ multiselect_communicator interface.py ------------------ - - - # -------------------------- multiselect_client interface.py -------------------------- @@ -1680,8 +1676,6 @@ async def try_select( """ - - # -------------------------- routing interface.py -------------------------- diff --git a/pyproject.toml b/pyproject.toml index 822c7973f..218603ac2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -285,4 +285,3 @@ project_excludes = [ [tool.poetry.group.dev.dependencies] pytest-trio = "^0.8.0" - diff --git a/tests/core/host/test_mux_type_compliance.py b/tests/core/host/test_mux_type_compliance.py index 134295625..cb3d6b0b1 100644 --- a/tests/core/host/test_mux_type_compliance.py +++ b/tests/core/host/test_mux_type_compliance.py @@ -15,16 +15,13 @@ # For expected errors in negotiation tests from libp2p.protocol_muxer.exceptions import ( - MultiselectError, MultiselectCommunicatorError, + MultiselectError, ) from libp2p.protocol_muxer.multiselect import Multiselect -from libp2p.protocol_muxer.multiselect_client import ( - MultiselectClient, -) # Needed for mock calls -from libp2p.protocol_muxer.multiselect_communicator import ( - MultiselectCommunicator, -) # Needed for mock calls + +# Needed for mock calls +from libp2p.protocol_muxer.multiselect_communicator import MultiselectCommunicator # --- Fixtures for setting up the test environment --- @@ -59,9 +56,8 @@ def mock_network_service(mock_peer_id, mock_peerstore): mock_network.get_peer_id.return_value = mock_peer_id mock_network.connections = {} # Simulate no active connections initially mock_network.listeners = {} # Simulate no active listeners initially - mock_network.set_stream_handler = ( - MagicMock() - ) # Mock setting stream handler if called during init + # Mock setting stream handler if called during init + mock_network.set_stream_handler = MagicMock() mock_network.new_stream = AsyncMock() # Mock for new_stream calls in BasicHost return mock_network @@ -81,12 +77,11 @@ def basic_host(mock_network_service): def mock_communicator(): """ Provides a mock for IMultiselectCommunicator for negotiation tests. - By default, it will provide responses for a successful handshake and a protocol proposal. - Reset side_effect in specific tests if different behavior is needed. + By default, it will provide responses for a successful handshake and a protocol + proposal. Reset side_effect in specific tests if different behavior is needed. """ - mock = AsyncMock( - spec=MultiselectCommunicator - ) # Use concrete spec for more accurate method mocks + # Use concrete spec for more accurate method mocks + mock = AsyncMock(spec=MultiselectCommunicator) mock.read = AsyncMock() mock.write = AsyncMock() return mock @@ -146,6 +141,7 @@ def test_get_mux_interface_compliance(basic_host): # --- Functionality / Integration Tests --- + @pytest.mark.trio async def test_get_mux_add_handler_and_get_protocols(basic_host): """ @@ -196,15 +192,16 @@ async def test_get_mux_negotiate_success(basic_host, mock_communicator): # Define a protocol and its handler that `negotiate` should successfully find selected_protocol_str = "/app/my-protocol/1.0.0" selected_protocol = TProtocol(selected_protocol_str) - dummy_negotiate_handler = AsyncMock( - spec=StreamHandlerFn - ) # Handler for the selected protocol + # Handler for the selected protocol + dummy_negotiate_handler = AsyncMock(spec=StreamHandlerFn) mux.add_handler(selected_protocol, dummy_negotiate_handler) - # Configure the mock_communicator to simulate a successful negotiation sequence + # Configure mock_communicator to simulate a successful negotiation mock_communicator.read.side_effect = [ - "/multistream/1.0.0", # First read: Client sends its multistream protocol (handshake) - selected_protocol_str, # Second read: Client proposes the app protocol + # First read: Client sends its multistream protocol (handshake) + "/multistream/1.0.0", + # Second read: Client proposes the app protocol + selected_protocol_str, ] # Perform the negotiation @@ -237,13 +234,14 @@ async def test_get_mux_negotiate_protocol_not_found(basic_host, mock_communicato # Ensure the protocol we propose isn't actually registered (beyond defaults) non_existent_protocol = TProtocol("/non-existent/protocol") - assert non_existent_protocol not in mux.get_protocols() # Ensure it's not present + # Ensure it's not present + assert non_existent_protocol not in mux.get_protocols() - # Configure the mock_communicator to simulate a handshake followed by a non-existent protocol + # Configure mock_communicator for a handshake followed by a non-existent protocol mock_communicator.read.side_effect = [ "/multistream/1.0.0", # Handshake response str(non_existent_protocol), # Client proposes a non-existent protocol - MultiselectCommunicatorError("Mock is exhausted") + MultiselectCommunicatorError("Mock is exhausted"), ] # Expect a MultiselectError as the protocol won't be found From c84e7b2cb48058b422049793cbb9e6305337854a Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 16:13:52 +0100 Subject: [PATCH 7/8] REFACTOR: linting checks and resolved --- tests/core/host/test_mux_type_compliance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/host/test_mux_type_compliance.py b/tests/core/host/test_mux_type_compliance.py index 43328a3ac..eff68c5c6 100644 --- a/tests/core/host/test_mux_type_compliance.py +++ b/tests/core/host/test_mux_type_compliance.py @@ -20,7 +20,7 @@ ) from libp2p.protocol_muxer.multiselect import Multiselect -# Needed for mock calls + from libp2p.protocol_muxer.multiselect_communicator import MultiselectCommunicator # --- Fixtures for setting up the test environment --- From b3016e82c543a29f84ffac95838ebb8800d1c0c2 Mon Sep 17 00:00:00 2001 From: mannyuncharted Date: Wed, 2 Jul 2025 16:50:28 +0100 Subject: [PATCH 8/8] FIX: resolved the issues with pytests --- pyproject.toml | 6 +----- tests/core/host/conftest.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 tests/core/host/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 218603ac2..9d7f355f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ "trio>=0.26.0", "fastecdsa==2.3.2; sys_platform != 'win32'", "zeroconf (>=0.147.0,<0.148.0)", - "pytest-trio (>=0.8.0,<0.9.0)", ] classifiers = [ "Development Status :: 4 - Beta", @@ -281,7 +280,4 @@ project_excludes = [ "**/*pb2.py", "**/*.pyi", ".venv/**", -] - -[tool.poetry.group.dev.dependencies] -pytest-trio = "^0.8.0" +] \ No newline at end of file diff --git a/tests/core/host/conftest.py b/tests/core/host/conftest.py deleted file mode 100644 index dee6ee6d5..000000000 --- a/tests/core/host/conftest.py +++ /dev/null @@ -1 +0,0 @@ -pytest_plugins = ["trio"]