• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 gRPC authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Provides distutils command classes for the gRPC Python setup process."""
15
16from distutils import errors as _errors
17import glob
18import os
19import os.path
20import platform
21import re
22import shutil
23import sys
24
25import setuptools
26from setuptools.command import build_ext
27from setuptools.command import build_py
28from setuptools.command import easy_install
29from setuptools.command import install
30from setuptools.command import test
31
32PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
33GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
34GRPC_PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
35PROTO_STEM = os.path.join(PYTHON_STEM, 'src', 'proto')
36PYTHON_PROTO_TOP_LEVEL = os.path.join(PYTHON_STEM, 'src')
37
38
39class CommandError(object):
40    pass
41
42
43class GatherProto(setuptools.Command):
44
45    description = 'gather proto dependencies'
46    user_options = []
47
48    def initialize_options(self):
49        pass
50
51    def finalize_options(self):
52        pass
53
54    def run(self):
55        # TODO(atash) ensure that we're running from the repository directory when
56        # this command is used
57        try:
58            shutil.rmtree(PROTO_STEM)
59        except Exception as error:
60            # We don't care if this command fails
61            pass
62        shutil.copytree(GRPC_PROTO_STEM, PROTO_STEM)
63        for root, _, _ in os.walk(PYTHON_PROTO_TOP_LEVEL):
64            path = os.path.join(root, '__init__.py')
65            open(path, 'a').close()
66
67
68class BuildPy(build_py.build_py):
69    """Custom project build command."""
70
71    def run(self):
72        try:
73            self.run_command('build_package_protos')
74        except CommandError as error:
75            sys.stderr.write('warning: %s\n' % error.message)
76        build_py.build_py.run(self)
77
78
79class TestLite(setuptools.Command):
80    """Command to run tests without fetching or building anything."""
81
82    description = 'run tests without fetching or building anything.'
83    user_options = []
84
85    def initialize_options(self):
86        pass
87
88    def finalize_options(self):
89        # distutils requires this override.
90        pass
91
92    def run(self):
93        self._add_eggs_to_path()
94
95        import tests
96        loader = tests.Loader()
97        loader.loadTestsFromNames(['tests'])
98        runner = tests.Runner(dedicated_threads=True)
99        result = runner.run(loader.suite)
100        if not result.wasSuccessful():
101            sys.exit('Test failure')
102
103    def _add_eggs_to_path(self):
104        """Fetch install and test requirements"""
105        self.distribution.fetch_build_eggs(self.distribution.install_requires)
106        self.distribution.fetch_build_eggs(self.distribution.tests_require)
107
108
109class TestPy3Only(setuptools.Command):
110    """Command to run tests for Python 3+ features.
111
112    This does not include asyncio tests, which are housed in a separate
113    directory.
114    """
115
116    description = 'run tests for py3+ features'
117    user_options = []
118
119    def initialize_options(self):
120        pass
121
122    def finalize_options(self):
123        pass
124
125    def run(self):
126        self._add_eggs_to_path()
127        import tests
128        loader = tests.Loader()
129        loader.loadTestsFromNames(['tests_py3_only'])
130        runner = tests.Runner()
131        result = runner.run(loader.suite)
132        if not result.wasSuccessful():
133            sys.exit('Test failure')
134
135    def _add_eggs_to_path(self):
136        self.distribution.fetch_build_eggs(self.distribution.install_requires)
137        self.distribution.fetch_build_eggs(self.distribution.tests_require)
138
139
140class TestAio(setuptools.Command):
141    """Command to run aio tests without fetching or building anything."""
142
143    description = 'run aio tests without fetching or building anything.'
144    user_options = []
145
146    def initialize_options(self):
147        pass
148
149    def finalize_options(self):
150        pass
151
152    def run(self):
153        self._add_eggs_to_path()
154
155        import tests
156        loader = tests.Loader()
157        loader.loadTestsFromNames(['tests_aio'])
158        # Even without dedicated threads, the framework will somehow spawn a
159        # new thread for tests to run upon. New thread doesn't have event loop
160        # attached by default, so initialization is needed.
161        runner = tests.Runner(dedicated_threads=False)
162        result = runner.run(loader.suite)
163        if not result.wasSuccessful():
164            sys.exit('Test failure')
165
166    def _add_eggs_to_path(self):
167        """Fetch install and test requirements"""
168        self.distribution.fetch_build_eggs(self.distribution.install_requires)
169        self.distribution.fetch_build_eggs(self.distribution.tests_require)
170
171
172class TestGevent(setuptools.Command):
173    """Command to run tests w/gevent."""
174
175    BANNED_TESTS = (
176        # Fork support is not compatible with gevent
177        'fork._fork_interop_test.ForkInteropTest',
178        # These tests send a lot of RPCs and are really slow on gevent.  They will
179        # eventually succeed, but need to dig into performance issues.
180        'unit._cython._no_messages_server_completion_queue_per_call_test.Test.test_rpcs',
181        'unit._cython._no_messages_single_server_completion_queue_test.Test.test_rpcs',
182        'unit._compression_test',
183        # TODO(https://github.com/grpc/grpc/issues/16890) enable this test
184        'unit._cython._channel_test.ChannelTest.test_multiple_channels_lonely_connectivity',
185        # I have no idea why this doesn't work in gevent, but it shouldn't even be
186        # using the c-core
187        'testing._client_test.ClientTest.test_infinite_request_stream_real_time',
188        # TODO(https://github.com/grpc/grpc/issues/15743) enable this test
189        'unit._session_cache_test.SSLSessionCacheTest.testSSLSessionCacheLRU',
190        # TODO(https://github.com/grpc/grpc/issues/14789) enable this test
191        'unit._server_ssl_cert_config_test',
192        # TODO(https://github.com/grpc/grpc/issues/14901) enable this test
193        'protoc_plugin._python_plugin_test.PythonPluginTest',
194        'protoc_plugin._python_plugin_test.SimpleStubsPluginTest',
195        # Beta API is unsupported for gevent
196        'protoc_plugin.beta_python_plugin_test',
197        'unit.beta._beta_features_test',
198        # TODO(https://github.com/grpc/grpc/issues/15411) unpin gevent version
199        # This test will stuck while running higher version of gevent
200        'unit._auth_context_test.AuthContextTest.testSessionResumption',
201        # TODO(https://github.com/grpc/grpc/issues/15411) enable these tests
202        'unit._channel_ready_future_test.ChannelReadyFutureTest.test_immediately_connectable_channel_connectivity',
203        "unit._cython._channel_test.ChannelTest.test_single_channel_lonely_connectivity",
204        'unit._exit_test.ExitTest.test_in_flight_unary_unary_call',
205        'unit._exit_test.ExitTest.test_in_flight_unary_stream_call',
206        'unit._exit_test.ExitTest.test_in_flight_stream_unary_call',
207        'unit._exit_test.ExitTest.test_in_flight_stream_stream_call',
208        'unit._exit_test.ExitTest.test_in_flight_partial_unary_stream_call',
209        'unit._exit_test.ExitTest.test_in_flight_partial_stream_unary_call',
210        'unit._exit_test.ExitTest.test_in_flight_partial_stream_stream_call',
211        # TODO(https://github.com/grpc/grpc/issues/18980): Reenable.
212        'unit._signal_handling_test.SignalHandlingTest',
213        'unit._metadata_flags_test',
214        'health_check._health_servicer_test.HealthServicerTest.test_cancelled_watch_removed_from_watch_list',
215        # TODO(https://github.com/grpc/grpc/issues/17330) enable these three tests
216        'channelz._channelz_servicer_test.ChannelzServicerTest.test_many_subchannels',
217        'channelz._channelz_servicer_test.ChannelzServicerTest.test_many_subchannels_and_sockets',
218        'channelz._channelz_servicer_test.ChannelzServicerTest.test_streaming_rpc',
219        # TODO(https://github.com/grpc/grpc/issues/15411) enable this test
220        'unit._cython._channel_test.ChannelTest.test_negative_deadline_connectivity',
221        # TODO(https://github.com/grpc/grpc/issues/15411) enable this test
222        'unit._local_credentials_test.LocalCredentialsTest',
223        # TODO(https://github.com/grpc/grpc/issues/22020) LocalCredentials
224        # aren't supported with custom io managers.
225        'unit._contextvars_propagation_test',
226        'testing._time_test.StrictRealTimeTest',
227    )
228    BANNED_WINDOWS_TESTS = (
229        # TODO(https://github.com/grpc/grpc/pull/15411) enable this test
230        'unit._dns_resolver_test.DNSResolverTest.test_connect_loopback',)
231    description = 'run tests with gevent.  Assumes grpc/gevent are installed'
232    user_options = []
233
234    def initialize_options(self):
235        pass
236
237    def finalize_options(self):
238        # distutils requires this override.
239        pass
240
241    def run(self):
242        from gevent import monkey
243        monkey.patch_all()
244
245        import tests
246
247        import grpc.experimental.gevent
248        grpc.experimental.gevent.init_gevent()
249
250        import gevent
251
252        import tests
253        loader = tests.Loader()
254        loader.loadTestsFromNames(['tests'])
255        runner = tests.Runner()
256        if sys.platform == 'win32':
257            runner.skip_tests(self.BANNED_TESTS + self.BANNED_WINDOWS_TESTS)
258        else:
259            runner.skip_tests(self.BANNED_TESTS)
260        result = gevent.spawn(runner.run, loader.suite)
261        result.join()
262        if not result.value.wasSuccessful():
263            sys.exit('Test failure')
264
265
266class RunInterop(test.test):
267
268    description = 'run interop test client/server'
269    user_options = [
270        ('args=', None, 'pass-thru arguments for the client/server'),
271        ('client', None, 'flag indicating to run the client'),
272        ('server', None, 'flag indicating to run the server'),
273        ('use-asyncio', None, 'flag indicating to run the asyncio stack')
274    ]
275
276    def initialize_options(self):
277        self.args = ''
278        self.client = False
279        self.server = False
280        self.use_asyncio = False
281
282    def finalize_options(self):
283        if self.client and self.server:
284            raise _errors.DistutilsOptionError(
285                'you may only specify one of client or server')
286
287    def run(self):
288        if self.distribution.install_requires:
289            self.distribution.fetch_build_eggs(
290                self.distribution.install_requires)
291        if self.distribution.tests_require:
292            self.distribution.fetch_build_eggs(self.distribution.tests_require)
293        if self.client:
294            self.run_client()
295        elif self.server:
296            self.run_server()
297
298    def run_server(self):
299        # We import here to ensure that our setuptools parent has had a chance to
300        # edit the Python system path.
301        if self.use_asyncio:
302            import asyncio
303            from tests_aio.interop import server
304            sys.argv[1:] = self.args.split()
305            asyncio.get_event_loop().run_until_complete(server.serve())
306        else:
307            from tests.interop import server
308            sys.argv[1:] = self.args.split()
309            server.serve()
310
311    def run_client(self):
312        # We import here to ensure that our setuptools parent has had a chance to
313        # edit the Python system path.
314        from tests.interop import client
315        sys.argv[1:] = self.args.split()
316        client.test_interoperability()
317
318
319class RunFork(test.test):
320
321    description = 'run fork test client'
322    user_options = [('args=', 'a', 'pass-thru arguments for the client')]
323
324    def initialize_options(self):
325        self.args = ''
326
327    def finalize_options(self):
328        # distutils requires this override.
329        pass
330
331    def run(self):
332        if self.distribution.install_requires:
333            self.distribution.fetch_build_eggs(
334                self.distribution.install_requires)
335        if self.distribution.tests_require:
336            self.distribution.fetch_build_eggs(self.distribution.tests_require)
337        # We import here to ensure that our setuptools parent has had a chance to
338        # edit the Python system path.
339        from tests.fork import client
340        sys.argv[1:] = self.args.split()
341        client.test_fork()
342