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 _testsuite_client_config: client.TestsuiteClientConfig,
87 _service_client_base,
88 _service_client_testsuite,
89 # User defined client deps must be last in order to use
90 # fixtures defined above.
91 extra_client_deps,
92 auto_client_deps,
93) -> client.Client:
94 """
95 Main fixture that provides access to userver based service.
96
97 @snippet samples/testsuite-support/tests/test_ping.py service_client
98 @anchor service_client
99 @ingroup userver_testsuite_fixtures
100 """
101 # The service is lazily started here (not at the 'session' scope)
102 # to allow '*_client_deps' to be active during service start
103 daemon = await ensure_daemon_started(service_daemon)
104
105 if not _testsuite_client_config.testsuite_action_path:
106 yield _service_client_base
107 else:
108 service_client = _service_client_testsuite(daemon)
109 async with userver_client_cleanup(service_client):
110 yield service_client
111
112
113@pytest.fixture
115 request,
116 _userver_logging_plugin,
117 _dynamic_config_defaults_storage,
118 _check_config_marks,
119 dynamic_config,
120) -> typing.Callable[[client.Client], typing.AsyncGenerator]:
121 """
122 Contains the pre-test and post-test setup that depends
123 on @ref service_client.
124
125 Feel free to override, but in that case make sure to call the original
126 `userver_client_cleanup` fixture instance.
127
128 @ingroup userver_testsuite_fixtures
129 """
130 marker = request.node.get_closest_marker('suspend_periodic_tasks')
131 if marker:
132 tasks_to_suspend = marker.args
133 else:
134 tasks_to_suspend = ()
135
136 @compat.asynccontextmanager
137 async def cleanup_manager(client: client.Client):
138 @_userver_logging_plugin.register_flusher
139 async def do_flush():
140 try:
141 await client.log_flush()
142 except aiohttp.client_exceptions.ClientError:
143 pass
144 except RuntimeError:
145 # TODO: find a better way to handle closed aiohttp session
146 pass
147
148 # Service is already started we don't want startup logs to be shown
149 _userver_logging_plugin.update_position()
150
151 await _dynamic_config_defaults_storage.update(client, dynamic_config)
152 _check_config_marks()
153
154 await client.suspend_periodic_tasks(tasks_to_suspend)
155 try:
156 yield
157 finally:
158 await client.resume_all_periodic_tasks()
159
160 return cleanup_manager
161
162
163@pytest.fixture
164async def websocket_client(service_client, service_port):
165 """
166 Fixture that provides access to userver based websocket service.
167
168 @anchor websocket_client
169 @ingroup userver_testsuite_fixtures
170 """
171
172 class Client:
173 @compat.asynccontextmanager
174 async def get(self, path):
175 update_server_state = getattr(
176 service_client, 'update_server_state', None,
177 )
178 if update_server_state:
179 await update_server_state()
180 ws_context = websockets.connect(
181 f'ws://localhost:{service_port}/{path}',
182 )
183 async with ws_context as socket:
184 yield socket
185
186 return Client()
187
188
189@pytest.fixture
191 service_client,
192 service_client_options,
193 mockserver,
194 monitor_baseurl: str,
195 _testsuite_client_config: client.TestsuiteClientConfig,
197 """
198 Main fixture that provides access to userver monitor listener.
199
200 @snippet samples/testsuite-support/tests/test_metrics.py metrics labels
201 @ingroup userver_testsuite_fixtures
202 """
203 aiohttp_client = client.AiohttpClientMonitor(
204 monitor_baseurl,
205 config=_testsuite_client_config,
206 headers={'x-yatraceid': mockserver.trace_id},
207 **service_client_options,
208 )
209 return client.ClientMonitor(aiohttp_client)
210
211
212@pytest.fixture
213async def _service_client_base(service_baseurl, service_client_options):
214 class _ClientDiagnose(base_service_client.Client):
215 def __getattr__(self, name: str) -> None:
216 raise AttributeError(
217 f'"Client" object has no attribute "{name}". '
218 'Note that "service_client" fixture returned the basic '
219 '"testsuite.daemons.service_client.Client" client rather than '
220 'a "pytest_userver.client.Client" client with userver '
221 'extensions. That happened because the service '
222 'static configuration file contains no "tests-control" '
223 'component with "action" field.',
224 )
225
226 return _ClientDiagnose(service_baseurl, **service_client_options)
227
228
229@pytest.fixture
230def _service_client_testsuite(
231 service_baseurl,
232 service_client_options,
233 mocked_time,
234 userver_cache_control,
235 userver_log_capture,
236 testpoint,
237 testpoint_control,
238 cache_invalidation_state,
239 service_periodic_tasks_state,
240 _testsuite_client_config: client.TestsuiteClientConfig,
241) -> typing.Callable[[DaemonInstance], client.Client]:
242 def create_client(daemon):
243 aiohttp_client = client.AiohttpClient(
244 service_baseurl,
245 config=_testsuite_client_config,
246 testpoint=testpoint,
247 testpoint_control=testpoint_control,
248 periodic_tasks_state=service_periodic_tasks_state,
249 log_capture_fixture=userver_log_capture,
250 mocked_time=mocked_time,
251 cache_invalidation_state=cache_invalidation_state,
252 cache_control=userver_cache_control(daemon),
253 **service_client_options,
254 )
255 return client.Client(aiohttp_client)
256
257 return create_client
258
259
260@pytest.fixture(scope='session')
261def service_baseurl(service_port) -> str:
262 """
263 Returns the main listener URL of the service.
264
265 Override this fixture to change the main listener URL that the testsuite
266 uses for tests.
267
268 @ingroup userver_testsuite_fixtures
269 """
270 return f'http://localhost:{service_port}/'
271
272
273@pytest.fixture(scope='session')
274def monitor_baseurl(monitor_port) -> str:
275 """
276 Returns the main monitor URL of the service.
277
278 Override this fixture to change the main monitor URL that the testsuite
279 uses for tests.
280
281 @ingroup userver_testsuite_fixtures
282 """
283 return f'http://localhost:{monitor_port}/'
284
285
286@pytest.fixture(scope='session')
287def service_periodic_tasks_state() -> client.PeriodicTasksState:
289
290
291@pytest.fixture(scope='session')
292def _testsuite_client_config(
293 pytestconfig, service_config,
294) -> client.TestsuiteClientConfig:
295 components = service_config['components_manager']['components']
296
297 def get_component_path(name, argname=None):
298 if name in components:
299 path = components[name]['path']
300 path = path.rstrip('*')
301
302 if argname and f'{{{argname}}}' not in path:
303 raise RuntimeError(
304 f'Component {name} must provide path argument {argname}',
305 )
306 return path
307 return None
308
310 server_monitor_path=get_component_path('handler-server-monitor'),
311 testsuite_action_path=get_component_path('tests-control', 'action'),
312 )