1# Copyright 2016 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 15import abc 16import contextlib 17import importlib 18import os 19from os import path 20import pkgutil 21import platform 22import shutil 23import sys 24import tempfile 25import unittest 26 27import six 28 29import grpc 30from grpc_tools import protoc 31from tests.unit import test_common 32 33_MESSAGES_IMPORT = b'import "messages.proto";' 34_SPLIT_NAMESPACE = b'package grpc_protoc_plugin.invocation_testing.split;' 35_COMMON_NAMESPACE = b'package grpc_protoc_plugin.invocation_testing;' 36 37_RELATIVE_PROTO_PATH = 'relative_proto_path' 38_RELATIVE_PYTHON_OUT = 'relative_python_out' 39 40 41@contextlib.contextmanager 42def _system_path(path_insertion): 43 old_system_path = sys.path[:] 44 sys.path = sys.path[0:1] + path_insertion + sys.path[1:] 45 yield 46 sys.path = old_system_path 47 48 49# NOTE(nathaniel): https://twitter.com/exoplaneteer/status/677259364256747520 50# Life lesson "just always default to idempotence" reinforced. 51def _create_directory_tree(root, path_components_sequence): 52 created = set() 53 for path_components in path_components_sequence: 54 thus_far = '' 55 for path_component in path_components: 56 relative_path = path.join(thus_far, path_component) 57 if relative_path not in created: 58 os.makedirs(path.join(root, relative_path)) 59 created.add(relative_path) 60 thus_far = path.join(thus_far, path_component) 61 62 63def _massage_proto_content(proto_content, test_name_bytes, 64 messages_proto_relative_file_name_bytes): 65 package_substitution = (b'package grpc_protoc_plugin.invocation_testing.' + 66 test_name_bytes + b';') 67 common_namespace_substituted = proto_content.replace( 68 _COMMON_NAMESPACE, package_substitution) 69 split_namespace_substituted = common_namespace_substituted.replace( 70 _SPLIT_NAMESPACE, package_substitution) 71 message_import_replaced = split_namespace_substituted.replace( 72 _MESSAGES_IMPORT, 73 b'import "' + messages_proto_relative_file_name_bytes + b'";') 74 return message_import_replaced 75 76 77def _packagify(directory): 78 for subdirectory, _, _ in os.walk(directory): 79 init_file_name = path.join(subdirectory, '__init__.py') 80 with open(init_file_name, 'wb') as init_file: 81 init_file.write(b'') 82 83 84class _Servicer(object): 85 86 def __init__(self, response_class): 87 self._response_class = response_class 88 89 def Call(self, request, context): 90 return self._response_class() 91 92 93def _protoc(proto_path, python_out, grpc_python_out_flag, grpc_python_out, 94 absolute_proto_file_names): 95 args = [ 96 '', 97 '--proto_path={}'.format(proto_path), 98 ] 99 if python_out is not None: 100 args.append('--python_out={}'.format(python_out)) 101 if grpc_python_out is not None: 102 args.append('--grpc_python_out={}:{}'.format(grpc_python_out_flag, 103 grpc_python_out)) 104 args.extend(absolute_proto_file_names) 105 return protoc.main(args) 106 107 108class _Mid2016ProtocStyle(object): 109 110 def name(self): 111 return 'Mid2016ProtocStyle' 112 113 def grpc_in_pb2_expected(self): 114 return True 115 116 def protoc(self, proto_path, python_out, absolute_proto_file_names): 117 return (_protoc(proto_path, python_out, 'grpc_1_0', python_out, 118 absolute_proto_file_names),) 119 120 121class _SingleProtocExecutionProtocStyle(object): 122 123 def name(self): 124 return 'SingleProtocExecutionProtocStyle' 125 126 def grpc_in_pb2_expected(self): 127 return False 128 129 def protoc(self, proto_path, python_out, absolute_proto_file_names): 130 return (_protoc(proto_path, python_out, 'grpc_2_0', python_out, 131 absolute_proto_file_names),) 132 133 134class _ProtoBeforeGrpcProtocStyle(object): 135 136 def name(self): 137 return 'ProtoBeforeGrpcProtocStyle' 138 139 def grpc_in_pb2_expected(self): 140 return False 141 142 def protoc(self, proto_path, python_out, absolute_proto_file_names): 143 pb2_protoc_exit_code = _protoc(proto_path, python_out, None, None, 144 absolute_proto_file_names) 145 pb2_grpc_protoc_exit_code = _protoc(proto_path, None, 'grpc_2_0', 146 python_out, 147 absolute_proto_file_names) 148 return pb2_protoc_exit_code, pb2_grpc_protoc_exit_code 149 150 151class _GrpcBeforeProtoProtocStyle(object): 152 153 def name(self): 154 return 'GrpcBeforeProtoProtocStyle' 155 156 def grpc_in_pb2_expected(self): 157 return False 158 159 def protoc(self, proto_path, python_out, absolute_proto_file_names): 160 pb2_grpc_protoc_exit_code = _protoc(proto_path, None, 'grpc_2_0', 161 python_out, 162 absolute_proto_file_names) 163 pb2_protoc_exit_code = _protoc(proto_path, python_out, None, None, 164 absolute_proto_file_names) 165 return pb2_grpc_protoc_exit_code, pb2_protoc_exit_code 166 167 168_PROTOC_STYLES = ( 169 _Mid2016ProtocStyle(), 170 _SingleProtocExecutionProtocStyle(), 171 _ProtoBeforeGrpcProtocStyle(), 172 _GrpcBeforeProtoProtocStyle(), 173) 174 175 176@unittest.skipIf(platform.python_implementation() == 'PyPy', 177 'Skip test if run with PyPy!') 178class _Test(six.with_metaclass(abc.ABCMeta, unittest.TestCase)): 179 180 def setUp(self): 181 self._directory = tempfile.mkdtemp(suffix=self.NAME, dir='.') 182 self._proto_path = path.join(self._directory, _RELATIVE_PROTO_PATH) 183 self._python_out = path.join(self._directory, _RELATIVE_PYTHON_OUT) 184 185 os.makedirs(self._proto_path) 186 os.makedirs(self._python_out) 187 188 proto_directories_and_names = { 189 ( 190 self.MESSAGES_PROTO_RELATIVE_DIRECTORY_NAMES, 191 self.MESSAGES_PROTO_FILE_NAME, 192 ), 193 ( 194 self.SERVICES_PROTO_RELATIVE_DIRECTORY_NAMES, 195 self.SERVICES_PROTO_FILE_NAME, 196 ), 197 } 198 messages_proto_relative_file_name_forward_slashes = '/'.join( 199 self.MESSAGES_PROTO_RELATIVE_DIRECTORY_NAMES + 200 (self.MESSAGES_PROTO_FILE_NAME,)) 201 _create_directory_tree( 202 self._proto_path, 203 (relative_proto_directory_names for relative_proto_directory_names, 204 _ in proto_directories_and_names)) 205 self._absolute_proto_file_names = set() 206 for relative_directory_names, file_name in proto_directories_and_names: 207 absolute_proto_file_name = path.join( 208 self._proto_path, *relative_directory_names + (file_name,)) 209 raw_proto_content = pkgutil.get_data( 210 'tests.protoc_plugin.protos.invocation_testing', 211 path.join(*relative_directory_names + (file_name,))) 212 massaged_proto_content = _massage_proto_content( 213 raw_proto_content, self.NAME.encode(), 214 messages_proto_relative_file_name_forward_slashes.encode()) 215 with open(absolute_proto_file_name, 'wb') as proto_file: 216 proto_file.write(massaged_proto_content) 217 self._absolute_proto_file_names.add(absolute_proto_file_name) 218 219 def tearDown(self): 220 shutil.rmtree(self._directory) 221 222 def _protoc(self): 223 protoc_exit_codes = self.PROTOC_STYLE.protoc( 224 self._proto_path, self._python_out, self._absolute_proto_file_names) 225 for protoc_exit_code in protoc_exit_codes: 226 self.assertEqual(0, protoc_exit_code) 227 228 _packagify(self._python_out) 229 230 generated_modules = {} 231 expected_generated_full_module_names = { 232 self.EXPECTED_MESSAGES_PB2, 233 self.EXPECTED_SERVICES_PB2, 234 self.EXPECTED_SERVICES_PB2_GRPC, 235 } 236 with _system_path([self._python_out]): 237 for full_module_name in expected_generated_full_module_names: 238 module = importlib.import_module(full_module_name) 239 generated_modules[full_module_name] = module 240 241 self._messages_pb2 = generated_modules[self.EXPECTED_MESSAGES_PB2] 242 self._services_pb2 = generated_modules[self.EXPECTED_SERVICES_PB2] 243 self._services_pb2_grpc = generated_modules[ 244 self.EXPECTED_SERVICES_PB2_GRPC] 245 246 def _services_modules(self): 247 if self.PROTOC_STYLE.grpc_in_pb2_expected(): 248 return self._services_pb2, self._services_pb2_grpc 249 else: 250 return (self._services_pb2_grpc,) 251 252 def test_imported_attributes(self): 253 self._protoc() 254 255 self._messages_pb2.Request 256 self._messages_pb2.Response 257 self._services_pb2.DESCRIPTOR.services_by_name['TestService'] 258 for services_module in self._services_modules(): 259 services_module.TestServiceStub 260 services_module.TestServiceServicer 261 services_module.add_TestServiceServicer_to_server 262 263 def test_call(self): 264 self._protoc() 265 266 for services_module in self._services_modules(): 267 server = test_common.test_server() 268 services_module.add_TestServiceServicer_to_server( 269 _Servicer(self._messages_pb2.Response), server) 270 port = server.add_insecure_port('[::]:0') 271 server.start() 272 channel = grpc.insecure_channel('localhost:{}'.format(port)) 273 stub = services_module.TestServiceStub(channel) 274 response = stub.Call(self._messages_pb2.Request()) 275 self.assertEqual(self._messages_pb2.Response(), response) 276 server.stop(None) 277 278 279def _create_test_case_class(split_proto, protoc_style): 280 attributes = {} 281 282 name = '{}{}'.format('SplitProto' if split_proto else 'SameProto', 283 protoc_style.name()) 284 attributes['NAME'] = name 285 286 if split_proto: 287 attributes['MESSAGES_PROTO_RELATIVE_DIRECTORY_NAMES'] = ( 288 'split_messages', 289 'sub', 290 ) 291 attributes['MESSAGES_PROTO_FILE_NAME'] = 'messages.proto' 292 attributes['SERVICES_PROTO_RELATIVE_DIRECTORY_NAMES'] = ( 293 'split_services',) 294 attributes['SERVICES_PROTO_FILE_NAME'] = 'services.proto' 295 attributes['EXPECTED_MESSAGES_PB2'] = 'split_messages.sub.messages_pb2' 296 attributes['EXPECTED_SERVICES_PB2'] = 'split_services.services_pb2' 297 attributes['EXPECTED_SERVICES_PB2_GRPC'] = ( 298 'split_services.services_pb2_grpc') 299 else: 300 attributes['MESSAGES_PROTO_RELATIVE_DIRECTORY_NAMES'] = () 301 attributes['MESSAGES_PROTO_FILE_NAME'] = 'same.proto' 302 attributes['SERVICES_PROTO_RELATIVE_DIRECTORY_NAMES'] = () 303 attributes['SERVICES_PROTO_FILE_NAME'] = 'same.proto' 304 attributes['EXPECTED_MESSAGES_PB2'] = 'same_pb2' 305 attributes['EXPECTED_SERVICES_PB2'] = 'same_pb2' 306 attributes['EXPECTED_SERVICES_PB2_GRPC'] = 'same_pb2_grpc' 307 308 attributes['PROTOC_STYLE'] = protoc_style 309 310 attributes['__module__'] = _Test.__module__ 311 312 return type('{}Test'.format(name), (_Test,), attributes) 313 314 315def _create_test_case_classes(): 316 for split_proto in ( 317 False, 318 True, 319 ): 320 for protoc_style in _PROTOC_STYLES: 321 yield _create_test_case_class(split_proto, protoc_style) 322 323 324def load_tests(loader, tests, pattern): 325 tests = tuple( 326 loader.loadTestsFromTestCase(test_case_class) 327 for test_case_class in _create_test_case_classes()) 328 return unittest.TestSuite(tests=tests) 329 330 331if __name__ == '__main__': 332 unittest.main(verbosity=2) 333