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