userver: /home/antonyzhilin/arcadia/taxi/uservices/userver/testsuite/pytest_plugins/pytest_userver/plugins/base.py Source File
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
base.py
1"""
2Configure the service in testsuite.
3"""
4
5import pathlib
6import random
7import socket
8from typing import Callable
9from typing import Optional
10
11import pytest
12
13USERVER_CONFIG_HOOKS = ['userver_base_prepare_service_config']
14
15
16def pytest_addoption(parser) -> None:
17 group = parser.getgroup('userver')
18 group.addoption(
19 '--build-dir',
20 type=pathlib.Path,
21 help='Path to service build directory.',
22 )
23
24 group = parser.getgroup('Test service')
25 group.addoption(
26 '--service-binary',
27 type=pathlib.Path,
28 help='Path to service binary.',
29 )
30 group.addoption(
31 '--service-port',
32 help=('Main HTTP port of the service (default: use the port from the static config)'),
33 default=None,
34 type=int,
35 )
36 group.addoption(
37 '--monitor-port',
38 help=('Monitor HTTP port of the service (default: use the port from the static config)'),
39 default=None,
40 type=int,
41 )
42 group.addoption(
43 '--service-source-dir',
44 type=pathlib.Path,
45 help='Path to service source directory.',
46 default=pathlib.Path('.'),
47 )
48
49
50def pytest_configure(config):
51 config.option.asyncio_mode = 'auto'
52
53
54@pytest.fixture(scope='session')
55def service_source_dir(pytestconfig) -> pathlib.Path:
56 """
57 Returns the path to the service source directory that is set by command
58 line `--service-source-dir` option.
59
60 Override this fixture to change the way the path to the service
61 source directory is detected by testsuite.
62
63 @ingroup userver_testsuite_fixtures
64 """
65 return pytestconfig.option.service_source_dir
66
67
68@pytest.fixture(scope='session')
69def build_dir(pytestconfig) -> pathlib.Path:
70 """
71 Returns the build directory set by command line `--build-dir` option.
72
73 Override this fixture to change the way the build directory is
74 detected by the testsuite.
75
76 @ingroup userver_testsuite_fixtures
77 """
78 return pytestconfig.option.build_dir
79
80
81@pytest.fixture(scope='session')
82def service_binary(pytestconfig) -> pathlib.Path:
83 """
84 Returns the path to service binary set by command line `--service-binary`
85 option.
86
87 Override this fixture to change the way the path to service binary is
88 detected by the testsuite.
89
90 @ingroup userver_testsuite_fixtures
91 """
92 return pytestconfig.option.service_binary
93
94
95@pytest.fixture(scope='session')
96def service_port(pytestconfig, _original_service_config, choose_free_port) -> int:
97 """
98 Returns the main listener port number of the service set by command line
99 `--service-port` option.
100 If no port is specified in the command line option, keeps the original port
101 specified in the static config.
102
103 Override this fixture to change the way the main listener port number is
104 detected by the testsuite.
105
106 @ingroup userver_testsuite_fixtures
107 """
108 return pytestconfig.option.service_port or _get_port(
109 _original_service_config,
110 choose_free_port,
111 'listener',
112 service_port,
113 '--service-port',
114 )
115
116
117@pytest.fixture(scope='session')
118def monitor_port(pytestconfig, _original_service_config, choose_free_port) -> int:
119 """
120 Returns the monitor listener port number of the service set by command line
121 `--monitor-port` option.
122 If no port is specified in the command line option, keeps the original port
123 specified in the static config.
124
125 Override this fixture to change the way the monitor listener port number
126 is detected by testsuite.
127
128 @ingroup userver_testsuite_fixtures
129 """
130 return pytestconfig.option.monitor_port or _get_port(
131 _original_service_config,
132 choose_free_port,
133 'listener-monitor',
134 monitor_port,
135 '--service-port',
136 )
137
138
139def _get_port(
140 original_service_config,
141 choose_free_port,
142 listener_name,
143 port_fixture,
144 option_name,
145) -> int:
146 config_yaml = original_service_config.config_yaml
147 config_vars = original_service_config.config_vars
148 components = config_yaml['components_manager']['components']
149 listener = components.get('server', {}).get(listener_name, {})
150 if not listener:
151 return -1
152 port = listener.get('port', None)
153 if isinstance(port, str) and port.startswith('$'):
154 port = config_vars.get(port[1:], None) or listener.get(
155 'port#fallback',
156 None,
157 )
158 assert port, (
159 f'Please specify '
160 f'components_manager.components.server.{listener_name}.port '
161 f'in the static config, or pass {option_name} pytest option, '
162 f'or override the {port_fixture.__name__} fixture'
163 )
164 return choose_free_port(port)
165
166
167# Beware: global variable
168_allocated_ports = set()
169
170
171@pytest.fixture(scope='session')
172def choose_free_port() -> Callable[[Optional[int]], int]:
173 """
174 Returns a function that chooses a free port based on the hint given in the parameter.
175
176 @ingroup userver_testsuite_fixtures
177 """
178 return _choose_free_port
179
180
181def _choose_free_port(first_port_hint: Optional[int], /) -> int:
182 def _try_port(port):
183 global _allocated_ports
184 if port in _allocated_ports:
185 return None
186 try:
187 server.bind(('0.0.0.0', port))
188 _allocated_ports.add(port)
189 return port
190 except BaseException:
191 return None
192
193 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
194 if first_port_hint is not None:
195 for port in range(first_port_hint, first_port_hint + 100):
196 if port := _try_port(port):
197 return port
198
199 for _attempt in random.sample(range(1024, 65535), k=100):
200 if port := _try_port(port):
201 return port
202
203 raise RuntimeError('Failed to pick a free TCP port')
204
205
206@pytest.fixture(scope='session')
207def userver_base_prepare_service_config():
208 def patch_config(config, config_vars):
209 components = config['components_manager']['components']
210 if 'congestion-control' in components:
211 if components['congestion-control'] is None:
212 components['congestion-control'] = {}
213
214 components['congestion-control']['fake-mode'] = True
215
216 return patch_config