userver: /data/code/service_template/third_party/userver/testsuite/pytest_plugins/pytest_userver/plugins/dynamic_config.py Source File
Loading...
Searching...
No Matches
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()}