5b6224daa8
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m59s
CI / push-validation (pull_request) Successful in 36s
CI / build (pull_request) Successful in 1m5s
CI / helm (pull_request) Successful in 51s
CI / benchmark-regression (pull_request) Failing after 1m45s
CI / typecheck (pull_request) Successful in 2m23s
CI / quality (pull_request) Successful in 2m13s
CI / security (pull_request) Successful in 2m19s
CI / integration_tests (pull_request) Successful in 4m30s
CI / e2e_tests (pull_request) Successful in 4m42s
CI / unit_tests (pull_request) Successful in 5m54s
CI / docker (pull_request) Successful in 1m44s
CI / coverage (pull_request) Successful in 11m32s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Has started running
CI / helm (push) Successful in 38s
CI / build (push) Successful in 1m6s
CI / push-validation (push) Successful in 24s
CI / lint (push) Successful in 1m16s
CI / quality (push) Successful in 1m29s
CI / typecheck (push) Successful in 1m41s
CI / security (push) Successful in 1m55s
CI / integration_tests (push) Successful in 3m54s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 5m5s
CI / e2e_tests (push) Successful in 5m21s
CI / docker (push) Successful in 1m52s
CI / coverage (push) Successful in 15m41s
CI / status-check (push) Successful in 3s
- Add guard in TypeError fallback path: raise ProtocolMismatchError when issubclass raises TypeError and protocol has no inspectable members, preventing silent True return for unverifiable protocols - Update test scenario descriptions to accurately reflect the new implementation (no instantiation occurs at any point) - Update step definition comments to remove references to old instantiation-based code paths ISSUES CLOSED: #7418
202 lines
7.3 KiB
Python
202 lines
7.3 KiB
Python
"""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
|
|
- validate_protocol: issubclass succeeds (class satisfies protocol structurally)
|
|
- validate_protocol: issubclass raises TypeError for unverifiable protocol
|
|
- validate_protocol: issubclass returns False (class missing required members)
|
|
"""
|
|
|
|
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 = "cleveragents.domain.models.acms.nonexistent: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: issubclass succeeds for class satisfying protocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@runtime_checkable
|
|
class _SampleProtocol(Protocol):
|
|
"""A simple runtime-checkable Protocol for testing issubclass check."""
|
|
|
|
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; issubclass succeeds and returns True."""
|
|
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 issubclass check should have returned True."""
|
|
assert context.validate_result is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_protocol: issubclass raises TypeError for unverifiable protocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("I have a class that cannot be instantiated without arguments")
|
|
def step_class_cannot_instantiate(context):
|
|
"""Create a class whose __init__ requires mandatory arguments."""
|
|
|
|
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
|
|
|
|
A class with a metaclass that raises TypeError on __subclasscheck__
|
|
achieves this. Since this protocol declares no members, the structural
|
|
fallback cannot verify conformance and must raise ProtocolMismatchError.
|
|
"""
|
|
|
|
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: issubclass returns False (class missing required members)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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
|