|
@@ -111,7 +111,7 @@ def _model_silo_limit(t: type[Model]) -> ModelSiloLimit:
|
|
|
return silo_limit
|
|
|
|
|
|
|
|
|
-class AncestorAlreadySiloDecoratedException(Exception):
|
|
|
+class SubclassNotSiloDecoratedException(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
@@ -192,20 +192,47 @@ class _SiloModeTestModification:
|
|
|
def _create_overriding_test_class(
|
|
|
self, test_class: type[TestCase], silo_mode: SiloMode, name_suffix: str = ""
|
|
|
) -> type[TestCase]:
|
|
|
- def override_method(method_name: str) -> Callable[..., Any]:
|
|
|
- context = self.test_config(silo_mode)
|
|
|
- method: Callable[..., Any] = getattr(test_class, method_name)
|
|
|
- return context(method)
|
|
|
+ silo_mode_attr = "__silo_mode_override"
|
|
|
+
|
|
|
+ @contextmanager
|
|
|
+ def create_context(obj: TestCase) -> Generator[None, None, None]:
|
|
|
+ tagged_class, tagged_mode = getattr(obj, silo_mode_attr)
|
|
|
+
|
|
|
+ if type(obj) is not tagged_class:
|
|
|
+ # This condition indicates that the test case inherits the silo mode
|
|
|
+ # attribute from a superclass. Although we could just test in that
|
|
|
+ # mode, doing so would silently skip other modes if the superclass is
|
|
|
+ # supposed to be tested in more than one mode. So, enforce a general
|
|
|
+ # rule that test case subclasses must have decorators of their own.
|
|
|
+ sup = tagged_class.__name__
|
|
|
+ sub = type(obj).__name__
|
|
|
+ raise SubclassNotSiloDecoratedException(
|
|
|
+ f"A test class ({sub}) extends a silo-decorated test class ({sup}) "
|
|
|
+ f"without a silo decorator of its own. Add a decorator to {sub}. "
|
|
|
+ f"(You probably want to copy and paste the decorator from {sup}. "
|
|
|
+ f"If you don't want to run {sub} in a silo mode at all, use "
|
|
|
+ f"`@no_silo_test`.)"
|
|
|
+ )
|
|
|
+
|
|
|
+ with self.test_config(tagged_mode):
|
|
|
+ yield
|
|
|
|
|
|
# Unfortunately, due to the way DjangoTestCase setup and app manipulation works, `override_settings` in a
|
|
|
# run method produces unusual, broken results. We're forced to wrap the hidden methods that invoke setup
|
|
|
# test method in order to use override_settings correctly in django test cases.
|
|
|
- new_methods = {
|
|
|
- method_name: override_method(method_name)
|
|
|
- for method_name in ("_callSetUp", "_callTestMethod")
|
|
|
- }
|
|
|
+
|
|
|
+ def _callSetUp(obj: TestCase) -> Any:
|
|
|
+ with create_context(obj):
|
|
|
+ return TestCase._callSetUp(obj) # type: ignore[attr-defined]
|
|
|
+
|
|
|
+ def _callTestMethod(obj: TestCase, method: Any) -> Any:
|
|
|
+ with create_context(obj):
|
|
|
+ return TestCase._callTestMethod(obj, method) # type: ignore[attr-defined]
|
|
|
+
|
|
|
+ new_methods = {"_callSetUp": _callSetUp, "_callTestMethod": _callTestMethod}
|
|
|
name = test_class.__name__ + name_suffix
|
|
|
new_class = type(name, (test_class,), new_methods)
|
|
|
+ setattr(new_class, silo_mode_attr, (new_class, silo_mode))
|
|
|
return cast(type[TestCase], new_class)
|
|
|
|
|
|
def _arrange_silo_modes(self) -> tuple[SiloMode, Collection[SiloMode]]:
|
|
@@ -274,32 +301,6 @@ class _SiloModeTestModification:
|
|
|
|
|
|
return self._mark_parameterized_by_silo_mode(decorated_obj)
|
|
|
|
|
|
- def _validate_that_no_ancestor_is_silo_decorated(self, object_to_validate: Any):
|
|
|
- # Deprecated? Silo decorators at multiple places in the inheritance tree may
|
|
|
- # be necessary if a base class needs to be run in a non-default mode,
|
|
|
- # especially when the default is no longer region mode. The previous
|
|
|
- # rationale may have been limited to problems around swapping the local
|
|
|
- # region, which may now be resolved.
|
|
|
- #
|
|
|
- # TODO(RyanSkonnord): Delete this after ascertaining that it's safe to have
|
|
|
- # silo decorators on test case class ancestors
|
|
|
-
|
|
|
- class_queue = [object_to_validate]
|
|
|
-
|
|
|
- # Do a breadth-first traversal of all base classes to ensure that the
|
|
|
- # object does not inherit from a class which has already been decorated,
|
|
|
- # even in multi-inheritance scenarios.
|
|
|
- while len(class_queue) > 0:
|
|
|
- current_class = class_queue.pop(0)
|
|
|
- if getattr(current_class, "_silo_modes", None):
|
|
|
- raise AncestorAlreadySiloDecoratedException(
|
|
|
- f"Cannot decorate class '{object_to_validate.__name__}', "
|
|
|
- f"which inherits from a silo decorated class ({current_class.__name__})"
|
|
|
- )
|
|
|
- class_queue.extend(current_class.__bases__)
|
|
|
-
|
|
|
- object_to_validate._silo_modes = self.silo_modes
|
|
|
-
|
|
|
|
|
|
all_silo_test = SiloModeTestDecorator(*SiloMode)
|
|
|
"""
|