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 subprocess
24import sys
25import traceback
26
27import setuptools
28from setuptools.command import build_ext
29from setuptools.command import build_py
30from setuptools.command import easy_install
31from setuptools.command import install
32from setuptools.command import test
33
34PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
35GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
36GRPC_PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
37PROTO_STEM = os.path.join(PYTHON_STEM, 'src', 'proto')
38PYTHON_PROTO_TOP_LEVEL = os.path.join(PYTHON_STEM, 'src')
39
40
41class CommandError(object):
42    pass
43
44
45class GatherProto(setuptools.Command):
46
47    description = 'gather proto dependencies'
48    user_options = []
49
50    def initialize_options(self):
51        pass
52
53    def finalize_options(self):
54        pass
55
56    def run(self):
57        # TODO(atash) ensure that we're running from the repository directory when
58        # this command is used
59        try:
60            shutil.rmtree(PROTO_STEM)
61        except Exception as error:
62            # We don't care if this command fails
63            pass
64        shutil.copytree(GRPC_PROTO_STEM, PROTO_STEM)
65        for root, _, _ in os.walk(PYTHON_PROTO_TOP_LEVEL):
66            path = os.path.join(root, '__init__.py')
67            open(path, 'a').close()
68
69
70class BuildPy(build_py.build_py):
71    """Custom project build command."""
72
73    def run(self):
74        try:
75            self.run_command('build_package_protos')
76        except CommandError as error:
77            sys.stderr.write('warning: %s\n' % error.message)
78        build_py.build_py.run(self)
79
80
81class TestLite(setuptools.Command):
82    """Command to run tests without fetching or building anything."""
83
84    description = 'run tests without fetching or building anything.'
85    user_options = []
86
87    def initialize_options(self):
88        pass
89
90    def finalize_options(self):
91        # distutils requires this override.
92        pass
93
94    def run(self):
95        self._add_eggs_to_path()
96
97        import tests
98        loader = tests.Loader()
99        loader.loadTestsFromNames(['tests'])
100        runner = tests.Runner()
101        result = runner.run(loader.suite)
102        if not result.wasSuccessful():
103            sys.exit('Test failure')
104
105    def _add_eggs_to_path(self):
106        """Fetch install and test requirements"""
107        self.distribution.fetch_build_eggs(self.distribution.install_requires)
108        self.distribution.fetch_build_eggs(self.distribution.tests_require)
109
110
111class TestGevent(setuptools.Command):
112    """Command to run tests w/gevent."""
113
114    BANNED_TESTS = (
115        # These tests send a lot of RPCs and are really slow on gevent.  They will
116        # eventually succeed, but need to dig into performance issues.
117        'unit._cython._no_messages_server_completion_queue_per_call_test.Test.test_rpcs',
118        'unit._cython._no_messages_single_server_completion_queue_test.Test.test_rpcs',
119        # I have no idea why this doesn't work in gevent, but it shouldn't even be
120        # using the c-core
121        'testing._client_test.ClientTest.test_infinite_request_stream_real_time',
122        # TODO(https://github.com/grpc/grpc/issues/15743) enable this test
123        'unit._session_cache_test.SSLSessionCacheTest.testSSLSessionCacheLRU',
124        # TODO(https://github.com/grpc/grpc/issues/14789) enable this test
125        'unit._server_ssl_cert_config_test',
126        # TODO(https://github.com/grpc/grpc/issues/14901) enable this test
127        'protoc_plugin._python_plugin_test.PythonPluginTest',
128        # Beta API is unsupported for gevent
129        'protoc_plugin.beta_python_plugin_test',
130        'unit.beta._beta_features_test',
131    )
132    description = 'run tests with gevent.  Assumes grpc/gevent are installed'
133    user_options = []
134
135    def initialize_options(self):
136        pass
137
138    def finalize_options(self):
139        # distutils requires this override.
140        pass
141
142    def run(self):
143        from gevent import monkey
144        monkey.patch_all()
145
146        import tests
147
148        import grpc.experimental.gevent
149        grpc.experimental.gevent.init_gevent()
150
151        import gevent
152
153        import tests
154        loader = tests.Loader()
155        loader.loadTestsFromNames(['tests'])
156        runner = tests.Runner()
157        runner.skip_tests(self.BANNED_TESTS)
158        result = gevent.spawn(runner.run, loader.suite)
159        result.join()
160        if not result.value.wasSuccessful():
161            sys.exit('Test failure')
162
163
164class RunInterop(test.test):
165
166    description = 'run interop test client/server'
167    user_options = [('args=', 'a', 'pass-thru arguments for the client/server'),
168                    ('client', 'c', 'flag indicating to run the client'),
169                    ('server', 's', 'flag indicating to run the server')]
170
171    def initialize_options(self):
172        self.args = ''
173        self.client = False
174        self.server = False
175
176    def finalize_options(self):
177        if self.client and self.server:
178            raise _errors.DistutilsOptionError(
179                'you may only specify one of client or server')
180
181    def run(self):
182        if self.distribution.install_requires:
183            self.distribution.fetch_build_eggs(
184                self.distribution.install_requires)
185        if self.distribution.tests_require:
186            self.distribution.fetch_build_eggs(self.distribution.tests_require)
187        if self.client:
188            self.run_client()
189        elif self.server:
190            self.run_server()
191
192    def run_server(self):
193        # We import here to ensure that our setuptools parent has had a chance to
194        # edit the Python system path.
195        from tests.interop import server
196        sys.argv[1:] = self.args.split()
197        server.serve()
198
199    def run_client(self):
200        # We import here to ensure that our setuptools parent has had a chance to
201        # edit the Python system path.
202        from tests.interop import client
203        sys.argv[1:] = self.args.split()
204        client.test_interoperability()
205
206
207class RunFork(test.test):
208
209    description = 'run fork test client'
210    user_options = [('args=', 'a', 'pass-thru arguments for the client')]
211
212    def initialize_options(self):
213        self.args = ''
214
215    def finalize_options(self):
216        # distutils requires this override.
217        pass
218
219    def run(self):
220        if self.distribution.install_requires:
221            self.distribution.fetch_build_eggs(
222                self.distribution.install_requires)
223        if self.distribution.tests_require:
224            self.distribution.fetch_build_eggs(self.distribution.tests_require)
225        # We import here to ensure that our setuptools parent has had a chance to
226        # edit the Python system path.
227        from tests.fork import client
228        sys.argv[1:] = self.args.split()
229        client.test_fork()
230