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 # TODO(https://github.com/grpc/grpc/pull/15411) enable this test 232 'unit._server_test.ServerTest.test_failed_port_binding_exception', 233 ) 234 description = 'run tests with gevent. Assumes grpc/gevent are installed' 235 user_options = [] 236 237 def initialize_options(self): 238 pass 239 240 def finalize_options(self): 241 # distutils requires this override. 242 pass 243 244 def run(self): 245 from gevent import monkey 246 monkey.patch_all() 247 248 import tests 249 250 import grpc.experimental.gevent 251 grpc.experimental.gevent.init_gevent() 252 253 import gevent 254 255 import tests 256 loader = tests.Loader() 257 loader.loadTestsFromNames(['tests']) 258 runner = tests.Runner() 259 if sys.platform == 'win32': 260 runner.skip_tests(self.BANNED_TESTS + self.BANNED_WINDOWS_TESTS) 261 else: 262 runner.skip_tests(self.BANNED_TESTS) 263 result = gevent.spawn(runner.run, loader.suite) 264 result.join() 265 if not result.value.wasSuccessful(): 266 sys.exit('Test failure') 267 268 269class RunInterop(test.test): 270 271 description = 'run interop test client/server' 272 user_options = [ 273 ('args=', None, 'pass-thru arguments for the client/server'), 274 ('client', None, 'flag indicating to run the client'), 275 ('server', None, 'flag indicating to run the server'), 276 ('use-asyncio', None, 'flag indicating to run the asyncio stack') 277 ] 278 279 def initialize_options(self): 280 self.args = '' 281 self.client = False 282 self.server = False 283 self.use_asyncio = False 284 285 def finalize_options(self): 286 if self.client and self.server: 287 raise _errors.DistutilsOptionError( 288 'you may only specify one of client or server') 289 290 def run(self): 291 if self.distribution.install_requires: 292 self.distribution.fetch_build_eggs( 293 self.distribution.install_requires) 294 if self.distribution.tests_require: 295 self.distribution.fetch_build_eggs(self.distribution.tests_require) 296 if self.client: 297 self.run_client() 298 elif self.server: 299 self.run_server() 300 301 def run_server(self): 302 # We import here to ensure that our setuptools parent has had a chance to 303 # edit the Python system path. 304 if self.use_asyncio: 305 import asyncio 306 from tests_aio.interop import server 307 sys.argv[1:] = self.args.split() 308 asyncio.get_event_loop().run_until_complete(server.serve()) 309 else: 310 from tests.interop import server 311 sys.argv[1:] = self.args.split() 312 server.serve() 313 314 def run_client(self): 315 # We import here to ensure that our setuptools parent has had a chance to 316 # edit the Python system path. 317 from tests.interop import client 318 sys.argv[1:] = self.args.split() 319 client.test_interoperability() 320 321 322class RunFork(test.test): 323 324 description = 'run fork test client' 325 user_options = [('args=', 'a', 'pass-thru arguments for the client')] 326 327 def initialize_options(self): 328 self.args = '' 329 330 def finalize_options(self): 331 # distutils requires this override. 332 pass 333 334 def run(self): 335 if self.distribution.install_requires: 336 self.distribution.fetch_build_eggs( 337 self.distribution.install_requires) 338 if self.distribution.tests_require: 339 self.distribution.fetch_build_eggs(self.distribution.tests_require) 340 # We import here to ensure that our setuptools parent has had a chance to 341 # edit the Python system path. 342 from tests.fork import client 343 sys.argv[1:] = self.args.split() 344 client.test_fork() 345