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