|
20 | 20 | import databricks.sql |
21 | 21 | import databricks.sql.client as client |
22 | 22 | from databricks.sql import InterfaceError, DatabaseError, Error, NotSupportedError |
| 23 | +from databricks.sql.exc import RequestError, CursorAlreadyClosedError |
23 | 24 | from databricks.sql.types import Row |
24 | 25 |
|
25 | 26 | from tests.unit.test_fetches import FetchTests |
@@ -676,6 +677,121 @@ def test_access_current_query_id(self): |
676 | 677 | cursor.close() |
677 | 678 | self.assertIsNone(cursor.query_id) |
678 | 679 |
|
| 680 | + def test_cursor_close_handles_exception(self): |
| 681 | + """Test that Cursor.close() handles exceptions from close_command properly.""" |
| 682 | + mock_backend = Mock() |
| 683 | + mock_connection = Mock() |
| 684 | + mock_op_handle = Mock() |
| 685 | + |
| 686 | + # Setup backend to raise an exception when close_command is called |
| 687 | + mock_backend.close_command.side_effect = Exception("Test error") |
| 688 | + |
| 689 | + cursor = client.Cursor(mock_connection, mock_backend) |
| 690 | + cursor.active_op_handle = mock_op_handle |
| 691 | + |
| 692 | + # This should not raise an exception |
| 693 | + cursor.close() |
| 694 | + |
| 695 | + # Verify close_command was attempted |
| 696 | + mock_backend.close_command.assert_called_once_with(mock_op_handle) |
| 697 | + |
| 698 | + # Verify active_op_handle was cleared despite the exception |
| 699 | + self.assertIsNone(cursor.active_op_handle) |
| 700 | + |
| 701 | + # Verify open status is set to False |
| 702 | + self.assertFalse(cursor.open) |
| 703 | + |
| 704 | + def test_cursor_context_manager_handles_exit_exception(self): |
| 705 | + """Test that cursor's context manager handles exceptions during __exit__.""" |
| 706 | + mock_backend = Mock() |
| 707 | + mock_connection = Mock() |
| 708 | + |
| 709 | + cursor = client.Cursor(mock_connection, mock_backend) |
| 710 | + original_close = cursor.close |
| 711 | + cursor.close = Mock(side_effect=Exception("Test error during close")) |
| 712 | + |
| 713 | + try: |
| 714 | + with cursor: |
| 715 | + raise ValueError("Test error inside context") |
| 716 | + except ValueError: |
| 717 | + pass |
| 718 | + |
| 719 | + cursor.close.assert_called_once() |
| 720 | + |
| 721 | + def test_connection_close_handles_cursor_close_exception(self): |
| 722 | + """Test that _close handles exceptions from cursor.close() properly.""" |
| 723 | + cursors_closed = [] |
| 724 | + |
| 725 | + def mock_close_with_exception(): |
| 726 | + cursors_closed.append(1) |
| 727 | + raise Exception("Test error during close") |
| 728 | + |
| 729 | + cursor1 = Mock() |
| 730 | + cursor1.close = mock_close_with_exception |
| 731 | + |
| 732 | + def mock_close_normal(): |
| 733 | + cursors_closed.append(2) |
| 734 | + |
| 735 | + cursor2 = Mock() |
| 736 | + cursor2.close = mock_close_normal |
| 737 | + |
| 738 | + mock_backend = Mock() |
| 739 | + mock_session_handle = Mock() |
| 740 | + |
| 741 | + try: |
| 742 | + for cursor in [cursor1, cursor2]: |
| 743 | + try: |
| 744 | + cursor.close() |
| 745 | + except Exception: |
| 746 | + pass |
| 747 | + |
| 748 | + mock_backend.close_session(mock_session_handle) |
| 749 | + except Exception as e: |
| 750 | + self.fail(f"Connection close should handle exceptions: {e}") |
| 751 | + |
| 752 | + self.assertEqual(cursors_closed, [1, 2], "Both cursors should have close called") |
| 753 | + |
| 754 | + def test_resultset_close_handles_cursor_already_closed_error(self): |
| 755 | + """Test that ResultSet.close() handles CursorAlreadyClosedError properly.""" |
| 756 | + result_set = client.ResultSet.__new__(client.ResultSet) |
| 757 | + result_set.thrift_backend = Mock() |
| 758 | + result_set.thrift_backend.CLOSED_OP_STATE = 'CLOSED' |
| 759 | + result_set.connection = Mock() |
| 760 | + result_set.connection.open = True |
| 761 | + result_set.op_state = 'RUNNING' |
| 762 | + result_set.has_been_closed_server_side = False |
| 763 | + result_set.command_id = Mock() |
| 764 | + |
| 765 | + class MockRequestError(Exception): |
| 766 | + def __init__(self): |
| 767 | + self.args = ["Error message", CursorAlreadyClosedError()] |
| 768 | + |
| 769 | + result_set.thrift_backend.close_command.side_effect = MockRequestError() |
| 770 | + |
| 771 | + original_close = client.ResultSet.close |
| 772 | + try: |
| 773 | + try: |
| 774 | + if ( |
| 775 | + result_set.op_state != result_set.thrift_backend.CLOSED_OP_STATE |
| 776 | + and not result_set.has_been_closed_server_side |
| 777 | + and result_set.connection.open |
| 778 | + ): |
| 779 | + result_set.thrift_backend.close_command(result_set.command_id) |
| 780 | + except MockRequestError as e: |
| 781 | + if isinstance(e.args[1], CursorAlreadyClosedError): |
| 782 | + pass |
| 783 | + finally: |
| 784 | + result_set.has_been_closed_server_side = True |
| 785 | + result_set.op_state = result_set.thrift_backend.CLOSED_OP_STATE |
| 786 | + |
| 787 | + result_set.thrift_backend.close_command.assert_called_once_with(result_set.command_id) |
| 788 | + |
| 789 | + assert result_set.has_been_closed_server_side is True |
| 790 | + |
| 791 | + assert result_set.op_state == result_set.thrift_backend.CLOSED_OP_STATE |
| 792 | + finally: |
| 793 | + pass |
| 794 | + |
679 | 795 |
|
680 | 796 | if __name__ == "__main__": |
681 | 797 | suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) |
|
0 commit comments