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
551 allowed_urls.append(mockserver_info.ws_url(
'/'))
552 if mockserver_ssl_info:
553 allowed_urls.append(mockserver_ssl_info.ws_url(
'/'))
555 http_client_core[
'testsuite-allowed-url-prefixes'] = allowed_urls
560@pytest.fixture(scope='session')
563 Default log level to use in userver if no command line option was provided.
567 @ingroup userver_testsuite_fixtures
572@pytest.fixture(scope='session')
575 Returns --service-log-level value if provided, otherwise returns
576 userver_default_log_level() value from fixture.
578 @ingroup userver_testsuite_fixtures
580 if pytestconfig.option.service_log_level:
581 return pytestconfig.option.service_log_level
582 return userver_default_log_level
585@pytest.fixture(scope='session')
588 Returns a function that adjusts the static configuration file for testsuite.
589 Sets the `logging.loggers.default` to log to `@stderr` with level set
590 from `--service-log-level` pytest configuration option.
592 @ingroup userver_testsuite_fixtures
595 if _service_logfile_path:
596 default_file_path = str(_service_logfile_path)
598 default_file_path =
'@stderr'
600 def _patch_config(config_yaml, config_vars):
601 components = config_yaml[
'components_manager'][
'components']
602 if 'logging' in components:
603 loggers = components[
'logging'].setdefault(
'loggers', {})
604 for logger
in loggers.values():
605 logger[
'file_path'] =
'@null'
606 loggers[
'default'] = {
607 'file_path': default_file_path,
608 'level': userver_log_level,
609 'overflow_behavior':
'discard',
611 config_vars[
'logger_level'] = userver_log_level
616@pytest.fixture(scope='session')
619 Returns a function that adjusts the static configuration file for testsuite.
620 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
621 leave the default file logger.
623 @ingroup userver_testsuite_fixtures
626 def _patch_config(config_yaml, config_vars):
627 components = config_yaml[
'components_manager'][
'components']
628 if 'otlp-logger' in components:
629 components[
'otlp-logger'][
'load-enabled'] =
False
634@pytest.fixture(scope='session')
637 Returns a function that adjusts the static configuration file for testsuite.
639 Sets up `testsuite-support` component, which:
641 - increases timeouts for userver drivers
642 - disables periodic cache updates
643 - enables testsuite tasks
645 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
646 `tests-control.testpoint-url` to mockserver URL.
648 @ingroup userver_testsuite_fixtures
651 def _set_postgresql_options(testsuite_support: dict) ->
None:
652 testsuite_support[
'testsuite-pg-execute-timeout'] =
'35s'
653 testsuite_support[
'testsuite-pg-statement-timeout'] =
'30s'
654 testsuite_support[
'testsuite-pg-readonly-master-expected'] =
True
656 def _set_redis_timeout(testsuite_support: dict) ->
None:
657 testsuite_support[
'testsuite-redis-timeout-connect'] =
'40s'
658 testsuite_support[
'testsuite-redis-timeout-single'] =
'30s'
659 testsuite_support[
'testsuite-redis-timeout-all'] =
'30s'
661 def _disable_cache_periodic_update(testsuite_support: dict) ->
None:
662 testsuite_support[
'testsuite-periodic-update-enabled'] =
False
664 def patch_config(config, config_vars) -> None:
666 config[
'components_manager'].pop(
'graceful_shutdown_interval',
None)
667 components: dict = config[
'components_manager'][
'components']
668 if 'testsuite-support' not in components:
670 if components[
'testsuite-support']
is None:
671 components[
'testsuite-support'] = {}
672 testsuite_support = components[
'testsuite-support']
673 testsuite_support[
'testsuite-increased-timeout'] =
'30s'
674 testsuite_support[
'testsuite-grpc-is-tls-enabled'] =
False
675 _set_postgresql_options(testsuite_support)
676 _set_redis_timeout(testsuite_support)
677 service_runner = pytestconfig.option.service_runner_mode
678 if not service_runner:
679 _disable_cache_periodic_update(testsuite_support)
680 testsuite_support[
'testsuite-tasks-enabled'] =
not service_runner
681 testsuite_support[
'testsuite-periodic-dumps-enabled'] =
'$userver-dumps-periodic'
682 components[
'testsuite-support'] = testsuite_support
684 config_vars[
'testsuite-enabled'] =
True
685 if 'tests-control' in components:
686 components[
'tests-control'][
'testpoint-url'] = mockserver_info.url(
693@pytest.fixture(scope='session')
696 Returns a function that adjusts the static configuration file for testsuite.
697 Sets the `default-secdist-provider.config` to the value of
698 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
701 @ingroup userver_testsuite_fixtures
704 def _patch_config(config_yaml, config_vars):
705 if not service_secdist_path:
708 components = config_yaml[
'components_manager'][
'components']
709 if 'default-secdist-provider' not in components:
712 if not service_secdist_path.is_file():
714 f
'"{service_secdist_path}" is not a file. Provide a '
715 f
'"--service-secdist" pytest option or override the '
716 f
'"service_secdist_path" fixture.',
718 components[
'default-secdist-provider'][
'config'] = str(
719 service_secdist_path,
725@pytest.fixture(scope='session')
726def userver_config_testsuite_middleware(userver_testsuite_middleware_enabled: bool) -> ServiceConfigPatch:
727 def patch_config(config_yaml, config_vars):
728 if not userver_testsuite_middleware_enabled:
731 components = config_yaml[
'components_manager'][
'components']
732 if 'server' not in components:
735 pipeline_builder = components.setdefault(
736 'default-server-middleware-pipeline-builder',
739 middlewares = pipeline_builder.setdefault(
'append', [])
740 middlewares.append(
'testsuite-exceptions-handling-middleware')
745@pytest.fixture(scope='session')
747 """Whether testsuite middleware is enabled."""
751@pytest.fixture(scope='session')
754 Returns a function that adjusts the static configuration file for testsuite.
755 Sets the `deadlock_detector` parameter of the `coro_pool` component to the value of
756 @ref pytest_userver.plugins.config.userver_deadlock_detector_mode "userver_deadlock_detector_mode"
759 @ingroup userver_testsuite_fixtures
762 def patch_config(config_yaml, config_vars):
763 coro_pool = config_yaml[
'components_manager'].setdefault(
'coro_pool', {})
764 coro_pool[
'deadlock_detector'] = userver_deadlock_detector_mode
769@pytest.fixture(scope='session')
772 Returns Deadlock detector mode for testsuite.
773 Override this fixture to modify the deadlock detector settings.
774 By default, it operates in `detect-only` mode. For a full list of available options, refer to the
775 `coro_pool.deadlock_detector` parameter in the `components::ManagerControllerComponent`.
777 @ingroup userver_testsuite_fixtures