Skip to content

Commit 10ed5d6

Browse files
committed
chore: added test for executable.py
Signed-off-by: Manish Dait <[email protected]>
1 parent d5fbcfd commit 10ed5d6

File tree

2 files changed

+157
-3
lines changed

2 files changed

+157
-3
lines changed

src/hiero_sdk_python/executable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def set_max_attempts(self, max_attempts: int):
134134
self._max_attempts = max_attempts
135135
return self
136136

137-
def set_grpc_deadline(self, grpc_deadline: Union[int, float]) -> "_Executable":
137+
def set_grpc_deadline(self, grpc_deadline: Union[int, float]):
138138
"""
139139
Set the gRPC call deadline (per attempt).
140140
@@ -217,7 +217,7 @@ def set_min_backoff(self, min_backoff: Union[int, float]):
217217
self._min_backoff = min_backoff
218218
return self
219219

220-
def set_max_backoff(self, max_backoff: Union[int, float]) -> "Client":
220+
def set_max_backoff(self, max_backoff: Union[int, float]):
221221
"""
222222
Set the maximum backoff delay between retries.
223223

tests/unit/executable_test.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from hiero_sdk_python.account.account_id import AccountId
77
from hiero_sdk_python.crypto.private_key import PrivateKey
88
from hiero_sdk_python.exceptions import MaxAttemptsError, PrecheckError
9+
from hiero_sdk_python.executable import _is_transaction_receipt_or_record_request
910
from hiero_sdk_python.hapi.services import (
1011
basic_types_pb2,
1112
crypto_get_account_balance_pb2,
13+
query_pb2,
1214
response_header_pb2,
1315
response_pb2,
1416
transaction_get_receipt_pb2,
@@ -723,6 +725,32 @@ def test_set_request_timeout_with_invalid_value(invalid_request_timeout):
723725
query = CryptoGetAccountBalanceQuery()
724726
query.set_request_timeout(invalid_request_timeout)
725727

728+
def test_warning_when_grpc_deadline_exceeds_request_timeout():
729+
"""Warn when grpc_deadline is greater than request_timeout."""
730+
tx = AccountCreateTransaction()
731+
732+
tx.set_request_timeout(5)
733+
734+
with pytest.warns(FutureWarning):
735+
tx.set_grpc_deadline(10)
736+
737+
def test_warning_when_request_timeout_less_than_grpc_deadline():
738+
"""Warn when request_timeout is less than grpc_deadline."""
739+
tx = AccountCreateTransaction()
740+
tx.set_grpc_deadline(10)
741+
742+
with pytest.warns(FutureWarning):
743+
tx.set_request_timeout(5)
744+
745+
def test_is_transaction_receipt_or_record_request():
746+
"""Detect receipt and record query requests correctly."""
747+
receipt_query = query_pb2.Query(
748+
transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptQuery()
749+
)
750+
751+
assert _is_transaction_receipt_or_record_request(receipt_query) is True
752+
assert _is_transaction_receipt_or_record_request(object()) is False
753+
726754
# Set min_backoff
727755
def test_set_min_backoff_with_valid_param():
728756
"""Test that set_min_backoff updates default value of _min_backoff."""
@@ -860,6 +888,17 @@ def test_set_max_backoff_less_than_min_backoff():
860888

861889
query.set_max_backoff(2)
862890

891+
def test_backoff_is_capped_by_max_backoff():
892+
"""Backoff delay must not exceed max_backoff."""
893+
tx = AccountCreateTransaction()
894+
tx.set_min_backoff(2)
895+
tx.set_max_backoff(5)
896+
897+
# attempt=0 min * 2 = 4
898+
assert tx._calculate_backoff(0) == 4
899+
# attempt=1 min * 4 = 8 : capped to 5
900+
assert tx._calculate_backoff(1) == 5
901+
863902
def test_execution_config_inherits_from_client(mock_client):
864903
"""Test that resolve_execution_config inherits config from client if not set."""
865904
mock_client.max_attempts = 7
@@ -911,7 +950,7 @@ def test_set_node_account_ids_overrides_client_nodes(mock_client):
911950

912951
assert tx.node_account_ids == [node]
913952

914-
953+
# reuest timeout
915954
def test_request_timeout_exceeded_stops_execution():
916955
"""Test that execution stops when request_timeout is exceeded."""
917956
busy_response = TransactionResponseProto(
@@ -950,4 +989,119 @@ def fake_time():
950989
with pytest.raises(MaxAttemptsError):
951990
tx.execute(client)
952991

992+
993+
@pytest.mark.parametrize(
994+
'error',
995+
[
996+
RealRpcError(grpc.StatusCode.DEADLINE_EXCEEDED, "timeout"),
997+
RealRpcError(grpc.StatusCode.UNAVAILABLE, "unavailable"),
998+
RealRpcError(grpc.StatusCode.RESOURCE_EXHAUSTED, "busy")
999+
]
1000+
)
1001+
def test_should_exponential_error_mark_node_unhealty_and_advance(error):
1002+
"""Exponential gRPC retry errors advance the node without sleep-based backoff."""
1003+
ok_response = TransactionResponseProto(
1004+
nodeTransactionPrecheckCode=ResponseCode.OK
1005+
)
1006+
1007+
receipt_response = response_pb2.Response(
1008+
transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse(
1009+
header=response_header_pb2.ResponseHeader(
1010+
nodeTransactionPrecheckCode=ResponseCode.OK
1011+
),
1012+
receipt=transaction_receipt_pb2.TransactionReceipt(
1013+
status=ResponseCode.SUCCESS
1014+
)
1015+
)
1016+
)
1017+
1018+
response_sequences = [
1019+
[error],
1020+
[ok_response, receipt_response],
1021+
]
1022+
1023+
with mock_hedera_servers(response_sequences) as client, patch("hiero_sdk_python.executable.time.sleep") as mock_sleep:
1024+
tx = (
1025+
AccountCreateTransaction()
1026+
.set_key_without_alias(PrivateKey.generate().public_key())
1027+
.set_initial_balance(1)
1028+
)
1029+
1030+
receipt = tx.execute(client)
1031+
1032+
assert receipt.status == ResponseCode.SUCCESS
1033+
# No delay_for_attempt backoff call, Node is mark unhealthy and advance
1034+
assert mock_sleep.call_count == 0
1035+
# Node must have changed
1036+
assert tx._node_account_ids_index == 1
1037+
9531038

1039+
def test_rst_stream_error_marks_node_unhealthy_and_advances_without_backoff():
1040+
"""INTERNAL RST_STREAM errors trigger exponential retry by advancing the node without sleep-based backoff."""
1041+
error = RealRpcError(
1042+
grpc.StatusCode.INTERNAL,
1043+
"received rst stream"
1044+
)
1045+
1046+
ok_response = TransactionResponseProto(
1047+
nodeTransactionPrecheckCode=ResponseCode.OK
1048+
)
1049+
1050+
receipt_response = response_pb2.Response(
1051+
transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse(
1052+
header=response_header_pb2.ResponseHeader(
1053+
nodeTransactionPrecheckCode=ResponseCode.OK
1054+
),
1055+
receipt=transaction_receipt_pb2.TransactionReceipt(
1056+
status=ResponseCode.SUCCESS
1057+
)
1058+
)
1059+
)
1060+
1061+
response_sequences = [
1062+
[error],
1063+
[ok_response, receipt_response],
1064+
]
1065+
1066+
with (
1067+
mock_hedera_servers(response_sequences) as client,
1068+
patch("hiero_sdk_python.executable.time.sleep") as mock_sleep,
1069+
):
1070+
tx = (
1071+
AccountCreateTransaction()
1072+
.set_key_without_alias(PrivateKey.generate().public_key())
1073+
.set_initial_balance(1)
1074+
)
1075+
1076+
receipt = tx.execute(client)
1077+
1078+
# Retry succeeds
1079+
assert receipt.status == ResponseCode.SUCCESS
1080+
# RST_STREAM exponential retry does not use delay-based backoff
1081+
assert mock_sleep.call_count == 0
1082+
# Node must advance after marking the first node unhealthy
1083+
assert tx._node_account_ids_index == 1
1084+
1085+
1086+
@pytest.mark.parametrize(
1087+
'error',
1088+
[
1089+
RealRpcError(grpc.StatusCode.ALREADY_EXISTS, "already exists"),
1090+
RealRpcError(grpc.StatusCode.ABORTED, "aborted"),
1091+
RealRpcError(grpc.StatusCode.UNAUTHENTICATED, "unauthenticated")
1092+
]
1093+
)
1094+
def test_non_exponential_grpc_error_raises_exception(error):
1095+
"""Errors that are not retried exponentially should raise error immediately"""
1096+
response_sequences = [
1097+
[error]
1098+
]
1099+
1100+
with mock_hedera_servers(response_sequences) as client, pytest.raises(grpc.RpcError):
1101+
tx = (
1102+
AccountCreateTransaction()
1103+
.set_key_without_alias(PrivateKey.generate().public_key())
1104+
.set_initial_balance(1)
1105+
)
1106+
1107+
tx.execute(client)

0 commit comments

Comments
 (0)