userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/plugins/config.py Source File
Loading...
Searching...
No Matches
config.py
1"""
2Work with the configuration files of the service in testsuite.
3"""
4
5# pylint: disable=redefined-outer-name
6import copy
7import logging
8import os
9import pathlib
10import subprocess
11import types
12import typing
13
14import pytest
15import yaml
16
17# flake8: noqa E266
18
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',
40]
41
42
43# @cond
44
45
46logger = logging.getLogger(__name__)
47
48
49class _UserverConfigPlugin:
50 def __init__(self):
51 self._config_hooks = []
52
53 @property
54 def userver_config_hooks(self):
55 return self._config_hooks
56
57 def pytest_plugin_registered(self, plugin, manager):
58 if not isinstance(plugin, types.ModuleType):
59 return
60 uhooks = getattr(plugin, 'USERVER_CONFIG_HOOKS', None)
61 if uhooks is not None:
62 self._config_hooks.extend(uhooks)
63
64
65class _UserverConfig(typing.NamedTuple):
66 config_yaml: dict
67 config_vars: dict
68
69
70def pytest_configure(config):
71 config.pluginmanager.register(_UserverConfigPlugin(), 'userver_config')
72
73
74def pytest_addoption(parser) -> None:
75 group = parser.getgroup('userver-config')
76 group.addoption(
77 '--service-log-level',
78 type=str.lower,
79 choices=['trace', 'debug', 'info', 'warning', 'error', 'critical'],
80 )
81 group.addoption(
82 '--service-config',
83 type=pathlib.Path,
84 help='Path to service.yaml file.',
85 )
86 group.addoption(
87 '--service-config-vars',
88 type=pathlib.Path,
89 help='Path to config_vars.yaml file.',
90 )
91 group.addoption(
92 '--service-secdist',
93 type=pathlib.Path,
94 help='Path to secure_data.json file.',
95 )
96 group.addoption(
97 '--config-fallback',
98 type=pathlib.Path,
99 help='Path to dynamic config fallback file.',
100 )
101 group.addoption(
102 '--dump-config',
103 action='store_true',
104 help='Dump config from binary before running tests',
105 )
106
107
108# @endcond
109
110
111@pytest.fixture(scope='session')
112def service_config_path(pytestconfig, service_binary) -> pathlib.Path:
113 """
114 Returns the path to service.yaml file set by command line
115 `--service-config` option.
116
117 Override this fixture to change the way path to service.yaml is provided.
118
119 @ingroup userver_testsuite_fixtures
120 """
121 if pytestconfig.option.dump_config:
122 subprocess.run([
123 service_binary,
124 '--dump-config',
125 pytestconfig.option.service_config,
126 ])
127 return pytestconfig.option.service_config
128
129
130@pytest.fixture(scope='session')
131def db_dump_schema_path(service_binary, service_tmpdir) -> pathlib.Path:
132 """
133 Runs the service binary with `--dump-db-schema` argument, dumps the 0_db_schema.sql file with database schema and
134 returns path to it.
135
136 Override this fixture to change the way to dump the database schema.
137
138 @ingroup userver_testsuite_fixtures
139
140 """
141 path = service_tmpdir.joinpath('schemas')
142 os.mkdir(path)
143 subprocess.run([
144 service_binary,
145 '--dump-db-schema',
146 path / '0_db_schema.sql',
147 ])
148 return path
149
150
151@pytest.fixture(scope='session')
152def service_config_vars_path(pytestconfig) -> typing.Optional[pathlib.Path]:
153 """
154 Returns the path to config_vars.yaml file set by command line
155 `--service-config-vars` option.
156
157 Override this fixture to change the way path to config_vars.yaml is
158 provided.
159
160 @ingroup userver_testsuite_fixtures
161 """
162 return pytestconfig.option.service_config_vars
163
164
165@pytest.fixture(scope='session')
166def service_secdist_path(pytestconfig) -> typing.Optional[pathlib.Path]:
167 """
168 Returns the path to secure_data.json file set by command line
169 `--service-secdist` option.
170
171 Override this fixture to change the way path to secure_data.json is
172 provided.
173
174 @ingroup userver_testsuite_fixtures
175 """
176 return pytestconfig.option.service_secdist
177
178
179@pytest.fixture(scope='session')
180def config_fallback_path(pytestconfig) -> pathlib.Path:
181 """
182 Returns the path to dynamic config fallback file set by command line
183 `--config-fallback` option.
184
185 Override this fixture to change the way path to dynamic config fallback is
186 provided.
187
188 @ingroup userver_testsuite_fixtures
189 """
190 return pytestconfig.option.config_fallback
191
192
193@pytest.fixture(scope='session')
194def service_tmpdir(service_binary, tmp_path_factory):
195 """
196 Returns the path for temporary files. The path is the same for the whole
197 session and files are not removed (at least by this fixture) between
198 tests.
199
200 @ingroup userver_testsuite_fixtures
201 """
202 return tmp_path_factory.mktemp(
203 pathlib.Path(service_binary).name, numbered=False,
204 )
205
206
207@pytest.fixture(scope='session')
209 service_tmpdir, service_config, service_config_yaml,
210) -> pathlib.Path:
211 """
212 Dumps the contents of the service_config_yaml into a static config for
213 testsuite and returns the path to the config file.
214
215 @ingroup userver_testsuite_fixtures
216 """
217 dst_path = service_tmpdir / 'config.yaml'
218
219 logger.debug(
220 'userver fixture "service_config_path_temp" writes the patched static '
221 'config to "%s" equivalent to:\n%s',
222 dst_path,
223 yaml.dump(service_config),
224 )
225 dst_path.write_text(yaml.dump(service_config_yaml))
226
227 return dst_path
228
229
230@pytest.fixture(scope='session')
231def service_config_yaml(_service_config_hooked) -> dict:
232 """
233 Returns the static config values after the USERVER_CONFIG_HOOKS were
234 applied (if any). Prefer using
235 pytest_userver.plugins.config.service_config
236
237 @ingroup userver_testsuite_fixtures
238 """
239 return _service_config_hooked.config_yaml
240
241
242@pytest.fixture(scope='session')
243def service_config_vars(_service_config_hooked) -> dict:
244 """
245 Returns the static config variables (config_vars.yaml) values after the
246 USERVER_CONFIG_HOOKS were applied (if any). Prefer using
247 pytest_userver.plugins.config.service_config
248
249 @ingroup userver_testsuite_fixtures
250 """
251 return _service_config_hooked.config_vars
252
253
254def _substitute_values(config, service_config_vars: dict, service_env) -> None:
255 if isinstance(config, dict):
256 for key, value in config.items():
257 if not isinstance(value, str):
258 _substitute_values(value, service_config_vars, service_env)
259 continue
260
261 if not value.startswith('$'):
262 continue
263
264 new_value = service_config_vars.get(value[1:])
265 if new_value is not None:
266 config[key] = new_value
267 continue
268
269 env = config.get(f'{key}#env')
270 if env:
271 if service_env:
272 new_value = service_env.get(service_env)
273 if not new_value:
274 new_value = os.environ.get(env)
275 if new_value:
276 config[key] = new_value
277 continue
278
279 fallback = config.get(f'{key}#fallback')
280 if fallback:
281 config[key] = fallback
282
283 if isinstance(config, list):
284 for i, value in enumerate(config):
285 if not isinstance(value, str):
286 _substitute_values(value, service_config_vars, service_env)
287 continue
288
289 if not value.startswith('$'):
290 continue
291
292 new_value = service_config_vars.get(value[1:])
293 if new_value is not None:
294 config[i] = new_value
295
296
297@pytest.fixture(scope='session')
299 service_config_yaml, service_config_vars, service_env,
300) -> dict:
301 """
302 Returns the static config values after the USERVER_CONFIG_HOOKS were
303 applied (if any) and with all the '$', environment and fallback variables
304 substituted.
305
306 @ingroup userver_testsuite_fixtures
307 """
308 config = copy.deepcopy(service_config_yaml)
309 _substitute_values(config, service_config_vars, service_env)
310 config.pop('config_vars', None)
311 return config
312
313
314@pytest.fixture(scope='session')
315def _original_service_config(
316 service_config_path, service_config_vars_path,
317) -> _UserverConfig:
318 config_vars: dict
319 config_yaml: dict
320
321 with open(service_config_path, mode='rt') as fp:
322 config_yaml = yaml.safe_load(fp)
323
324 if service_config_vars_path:
325 with open(service_config_vars_path, mode='rt') as fp:
326 config_vars = yaml.safe_load(fp)
327 else:
328 config_vars = {}
329
330 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
331
332
333@pytest.fixture(scope='session')
334def _service_config_hooked(
335 pytestconfig, request, service_tmpdir, _original_service_config,
336) -> _UserverConfig:
337 config_yaml = copy.deepcopy(_original_service_config.config_yaml)
338 config_vars = copy.deepcopy(_original_service_config.config_vars)
339
340 plugin = pytestconfig.pluginmanager.get_plugin('userver_config')
341 for hook in plugin.userver_config_hooks:
342 if not callable(hook):
343 hook_func = request.getfixturevalue(hook)
344 else:
345 hook_func = hook
346 hook_func(config_yaml, config_vars)
347
348 if not config_vars:
349 config_yaml.pop('config_vars', None)
350 else:
351 config_vars_path = service_tmpdir / 'config_vars.yaml'
352 config_vars_text = yaml.dump(config_vars)
353 logger.debug(
354 'userver fixture "service_config" writes the patched static '
355 'config vars to "%s":\n%s',
356 config_vars_path,
357 config_vars_text,
358 )
359 config_vars_path.write_text(config_vars_text)
360 config_yaml['config_vars'] = str(config_vars_path)
361
362 return _UserverConfig(config_yaml=config_yaml, config_vars=config_vars)
363
364
365@pytest.fixture(scope='session')
366def userver_config_http_server(service_port, monitor_port):
367 """
368 Returns a function that adjusts the static configuration file for testsuite.
369 Sets the `server.listener.port` to listen on
370 @ref pytest_userver.plugins.base.service_port "service_port" fixture value;
371 sets the `server.listener-monitor.port` to listen on
372 @ref pytest_userver.plugins.base.monitor_port "monitor_port"
373 fixture value.
374
375 @ingroup userver_testsuite_fixtures
376 """
377
378 def _patch_config(config_yaml, config_vars):
379 components = config_yaml['components_manager']['components']
380 if 'server' in components:
381 server = components['server']
382 if 'listener' in server:
383 server['listener']['port'] = service_port
384
385 if 'listener-monitor' in server:
386 server['listener-monitor']['port'] = monitor_port
387
388 return _patch_config
389
390
391@pytest.fixture(scope='session')
392def allowed_url_prefixes_extra() -> typing.List[str]:
393 """
394 By default, userver HTTP client is only allowed to talk to mockserver
395 when running in testsuite. This makes tests repeatable and encapsulated.
396
397 Override this fixture to whitelist some additional URLs.
398 It is still strongly advised to only talk to localhost in tests.
399
400 @ingroup userver_testsuite_fixtures
401 """
402 return []
403
404
405@pytest.fixture(scope='session')
407 mockserver_info, mockserver_ssl_info, allowed_url_prefixes_extra,
408):
409 """
410 Returns a function that adjusts the static configuration file for testsuite.
411 Sets increased timeout and limits allowed URLs for `http-client` component.
412
413 @ingroup userver_testsuite_fixtures
414 """
415
416 def patch_config(config, config_vars):
417 components: dict = config['components_manager']['components']
418 if not {'http-client', 'testsuite-support'}.issubset(
419 components.keys(),
420 ):
421 return
422 http_client = components['http-client'] or {}
423 http_client['testsuite-enabled'] = True
424 http_client['testsuite-timeout'] = '10s'
425
426 allowed_urls = [mockserver_info.base_url]
427 if mockserver_ssl_info:
428 allowed_urls.append(mockserver_ssl_info.base_url)
429 allowed_urls += allowed_url_prefixes_extra
430 http_client['testsuite-allowed-url-prefixes'] = allowed_urls
431
432 return patch_config
433
434
435@pytest.fixture(scope='session')
437 """
438 Default log level to use in userver if no caoomand line option was provided.
439
440 Returns 'debug'.
441
442 @ingroup userver_testsuite_fixtures
443 """
444 return 'debug'
445
446
447@pytest.fixture(scope='session')
448def userver_log_level(pytestconfig, userver_default_log_level) -> str:
449 """
450 Returns --service-log-level value if provided, otherwise returns
451 userver_default_log_level() value from fixture.
452
453 @ingroup userver_testsuite_fixtures
454 """
455 if pytestconfig.option.service_log_level:
456 return pytestconfig.option.service_log_level
457 return userver_default_log_level
458
459
460@pytest.fixture(scope='session')
461def userver_config_logging(userver_log_level, _service_logfile_path):
462 """
463 Returns a function that adjusts the static configuration file for testsuite.
464 Sets the `logging.loggers.default` to log to `@stderr` with level set
465 from `--service-log-level` pytest configuration option.
466
467 @ingroup userver_testsuite_fixtures
468 """
469
470 if _service_logfile_path:
471 default_file_path = str(_service_logfile_path)
472 else:
473 default_file_path = '@stderr'
474
475 def _patch_config(config_yaml, config_vars):
476 components = config_yaml['components_manager']['components']
477 if 'logging' in components:
478 loggers = components['logging'].setdefault('loggers', {})
479 for logger in loggers.values():
480 logger['file_path'] = '@null'
481 loggers['default'] = {
482 'file_path': default_file_path,
483 'level': userver_log_level,
484 'overflow_behavior': 'discard',
485 }
486 config_vars['logger_level'] = userver_log_level
487
488 return _patch_config
489
490
491@pytest.fixture(scope='session')
493 """
494 Returns a function that adjusts the static configuration file for testsuite.
495 Sets the `otlp-logger.load-enabled` to `false` to disable OTLP logging and
496 leave the default file logger.
497
498 @ingroup userver_testsuite_fixtures
499 """
500
501 def _patch_config(config_yaml, config_vars):
502 components = config_yaml['components_manager']['components']
503 if 'otlp-logger' in components:
504 components['otlp-logger']['load-enabled'] = False
505
506 return _patch_config
507
508
509@pytest.fixture(scope='session')
510def userver_config_testsuite(pytestconfig, mockserver_info):
511 """
512 Returns a function that adjusts the static configuration file for testsuite.
513
514 Sets up `testsuite-support` component, which:
515
516 - increases timeouts for userver drivers
517 - disables periodic cache updates
518 - enables testsuite tasks
519
520 Sets the `testsuite-enabled` in config_vars.yaml to `True`; sets the
521 `tests-control.testpoint-url` to mockserver URL.
522
523 @ingroup userver_testsuite_fixtures
524 """
525
526 def _set_postgresql_options(testsuite_support: dict) -> None:
527 testsuite_support['testsuite-pg-execute-timeout'] = '35s'
528 testsuite_support['testsuite-pg-statement-timeout'] = '30s'
529 testsuite_support['testsuite-pg-readonly-master-expected'] = True
530
531 def _set_redis_timeout(testsuite_support: dict) -> None:
532 testsuite_support['testsuite-redis-timeout-connect'] = '40s'
533 testsuite_support['testsuite-redis-timeout-single'] = '30s'
534 testsuite_support['testsuite-redis-timeout-all'] = '30s'
535
536 def _disable_cache_periodic_update(testsuite_support: dict) -> None:
537 testsuite_support['testsuite-periodic-update-enabled'] = False
538
539 def patch_config(config, config_vars) -> None:
540 # Don't delay tests teardown unnecessarily.
541 config['components_manager'].pop('graceful_shutdown_interval', None)
542 components: dict = config['components_manager']['components']
543 if 'testsuite-support' not in components:
544 return
545 testsuite_support = components['testsuite-support'] or {}
546 testsuite_support['testsuite-increased-timeout'] = '30s'
547 testsuite_support['testsuite-grpc-is-tls-enabled'] = False
548 _set_postgresql_options(testsuite_support)
549 _set_redis_timeout(testsuite_support)
550 service_runner = pytestconfig.getoption('--service-runner-mode', False)
551 if not service_runner:
552 _disable_cache_periodic_update(testsuite_support)
553 testsuite_support['testsuite-tasks-enabled'] = not service_runner
554 testsuite_support['testsuite-periodic-dumps-enabled'] = (
555 '$userver-dumps-periodic'
556 )
557 components['testsuite-support'] = testsuite_support
558
559 config_vars['testsuite-enabled'] = True
560 if 'tests-control' in components:
561 components['tests-control']['testpoint-url'] = mockserver_info.url(
562 'testpoint',
563 )
564
565 return patch_config
566
567
568@pytest.fixture(scope='session')
569def userver_config_secdist(service_secdist_path):
570 """
571 Returns a function that adjusts the static configuration file for testsuite.
572 Sets the `default-secdist-provider.config` to the value of
573 @ref pytest_userver.plugins.config.service_secdist_path "service_secdist_path"
574 fixture.
575
576 @ingroup userver_testsuite_fixtures
577 """
578
579 def _patch_config(config_yaml, config_vars):
580 if not service_secdist_path:
581 return
582
583 components = config_yaml['components_manager']['components']
584 if 'default-secdist-provider' not in components:
585 return
586
587 if not service_secdist_path.is_file():
588 raise ValueError(
589 f'"{service_secdist_path}" is not a file. Provide a '
590 f'"--service-secdist" pytest option or override the '
591 f'"service_secdist_path" fixture.',
592 )
593 components['default-secdist-provider']['config'] = str(
594 service_secdist_path,
595 )
596
597 return _patch_config
598
599
600@pytest.fixture(scope='session')
601def userver_config_testsuite_middleware(
602 userver_testsuite_middleware_enabled: bool,
603):
604 def patch_config(config_yaml, config_vars):
605 if not userver_testsuite_middleware_enabled:
606 return
607
608 components = config_yaml['components_manager']['components']
609 if 'server' not in components:
610 return
611
612 pipeline_builder = components.setdefault(
613 'default-server-middleware-pipeline-builder', {},
614 )
615 middlewares = pipeline_builder.setdefault('append', [])
616 middlewares.append('testsuite-exceptions-handling-middleware')
617
618 return patch_config
619
620
621@pytest.fixture(scope='session')
623 """Enabled testsuite middleware."""
624 return True