diff --git a/changelog.d/15283.misc b/changelog.d/15283.misc new file mode 100644 index 000000000000..99d753f8f051 --- /dev/null +++ b/changelog.d/15283.misc @@ -0,0 +1 @@ +Speedup tests by caching HomeServerConfig instances. diff --git a/tests/unittest.py b/tests/unittest.py index f9160faa1d0c..3cb96df8b21d 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -16,6 +16,7 @@ import gc import hashlib import hmac +import json import logging import secrets import time @@ -28,12 +29,14 @@ Generic, Iterable, List, + Literal, NoReturn, Optional, Tuple, Type, TypeVar, Union, + overload, ) from unittest.mock import Mock, patch @@ -53,6 +56,7 @@ from synapse import events from synapse.api.constants import EventTypes from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion +from synapse.config._base import Config, RootConfig from synapse.config.homeserver import HomeServerConfig from synapse.config.server import DEFAULT_ROOM_VERSION from synapse.crypto.event_signing import add_hashes_and_signatures @@ -124,6 +128,68 @@ def new(*args: P.args, **kwargs: P.kwargs) -> R: return _around +@overload +def deepcopy_config(config: RootConfig, root: Literal[True]) -> RootConfig: + ... + + +@overload +def deepcopy_config(config: Config, root: Literal[False]) -> Config: + ... + + +def deepcopy_config(config, root): + if root: + new_config = config.__class__(config.config_files) + else: + new_config = config.__class__(config.root) + + for attr_name in config.__dict__: + if attr_name.startswith("__") or attr_name == "root": + continue + attr = getattr(config, attr_name) + if isinstance(attr, Config): + new_attr = deepcopy_config(attr, root=False) + else: + new_attr = attr + + setattr(new_config, attr_name, new_attr) + + return new_config + + +_make_homeserver_config_obj_cache: Dict[str, Union[RootConfig, Config]] = {} + + +def make_homeserver_config_obj(config: Dict[str, Any]) -> RootConfig: + """Creates a :class:`HomeServerConfig` instance with the given configuration dict. + + This is equivalent to:: + + config_obj = HomeServerConfig() + config_obj.parse_config_dict(config, "", "") + + but it keeps a cache of `HomeServerConfig` instances and deepcopies them as needed, + to avoid validating the whole configuration every time. + """ + cache_key = json.dumps(config) + + if cache_key in _make_homeserver_config_obj_cache: + # Cache hit: reuse the existing instance + config_obj = _make_homeserver_config_obj_cache[cache_key] + else: + # Cache miss; create the actual instance + config_obj = HomeServerConfig() + config_obj.parse_config_dict(config, "", "") + + # Add to the cache + _make_homeserver_config_obj_cache[cache_key] = config_obj + + assert isinstance(config_obj, RootConfig) + + return deepcopy_config(config_obj, root=True) + + class TestCase(unittest.TestCase): """A subclass of twisted.trial's TestCase which looks for 'loglevel' attributes on both itself and its individual test methods, to override the @@ -518,8 +584,7 @@ def setup_test_homeserver(self, *args: Any, **kwargs: Any) -> HomeServer: config = kwargs["config"] # Parse the config from a config dict into a HomeServerConfig - config_obj = HomeServerConfig() - config_obj.parse_config_dict(config, "", "") + config_obj = make_homeserver_config_obj(config) kwargs["config"] = config_obj async def run_bg_updates() -> None: