userver: /data/code/service_template/third_party/userver/testsuite/pytest_plugins/pytest_userver/plugins/config.py Source File
Loading...
Searching...
No Matches
config.py
1"""
2Work with the configuration files of the service in testsuite.
3"""
4
5import copy
6import logging
7import pathlib
8import types
9import typing
10
11import pytest
12import yaml
13
14
15# flake8: noqa E266
16
30USERVER_CONFIG_HOOKS = [
31 'userver_config_http_server',
32 'userver_config_http_client',
33 'userver_config_logging',
34 'userver_config_testsuite',
35 'userver_config_secdist',
36]
37
38
39# @cond
40
41
42logger = logging.getLogger(__name__)
43
44
45class _UserverConfigPlugin:
46 def __init__(self):
47 self._config_hooks = []
48
49 @property
50 def userver_config_hooks(self):
51 return self._config_hooks
52
53 def pytest_plugin_registered(self, plugin, manager):
54 if not isinstance(plugin, types.ModuleType):
55 return
56 uhooks = getattr(plugin, 'USERVER_CONFIG_HOOKS', None)
57 if uhooks is not None:
58 self._config_hooks.extend(uhooks)
59
60
61class _UserverConfig(typing.NamedTuple):
62 config_yaml: dict
63 config_vars: dict
64
65
66def pytest_configure(config):
67 config.pluginmanager.register(_UserverConfigPlugin(), 'userver_config')
68 config.addinivalue_line(
69 'markers', 'config: per-test dynamic config values',
70 )
71
72
73def pytest_addoption(parser) -> None:
74 group = parser.getgroup('userver-config')
75 group.addoption(
76 '--service-log-level',
77 type=str.lower,
78 default='debug',
79 choices=['trace', 'debug', 'info', 'warning', 'error', 'critical'],
80 )
81 group.addoption(
82 '--service-config',
83 type=pathlib.Path,
84 help='Path to service.yaml file.',
85 )
86 group.addoption(
87 '--service-config-vars',
88 type=pathlib.Path,
89 help='Path to config_vars.yaml file.',
90 )
91 group.addoption(
92 '--service-secdist',
93 type=pathlib.Path,
94 help='Path to secure_data.json file.',
95 )
96 group.addoption(
97 '--config-fallback',
98 type=pathlib.Path,
99 help='Path to dynamic config fallback file.',
100 )
101
102
103# @endcond
104
105
106@pytest.fixture(name='service_config_path', scope='session')
107def _service_config_path(pytestconfig) -> pathlib.Path:
108 """
109 Returns the path to service.yaml file set by command line
110 `--service-config` option.
111
112 Override this fixture to change the way path to service.yaml is provided.
113
114 @ingroup userver_testsuite_fixtures
115 """
116 return pytestconfig.option.service_config
117
118
119@pytest.fixture(name='service_config_vars_path', scope='session')
120def _service_config_vars_path(pytestconfig) -> typing.Optional[pathlib.Path]:
121 """
122 Returns the path to config_vars.yaml file set by command line
123 `--service-config-vars` option.
124
125 Override this fixture to change the way path to config_vars.yaml is
126 provided.
127
128 @ingroup userver_testsuite_fixtures
129 """
130 return pytestconfig.option.service_config_vars
131
132
133@pytest.fixture(name='service_secdist_path', scope='session')
134def _service_secdist_path(pytestconfig) -> typing.Optional[pathlib.Path]:
135 """
136 Returns the path to secure_data.json file set by command line
137 `--service-secdist` option.
138
139 Override this fixture to change the way path to secure_data.json is
140 provided.
141
142 @ingroup userver_testsuite_fixtures
143 """
144 return pytestconfig.option.service_secdist
145
146
147@pytest.fixture(scope='session')
148def config_fallback_path(pytestconfig) -> pathlib.Path:
149 """
150 Returns the path to dynamic config fallback file set by command line
151 `--config-fallback` option.
152
153 Override this fixture to change the way path to dynamic config fallback is
154 provided.
155
156 @ingroup userver_testsuite_fixtures
157 """
158 return pytestconfig.option.config_fallback
159
160
161@pytest.fixture(name='service_tmpdir', scope='session')
162def _service_tmpdir(service_binary, tmp_path_factory):
163 """
164 Returns the path for temporary files. The path is the same for the whole
165 session and files are not removed (at least by this fixture) between
166 tests.
167
168 @ingroup userver_testsuite_fixtures
169 """
170 return tmp_path_factory.mktemp(pathlib.Path(service_binary).name)
171
172
173@pytest.fixture(scope='session')
175 service_tmpdir, service_config_yaml,
176) -> pathlib.Path:
177 """
178 Dumps the contents of the service_config_yaml into a static config for
179 testsuite and returns the path to the config file.
180
181 @ingroup userver_testsuite_fixtures
182 """
183 dst_path = service_tmpdir / 'config.yaml'
184
185 config_text = yaml.dump(service_config_yaml)
186 logger.debug(
187 'userver fixture "service_config_path_temp" writes the patched static '
188 'config to "%s":\n%s',
189 dst_path,
190 config_text,
191 )
192 dst_path.write_text(config_text)
193
194 return dst_path
195
196
197@pytest.fixture(name='service_config_yaml', scope='session')
198def _service_config_yaml(_service_config) -> dict:
199 """
200 Returns the static config values after the USERVER_CONFIG_HOOKS were
201 applied (if any).
202
203 @ingroup userver_testsuite_fixtures
204 """
205 return _service_config.config_yaml
206
207
208@pytest.fixture(scope='session')
209def service_config_vars(_service_config) -> dict:
210 """
211 Returns the static config variables (config_vars.yaml) values after the
212 USERVER_CONFIG_HOOKS were applied (if any).
213
214 @ingroup userver_testsuite_fixtures
215 """
216 return _service_config.config_vars
217
218
219@pytest.fixture(name='_original_service_config', scope='session')
220def _original_service_config_fixture(
221 service_config_path, service_config_vars_path,
222) -> _UserverConfig:
223 config_vars: dict
224 config_yaml: dict
225
226 with open(service_config_path, mode='rt') as fp:
227 config_yaml = yaml.safe_load(fp)
228
229 if service_config_vars_path:
230 with open(service_config_vars_path, mode='rt') as fp:
231 config_vars = yaml.safe_load(fp)
232 else:
233 config_vars = {}
234
235 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
236
237
238@pytest.fixture(scope='session')
239def _service_config(
240 pytestconfig, request, service_tmpdir, _original_service_config,
241) -> _UserverConfig:
242 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
243 config_vars = copy.deepcopy(_original_service_config.config_vars)
244
245 plugin = pytestconfig.pluginmanager.get_plugin('userver_config')
246 for hook in plugin.userver_config_hooks:
247 if not callable(hook):
248 hook_func = request.getfixturevalue(hook)
249 else:
250 hook_func = hook
251 hook_func(config_yaml, config_vars)
252
253 if not config_vars:
254 config_yaml.pop('config_vars', None)
255 else:
256 config_vars_path = service_tmpdir / 'config_vars.yaml'
257 config_vars_text = yaml.dump(config_vars)
258 logger.debug(
259 'userver fixture "service_config" writes the patched static '
260 'config vars to "%s":\n%s',
261 config_vars_path,
262 config_vars_text,
263 )
264 config_vars_path.write_text(config_vars_text)
265 config_yaml['config_vars'] = str(config_vars_path)
266
267 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
268
269
270@pytest.fixture(scope='session')
271def userver_config_http_server(service_port, monitor_port):
272 """
273 Returns a function that adjusts the static configuration file for testsuite.
274 Sets the `server.listener.port` to listen on
275 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
276 sets the `server.listener-monitor.port` to listen on
277 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
278 fixture value.
279
280 @ingroup userver_testsuite_fixtures
281 """
282
283 def _patch_config(config_yaml, config_vars):
284 components = config_yaml['components_manager']['components']
285 if 'server' in components:
286 server = components['server']
287 if 'listener' in server:
288 server['listener']['port'] = service_port
289
290 if 'listener-monitor' in server:
291 server['listener-monitor']['port'] = monitor_port
292
293 return _patch_config
294
295
296@pytest.fixture(name='allowed_url_prefixes_extra', scope='session')
297def _allowed_url_prefixes_extra() -> typing.List[str]:
298 """
299 By default, userver HTTP client is only allowed to talk to mockserver
300 when running in testsuite. This makes tests repeatable and encapsulated.
301
302 Override this fixture to whitelist some additional URLs.
303 It is still strongly advised to only talk to localhost in tests.
304
305 @ingroup userver_testsuite_fixtures
306 """
307 return []
308
309
310@pytest.fixture(scope='session')
312 mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
313):
314 """
315 Returns a function that adjusts the static configuration file for testsuite.
316 Sets increased timeout and limits allowed URLs for `http-client` component.
317
318 @ingroup userver_testsuite_fixtures
319 """
320
321 def patch_config(config, config_vars):
322 components: dict = config['components_manager']['components']
323 if not {'http-client', 'testsuite-support'}.issubset(
324 components.keys(),
325 ):
326 return
327 http_client = components['http-client'] or {}
328 http_client['testsuite-enabled'] = True
329 http_client['testsuite-timeout'] = '10s'
330
331 allowed_urls = [mockserver_info.base_url]
332 if mockserver_ssl_info:
333 allowed_urls.append(mockserver_ssl_info.base_url)
334 allowed_urls += allowed_url_prefixes_extra
335 http_client['testsuite-allowed-url-prefixes'] = allowed_urls
336
337 return patch_config
338
339
340@pytest.fixture(scope='session')
341def userver_config_logging(pytestconfig):
342 """
343 Returns a function that adjusts the static configuration file for testsuite.
344 Sets the `logging.loggers.default` to log to `@stderr` with level set
345 from `--service-log-level` pytest configuration option.
346
347 @ingroup userver_testsuite_fixtures
348 """
349 log_level = pytestconfig.option.service_log_level
350
351 def _patch_config(config_yaml, config_vars):
352 components = config_yaml['components_manager']['components']
353 if 'logging' in components:
354 components['logging']['loggers'] = {
355 'default': {
356 'file_path': '@stderr',
357 'level': log_level,
358 'overflow_behavior': 'discard',
359 },
360 }
361 config_vars['logger_level'] = log_level
362
363 return _patch_config
364
365
366@pytest.fixture(scope='session')
367def userver_config_testsuite(pytestconfig, mockserver_info):
368 """
369 Returns a function that adjusts the static configuration file for testsuite.
370
371 Sets up `testsuite-support` component, which:
372
373 - increases timeouts for userver drivers
374 - disables periodic cache updates
375 - enables testsuite tasks
376
377 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
378 `tests-control.testpoint-url` to mockserver URL.
379
380 @ingroup userver_testsuite_fixtures
381 """
382
383 def _set_postgresql_options(testsuite_support: dict) -> None:
384 testsuite_support['testsuite-pg-execute-timeout'] = '35s'
385 testsuite_support['testsuite-pg-statement-timeout'] = '30s'
386 testsuite_support['testsuite-pg-readonly-master-expected'] = True
387
388 def _set_redis_timeout(testsuite_support: dict) -> None:
389 testsuite_support['testsuite-redis-timeout-connect'] = '40s'
390 testsuite_support['testsuite-redis-timeout-single'] = '30s'
391 testsuite_support['testsuite-redis-timeout-all'] = '30s'
392
393 def _disable_cache_periodic_update(testsuite_support: dict) -> None:
394 testsuite_support['testsuite-periodic-update-enabled'] = False
395
396 def patch_config(config, config_vars) -> None:
397 components: dict = config['components_manager']['components']
398 if 'testsuite-support' not in components:
399 return
400 testsuite_support = components['testsuite-support'] or {}
401 _set_postgresql_options(testsuite_support)
402 _set_redis_timeout(testsuite_support)
403 service_runner = pytestconfig.getoption('--service-runner-mode', False)
404 if not service_runner:
405 _disable_cache_periodic_update(testsuite_support)
406 testsuite_support['testsuite-tasks-enabled'] = not service_runner
407 testsuite_support[
408 'testsuite-periodic-dumps-enabled'
409 ] = '$userver-dumps-periodic'
410 components['testsuite-support'] = testsuite_support
411
412 config_vars['testsuite-enabled'] = True
413 if 'tests-control' in components:
414 components['tests-control']['testpoint-url'] = mockserver_info.url(
415 'testpoint',
416 )
417
418 return patch_config
419
420
421@pytest.fixture(scope='session')
422def userver_config_secdist(service_secdist_path):
423 """
424 Returns a function that adjusts the static configuration file for testsuite.
425 Sets the `default-secdist-provider.config` to the value of
426 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
427 fixture.
428
429 @ingroup userver_testsuite_fixtures
430 """
431
432 def _patch_config(config_yaml, config_vars):
433 if not service_secdist_path:
434 return
435
436 components = config_yaml['components_manager']['components']
437 if 'default-secdist-provider' not in components:
438 return
439
440 if not service_secdist_path.is_file():
441 raise ValueError(
442 f'"{service_secdist_path}" is not a file. Provide a '
443 f'"--service-secdist" pytest option or override the '
444 f'"service_secdist_path" fixture.',
445 )
446 components['default-secdist-provider']['config'] = str(
447 service_secdist_path,
448 )
449
450 return _patch_config