2Work with the configuration files of the service in testsuite.
16from collections.abc
import Callable
17from collections.abc
import Mapping
18from typing
import TypeAlias
38USERVER_CONFIG_HOOKS = [
39 'userver_config_substitutions',
40 'userver_config_http_server',
41 'userver_config_http_client',
42 'userver_config_logging',
43 'userver_config_logging_otlp',
44 'userver_config_testsuite',
45 'userver_config_secdist',
46 'userver_config_testsuite_middleware',
47 'userver_config_deadlock_detector',
50ServiceConfigPatch: TypeAlias = Callable[[dict, dict],
None]
56logger = logging.getLogger(__name__)
59class _UserverConfigPlugin:
61 self._config_hooks = []
64 def userver_config_hooks(self):
65 return self._config_hooks
67 def pytest_plugin_registered(self, plugin, manager):
68 if not isinstance(plugin, types.ModuleType):
70 uhooks = getattr(plugin,
'USERVER_CONFIG_HOOKS',
None)
71 if uhooks
is not None:
72 self._config_hooks.extend(uhooks)
75@dataclasses.dataclass(frozen=True)
81def pytest_configure(config):
82 config.pluginmanager.register(_UserverConfigPlugin(),
'userver_config')
85def pytest_addoption(parser) -> None:
86 group = parser.getgroup(
'userver-config')
88 '--service-log-level',
90 choices=[
'trace',
'debug',
'info',
'warning',
'error',
'critical'],
95 help=
'Path to service.yaml file.',
98 '--service-config-vars',
100 help=
'Path to config_vars.yaml file.',
105 help=
'Path to secure_data.json file.',
110 help=
'Path to dynamic config fallback file.',
115 help=
'Dump config from binary before running tests',
122@pytest.fixture(scope='session')
125 Returns the path to service.yaml file set by command line
126 `--service-config` option.
128 Override this fixture to change the way path to the static config is provided.
130 @ingroup userver_testsuite_fixtures
132 if pytestconfig.option.dump_config:
136 pytestconfig.option.service_config,
138 return pytestconfig.option.service_config
141@pytest.fixture(scope='session')
144 Runs the service binary with `--dump-db-schema` argument, dumps the 0_db_schema.sql file with database schema and
147 Override this fixture to change the way to dump the database schema.
149 @ingroup userver_testsuite_fixtures
152 path = service_tmpdir.joinpath(
'schemas')
157 path /
'0_db_schema.sql',
162@pytest.fixture(scope='session')
165 Returns the path to config_vars.yaml file set by command line
166 `--service-config-vars` option.
168 Override this fixture to change the way path to config_vars.yaml is
171 @ingroup userver_testsuite_fixtures
173 return pytestconfig.option.service_config_vars
176@pytest.fixture(scope='session')
179 Returns the path to secure_data.json file set by command line
180 `--service-secdist` option.
182 Override this fixture to change the way path to secure_data.json is
185 @ingroup userver_testsuite_fixtures
187 return pytestconfig.option.service_secdist
190@pytest.fixture(scope='session')
193 Returns the path to dynamic config fallback file set by command line
194 `--config-fallback` option.
196 Override this fixture to change the way path to dynamic config fallback is
199 @ingroup userver_testsuite_fixtures
201 return pytestconfig.option.config_fallback
204@pytest.fixture(scope='session')
207 Returns the path for temporary files. The path is the same for the whole
208 session and files are not removed (at least by this fixture) between
211 @ingroup userver_testsuite_fixtures
213 return tmp_path_factory.mktemp(
214 pathlib.Path(service_binary).name,
219@pytest.fixture(scope='session')
227 Dumps the contents of the service_config_yaml and service_config_vars into a static config for
228 testsuite and returns the path to the config file.
230 @ingroup userver_testsuite_fixtures
232 dst_path = service_tmpdir /
'config.yaml'
234 service_config_yaml = dict(service_config_yaml)
235 if not service_config_vars:
236 service_config_yaml.pop(
'config_vars',
None)
238 config_vars_path = service_tmpdir /
'config_vars.yaml'
239 config_vars_text = yaml.dump(service_config_vars)
241 'userver fixture "service_config_path_temp" writes the patched static config vars to "%s":\n%s',
245 config_vars_path.write_text(config_vars_text)
246 service_config_yaml[
'config_vars'] = str(config_vars_path)
249 'userver fixture "service_config_path_temp" writes the patched static config to "%s" equivalent to:\n%s',
251 yaml.dump(service_config),
253 dst_path.write_text(yaml.dump(service_config_yaml))
258@pytest.fixture(scope='session')
261 Returns the static config values after the USERVER_CONFIG_HOOKS were
262 applied (if any). Prefer using
263 pytest_userver.plugins.config.service_config
265 @ingroup userver_testsuite_fixtures
267 return _service_config_hooked.config_yaml
270@pytest.fixture(scope='session')
273 Returns the static config variables (config_vars.yaml) values after the
274 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
275 pytest_userver.plugins.config.service_config
277 @ingroup userver_testsuite_fixtures
279 return _service_config_hooked.config_vars
282def _substitute_values(config, service_config_vars: dict, service_env) ->
None:
283 if isinstance(config, dict):
284 for key, value
in config.items():
285 if not isinstance(value, str):
286 _substitute_values(value, service_config_vars, service_env)
289 if not value.startswith(
'$'):
292 new_value = service_config_vars.get(value[1:])
293 if new_value
is not None:
294 config[key] = new_value
297 env = config.get(f
'{key}#env')
300 new_value = service_env.get(env)
302 new_value = os.environ.get(env)
304 config[key] = new_value
307 fallback = config.get(f
'{key}#fallback')
309 config[key] = fallback
314 if isinstance(config, list):
315 for i, value
in enumerate(config):
316 if not isinstance(value, str):
317 _substitute_values(value, service_config_vars, service_env)
320 if not value.startswith(
'$'):
323 new_value = service_config_vars.get(value[1:])
324 if new_value
is not None:
325 config[i] = new_value
328@pytest.fixture(scope='session')
331 A function that takes `config_yaml`, `config_vars` and applies all
332 substitutions just like the service would.
334 Useful when patching the service config. It's a good idea to pass
335 a component's config instead of the whole `config_yaml` to avoid
338 @warning The returned YAML is a clone, mutating it will not modify
339 the actual config while in a config hook!
341 @ingroup userver_testsuite_fixtures
344 def substitute(config_yaml, config_vars, /):
345 if config_yaml
is not None and not isinstance(config_yaml, dict)
and not isinstance(config_yaml, list):
347 f
'{substitute_config_vars.__name__} can only be meaningfully '
348 'called with dict and list nodes of config_yaml, while given: '
349 f
'{config_yaml!r}. Pass a containing object instead.',
352 config = copy.deepcopy(config_yaml)
353 _substitute_values(config, config_vars, service_env)
359@pytest.fixture(scope='session')
363 substitute_config_vars,
366 Returns the static config values after the USERVER_CONFIG_HOOKS were
367 applied (if any) and with all the '$', environment and fallback variables
370 @ingroup userver_testsuite_fixtures
373 config.pop(
'config_vars',
None)
377@pytest.fixture(scope='session')
378def _original_service_config(
380 service_config_vars_path,
385 assert service_config_path
is not None,
'Please specify proper path to the static config file, not None'
387 with open(service_config_path, mode=
'rt')
as fp:
388 config_yaml = yaml.safe_load(fp)
390 if service_config_vars_path:
391 with open(service_config_vars_path, mode=
'rt')
as fp:
392 config_vars = yaml.safe_load(fp)
396 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
399@pytest.fixture(scope='session')
400def _service_config_hooked(
404 _original_service_config,
406 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
407 config_vars = copy.deepcopy(_original_service_config.config_vars)
409 plugin = pytestconfig.pluginmanager.get_plugin(
'userver_config')
410 local_hooks = (daemon_scoped_mark
or {}).get(
'config_hooks', ())
412 for hook
in itertools.chain(plugin.userver_config_hooks, local_hooks):
413 if not callable(hook):
414 hook_func = request.getfixturevalue(hook)
417 hook_func(config_yaml, config_vars)
419 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
422@pytest.fixture(scope='session')
423def _service_config_substitution_vars(request, mockserver_info) -> Mapping[str, str]:
424 substitution_vars = {
425 'mockserver': mockserver_info.base_url.removesuffix(
'/'),
427 if request.config.pluginmanager.hasplugin(
'pytest_userver.plugins.grpc.mockserver'):
428 grpc_mockserver_endpoint = request.getfixturevalue(
'grpc_mockserver_endpoint')
429 substitution_vars[
'grpc_mockserver'] = grpc_mockserver_endpoint
430 return substitution_vars
433@pytest.fixture(scope='session')
436 Replaces substitution vars in all strings within `config_vars` using
437 [string.Template.substitute](https://docs.python.org/3/library/string.html#string.Template.substitute).
439 Substitution vars can be used as a shorthand for writing a full-fledged @ref SERVICE_CONFIG_HOOKS "config hook"
440 in many common cases.
442 Unlike normal `config_vars`, substitution vars can also apply to a part of a string.
443 For example, for `config_vars` entry
446 frobnicator-url: $mockserver/frobnicator
449 a possible patching result is as follows:
452 frobnicator-url: http://127.0.0.1:1234/frobnicator
455 Currently, the following substitution vars are supported:
457 * `mockserver` - mockserver url
458 * `grpc_mockserver` - grpc mockserver endpoint
460 @ingroup userver_testsuite_fixtures
463 def _substitute(key, value, parent: list | dict) ->
None:
464 if isinstance(value, str):
465 parent[key] = string.Template(value).safe_substitute(_service_config_substitution_vars)
466 elif isinstance(value, dict):
467 for child_key, child_value
in value.items():
468 _substitute(child_key, child_value, value)
469 elif isinstance(value, list):
470 for child_key, child_value
in enumerate(value):
471 _substitute(child_key, child_value, value)
473 def patch_config(config_yaml, config_vars):
474 for key, value
in config_vars.items():
475 _substitute(key, value, config_vars)
480@pytest.fixture(scope='session')
483 Returns a function that adjusts the static configuration file for testsuite.
484 Sets the `server.listener.port` to listen on
485 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
486 sets the `server.listener-monitor.port` to listen on
487 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
490 @ingroup userver_testsuite_fixtures
493 def _patch_config(config_yaml, config_vars):
494 components = config_yaml[
'components_manager'][
'components']
495 if 'server' in components:
496 server = components[
'server']
497 if 'listener' in server:
498 server[
'listener'][
'port'] = service_port
500 if 'listener-monitor' in server:
501 server[
'listener-monitor'][
'port'] = monitor_port
506@pytest.fixture(scope='session')
509 By default, userver HTTP client is only allowed to talk to mockserver
510 when running in testsuite. This makes tests repeatable and encapsulated.
512 Override this fixture to whitelist some additional URLs.
513 It is still strongly advised to only talk to localhost in tests.
515 @ingroup userver_testsuite_fixtures
520@pytest.fixture(scope='session')
524 allowed_url_prefixes_extra,
525) -> ServiceConfigPatch:
527 Returns a function that adjusts the static configuration file for testsuite.
528 Sets increased timeout and limits allowed URLs for `http-client-core` component.
530 @ingroup userver_testsuite_fixtures
533 def patch_config(config, config_vars) -> None:
534 components: dict = config[
'components_manager'][
'components']
535 if not {
'http-client-core',
'testsuite-support'}.issubset(
539 if components[
'http-client-core']
is None:
540 components[
'http-client-core'] = {}
541 http_client_core = components[
'http-client-core']
542 http_client_core[
'testsuite-enabled'] =
True
543 http_client_core[
'testsuite-timeout'] =
'10s'
545 allowed_urls = [mockserver_info.base_url]
546 if mockserver_ssl_info:
547 allowed_urls.append(mockserver_ssl_info.base_url)
548 allowed_urls += allowed_url_prefixes_extra
549 http_client_core[
'testsuite-allowed-url-prefixes'] = allowed_urls
554@pytest.fixture(scope='session')
557 Default log level to use in userver if no command line option was provided.
561 @ingroup userver_testsuite_fixtures
566@pytest.fixture(scope='session')
569 Returns --service-log-level value if provided, otherwise returns
570 userver_default_log_level() value from fixture.
572 @ingroup userver_testsuite_fixtures
574 if pytestconfig.option.service_log_level:
575 return pytestconfig.option.service_log_level
576 return userver_default_log_level
579@pytest.fixture(scope='session')
582 Returns a function that adjusts the static configuration file for testsuite.
583 Sets the `logging.loggers.default` to log to `@stderr` with level set
584 from `--service-log-level` pytest configuration option.
586 @ingroup userver_testsuite_fixtures
589 if _service_logfile_path:
590 default_file_path = str(_service_logfile_path)
592 default_file_path =
'@stderr'
594 def _patch_config(config_yaml, config_vars):
595 components = config_yaml[
'components_manager'][
'components']
596 if 'logging' in components:
597 loggers = components[
'logging'].setdefault(
'loggers', {})
598 for logger
in loggers.values():
599 logger[
'file_path'] =
'@null'
600 loggers[
'default'] = {
601 'file_path': default_file_path,
602 'level': userver_log_level,
603 'overflow_behavior':
'discard',
605 config_vars[
'logger_level'] = userver_log_level
610@pytest.fixture(scope='session')
613 Returns a function that adjusts the static configuration file for testsuite.
614 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
615 leave the default file logger.
617 @ingroup userver_testsuite_fixtures
620 def _patch_config(config_yaml, config_vars):
621 components = config_yaml[
'components_manager'][
'components']
622 if 'otlp-logger' in components:
623 components[
'otlp-logger'][
'load-enabled'] =
False
628@pytest.fixture(scope='session')
631 Returns a function that adjusts the static configuration file for testsuite.
633 Sets up `testsuite-support` component, which:
635 - increases timeouts for userver drivers
636 - disables periodic cache updates
637 - enables testsuite tasks
639 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
640 `tests-control.testpoint-url` to mockserver URL.
642 @ingroup userver_testsuite_fixtures
645 def _set_postgresql_options(testsuite_support: dict) ->
None:
646 testsuite_support[
'testsuite-pg-execute-timeout'] =
'35s'
647 testsuite_support[
'testsuite-pg-statement-timeout'] =
'30s'
648 testsuite_support[
'testsuite-pg-readonly-master-expected'] =
True
650 def _set_redis_timeout(testsuite_support: dict) ->
None:
651 testsuite_support[
'testsuite-redis-timeout-connect'] =
'40s'
652 testsuite_support[
'testsuite-redis-timeout-single'] =
'30s'
653 testsuite_support[
'testsuite-redis-timeout-all'] =
'30s'
655 def _disable_cache_periodic_update(testsuite_support: dict) ->
None:
656 testsuite_support[
'testsuite-periodic-update-enabled'] =
False
658 def patch_config(config, config_vars) -> None:
660 config[
'components_manager'].pop(
'graceful_shutdown_interval',
None)
661 components: dict = config[
'components_manager'][
'components']
662 if 'testsuite-support' not in components:
664 if components[
'testsuite-support']
is None:
665 components[
'testsuite-support'] = {}
666 testsuite_support = components[
'testsuite-support']
667 testsuite_support[
'testsuite-increased-timeout'] =
'30s'
668 testsuite_support[
'testsuite-grpc-is-tls-enabled'] =
False
669 _set_postgresql_options(testsuite_support)
670 _set_redis_timeout(testsuite_support)
671 service_runner = pytestconfig.option.service_runner_mode
672 if not service_runner:
673 _disable_cache_periodic_update(testsuite_support)
674 testsuite_support[
'testsuite-tasks-enabled'] =
not service_runner
675 testsuite_support[
'testsuite-periodic-dumps-enabled'] =
'$userver-dumps-periodic'
676 components[
'testsuite-support'] = testsuite_support
678 config_vars[
'testsuite-enabled'] =
True
679 if 'tests-control' in components:
680 components[
'tests-control'][
'testpoint-url'] = mockserver_info.url(
687@pytest.fixture(scope='session')
690 Returns a function that adjusts the static configuration file for testsuite.
691 Sets the `default-secdist-provider.config` to the value of
692 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
695 @ingroup userver_testsuite_fixtures
698 def _patch_config(config_yaml, config_vars):
699 if not service_secdist_path:
702 components = config_yaml[
'components_manager'][
'components']
703 if 'default-secdist-provider' not in components:
706 if not service_secdist_path.is_file():
708 f
'"{service_secdist_path}" is not a file. Provide a '
709 f
'"--service-secdist" pytest option or override the '
710 f
'"service_secdist_path" fixture.',
712 components[
'default-secdist-provider'][
'config'] = str(
713 service_secdist_path,
719@pytest.fixture(scope='session')
720def userver_config_testsuite_middleware(userver_testsuite_middleware_enabled: bool) -> ServiceConfigPatch:
721 def patch_config(config_yaml, config_vars):
722 if not userver_testsuite_middleware_enabled:
725 components = config_yaml[
'components_manager'][
'components']
726 if 'server' not in components:
729 pipeline_builder = components.setdefault(
730 'default-server-middleware-pipeline-builder',
733 middlewares = pipeline_builder.setdefault(
'append', [])
734 middlewares.append(
'testsuite-exceptions-handling-middleware')
739@pytest.fixture(scope='session')
741 """Whether testsuite middleware is enabled."""
745@pytest.fixture(scope='session')
748 Returns a function that adjusts the static configuration file for testsuite.
749 Sets the `deadlock_detector` parameter of the `coro_pool` component to the value of
750 @ref pytest_userver.plugins.config.userver_deadlock_detector_mode "userver_deadlock_detector_mode"
753 @ingroup userver_testsuite_fixtures
756 def patch_config(config_yaml, config_vars):
757 coro_pool = config_yaml[
'components_manager'].setdefault(
'coro_pool', {})
758 coro_pool[
'deadlock_detector'] = userver_deadlock_detector_mode
763@pytest.fixture(scope='session')
766 Returns Deadlock detector mode for testsuite.
767 Override this fixture to modify the deadlock detector settings.
768 By default, it operates in `detect-only` mode. For a full list of available options, refer to the
769 `coro_pool.deadlock_detector` parameter in the `components::ManagerControllerComponent`.
771 @ingroup userver_testsuite_fixtures