• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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