userver: /data/code/userver/testsuite/pytest_plugins/pytest_userver/grpc/__init__.py Source File
Loading...
Searching...
No Matches
__init__.py
1"""
2Mocks for the gRPC servers, a.k.a. `pytest_userver.grpc.
3
4@sa @ref scripts/docs/en/userver/tutorial/grpc_service.md
5@sa @ref pytest_userver.plugins.grpc.mockserver
6"""
7
8from __future__ import annotations
9
10import asyncio
11from collections.abc import Callable
12import contextlib
13import inspect
14import types
15import typing
16from typing import Any
17from typing import Generic
18from typing import TypeAlias
19from typing import TypeVar
20
21import grpc
22
23import testsuite.utils.callinfo
24
25from ._mocked_errors import MockedError # noqa: F401
26from ._mocked_errors import NetworkError # noqa: F401
27from ._mocked_errors import TimeoutError # noqa: F401
28from ._servicer_mock import _check_is_servicer_class
29from ._servicer_mock import _create_servicer_mock
30from ._servicer_mock import _ServiceMock
31
32Handler: TypeAlias = Callable[[Any, grpc.aio.ServicerContext], Any]
33MockDecorator: TypeAlias = Callable[[Handler], testsuite.utils.callinfo.AsyncCallQueue]
34AsyncExcAppend: TypeAlias = Callable[[Exception], None]
35Servicer = TypeVar('Servicer')
36
37
39 """
40 Allows to install mocks that are kept active between tests, see
41 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver_session "grpc_mockserver_session".
42
43 @warning This is a sharp knife, use with caution! For most use-cases, prefer
44 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver "grpc_mockserver" instead.
45 """
46
47 def __init__(self, *, server: grpc.aio.Server, experimental: bool = False) -> None:
48 """
49 @warning This initializer is an **experimental API**, likely to break in the future. Consider using
50 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver_session "grpc_mockserver_session" instead.
51
52 Initializes `MockserverSession`. Takes ownership of `server`.
53 To properly start and stop the server, use `MockserverSession` as an async contextmanager:
54
55 @code{.py}
56 async with pytest_userver.grpc.mockserver.MockserverSession(server=server) as mockserver:
57 # ...
58 @endcode
59
60 @note `MockserverSession` is usually obtained from
61 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver_session "grpc_mockserver_session".
62 """
63 assert experimental
64 self._server = server
65 self._auto_service_mocks: dict[type, _ServiceMock] = {}
66 self._asyncexc_append: AsyncExcAppend | None = None
67
68 def _get_auto_service_mock(self, servicer_class: type, /) -> _ServiceMock:
69 existing_mock = self._auto_service_mocks.get(servicer_class)
70 if existing_mock:
71 return existing_mock
72 new_mock = _create_servicer_mock(servicer_class)
73 new_mock.set_asyncexc_append(self._asyncexc_append)
74 self._auto_service_mocks[servicer_class] = new_mock
75 new_mock.add_to_server(self._server)
76 return new_mock
77
78 def _set_asyncexc_append(self, asyncexc_append: AsyncExcAppend | None, /) -> None:
79 self._asyncexc_append = asyncexc_append
80 for mock in self._auto_service_mocks.values():
81 mock.set_asyncexc_append(asyncexc_append)
82
83 def reset_mocks(self) -> None:
84 """
85 @brief Removes all mocks for this mockserver that have been installed using
86 `MockserverSession` or @ref pytest_userver.grpc.Mockserver "Mockserver" API.
87 @note Mocks installed manually using @ref MockserverSession.server will not be removed, though.
88 """
89 for mock in self._auto_service_mocks.values():
90 mock.reset_handlers()
91
92 @contextlib.contextmanager
93 def asyncexc_append_scope(self, asyncexc_append: AsyncExcAppend | None, /):
94 """
95 Sets testsuite's `asyncexc_append` for use in the returned scope.
96 """
97 self._set_asyncexc_append(asyncexc_append)
98 try:
99 yield
100 finally:
101 self._set_asyncexc_append(None)
102
103 @property
104 def server(self) -> grpc.aio.Server:
105 """
106 The underlying [grpc.aio.Server](https://grpc.github.io/grpc/python/grpc_asyncio.html#grpc.aio.Server).
107 """
108 return self._server
109
110 async def start(self) -> None:
111 """
112 Starts the server.
113 @note Prefer starting mockserver using the async contextmanager syntax if possible.
114 """
115 await self._server.start()
116
117 async def stop(self) -> None:
118 """
119 Stops the server properly. Prefer this method to stopping `server` manually.
120 """
121 await _stop_server(self._server)
122
123 async def __aenter__(self) -> MockserverSession:
124 await self.start()
125 return self
126
127 async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
128 await self.stop()
129
130
132 """
133 Allows to install mocks that are reset between tests, see
134 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver "grpc_mockserver".
135 """
136
137 def __init__(self, *, mockserver_session: MockserverSession, experimental: bool = False) -> None:
138 """
139 @warning This initializer is an **experimental API**, likely to break in the future. Consider using
140 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver "grpc_mockserver" instead.
141
142 Initializes Mockserver.
143
144 @note `Mockserver` is usually obtained from
145 @ref pytest_userver.plugins.grpc.mockserver.grpc_mockserver "grpc_mockserver".
146 """
147 assert experimental
148 self._mockserver_session = mockserver_session
149
150 def __call__(self, servicer_method, /) -> MockDecorator:
151 """
152 Returns a decorator to mock the specified gRPC service method implementation.
153
154 Example:
155
156 @snippet samples/grpc_service/testsuite/test_grpc.py Prepare modules
157 @snippet samples/grpc_service/testsuite/test_grpc.py grpc client test
158 """
159 servicer_class = _get_class_from_method(servicer_method)
160 mock = self._mockserver_session._get_auto_service_mock(servicer_class) # pylint: disable=protected-access
161 return mock.install_handler(servicer_method.__name__)
162
163 def mock_factory(self, servicer_class, /) -> Callable[[str], MockDecorator]:
164 """
165 Allows to create a fixture as a shorthand for mocking methods of the specified gRPC service.
166
167 Example:
168
169 @snippet grpc/functional_tests/metrics/tests/conftest.py Prepare modules
170 @snippet grpc/functional_tests/metrics/tests/conftest.py Prepare server mock
171 @snippet grpc/functional_tests/metrics/tests/test_metrics.py grpc client test
172 """
173
174 def factory(method_name):
175 method = getattr(servicer_class, method_name, None)
176 if method is None:
177 raise ValueError(f'No method "{method_name}" in servicer class "{servicer_class.__name__}"')
178 return self(method)
179
180 _check_is_servicer_class(servicer_class)
181 return factory
182
183 def install_servicer(self, servicer: Servicer, /) -> Servicer:
184 """
185 Installs as a mock `servicer`, the class of which should inherit from a generated `*Servicer` class.
186
187 1. Write a service implementation:
188
189 @snippet grpc/functional_tests/middleware_client/tests/test_install_servicer.py servicer
190
191 @note Inheritance from multiple `*Servicer` classes at once is allowed.
192
193 2. Install servicer instance:
194
195 @snippet grpc/functional_tests/middleware_client/tests/test_install_servicer.py install servicer
196
197 3. Use service mock:
198
199 @snippet grpc/functional_tests/middleware_client/tests/test_install_servicer.py use mock
200 """
201 base_servicer_classes = [cls for cls in inspect.getmro(type(servicer)) if _is_servicer_class(cls)]
202 if not base_servicer_classes:
203 raise ValueError(f"Given object's type ({type(servicer)}) is not inherited from any grpc *Servicer class")
204 proxy = _MockProxy(servicer)
205 for servicer_class in base_servicer_classes:
206 # pylint: disable=protected-access
207 mock = self._mockserver_session._get_auto_service_mock(servicer_class)
208 for python_method_name in mock.known_methods:
209 if _get_class_that_defined_method(type(servicer), python_method_name) not in base_servicer_classes:
210 handler_func: types.MethodType = getattr(servicer, python_method_name)
211 callqueue = mock.install_handler(python_method_name)(handler_func)
212 object.__setattr__(proxy, python_method_name, callqueue)
213 return typing.cast(Servicer, proxy)
214
215
216# @cond
217
218
219async def _stop_server(server: grpc.aio.Server, /) -> None:
220 async def stop_server():
221 await server.stop(grace=None)
222 await server.wait_for_termination()
223
224 stop_server_task = asyncio.shield(asyncio.create_task(stop_server()))
225 # asyncio.shield does not protect our await from cancellations, and we
226 # really need to wait for the server stopping before continuing.
227 try:
228 await stop_server_task
229 except asyncio.CancelledError:
230 await stop_server_task
231 # Propagate cancellation when we are done
232 raise
233
234
235def _get_class_from_method(method) -> type:
236 # https://stackoverflow.com/a/54597033/5173839
237 assert inspect.isfunction(method), f'Expected an unbound method: foo(ClassName.MethodName), got: {method}'
238 class_name = method.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0]
239 try:
240 cls = getattr(inspect.getmodule(method), class_name)
241 except AttributeError:
242 cls = method.__globals__.get(class_name)
243 assert isinstance(cls, type)
244 return cls
245
246
247def _get_class_that_defined_method(cls: type, method_name: str) -> type | None:
248 for cls in inspect.getmro(cls):
249 if method_name in cls.__dict__:
250 return cls
251 # May happen with a method defined via __getattr__.
252 return None
253
254
255def _is_servicer_class(cls: type) -> bool:
256 try:
257 return '_pb2_grpc.' in inspect.getfile(cls)
258 except TypeError:
259 return False
260
261
262class _MockProxy(Generic[Servicer]):
263 def __init__(self, wrapped_servicer: Servicer) -> None:
264 object.__setattr__(self, '_wrapped_servicer', wrapped_servicer)
265
266 def __getattr__(self, name: str) -> Any:
267 servicer: Servicer = object.__getattribute__(self, '_wrapped_servicer')
268 return getattr(servicer, name)
269
270 def __setattr__(self, name: str, value: Any) -> None:
271 servicer: Servicer = object.__getattribute__(self, '_wrapped_servicer')
272 setattr(servicer, name, value)
273
274
275# @endcond