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