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