Skip to content

Commit fb46558

Browse files
committed
Add tests for the new exceptions
Signed-off-by: Leandro Lucarella <[email protected]>
1 parent c9f783c commit fb46558

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed

tests/test_exception.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for the microgrid client exceptions."""
5+
6+
import re
7+
from typing import Protocol
8+
from unittest import mock
9+
10+
import grpclib
11+
import pytest
12+
13+
from frequenz.client.microgrid import (
14+
ClientError,
15+
DataLoss,
16+
EntityAlreadyExists,
17+
EntityNotFound,
18+
GrpcStatusError,
19+
InternalError,
20+
InvalidArgument,
21+
OperationAborted,
22+
OperationCancelled,
23+
OperationNotImplemented,
24+
OperationOutOfRange,
25+
OperationPreconditionFailed,
26+
OperationTimedOut,
27+
OperationUnauthenticated,
28+
PermissionDenied,
29+
ResourceExhausted,
30+
ServiceUnavailable,
31+
UnknownError,
32+
)
33+
34+
35+
class _GrpcStatusErrorCtor(Protocol):
36+
"""A protocol for the constructor of a subclass of `GrpcStatusError`."""
37+
38+
def __call__(
39+
self, *, server_url: str, operation: str, grpc_error: grpclib.GRPCError
40+
) -> GrpcStatusError: ...
41+
42+
43+
ERROR_TUPLES: list[tuple[type[GrpcStatusError], grpclib.Status, str, bool]] = [
44+
(
45+
OperationCancelled,
46+
grpclib.Status.CANCELLED,
47+
"The operation was cancelled",
48+
True,
49+
),
50+
(
51+
UnknownError,
52+
grpclib.Status.UNKNOWN,
53+
"There was an error that can't be described using other statuses",
54+
True,
55+
),
56+
(
57+
InvalidArgument,
58+
grpclib.Status.INVALID_ARGUMENT,
59+
"The client specified an invalid argument",
60+
False,
61+
),
62+
(
63+
OperationTimedOut,
64+
grpclib.Status.DEADLINE_EXCEEDED,
65+
"The time limit was exceeded while waiting for the operation to complete",
66+
True,
67+
),
68+
(
69+
EntityNotFound,
70+
grpclib.Status.NOT_FOUND,
71+
"The requested entity was not found",
72+
True,
73+
),
74+
(
75+
EntityAlreadyExists,
76+
grpclib.Status.ALREADY_EXISTS,
77+
"The entity that we attempted to create already exists",
78+
True,
79+
),
80+
(
81+
PermissionDenied,
82+
grpclib.Status.PERMISSION_DENIED,
83+
"The caller does not have permission to execute the specified operation",
84+
True,
85+
),
86+
(
87+
ResourceExhausted,
88+
grpclib.Status.RESOURCE_EXHAUSTED,
89+
"Some resource has been exhausted (for example per-user quota, disk space, etc.)",
90+
True,
91+
),
92+
(
93+
OperationPreconditionFailed,
94+
grpclib.Status.FAILED_PRECONDITION,
95+
"The operation was rejected because the system is not in a required state",
96+
True,
97+
),
98+
(OperationAborted, grpclib.Status.ABORTED, "The operation was aborted", True),
99+
(
100+
OperationOutOfRange,
101+
grpclib.Status.OUT_OF_RANGE,
102+
"The operation was attempted past the valid range",
103+
True,
104+
),
105+
(
106+
OperationNotImplemented,
107+
grpclib.Status.UNIMPLEMENTED,
108+
"The operation is not implemented or not supported/enabled in this service",
109+
False,
110+
),
111+
(
112+
InternalError,
113+
grpclib.Status.INTERNAL,
114+
"Some invariants expected by the underlying system have been broken",
115+
True,
116+
),
117+
(
118+
ServiceUnavailable,
119+
grpclib.Status.UNAVAILABLE,
120+
"The service is currently unavailable",
121+
True,
122+
),
123+
(
124+
DataLoss,
125+
grpclib.Status.DATA_LOSS,
126+
"Unrecoverable data loss or corruption",
127+
False,
128+
),
129+
(
130+
OperationUnauthenticated,
131+
grpclib.Status.UNAUTHENTICATED,
132+
"The request does not have valid authentication credentials for the operation",
133+
False,
134+
),
135+
]
136+
137+
138+
@pytest.mark.parametrize(
139+
"exception_class, grpc_status, expected_description, retryable", ERROR_TUPLES
140+
)
141+
def test_grpc_status_error(
142+
exception_class: _GrpcStatusErrorCtor,
143+
grpc_status: grpclib.Status,
144+
expected_description: str,
145+
retryable: bool,
146+
) -> None:
147+
"""Test gRPC status errors are correctly created from gRPC errors."""
148+
grpc_error = grpclib.GRPCError(
149+
grpc_status, "grpc error message", "grpc error details"
150+
)
151+
exception = exception_class(
152+
server_url="http://testserver",
153+
operation="test_operation",
154+
grpc_error=grpc_error,
155+
)
156+
157+
assert exception.server_url == "http://testserver"
158+
assert exception.operation == "test_operation"
159+
assert expected_description == exception.description
160+
assert exception.grpc_error == grpc_error
161+
assert exception.is_retryable == retryable
162+
163+
164+
def test_grpc_unknown_status_error() -> None:
165+
"""Test that an UnknownError is created for an unknown gRPC status."""
166+
expected_description = "Test error"
167+
grpc_error = grpclib.GRPCError(
168+
mock.MagicMock(name="unknown_status"),
169+
"grpc error message",
170+
"grpc error details",
171+
)
172+
exception = GrpcStatusError(
173+
server_url="http://testserver",
174+
operation="test_operation",
175+
description=expected_description,
176+
grpc_error=grpc_error,
177+
retryable=True,
178+
)
179+
180+
assert exception.server_url == "http://testserver"
181+
assert exception.operation == "test_operation"
182+
assert expected_description in exception.description
183+
assert exception.grpc_error == grpc_error
184+
assert exception.is_retryable is True
185+
186+
187+
def test_client_error() -> None:
188+
"""Test the ClientError class."""
189+
error = ClientError(
190+
server_url="http://testserver",
191+
operation="test_operation",
192+
description="An error occurred",
193+
retryable=True,
194+
)
195+
196+
assert error.server_url == "http://testserver"
197+
assert error.operation == "test_operation"
198+
assert error.description == "An error occurred"
199+
assert error.is_retryable is True
200+
201+
202+
@pytest.mark.parametrize(
203+
"exception_class, grpc_status, expected_description, retryable",
204+
ERROR_TUPLES
205+
+ [
206+
(
207+
GrpcStatusError,
208+
mock.MagicMock(name="unknown_status"),
209+
"Got an unrecognized status code",
210+
True,
211+
)
212+
],
213+
)
214+
def test_from_grpc_error(
215+
exception_class: type[GrpcStatusError],
216+
grpc_status: grpclib.Status,
217+
expected_description: str,
218+
retryable: bool,
219+
) -> None:
220+
"""Test that the from_grpc_error method creates the correct exception."""
221+
grpc_error = grpclib.GRPCError(
222+
grpc_status, "grpc error message", "grpc error details"
223+
)
224+
with pytest.raises(
225+
exception_class,
226+
match=r"Failed calling 'test_operation' on 'http://testserver': "
227+
rf"{re.escape(expected_description)} "
228+
rf"<status={re.escape(str(grpc_status.name))}>: "
229+
r"grpc error message \(grpc error details\)",
230+
) as exc_info:
231+
raise ClientError.from_grpc_error(
232+
server_url="http://testserver",
233+
operation="test_operation",
234+
grpc_error=grpc_error,
235+
)
236+
237+
exception = exc_info.value
238+
assert isinstance(exception, exception_class)
239+
assert exception.server_url == "http://testserver"
240+
assert exception.operation == "test_operation"
241+
assert exception.grpc_error == grpc_error
242+
assert expected_description == exception.description
243+
assert exception.is_retryable == retryable

0 commit comments

Comments
 (0)