"""Step definitions for plugins_loader_coverage.feature. These steps target specific uncovered lines in cleveragents/infrastructure/plugins/loader.py: - Lines 203-209: except block in load_from_entry_points when ep.load() fails - Lines 242, 244-246: validate_protocol fallback to issubclass on instantiation failure - Lines 247-248: validate_protocol when issubclass raises TypeError """ from typing import Protocol, runtime_checkable from unittest.mock import MagicMock, patch from behave import given, then, when from cleveragents.infrastructure.plugins.exceptions import ProtocolMismatchError from cleveragents.infrastructure.plugins.loader import PluginLoader # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the plugin loader module is imported") def step_plugin_loader_imported(context): """Verify the PluginLoader class is importable.""" assert PluginLoader is not None context.plugin_loader = PluginLoader() # --------------------------------------------------------------------------- # Entry-point discovery: failure path (lines 203-209) # --------------------------------------------------------------------------- @given("the entry points are mocked with one that raises on load") def step_mock_failing_entry_point(context): """Create a mock entry point whose load() raises an exception.""" failing_ep = MagicMock() failing_ep.name = "broken_plugin" failing_ep.value = "some.module:BrokenClass" failing_ep.load.side_effect = ImportError("Simulated import failure") # Mock the entry_points() call to return a selectable object mock_eps = MagicMock() mock_eps.select.return_value = [failing_ep] context.mock_entry_points = mock_eps context.failing_ep = failing_ep @when("I call load_from_entry_points") def step_call_load_from_entry_points(context): """Call load_from_entry_points with the mocked entry points.""" with patch( "cleveragents.infrastructure.plugins.loader.importlib.metadata.entry_points", return_value=context.mock_entry_points, ): context.discovered = context.plugin_loader.load_from_entry_points() @then("the plugin result should be an empty descriptor list") def step_verify_empty_descriptors(context): """The failing entry point should have been skipped.""" assert context.discovered == [], f"Expected empty list, got {context.discovered}" @then("the failed entry point should have been logged as a warning") def step_verify_warning_logged(context): """Verify the entry point was attempted (load was called).""" context.failing_ep.load.assert_called_once() # --------------------------------------------------------------------------- # validate_protocol: instantiation fails, issubclass succeeds (lines 242, 244-246) # --------------------------------------------------------------------------- @runtime_checkable class _SampleProtocol(Protocol): """A simple runtime-checkable Protocol for testing issubclass fallback.""" def do_work(self) -> str: ... @given("I have a class that requires constructor arguments") def step_class_requires_args(context): """Create a class whose __init__ requires mandatory arguments.""" class RequiresArgs(_SampleProtocol): def __init__(self, mandatory_arg): self._arg = mandatory_arg def do_work(self) -> str: return "done" context.non_instantiable_class = RequiresArgs @given("I have a runtime checkable protocol the class satisfies via issubclass") def step_protocol_satisfied_by_subclass(context): """Store the protocol that the class satisfies structurally.""" context.target_protocol = _SampleProtocol @when("I call validate_protocol with the non-instantiable class and protocol") def step_call_validate_protocol_subclass_fallback(context): """Call validate_protocol; expect it to fall back to issubclass and succeed.""" context.validate_result = PluginLoader.validate_protocol( context.non_instantiable_class, context.target_protocol, ) @then("validate_protocol should return True") def step_verify_validate_true(context): """The fallback issubclass check should have returned True.""" assert context.validate_result is True # --------------------------------------------------------------------------- # validate_protocol: instantiation fails, issubclass raises TypeError (lines 247-248) # --------------------------------------------------------------------------- @given("I have a class that cannot be instantiated without arguments") def step_class_cannot_instantiate(context): """Create a class whose __init__ raises when called with no args.""" class NeedsArgs: def __init__(self, required): self._r = required context.non_instantiable_class = NeedsArgs @given("I have a protocol-like object that causes issubclass to raise TypeError") def step_protocol_causes_typeerror(context): """Create a protocol-like object that causes issubclass to raise TypeError. We need an object that: - Has a __name__ attribute (so the error message in validate_protocol works) - Causes issubclass() to raise TypeError when used as second arg - Also causes isinstance() to raise TypeError A class with a metaclass that raises TypeError on __instancecheck__ and __subclasscheck__ achieves this. """ class TypeErrorMeta(type): def __instancecheck__(cls, instance): raise TypeError("Simulated TypeError from isinstance") def __subclasscheck__(cls, instance): raise TypeError("Simulated TypeError from issubclass") class BadProtocol(metaclass=TypeErrorMeta): pass context.target_protocol = BadProtocol @when("I call validate_protocol expecting a mismatch error") def step_call_validate_protocol_expect_error(context): """Call validate_protocol and capture the expected error.""" try: PluginLoader.validate_protocol( context.non_instantiable_class, context.target_protocol, ) context.validate_error = None except ProtocolMismatchError as exc: context.validate_error = exc except Exception as exc: context.validate_error = exc @then("a plugin-loader ProtocolMismatchError should be raised") def step_verify_protocol_mismatch(context): """Verify that ProtocolMismatchError was raised.""" assert context.validate_error is not None, "Expected an error but none was raised" assert isinstance(context.validate_error, ProtocolMismatchError), ( f"Expected ProtocolMismatchError, got {type(context.validate_error).__name__}: " f"{context.validate_error}" ) # --------------------------------------------------------------------------- # validate_protocol: instantiation fails, issubclass returns False # --------------------------------------------------------------------------- @given("I have a runtime checkable protocol the class does not satisfy") def step_protocol_not_satisfied(context): """Store a protocol the class does NOT satisfy.""" @runtime_checkable class UnrelatedProtocol(Protocol): def completely_different_method(self) -> int: ... context.target_protocol = UnrelatedProtocol