1# Copyright 2018 The 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"""Tests of grpc_status. 15 16isort:skip_file 17""" 18 19# NOTE(lidiz) This module only exists in Bazel BUILD file, for more details 20# please refer to comments in the "bazel_namespace_package_hack" module. 21try: 22 from tests import bazel_namespace_package_hack 23 24 bazel_namespace_package_hack.sys_path_to_site_dir_hack() 25except ImportError: 26 pass 27 28import unittest 29 30import logging 31import traceback 32import sys 33 34import grpc 35from grpc_status import rpc_status 36 37from tests.unit import test_common 38 39from google.protobuf import any_pb2 40from google.rpc import code_pb2, status_pb2, error_details_pb2 41 42_STATUS_OK = "/test/StatusOK" 43_STATUS_NOT_OK = "/test/StatusNotOk" 44_ERROR_DETAILS = "/test/ErrorDetails" 45_INCONSISTENT = "/test/Inconsistent" 46_INVALID_CODE = "/test/InvalidCode" 47 48_REQUEST = b"\x00\x00\x00" 49_RESPONSE = b"\x01\x01\x01" 50 51_GRPC_DETAILS_METADATA_KEY = "grpc-status-details-bin" 52 53_STATUS_DETAILS = "This is an error detail" 54_STATUS_DETAILS_ANOTHER = "This is another error detail" 55 56 57def _ok_unary_unary(request, servicer_context): 58 return _RESPONSE 59 60 61def _not_ok_unary_unary(request, servicer_context): 62 servicer_context.abort(grpc.StatusCode.INTERNAL, _STATUS_DETAILS) 63 64 65def _error_details_unary_unary(request, servicer_context): 66 details = any_pb2.Any() 67 details.Pack( 68 error_details_pb2.DebugInfo( 69 stack_entries=traceback.format_stack(), 70 detail="Intentionally invoked", 71 ) 72 ) 73 rich_status = status_pb2.Status( 74 code=code_pb2.INTERNAL, 75 message=_STATUS_DETAILS, 76 details=[details], 77 ) 78 servicer_context.abort_with_status(rpc_status.to_status(rich_status)) 79 80 81def _inconsistent_unary_unary(request, servicer_context): 82 rich_status = status_pb2.Status( 83 code=code_pb2.INTERNAL, 84 message=_STATUS_DETAILS, 85 ) 86 servicer_context.set_code(grpc.StatusCode.NOT_FOUND) 87 servicer_context.set_details(_STATUS_DETAILS_ANOTHER) 88 # User put inconsistent status information in trailing metadata 89 servicer_context.set_trailing_metadata( 90 ((_GRPC_DETAILS_METADATA_KEY, rich_status.SerializeToString()),) 91 ) 92 93 94def _invalid_code_unary_unary(request, servicer_context): 95 rich_status = status_pb2.Status( 96 code=42, 97 message="Invalid code", 98 ) 99 servicer_context.abort_with_status(rpc_status.to_status(rich_status)) 100 101 102class _GenericHandler(grpc.GenericRpcHandler): 103 def service(self, handler_call_details): 104 if handler_call_details.method == _STATUS_OK: 105 return grpc.unary_unary_rpc_method_handler(_ok_unary_unary) 106 elif handler_call_details.method == _STATUS_NOT_OK: 107 return grpc.unary_unary_rpc_method_handler(_not_ok_unary_unary) 108 elif handler_call_details.method == _ERROR_DETAILS: 109 return grpc.unary_unary_rpc_method_handler( 110 _error_details_unary_unary 111 ) 112 elif handler_call_details.method == _INCONSISTENT: 113 return grpc.unary_unary_rpc_method_handler( 114 _inconsistent_unary_unary 115 ) 116 elif handler_call_details.method == _INVALID_CODE: 117 return grpc.unary_unary_rpc_method_handler( 118 _invalid_code_unary_unary 119 ) 120 else: 121 return None 122 123 124@unittest.skipIf( 125 sys.version_info[0] < 3, "ProtoBuf descriptor has moved on from Python2" 126) 127class StatusTest(unittest.TestCase): 128 def setUp(self): 129 self._server = test_common.test_server() 130 self._server.add_generic_rpc_handlers((_GenericHandler(),)) 131 port = self._server.add_insecure_port("[::]:0") 132 self._server.start() 133 134 self._channel = grpc.insecure_channel("localhost:%d" % port) 135 136 def tearDown(self): 137 self._server.stop(None) 138 self._channel.close() 139 140 def test_status_ok(self): 141 _, call = self._channel.unary_unary( 142 _STATUS_OK, 143 _registered_method=True, 144 ).with_call(_REQUEST) 145 146 # Succeed RPC doesn't have status 147 status = rpc_status.from_call(call) 148 self.assertIs(status, None) 149 150 def test_status_not_ok(self): 151 with self.assertRaises(grpc.RpcError) as exception_context: 152 self._channel.unary_unary( 153 _STATUS_NOT_OK, 154 _registered_method=True, 155 ).with_call(_REQUEST) 156 rpc_error = exception_context.exception 157 158 self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) 159 # Failed RPC doesn't automatically generate status 160 status = rpc_status.from_call(rpc_error) 161 self.assertIs(status, None) 162 163 def test_error_details(self): 164 with self.assertRaises(grpc.RpcError) as exception_context: 165 self._channel.unary_unary( 166 _ERROR_DETAILS, 167 _registered_method=True, 168 ).with_call(_REQUEST) 169 rpc_error = exception_context.exception 170 171 status = rpc_status.from_call(rpc_error) 172 self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL) 173 self.assertEqual(status.code, code_pb2.Code.Value("INTERNAL")) 174 175 # Check if the underlying proto message is intact 176 self.assertEqual( 177 status.details[0].Is(error_details_pb2.DebugInfo.DESCRIPTOR), True 178 ) 179 info = error_details_pb2.DebugInfo() 180 status.details[0].Unpack(info) 181 self.assertIn("_error_details_unary_unary", info.stack_entries[-1]) 182 183 def test_code_message_validation(self): 184 with self.assertRaises(grpc.RpcError) as exception_context: 185 self._channel.unary_unary( 186 _INCONSISTENT, 187 _registered_method=True, 188 ).with_call(_REQUEST) 189 rpc_error = exception_context.exception 190 self.assertEqual(rpc_error.code(), grpc.StatusCode.NOT_FOUND) 191 192 # Code/Message validation failed 193 self.assertRaises(ValueError, rpc_status.from_call, rpc_error) 194 195 def test_invalid_code(self): 196 with self.assertRaises(grpc.RpcError) as exception_context: 197 self._channel.unary_unary( 198 _INVALID_CODE, 199 _registered_method=True, 200 ).with_call(_REQUEST) 201 rpc_error = exception_context.exception 202 self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN) 203 # Invalid status code exception raised during conversion 204 self.assertIn("Invalid status code", rpc_error.details()) 205 206 207if __name__ == "__main__": 208 logging.basicConfig() 209 unittest.main(verbosity=2) 210