userver: /data/code/service_template/third_party/userver/testsuite/pytest_plugins/pytest_userver/plugins/grpc/mockserver.py Source File
Loading...
Searching...
No Matches
mockserver.py
1"""
2Mocks for the gRPC servers.
3
4@sa @ref scripts/docs/en/userver/tutorial/grpc_service.md
5"""
6
7# pylint: disable=no-member
8import asyncio
9import contextlib
10import functools
11import socket
12
13import grpc
14import pytest
15
16from testsuite.utils import callinfo
17
18DEFAULT_PORT = 8091
19
20
22 def __init__(self, servicer, methods):
23 self.servicer = servicer
24 self._known_methods = methods
25 self._methods = {}
26
27 def get(self, method, default):
28 return self._methods.get(method, default)
29
30 @contextlib.contextmanager
31 def mock(self):
32 try:
33 yield self.install_handler
34 finally:
35 self._methods = {}
36
37 def install_handler(self, method: str):
38 def decorator(func):
39 if method not in self._known_methods:
40 raise RuntimeError(
41 f'Trying to mock unknown grpc method {method}',
42 )
43
44 wrapped = callinfo.acallqueue(func)
45 self._methods[method] = wrapped
46 return wrapped
47
48 return decorator
49
50
51@pytest.fixture(scope='session')
52def _grpc_mockserver_endpoint(pytestconfig):
53 port = pytestconfig.option.grpc_mockserver_port
54 if pytestconfig.option.service_wait or pytestconfig.option.service_disable:
55 port = port or DEFAULT_PORT
56 if port == 0:
57 port = _find_free_port()
58 return f'{pytestconfig.option.grpc_mockserver_host}:{port}'
59
60
61@pytest.fixture(scope='session')
62def grpc_mockserver_endpoint(pytestconfig, _grpc_port) -> str:
63 """
64 Returns the gRPC endpoint to start the mocking server that is set by
65 command line `--grpc-mockserver-host` and `--grpc-mockserver-port` options.
66
67 Override this fixture to change the way the gRPC endpoint
68 is detected by the testsuite.
69
70 @snippet samples/grpc_service/tests/conftest.py Prepare configs
71 @ingroup userver_testsuite_fixtures
72 """
73 return f'{pytestconfig.option.grpc_mockserver_host}:{_grpc_port}'
74
75
76def _find_free_port() -> int:
77 with contextlib.closing(
78 socket.socket(socket.AF_INET6, socket.SOCK_STREAM),
79 ) as s:
80 s.bind(('', 0))
81 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
82 return s.getsockname()[1]
83
84
85@pytest.fixture(scope='session')
86def _grpc_port(_grpc_mockserver_endpoint):
87 _, port = _grpc_mockserver_endpoint.rsplit(':', 1)
88 return int(port)
89
90
91@pytest.fixture(scope='session')
92async def grpc_mockserver(_grpc_mockserver_endpoint):
93 """
94 Returns the gRPC mocking server.
95
96 Override this fixture to change the way the gRPC
97 mocking server is started by the testsuite.
98
99 @snippet samples/grpc_service/tests/conftest.py Prepare server mock
100 @ingroup userver_testsuite_fixtures
101 """
102 server = grpc.aio.server()
103 server.add_insecure_port(_grpc_mockserver_endpoint)
104 server_task = asyncio.create_task(server.start())
105
106 try:
107 yield server
108 finally:
109 await server.stop(grace=None)
110 await server.wait_for_termination()
111 await server_task
112
113
114@pytest.fixture(scope='session')
116 """
117 Creates the gRPC mock server for the provided type.
118
119 @snippet samples/grpc_service/tests/conftest.py Prepare server mock
120 @ingroup userver_testsuite_fixtures
121 """
122 return _create_servicer_mock
123
124
125def pytest_addoption(parser):
126 group = parser.getgroup('grpc-mockserver')
127 group.addoption(
128 '--grpc-mockserver-host',
129 default='[::]',
130 help='gRPC mockserver hostname, default is [::]',
131 )
132 group.addoption(
133 '--grpc-mockserver-port',
134 type=int,
135 default=0,
136 help='gRPC mockserver port, by default random port is used',
137 )
138
139
140def _create_servicer_mock(servicer_class):
141 def wrap_grpc_method(name, default_method):
142 @functools.wraps(default_method)
143 async def run_method(self, *args, **kwargs):
144 method = mock.get(name, None)
145 if method is not None:
146 return await method(*args, **kwargs)
147 else:
148 return await default_method(self, *args, **kwargs)
149
150 return run_method
151
152 methods = {}
153 for attname, value in servicer_class.__dict__.items():
154 if callable(value):
155 methods[attname] = wrap_grpc_method(attname, value)
156
157 mocked_servicer_class = type(
158 f'Mock{servicer_class.__name__}', (servicer_class,), methods,
159 )
160 servicer = mocked_servicer_class()
161 mock = GrpcServiceMock(servicer, frozenset(methods))
162 return mock