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