userver: /home/antonyzhilin/arcadia/taxi/uservices/userver/testsuite/pytest_plugins/pytest_userver/plugins/service.py Source File
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
service.py
1"""
2Start the service in testsuite.
3"""
4
5# pylint: disable=redefined-outer-name
6import logging
7import pathlib
8import time
9import typing
10from typing import Any
11from typing import Dict
12from typing import Iterable
13from typing import List
14from typing import Optional
15
16import pytest
17
18from testsuite.daemons.pytest_plugin import DaemonInstance
19from testsuite.utils import url_util
20
21from pytest_userver.utils import net
22
23logger = logging.getLogger(__name__)
24
25
26def pytest_addoption(parser) -> None:
27 group = parser.getgroup('userver')
28 group.addoption(
29 '--service-logs-file',
30 type=pathlib.Path,
31 help='Write service output to specified file',
32 )
33 group.addoption(
34 '--service-logs-pretty',
35 action='store_true',
36 help='Enable pretty print and colorize service logs',
37 )
38 group.addoption(
39 '--service-logs-pretty-verbose',
40 dest='service_logs_pretty',
41 action='store_const',
42 const='verbose',
43 help='Enable pretty print and colorize service logs in verbose mode',
44 )
45 group.addoption(
46 '--service-logs-pretty-disable',
47 action='store_false',
48 dest='service_logs_pretty',
49 help='Disable pretty print and colorize service logs',
50 )
51
52
53@pytest.fixture(scope='session')
55 """
56 Override this to pass extra environment variables to the service.
57
58 @snippet samples/redis_service/testsuite/conftest.py service_env
59 @ingroup userver_testsuite_fixtures
60 """
61 return None
62
63
64@pytest.fixture(scope='session')
65async def service_http_ping_url(service_config, service_baseurl) -> Optional[str]:
66 """
67 Returns the service HTTP ping URL that is used by the testsuite to detect
68 that the service is ready to work. Returns None if there's no such URL.
69
70 By default, attempts to find server::handlers::Ping component by
71 "handler-ping" name in static config. Override this fixture to change the
72 behavior.
73
74 @ingroup userver_testsuite_fixtures
75 """
76 components = service_config['components_manager']['components']
77 ping_handler = components.get('handler-ping')
78 if ping_handler:
79 return url_util.join(service_baseurl, ping_handler['path'])
80 return None
81
82
83@pytest.fixture(scope='session')
84def service_non_http_health_checks( # pylint: disable=invalid-name
85 service_config,
86) -> net.HealthChecks:
87 """
88 Returns a health checks info.
89
90 By default, returns pytest_userver.utils.net.get_health_checks_info().
91
92 Override this fixture to change the way testsuite detects the tested
93 service being alive.
94
95 @ingroup userver_testsuite_fixtures
96 """
97
98 return net.get_health_checks_info(service_config)
99
100
101@pytest.fixture(scope='session')
103 create_daemon_scope,
104 daemon_scoped_mark,
105 service_env,
106 service_http_ping_url,
107 service_config_path_temp,
108 service_binary,
109 service_non_http_health_checks,
110):
111 """
112 Prepares the start of the service daemon.
113 Configures the health checking to use service_http_ping_url fixture value
114 if it is not None; otherwise uses the service_non_http_health_checks info.
115
116 @see @ref pytest_userver.plugins.service.service_daemon_instance "service_daemon_instance"
117 @ingroup userver_testsuite_fixtures
118 """
119 assert service_http_ping_url or service_non_http_health_checks.tcp, (
120 '"service_http_ping_url" and "create_health_checker" fixtures '
121 'returned None. Testsuite is unable to detect if the service is ready '
122 'to accept requests.',
123 )
124
125 logger.debug(
126 'userver fixture "service_daemon_scope" would check for "%s"',
127 service_non_http_health_checks,
128 )
129
130 class LocalCounters:
131 last_log_time = 0.0
132 attempts = 0
133
134 async def _checker(*, session, process) -> bool:
135 LocalCounters.attempts += 1
136 new_log_time = time.monotonic()
137 if new_log_time - LocalCounters.last_log_time > 1.0:
138 LocalCounters.last_log_time = new_log_time
139 logger.debug(
140 'userver fixture "service_daemon_scope" checking "%s", attempt %s',
141 service_non_http_health_checks,
142 LocalCounters.attempts,
143 )
144
145 return await net.check_availability(service_non_http_health_checks)
146
147 health_check = _checker
148 if service_http_ping_url:
149 health_check = None
150
151 async with create_daemon_scope(
152 args=[str(service_binary), '--config', str(service_config_path_temp)],
153 ping_url=service_http_ping_url,
154 health_check=health_check,
155 env=service_env,
156 ) as scope:
157 yield scope
158
159
160@pytest.fixture
161def extra_client_deps() -> None:
162 """
163 Service client dependencies hook. Feel free to override, e.g.:
164
165 @code
166 @pytest.fixture
167 def extra_client_deps(some_fixtures_to_wait_before_service_start):
168 pass
169 @endcode
170
171 @ingroup userver_testsuite_fixtures
172 """
173
174
175@pytest.fixture
176def auto_client_deps(request) -> None:
177 """
178 Ensures that the following fixtures, if available, are run before service start:
179
180 * `pgsql`
181 * `mongodb`
182 * `clickhouse`
183 * `rabbitmq`
184 * kafka (`kafka_producer`, `kafka_consumer`)
185 * `redis_store`
186 * `mysql`
187 * @ref pytest_userver.plugins.ydb.ydbsupport.ydb "ydb"
188 * @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver "grpc_mockserver"
189
190 To add other dependencies prefer overriding the
191 @ref pytest_userver.plugins.service.extra_client_deps "extra_client_deps"
192 fixture.
193
194 @ingroup userver_testsuite_fixtures
195 """
196 known_deps = {
197 'pgsql',
198 'mongodb',
199 'clickhouse',
200 'rabbitmq',
201 'kafka_producer',
202 'kafka_consumer',
203 'redis_store',
204 'mysql',
205 'ydb',
206 'grpc_mockserver',
207 }
208
209 try:
210 fixture_lookup_error = pytest.FixtureLookupError
211 except AttributeError:
212 # support for an older version of the pytest
213 import _pytest.fixtures
214
215 fixture_lookup_error = _pytest.fixtures.FixtureLookupError
216
217 resolved_deps = []
218 for dep in known_deps:
219 try:
220 request.getfixturevalue(dep)
221 resolved_deps.append(dep)
222 except fixture_lookup_error:
223 pass
224
225 logger.debug(
226 'userver fixture "auto_client_deps" resolved dependencies %s',
227 resolved_deps,
228 )
229
230
231@pytest.fixture
233 testpoint,
234 cleanup_userver_dumps,
235 userver_log_capture,
236 dynamic_config,
237 mock_configs_service,
238):
239 """
240 Service client dependencies hook, like
241 @ref pytest_userver.plugins.service.extra_client_deps "extra_client_deps".
242
243 Feel free to override globally in a more specific pytest plugin
244 (one that comes after userver plugins),
245 but make sure to depend on the original fixture:
246
247 @code
248 @pytest.fixture(name='builtin_client_deps')
249 def _builtin_client_deps(builtin_client_deps, some_extra_fixtures):
250 pass
251 @endcode
252
253 @ingroup userver_testsuite_fixtures
254 """
255
256
257@pytest.fixture
259 ensure_daemon_started,
260 service_daemon_scope,
261 builtin_client_deps,
262 auto_client_deps,
263 # User defined client deps must be last in order to use
264 # fixtures defined above.
265 extra_client_deps,
266) -> DaemonInstance:
267 """
268 Calls `ensure_daemon_started` on
269 @ref pytest_userver.plugins.service.service_daemon_scope "service_daemon_scope"
270 to actually start the service. Makes sure that all the dependencies are prepared
271 before the service starts.
272
273 @see @ref pytest_userver.plugins.service.extra_client_deps "extra_client_deps"
274 @see @ref pytest_userver.plugins.service.auto_client_deps "auto_client_deps"
275 @see @ref pytest_userver.plugins.service.builtin_client_deps "builtin_client_deps"
276 @ingroup userver_testsuite_fixtures
277 """
278 # TODO also run userver_client_cleanup here
279 return await ensure_daemon_started(service_daemon_scope)
280
281
282@pytest.fixture(scope='session')
283def daemon_scoped_mark(request) -> Optional[Dict[str, Any]]:
284 """
285 Depend on this fixture directly or transitively to make your fixture a per-daemon fixture.
286
287 Example:
288
289 @code
290 @pytest.fixture(scope='session')
291 def users_cache_state(daemon_scoped_mark, ...):
292 return UsersCacheState(users_list=[])
293 @endcode
294
295 For tests marked with `@pytest.mark.uservice_oneshot(...)`, the service will be restarted,
296 and all the per-daemon fixtures will be recreated.
297
298 This fixture returns kwargs passed to the `uservice_oneshot` mark (which may be an empty dict).
299 For normal tests, this fixture returns `None`.
300
301 @ingroup userver_testsuite_fixtures
302 """
303 # === How daemon-scoped fixtures work ===
304 # pytest always keeps no more than 1 instance of each parametrized fixture.
305 # When a parametrized fixture is requested, FixtureDef checks using __eq__ whether the current value
306 # of fixture param equals the cached value. If they differ, then the fixture and all the dependent fixtures
307 # are torn down (in reverse-dependency order), then the fixture is set up again.
308 # TLDR: when the param changes, the whole tree of daemon-specific fixtures is torn down.
309 return getattr(request, 'param', None)
310
311
312# @cond
313
314
315def pytest_configure(config):
316 config.addinivalue_line(
317 'markers',
318 'uservice_oneshot: use a per-test service daemon instance',
319 )
320
321
322def _contains_oneshot_marker(parametrize: Iterable[pytest.Mark]) -> bool:
323 """
324 Check if at least one of 'parametrize' marks is of the form:
325
326 @pytest.mark.parametrize(
327 "foo, bar",
328 [
329 ("a", 10),
330 pytest.param("b", 20, marks=pytest.mark.uservice_oneshot), # <====
331 ]
332 )
333 """
334 return any(
335 True
336 for parametrize_mark in parametrize
337 if len(parametrize_mark.args) >= 2
338 for parameter_set in parametrize_mark.args[1]
339 if hasattr(parameter_set, 'marks')
340 for mark in parameter_set.marks
341 if mark.name == 'uservice_oneshot'
342 )
343
344
345def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
346 oneshot_marker = metafunc.definition.get_closest_marker('uservice_oneshot')
347 parametrize_markers = metafunc.definition.iter_markers('parametrize')
348 if oneshot_marker is not None or _contains_oneshot_marker(parametrize_markers):
349 # Set a dummy parameter value. Actual param is patched in pytest_collection_modifyitems.
350 metafunc.parametrize(
351 (daemon_scoped_mark.__name__,),
352 [(None,)],
353 indirect=True,
354 # TODO use pytest.HIDDEN_PARAM after it becomes available
355 # https://github.com/pytest-dev/pytest/issues/13228
356 ids=['uservice_oneshot'],
357 # TODO use scope='function' after it stops breaking fixture dependencies
358 # https://github.com/pytest-dev/pytest/issues/13248
359 scope=None,
360 )
361
362
363# TODO use dependent parametrize instead of patching param value after it becomes available
364# https://github.com/pytest-dev/pytest/issues/13233
365def pytest_collection_modifyitems(items: List[pytest.Item]):
366 for item in items:
367 oneshot_marker = item.get_closest_marker('uservice_oneshot')
368 if oneshot_marker and isinstance(item, pytest.Function):
369 func_item = typing.cast(pytest.Function, item)
370 func_item.callspec.params[daemon_scoped_mark.__name__] = dict(oneshot_marker.kwargs, function=func_item)
371
372
373# @endcond