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