userver: /data/code/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
5# pylint: disable=redefined-outer-name
6import copy
7import logging
8import os
9import pathlib
10import types
11import typing
12
13import pytest
14import yaml
15
16
17# flake8: noqa E266
18
32USERVER_CONFIG_HOOKS = [
33 'userver_config_http_server',
34 'userver_config_http_client',
35 'userver_config_logging',
36 'userver_config_logging_otlp',
37 'userver_config_testsuite',
38 'userver_config_secdist',
39 'userver_config_testsuite_middleware',
40]
41
42
43# @cond
44
45
46logger = logging.getLogger(__name__)
47
48
49class _UserverConfigPlugin:
50 def __init__(self):
51 self._config_hooks = []
52
53 @property
54 def userver_config_hooks(self):
55 return self._config_hooks
56
57 def pytest_plugin_registered(self, plugin, manager):
58 if not isinstance(plugin, types.ModuleType):
59 return
60 uhooks = getattr(plugin, 'USERVER_CONFIG_HOOKS', None)
61 if uhooks is not None:
62 self._config_hooks.extend(uhooks)
63
64
65class _UserverConfig(typing.NamedTuple):
66 config_yaml: dict
67 config_vars: dict
68
69
70def pytest_configure(config):
71 config.pluginmanager.register(_UserverConfigPlugin(), 'userver_config')
72
73
74def pytest_addoption(parser) -> None:
75 group = parser.getgroup('userver-config')
76 group.addoption(
77 '--service-log-level',
78 type=str.lower,
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(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(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(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(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, 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 logger.debug(
186 'userver fixture "service_config_path_temp" writes the patched static '
187 'config to "%s" equivalent to:\n%s',
188 dst_path,
189 yaml.dump(service_config),
190 )
191 dst_path.write_text(yaml.dump(service_config_yaml))
192
193 return dst_path
194
195
196@pytest.fixture(scope='session')
197def service_config_yaml(_service_config_hooked) -> dict:
198 """
199 Returns the static config values after the USERVER_CONFIG_HOOKS were
200 applied (if any). Prefer using
201 pytest_userver.plugins.config.service_config
202
203 @ingroup userver_testsuite_fixtures
204 """
205 return _service_config_hooked.config_yaml
206
207
208@pytest.fixture(scope='session')
209def service_config_vars(_service_config_hooked) -> dict:
210 """
211 Returns the static config variables (config_vars.yaml) values after the
212 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
213 pytest_userver.plugins.config.service_config
214
215 @ingroup userver_testsuite_fixtures
216 """
217 return _service_config_hooked.config_vars
218
219
220def _substitute_values(config, service_config_vars: dict, service_env) -> None:
221 if isinstance(config, dict):
222 for key, value in config.items():
223 if not isinstance(value, str):
224 _substitute_values(value, service_config_vars, service_env)
225 continue
226
227 if not value.startswith('$'):
228 continue
229
230 new_value = service_config_vars.get(value[1:])
231 if new_value is not None:
232 config[key] = new_value
233 continue
234
235 env = config.get(f'{key}#env')
236 if env:
237 if service_env:
238 new_value = service_env.get(service_env)
239 if not new_value:
240 new_value = os.environ.get(env)
241 if new_value:
242 config[key] = new_value
243 continue
244
245 fallback = config.get(f'{key}#fallback')
246 if fallback:
247 config[key] = fallback
248
249 if isinstance(config, list):
250 for i, value in enumerate(config):
251 if not isinstance(value, str):
252 _substitute_values(value, service_config_vars, service_env)
253 continue
254
255 if not value.startswith('$'):
256 continue
257
258 new_value = service_config_vars.get(value[1:])
259 if new_value is not None:
260 config[i] = new_value
261
262
263@pytest.fixture(scope='session')
265 service_config_yaml, service_config_vars, service_env,
266) -> dict:
267 """
268 Returns the static config values after the USERVER_CONFIG_HOOKS were
269 applied (if any) and with all the '$', environment and fallback variables
270 substituted.
271
272 @ingroup userver_testsuite_fixtures
273 """
274 config = copy.deepcopy(service_config_yaml)
275 _substitute_values(config, service_config_vars, service_env)
276 config.pop('config_vars', None)
277 return config
278
279
280@pytest.fixture(scope='session')
281def _original_service_config(
282 service_config_path, service_config_vars_path,
283) -> _UserverConfig:
284 config_vars: dict
285 config_yaml: dict
286
287 with open(service_config_path, mode='rt') as fp:
288 config_yaml = yaml.safe_load(fp)
289
290 if service_config_vars_path:
291 with open(service_config_vars_path, mode='rt') as fp:
292 config_vars = yaml.safe_load(fp)
293 else:
294 config_vars = {}
295
296 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
297
298
299@pytest.fixture(scope='session')
300def _service_config_hooked(
301 pytestconfig, request, service_tmpdir, _original_service_config,
302) -> _UserverConfig:
303 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
304 config_vars = copy.deepcopy(_original_service_config.config_vars)
305
306 plugin = pytestconfig.pluginmanager.get_plugin('userver_config')
307 for hook in plugin.userver_config_hooks:
308 if not callable(hook):
309 hook_func = request.getfixturevalue(hook)
310 else:
311 hook_func = hook
312 hook_func(config_yaml, config_vars)
313
314 if not config_vars:
315 config_yaml.pop('config_vars', None)
316 else:
317 config_vars_path = service_tmpdir / 'config_vars.yaml'
318 config_vars_text = yaml.dump(config_vars)
319 logger.debug(
320 'userver fixture "service_config" writes the patched static '
321 'config vars to "%s":\n%s',
322 config_vars_path,
323 config_vars_text,
324 )
325 config_vars_path.write_text(config_vars_text)
326 config_yaml['config_vars'] = str(config_vars_path)
327
328 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
329
330
331@pytest.fixture(scope='session')
332def userver_config_http_server(service_port, monitor_port):
333 """
334 Returns a function that adjusts the static configuration file for testsuite.
335 Sets the `server.listener.port` to listen on
336 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
337 sets the `server.listener-monitor.port` to listen on
338 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
339 fixture value.
340
341 @ingroup userver_testsuite_fixtures
342 """
343
344 def _patch_config(config_yaml, config_vars):
345 components = config_yaml['components_manager']['components']
346 if 'server' in components:
347 server = components['server']
348 if 'listener' in server:
349 server['listener']['port'] = service_port
350
351 if 'listener-monitor' in server:
352 server['listener-monitor']['port'] = monitor_port
353
354 return _patch_config
355
356
357@pytest.fixture(scope='session')
358def allowed_url_prefixes_extra() -> typing.List[str]:
359 """
360 By default, userver HTTP client is only allowed to talk to mockserver
361 when running in testsuite. This makes tests repeatable and encapsulated.
362
363 Override this fixture to whitelist some additional URLs.
364 It is still strongly advised to only talk to localhost in tests.
365
366 @ingroup userver_testsuite_fixtures
367 """
368 return []
369
370
371@pytest.fixture(scope='session')
373 mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
374):
375 """
376 Returns a function that adjusts the static configuration file for testsuite.
377 Sets increased timeout and limits allowed URLs for `http-client` component.
378
379 @ingroup userver_testsuite_fixtures
380 """
381
382 def patch_config(config, config_vars):
383 components: dict = config['components_manager']['components']
384 if not {'http-client', 'testsuite-support'}.issubset(
385 components.keys(),
386 ):
387 return
388 http_client = components['http-client'] or {}
389 http_client['testsuite-enabled'] = True
390 http_client['testsuite-timeout'] = '10s'
391
392 allowed_urls = [mockserver_info.base_url]
393 if mockserver_ssl_info:
394 allowed_urls.append(mockserver_ssl_info.base_url)
395 allowed_urls += allowed_url_prefixes_extra
396 http_client['testsuite-allowed-url-prefixes'] = allowed_urls
397
398 return patch_config
399
400
401@pytest.fixture(scope='session')
403 """
404 Default log level to use in userver if no caoomand line option was provided.
405
406 Returns 'debug'.
407
408 @ingroup userver_testsuite_fixtures
409 """
410 return 'debug'
411
412
413@pytest.fixture(scope='session')
414def userver_log_level(pytestconfig, userver_default_log_level) -> str:
415 """
416 Returns --service-log-level value if provided, otherwise returns
417 userver_default_log_level() value from fixture.
418
419 @ingroup userver_testsuite_fixtures
420 """
421 if pytestconfig.option.service_log_level:
422 return pytestconfig.option.service_log_level
423 return userver_default_log_level
424
425
426@pytest.fixture(scope='session')
427def userver_config_logging(userver_log_level, _service_logfile_path):
428 """
429 Returns a function that adjusts the static configuration file for testsuite.
430 Sets the `logging.loggers.default` to log to `@stderr` with level set
431 from `--service-log-level` pytest configuration option.
432
433 @ingroup userver_testsuite_fixtures
434 """
435
436 if _service_logfile_path:
437 default_file_path = str(_service_logfile_path)
438 else:
439 default_file_path = '@stderr'
440
441 def _patch_config(config_yaml, config_vars):
442 components = config_yaml['components_manager']['components']
443 if 'logging' in components:
444 loggers = components['logging'].setdefault('loggers', {})
445 for logger in loggers.values():
446 logger['file_path'] = '@null'
447 loggers['default'] = {
448 'file_path': default_file_path,
449 'level': userver_log_level,
450 'overflow_behavior': 'discard',
451 }
452 config_vars['logger_level'] = userver_log_level
453
454 return _patch_config
455
456
457@pytest.fixture(scope='session')
459 """
460 Returns a function that adjusts the static configuration file for testsuite.
461 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
462 leave the default file logger.
463
464 @ingroup userver_testsuite_fixtures
465 """
466
467 def _patch_config(config_yaml, config_vars):
468 components = config_yaml['components_manager']['components']
469 if 'otlp-logger' in components:
470 components['otlp-logger']['load-enabled'] = False
471
472 return _patch_config
473
474
475@pytest.fixture(scope='session')
476def userver_config_testsuite(pytestconfig, mockserver_info):
477 """
478 Returns a function that adjusts the static configuration file for testsuite.
479
480 Sets up `testsuite-support` component, which:
481
482 - increases timeouts for userver drivers
483 - disables periodic cache updates
484 - enables testsuite tasks
485
486 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
487 `tests-control.testpoint-url` to mockserver URL.
488
489 @ingroup userver_testsuite_fixtures
490 """
491
492 def _set_postgresql_options(testsuite_support: dict) -> None:
493 testsuite_support['testsuite-pg-execute-timeout'] = '35s'
494 testsuite_support['testsuite-pg-statement-timeout'] = '30s'
495 testsuite_support['testsuite-pg-readonly-master-expected'] = True
496
497 def _set_redis_timeout(testsuite_support: dict) -> None:
498 testsuite_support['testsuite-redis-timeout-connect'] = '40s'
499 testsuite_support['testsuite-redis-timeout-single'] = '30s'
500 testsuite_support['testsuite-redis-timeout-all'] = '30s'
501
502 def _disable_cache_periodic_update(testsuite_support: dict) -> None:
503 testsuite_support['testsuite-periodic-update-enabled'] = False
504
505 def patch_config(config, config_vars) -> None:
506 components: dict = config['components_manager']['components']
507 if 'testsuite-support' not in components:
508 return
509 testsuite_support = components['testsuite-support'] or {}
510 testsuite_support['testsuite-increased-timeout'] = '30s'
511 testsuite_support['testsuite-grpc-is-tls-enabled'] = False
512 _set_postgresql_options(testsuite_support)
513 _set_redis_timeout(testsuite_support)
514 service_runner = pytestconfig.getoption('--service-runner-mode', False)
515 if not service_runner:
516 _disable_cache_periodic_update(testsuite_support)
517 testsuite_support['testsuite-tasks-enabled'] = not service_runner
518 testsuite_support[
519 'testsuite-periodic-dumps-enabled'
520 ] = '$userver-dumps-periodic'
521 components['testsuite-support'] = testsuite_support
522
523 config_vars['testsuite-enabled'] = True
524 if 'tests-control' in components:
525 components['tests-control']['testpoint-url'] = mockserver_info.url(
526 'testpoint',
527 )
528
529 return patch_config
530
531
532@pytest.fixture(scope='session')
533def userver_config_secdist(service_secdist_path):
534 """
535 Returns a function that adjusts the static configuration file for testsuite.
536 Sets the `default-secdist-provider.config` to the value of
537 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
538 fixture.
539
540 @ingroup userver_testsuite_fixtures
541 """
542
543 def _patch_config(config_yaml, config_vars):
544 if not service_secdist_path:
545 return
546
547 components = config_yaml['components_manager']['components']
548 if 'default-secdist-provider' not in components:
549 return
550
551 if not service_secdist_path.is_file():
552 raise ValueError(
553 f'"{service_secdist_path}" is not a file. Provide a '
554 f'"--service-secdist" pytest option or override the '
555 f'"service_secdist_path" fixture.',
556 )
557 components['default-secdist-provider']['config'] = str(
558 service_secdist_path,
559 )
560
561 return _patch_config
562
563
564@pytest.fixture(scope='session')
565def userver_config_testsuite_middleware(
566 userver_testsuite_middleware_enabled: bool,
567):
568 def patch_config(config_yaml, config_vars):
569 if not userver_testsuite_middleware_enabled:
570 return
571
572 components = config_yaml['components_manager']['components']
573 if 'server' not in components:
574 return
575
576 pipeline_builder = components.setdefault(
577 'default-server-middleware-pipeline-builder', {},
578 )
579 middlewares = pipeline_builder.setdefault('append', [])
580 middlewares.append('testsuite-exceptions-handling-middleware')
581
582 return patch_config
583
584
585@pytest.fixture(scope='session')
587 """Enabled testsuite middleware."""
588 return True