9from typing
import Any, AsyncContextManager
14from testsuite
import types
17from .
import service_client, service_daemon
18from .classes
import DaemonInstance
19from .spawn
import __tracebackhide__
22 'SIGINT': signal.SIGINT,
23 'SIGKILL': signal.SIGKILL,
24 'SIGQUIT': signal.SIGQUIT,
25 'SIGTERM': signal.SIGTERM,
35 multiple: bool =
False,
41 async def spawn(self) -> 'DaemonInstance':
44 if inspect.iscoroutine(manager):
46 f
'Please rewrite your spawner into async context manager {self._spawn}',
47 PendingDeprecationWarning,
49 manager = await manager
50 process = await manager.__aenter__()
55 _cells: dict[str, tuple[_DaemonScope, DaemonInstance]]
57 def __init__(self) -> None:
60 async def aclose(self, skip_multiple=False) -> None:
62 for name, (scope, daemon)
in self.
_cells.items():
63 if skip_multiple
and scope.multiple:
64 cells_left[name] = scope, daemon
69 @contextlib.asynccontextmanager
75 multiple: bool =
False,
76 ) -> AsyncGenerator[_DaemonScope,
None]:
78 Creates new scope evicting previous daemon.
80 :param name: scope identifier.
81 :param spawn: spawner instance.
82 :param multiple: do not fail when this scope is requested with others.
90 async def request(self, scope: _DaemonScope) -> DaemonInstance:
91 if scope.name
in self.
_cells:
92 _, daemon = self.
_cells[scope.name]
93 if daemon.process
is None:
95 if daemon.process.poll()
is None:
97 await self.
aclose(skip_multiple=
True)
98 daemon = await scope.spawn()
99 self.
_cells[scope.name] = scope, daemon
102 def has_running_daemons(self) -> bool:
103 for _, daemon
in self.
_cells.values():
104 if daemon.process
and daemon.process.poll()
is None:
108 async def _cleanup_cell(self, name):
109 if name
not in self.
_cells:
111 scope, daemon = self.
_cells.pop(name)
114 async def _close_daemon(self, daemon: DaemonInstance):
115 await daemon.aclose()
119 """Fixture that starts requested service."""
121 async def __call__(self, scope: _DaemonScope) -> DaemonInstance: ...
129 base_command: Sequence[str] |
None =
None,
130 env: dict[str, str] |
None =
None,
131 poll_retries: int = service_daemon.POLL_RETRIES,
132 ping_url: str |
None =
None,
133 ping_request_timeout: float = service_daemon.PING_REQUEST_TIMEOUT,
134 ping_response_codes: tuple[int] = service_daemon.PING_RESPONSE_CODES,
135 health_check: service_daemon.HealthCheckType |
None =
None,
136 subprocess_spawner: Callable[..., subprocess.Popen] |
None =
None,
137 subprocess_options: dict[str, Any] |
None =
None,
138 setup_service: Callable[[subprocess.Popen],
None] |
None =
None,
139 shutdown_signal: int |
None =
None,
143 """Creates service spawner asynccontextmanager factory.
145 :param args: command arguments
146 :param base_command: Arguments to be prepended to ``args``.
147 :param env: Environment variables dictionary.
148 :param poll_retries: Number of tries for service health check
149 :param ping_url: service health check url, service is considered up
151 :param ping_request_timeout: Timeout for ping_url request
152 :param ping_response_codes: HTTP resopnse codes tuple meaning that
153 service is up and running.
154 :param health_check: Async function to check service is running.
155 :param subprocess_spawner: callable with `subprocess.Popen` interface.
156 :param subprocess_options: Custom subprocess options.
157 :param setup_service: Function to be called right after service
159 :param shutdown_signal: Signal used to stop running services.
160 :returns: Return asynccontextmanager factory that might be used
161 within ``register_daemon_scope`` fixture.
166 """Create daemon scope for daemon with command to start."""
172 ping_url: str |
None =
None,
173 name: str |
None =
None,
174 base_command: Sequence |
None =
None,
175 env: dict[str, str] |
None =
None,
176 poll_retries: int = service_daemon.POLL_RETRIES,
177 ping_request_timeout: float = service_daemon.PING_REQUEST_TIMEOUT,
178 ping_response_codes: tuple[int] = service_daemon.PING_RESPONSE_CODES,
179 health_check: service_daemon.HealthCheckType |
None =
None,
180 subprocess_options: dict[str, Any] |
None =
None,
181 setup_service: Callable[[subprocess.Popen],
None] |
None =
None,
182 shutdown_signal: int |
None =
None,
186 ) -> AsyncContextManager[_DaemonScope]:
188 :param args: command arguments
189 :param base_command: Arguments to be prepended to ``args``.
190 :param env: Environment variables dictionary.
191 :param poll_retries: Number of tries for service health check
192 :param ping_url: service health check url, service is considered up
194 :param ping_request_timeout: Timeout for ping_url request
195 :param ping_response_codes: HTTP resopnse codes tuple meaning that
196 service is up and running.
197 :param health_check: Async function to check service is running.
198 :param subprocess_options: Custom subprocess options.
199 :param setup_service: Function to be called right after service
201 :param shutdown_signal: Signal used to stop running services.
202 :param multiple: do not fail when this scope is requested with others.
203 :returns: Returns internal daemon scope instance to be used with
204 ``ensure_daemon_started`` fixture.
209 """Creates service client instance.
213 .. code-block:: python
215 def my_client(create_service_client):
216 return create_service_client('http://localhost:9999/')
227 :param base_url: base url for http client
228 :param client_class: client class to use
229 :returns: ``client_class`` instance
234def ensure_daemon_started(
235 _global_daemon_store: _DaemonStore, _testsuite_suspend_capture, pytestconfig
236) -> EnsureDaemonStartedFixture:
239 async def ensure_daemon_started(scope: _DaemonScope) -> DaemonInstance:
240 if not scope.multiple:
241 requests.add(scope.name)
242 if len(requests) > 1:
243 pytest.fail(f
'Test requested multiple daemons: {requests!r}')
245 if pytestconfig.option.service_wait:
246 with _testsuite_suspend_capture():
247 return await _global_daemon_store.request(scope)
248 return await _global_daemon_store.request(scope)
250 return ensure_daemon_started
253@pytest.fixture(scope='session')
254def service_spawner_factory(
256 service_client_session_factory: Any,
257 wait_service_started: Any,
258) -> ServiceSpawnerFactory:
259 def service_spawner_factory(
262 base_command: Sequence[str] |
None =
None,
263 env: dict[str, str] |
None =
None,
264 poll_retries: int = service_daemon.POLL_RETRIES,
265 ping_url: str |
None =
None,
266 ping_request_timeout: float = service_daemon.PING_REQUEST_TIMEOUT,
267 ping_response_codes: tuple[int] = service_daemon.PING_RESPONSE_CODES,
268 health_check: service_daemon.HealthCheckType |
None =
None,
269 subprocess_spawner: Callable[..., subprocess.Popen] |
None =
None,
270 subprocess_options: dict[str, Any] |
None =
None,
271 setup_service: Callable[[subprocess.Popen],
None] |
None =
None,
272 shutdown_signal: int |
None =
None,
276 shutdown_timeout = pytestconfig.option.service_shutdown_timeout
277 if shutdown_signal
is None:
278 shutdown_signal = SHUTDOWN_SIGNALS[
279 pytestconfig.option.service_shutdown_signal
282 health_check = service_daemon.make_health_check(
284 ping_request_timeout=ping_request_timeout,
285 ping_response_codes=ping_response_codes,
286 health_check=health_check,
289 command_args = _build_command_args(args, base_command)
291 @contextlib.asynccontextmanager
293 if pytestconfig.option.service_wait:
294 manager = wait_service_started(
296 health_check=health_check,
298 elif pytestconfig.option.service_disable:
299 manager = service_daemon.start_dummy_process()
301 manager = service_daemon.start(
304 shutdown_signal=shutdown_signal,
305 shutdown_timeout=shutdown_timeout,
306 poll_retries=poll_retries,
307 health_check=health_check,
308 session_factory=service_client_session_factory,
309 subprocess_options=subprocess_options,
310 setup_service=setup_service,
311 subprocess_spawner=subprocess_spawner,
312 stdout_handler=stdout_handler,
313 stderr_handler=stderr_handler,
315 async with manager
as process:
320 return service_spawner_factory
323@pytest.fixture(scope='session')
324def service_spawner(service_spawner_factory):
325 def service_spawner(*args, **kwargs):
326 factory = service_spawner_factory(*args, **kwargs)
328 'service_spawner() fixture is deprecated, '
329 'use service_spawner_factory()',
330 PendingDeprecationWarning,
338 return service_spawner
341@pytest.fixture(scope='session')
343 _global_daemon_store: _DaemonStore,
344 service_spawner_factory: ServiceSpawnerFactory,
345) -> CreateDaemonScope:
346 """Create daemon scope for daemon with command to start.
348 @ingroup userver_testsuite_fixtures
349 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/daemons/pytest_plugin.py#L342)
355 ping_url: str |
None =
None,
356 name: str |
None =
None,
357 base_command: Sequence |
None =
None,
358 env: dict[str, str] |
None =
None,
359 poll_retries: int = service_daemon.POLL_RETRIES,
360 ping_request_timeout: float = service_daemon.PING_REQUEST_TIMEOUT,
361 ping_response_codes: tuple[int] = service_daemon.PING_RESPONSE_CODES,
362 health_check: service_daemon.HealthCheckType |
None =
None,
363 subprocess_options: dict[str, Any] |
None =
None,
364 setup_service: Callable[[subprocess.Popen],
None] |
None =
None,
365 shutdown_signal: int |
None =
None,
369 ) -> AsyncContextManager[_DaemonScope]:
371 :param args: command arguments
372 :param base_command: Arguments to be prepended to ``args``.
373 :param env: Environment variables dictionary.
374 :param poll_retries: Number of tries for service health check
375 :param ping_url: service health check url, service is considered up
377 :param ping_request_timeout: Timeout for ping_url request
378 :param ping_response_codes: HTTP resopnse codes tuple meaning that
379 service is up and running.
380 :param health_check: Async function to check service is running.
381 :param subprocess_options: Custom subprocess options.
382 :param setup_service: Function to be called right after service
384 :param shutdown_signal: Signal used to stop running services.
385 :param multiple: do not fail when this scope is requested with others.
386 :returns: Returns internal daemon scope instance to be used with
387 ``ensure_daemon_started`` fixture.
390 name =
' '.join(args)
391 return _global_daemon_store.scope(
393 spawn=service_spawner_factory(
395 base_command=base_command,
397 poll_retries=poll_retries,
399 ping_request_timeout=ping_request_timeout,
400 ping_response_codes=ping_response_codes,
401 health_check=health_check,
402 subprocess_options=subprocess_options,
403 setup_service=setup_service,
404 shutdown_signal=shutdown_signal,
405 stdout_handler=stdout_handler,
406 stderr_handler=stderr_handler,
411 return create_daemon_scope
415def create_service_client(
416 service_client_default_headers: dict[str, str],
417 service_client_options: dict[str, Any],
418) -> CreateServiceClientFixture:
419 def create_service_client(
426 :param base_url: base url for http client
427 :param client_class: client class to use
428 :returns: ``client_class`` instance
432 headers=service_client_default_headers,
433 **service_client_options,
437 return create_service_client
440@pytest.fixture(scope='session')
441def wait_service_started(pytestconfig, service_client_session_factory):
442 reporter = pytestconfig.pluginmanager.getplugin(
'terminalreporter')
444 @contextlib.asynccontextmanager
445 async def waiter(*, args, health_check):
446 await service_daemon.service_wait(
449 health_check=health_check,
450 session_factory=service_client_session_factory,
457def pytest_addoption(parser):
458 group = parser.getgroup(
'services')
463 'Service client timeout in seconds. 0 means no timeout. '
464 'Default is %(default)s'
472 help=
'Do not start service daemon from testsuite',
477 help=
'Wait for service to start outside of testsuite itself, e.g. gdb',
480 '--service-shutdown-timeout',
481 help=
'Service shutdown timeout in seconds. Default is %(default)s',
486 '--service-shutdown-signal',
487 help=
'Service shutdown signal. Default is %(default)s',
489 choices=sorted(SHUTDOWN_SIGNALS.keys()),
493@pytest.fixture(scope='session')
495 """Context manager that registers service process session.
497 Yields daemon scope instance.
499 @param name service name
500 @param spawn asynccontextmanager service factory
502 @ingroup userver_testsuite_fixtures
503 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/daemons/pytest_plugin.py#L490)
505 return _global_daemon_store.scope
508@pytest.fixture(scope='session')
509def service_client_session_factory() -> service_daemon.ClientSessionFactory:
510 def make_session(**kwargs):
511 return aiohttp.ClientSession(**kwargs)
517async def service_client_session(
518 service_client_session_factory,
519) -> types.AsyncYieldFixture[aiohttp.ClientSession]:
520 async with service_client_session_factory()
as session:
526 """Default service client headers.
528 Fill free to override in your conftest.py
530 @ingroup userver_testsuite_fixtures
531 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/daemons/pytest_plugin.py#L518)
539 service_client_session: aiohttp.ClientSession,
541) -> types.YieldFixture[dict[str, Any]]:
542 """Returns service client options dictionary.
544 @ingroup userver_testsuite_fixtures
545 Part of the [yandex-taxi-testsuite](https://github.com/yandex/yandex-taxi-testsuite/blob/develop/testsuite/daemons/pytest_plugin.py#L527)
548 'session': service_client_session,
549 'timeout': pytestconfig.option.service_timeout
or None,
550 'span_id_header': mockserver.span_id_header,
554@pytest.fixture(scope='session')
555async def _global_daemon_store():
557 async with contextlib.aclosing(store):
561@pytest.fixture(scope='session')
562def _testsuite_suspend_capture(pytestconfig):
563 capmanager = pytestconfig.pluginmanager.getplugin(
'capturemanager')
565 @contextlib.contextmanager
568 capmanager.suspend_global_capture()
571 capmanager.resume_global_capture()
576def _build_command_args(
578 base_command: Sequence |
None,
580 return tuple(str(arg)
for arg
in itertools.chain(base_command
or (), args))