userver: /data/code/service_template/third_party/userver/testsuite/pytest_plugins/pytest_userver/plugins/dynamic_config.py Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
dynamic_config.py
1"""
2Supply dynamic configs for the service in testsuite.
3"""
4
5import copy
6import datetime
7import json
8import pathlib
9import typing
10
11import pytest
12
13from pytest_userver.plugins import caches
14
15USERVER_CONFIG_HOOKS = [
16 'userver_config_dynconf_cache',
17 'userver_config_dynconf_fallback',
18 'userver_config_dynconf_url',
19]
20
21_CONFIG_CACHES = ['dynamic-config-client-updater']
22
23
24class BaseError(Exception):
25 """Base class for exceptions from this module"""
26
27
29 """Config parameter was not found and no default was provided"""
30
31
33 """Simple dynamic config backend."""
34
35 def __init__(
36 self,
37 *,
38 initial_values: typing.Dict[str, typing.Any],
39 config_cache_components: typing.Iterable[str],
40 cache_invalidation_state: caches.InvalidationState,
41 ):
42 self._values = copy.deepcopy(initial_values)
43 self._cache_invalidation_state = cache_invalidation_state
44 self._config_cache_components = config_cache_components
45
46 def set_values(self, values):
47 self._values.update(values)
48 self._sync_with_service()
49
50 def get_values(self):
51 return self._values.copy()
52
53 def remove_values(self, keys):
54 extra_keys = set(keys).difference(self._values.keys())
55 if extra_keys:
57 f'Attempting to remove nonexistent configs: {extra_keys}',
58 )
59 for key in keys:
60 self._values.pop(key)
61 self._sync_with_service()
62
63 def set(self, **values):
64 self.set_values(values)
65
66 def get(self, key, default=None):
67 if key not in self._values:
68 if default is not None:
69 return default
70 raise DynamicConfigNotFoundError(f'Config {key!r} is not found')
71 return self._values[key]
72
73 def remove(self, key):
74 return self.remove_values([key])
75
76 def _sync_with_service(self):
77 self._cache_invalidation_state.invalidate(
78 self._config_cache_components,
79 )
80
81
82@pytest.fixture
84 request,
85 search_path,
86 load_json,
87 object_substitute,
88 config_service_defaults,
89 cache_invalidation_state,
90) -> DynamicConfig:
91 """
92 Fixture that allows to control dynamic config values used by the service.
93
94 After change to the config, be sure to call:
95 @code
96 await service_client.update_server_state()
97 @endcode
98
99 HTTP client requests call it automatically before each request.
100
101 @ingroup userver_testsuite_fixtures
102 """
103 all_values = config_service_defaults.copy()
104 for path in reversed(list(search_path('config.json'))):
105 values = load_json(path)
106 all_values.update(values)
107 for marker in request.node.iter_markers('config'):
108 marker_json = object_substitute(marker.kwargs)
109 all_values.update(marker_json)
110 return DynamicConfig(
111 initial_values=all_values,
112 config_cache_components=_CONFIG_CACHES,
113 cache_invalidation_state=cache_invalidation_state,
114 )
115
116
117@pytest.fixture
118def taxi_config(dynamic_config) -> DynamicConfig:
119 """
120 Deprecated, use `dynamic_config` instead.
121 """
122 return dynamic_config
123
124
125@pytest.fixture(scope='session')
126def dynamic_config_fallback_patch() -> typing.Dict[str, typing.Any]:
127 """
128 Override this fixture to replace some dynamic config values specifically
129 for testsuite tests:
130
131 @code
132 @pytest.fixture(scope='session')
133 def dynamic_config_fallback_patch():
134 return {"MY_CONFIG_NAME": 42}
135 @endcode
136
137 @ingroup userver_testsuite_fixtures
138 """
139 return {}
140
141
142@pytest.fixture(scope='session')
144 config_fallback_path, dynamic_config_fallback_patch,
145) -> typing.Dict[str, typing.Any]:
146 """
147 Fixture that returns default values for dynamic config. You may override
148 it in your local conftest.py or fixture:
149
150 @code
151 @pytest.fixture(scope='session')
152 def config_service_defaults():
153 with open('defaults.json') as fp:
154 return json.load(fp)
155 @endcode
156
157 @ingroup userver_testsuite_fixtures
158 """
159 if config_fallback_path and pathlib.Path(config_fallback_path).exists():
160 with open(config_fallback_path, 'r', encoding='utf-8') as file:
161 fallback = json.load(file)
162 fallback.update(dynamic_config_fallback_patch)
163 return fallback
164
165 raise RuntimeError(
166 'Either provide the path to dynamic config defaults file using '
167 '--config-fallback pytest option, or override '
168 f'{config_service_defaults.__name__} fixture to provide custom '
169 'dynamic config loading behavior.',
170 )
171
172
173@pytest.fixture(scope='session')
174def userver_config_dynconf_cache(service_tmpdir):
175 def patch_config(config, _config_vars) -> None:
176 components = config['components_manager']['components']
177 dynamic_config_component = components.get('dynamic-config', {})
178 if dynamic_config_component.get('fs-cache-path', '') == '':
179 return
180
181 cache_path = service_tmpdir / 'configs' / 'config_cache.json'
182
183 if cache_path.is_file():
184 # To avoid leaking dynamic config values between test sessions
185 cache_path.unlink()
186
187 dynamic_config_component['fs-cache-path'] = str(cache_path)
188
189 return patch_config
190
191
192_COMPONENTS_WITH_FALLBACK = {
193 'dynamic-config-fallbacks',
194 'dynamic-config-client-updater',
195}
196
197
198@pytest.fixture(scope='session')
200 pytestconfig, config_service_defaults, service_tmpdir,
201):
202 """
203 Returns a function that adjusts the static configuration file for
204 the testsuite.
205 Sets the `fallback-path` of the `dynamic-config-client-updater` and
206 `dynamic-config-fallbacks` according to `config_service_defaults`.
207
208 @ingroup userver_testsuite_fixtures
209 """
210
211 def _patch_config(config_yaml, _config_vars):
212 components = config_yaml['components_manager']['components']
213 if not (components.keys() & _COMPONENTS_WITH_FALLBACK):
214 return
215
216 fallback_path = (
217 service_tmpdir / 'configs' / 'dynamic_config_fallback.json'
218 )
219 fallback_path.parent.mkdir(exist_ok=True)
220 with open(fallback_path, 'w', encoding='utf-8') as file:
221 json.dump(config_service_defaults, file)
222
223 for component_name in _COMPONENTS_WITH_FALLBACK:
224 if component_name not in components:
225 continue
226 component = components[component_name]
227 component['fallback-path'] = str(fallback_path)
228
229 return _patch_config
230
231
232@pytest.fixture(scope='session')
233def userver_config_dynconf_url(mockserver_info):
234 """
235 Returns a function that adjusts the static configuration file for
236 the testsuite.
237 Sets the `dynamic-config-client.config-url` to the value of mockserver
238 configs-service, so that the
239 @ref pytest_userver.plugins.dynamic_config.mock_configs_service
240 "mock_configs_service" fixture could work.
241
242 @ingroup userver_testsuite_fixtures
243 """
244
245 def _patch_config(config, _config_vars) -> None:
246 components = config['components_manager']['components']
247 client = components.get('dynamic-config-client', None)
248 if client:
249 client['config-url'] = mockserver_info.url('configs-service')
250 client['append-path-to-url'] = True
251
252 return _patch_config
253
254
255@pytest.fixture
256def mock_configs_service(mockserver, dynamic_config) -> None:
257 """
258 Adds a mockserver handler that forwards dynamic_config to service's
259 `dynamic-config-client` component.
260
261 @ingroup userver_testsuite_fixtures
262 """
263
264 def service_timestamp():
265 return datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
266
267 @mockserver.json_handler('/configs-service/configs/values')
268 def _mock_configs(request):
269 values = dynamic_config.get_values()
270 if request.json.get('ids'):
271 values = {
272 name: values[name]
273 for name in request.json['ids']
274 if name in values
275 }
276 return {'configs': values, 'updated_at': service_timestamp()}
277
278 @mockserver.json_handler('/configs-service/configs/status')
279 def _mock_configs_status(_request):
280 return {'updated_at': service_timestamp()}