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# flake8: noqa E266
17
31USERVER_CONFIG_HOOKS = [
32 'userver_config_http_server',
33 'userver_config_http_client',
34 'userver_config_logging',
35 'userver_config_logging_otlp',
36 'userver_config_testsuite',
37 'userver_config_secdist',
38 'userver_config_testsuite_middleware',
39]
40
41
42# @cond
43
44
45logger = logging.getLogger(__name__)
46
47
48class _UserverConfigPlugin:
49 def __init__(self):
50 self._config_hooks = []
51
52 @property
53 def userver_config_hooks(self):
54 return self._config_hooks
55
56 def pytest_plugin_registered(self, plugin, manager):
57 if not isinstance(plugin, types.ModuleType):
58 return
59 uhooks = getattr(plugin, 'USERVER_CONFIG_HOOKS', None)
60 if uhooks is not None:
61 self._config_hooks.extend(uhooks)
62
63
64class _UserverConfig(typing.NamedTuple):
65 config_yaml: dict
66 config_vars: dict
67
68
69def pytest_configure(config):
70 config.pluginmanager.register(_UserverConfigPlugin(), 'userver_config')
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 choices=['trace', 'debug', 'info', 'warning', 'error', 'critical'],
79 )
80 group.addoption(
81 '--service-config',
82 type=pathlib.Path,
83 help='Path to service.yaml file.',
84 )
85 group.addoption(
86 '--service-config-vars',
87 type=pathlib.Path,
88 help='Path to config_vars.yaml file.',
89 )
90 group.addoption(
91 '--service-secdist',
92 type=pathlib.Path,
93 help='Path to secure_data.json file.',
94 )
95 group.addoption(
96 '--config-fallback',
97 type=pathlib.Path,
98 help='Path to dynamic config fallback file.',
99 )
100
101
102# @endcond
103
104
105@pytest.fixture(scope='session')
106def service_config_path(pytestconfig) -> pathlib.Path:
107 """
108 Returns the path to service.yaml file set by command line
109 `--service-config` option.
110
111 Override this fixture to change the way path to service.yaml is provided.
112
113 @ingroup userver_testsuite_fixtures
114 """
115 return pytestconfig.option.service_config
116
117
118@pytest.fixture(scope='session')
119def service_config_vars_path(pytestconfig) -> typing.Optional[pathlib.Path]:
120 """
121 Returns the path to config_vars.yaml file set by command line
122 `--service-config-vars` option.
123
124 Override this fixture to change the way path to config_vars.yaml is
125 provided.
126
127 @ingroup userver_testsuite_fixtures
128 """
129 return pytestconfig.option.service_config_vars
130
131
132@pytest.fixture(scope='session')
133def service_secdist_path(pytestconfig) -> typing.Optional[pathlib.Path]:
134 """
135 Returns the path to secure_data.json file set by command line
136 `--service-secdist` option.
137
138 Override this fixture to change the way path to secure_data.json is
139 provided.
140
141 @ingroup userver_testsuite_fixtures
142 """
143 return pytestconfig.option.service_secdist
144
145
146@pytest.fixture(scope='session')
147def config_fallback_path(pytestconfig) -> pathlib.Path:
148 """
149 Returns the path to dynamic config fallback file set by command line
150 `--config-fallback` option.
151
152 Override this fixture to change the way path to dynamic config fallback is
153 provided.
154
155 @ingroup userver_testsuite_fixtures
156 """
157 return pytestconfig.option.config_fallback
158
159
160@pytest.fixture(scope='session')
161def service_tmpdir(service_binary, tmp_path_factory):
162 """
163 Returns the path for temporary files. The path is the same for the whole
164 session and files are not removed (at least by this fixture) between
165 tests.
166
167 @ingroup userver_testsuite_fixtures
168 """
169 return tmp_path_factory.mktemp(
170 pathlib.Path(service_binary).name, numbered=False,
171 )
172
173
174@pytest.fixture(scope='session')
176 service_tmpdir, service_config, service_config_yaml,
177) -> pathlib.Path:
178 """
179 Dumps the contents of the service_config_yaml into a static config for
180 testsuite and returns the path to the config file.
181
182 @ingroup userver_testsuite_fixtures
183 """
184 dst_path = service_tmpdir / 'config.yaml'
185
186 logger.debug(
187 'userver fixture "service_config_path_temp" writes the patched static '
188 'config to "%s" equivalent to:\n%s',
189 dst_path,
190 yaml.dump(service_config),
191 )
192 dst_path.write_text(yaml.dump(service_config_yaml))
193
194 return dst_path
195
196
197@pytest.fixture(scope='session')
198def service_config_yaml(_service_config_hooked) -> dict:
199 """
200 Returns the static config values after the USERVER_CONFIG_HOOKS were
201 applied (if any). Prefer using
202 pytest_userver.plugins.config.service_config
203
204 @ingroup userver_testsuite_fixtures
205 """
206 return _service_config_hooked.config_yaml
207
208
209@pytest.fixture(scope='session')
210def service_config_vars(_service_config_hooked) -> dict:
211 """
212 Returns the static config variables (config_vars.yaml) values after the
213 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
214 pytest_userver.plugins.config.service_config
215
216 @ingroup userver_testsuite_fixtures
217 """
218 return _service_config_hooked.config_vars
219
220
221def _substitute_values(config, service_config_vars: dict, service_env) -> None:
222 if isinstance(config, dict):
223 for key, value in config.items():
224 if not isinstance(value, str):
225 _substitute_values(value, service_config_vars, service_env)
226 continue
227
228 if not value.startswith('$'):
229 continue
230
231 new_value = service_config_vars.get(value[1:])
232 if new_value is not None:
233 config[key] = new_value
234 continue
235
236 env = config.get(f'{key}#env')
237 if env:
238 if service_env:
239 new_value = service_env.get(service_env)
240 if not new_value:
241 new_value = os.environ.get(env)
242 if new_value:
243 config[key] = new_value
244 continue
245
246 fallback = config.get(f'{key}#fallback')
247 if fallback:
248 config[key] = fallback
249
250 if isinstance(config, list):
251 for i, value in enumerate(config):
252 if not isinstance(value, str):
253 _substitute_values(value, service_config_vars, service_env)
254 continue
255
256 if not value.startswith('$'):
257 continue
258
259 new_value = service_config_vars.get(value[1:])
260 if new_value is not None:
261 config[i] = new_value
262
263
264@pytest.fixture(scope='session')
266 service_config_yaml, service_config_vars, service_env,
267) -> dict:
268 """
269 Returns the static config values after the USERVER_CONFIG_HOOKS were
270 applied (if any) and with all the '$', environment and fallback variables
271 substituted.
272
273 @ingroup userver_testsuite_fixtures
274 """
275 config = copy.deepcopy(service_config_yaml)
276 _substitute_values(config, service_config_vars, service_env)
277 config.pop('config_vars', None)
278 return config
279
280
281@pytest.fixture(scope='session')
282def _original_service_config(
283 service_config_path, service_config_vars_path,
284) -> _UserverConfig:
285 config_vars: dict
286 config_yaml: dict
287
288 with open(service_config_path, mode='rt') as fp:
289 config_yaml = yaml.safe_load(fp)
290
291 if service_config_vars_path:
292 with open(service_config_vars_path, mode='rt') as fp:
293 config_vars = yaml.safe_load(fp)
294 else:
295 config_vars = {}
296
297 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
298
299
300@pytest.fixture(scope='session')
301def _service_config_hooked(
302 pytestconfig, request, service_tmpdir, _original_service_config,
303) -> _UserverConfig:
304 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
305 config_vars = copy.deepcopy(_original_service_config.config_vars)
306
307 plugin = pytestconfig.pluginmanager.get_plugin('userver_config')
308 for hook in plugin.userver_config_hooks:
309 if not callable(hook):
310 hook_func = request.getfixturevalue(hook)
311 else:
312 hook_func = hook
313 hook_func(config_yaml, config_vars)
314
315 if not config_vars:
316 config_yaml.pop('config_vars', None)
317 else:
318 config_vars_path = service_tmpdir / 'config_vars.yaml'
319 config_vars_text = yaml.dump(config_vars)
320 logger.debug(
321 'userver fixture "service_config" writes the patched static '
322 'config vars to "%s":\n%s',
323 config_vars_path,
324 config_vars_text,
325 )
326 config_vars_path.write_text(config_vars_text)
327 config_yaml['config_vars'] = str(config_vars_path)
328
329 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
330
331
332@pytest.fixture(scope='session')
333def userver_config_http_server(service_port, monitor_port):
334 """
335 Returns a function that adjusts the static configuration file for testsuite.
336 Sets the `server.listener.port` to listen on
337 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
338 sets the `server.listener-monitor.port` to listen on
339 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
340 fixture value.
341
342 @ingroup userver_testsuite_fixtures
343 """
344
345 def _patch_config(config_yaml, config_vars):
346 components = config_yaml['components_manager']['components']
347 if 'server' in components:
348 server = components['server']
349 if 'listener' in server:
350 server['listener']['port'] = service_port
351
352 if 'listener-monitor' in server:
353 server['listener-monitor']['port'] = monitor_port
354
355 return _patch_config
356
357
358@pytest.fixture(scope='session')
359def allowed_url_prefixes_extra() -> typing.List[str]:
360 """
361 By default, userver HTTP client is only allowed to talk to mockserver
362 when running in testsuite. This makes tests repeatable and encapsulated.
363
364 Override this fixture to whitelist some additional URLs.
365 It is still strongly advised to only talk to localhost in tests.
366
367 @ingroup userver_testsuite_fixtures
368 """
369 return []
370
371
372@pytest.fixture(scope='session')
374 mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
375):
376 """
377 Returns a function that adjusts the static configuration file for testsuite.
378 Sets increased timeout and limits allowed URLs for `http-client` component.
379
380 @ingroup userver_testsuite_fixtures
381 """
382
383 def patch_config(config, config_vars):
384 components: dict = config['components_manager']['components']
385 if not {'http-client', 'testsuite-support'}.issubset(
386 components.keys(),
387 ):
388 return
389 http_client = components['http-client'] or {}
390 http_client['testsuite-enabled'] = True
391 http_client['testsuite-timeout'] = '10s'
392
393 allowed_urls = [mockserver_info.base_url]
394 if mockserver_ssl_info:
395 allowed_urls.append(mockserver_ssl_info.base_url)
396 allowed_urls += allowed_url_prefixes_extra
397 http_client['testsuite-allowed-url-prefixes'] = allowed_urls
398
399 return patch_config
400
401
402@pytest.fixture(scope='session')
404 """
405 Default log level to use in userver if no caoomand line option was provided.
406
407 Returns 'debug'.
408
409 @ingroup userver_testsuite_fixtures
410 """
411 return 'debug'
412
413
414@pytest.fixture(scope='session')
415def userver_log_level(pytestconfig, userver_default_log_level) -> str:
416 """
417 Returns --service-log-level value if provided, otherwise returns
418 userver_default_log_level() value from fixture.
419
420 @ingroup userver_testsuite_fixtures
421 """
422 if pytestconfig.option.service_log_level:
423 return pytestconfig.option.service_log_level
424 return userver_default_log_level
425
426
427@pytest.fixture(scope='session')
428def userver_config_logging(userver_log_level, _service_logfile_path):
429 """
430 Returns a function that adjusts the static configuration file for testsuite.
431 Sets the `logging.loggers.default` to log to `@stderr` with level set
432 from `--service-log-level` pytest configuration option.
433
434 @ingroup userver_testsuite_fixtures
435 """
436
437 if _service_logfile_path:
438 default_file_path = str(_service_logfile_path)
439 else:
440 default_file_path = '@stderr'
441
442 def _patch_config(config_yaml, config_vars):
443 components = config_yaml['components_manager']['components']
444 if 'logging' in components:
445 loggers = components['logging'].setdefault('loggers', {})
446 for logger in loggers.values():
447 logger['file_path'] = '@null'
448 loggers['default'] = {
449 'file_path': default_file_path,
450 'level': userver_log_level,
451 'overflow_behavior': 'discard',
452 }
453 config_vars['logger_level'] = userver_log_level
454
455 return _patch_config
456
457
458@pytest.fixture(scope='session')
460 """
461 Returns a function that adjusts the static configuration file for testsuite.
462 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
463 leave the default file logger.
464
465 @ingroup userver_testsuite_fixtures
466 """
467
468 def _patch_config(config_yaml, config_vars):
469 components = config_yaml['components_manager']['components']
470 if 'otlp-logger' in components:
471 components['otlp-logger']['load-enabled'] = False
472
473 return _patch_config
474
475
476@pytest.fixture(scope='session')
477def userver_config_testsuite(pytestconfig, mockserver_info):
478 """
479 Returns a function that adjusts the static configuration file for testsuite.
480
481 Sets up `testsuite-support` component, which:
482
483 - increases timeouts for userver drivers
484 - disables periodic cache updates
485 - enables testsuite tasks
486
487 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
488 `tests-control.testpoint-url` to mockserver URL.
489
490 @ingroup userver_testsuite_fixtures
491 """
492
493 def _set_postgresql_options(testsuite_support: dict) -> None:
494 testsuite_support['testsuite-pg-execute-timeout'] = '35s'
495 testsuite_support['testsuite-pg-statement-timeout'] = '30s'
496 testsuite_support['testsuite-pg-readonly-master-expected'] = True
497
498 def _set_redis_timeout(testsuite_support: dict) -> None:
499 testsuite_support['testsuite-redis-timeout-connect'] = '40s'
500 testsuite_support['testsuite-redis-timeout-single'] = '30s'
501 testsuite_support['testsuite-redis-timeout-all'] = '30s'
502
503 def _disable_cache_periodic_update(testsuite_support: dict) -> None:
504 testsuite_support['testsuite-periodic-update-enabled'] = False
505
506 def patch_config(config, config_vars) -> None:
507 # Don't delay tests teardown unnecessarily.
508 config['components_manager'].pop('graceful_shutdown_interval', None)
509 components: dict = config['components_manager']['components']
510 if 'testsuite-support' not in components:
511 return
512 testsuite_support = components['testsuite-support'] or {}
513 testsuite_support['testsuite-increased-timeout'] = '30s'
514 testsuite_support['testsuite-grpc-is-tls-enabled'] = False
515 _set_postgresql_options(testsuite_support)
516 _set_redis_timeout(testsuite_support)
517 service_runner = pytestconfig.getoption('--service-runner-mode', False)
518 if not service_runner:
519 _disable_cache_periodic_update(testsuite_support)
520 testsuite_support['testsuite-tasks-enabled'] = not service_runner
521 testsuite_support['testsuite-periodic-dumps-enabled'] = (
522 '$userver-dumps-periodic'
523 )
524 components['testsuite-support'] = testsuite_support
525
526 config_vars['testsuite-enabled'] = True
527 if 'tests-control' in components:
528 components['tests-control']['testpoint-url'] = mockserver_info.url(
529 'testpoint',
530 )
531
532 return patch_config
533
534
535@pytest.fixture(scope='session')
536def userver_config_secdist(service_secdist_path):
537 """
538 Returns a function that adjusts the static configuration file for testsuite.
539 Sets the `default-secdist-provider.config` to the value of
540 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
541 fixture.
542
543 @ingroup userver_testsuite_fixtures
544 """
545
546 def _patch_config(config_yaml, config_vars):
547 if not service_secdist_path:
548 return
549
550 components = config_yaml['components_manager']['components']
551 if 'default-secdist-provider' not in components:
552 return
553
554 if not service_secdist_path.is_file():
555 raise ValueError(
556 f'"{service_secdist_path}" is not a file. Provide a '
557 f'"--service-secdist" pytest option or override the '
558 f'"service_secdist_path" fixture.',
559 )
560 components['default-secdist-provider']['config'] = str(
561 service_secdist_path,
562 )
563
564 return _patch_config
565
566
567@pytest.fixture(scope='session')
568def userver_config_testsuite_middleware(
569 userver_testsuite_middleware_enabled: bool,
570):
571 def patch_config(config_yaml, config_vars):
572 if not userver_testsuite_middleware_enabled:
573 return
574
575 components = config_yaml['components_manager']['components']
576 if 'server' not in components:
577 return
578
579 pipeline_builder = components.setdefault(
580 'default-server-middleware-pipeline-builder', {},
581 )
582 middlewares = pipeline_builder.setdefault('append', [])
583 middlewares.append('testsuite-exceptions-handling-middleware')
584
585 return patch_config
586
587
588@pytest.fixture(scope='session')
590 """Enabled testsuite middleware."""
591 return True