|
@@ -1,10 +1,9 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import contextlib
|
|
|
-import functools
|
|
|
import logging
|
|
|
+import threading
|
|
|
from abc import ABC, abstractmethod
|
|
|
-from types import TracebackType
|
|
|
from typing import Any, Callable, Dict, Generator, Generic, Mapping, Optional, Type, TypeVar, cast
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
@@ -70,29 +69,36 @@ class DelegatedBySiloMode(Generic[ServiceInterface]):
|
|
|
self._singleton = {}
|
|
|
|
|
|
|
|
|
-def CreateStubFromBase(base: Type[ServiceInterface]) -> Type[ServiceInterface]:
|
|
|
+hc_test_stub: Any = threading.local()
|
|
|
+
|
|
|
+
|
|
|
+def CreateStubFromBase(
|
|
|
+ base: Type[ServiceInterface], target_mode: SiloMode
|
|
|
+) -> Type[ServiceInterface]:
|
|
|
"""
|
|
|
Using a concrete implementation class of a service, creates a new concrete implementation class suitable for a test
|
|
|
stub. It retains parity with the given base by passing through all of its abstract method implementations to the
|
|
|
- given base class, but wraps it with `exempt_from_silo_limits`, allowing tests written for monolith mode to largely
|
|
|
+ given base class, but wraps it to run in the target silo mode, allowing tests written for monolith mode to largely
|
|
|
work symmetrically. In the future, however, when monolith mode separate is deprecated, this logic should be
|
|
|
- replaced by true mocking utilities.
|
|
|
+ replaced by true mocking utilities, for say, target RPC endpoints.
|
|
|
|
|
|
This implementation will not work outside of test contexts.
|
|
|
"""
|
|
|
Super = base.__bases__[0]
|
|
|
|
|
|
- def __init__(self: Any, *args: Any, **kwds: Any) -> None:
|
|
|
- self.backing_service = base(*args, **kwds)
|
|
|
+ def __init__(self: Any, backing_service: ServiceInterface) -> None:
|
|
|
+ self.backing_service = backing_service
|
|
|
|
|
|
def close(self: Any) -> None:
|
|
|
self.backing_service.close()
|
|
|
|
|
|
def make_method(method_name: str) -> Any:
|
|
|
def method(self: Any, *args: Any, **kwds: Any) -> Any:
|
|
|
- from sentry.testutils.silo import exempt_from_silo_limits
|
|
|
+ from django.test import override_settings
|
|
|
|
|
|
- with exempt_from_silo_limits():
|
|
|
+ with override_settings(SILO_MODE=target_mode):
|
|
|
+ if cb := getattr(hc_test_stub, "cb", None):
|
|
|
+ cb(self.backing_service, method_name, *args, **kwds)
|
|
|
return getattr(self.backing_service, method_name)(*args, **kwds)
|
|
|
|
|
|
return method
|
|
@@ -109,6 +115,14 @@ def CreateStubFromBase(base: Type[ServiceInterface]) -> Type[ServiceInterface]:
|
|
|
return cast(Type[ServiceInterface], type(f"Stub{Super.__name__}", (Super,), methods))
|
|
|
|
|
|
|
|
|
+def stubbed(f: Callable[[], ServiceInterface], mode: SiloMode) -> Callable[[], ServiceInterface]:
|
|
|
+ def factory() -> ServiceInterface:
|
|
|
+ backing = f()
|
|
|
+ return cast(ServiceInterface, cast(Any, CreateStubFromBase(type(backing), mode))(backing))
|
|
|
+
|
|
|
+ return factory
|
|
|
+
|
|
|
+
|
|
|
def silo_mode_delegation(
|
|
|
mapping: Mapping[SiloMode, Callable[[], ServiceInterface]]
|
|
|
) -> ServiceInterface:
|
|
@@ -117,72 +131,3 @@ def silo_mode_delegation(
|
|
|
the mapping values.
|
|
|
"""
|
|
|
return cast(ServiceInterface, DelegatedBySiloMode(mapping))
|
|
|
-
|
|
|
-
|
|
|
-@contextlib.contextmanager
|
|
|
-def service_stubbed(
|
|
|
- service: InterfaceWithLifecycle,
|
|
|
- stub: Optional[InterfaceWithLifecycle],
|
|
|
- silo_mode: Optional[SiloMode] = None,
|
|
|
-) -> Generator[None, None, None]:
|
|
|
- """
|
|
|
- Replaces a service created with silo_mode_delegation with a replacement implementation while inside of the scope,
|
|
|
- closing the existing implementation on enter and closing the given implementation on exit.
|
|
|
- """
|
|
|
- if silo_mode is None:
|
|
|
- silo_mode = SiloMode.get_current_mode()
|
|
|
-
|
|
|
- if isinstance(service, DelegatedBySiloMode):
|
|
|
- with service.with_replacement(stub, silo_mode):
|
|
|
- yield
|
|
|
- else:
|
|
|
- raise ValueError("Service needs to be a DelegatedBySilMode object, but it was not!")
|
|
|
-
|
|
|
-
|
|
|
-class use_real_service:
|
|
|
- service: InterfaceWithLifecycle
|
|
|
- silo_mode: SiloMode | None
|
|
|
- context: contextlib.ExitStack
|
|
|
-
|
|
|
- def __init__(self, service: InterfaceWithLifecycle, silo_mode: SiloMode | None):
|
|
|
- self.silo_mode = silo_mode
|
|
|
- self.service = service
|
|
|
- self.context = contextlib.ExitStack()
|
|
|
-
|
|
|
- def __enter__(self) -> None:
|
|
|
- from django.test import override_settings
|
|
|
-
|
|
|
- if isinstance(self.service, DelegatedBySiloMode):
|
|
|
- if self.silo_mode is not None:
|
|
|
- self.context.enter_context(override_settings(SILO_MODE=self.silo_mode))
|
|
|
- self.context.enter_context(
|
|
|
- cast(
|
|
|
- Any,
|
|
|
- self.service.with_replacement(None, self.silo_mode),
|
|
|
- )
|
|
|
- )
|
|
|
- else:
|
|
|
- self.context.enter_context(
|
|
|
- cast(
|
|
|
- Any,
|
|
|
- self.service.with_replacement(None, SiloMode.get_current_mode()),
|
|
|
- )
|
|
|
- )
|
|
|
- else:
|
|
|
- raise ValueError("Service needs to be a DelegatedBySiloMode object, but it was not!")
|
|
|
-
|
|
|
- def __call__(self, f: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
- @functools.wraps(f)
|
|
|
- def wrapped(*args: Any, **kwds: Any) -> Any:
|
|
|
- with use_real_service(self.service, self.silo_mode):
|
|
|
- return f(*args, **kwds)
|
|
|
-
|
|
|
- return wrapped
|
|
|
-
|
|
|
- def __exit__(
|
|
|
- self,
|
|
|
- __exc_type: Type[BaseException] | None,
|
|
|
- __exc_value: BaseException | None,
|
|
|
- __traceback: TracebackType | None,
|
|
|
- ) -> bool | None:
|
|
|
- return self.context.__exit__(__exc_type, __exc_value, __traceback)
|