|
8 | 8 |
|
9 | 9 | from snowflake.connector._utils import ( |
10 | 10 | _CoreLoader, |
| 11 | + _NanoarrowLoader, |
11 | 12 | _TrackedQueryCancellationTimer, |
12 | 13 | build_minicore_usage_for_session, |
13 | 14 | build_minicore_usage_for_telemetry, |
| 15 | + build_nanoarrow_usage_for_telemetry, |
14 | 16 | ) |
15 | 17 |
|
16 | 18 | pytestmark = pytest.mark.skipolddriver |
@@ -747,3 +749,205 @@ def test_build_minicore_usage_for_telemetry_with_load_error(self): |
747 | 749 | assert result["CORE_VERSION"] is None |
748 | 750 | assert result["CORE_FILE_NAME"] is None |
749 | 751 | assert result["CORE_LOAD_ERROR"] == "Library not found" |
| 752 | + |
| 753 | + |
| 754 | +class TestNanoarrowLoader: |
| 755 | + """Tests for the NanoarrowLoader class.""" |
| 756 | + |
| 757 | + def test_nanoarrow_loader_initialization(self): |
| 758 | + """Test that NanoarrowLoader initializes with None error.""" |
| 759 | + loader = _NanoarrowLoader() |
| 760 | + assert loader._error is None |
| 761 | + |
| 762 | + def test_set_load_error(self): |
| 763 | + """Test that set_load_error stores the error.""" |
| 764 | + loader = _NanoarrowLoader() |
| 765 | + test_error = Exception("Test error") |
| 766 | + loader.set_load_error(test_error) |
| 767 | + assert loader._error is test_error |
| 768 | + |
| 769 | + def test_get_load_error_with_error(self): |
| 770 | + """Test get_load_error returns error message when error exists.""" |
| 771 | + loader = _NanoarrowLoader() |
| 772 | + test_error = Exception("Test error message") |
| 773 | + loader._error = test_error |
| 774 | + |
| 775 | + result = loader.get_load_error() |
| 776 | + |
| 777 | + assert result == "Test error message" |
| 778 | + |
| 779 | + |
| 780 | +class TestBuildNanoarrowUsageForTelemetry: |
| 781 | + """Tests for build_nanoarrow_usage_for_telemetry function.""" |
| 782 | + |
| 783 | + def test_build_nanoarrow_usage_for_telemetry_returns_expected_keys(self): |
| 784 | + """Test that build_nanoarrow_usage_for_telemetry returns dict with expected keys.""" |
| 785 | + result = build_nanoarrow_usage_for_telemetry() |
| 786 | + |
| 787 | + assert isinstance(result, dict) |
| 788 | + assert "OS" in result |
| 789 | + assert "OS_VERSION" in result |
| 790 | + assert "NANOARROW_LOAD_ERROR" in result |
| 791 | + assert "ISA" in result |
| 792 | + |
| 793 | + def test_build_nanoarrow_usage_for_telemetry_with_mocked_error(self): |
| 794 | + """Test build_nanoarrow_usage_for_telemetry with mocked nanoarrow loader error.""" |
| 795 | + with mock.patch( |
| 796 | + "snowflake.connector._utils._nanoarrow_loader.get_load_error", |
| 797 | + return_value="Nanoarrow load failed", |
| 798 | + ): |
| 799 | + result = build_nanoarrow_usage_for_telemetry() |
| 800 | + |
| 801 | + assert result["NANOARROW_LOAD_ERROR"] == "Nanoarrow load failed" |
| 802 | + |
| 803 | + |
| 804 | +class TestNanoarrowImportErrorInCursor: |
| 805 | + """Tests for nanoarrow import error handling in cursor.py.""" |
| 806 | + |
| 807 | + def test_import_error_populates_nanoarrow_loader_error(self): |
| 808 | + """Test that ImportError during nanoarrow import in cursor.py populates _nanoarrow_loader error field.""" |
| 809 | + import importlib |
| 810 | + import sys |
| 811 | + |
| 812 | + # Save the original _nanoarrow_loader state and modules |
| 813 | + from snowflake.connector._utils import _nanoarrow_loader |
| 814 | + |
| 815 | + original_error = _nanoarrow_loader._error |
| 816 | + |
| 817 | + # Save cursor-related modules for restoration |
| 818 | + modules_to_remove = [ |
| 819 | + key |
| 820 | + for key in list(sys.modules.keys()) |
| 821 | + if "snowflake.connector.cursor" in key |
| 822 | + ] |
| 823 | + saved_cursor_modules = {key: sys.modules.pop(key) for key in modules_to_remove} |
| 824 | + |
| 825 | + # Save nanoarrow_arrow_iterator module if present |
| 826 | + nanoarrow_key = "snowflake.connector.nanoarrow_arrow_iterator" |
| 827 | + saved_nanoarrow = sys.modules.pop(nanoarrow_key, None) |
| 828 | + |
| 829 | + try: |
| 830 | + # Create a mock module that raises ImportError when accessed |
| 831 | + test_import_error = ImportError( |
| 832 | + "No module named 'nanoarrow_cpp': DLL load failed" |
| 833 | + ) |
| 834 | + |
| 835 | + # Inject a module that raises ImportError on import |
| 836 | + class FailingModule: |
| 837 | + def __getattr__(self, name): |
| 838 | + raise test_import_error |
| 839 | + |
| 840 | + # This makes import fail when trying to access anything from the module |
| 841 | + sys.modules[nanoarrow_key] = FailingModule() |
| 842 | + |
| 843 | + # Reset the nanoarrow loader error before test |
| 844 | + _nanoarrow_loader._error = None |
| 845 | + |
| 846 | + # Patch the set_load_error to track if it was called |
| 847 | + original_set_load_error = _nanoarrow_loader.set_load_error |
| 848 | + set_load_error_called_with = [] |
| 849 | + |
| 850 | + def tracking_set_load_error(err): |
| 851 | + set_load_error_called_with.append(err) |
| 852 | + original_set_load_error(err) |
| 853 | + |
| 854 | + _nanoarrow_loader.set_load_error = tracking_set_load_error |
| 855 | + |
| 856 | + try: |
| 857 | + # Force reimport of cursor module - this should trigger the ImportError handling |
| 858 | + importlib.import_module("snowflake.connector.cursor") |
| 859 | + except Exception: |
| 860 | + pass # Import may fail, but the error handler should still be called |
| 861 | + |
| 862 | + # Verify that set_load_error was called with an ImportError |
| 863 | + # or that the error was set directly |
| 864 | + if set_load_error_called_with: |
| 865 | + assert any( |
| 866 | + isinstance(err, (ImportError, AttributeError)) |
| 867 | + for err in set_load_error_called_with |
| 868 | + ) |
| 869 | + # Alternatively, check if the error was set |
| 870 | + elif _nanoarrow_loader._error is not None: |
| 871 | + assert isinstance( |
| 872 | + _nanoarrow_loader._error, (ImportError, AttributeError, Exception) |
| 873 | + ) |
| 874 | + |
| 875 | + finally: |
| 876 | + # Restore the set_load_error method |
| 877 | + _nanoarrow_loader.set_load_error = original_set_load_error |
| 878 | + |
| 879 | + # Restore original error state |
| 880 | + _nanoarrow_loader._error = original_error |
| 881 | + |
| 882 | + # Restore modules |
| 883 | + if saved_nanoarrow is not None: |
| 884 | + sys.modules[nanoarrow_key] = saved_nanoarrow |
| 885 | + elif nanoarrow_key in sys.modules: |
| 886 | + del sys.modules[nanoarrow_key] |
| 887 | + |
| 888 | + sys.modules.update(saved_cursor_modules) |
| 889 | + |
| 890 | + def test_nanoarrow_loader_set_load_error_simulates_cursor_behavior(self): |
| 891 | + """Test that NanoarrowLoader.set_load_error correctly stores ImportError as cursor.py does.""" |
| 892 | + # This test simulates exactly what cursor.py does on import failure: |
| 893 | + # try: |
| 894 | + # from .nanoarrow_arrow_iterator import PyArrowIterator |
| 895 | + # CAN_USE_ARROW_RESULT_FORMAT = True |
| 896 | + # except ImportError as e: |
| 897 | + # _nanoarrow_loader.set_load_error(e) |
| 898 | + # CAN_USE_ARROW_RESULT_FORMAT = False |
| 899 | + |
| 900 | + loader = _NanoarrowLoader() |
| 901 | + |
| 902 | + # Simulate the ImportError that occurs when nanoarrow_arrow_iterator fails to import |
| 903 | + simulated_import_error = ImportError( |
| 904 | + "No module named 'nanoarrow_cpp': cannot import name 'ArrowResult'" |
| 905 | + ) |
| 906 | + |
| 907 | + # This is the exact call made in cursor.py |
| 908 | + loader.set_load_error(simulated_import_error) |
| 909 | + |
| 910 | + # Verify the error was stored |
| 911 | + assert loader._error is simulated_import_error |
| 912 | + assert "No module named 'nanoarrow_cpp'" in loader.get_load_error() |
| 913 | + assert "ArrowResult" in loader.get_load_error() |
| 914 | + |
| 915 | + def test_nanoarrow_import_error_accessible_via_telemetry_function(self): |
| 916 | + """Test that import error from cursor.py is accessible via build_nanoarrow_usage_for_telemetry.""" |
| 917 | + from snowflake.connector._utils import _nanoarrow_loader |
| 918 | + |
| 919 | + # Save original state |
| 920 | + original_error = _nanoarrow_loader._error |
| 921 | + |
| 922 | + try: |
| 923 | + # Simulate the error that would be set during cursor.py import failure |
| 924 | + test_error = ImportError( |
| 925 | + "Failed to import ArrowResult: nanoarrow_cpp not found" |
| 926 | + ) |
| 927 | + _nanoarrow_loader.set_load_error(test_error) |
| 928 | + |
| 929 | + # Call the telemetry function and verify the error is reported |
| 930 | + result = build_nanoarrow_usage_for_telemetry() |
| 931 | + |
| 932 | + assert "Failed to import ArrowResult" in result["NANOARROW_LOAD_ERROR"] |
| 933 | + assert "nanoarrow_cpp not found" in result["NANOARROW_LOAD_ERROR"] |
| 934 | + |
| 935 | + finally: |
| 936 | + # Restore original error state |
| 937 | + _nanoarrow_loader._error = original_error |
| 938 | + |
| 939 | + def test_dll_load_failure_error_captured_correctly(self): |
| 940 | + """Test that DLL load failure errors during nanoarrow import are captured.""" |
| 941 | + loader = _NanoarrowLoader() |
| 942 | + |
| 943 | + # This simulates a common error on Windows when DLL dependencies are missing |
| 944 | + dll_error = ImportError( |
| 945 | + "DLL load failed while importing 'nanoarrow_cpp': " |
| 946 | + "The specified module could not be found." |
| 947 | + ) |
| 948 | + |
| 949 | + loader.set_load_error(dll_error) |
| 950 | + |
| 951 | + error_msg = loader.get_load_error() |
| 952 | + assert "DLL load failed" in error_msg |
| 953 | + assert "nanoarrow_cpp" in error_msg |
0 commit comments