112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
"""``parser_available`` memoisation contract.
|
|
|
|
Round-3 review finding #14: ``/health`` used to trigger a heavy
|
|
``import everalgo.parser`` on every hit — pypdf / python-docx /
|
|
tesseract pulled in on the event loop, hundreds of ms of blocking
|
|
work per request. The fix wraps :func:`parser_available` in
|
|
``functools.lru_cache(maxsize=1)`` so only the first call in the
|
|
process pays the import cost; a lifespan warms that first call at
|
|
boot so ``/health`` itself never triggers it.
|
|
|
|
These tests pin the cache: the second call must reuse the cached
|
|
answer without re-attempting the import. ``cache_clear()`` is invoked
|
|
explicitly around each mutation of ``sys.modules`` to avoid
|
|
cross-test contamination — that discipline is contractual for any
|
|
test that toggles the module's importability.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from everos.component.parser import parser_available
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_cache_between_tests() -> None:
|
|
"""Drop the lru_cache before and after each test.
|
|
|
|
Without this, ``sys.modules`` swaps inside one test leak into the
|
|
next — the second test would see the frozen decision from the
|
|
first even after restoring ``sys.modules``.
|
|
"""
|
|
parser_available.cache_clear()
|
|
yield
|
|
parser_available.cache_clear()
|
|
|
|
|
|
def test_parser_available_second_call_hits_cache() -> None:
|
|
"""Second invocation must NOT re-run the ``import everalgo.parser``.
|
|
|
|
Uses :meth:`functools.lru_cache.cache_info` to confirm the second
|
|
call is a cache hit — the underlying ``import`` statement never
|
|
ran a second time, so ``/health`` can no longer trigger a fresh
|
|
import per request.
|
|
|
|
We deliberately avoid spying on ``builtins.__import__``: even a
|
|
cached ``import`` still goes through ``__import__`` for the
|
|
``sys.modules`` lookup, so a call-count assertion at that layer
|
|
can't distinguish "cached" from "re-imported". The cache-info
|
|
counters are the only reliable signal here.
|
|
"""
|
|
first = parser_available()
|
|
second = parser_available()
|
|
|
|
info = parser_available.cache_info()
|
|
assert first is second, "cached identity"
|
|
assert info.hits >= 1, f"expected at least one cache hit; got {info}"
|
|
assert info.misses == 1, f"expected exactly one miss (the warming call); got {info}"
|
|
|
|
|
|
def test_parser_available_caches_missing_extra_result() -> None:
|
|
"""When the extra isn't installed, the cache must remember ``False``
|
|
just as strongly as it would remember ``True``. Otherwise ``/health``
|
|
on a base install would re-attempt the (failing) import every hit."""
|
|
# Simulate the missing-extra case by making the module import raise
|
|
# ImportError. Patch `sys.modules` to insert a sentinel that Python's
|
|
# import machinery treats as "already imported → don't re-run"; use
|
|
# `builtins.__import__` wrapper to raise instead.
|
|
original_import = __import__
|
|
|
|
def _fake_import(name, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
if name == "everalgo.parser":
|
|
raise ImportError("simulated missing extra")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
with patch("builtins.__import__", side_effect=_fake_import):
|
|
first = parser_available()
|
|
second = parser_available()
|
|
|
|
# Both calls returned False — but critically only the first ran the
|
|
# failing import; the second was a hot cache read (proved by the fact
|
|
# that we can `parser_available.cache_info().hits >= 1`).
|
|
assert first is False
|
|
assert second is False
|
|
assert parser_available.cache_info().hits >= 1
|
|
|
|
|
|
def test_parser_available_cache_clear_re_evaluates() -> None:
|
|
"""``cache_clear()`` is contractually the escape hatch for tests —
|
|
prove it works, so future tests toggling ``sys.modules`` have a
|
|
reliable reset primitive."""
|
|
# Force a False decision, then clear + swap to a True decision.
|
|
fake_module = MagicMock()
|
|
|
|
def _import_missing(name, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
if name == "everalgo.parser":
|
|
raise ImportError("first pass: missing")
|
|
return __import__(name, *args, **kwargs)
|
|
|
|
with patch("builtins.__import__", side_effect=_import_missing):
|
|
assert parser_available() is False
|
|
|
|
parser_available.cache_clear()
|
|
|
|
# Now pretend the module is importable by pre-seeding sys.modules —
|
|
# `import everalgo.parser` will find it there and skip the loader.
|
|
with patch.dict(sys.modules, {"everalgo.parser": fake_module}):
|
|
assert parser_available() is True
|