userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/plugins/base.py Source File
Loading...
Searching...
No Matches
base.py
1"""
2Configure the service in testsuite.
3"""
4
5from collections.abc import Callable
6import pathlib
7import socket
8
9import pytest
10
11import testsuite.plugins.network
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(), # Current directory by default.
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
139@pytest.fixture(scope='session')
141 """
142 Returns congestion control fake-mode value.
143
144 @ingroup userver_testsuite_fixtures
145 """
146 return True
147
148
149def _get_port(
150 original_service_config,
151 choose_free_port,
152 listener_name,
153 port_fixture,
154 option_name,
155) -> int:
156 config_yaml = original_service_config.config_yaml
157 config_vars = original_service_config.config_vars
158 components = config_yaml['components_manager']['components']
159 listener = components.get('server', {}).get(listener_name, {})
160 if not listener:
161 return -1
162
163 port = listener.get('port', None) or listener.get('ports', [None])[0]
164 if isinstance(port, str) and port.startswith('$'):
165 port = config_vars.get(port[1:], None) or listener.get(
166 'port#fallback',
167 None,
168 )
169 assert port, (
170 f'Please specify '
171 f'components_manager.components.server.{listener_name}.port '
172 f'in the static config, or pass {option_name} pytest option, '
173 f'or override the {port_fixture.__name__} fixture'
174 )
175 return choose_free_port(port)
176
177
178# Beware: global variable
179_allocated_ports = set()
180
181
182@pytest.fixture(scope='session')
184 pytestconfig,
185 get_free_port,
186 _testsuite_socket_cleanup,
187 _testsuite_default_af,
188) -> Callable[[int | None], int]:
189 """
190 A function that chooses a free port based on the optional hint given in the parameter.
191
192 @ingroup userver_testsuite_fixtures
193 """
194
195 family, address = _testsuite_default_af
196
197 def choose(port_hint: int | None = None, /) -> int:
198 should_not_randomize_ports = pytestconfig.option.service_runner_mode
199 if should_not_randomize_ports and port_hint is not None and port_hint != 0:
200 if _is_port_free(port_hint, family, address):
201 _allocated_ports.add(port_hint)
202 return port_hint
203 port = _get_free_port_not_allocated(get_free_port)
204 _allocated_ports.add(port)
205 return port
206
207 return choose
208
209
210def _get_free_port_not_allocated(get_free_port) -> int:
211 for _ in range(100):
212 port = get_free_port()
213 if port not in _allocated_ports:
214 return port
215 raise testsuite.plugins.network.NoEnabledPorts()
216
217
218def _is_port_free(port_num: int, family: int, address: str) -> bool:
219 try:
220 with socket.socket(family, socket.SOCK_STREAM) as sock:
221 sock.bind((address, port_num))
222 except OSError:
223 return False
224 else:
225 return True
226
227
228@pytest.fixture(scope='session')
229def userver_base_prepare_service_config(congestion_control_fake_mode):
230 def patch_config(config, config_vars):
231 components = config['components_manager']['components']
232 if 'congestion-control' in components:
233 if components['congestion-control'] is None:
234 components['congestion-control'] = {}
235
236 components['congestion-control']['fake-mode'] = congestion_control_fake_mode
237
238 return patch_config