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