From 91e8a02954a22bb82216a262f608e0c11f3d5140 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Fri, 5 Sep 2025 17:05:32 +0100 Subject: [PATCH 1/4] feat: handle heartbeat responses in Notecard transaction --- notecard/notecard.py | 10 +++++++ test/test_notecard.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/notecard/notecard.py b/notecard/notecard.py index fabd7d1..b607d6b 100644 --- a/notecard/notecard.py +++ b/notecard/notecard.py @@ -361,6 +361,16 @@ def Transaction(self, req, lock=True): error = True break + elif '{heartbeat}' in rsp_json['err']: + if self._debug: + try: + print('Response has heartbeat field indicating ' + \ + f'heartbeat: {rsp_json["status"]}') + except Exception as e: + print(e) + + error = False + continue error = False break diff --git a/test/test_notecard.py b/test/test_notecard.py index 08c1d06..e5d8c5d 100644 --- a/test/test_notecard.py +++ b/test/test_notecard.py @@ -317,6 +317,70 @@ def test_transaction_does_not_retry_on_bad_bin_error_in_response( assert card._transact.call_count == 1 + @pytest.mark.parametrize('num_heartbeats,debug_enabled', [ + (1, False), + (4, False), + (1, True), + (4, True), + (notecard.CARD_TRANSACTION_RETRIES + 1, False), + (notecard.CARD_TRANSACTION_RETRIES + 1, True), + ]) + def test_transaction_continues_after_heartbeat_to_get_valid_response( + self, arrange_transaction_test, num_heartbeats, debug_enabled): + card = arrange_transaction_test() + card._debug = debug_enabled + req = {"req": "note.add"} + + # num_heartbeats of heartbeat responses followed by valid response + heartbeat_response = b'{"err":"{heartbeat}"}\r\n' + valid_response = b'{"total":42}\r\n' + card._transact.side_effect = [heartbeat_response] * num_heartbeats + [valid_response] + + json_responses = [{'err': '{heartbeat}'}] * num_heartbeats + [{'total': 42}] + + with patch('notecard.notecard.json.loads') as mock_loads: + mock_loads.side_effect = json_responses + if debug_enabled: + with patch('builtins.print') as mock_print: + result = card.Transaction(req) + # Verify debug messages were printed for each heartbeat + assert mock_print.call_count >= num_heartbeats + else: + result = card.Transaction(req) + + assert card._transact.call_count == num_heartbeats + 1 + assert result == {'total': 42} + + def test_transaction_debug_heartbeat_with_and_without_status( + self, arrange_transaction_test): + card = arrange_transaction_test() + card._debug = True + req = {"req": "note.add"} + + # First heartbeat has a status field, second doesn't, then valid response + heartbeat_with_status = b'{"err":"{heartbeat}","status":"testing stsafe"}\r\n' + heartbeat_without_status = b'{"err":"{heartbeat}"}\r\n' + valid_response = b'{"total":42}\r\n' + card._transact.side_effect = [heartbeat_with_status, heartbeat_without_status, valid_response] + + json_responses = [ + {'err': '{heartbeat}', 'status': 'testing stsafe'}, + {'err': '{heartbeat}'}, + {'total': 42} + ] + + with patch('notecard.notecard.json.loads') as mock_loads: + mock_loads.side_effect = json_responses + with patch('builtins.print') as mock_print: + result = card.Transaction(req) + + # Verify the status message was printed for first heartbeat + mock_print.assert_any_call('Response has heartbeat field indicating heartbeat: testing stsafe') + # Verify the exception was printed for second heartbeat (KeyError: 'status') + printed_calls = [str(call) for call in mock_print.call_args_list] + assert any("KeyError" in call and "'status'" in call for call in printed_calls) + assert result == {'total': 42} + @pytest.mark.parametrize( 'rsp_expected,return_type', [ From 7b2a4b408d241fc65b48752e4abdec69115515cc Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Fri, 5 Sep 2025 17:07:09 +0100 Subject: [PATCH 2/4] fix: flake8 --- notecard/notecard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notecard/notecard.py b/notecard/notecard.py index b607d6b..826367e 100644 --- a/notecard/notecard.py +++ b/notecard/notecard.py @@ -365,7 +365,7 @@ def Transaction(self, req, lock=True): if self._debug: try: print('Response has heartbeat field indicating ' + \ - f'heartbeat: {rsp_json["status"]}') + f'heartbeat: {rsp_json["status"]}') except Exception as e: print(e) From ba2b68aab826a8ba4bd72ca067c502e99704ff9e Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Fri, 5 Sep 2025 18:44:09 +0100 Subject: [PATCH 3/4] refactor: update debug message format in Notecard class --- notecard/notecard.py | 7 +++---- test/test_notecard.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/notecard/notecard.py b/notecard/notecard.py index 826367e..a79e379 100644 --- a/notecard/notecard.py +++ b/notecard/notecard.py @@ -364,10 +364,9 @@ def Transaction(self, req, lock=True): elif '{heartbeat}' in rsp_json['err']: if self._debug: try: - print('Response has heartbeat field indicating ' + \ - f'heartbeat: {rsp_json["status"]}') - except Exception as e: - print(e) + print(f'[DEBUG] {rsp_json["status"]}') + except: + pass error = False continue diff --git a/test/test_notecard.py b/test/test_notecard.py index e5d8c5d..f76e4f6 100644 --- a/test/test_notecard.py +++ b/test/test_notecard.py @@ -332,12 +332,12 @@ def test_transaction_continues_after_heartbeat_to_get_valid_response( req = {"req": "note.add"} # num_heartbeats of heartbeat responses followed by valid response - heartbeat_response = b'{"err":"{heartbeat}"}\r\n' + heartbeat_response = b'{"err":"{heartbeat}","status":"testing"}\r\n' + json_responses = [{'err': '{heartbeat}', 'status': 'testing'}] * num_heartbeats + [{'total': 42}] + valid_response = b'{"total":42}\r\n' card._transact.side_effect = [heartbeat_response] * num_heartbeats + [valid_response] - json_responses = [{'err': '{heartbeat}'}] * num_heartbeats + [{'total': 42}] - with patch('notecard.notecard.json.loads') as mock_loads: mock_loads.side_effect = json_responses if debug_enabled: @@ -374,11 +374,12 @@ def test_transaction_debug_heartbeat_with_and_without_status( with patch('builtins.print') as mock_print: result = card.Transaction(req) - # Verify the status message was printed for first heartbeat - mock_print.assert_any_call('Response has heartbeat field indicating heartbeat: testing stsafe') - # Verify the exception was printed for second heartbeat (KeyError: 'status') - printed_calls = [str(call) for call in mock_print.call_args_list] - assert any("KeyError" in call and "'status'" in call for call in printed_calls) + # Verify the debug message was printed for first heartbeat (has status) + mock_print.assert_any_call('[DEBUG] testing stsafe') + # For second heartbeat (no status), exception is silently ignored (pass) + # So we should only see one debug print call + debug_calls = [call for call in mock_print.call_args_list if '[DEBUG]' in str(call)] + assert len(debug_calls) == 1 assert result == {'total': 42} @pytest.mark.parametrize( From 96db8bdde8a2714a58b285bac834f38495e1b55b Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Fri, 5 Sep 2025 18:45:01 +0100 Subject: [PATCH 4/4] fix: organised tests --- test/test_notecard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_notecard.py b/test/test_notecard.py index f76e4f6..03ec774 100644 --- a/test/test_notecard.py +++ b/test/test_notecard.py @@ -320,9 +320,9 @@ def test_transaction_does_not_retry_on_bad_bin_error_in_response( @pytest.mark.parametrize('num_heartbeats,debug_enabled', [ (1, False), (4, False), + (notecard.CARD_TRANSACTION_RETRIES + 1, False), (1, True), (4, True), - (notecard.CARD_TRANSACTION_RETRIES + 1, False), (notecard.CARD_TRANSACTION_RETRIES + 1, True), ]) def test_transaction_continues_after_heartbeat_to_get_valid_response(