userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/plugins/service_client.py Source File
Loading...
Searching...
No Matches
service_client.py
1"""
2Service main and monitor clients.
3"""
4
5# pylint: disable=redefined-outer-name
6import logging
7import typing
8
9import aiohttp.client_exceptions
10import pytest
11import websockets
12
13from testsuite.daemons import service_client as base_service_client
14from testsuite.daemons.pytest_plugin import DaemonInstance
15from testsuite.utils import compat
16
17from pytest_userver import client
18
19
20logger = logging.getLogger(__name__)
21
22
23@pytest.fixture
24def extra_client_deps() -> None:
25 """
26 Service client dependencies hook. Feel free to override, e.g.:
27
28 @code
29 @pytest.fixture
30 def extra_client_deps(some_fixtures_to_wait_before_service_start):
31 pass
32 @endcode
33 @ingroup userver_testsuite_fixtures
34 """
35
36
37@pytest.fixture
38def auto_client_deps(request) -> None:
39 """
40 Service client dependencies hook that knows about pgsql, mongodb,
41 clickhouse, rabbitmq, redis_store, ydb, and mysql dependencies.
42 To add some other dependencies prefer overriding the
43 extra_client_deps() fixture.
44
45 @ingroup userver_testsuite_fixtures
46 """
47 known_deps = {
48 'pgsql',
49 'mongodb',
50 'clickhouse',
51 'rabbitmq',
52 'redis_store',
53 'mysql',
54 'ydb',
55 }
56
57 try:
58 fixture_lookup_error = pytest.FixtureLookupError
59 except AttributeError:
60 # support for an older version of the pytest
61 import _pytest.fixtures
62 fixture_lookup_error = _pytest.fixtures.FixtureLookupError
63
64 resolved_deps = []
65 for dep in known_deps:
66 try:
67 request.getfixturevalue(dep)
68 resolved_deps.append(dep)
69 except fixture_lookup_error:
70 pass
71
72 logger.debug(
73 'userver fixture "auto_client_deps" resolved dependencies %s',
74 resolved_deps,
75 )
76
77
78@pytest.fixture
79async def service_client(
80 ensure_daemon_started,
81 service_daemon,
82 dynamic_config,
83 mock_configs_service,
84 cleanup_userver_dumps,
85 userver_client_cleanup,
86 _config_service_defaults_updated,
87 _testsuite_client_config: client.TestsuiteClientConfig,
88 _service_client_base,
89 _service_client_testsuite,
90 # User defined client deps must be last in order to use
91 # fixtures defined above.
92 extra_client_deps,
93 auto_client_deps,
94) -> client.Client:
95 """
96 Main fixture that provides access to userver based service.
97
98 @snippet samples/testsuite-support/tests/test_ping.py service_client
99 @anchor service_client
100 @ingroup userver_testsuite_fixtures
101 """
102 # The service is lazily started here (not at the 'session' scope)
103 # to allow '*_client_deps' to be active during service start
104 daemon = await ensure_daemon_started(service_daemon)
105
106 if not _testsuite_client_config.testsuite_action_path:
107 yield _service_client_base
108 else:
109 service_client = _service_client_testsuite(daemon)
110 await _config_service_defaults_updated.update(
111 service_client, dynamic_config,
112 )
113
114 async with userver_client_cleanup(service_client):
115 yield service_client
116
117
118@pytest.fixture
119def userver_client_cleanup(request, userver_flush_logs):
120 marker = request.node.get_closest_marker('suspend_periodic_tasks')
121 if marker:
122 tasks_to_suspend = marker.args
123 else:
124 tasks_to_suspend = ()
125
126 @compat.asynccontextmanager
127 async def cleanup_manager(client: client.AiohttpClient):
128 async with userver_flush_logs(client):
129 await client.suspend_periodic_tasks(tasks_to_suspend)
130 try:
131 yield client
132 finally:
133 await client.resume_all_periodic_tasks()
134
135 return cleanup_manager
136
137
138@pytest.fixture
140 """Flush logs in case of failure."""
141
142 @compat.asynccontextmanager
143 async def flush_logs(service_client: client.AiohttpClient):
144 async def do_flush():
145 try:
146 await service_client.log_flush()
147 except aiohttp.client_exceptions.ClientResponseError:
148 pass
149
150 failed = False
151 try:
152 yield
153 except Exception:
154 failed = True
155 raise
156 finally:
157 item = request.node
158 if failed or item.utestsuite_report.failed:
159 await do_flush()
160
161 return flush_logs
162
163
164@pytest.fixture
165async def websocket_client(service_client, service_port):
166 """
167 Fixture that provides access to userver based websocket service.
168
169 @anchor websocket_client
170 @ingroup userver_testsuite_fixtures
171 """
172
173 class Client:
174 @compat.asynccontextmanager
175 async def get(self, path):
176 update_server_state = getattr(
177 service_client, 'update_server_state', None,
178 )
179 if update_server_state:
180 await update_server_state()
181 ws_context = websockets.connect(
182 f'ws://localhost:{service_port}/{path}',
183 )
184 async with ws_context as socket:
185 yield socket
186
187 return Client()
188
189
190@pytest.fixture
192 service_client,
193 service_client_options,
194 mockserver,
195 monitor_baseurl: str,
196 _testsuite_client_config: client.TestsuiteClientConfig,
198 """
199 Main fixture that provides access to userver monitor listener.
200
201 @snippet samples/testsuite-support/tests/test_metrics.py metrics labels
202 @ingroup userver_testsuite_fixtures
203 """
204 aiohttp_client = client.AiohttpClientMonitor(
205 monitor_baseurl,
206 config=_testsuite_client_config,
207 headers={'x-yatraceid': mockserver.trace_id},
208 **service_client_options,
209 )
210 return client.ClientMonitor(aiohttp_client)
211
212
213@pytest.fixture
214async def _service_client_base(service_baseurl, service_client_options):
215 class _ClientDiagnose(base_service_client.Client):
216 def __getattr__(self, name: str) -> None:
217 raise AttributeError(
218 f'"Client" object has no attribute "{name}". '
219 'Note that "service_client" fixture returned the basic '
220 '"testsuite.daemons.service_client.Client" client rather than '
221 'a "pytest_userver.client.Client" client with userver '
222 'extensions. That happened because the service '
223 'static configuration file contains no "tests-control" '
224 'component with "action" field.',
225 )
226
227 return _ClientDiagnose(service_baseurl, **service_client_options)
228
229
230@pytest.fixture
231def _service_client_testsuite(
232 service_baseurl,
233 service_client_options,
234 mocked_time,
235 userver_cache_control,
236 userver_log_capture,
237 testpoint,
238 testpoint_control,
239 cache_invalidation_state,
240 service_periodic_tasks_state,
241 _testsuite_client_config: client.TestsuiteClientConfig,
242) -> typing.Callable[[DaemonInstance], client.Client]:
243 def create_client(daemon):
244 aiohttp_client = client.AiohttpClient(
245 service_baseurl,
246 config=_testsuite_client_config,
247 testpoint=testpoint,
248 testpoint_control=testpoint_control,
249 periodic_tasks_state=service_periodic_tasks_state,
250 log_capture_fixture=userver_log_capture,
251 mocked_time=mocked_time,
252 cache_invalidation_state=cache_invalidation_state,
253 cache_control=userver_cache_control(daemon),
254 **service_client_options,
255 )
256 return client.Client(aiohttp_client)
257
258 return create_client
259
260
261@pytest.fixture(scope='session')
262def service_baseurl(service_port) -> str:
263 """
264 Returns the main listener URL of the service.
265
266 Override this fixture to change the main listener URL that the testsuite
267 uses for tests.
268
269 @ingroup userver_testsuite_fixtures
270 """
271 return f'http://localhost:{service_port}/'
272
273
274@pytest.fixture(scope='session')
275def monitor_baseurl(monitor_port) -> str:
276 """
277 Returns the main monitor URL of the service.
278
279 Override this fixture to change the main monitor URL that the testsuite
280 uses for tests.
281
282 @ingroup userver_testsuite_fixtures
283 """
284 return f'http://localhost:{monitor_port}/'
285
286
287@pytest.fixture(scope='session')
288def service_periodic_tasks_state() -> client.PeriodicTasksState:
290
291
292@pytest.fixture(scope='session')
293def _testsuite_client_config(
294 pytestconfig, service_config,
295) -> client.TestsuiteClientConfig:
296 components = service_config['components_manager']['components']
297
298 def get_component_path(name, argname=None):
299 if name in components:
300 path = components[name]['path']
301 path = path.rstrip('*')
302
303 if argname and f'{{{argname}}}' not in path:
304 raise RuntimeError(
305 f'Component {name} must provide path argument {argname}',
306 )
307 return path
308 return None
309
311 server_monitor_path=get_component_path('handler-server-monitor'),
312 testsuite_action_path=get_component_path('tests-control', 'action'),
313 )