userver: /home/user/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
6from collections.abc import AsyncGenerator
7from collections.abc import Callable
8import contextlib
9import warnings
10
11import aiohttp.client_exceptions
12import pytest
13import websockets.legacy.client as websockets
14
15from testsuite.daemons import service_client as base_service_client
16
17from pytest_userver import client
18from pytest_userver import userver_warnings
19
20
21@pytest.fixture
22async def service_client(
23 service_daemon_instance,
24 service_baseurl,
25 service_client_options,
26 userver_service_client_options,
27 userver_client_cleanup,
28 _testsuite_client_config: client.TestsuiteClientConfig,
29) -> client.Client:
30 """
31 Main fixture that provides access to userver based service.
32
33 @snippet samples/testsuite-support/tests/test_ping.py service_client
34 @anchor service_client
35 @ingroup userver_testsuite_fixtures
36 """
37 if not _testsuite_client_config.testsuite_action_path:
38 yield _ClientDiagnose(service_baseurl, **service_client_options)
39 return
40
41 aiohttp_client = client.AiohttpClient(
42 service_baseurl,
43 **userver_service_client_options,
44 )
45 userver_client = client.Client(aiohttp_client)
46 async with userver_client_cleanup(userver_client):
47 yield userver_client
48
49
50@pytest.fixture
52 request,
53 service_logs_update_position,
54 servicelogs_register_flusher,
55 _dynamic_config_defaults_storage,
56 _check_config_marks,
57 dynamic_config,
58) -> Callable[[client.Client], AsyncGenerator]:
59 """
60 Contains the pre-test and post-test setup that depends
61 on @ref service_client.
62
63 Feel free to override, but in that case make sure to call the original
64 `userver_client_cleanup` fixture instance.
65
66 @ingroup userver_testsuite_fixtures
67 """
68 marker = request.node.get_closest_marker('suspend_periodic_tasks')
69 if marker:
70 warnings.warn(userver_warnings.WARN_PERIODIC_DEPRECATION, DeprecationWarning)
71 tasks_to_suspend = marker.args
72 else:
73 tasks_to_suspend = ()
74
75 @contextlib.asynccontextmanager
76 async def cleanup_manager(client: client.Client):
77 @servicelogs_register_flusher
78 async def do_flush():
79 try:
80 await client.log_flush()
81 except aiohttp.client_exceptions.ClientError:
82 pass
83 except RuntimeError:
84 # TODO: find a better way to handle closed aiohttp session
85 pass
86
87 # Service is already started we don't want startup logs to be shown
88 service_logs_update_position()
89
90 await _dynamic_config_defaults_storage.update(client, dynamic_config)
91 _check_config_marks()
92
93 await client.suspend_periodic_tasks(tasks_to_suspend)
94 try:
95 yield
96 finally:
97 await client.resume_all_periodic_tasks()
98
99 return cleanup_manager
100
101
102@pytest.fixture
103async def websocket_client(service_client, service_port):
104 """
105 Fixture that provides access to userver based websocket service.
106
107 Usage example:
108
109 @snippet samples/websocket_service/tests/test_websocket.py Functional test
110
111 You can pass extra kwargs to `get`, they will be forwarded to [websockets.connect][1].
112
113 [1]: https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#websockets.asyncio.client.connect
114
115 @anchor websocket_client
116 @ingroup userver_testsuite_fixtures
117 """
118
119 class Client:
120 @contextlib.asynccontextmanager
121 async def get(self, path, **kwargs):
122 update_server_state = getattr(
123 service_client,
124 'update_server_state',
125 None,
126 )
127 if update_server_state:
128 await update_server_state()
129 ws_context = websockets.connect(uri=f'ws://localhost:{service_port}/{path}', **kwargs)
130 async with ws_context as socket:
131 yield socket
132
133 return Client()
134
135
136@pytest.fixture
138 service_client, # For daemon setup and userver_client_cleanup
139 userver_monitor_client_options,
140 monitor_baseurl,
141) -> client.ClientMonitor:
142 """
143 Main fixture that provides access to userver monitor listener.
144
145 @snippet samples/testsuite-support/tests/test_metrics.py metrics labels
146 @ingroup userver_testsuite_fixtures
147 """
148 aiohttp_client = client.AiohttpClientMonitor(
149 monitor_baseurl,
150 **userver_monitor_client_options,
151 )
152 return client.ClientMonitor(aiohttp_client)
153
154
155# @cond
156
157
158class _ClientDiagnose(base_service_client.Client):
159 def __getattr__(self, name: str) -> None:
160 raise AttributeError(
161 f'"Client" object has no attribute "{name}". '
162 'Note that "service_client" fixture returned the basic '
163 '"testsuite.daemons.service_client.Client" client rather than '
164 'a "pytest_userver.client.Client" client with userver '
165 'extensions. That happened because the service '
166 'static configuration file contains no "tests-control" '
167 'component with "action" field.',
168 )
169
170
171# Overriding testsuite fixture
172@pytest.fixture(name='service_client_options')
173def _service_client_options(
174 service_client_options,
175 service_client_default_headers,
176 mockserver,
177):
178 # Should only use options for the base testsuite client here,
179 # not for the userver client.
180 return dict(
181 service_client_options,
182 headers={
183 **service_client_default_headers,
184 mockserver.trace_id_header: mockserver.trace_id,
185 },
186 )
187
188
189@pytest.fixture
190def userver_service_client_options(
191 service_client_options,
192 _testsuite_client_config: client.TestsuiteClientConfig,
193 testpoint,
194 testpoint_control,
195 service_periodic_tasks_state,
196 userver_log_capture,
197 mocked_time,
198 cache_invalidation_state,
199 userver_cache_control,
200 asyncexc_check,
201 userver_allow_all_caches_invalidation,
202):
203 return dict(
204 **service_client_options,
205 config=_testsuite_client_config,
206 testpoint=testpoint,
207 testpoint_control=testpoint_control,
208 periodic_tasks_state=service_periodic_tasks_state,
209 log_capture_fixture=userver_log_capture,
210 mocked_time=mocked_time,
211 cache_invalidation_state=cache_invalidation_state,
212 cache_control=userver_cache_control,
213 asyncexc_check=asyncexc_check,
214 allow_all_caches_invalidation=userver_allow_all_caches_invalidation,
215 )
216
217
218@pytest.fixture
219def userver_monitor_client_options(
220 service_client_options,
221 _testsuite_client_config: client.TestsuiteClientConfig,
222):
223 return dict(**service_client_options, config=_testsuite_client_config)
224
225
226# @endcond
227
228
229@pytest.fixture(scope='session')
230def service_baseurl(service_port) -> str:
231 """
232 Returns the main listener URL of the service.
233
234 Override this fixture to change the main listener URL that the testsuite
235 uses for tests.
236
237 @ingroup userver_testsuite_fixtures
238 """
239 return f'http://localhost:{service_port}/'
240
241
242@pytest.fixture(scope='session')
243def monitor_baseurl(monitor_port) -> str:
244 """
245 Returns the main monitor URL of the service.
246
247 Override this fixture to change the main monitor URL that the testsuite
248 uses for tests.
249
250 @ingroup userver_testsuite_fixtures
251 """
252 return f'http://localhost:{monitor_port}/'
253
254
255@pytest.fixture(scope='session')
256def service_periodic_tasks_state() -> client.PeriodicTasksState:
258
259
260@pytest.fixture(scope='session')
261def _testsuite_client_config(service_config) -> client.TestsuiteClientConfig:
262 components = service_config['components_manager']['components']
263
264 def get_component_path(name, argname=None):
265 if name in components:
266 path = components[name]['path']
267 path = path.rstrip('*')
268
269 if argname and f'{{{argname}}}' not in path:
270 raise RuntimeError(
271 f'Component {name} must provide path argument {argname}',
272 )
273 return path
274 return None
275
277 server_monitor_path=get_component_path('handler-server-monitor'),
278 testsuite_action_path=get_component_path('tests-control', 'action'),
279 )