2Work with the configuration files of the service in testsuite. 
   32USERVER_CONFIG_HOOKS = [
 
   33    'userver_config_http_server',
 
   34    'userver_config_http_client',
 
   35    'userver_config_logging',
 
   36    'userver_config_testsuite',
 
   37    'userver_config_secdist',
 
   38    'userver_config_testsuite_middleware',
 
   45logger = logging.getLogger(__name__)
 
   48class _UserverConfigPlugin:
 
   50        self._config_hooks = []
 
   53    def userver_config_hooks(self):
 
   54        return self._config_hooks
 
   56    def pytest_plugin_registered(self, plugin, manager):
 
   57        if not isinstance(plugin, types.ModuleType):
 
   59        uhooks = getattr(plugin, 
'USERVER_CONFIG_HOOKS', 
None)
 
   60        if uhooks 
is not None:
 
   61            self._config_hooks.extend(uhooks)
 
   64class _UserverConfig(typing.NamedTuple):
 
   69def pytest_configure(config):
 
   70    config.pluginmanager.register(_UserverConfigPlugin(), 
'userver_config')
 
   71    config.addinivalue_line(
 
   72        'markers', 
'config: per-test dynamic config values',
 
   76def pytest_addoption(parser) -> None:
 
   77    group = parser.getgroup(
'userver-config')
 
   79        '--service-log-level',
 
   81        choices=[
'trace', 
'debug', 
'info', 
'warning', 
'error', 
'critical'],
 
   86        help=
'Path to service.yaml file.',
 
   89        '--service-config-vars',
 
   91        help=
'Path to config_vars.yaml file.',
 
   96        help=
'Path to secure_data.json file.',
 
  101        help=
'Path to dynamic config fallback file.',
 
  108@pytest.fixture(scope='session') 
  111    Returns the path to service.yaml file set by command line 
  112    `--service-config` option. 
  114    Override this fixture to change the way path to service.yaml is provided. 
  116    @ingroup userver_testsuite_fixtures 
  118    return pytestconfig.option.service_config
 
  121@pytest.fixture(scope='session') 
 
  124    Returns the path to config_vars.yaml file set by command line 
  125    `--service-config-vars` option. 
  127    Override this fixture to change the way path to config_vars.yaml is 
  130    @ingroup userver_testsuite_fixtures 
  132    return pytestconfig.option.service_config_vars
 
  135@pytest.fixture(scope='session') 
 
  138    Returns the path to secure_data.json file set by command line 
  139    `--service-secdist` option. 
  141    Override this fixture to change the way path to secure_data.json is 
  144    @ingroup userver_testsuite_fixtures 
  146    return pytestconfig.option.service_secdist
 
  149@pytest.fixture(scope='session') 
 
  152    Returns the path to dynamic config fallback file set by command line 
  153    `--config-fallback` option. 
  155    Override this fixture to change the way path to dynamic config fallback is 
  158    @ingroup userver_testsuite_fixtures 
  160    return pytestconfig.option.config_fallback
 
  163@pytest.fixture(scope='session') 
 
  166    Returns the path for temporary files. The path is the same for the whole 
  167    session and files are not removed (at least by this fixture) between 
  170    @ingroup userver_testsuite_fixtures 
  172    return tmp_path_factory.mktemp(pathlib.Path(service_binary).name)
 
  175@pytest.fixture(scope='session') 
 
  177        service_tmpdir, service_config, service_config_yaml,
 
  180    Dumps the contents of the service_config_yaml into a static config for 
  181    testsuite and returns the path to the config file. 
  183    @ingroup userver_testsuite_fixtures 
  185    dst_path = service_tmpdir / 
'config.yaml' 
  188        'userver fixture "service_config_path_temp" writes the patched static ' 
  189        'config to "%s" equivalent to:\n%s',
 
  191        yaml.dump(service_config),
 
  193    dst_path.write_text(yaml.dump(service_config_yaml))
 
  198@pytest.fixture(scope='session') 
 
  201    Returns the static config values after the USERVER_CONFIG_HOOKS were 
  202    applied (if any). Prefer using 
  203    pytest_userver.plugins.config.service_config 
  205    @ingroup userver_testsuite_fixtures 
  207    return _service_config_hooked.config_yaml
 
  210@pytest.fixture(scope='session') 
 
  213    Returns the static config variables (config_vars.yaml) values after the 
  214    USERVER_CONFIG_HOOKS were applied (if any). Prefer using 
  215    pytest_userver.plugins.config.service_config 
  217    @ingroup userver_testsuite_fixtures 
  219    return _service_config_hooked.config_vars
 
 
  222def _substitute_values(config, service_config_vars: dict, service_env) -> 
None:
 
  223    if isinstance(config, dict):
 
  224        for key, value 
in config.items():
 
  225            if not isinstance(value, str):
 
  226                _substitute_values(value, service_config_vars, service_env)
 
  229            if not value.startswith(
'$'):
 
  232            new_value = service_config_vars.get(value[1:])
 
  233            if new_value 
is not None:
 
  234                config[key] = new_value
 
  237            env = config.get(f
'{key}#env')
 
  240                    new_value = service_env.get(service_env)
 
  242                    new_value = os.environ.get(env)
 
  244                    config[key] = new_value
 
  247            fallback = config.get(f
'{key}#fallback')
 
  249                config[key] = fallback
 
  251    if isinstance(config, list):
 
  252        for i, value 
in enumerate(config):
 
  253            if not isinstance(value, str):
 
  254                _substitute_values(value, service_config_vars, service_env)
 
  257            if not value.startswith(
'$'):
 
  260            new_value = service_config_vars.get(value[1:])
 
  261            if new_value 
is not None:
 
  262                config[i] = new_value
 
  265@pytest.fixture(scope='session') 
  267        service_config_yaml, service_config_vars, service_env,
 
  270    Returns the static config values after the USERVER_CONFIG_HOOKS were 
  271    applied (if any) and with all the '$', environment and fallback variables 
  274    @ingroup userver_testsuite_fixtures 
  276    config = copy.deepcopy(service_config_yaml)
 
  277    _substitute_values(config, service_config_vars, service_env)
 
  278    config.pop(
'config_vars', 
None)
 
  282@pytest.fixture(scope='session') 
 
  283def _original_service_config(
 
  284        service_config_path, service_config_vars_path,
 
  289    with open(service_config_path, mode=
'rt') 
as fp:
 
  290        config_yaml = yaml.safe_load(fp)
 
  292    if service_config_vars_path:
 
  293        with open(service_config_vars_path, mode=
'rt') 
as fp:
 
  294            config_vars = yaml.safe_load(fp)
 
  298    return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
 
  301@pytest.fixture(scope='session') 
  302def _service_config_hooked(
 
  303        pytestconfig, request, service_tmpdir, _original_service_config,
 
  305    config_yaml = copy.deepcopy(_original_service_config.config_yaml)
 
  306    config_vars = copy.deepcopy(_original_service_config.config_vars)
 
  308    plugin = pytestconfig.pluginmanager.get_plugin(
'userver_config')
 
  309    for hook 
in plugin.userver_config_hooks:
 
  310        if not callable(hook):
 
  311            hook_func = request.getfixturevalue(hook)
 
  314        hook_func(config_yaml, config_vars)
 
  317        config_yaml.pop(
'config_vars', 
None)
 
  319        config_vars_path = service_tmpdir / 
'config_vars.yaml' 
  320        config_vars_text = yaml.dump(config_vars)
 
  322            'userver fixture "service_config" writes the patched static ' 
  323            'config vars to "%s":\n%s',
 
  327        config_vars_path.write_text(config_vars_text)
 
  328        config_yaml[
'config_vars'] = str(config_vars_path)
 
  330    return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
 
  333@pytest.fixture(scope='session') 
  336    Returns a function that adjusts the static configuration file for testsuite. 
  337    Sets the `server.listener.port` to listen on 
  338    @ref pytest_userver.plugins.base.service_port "service_port" fixture value; 
  339    sets the `server.listener-monitor.port` to listen on 
  340    @ref pytest_userver.plugins.base.monitor_port "monitor_port" 
  343    @ingroup userver_testsuite_fixtures 
  346    def _patch_config(config_yaml, config_vars):
 
  347        components = config_yaml[
'components_manager'][
'components']
 
  348        if 'server' in components:
 
  349            server = components[
'server']
 
  350            if 'listener' in server:
 
  351                server[
'listener'][
'port'] = service_port
 
  353            if 'listener-monitor' in server:
 
  354                server[
'listener-monitor'][
'port'] = monitor_port
 
  359@pytest.fixture(scope='session') 
 
  362    By default, userver HTTP client is only allowed to talk to mockserver 
  363    when running in testsuite. This makes tests repeatable and encapsulated. 
  365    Override this fixture to whitelist some additional URLs. 
  366    It is still strongly advised to only talk to localhost in tests. 
  368    @ingroup userver_testsuite_fixtures 
  373@pytest.fixture(scope='session') 
 
  375        mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
 
  378    Returns a function that adjusts the static configuration file for testsuite. 
  379    Sets increased timeout and limits allowed URLs for `http-client` component. 
  381    @ingroup userver_testsuite_fixtures 
  384    def patch_config(config, config_vars):
 
  385        components: dict = config[
'components_manager'][
'components']
 
  386        if not {
'http-client', 
'testsuite-support'}.issubset(
 
  390        http_client = components[
'http-client'] 
or {}
 
  391        http_client[
'testsuite-enabled'] = 
True 
  392        http_client[
'testsuite-timeout'] = 
'10s' 
  394        allowed_urls = [mockserver_info.base_url]
 
  395        if mockserver_ssl_info:
 
  396            allowed_urls.append(mockserver_ssl_info.base_url)
 
  397        allowed_urls += allowed_url_prefixes_extra
 
  398        http_client[
'testsuite-allowed-url-prefixes'] = allowed_urls
 
  403@pytest.fixture(scope='session') 
 
  406    Default log level to use in userver if no caoomand line option was provided. 
  410    @ingroup userver_testsuite_fixtures 
  415@pytest.fixture(scope='session') 
 
  418    Returns --service-log-level value if provided, otherwise returns 
  419    userver_default_log_level() value from fixture. 
  421    @ingroup userver_testsuite_fixtures 
  423    if pytestconfig.option.service_log_level:
 
  424        return pytestconfig.option.service_log_level
 
  425    return userver_default_log_level
 
  428@pytest.fixture(scope='session') 
 
  431    Returns a function that adjusts the static configuration file for testsuite. 
  432    Sets the `logging.loggers.default` to log to `@stderr` with level set 
  433    from `--service-log-level` pytest configuration option. 
  435    @ingroup userver_testsuite_fixtures 
  438    if _service_logfile_path:
 
  439        default_file_path = str(_service_logfile_path)
 
  441        default_file_path = 
'@stderr' 
  443    def _patch_config(config_yaml, config_vars):
 
  444        components = config_yaml[
'components_manager'][
'components']
 
  445        if 'logging' in components:
 
  446            loggers = components[
'logging'].setdefault(
'loggers', {})
 
  447            for logger 
in loggers.values():
 
  448                logger[
'file_path'] = 
'@null' 
  449            loggers[
'default'] = {
 
  450                'file_path': default_file_path,
 
  451                'level': userver_log_level,
 
  452                'overflow_behavior': 
'discard',
 
  454        config_vars[
'logger_level'] = userver_log_level
 
  459@pytest.fixture(scope='session') 
 
  462    Returns a function that adjusts the static configuration file for testsuite. 
  464    Sets up `testsuite-support` component, which: 
  466    - increases timeouts for userver drivers 
  467    - disables periodic cache updates 
  468    - enables testsuite tasks 
  470    Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the 
  471    `tests-control.testpoint-url` to mockserver URL. 
  473    @ingroup userver_testsuite_fixtures 
  476    def _set_postgresql_options(testsuite_support: dict) -> 
None:
 
  477        testsuite_support[
'testsuite-pg-execute-timeout'] = 
'35s' 
  478        testsuite_support[
'testsuite-pg-statement-timeout'] = 
'30s' 
  479        testsuite_support[
'testsuite-pg-readonly-master-expected'] = 
True 
  481    def _set_redis_timeout(testsuite_support: dict) -> 
None:
 
  482        testsuite_support[
'testsuite-redis-timeout-connect'] = 
'40s' 
  483        testsuite_support[
'testsuite-redis-timeout-single'] = 
'30s' 
  484        testsuite_support[
'testsuite-redis-timeout-all'] = 
'30s' 
  486    def _disable_cache_periodic_update(testsuite_support: dict) -> 
None:
 
  487        testsuite_support[
'testsuite-periodic-update-enabled'] = 
False 
  489    def patch_config(config, config_vars) -> None:
 
  490        components: dict = config[
'components_manager'][
'components']
 
  491        if 'testsuite-support' not in components:
 
  493        testsuite_support = components[
'testsuite-support'] 
or {}
 
  494        testsuite_support[
'testsuite-increased-timeout'] = 
'30s' 
  495        _set_postgresql_options(testsuite_support)
 
  496        _set_redis_timeout(testsuite_support)
 
  497        service_runner = pytestconfig.getoption(
'--service-runner-mode', 
False)
 
  498        if not service_runner:
 
  499            _disable_cache_periodic_update(testsuite_support)
 
  500        testsuite_support[
'testsuite-tasks-enabled'] = 
not service_runner
 
  502            'testsuite-periodic-dumps-enabled' 
  503        ] = 
'$userver-dumps-periodic' 
  504        components[
'testsuite-support'] = testsuite_support
 
  506        config_vars[
'testsuite-enabled'] = 
True 
  507        if 'tests-control' in components:
 
  508            components[
'tests-control'][
'testpoint-url'] = mockserver_info.url(
 
  515@pytest.fixture(scope='session') 
 
  518    Returns a function that adjusts the static configuration file for testsuite. 
  519    Sets the `default-secdist-provider.config` to the value of 
  520    @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path" 
  523    @ingroup userver_testsuite_fixtures 
  526    def _patch_config(config_yaml, config_vars):
 
  527        if not service_secdist_path:
 
  530        components = config_yaml[
'components_manager'][
'components']
 
  531        if 'default-secdist-provider' not in components:
 
  534        if not service_secdist_path.is_file():
 
  536                f
'"{service_secdist_path}" is not a file. Provide a ' 
  537                f
'"--service-secdist" pytest option or override the ' 
  538                f
'"service_secdist_path" fixture.',
 
  540        components[
'default-secdist-provider'][
'config'] = str(
 
  541            service_secdist_path,
 
  547@pytest.fixture(scope='session') 
 
  548def userver_config_testsuite_middleware(
 
  549        userver_testsuite_middleware_enabled: bool,
 
  551    def patch_config(config_yaml, config_vars):
 
  552        if not userver_testsuite_middleware_enabled:
 
  555        components = config_yaml[
'components_manager'][
'components']
 
  556        if 'server' not in components:
 
  559        pipeline_builder = components.setdefault(
 
  560            'default-server-middleware-pipeline-builder', {},
 
  562        middlewares = pipeline_builder.setdefault(
'append', [])
 
  563        middlewares.append(
'testsuite-exceptions-handling-middleware')
 
  568@pytest.fixture(scope='session') 
  570    """Enabled testsuite middleware."""