2Work with the configuration files of the service in testsuite.
16from typing
import Callable
17from typing
import List
18from typing
import Mapping
19from typing
import Optional
20from typing
import Union
40USERVER_CONFIG_HOOKS = [
41 'userver_config_substitutions',
42 'userver_config_http_server',
43 'userver_config_http_client',
44 'userver_config_logging',
45 'userver_config_logging_otlp',
46 'userver_config_testsuite',
47 'userver_config_secdist',
48 'userver_config_testsuite_middleware',
51ServiceConfigPatch = Callable[[dict, dict],
None]
57logger = logging.getLogger(__name__)
60class _UserverConfigPlugin:
62 self._config_hooks = []
65 def userver_config_hooks(self):
66 return self._config_hooks
68 def pytest_plugin_registered(self, plugin, manager):
69 if not isinstance(plugin, types.ModuleType):
71 uhooks = getattr(plugin,
'USERVER_CONFIG_HOOKS',
None)
72 if uhooks
is not None:
73 self._config_hooks.extend(uhooks)
76@dataclasses.dataclass(frozen=True)
82def pytest_configure(config):
83 config.pluginmanager.register(_UserverConfigPlugin(),
'userver_config')
86def pytest_addoption(parser) -> None:
87 group = parser.getgroup(
'userver-config')
89 '--service-log-level',
91 choices=[
'trace',
'debug',
'info',
'warning',
'error',
'critical'],
96 help=
'Path to service.yaml file.',
99 '--service-config-vars',
101 help=
'Path to config_vars.yaml file.',
106 help=
'Path to secure_data.json file.',
111 help=
'Path to dynamic config fallback file.',
116 help=
'Dump config from binary before running tests',
123@pytest.fixture(scope='session')
126 Returns the path to service.yaml file set by command line
127 `--service-config` option.
129 Override this fixture to change the way path to the static config is provided.
131 @ingroup userver_testsuite_fixtures
133 if pytestconfig.option.dump_config:
137 pytestconfig.option.service_config,
139 return pytestconfig.option.service_config
142@pytest.fixture(scope='session')
145 Runs the service binary with `--dump-db-schema` argument, dumps the 0_db_schema.sql file with database schema and
148 Override this fixture to change the way to dump the database schema.
150 @ingroup userver_testsuite_fixtures
153 path = service_tmpdir.joinpath(
'schemas')
158 path /
'0_db_schema.sql',
163@pytest.fixture(scope='session')
166 Returns the path to config_vars.yaml file set by command line
167 `--service-config-vars` option.
169 Override this fixture to change the way path to config_vars.yaml is
172 @ingroup userver_testsuite_fixtures
174 return pytestconfig.option.service_config_vars
177@pytest.fixture(scope='session')
180 Returns the path to secure_data.json file set by command line
181 `--service-secdist` option.
183 Override this fixture to change the way path to secure_data.json is
186 @ingroup userver_testsuite_fixtures
188 return pytestconfig.option.service_secdist
191@pytest.fixture(scope='session')
194 Returns the path to dynamic config fallback file set by command line
195 `--config-fallback` option.
197 Override this fixture to change the way path to dynamic config fallback is
200 @ingroup userver_testsuite_fixtures
202 return pytestconfig.option.config_fallback
205@pytest.fixture(scope='session')
208 Returns the path for temporary files. The path is the same for the whole
209 session and files are not removed (at least by this fixture) between
212 @ingroup userver_testsuite_fixtures
214 return tmp_path_factory.mktemp(
215 pathlib.Path(service_binary).name,
220@pytest.fixture(scope='session')
228 Dumps the contents of the service_config_yaml and service_config_vars into a static config for
229 testsuite and returns the path to the config file.
231 @ingroup userver_testsuite_fixtures
233 dst_path = service_tmpdir /
'config.yaml'
235 service_config_yaml = dict(service_config_yaml)
236 if not service_config_vars:
237 service_config_yaml.pop(
'config_vars',
None)
239 config_vars_path = service_tmpdir /
'config_vars.yaml'
240 config_vars_text = yaml.dump(service_config_vars)
242 'userver fixture "service_config_path_temp" writes the patched static config vars to "%s":\n%s',
246 config_vars_path.write_text(config_vars_text)
247 service_config_yaml[
'config_vars'] = str(config_vars_path)
250 'userver fixture "service_config_path_temp" writes the patched static config to "%s" equivalent to:\n%s',
252 yaml.dump(service_config),
254 dst_path.write_text(yaml.dump(service_config_yaml))
259@pytest.fixture(scope='session')
262 Returns the static config values after the USERVER_CONFIG_HOOKS were
263 applied (if any). Prefer using
264 pytest_userver.plugins.config.service_config
266 @ingroup userver_testsuite_fixtures
268 return _service_config_hooked.config_yaml
271@pytest.fixture(scope='session')
274 Returns the static config variables (config_vars.yaml) values after the
275 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
276 pytest_userver.plugins.config.service_config
278 @ingroup userver_testsuite_fixtures
280 return _service_config_hooked.config_vars
283def _substitute_values(config, service_config_vars: dict, service_env) ->
None:
284 if isinstance(config, dict):
285 for key, value
in config.items():
286 if not isinstance(value, str):
287 _substitute_values(value, service_config_vars, service_env)
290 if not value.startswith(
'$'):
293 new_value = service_config_vars.get(value[1:])
294 if new_value
is not None:
295 config[key] = new_value
298 env = config.get(f
'{key}#env')
301 new_value = service_env.get(env)
303 new_value = os.environ.get(env)
305 config[key] = new_value
308 fallback = config.get(f
'{key}#fallback')
310 config[key] = fallback
315 if isinstance(config, list):
316 for i, value
in enumerate(config):
317 if not isinstance(value, str):
318 _substitute_values(value, service_config_vars, service_env)
321 if not value.startswith(
'$'):
324 new_value = service_config_vars.get(value[1:])
325 if new_value
is not None:
326 config[i] = new_value
329@pytest.fixture(scope='session')
332 A function that takes `config_yaml`, `config_vars` and applies all
333 substitutions just like the service would.
335 Useful when patching the service config. It's a good idea to pass
336 a component's config instead of the whole `config_yaml` to avoid
339 @warning The returned YAML is a clone, mutating it will not modify
340 the actual config while in a config hook!
342 @ingroup userver_testsuite_fixtures
345 def substitute(config_yaml, config_vars, /):
346 if config_yaml
is not None and not isinstance(config_yaml, dict)
and not isinstance(config_yaml, list):
348 f
'{substitute_config_vars.__name__} can only be meaningfully '
349 'called with dict and list nodes of config_yaml, while given: '
350 f
'{config_yaml!r}. Pass a containing object instead.',
353 config = copy.deepcopy(config_yaml)
354 _substitute_values(config, config_vars, service_env)
360@pytest.fixture(scope='session')
364 substitute_config_vars,
367 Returns the static config values after the USERVER_CONFIG_HOOKS were
368 applied (if any) and with all the '$', environment and fallback variables
371 @ingroup userver_testsuite_fixtures
374 config.pop(
'config_vars',
None)
378@pytest.fixture(scope='session')
379def _original_service_config(
381 service_config_vars_path,
386 assert service_config_path
is not None,
'Please specify proper path to the static config file, not None'
388 with open(service_config_path, mode=
'rt')
as fp:
389 config_yaml = yaml.safe_load(fp)
391 if service_config_vars_path:
392 with open(service_config_vars_path, mode=
'rt')
as fp:
393 config_vars = yaml.safe_load(fp)
397 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
400@pytest.fixture(scope='session')
401def _service_config_hooked(
405 _original_service_config,
407 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
408 config_vars = copy.deepcopy(_original_service_config.config_vars)
410 plugin = pytestconfig.pluginmanager.get_plugin(
'userver_config')
411 local_hooks = (daemon_scoped_mark
or {}).get(
'config_hooks', ())
413 for hook
in itertools.chain(plugin.userver_config_hooks, local_hooks):
414 if not callable(hook):
415 hook_func = request.getfixturevalue(hook)
418 hook_func(config_yaml, config_vars)
420 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
423@pytest.fixture(scope='session')
424def _service_config_substitution_vars(request, mockserver_info) -> Mapping[str, str]:
425 substitution_vars = {
426 'mockserver': mockserver_info.base_url.removesuffix(
'/'),
428 if request.config.pluginmanager.hasplugin(
'pytest_userver.plugins.grpc.mockserver'):
429 grpc_mockserver_endpoint = request.getfixturevalue(
'grpc_mockserver_endpoint')
430 substitution_vars[
'grpc_mockserver'] = grpc_mockserver_endpoint
431 return substitution_vars
434@pytest.fixture(scope='session')
437 Replaces substitution vars in all strings within `config_vars` using
438 [string.Template.substitute](https://docs.python.org/3/library/string.html#string.Template.substitute).
440 Substitution vars can be used as a shorthand for writing a full-fledged @ref SERVICE_CONFIG_HOOKS "config hook"
441 in many common cases.
443 Unlike normal `config_vars`, substitution vars can also apply to a part of a string.
444 For example, for `config_vars` entry
447 frobnicator-url: $mockserver/frobnicator
450 a possible patching result is as follows:
453 frobnicator-url: http://127.0.0.1:1234/frobnicator
456 Currently, the following substitution vars are supported:
458 * `mockserver` - mockserver url
459 * `grpc_mockserver` - grpc mockserver endpoint
461 @ingroup userver_testsuite_fixtures
464 def _substitute(key, value, parent: Union[list, dict]) ->
None:
465 if isinstance(value, str):
466 parent[key] = string.Template(value).safe_substitute(_service_config_substitution_vars)
467 elif isinstance(value, dict):
468 for child_key, child_value
in value.items():
469 _substitute(child_key, child_value, value)
470 elif isinstance(value, list):
471 for child_key, child_value
in enumerate(value):
472 _substitute(child_key, child_value, value)
474 def patch_config(config_yaml, config_vars):
475 for key, value
in config_vars.items():
476 _substitute(key, value, config_vars)
481@pytest.fixture(scope='session')
484 Returns a function that adjusts the static configuration file for testsuite.
485 Sets the `server.listener.port` to listen on
486 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
487 sets the `server.listener-monitor.port` to listen on
488 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
491 @ingroup userver_testsuite_fixtures
494 def _patch_config(config_yaml, config_vars):
495 components = config_yaml[
'components_manager'][
'components']
496 if 'server' in components:
497 server = components[
'server']
498 if 'listener' in server:
499 server[
'listener'][
'port'] = service_port
501 if 'listener-monitor' in server:
502 server[
'listener-monitor'][
'port'] = monitor_port
507@pytest.fixture(scope='session')
510 By default, userver HTTP client is only allowed to talk to mockserver
511 when running in testsuite. This makes tests repeatable and encapsulated.
513 Override this fixture to whitelist some additional URLs.
514 It is still strongly advised to only talk to localhost in tests.
516 @ingroup userver_testsuite_fixtures
521@pytest.fixture(scope='session')
525 allowed_url_prefixes_extra,
526) -> ServiceConfigPatch:
528 Returns a function that adjusts the static configuration file for testsuite.
529 Sets increased timeout and limits allowed URLs for `http-client` component.
531 @ingroup userver_testsuite_fixtures
534 def patch_config(config, config_vars):
535 components: dict = config[
'components_manager'][
'components']
536 if not {
'http-client',
'testsuite-support'}.issubset(
540 http_client = components[
'http-client']
or {}
541 http_client[
'testsuite-enabled'] =
True
542 http_client[
'testsuite-timeout'] =
'10s'
544 allowed_urls = [mockserver_info.base_url]
545 if mockserver_ssl_info:
546 allowed_urls.append(mockserver_ssl_info.base_url)
547 allowed_urls += allowed_url_prefixes_extra
548 http_client[
'testsuite-allowed-url-prefixes'] = allowed_urls
553@pytest.fixture(scope='session')
556 Default log level to use in userver if no command line option was provided.
560 @ingroup userver_testsuite_fixtures
565@pytest.fixture(scope='session')
568 Returns --service-log-level value if provided, otherwise returns
569 userver_default_log_level() value from fixture.
571 @ingroup userver_testsuite_fixtures
573 if pytestconfig.option.service_log_level:
574 return pytestconfig.option.service_log_level
575 return userver_default_log_level
578@pytest.fixture(scope='session')
581 Returns a function that adjusts the static configuration file for testsuite.
582 Sets the `logging.loggers.default` to log to `@stderr` with level set
583 from `--service-log-level` pytest configuration option.
585 @ingroup userver_testsuite_fixtures
588 if _service_logfile_path:
589 default_file_path = str(_service_logfile_path)
591 default_file_path =
'@stderr'
593 def _patch_config(config_yaml, config_vars):
594 components = config_yaml[
'components_manager'][
'components']
595 if 'logging' in components:
596 loggers = components[
'logging'].setdefault(
'loggers', {})
597 for logger
in loggers.values():
598 logger[
'file_path'] =
'@null'
599 loggers[
'default'] = {
600 'file_path': default_file_path,
601 'level': userver_log_level,
602 'overflow_behavior':
'discard',
604 config_vars[
'logger_level'] = userver_log_level
609@pytest.fixture(scope='session')
612 Returns a function that adjusts the static configuration file for testsuite.
613 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
614 leave the default file logger.
616 @ingroup userver_testsuite_fixtures
619 def _patch_config(config_yaml, config_vars):
620 components = config_yaml[
'components_manager'][
'components']
621 if 'otlp-logger' in components:
622 components[
'otlp-logger'][
'load-enabled'] =
False
627@pytest.fixture(scope='session')
630 Returns a function that adjusts the static configuration file for testsuite.
632 Sets up `testsuite-support` component, which:
634 - increases timeouts for userver drivers
635 - disables periodic cache updates
636 - enables testsuite tasks
638 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
639 `tests-control.testpoint-url` to mockserver URL.
641 @ingroup userver_testsuite_fixtures
644 def _set_postgresql_options(testsuite_support: dict) ->
None:
645 testsuite_support[
'testsuite-pg-execute-timeout'] =
'35s'
646 testsuite_support[
'testsuite-pg-statement-timeout'] =
'30s'
647 testsuite_support[
'testsuite-pg-readonly-master-expected'] =
True
649 def _set_redis_timeout(testsuite_support: dict) ->
None:
650 testsuite_support[
'testsuite-redis-timeout-connect'] =
'40s'
651 testsuite_support[
'testsuite-redis-timeout-single'] =
'30s'
652 testsuite_support[
'testsuite-redis-timeout-all'] =
'30s'
654 def _disable_cache_periodic_update(testsuite_support: dict) ->
None:
655 testsuite_support[
'testsuite-periodic-update-enabled'] =
False
657 def patch_config(config, config_vars) -> None:
659 config[
'components_manager'].pop(
'graceful_shutdown_interval',
None)
660 components: dict = config[
'components_manager'][
'components']
661 if 'testsuite-support' not in components:
663 testsuite_support = components[
'testsuite-support']
or {}
664 testsuite_support[
'testsuite-increased-timeout'] =
'30s'
665 testsuite_support[
'testsuite-grpc-is-tls-enabled'] =
False
666 _set_postgresql_options(testsuite_support)
667 _set_redis_timeout(testsuite_support)
668 service_runner = pytestconfig.option.service_runner_mode
669 if not service_runner:
670 _disable_cache_periodic_update(testsuite_support)
671 testsuite_support[
'testsuite-tasks-enabled'] =
not service_runner
672 testsuite_support[
'testsuite-periodic-dumps-enabled'] =
'$userver-dumps-periodic'
673 components[
'testsuite-support'] = testsuite_support
675 config_vars[
'testsuite-enabled'] =
True
676 if 'tests-control' in components:
677 components[
'tests-control'][
'testpoint-url'] = mockserver_info.url(
684@pytest.fixture(scope='session')
687 Returns a function that adjusts the static configuration file for testsuite.
688 Sets the `default-secdist-provider.config` to the value of
689 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
692 @ingroup userver_testsuite_fixtures
695 def _patch_config(config_yaml, config_vars):
696 if not service_secdist_path:
699 components = config_yaml[
'components_manager'][
'components']
700 if 'default-secdist-provider' not in components:
703 if not service_secdist_path.is_file():
705 f
'"{service_secdist_path}" is not a file. Provide a '
706 f
'"--service-secdist" pytest option or override the '
707 f
'"service_secdist_path" fixture.',
709 components[
'default-secdist-provider'][
'config'] = str(
710 service_secdist_path,
716@pytest.fixture(scope='session')
717def userver_config_testsuite_middleware(userver_testsuite_middleware_enabled: bool) -> ServiceConfigPatch:
718 def patch_config(config_yaml, config_vars):
719 if not userver_testsuite_middleware_enabled:
722 components = config_yaml[
'components_manager'][
'components']
723 if 'server' not in components:
726 pipeline_builder = components.setdefault(
727 'default-server-middleware-pipeline-builder',
730 middlewares = pipeline_builder.setdefault(
'append', [])
731 middlewares.append(
'testsuite-exceptions-handling-middleware')
736@pytest.fixture(scope='session')
738 """Whether testsuite middleware is enabled."""