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_logging_otlp',
37 'userver_config_testsuite',
38 'userver_config_secdist',
39 'userver_config_testsuite_middleware',
46logger = logging.getLogger(__name__)
49class _UserverConfigPlugin:
51 self._config_hooks = []
54 def userver_config_hooks(self):
55 return self._config_hooks
57 def pytest_plugin_registered(self, plugin, manager):
58 if not isinstance(plugin, types.ModuleType):
60 uhooks = getattr(plugin,
'USERVER_CONFIG_HOOKS',
None)
61 if uhooks
is not None:
62 self._config_hooks.extend(uhooks)
65class _UserverConfig(typing.NamedTuple):
70def pytest_configure(config):
71 config.pluginmanager.register(_UserverConfigPlugin(),
'userver_config')
74def pytest_addoption(parser) -> None:
75 group = parser.getgroup(
'userver-config')
77 '--service-log-level',
79 choices=[
'trace',
'debug',
'info',
'warning',
'error',
'critical'],
84 help=
'Path to service.yaml file.',
87 '--service-config-vars',
89 help=
'Path to config_vars.yaml file.',
94 help=
'Path to secure_data.json file.',
99 help=
'Path to dynamic config fallback file.',
106@pytest.fixture(scope='session')
109 Returns the path to service.yaml file set by command line
110 `--service-config` option.
112 Override this fixture to change the way path to service.yaml is provided.
114 @ingroup userver_testsuite_fixtures
116 return pytestconfig.option.service_config
119@pytest.fixture(scope='session')
122 Returns the path to config_vars.yaml file set by command line
123 `--service-config-vars` option.
125 Override this fixture to change the way path to config_vars.yaml is
128 @ingroup userver_testsuite_fixtures
130 return pytestconfig.option.service_config_vars
133@pytest.fixture(scope='session')
136 Returns the path to secure_data.json file set by command line
137 `--service-secdist` option.
139 Override this fixture to change the way path to secure_data.json is
142 @ingroup userver_testsuite_fixtures
144 return pytestconfig.option.service_secdist
147@pytest.fixture(scope='session')
150 Returns the path to dynamic config fallback file set by command line
151 `--config-fallback` option.
153 Override this fixture to change the way path to dynamic config fallback is
156 @ingroup userver_testsuite_fixtures
158 return pytestconfig.option.config_fallback
161@pytest.fixture(scope='session')
164 Returns the path for temporary files. The path is the same for the whole
165 session and files are not removed (at least by this fixture) between
168 @ingroup userver_testsuite_fixtures
170 return tmp_path_factory.mktemp(pathlib.Path(service_binary).name)
173@pytest.fixture(scope='session')
175 service_tmpdir, service_config, service_config_yaml,
178 Dumps the contents of the service_config_yaml into a static config for
179 testsuite and returns the path to the config file.
181 @ingroup userver_testsuite_fixtures
183 dst_path = service_tmpdir /
'config.yaml'
186 'userver fixture "service_config_path_temp" writes the patched static '
187 'config to "%s" equivalent to:\n%s',
189 yaml.dump(service_config),
191 dst_path.write_text(yaml.dump(service_config_yaml))
196@pytest.fixture(scope='session')
199 Returns the static config values after the USERVER_CONFIG_HOOKS were
200 applied (if any). Prefer using
201 pytest_userver.plugins.config.service_config
203 @ingroup userver_testsuite_fixtures
205 return _service_config_hooked.config_yaml
208@pytest.fixture(scope='session')
211 Returns the static config variables (config_vars.yaml) values after the
212 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
213 pytest_userver.plugins.config.service_config
215 @ingroup userver_testsuite_fixtures
217 return _service_config_hooked.config_vars
220def _substitute_values(config, service_config_vars: dict, service_env) ->
None:
221 if isinstance(config, dict):
222 for key, value
in config.items():
223 if not isinstance(value, str):
224 _substitute_values(value, service_config_vars, service_env)
227 if not value.startswith(
'$'):
230 new_value = service_config_vars.get(value[1:])
231 if new_value
is not None:
232 config[key] = new_value
235 env = config.get(f
'{key}#env')
238 new_value = service_env.get(service_env)
240 new_value = os.environ.get(env)
242 config[key] = new_value
245 fallback = config.get(f
'{key}#fallback')
247 config[key] = fallback
249 if isinstance(config, list):
250 for i, value
in enumerate(config):
251 if not isinstance(value, str):
252 _substitute_values(value, service_config_vars, service_env)
255 if not value.startswith(
'$'):
258 new_value = service_config_vars.get(value[1:])
259 if new_value
is not None:
260 config[i] = new_value
263@pytest.fixture(scope='session')
265 service_config_yaml, service_config_vars, service_env,
268 Returns the static config values after the USERVER_CONFIG_HOOKS were
269 applied (if any) and with all the '$', environment and fallback variables
272 @ingroup userver_testsuite_fixtures
274 config = copy.deepcopy(service_config_yaml)
275 _substitute_values(config, service_config_vars, service_env)
276 config.pop(
'config_vars',
None)
280@pytest.fixture(scope='session')
281def _original_service_config(
282 service_config_path, service_config_vars_path,
287 with open(service_config_path, mode=
'rt')
as fp:
288 config_yaml = yaml.safe_load(fp)
290 if service_config_vars_path:
291 with open(service_config_vars_path, mode=
'rt')
as fp:
292 config_vars = yaml.safe_load(fp)
296 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
299@pytest.fixture(scope='session')
300def _service_config_hooked(
301 pytestconfig, request, service_tmpdir, _original_service_config,
303 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
304 config_vars = copy.deepcopy(_original_service_config.config_vars)
306 plugin = pytestconfig.pluginmanager.get_plugin(
'userver_config')
307 for hook
in plugin.userver_config_hooks:
308 if not callable(hook):
309 hook_func = request.getfixturevalue(hook)
312 hook_func(config_yaml, config_vars)
315 config_yaml.pop(
'config_vars',
None)
317 config_vars_path = service_tmpdir /
'config_vars.yaml'
318 config_vars_text = yaml.dump(config_vars)
320 'userver fixture "service_config" writes the patched static '
321 'config vars to "%s":\n%s',
325 config_vars_path.write_text(config_vars_text)
326 config_yaml[
'config_vars'] = str(config_vars_path)
328 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
331@pytest.fixture(scope='session')
334 Returns a function that adjusts the static configuration file for testsuite.
335 Sets the `server.listener.port` to listen on
336 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
337 sets the `server.listener-monitor.port` to listen on
338 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
341 @ingroup userver_testsuite_fixtures
344 def _patch_config(config_yaml, config_vars):
345 components = config_yaml[
'components_manager'][
'components']
346 if 'server' in components:
347 server = components[
'server']
348 if 'listener' in server:
349 server[
'listener'][
'port'] = service_port
351 if 'listener-monitor' in server:
352 server[
'listener-monitor'][
'port'] = monitor_port
357@pytest.fixture(scope='session')
360 By default, userver HTTP client is only allowed to talk to mockserver
361 when running in testsuite. This makes tests repeatable and encapsulated.
363 Override this fixture to whitelist some additional URLs.
364 It is still strongly advised to only talk to localhost in tests.
366 @ingroup userver_testsuite_fixtures
371@pytest.fixture(scope='session')
373 mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
376 Returns a function that adjusts the static configuration file for testsuite.
377 Sets increased timeout and limits allowed URLs for `http-client` component.
379 @ingroup userver_testsuite_fixtures
382 def patch_config(config, config_vars):
383 components: dict = config[
'components_manager'][
'components']
384 if not {
'http-client',
'testsuite-support'}.issubset(
388 http_client = components[
'http-client']
or {}
389 http_client[
'testsuite-enabled'] =
True
390 http_client[
'testsuite-timeout'] =
'10s'
392 allowed_urls = [mockserver_info.base_url]
393 if mockserver_ssl_info:
394 allowed_urls.append(mockserver_ssl_info.base_url)
395 allowed_urls += allowed_url_prefixes_extra
396 http_client[
'testsuite-allowed-url-prefixes'] = allowed_urls
401@pytest.fixture(scope='session')
404 Default log level to use in userver if no caoomand line option was provided.
408 @ingroup userver_testsuite_fixtures
413@pytest.fixture(scope='session')
416 Returns --service-log-level value if provided, otherwise returns
417 userver_default_log_level() value from fixture.
419 @ingroup userver_testsuite_fixtures
421 if pytestconfig.option.service_log_level:
422 return pytestconfig.option.service_log_level
423 return userver_default_log_level
426@pytest.fixture(scope='session')
429 Returns a function that adjusts the static configuration file for testsuite.
430 Sets the `logging.loggers.default` to log to `@stderr` with level set
431 from `--service-log-level` pytest configuration option.
433 @ingroup userver_testsuite_fixtures
436 if _service_logfile_path:
437 default_file_path = str(_service_logfile_path)
439 default_file_path =
'@stderr'
441 def _patch_config(config_yaml, config_vars):
442 components = config_yaml[
'components_manager'][
'components']
443 if 'logging' in components:
444 loggers = components[
'logging'].setdefault(
'loggers', {})
445 for logger
in loggers.values():
446 logger[
'file_path'] =
'@null'
447 loggers[
'default'] = {
448 'file_path': default_file_path,
449 'level': userver_log_level,
450 'overflow_behavior':
'discard',
452 config_vars[
'logger_level'] = userver_log_level
457@pytest.fixture(scope='session')
460 Returns a function that adjusts the static configuration file for testsuite.
461 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
462 leave the default file logger.
464 @ingroup userver_testsuite_fixtures
467 def _patch_config(config_yaml, config_vars):
468 components = config_yaml[
'components_manager'][
'components']
469 if 'otlp-logger' in components:
470 components[
'otlp-logger'][
'load-enabled'] =
False
475@pytest.fixture(scope='session')
478 Returns a function that adjusts the static configuration file for testsuite.
480 Sets up `testsuite-support` component, which:
482 - increases timeouts for userver drivers
483 - disables periodic cache updates
484 - enables testsuite tasks
486 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
487 `tests-control.testpoint-url` to mockserver URL.
489 @ingroup userver_testsuite_fixtures
492 def _set_postgresql_options(testsuite_support: dict) ->
None:
493 testsuite_support[
'testsuite-pg-execute-timeout'] =
'35s'
494 testsuite_support[
'testsuite-pg-statement-timeout'] =
'30s'
495 testsuite_support[
'testsuite-pg-readonly-master-expected'] =
True
497 def _set_redis_timeout(testsuite_support: dict) ->
None:
498 testsuite_support[
'testsuite-redis-timeout-connect'] =
'40s'
499 testsuite_support[
'testsuite-redis-timeout-single'] =
'30s'
500 testsuite_support[
'testsuite-redis-timeout-all'] =
'30s'
502 def _disable_cache_periodic_update(testsuite_support: dict) ->
None:
503 testsuite_support[
'testsuite-periodic-update-enabled'] =
False
505 def patch_config(config, config_vars) -> None:
506 components: dict = config[
'components_manager'][
'components']
507 if 'testsuite-support' not in components:
509 testsuite_support = components[
'testsuite-support']
or {}
510 testsuite_support[
'testsuite-increased-timeout'] =
'30s'
511 testsuite_support[
'testsuite-grpc-is-tls-enabled'] =
False
512 _set_postgresql_options(testsuite_support)
513 _set_redis_timeout(testsuite_support)
514 service_runner = pytestconfig.getoption(
'--service-runner-mode',
False)
515 if not service_runner:
516 _disable_cache_periodic_update(testsuite_support)
517 testsuite_support[
'testsuite-tasks-enabled'] =
not service_runner
519 'testsuite-periodic-dumps-enabled'
520 ] =
'$userver-dumps-periodic'
521 components[
'testsuite-support'] = testsuite_support
523 config_vars[
'testsuite-enabled'] =
True
524 if 'tests-control' in components:
525 components[
'tests-control'][
'testpoint-url'] = mockserver_info.url(
532@pytest.fixture(scope='session')
535 Returns a function that adjusts the static configuration file for testsuite.
536 Sets the `default-secdist-provider.config` to the value of
537 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
540 @ingroup userver_testsuite_fixtures
543 def _patch_config(config_yaml, config_vars):
544 if not service_secdist_path:
547 components = config_yaml[
'components_manager'][
'components']
548 if 'default-secdist-provider' not in components:
551 if not service_secdist_path.is_file():
553 f
'"{service_secdist_path}" is not a file. Provide a '
554 f
'"--service-secdist" pytest option or override the '
555 f
'"service_secdist_path" fixture.',
557 components[
'default-secdist-provider'][
'config'] = str(
558 service_secdist_path,
564@pytest.fixture(scope='session')
565def userver_config_testsuite_middleware(
566 userver_testsuite_middleware_enabled: bool,
568 def patch_config(config_yaml, config_vars):
569 if not userver_testsuite_middleware_enabled:
572 components = config_yaml[
'components_manager'][
'components']
573 if 'server' not in components:
576 pipeline_builder = components.setdefault(
577 'default-server-middleware-pipeline-builder', {},
579 middlewares = pipeline_builder.setdefault(
'append', [])
580 middlewares.append(
'testsuite-exceptions-handling-middleware')
585@pytest.fixture(scope='session')
587 """Enabled testsuite middleware."""