Source code for collective.redismemoize.testing

"""Test layers, and helpers to test an add-on against a fake Redis.

:data:`REDIS_MEMOIZE_FIXTURE` is for add-on authors: put it in a layer's
bases and every ``@ram.cache`` function goes through this package, backed by
an in-process ``fakeredis`` server instead of a real Redis. Each test starts
with an empty prefix, an empty RAM cache, a closed breaker and no learned
routes. ``fakeredis`` is not a dependency of this package; add it to your
test extra.

:func:`activate` and :func:`deactivate` are the same thing for a test runner
without layers, such as a plain pytest fixture.
"""

from collective.redismemoize import api
from collective.redismemoize import client
from collective.redismemoize import config
from collective.redismemoize.chooser import choose_cache
from collective.redismemoize.routing import router
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from plone.memoize.interfaces import ICacheChooser
from plone.testing import Layer
from plone.testing import zca
from plone.testing.zope import WSGI_SERVER_FIXTURE
from typing import Any
from zope.component import getGlobalSiteManager

import collective.redismemoize
import os


#: What :func:`activate` sets unless told otherwise. The URL is never
#: contacted: the client factory is replaced by ``fakeredis``.
TEST_ENVIRON = {
    "REDIS_MEMOIZE_URL": "redis://fakeredis:6379/0",
    "REDIS_MEMOIZE_PREFIX": "test.memoize",
}

_saved_environ: dict[str, str | None] | None = None


[docs] def activate(server: Any = None, **variables: str) -> Any: """Point the package at a ``fakeredis`` server. :param server: A ``fakeredis.FakeServer``; a new one when omitted. :param variables: ``REDIS_MEMOIZE_*`` variables to set on top of :data:`TEST_ENVIRON`. :returns: The server. """ import fakeredis global _saved_environ if server is None: server = fakeredis.FakeServer() environ = {**TEST_ENVIRON, **variables} if _saved_environ is None: _saved_environ = {name: os.environ.get(name) for name in config.DEFAULTS} for name in config.DEFAULTS: os.environ.pop(name, None) os.environ.update(environ) config.reload() client.set_factory(lambda settings: fakeredis.FakeRedis(server=server)) router.reset() # Values cached in RAM before activation would answer every Redis miss. for cache in api.ram_caches(): cache.invalidateAll() return server
[docs] def deactivate() -> None: """Restore the environment :func:`activate` changed, and reset the state.""" global _saved_environ if _saved_environ is not None: for name, value in _saved_environ.items(): if value is None: os.environ.pop(name, None) else: os.environ[name] = value _saved_environ = None config.reload() client.set_factory(None) router.reset()
[docs] def clean() -> None: """Empty the prefix and the RAM cache, close the breaker, forget routes.""" client.reset() api.invalidate_all() router.reset()
[docs] class RedisMemoizeLayer(Layer): """Route ``plone.memoize`` through this package and a fake Redis. Loads the package's ``overrides.zcml`` into a stacked component registry, so the chooser is in place even in a layer that never loaded this package's ZCML. """ defaultBases = (zca.ZCML_DIRECTIVES,)
[docs] def setUp(self) -> None: """Register the chooser and activate the fake server.""" self["configurationContext"] = context = zca.stackConfigurationContext( self.get("configurationContext") ) zca.pushGlobalRegistry() from zope.configuration import xmlconfig xmlconfig.includeOverrides( context, "overrides.zcml", package=collective.redismemoize ) context.execute_actions() self["redis_server"] = activate()
[docs] def tearDown(self) -> None: """Deactivate the fake server and drop the stacked registry.""" deactivate() del self["redis_server"] zca.popGlobalRegistry() del self["configurationContext"]
[docs] def testSetUp(self) -> None: """Start every test from an empty cache, with this package's chooser. The registry stacked in :meth:`setUp` is not always the one a test sees. A Plone site's component registry is based on the global registry of the moment the site was created, so for a layer such as ``PLONE_FIXTURE``, set up before or after this one, the stock chooser ``plone.memoize`` registered there wins. For the duration of each test, every registry in the global chain that registers another chooser gets this package's one instead. """ self._replaced_choosers = [] for registry in _global_registries(): for registration in registry.registeredUtilities(): if ( registration.provided is ICacheChooser and registration.name == "" and registration.component is not choose_cache ): self._replaced_choosers.append((registry, registration.component)) for registry, _component in self._replaced_choosers: registry.registerUtility(choose_cache, ICacheChooser) clean()
[docs] def testTearDown(self) -> None: """Put back the choosers :meth:`testSetUp` replaced.""" for registry, component in self.__dict__.pop("_replaced_choosers", []): registry.registerUtility(component, ICacheChooser)
def _global_registries() -> list[Any]: """Return the global component registry and all its bases. :returns: The registries, the current global one first. """ registries: list[Any] = [] pending = [getGlobalSiteManager()] while pending: registry = pending.pop(0) registries.append(registry) pending.extend(registry.__bases__) return registries REDIS_MEMOIZE_FIXTURE = RedisMemoizeLayer()
[docs] class PackageLayer(PloneSandboxLayer): """The package's ZCML, overrides included, on a Plone site.""" defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
[docs] def setUpZope(self, app, configurationContext): """Load the package's ZCML. ``plone.autoinclude`` is disabled in the Plone fixture, so the overrides are loaded here the way a real startup would load them. """ self.loadZCML(package=collective.redismemoize) from zope.configuration import xmlconfig xmlconfig.includeOverrides( configurationContext, "overrides.zcml", package=collective.redismemoize ) configurationContext.execute_actions()
FIXTURE = PackageLayer() INTEGRATION_TESTING = IntegrationTesting( bases=(FIXTURE,), name="Collective.RedismemoizeLayer:IntegrationTesting", ) FUNCTIONAL_TESTING = FunctionalTesting( bases=(FIXTURE, WSGI_SERVER_FIXTURE), name="Collective.RedismemoizeLayer:FunctionalTesting", )