forked from HAL9000/cleveragents-core
31472b5413
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
171 lines
6.3 KiB
Python
171 lines
6.3 KiB
Python
"""Step definitions for services __init__ lazy-import coverage (round 3).
|
|
|
|
Exercises every lazy-loaded symbol declared in
|
|
``cleveragents.application.services._LAZY_IMPORTS`` so that the
|
|
``__getattr__`` code path (lines 561-569) is executed at runtime.
|
|
Each scenario mocks ``importlib.import_module`` to avoid pulling in the
|
|
full dependency graph and simply verifies that the lazy-loading machinery
|
|
resolves the requested attribute name correctly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
import types
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_PKG = "cleveragents.application.services"
|
|
|
|
|
|
def _get_fresh_module():
|
|
"""Return the services __init__ module after purging cached lazy attrs.
|
|
|
|
We reload the module so that every scenario exercises ``__getattr__``
|
|
afresh rather than hitting the ``globals()`` cache set by a previous
|
|
lazy load.
|
|
"""
|
|
mod = sys.modules.get(_PKG)
|
|
if mod is None:
|
|
mod = importlib.import_module(_PKG)
|
|
else:
|
|
# Remove any previously-cached lazy symbols from module globals
|
|
# so __getattr__ is triggered again.
|
|
for attr_name in list(mod._LAZY_IMPORTS):
|
|
mod.__dict__.pop(attr_name, None)
|
|
return mod
|
|
|
|
|
|
def _make_fake_submodule(attr_name: str) -> types.ModuleType:
|
|
"""Build a tiny fake module that carries ``attr_name`` as a sentinel."""
|
|
fake = types.ModuleType(f"fake_{attr_name}")
|
|
setattr(fake, attr_name, type(attr_name, (), {}))
|
|
return fake
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("svcov3 a fresh services module")
|
|
def step_svcov3_fresh_module(context):
|
|
"""Prepare a clean services module and set up the import_module mock."""
|
|
context.svcov3_mod = _get_fresh_module()
|
|
context.svcov3_error = None
|
|
context.svcov3_value = None
|
|
|
|
# Patch importlib.import_module inside the services __init__ so that we
|
|
# do not trigger heavy transitive imports. The patched version creates
|
|
# lightweight fake modules on the fly.
|
|
original_import_module = importlib.import_module
|
|
|
|
def _patched_import_module(name, package=None):
|
|
# Only intercept calls that originate from the lazy-loader.
|
|
if package == _PKG and name.startswith("."):
|
|
submodule_name = name.lstrip(".")
|
|
full_name = f"{_PKG}.{submodule_name}"
|
|
# If the real submodule is already in sys.modules use it -
|
|
# this avoids breakage when earlier tests legitimately loaded
|
|
# the submodule.
|
|
if full_name in sys.modules:
|
|
return sys.modules[full_name]
|
|
# Otherwise try a real import; fall back to a fake module.
|
|
try:
|
|
return original_import_module(name, package)
|
|
except Exception:
|
|
# Build a fake module that provides every attr declared
|
|
# in _LAZY_IMPORTS for this submodule.
|
|
fake = types.ModuleType(full_name)
|
|
lazy = context.svcov3_mod._LAZY_IMPORTS
|
|
for _sym, (sub, attr) in lazy.items():
|
|
if sub == submodule_name:
|
|
setattr(fake, attr, type(attr, (), {}))
|
|
sys.modules[full_name] = fake
|
|
return fake
|
|
return original_import_module(name, package)
|
|
|
|
patcher = patch("importlib.import_module", side_effect=_patched_import_module)
|
|
patcher.start()
|
|
context.add_cleanup(patcher.stop)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('svcov3 I access lazy attribute "{attr_name}"')
|
|
def step_svcov3_access_lazy(context, attr_name):
|
|
"""Trigger ``__getattr__`` for *attr_name* and store the result."""
|
|
try:
|
|
# Remove from globals to force __getattr__ path
|
|
context.svcov3_mod.__dict__.pop(attr_name, None)
|
|
context.svcov3_value = getattr(context.svcov3_mod, attr_name)
|
|
except Exception as exc:
|
|
context.svcov3_error = exc
|
|
|
|
|
|
@when('svcov3 I access lazy attribute "{attr_name}" again')
|
|
def step_svcov3_access_lazy_again(context, attr_name):
|
|
"""Access the attribute a second time — should come from globals cache."""
|
|
try:
|
|
context.svcov3_value = getattr(context.svcov3_mod, attr_name)
|
|
except Exception as exc:
|
|
context.svcov3_error = exc
|
|
|
|
|
|
@when('svcov3 I access an unknown attribute "{attr_name}"')
|
|
def step_svcov3_access_unknown(context, attr_name):
|
|
"""Try to access an attribute not in _LAZY_IMPORTS."""
|
|
try:
|
|
getattr(context.svcov3_mod, attr_name)
|
|
except AttributeError as exc:
|
|
context.svcov3_error = exc
|
|
except Exception as exc:
|
|
context.svcov3_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('svcov3 the attribute "{attr_name}" should be resolved')
|
|
def step_svcov3_resolved(context, attr_name):
|
|
"""Assert the lazy attribute was resolved without error."""
|
|
assert context.svcov3_error is None, (
|
|
f"Expected no error for {attr_name!r}, got: {context.svcov3_error}"
|
|
)
|
|
assert context.svcov3_value is not None, (
|
|
f"Expected a non-None value for {attr_name!r}"
|
|
)
|
|
|
|
|
|
@then('svcov3 an AttributeError should be stored for "{attr_name}"')
|
|
def step_svcov3_attribute_error(context, attr_name):
|
|
"""Assert that an ``AttributeError`` was raised."""
|
|
assert isinstance(context.svcov3_error, AttributeError), (
|
|
f"Expected AttributeError, got {type(context.svcov3_error).__name__}: "
|
|
f"{context.svcov3_error}"
|
|
)
|
|
assert attr_name in str(context.svcov3_error)
|
|
|
|
|
|
@then("svcov3 __all__ should equal sorted _LAZY_IMPORTS keys")
|
|
def step_svcov3_all_matches(context):
|
|
"""Verify ``__all__`` is the sorted list of ``_LAZY_IMPORTS`` keys."""
|
|
mod = context.svcov3_mod
|
|
expected = sorted(mod._LAZY_IMPORTS.keys())
|
|
assert mod.__all__ == expected, (
|
|
f"__all__ mismatch.\n"
|
|
f" Missing from __all__: {set(expected) - set(mod.__all__)}\n"
|
|
f" Extra in __all__: {set(mod.__all__) - set(expected)}"
|
|
)
|