From 5e8e5f63632906c737b5d946835c0839f2140115 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 16 Dec 2025 15:40:12 +0530 Subject: [PATCH 01/11] FIX: Resolve mssql-auth.dll not found error for paths with non-ASCII characters (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: On Windows, paths containing non-ASCII characters (e.g., usernames like 'Thalén' with 'é') were being corrupted due to: 1. GetModuleDirectory() using ANSI APIs (char[], PathRemoveFileSpecA) 2. LoadDriverLibrary() using broken UTF-8→UTF-16 conversion via std::wstring(path.begin(), path.end()) 3. LoadDriverOrThrowException() using same broken pattern for mssql-auth.dll Fix: Use std::filesystem::path which properly handles encoding on all platforms. On Windows, fs::path::c_str() returns wchar_t* with correct UTF-16 encoding. This fix enables users with non-ASCII characters in their Windows username or installation path to use Entra ID authentication successfully. --- mssql_python/pybind/ddbc_bindings.cpp | 49 ++++++++++++--------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index db629c26..e613c9de 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -845,34 +845,25 @@ std::string GetLastErrorMessage(); // TODO: Move this to Python std::string GetModuleDirectory() { + namespace fs = std::filesystem; py::object module = py::module::import("mssql_python"); py::object module_path = module.attr("__file__"); std::string module_file = module_path.cast(); -#ifdef _WIN32 - // Windows-specific path handling - char path[MAX_PATH]; - errno_t err = strncpy_s(path, MAX_PATH, module_file.c_str(), module_file.length()); - if (err != 0) { - LOG("GetModuleDirectory: strncpy_s failed copying path - " - "error_code=%d, path_length=%zu", - err, module_file.length()); - return {}; - } - PathRemoveFileSpecA(path); - return std::string(path); -#else - // macOS/Unix path handling without using std::filesystem - std::string::size_type pos = module_file.find_last_of('/'); - if (pos != std::string::npos) { - std::string dir = module_file.substr(0, pos); - return dir; + // Use std::filesystem::path for cross-platform path handling + // This properly handles UTF-8 encoded paths on all platforms + fs::path modulePath(module_file); + fs::path parentDir = modulePath.parent_path(); + + if (parentDir.empty()) { + LOG("GetModuleDirectory: Could not extract directory from module path - " + "path='%s'", + module_file.c_str()); + return module_file; } - LOG("GetModuleDirectory: Could not extract directory from module path - " - "path='%s'", - module_file.c_str()); - return module_file; -#endif + + // Return UTF-8 encoded string for consistent handling + return parentDir.string(); } // Platform-agnostic function to load the driver dynamic library @@ -880,9 +871,11 @@ DriverHandle LoadDriverLibrary(const std::string& driverPath) { LOG("LoadDriverLibrary: Attempting to load ODBC driver from path='%s'", driverPath.c_str()); #ifdef _WIN32 - // Windows: Convert string to wide string for LoadLibraryW - std::wstring widePath(driverPath.begin(), driverPath.end()); - HMODULE handle = LoadLibraryW(widePath.c_str()); + // Windows: Use std::filesystem::path for proper UTF-8 to UTF-16 conversion + // fs::path::c_str() returns wchar_t* on Windows with correct encoding + namespace fs = std::filesystem; + fs::path pathObj(driverPath); + HMODULE handle = LoadLibraryW(pathObj.c_str()); if (!handle) { LOG("LoadDriverLibrary: LoadLibraryW failed for path='%s' - %s", driverPath.c_str(), GetLastErrorMessage().c_str()); @@ -1013,8 +1006,8 @@ DriverHandle LoadDriverOrThrowException() { fs::path dllDir = fs::path(moduleDir) / "libs" / "windows" / archDir; fs::path authDllPath = dllDir / "mssql-auth.dll"; if (fs::exists(authDllPath)) { - HMODULE hAuth = LoadLibraryW( - std::wstring(authDllPath.native().begin(), authDllPath.native().end()).c_str()); + // Use fs::path::c_str() which returns wchar_t* on Windows with proper encoding + HMODULE hAuth = LoadLibraryW(authDllPath.c_str()); if (hAuth) { LOG("LoadDriverOrThrowException: mssql-auth.dll loaded " "successfully from '%s'", From 18d843a2bdac9e7cffe11e6d955671ef5351957f Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 16 Dec 2025 15:46:54 +0530 Subject: [PATCH 02/11] TEST: Add tests for UTF-8 path handling fix (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tests for the non-ASCII path encoding fix: 1. Default tests (cross-platform): - Verify module import exercises path handling code - Test UTF-8 string operations with international characters - Test pathlib with non-ASCII directory names 2. Windows-specific tests: - Verify DLL loading succeeds - Verify libs directory structure 3. Integration tests (Windows only, ~2-4 min total): - Create venv in paths with Swedish (Thalén), German (Müller), Japanese (日本語), and Chinese (中文) characters - Install mssql-python and verify import succeeds These tests ensure the fs::path fix for LoadLibraryW works correctly for users with non-ASCII characters in their Windows username. --- tests/test_015_utf8_path_handling.py | 403 +++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 tests/test_015_utf8_path_handling.py diff --git a/tests/test_015_utf8_path_handling.py b/tests/test_015_utf8_path_handling.py new file mode 100644 index 00000000..2432d659 --- /dev/null +++ b/tests/test_015_utf8_path_handling.py @@ -0,0 +1,403 @@ +""" +Tests for UTF-8 path handling fix (Issue #370). + +Verifies that the driver correctly handles paths containing non-ASCII +characters on Windows (e.g., usernames like 'Thalén', folders like 'café'). + +Bug Summary: +- GetModuleDirectory() used ANSI APIs (PathRemoveFileSpecA) which corrupted UTF-8 paths +- LoadDriverLibrary() used broken UTF-8→UTF-16 conversion: std::wstring(path.begin(), path.end()) +- LoadDriverOrThrowException() used same broken pattern for mssql-auth.dll + +Fix: +- Use std::filesystem::path which handles encoding correctly on all platforms +- fs::path::c_str() returns wchar_t* on Windows with proper UTF-16 encoding + +Test Categories: +================ +1. DEFAULT TESTS (run always) - Exercise the fixed code paths +2. SLOW TESTS (run with -m slow) - Full integration with non-ASCII venv paths +""" + +import pytest +import platform +import sys +import os +import tempfile +import shutil +import subprocess + + +class TestPathHandlingCodePaths: + """ + Test that path handling code paths are exercised correctly. + + These tests run by DEFAULT and verify the fixed C++ functions + (GetModuleDirectory, LoadDriverLibrary) are working. + """ + + def test_module_import_exercises_path_handling(self): + """ + Verify module import succeeds - this exercises GetModuleDirectory(). + + When mssql_python imports, it calls: + 1. GetModuleDirectory() - to find module location + 2. LoadDriverLibrary() - to load ODBC driver + 3. LoadLibraryW() for mssql-auth.dll on Windows + + If any of these fail due to path encoding issues, import fails. + """ + import mssql_python + + assert mssql_python is not None + assert hasattr(mssql_python, "__file__") + assert isinstance(mssql_python.__file__, str) + + def test_module_path_is_valid_utf8(self): + """Verify module path is valid UTF-8 string.""" + import mssql_python + + module_path = mssql_python.__file__ + + # Should be encodable/decodable as UTF-8 without errors + encoded = module_path.encode("utf-8") + decoded = encoded.decode("utf-8") + assert decoded == module_path + + def test_connect_function_available(self): + """Verify connect function is available (proves ddbc_bindings loaded).""" + import mssql_python + + assert hasattr(mssql_python, "connect") + assert callable(mssql_python.connect) + + def test_ddbc_bindings_loaded(self): + """Verify ddbc_bindings C++ module loaded successfully.""" + from mssql_python import ddbc_bindings + + assert ddbc_bindings is not None + + def test_connection_class_available(self): + """Verify Connection class from C++ bindings is accessible.""" + from mssql_python.ddbc_bindings import Connection + + assert Connection is not None + + +class TestPathWithNonAsciiCharacters: + """ + Test path handling with non-ASCII characters in strings. + + These tests verify that Python string operations with non-ASCII + characters work correctly (prerequisite for the C++ fix to work). + """ + + # Non-ASCII test strings representing real-world scenarios + NON_ASCII_PATHS = [ + "Thalén", # Swedish - the original issue reporter's username + "café", # French + "日本語", # Japanese + "中文", # Chinese + "über", # German + "Müller", # German umlaut + "España", # Spanish + "Россия", # Russian + "한국어", # Korean + "Ñoño", # Spanish ñ + "Ångström", # Swedish å + ] + + @pytest.mark.parametrize("non_ascii_name", NON_ASCII_PATHS) + def test_path_string_with_non_ascii(self, non_ascii_name): + """Test that Python can handle paths with non-ASCII characters.""" + # Simulate Windows-style path + test_path = f"C:\\Users\\{non_ascii_name}\\project\\.venv\\Lib\\site-packages" + + # Verify UTF-8 encoding/decoding works + encoded = test_path.encode("utf-8") + decoded = encoded.decode("utf-8") + assert decoded == test_path + assert non_ascii_name in decoded + + @pytest.mark.parametrize("non_ascii_name", NON_ASCII_PATHS) + def test_pathlib_with_non_ascii(self, non_ascii_name, tmp_path): + """Test that pathlib handles non-ASCII directory names.""" + from pathlib import Path + + test_dir = tmp_path / non_ascii_name + test_dir.mkdir() + assert test_dir.exists() + + # Create a file in the non-ASCII directory + test_file = test_dir / "test.txt" + test_file.write_text("test content", encoding="utf-8") + assert test_file.exists() + + # Read back + content = test_file.read_text(encoding="utf-8") + assert content == "test content" + + def test_path_with_multiple_non_ascii_segments(self, tmp_path): + """Test path with multiple non-ASCII directory segments.""" + from pathlib import Path + + # Create nested directories with non-ASCII names + nested = tmp_path / "Thalén" / "プロジェクト" / "código" + nested.mkdir(parents=True) + assert nested.exists() + + def test_path_with_spaces_and_non_ascii(self, tmp_path): + """Test path with both spaces and non-ASCII characters.""" + from pathlib import Path + + test_dir = tmp_path / "My Thalén Project" + test_dir.mkdir() + assert test_dir.exists() + + +@pytest.mark.skipif( + platform.system() != "Windows", reason="DLL loading and path encoding issue is Windows-specific" +) +class TestWindowsSpecificPathHandling: + """ + Windows-specific tests for path handling. + + These tests verify Windows-specific behavior related to the fix. + """ + + def test_module_loads_on_windows(self): + """Verify module loads correctly on Windows.""" + import mssql_python + from mssql_python import ddbc_bindings + + # If we get here, LoadLibraryW succeeded for: + # - msodbcsql18.dll + # - mssql-auth.dll (if exists) + assert ddbc_bindings is not None + + def test_libs_directory_exists(self): + """Verify the libs/windows directory structure exists.""" + import mssql_python + from pathlib import Path + + module_dir = Path(mssql_python.__file__).parent + libs_dir = module_dir / "libs" / "windows" + + # Check that at least one architecture directory exists + arch_dirs = ["x64", "x86", "arm64"] + found_arch = any((libs_dir / arch).exists() for arch in arch_dirs) + assert found_arch, f"No architecture directory found in {libs_dir}" + + def test_auth_dll_exists_if_libs_present(self): + """Verify mssql-auth.dll exists in the libs directory.""" + import mssql_python + from pathlib import Path + import struct + + module_dir = Path(mssql_python.__file__).parent + + # Determine architecture + arch = "x64" if struct.calcsize("P") * 8 == 64 else "x86" + # Check for ARM64 + import platform + + if platform.machine().lower() in ("arm64", "aarch64"): + arch = "arm64" + + auth_dll = module_dir / "libs" / "windows" / arch / "mssql-auth.dll" + + if auth_dll.parent.exists(): + # If the directory exists, the DLL should be there + assert auth_dll.exists(), f"mssql-auth.dll not found at {auth_dll}" + + +@pytest.mark.skipif(platform.system() != "Windows", reason="Integration test requires Windows") +class TestNonAsciiPathIntegration: + """ + Full integration tests for non-ASCII path handling. + + These tests create actual virtual environments in paths with non-ASCII + characters to verify the fix works end-to-end (Issue #370). + + NOTE: These tests take ~30-60s each as they: + 1. Create a virtual environment + 2. Install mssql-python via pip + 3. Verify import works + + This is acceptable overhead for a critical bug fix that affects users + with non-ASCII characters in their Windows username or installation path. + """ + + @pytest.fixture + def clean_test_dir(self, tmp_path): + """Create a clean test directory and cleanup after.""" + yield tmp_path + # Cleanup is handled automatically by tmp_path fixture + + def test_venv_in_swedish_username_path(self, clean_test_dir): + """ + Reproduce Issue #370: venv in path with Swedish character 'é'. + + This is the exact scenario reported by the user with username 'Thalén'. + """ + self._test_venv_in_non_ascii_path(clean_test_dir, "Thalén_test") + + def test_venv_in_german_umlaut_path(self, clean_test_dir): + """Test venv in path with German umlaut 'ü'.""" + self._test_venv_in_non_ascii_path(clean_test_dir, "Müller_projekt") + + def test_venv_in_japanese_path(self, clean_test_dir): + """Test venv in path with Japanese characters.""" + self._test_venv_in_non_ascii_path(clean_test_dir, "日本語プロジェクト") + + def test_venv_in_chinese_path(self, clean_test_dir): + """Test venv in path with Chinese characters.""" + self._test_venv_in_non_ascii_path(clean_test_dir, "中文项目") + + def _test_venv_in_non_ascii_path(self, base_path, folder_name): + """ + Helper method to test venv creation and mssql-python import in non-ASCII path. + + Args: + base_path: Base temporary directory + folder_name: Folder name containing non-ASCII characters + """ + # Create directory with non-ASCII character + test_dir = base_path / folder_name + test_dir.mkdir() + + venv_path = test_dir / ".venv" + + # Create virtual environment + result = subprocess.run( + [sys.executable, "-m", "venv", str(venv_path)], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, f"venv creation failed: {result.stderr}" + + # Get the Python executable in the venv + python_exe = venv_path / "Scripts" / "python.exe" + assert python_exe.exists(), f"Python not found at {python_exe}" + + # Install mssql-python in the venv + pip_result = subprocess.run( + [str(python_exe), "-m", "pip", "install", "mssql-python", "--quiet"], + capture_output=True, + text=True, + timeout=120, + ) + + if pip_result.returncode != 0: + pytest.skip(f"pip install failed (may not be published): {pip_result.stderr}") + + # Try to import mssql_python - this is where the bug manifests + # The C++ code calls LoadLibraryW with the path, and if UTF-8→UTF-16 + # conversion is broken, it fails with "mssql-auth.dll not found" + test_script = """ +import sys +try: + import mssql_python + from mssql_python import ddbc_bindings + print("SUCCESS") + print(f"Module: {mssql_python.__file__}") +except Exception as e: + print(f"FAILED: {e}") + sys.exit(1) +""" + + import_result = subprocess.run( + [str(python_exe), "-c", test_script], capture_output=True, text=True, timeout=30 + ) + + assert import_result.returncode == 0, ( + f"Import failed in non-ASCII path '{folder_name}'.\n" + f"stdout: {import_result.stdout}\n" + f"stderr: {import_result.stderr}\n" + f"This indicates the UTF-8 path encoding fix is not working." + ) + assert "SUCCESS" in import_result.stdout + + +class TestPathEncodingEdgeCases: + """Test edge cases in path encoding handling.""" + + def test_ascii_only_path_still_works(self): + """Verify ASCII-only paths continue to work (regression test).""" + import mssql_python + + # If we got here, module loaded successfully + assert mssql_python is not None + + def test_path_with_spaces(self): + """Verify paths with spaces work (common Windows scenario).""" + import mssql_python + + # Common Windows paths like "Program Files" have spaces + # Module should load regardless + assert mssql_python.__file__ is not None + + def test_very_long_path_component(self, tmp_path): + """Test handling of long path components.""" + from pathlib import Path + + # Windows MAX_PATH is 260, but individual components can be up to 255 + long_name = "a" * 200 + test_dir = tmp_path / long_name + test_dir.mkdir() + assert test_dir.exists() + + @pytest.mark.parametrize( + "char", + [ + "é", + "ñ", + "ü", + "ö", + "å", + "ø", + "æ", # European diacritics + "中", + "日", + "한", # CJK ideographs + "α", + "β", + "γ", # Greek letters + "й", + "ж", + "щ", # Cyrillic + ], + ) + def test_individual_non_ascii_chars_utf8_roundtrip(self, char): + """Test UTF-8 encoding roundtrip for individual non-ASCII characters.""" + test_path = f"C:\\Users\\Test{char}User\\project" + + # UTF-8 roundtrip + encoded = test_path.encode("utf-8") + decoded = encoded.decode("utf-8") + assert decoded == test_path + assert char in decoded + + def test_emoji_in_path(self, tmp_path): + """Test path with emoji characters (supplementary plane).""" + from pathlib import Path + + # Emoji are in the supplementary planes (> U+FFFF) + # This tests 4-byte UTF-8 sequences + try: + emoji_dir = tmp_path / "test_🚀_project" + emoji_dir.mkdir() + assert emoji_dir.exists() + except OSError: + # Some filesystems don't support emoji in filenames + pytest.skip("Filesystem doesn't support emoji in filenames") + + def test_mixed_scripts_in_path(self, tmp_path): + """Test path with mixed scripts (Latin + CJK + Cyrillic).""" + from pathlib import Path + + mixed_name = "Project_项目_Проект" + test_dir = tmp_path / mixed_name + test_dir.mkdir() + assert test_dir.exists() From 49ec0d9ec2113fce0e897944ec6da94ea6bb336d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 16 Dec 2025 17:20:01 +0530 Subject: [PATCH 03/11] TEST: Mark heavy tests as stress in test_013_SqlHandle_free_shutdown.py Mark 4 tests as @pytest.mark.stress (skipped by default per pytest.ini): - test_aggressive_dbc_segfault_reproduction: 10 real DB connections - test_force_gc_finalization_order_issue: 10 connections + 5 GC cycles - test_rapid_connection_churn_with_shutdown: 10 connections with churn - test_active_connections_thread_safety: 200 mock connections + 10 threads These tests are resource-intensive and slow down CI. They will still run when explicitly requested with 'pytest -m stress' or 'pytest -m ""'. --- tests/test_013_SqlHandle_free_shutdown.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_013_SqlHandle_free_shutdown.py b/tests/test_013_SqlHandle_free_shutdown.py index 950757c2..28e75bf0 100644 --- a/tests/test_013_SqlHandle_free_shutdown.py +++ b/tests/test_013_SqlHandle_free_shutdown.py @@ -32,10 +32,13 @@ import threading import time +import pytest + class TestHandleFreeShutdown: """Test SqlHandle::free() behavior for all handle types during Python shutdown.""" + @pytest.mark.stress def test_aggressive_dbc_segfault_reproduction(self, conn_str): """ AGGRESSIVE TEST: Try to reproduce DBC handle segfault during shutdown. @@ -157,6 +160,7 @@ def on_exit(): assert result.returncode == 0, f"Process failed. stderr: {result.stderr}" print(f"PASS: DBC handle cleanup properly skipped during shutdown") + @pytest.mark.stress def test_force_gc_finalization_order_issue(self, conn_str): """ TEST: Force specific GC finalization order to trigger segfault. @@ -434,6 +438,7 @@ def test_mixed_handle_cleanup_at_shutdown(self, conn_str): assert "Connection 3: everything properly closed" in result.stdout print(f"PASS: Mixed handle cleanup during shutdown") + @pytest.mark.stress def test_rapid_connection_churn_with_shutdown(self, conn_str): """ Test rapid connection creation/deletion followed by shutdown. @@ -1087,6 +1092,7 @@ def close(self): assert "Mixed scenario: PASSED" in result.stdout print(f"PASS: Cleanup connections mixed scenario") + @pytest.mark.stress def test_active_connections_thread_safety(self, conn_str): """ Test _active_connections thread-safety with concurrent registration. From 32ef88deaf790b74d0ea67f0e7697cdd527c5d6c Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 17 Dec 2025 08:44:17 +0000 Subject: [PATCH 04/11] fallback not needed --- mssql_python/pybind/ddbc_bindings.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index e613c9de..32aa81ea 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -862,6 +862,11 @@ std::string GetModuleDirectory() { return module_file; } + // Log successful path extraction for observability + LOG("GetModuleDirectory: Successfully extracted directory - " + "original_path='%s', directory='%s'", + module_file.c_str(), parentDir.string().c_str()); + // Return UTF-8 encoded string for consistent handling return parentDir.string(); } From 793117c4d0868a074074e247fb42ad9219172138 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 17 Dec 2025 08:45:47 +0000 Subject: [PATCH 05/11] remove older logic --- mssql_python/pybind/ddbc_bindings.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 32aa81ea..e4c69718 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -855,19 +855,14 @@ std::string GetModuleDirectory() { fs::path modulePath(module_file); fs::path parentDir = modulePath.parent_path(); - if (parentDir.empty()) { - LOG("GetModuleDirectory: Could not extract directory from module path - " - "path='%s'", - module_file.c_str()); - return module_file; - } - - // Log successful path extraction for observability - LOG("GetModuleDirectory: Successfully extracted directory - " + // Log path extraction for observability + LOG("GetModuleDirectory: Extracted directory - " "original_path='%s', directory='%s'", module_file.c_str(), parentDir.string().c_str()); // Return UTF-8 encoded string for consistent handling + // If parentDir is empty or invalid, subsequent operations (like LoadDriverLibrary) + // will fail naturally with clear error messages return parentDir.string(); } From 2307ff17ddcd85b93266ccd76991fbe58d5488e5 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 10:13:28 +0000 Subject: [PATCH 06/11] restore tests --- tests/test_013_SqlHandle_free_shutdown.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_013_SqlHandle_free_shutdown.py b/tests/test_013_SqlHandle_free_shutdown.py index 28e75bf0..950757c2 100644 --- a/tests/test_013_SqlHandle_free_shutdown.py +++ b/tests/test_013_SqlHandle_free_shutdown.py @@ -32,13 +32,10 @@ import threading import time -import pytest - class TestHandleFreeShutdown: """Test SqlHandle::free() behavior for all handle types during Python shutdown.""" - @pytest.mark.stress def test_aggressive_dbc_segfault_reproduction(self, conn_str): """ AGGRESSIVE TEST: Try to reproduce DBC handle segfault during shutdown. @@ -160,7 +157,6 @@ def on_exit(): assert result.returncode == 0, f"Process failed. stderr: {result.stderr}" print(f"PASS: DBC handle cleanup properly skipped during shutdown") - @pytest.mark.stress def test_force_gc_finalization_order_issue(self, conn_str): """ TEST: Force specific GC finalization order to trigger segfault. @@ -438,7 +434,6 @@ def test_mixed_handle_cleanup_at_shutdown(self, conn_str): assert "Connection 3: everything properly closed" in result.stdout print(f"PASS: Mixed handle cleanup during shutdown") - @pytest.mark.stress def test_rapid_connection_churn_with_shutdown(self, conn_str): """ Test rapid connection creation/deletion followed by shutdown. @@ -1092,7 +1087,6 @@ def close(self): assert "Mixed scenario: PASSED" in result.stdout print(f"PASS: Cleanup connections mixed scenario") - @pytest.mark.stress def test_active_connections_thread_safety(self, conn_str): """ Test _active_connections thread-safety with concurrent registration. From 94c285f8471d21c0654e91d8ea5cec37e425dafb Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 10:14:22 +0000 Subject: [PATCH 07/11] restore tests --- tests/test_013_SqlHandle_free_shutdown.py | 324 +++++++++++++++------- 1 file changed, 220 insertions(+), 104 deletions(-) diff --git a/tests/test_013_SqlHandle_free_shutdown.py b/tests/test_013_SqlHandle_free_shutdown.py index 5f1792a9..950757c2 100644 --- a/tests/test_013_SqlHandle_free_shutdown.py +++ b/tests/test_013_SqlHandle_free_shutdown.py @@ -63,10 +63,12 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): # This maximizes the chance of DBC handles being finalized # AFTER the static ENV handle has destructed connections = [] - for i in range(5): # Reduced for faster execution + for i in range(10): # Reduced from 20 to avoid timeout conn = connect("{conn_str}") # Don't even create cursors - just DBC handles connections.append(conn) + if i % 3 == 0: + print(f"Created {{i+1}} connections...") print(f"Created {{len(connections)}} DBC handles") print("Forcing GC to ensure objects are tracked...") @@ -85,7 +87,7 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) # Check for segfault @@ -101,7 +103,7 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): ), f"SEGFAULT reproduced with signal {signal_num} - DBC handles not protected" else: assert result.returncode == 0, f"Process failed. stderr: {result.stderr}" - assert "Created 5 DBC handles" in result.stdout + assert "Created 10 DBC handles" in result.stdout print(f"PASS: No segfault - DBC handles properly protected during shutdown") def test_dbc_handle_outlives_env_handle(self, conn_str): @@ -143,7 +145,7 @@ def on_exit(): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) if result.returncode < 0: @@ -178,7 +180,7 @@ def test_force_gc_finalization_order_issue(self, conn_str): connections = [] weakrefs = [] - for i in range(5): # Reduced for faster execution + for i in range(10): # Reduced from 15 to avoid timeout conn = connect("{conn_str}") wr = weakref.ref(conn) connections.append(conn) @@ -192,9 +194,9 @@ def test_force_gc_finalization_order_issue(self, conn_str): # Delete strong references del connections - # Force GC cycles + # Force multiple GC cycles print("Forcing GC cycles...") - for i in range(2): + for i in range(5): collected = gc.collect() print(f"GC cycle {{i+1}}: collected {{collected}} objects") @@ -209,7 +211,7 @@ def test_force_gc_finalization_order_issue(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) if result.returncode < 0: @@ -253,7 +255,7 @@ def test_stmt_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -298,7 +300,7 @@ def test_dbc_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -348,7 +350,7 @@ def test_env_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -422,7 +424,7 @@ def test_mixed_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -451,7 +453,7 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): from mssql_python import connect # Create and delete connections rapidly - for i in range(6): + for i in range(10): conn = connect("{conn_str}") cursor = conn.cursor() cursor.execute(f"SELECT {{i}} AS test") @@ -463,7 +465,7 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): conn.close() # Leave odd-numbered connections open - print("Created 6 connections, closed 3 explicitly") + print("Created 10 connections, closed 5 explicitly") # Force GC before shutdown gc.collect() @@ -477,11 +479,11 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" - assert "Created 6 connections, closed 3 explicitly" in result.stdout + assert "Created 10 connections, closed 5 explicitly" in result.stdout assert "Rapid churn test: Exiting with mixed cleanup" in result.stdout print(f"PASS: Rapid connection churn with shutdown") @@ -518,7 +520,7 @@ def test_exception_during_query_with_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -573,7 +575,7 @@ def callback(ref): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -633,7 +635,7 @@ def execute_query(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -707,7 +709,7 @@ def test_all_handle_types_comprehensive(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -719,12 +721,24 @@ def test_all_handle_types_comprehensive(self, conn_str): assert "=== Exiting ===" in result.stdout print(f"PASS: Comprehensive all handle types test") - @pytest.mark.parametrize( - "scenario,test_code,expected_msg", - [ - ( - "normal_flow", - """ + def test_cleanup_connections_normal_flow(self, conn_str): + """ + Test _cleanup_connections() with normal active connections. + + Validates that: + 1. Active connections (_closed=False) are properly closed + 2. The cleanup function is registered with atexit + 3. Connections can be registered and tracked + """ + script = textwrap.dedent( + f""" + import mssql_python + + # Verify cleanup infrastructure exists + assert hasattr(mssql_python, '_active_connections'), "Missing _active_connections" + assert hasattr(mssql_python, '_cleanup_connections'), "Missing _cleanup_connections" + assert hasattr(mssql_python, '_register_connection'), "Missing _register_connection" + # Create mock connection to test registration and cleanup class MockConnection: def __init__(self): @@ -744,12 +758,30 @@ def close(self): mssql_python._cleanup_connections() assert mock_conn.close_called, "close() should have been called" assert mock_conn._closed, "Connection should be marked as closed" - """, - "Normal flow: PASSED", - ), - ( - "already_closed", - """ + + print("Normal flow: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Normal flow: PASSED" in result.stdout + print(f"PASS: Cleanup connections normal flow") + + def test_cleanup_connections_already_closed(self, conn_str): + """ + Test _cleanup_connections() with already closed connections. + + Validates that connections with _closed=True are skipped + and close() is not called again. + """ + script = textwrap.dedent( + f""" + import mssql_python + class MockConnection: def __init__(self): self._closed = True # Already closed @@ -766,12 +798,30 @@ def close(self): # Cleanup should skip this connection mssql_python._cleanup_connections() assert not mock_conn.close_called, "close() should NOT have been called" - """, - "Already closed: PASSED", - ), - ( - "missing_attribute", - """ + + print("Already closed: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Already closed: PASSED" in result.stdout + print(f"PASS: Cleanup connections already closed") + + def test_cleanup_connections_missing_attribute(self, conn_str): + """ + Test _cleanup_connections() with connections missing _closed attribute. + + Validates that hasattr() check prevents AttributeError and + cleanup continues gracefully. + """ + script = textwrap.dedent( + f""" + import mssql_python + class MinimalConnection: # No _closed attribute def close(self): @@ -783,12 +833,32 @@ def close(self): # Should not crash mssql_python._cleanup_connections() - """, - "Missing attribute: PASSED", - ), - ( - "exception_handling", - """ + + print("Missing attribute: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Missing attribute: PASSED" in result.stdout + print(f"PASS: Cleanup connections missing _closed attribute") + + def test_cleanup_connections_exception_handling(self, conn_str): + """ + Test _cleanup_connections() exception handling. + + Validates that: + 1. Exceptions during close() are caught and silently ignored + 2. One failing connection doesn't prevent cleanup of others + 3. The function completes successfully despite errors + """ + script = textwrap.dedent( + f""" + import mssql_python + class GoodConnection: def __init__(self): self._closed = False @@ -816,15 +886,32 @@ def close(self): mssql_python._cleanup_connections() # Should not raise despite bad_conn throwing exception assert good_conn.close_called, "Good connection should still be closed" + print("Exception handling: PASSED") except Exception as e: print(f"Exception handling: FAILED - Exception escaped: {{e}}") raise - """, - "Exception handling: PASSED", - ), - ( - "multiple_connections", - """ + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Exception handling: PASSED" in result.stdout + print(f"PASS: Cleanup connections exception handling") + + def test_cleanup_connections_multiple_connections(self, conn_str): + """ + Test _cleanup_connections() with multiple connections. + + Validates that all registered connections are processed + and closed in the cleanup iteration. + """ + script = textwrap.dedent( + f""" + import mssql_python + class TestConnection: count = 0 @@ -848,12 +935,31 @@ def close(self): assert TestConnection.count == 5, f"All 5 connections should be closed, got {{TestConnection.count}}" assert all(c.close_called for c in connections), "All connections should have close() called" - """, - "Multiple connections: PASSED", - ), - ( - "weakset_behavior", - """ + + print("Multiple connections: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Multiple connections: PASSED" in result.stdout + print(f"PASS: Cleanup connections multiple connections") + + def test_cleanup_connections_weakset_behavior(self, conn_str): + """ + Test _cleanup_connections() WeakSet behavior. + + Validates that: + 1. WeakSet automatically removes garbage collected connections + 2. Only live references are processed during cleanup + 3. No crashes occur with GC'd connections + """ + script = textwrap.dedent( + f""" + import mssql_python import gc class TestConnection: @@ -876,23 +982,62 @@ def close(self): # Cleanup should not crash with removed connections mssql_python._cleanup_connections() - """, - "WeakSet behavior: PASSED", - ), - ( - "empty_list", - """ + + print("WeakSet behavior: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "WeakSet behavior: PASSED" in result.stdout + print(f"PASS: Cleanup connections WeakSet behavior") + + def test_cleanup_connections_empty_list(self, conn_str): + """ + Test _cleanup_connections() with empty connections list. + + Validates that cleanup completes successfully with no registered + connections without any errors. + """ + script = textwrap.dedent( + f""" + import mssql_python + # Clear any existing connections mssql_python._active_connections.clear() # Should not crash with empty set mssql_python._cleanup_connections() - """, - "Empty list: PASSED", - ), - ( - "mixed_scenario", - """ + + print("Empty list: PASSED") + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" + assert "Empty list: PASSED" in result.stdout + print(f"PASS: Cleanup connections empty list") + + def test_cleanup_connections_mixed_scenario(self, conn_str): + """ + Test _cleanup_connections() with mixed connection states. + + Validates handling of: + - Open connections (should be closed) + - Already closed connections (should be skipped) + - Connections that throw exceptions (should be caught) + - All in one cleanup run + """ + script = textwrap.dedent( + f""" + import mssql_python + class OpenConnection: def __init__(self): self._closed = False @@ -929,47 +1074,18 @@ def close(self): mssql_python._cleanup_connections() assert open_conn.close_called, "Open connection should have been closed" - """, - "Mixed scenario: PASSED", - ), - ], - ) - def test_cleanup_connections_scenarios(self, conn_str, scenario, test_code, expected_msg): - """ - Test _cleanup_connections() with various scenarios. - - Scenarios tested: - - normal_flow: Active connections properly closed - - already_closed: Closed connections skipped - - missing_attribute: Gracefully handles missing _closed attribute - - exception_handling: Exceptions caught, cleanup continues - - multiple_connections: All connections processed - - weakset_behavior: Auto-removes GC'd connections - - empty_list: No errors with empty set - - mixed_scenario: Mixed connection states handled correctly - """ - script = textwrap.dedent( - f""" - import mssql_python - - # Verify cleanup infrastructure exists - assert hasattr(mssql_python, '_active_connections'), "Missing _active_connections" - assert hasattr(mssql_python, '_cleanup_connections'), "Missing _cleanup_connections" - assert hasattr(mssql_python, '_register_connection'), "Missing _register_connection" - - {test_code} - print("{expected_msg}") + print("Mixed scenario: PASSED") """ ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert expected_msg in result.stdout - print(f"PASS: Cleanup connections scenario '{scenario}'") + assert "Mixed scenario: PASSED" in result.stdout + print(f"PASS: Cleanup connections mixed scenario") def test_active_connections_thread_safety(self, conn_str): """ @@ -1055,7 +1171,7 @@ def register_connections(thread_id, count): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" @@ -1154,7 +1270,7 @@ def close(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" @@ -1246,7 +1362,7 @@ def close(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" From 2f1d64bf0e2507f5a91e5da8b6560b4b40871334 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 10:15:38 +0000 Subject: [PATCH 08/11] restore tests --- tests/test_013_SqlHandle_free_shutdown.py | 326 +++++++--------------- 1 file changed, 106 insertions(+), 220 deletions(-) diff --git a/tests/test_013_SqlHandle_free_shutdown.py b/tests/test_013_SqlHandle_free_shutdown.py index 950757c2..7e426cfb 100644 --- a/tests/test_013_SqlHandle_free_shutdown.py +++ b/tests/test_013_SqlHandle_free_shutdown.py @@ -32,6 +32,8 @@ import threading import time +import pytest + class TestHandleFreeShutdown: """Test SqlHandle::free() behavior for all handle types during Python shutdown.""" @@ -63,12 +65,10 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): # This maximizes the chance of DBC handles being finalized # AFTER the static ENV handle has destructed connections = [] - for i in range(10): # Reduced from 20 to avoid timeout + for i in range(5): # Reduced for faster execution conn = connect("{conn_str}") # Don't even create cursors - just DBC handles connections.append(conn) - if i % 3 == 0: - print(f"Created {{i+1}} connections...") print(f"Created {{len(connections)}} DBC handles") print("Forcing GC to ensure objects are tracked...") @@ -87,7 +87,7 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) # Check for segfault @@ -103,7 +103,7 @@ def test_aggressive_dbc_segfault_reproduction(self, conn_str): ), f"SEGFAULT reproduced with signal {signal_num} - DBC handles not protected" else: assert result.returncode == 0, f"Process failed. stderr: {result.stderr}" - assert "Created 10 DBC handles" in result.stdout + assert "Created 5 DBC handles" in result.stdout print(f"PASS: No segfault - DBC handles properly protected during shutdown") def test_dbc_handle_outlives_env_handle(self, conn_str): @@ -145,7 +145,7 @@ def on_exit(): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) if result.returncode < 0: @@ -180,7 +180,7 @@ def test_force_gc_finalization_order_issue(self, conn_str): connections = [] weakrefs = [] - for i in range(10): # Reduced from 15 to avoid timeout + for i in range(5): # Reduced for faster execution conn = connect("{conn_str}") wr = weakref.ref(conn) connections.append(conn) @@ -194,9 +194,9 @@ def test_force_gc_finalization_order_issue(self, conn_str): # Delete strong references del connections - # Force multiple GC cycles + # Force GC cycles print("Forcing GC cycles...") - for i in range(5): + for i in range(2): collected = gc.collect() print(f"GC cycle {{i+1}}: collected {{collected}} objects") @@ -211,7 +211,7 @@ def test_force_gc_finalization_order_issue(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) if result.returncode < 0: @@ -255,7 +255,7 @@ def test_stmt_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -300,7 +300,7 @@ def test_dbc_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -350,7 +350,7 @@ def test_env_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -424,7 +424,7 @@ def test_mixed_handle_cleanup_at_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -453,7 +453,7 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): from mssql_python import connect # Create and delete connections rapidly - for i in range(10): + for i in range(6): conn = connect("{conn_str}") cursor = conn.cursor() cursor.execute(f"SELECT {{i}} AS test") @@ -465,7 +465,7 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): conn.close() # Leave odd-numbered connections open - print("Created 10 connections, closed 5 explicitly") + print("Created 6 connections, closed 3 explicitly") # Force GC before shutdown gc.collect() @@ -479,11 +479,11 @@ def test_rapid_connection_churn_with_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" - assert "Created 10 connections, closed 5 explicitly" in result.stdout + assert "Created 6 connections, closed 3 explicitly" in result.stdout assert "Rapid churn test: Exiting with mixed cleanup" in result.stdout print(f"PASS: Rapid connection churn with shutdown") @@ -520,7 +520,7 @@ def test_exception_during_query_with_shutdown(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -575,7 +575,7 @@ def callback(ref): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -635,7 +635,7 @@ def execute_query(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -709,7 +709,7 @@ def test_all_handle_types_comprehensive(self, conn_str): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=5 ) assert result.returncode == 0, f"Process crashed. stderr: {result.stderr}" @@ -721,24 +721,12 @@ def test_all_handle_types_comprehensive(self, conn_str): assert "=== Exiting ===" in result.stdout print(f"PASS: Comprehensive all handle types test") - def test_cleanup_connections_normal_flow(self, conn_str): - """ - Test _cleanup_connections() with normal active connections. - - Validates that: - 1. Active connections (_closed=False) are properly closed - 2. The cleanup function is registered with atexit - 3. Connections can be registered and tracked - """ - script = textwrap.dedent( - f""" - import mssql_python - - # Verify cleanup infrastructure exists - assert hasattr(mssql_python, '_active_connections'), "Missing _active_connections" - assert hasattr(mssql_python, '_cleanup_connections'), "Missing _cleanup_connections" - assert hasattr(mssql_python, '_register_connection'), "Missing _register_connection" - + @pytest.mark.parametrize( + "scenario,test_code,expected_msg", + [ + ( + "normal_flow", + """ # Create mock connection to test registration and cleanup class MockConnection: def __init__(self): @@ -758,30 +746,12 @@ def close(self): mssql_python._cleanup_connections() assert mock_conn.close_called, "close() should have been called" assert mock_conn._closed, "Connection should be marked as closed" - - print("Normal flow: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Normal flow: PASSED" in result.stdout - print(f"PASS: Cleanup connections normal flow") - - def test_cleanup_connections_already_closed(self, conn_str): - """ - Test _cleanup_connections() with already closed connections. - - Validates that connections with _closed=True are skipped - and close() is not called again. - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "Normal flow: PASSED", + ), + ( + "already_closed", + """ class MockConnection: def __init__(self): self._closed = True # Already closed @@ -798,30 +768,12 @@ def close(self): # Cleanup should skip this connection mssql_python._cleanup_connections() assert not mock_conn.close_called, "close() should NOT have been called" - - print("Already closed: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Already closed: PASSED" in result.stdout - print(f"PASS: Cleanup connections already closed") - - def test_cleanup_connections_missing_attribute(self, conn_str): - """ - Test _cleanup_connections() with connections missing _closed attribute. - - Validates that hasattr() check prevents AttributeError and - cleanup continues gracefully. - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "Already closed: PASSED", + ), + ( + "missing_attribute", + """ class MinimalConnection: # No _closed attribute def close(self): @@ -833,32 +785,12 @@ def close(self): # Should not crash mssql_python._cleanup_connections() - - print("Missing attribute: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Missing attribute: PASSED" in result.stdout - print(f"PASS: Cleanup connections missing _closed attribute") - - def test_cleanup_connections_exception_handling(self, conn_str): - """ - Test _cleanup_connections() exception handling. - - Validates that: - 1. Exceptions during close() are caught and silently ignored - 2. One failing connection doesn't prevent cleanup of others - 3. The function completes successfully despite errors - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "Missing attribute: PASSED", + ), + ( + "exception_handling", + """ class GoodConnection: def __init__(self): self._closed = False @@ -886,32 +818,15 @@ def close(self): mssql_python._cleanup_connections() # Should not raise despite bad_conn throwing exception assert good_conn.close_called, "Good connection should still be closed" - print("Exception handling: PASSED") except Exception as e: print(f"Exception handling: FAILED - Exception escaped: {{e}}") raise - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Exception handling: PASSED" in result.stdout - print(f"PASS: Cleanup connections exception handling") - - def test_cleanup_connections_multiple_connections(self, conn_str): - """ - Test _cleanup_connections() with multiple connections. - - Validates that all registered connections are processed - and closed in the cleanup iteration. - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "Exception handling: PASSED", + ), + ( + "multiple_connections", + """ class TestConnection: count = 0 @@ -935,31 +850,12 @@ def close(self): assert TestConnection.count == 5, f"All 5 connections should be closed, got {{TestConnection.count}}" assert all(c.close_called for c in connections), "All connections should have close() called" - - print("Multiple connections: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Multiple connections: PASSED" in result.stdout - print(f"PASS: Cleanup connections multiple connections") - - def test_cleanup_connections_weakset_behavior(self, conn_str): - """ - Test _cleanup_connections() WeakSet behavior. - - Validates that: - 1. WeakSet automatically removes garbage collected connections - 2. Only live references are processed during cleanup - 3. No crashes occur with GC'd connections - """ - script = textwrap.dedent( - f""" - import mssql_python + """, + "Multiple connections: PASSED", + ), + ( + "weakset_behavior", + """ import gc class TestConnection: @@ -982,62 +878,23 @@ def close(self): # Cleanup should not crash with removed connections mssql_python._cleanup_connections() - - print("WeakSet behavior: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "WeakSet behavior: PASSED" in result.stdout - print(f"PASS: Cleanup connections WeakSet behavior") - - def test_cleanup_connections_empty_list(self, conn_str): - """ - Test _cleanup_connections() with empty connections list. - - Validates that cleanup completes successfully with no registered - connections without any errors. - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "WeakSet behavior: PASSED", + ), + ( + "empty_list", + """ # Clear any existing connections mssql_python._active_connections.clear() # Should not crash with empty set mssql_python._cleanup_connections() - - print("Empty list: PASSED") - """ - ) - - result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 - ) - - assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Empty list: PASSED" in result.stdout - print(f"PASS: Cleanup connections empty list") - - def test_cleanup_connections_mixed_scenario(self, conn_str): - """ - Test _cleanup_connections() with mixed connection states. - - Validates handling of: - - Open connections (should be closed) - - Already closed connections (should be skipped) - - Connections that throw exceptions (should be caught) - - All in one cleanup run - """ - script = textwrap.dedent( - f""" - import mssql_python - + """, + "Empty list: PASSED", + ), + ( + "mixed_scenario", + """ class OpenConnection: def __init__(self): self._closed = False @@ -1074,18 +931,47 @@ def close(self): mssql_python._cleanup_connections() assert open_conn.close_called, "Open connection should have been closed" + """, + "Mixed scenario: PASSED", + ), + ], + ) + def test_cleanup_connections_scenarios(self, conn_str, scenario, test_code, expected_msg): + """ + Test _cleanup_connections() with various scenarios. + + Scenarios tested: + - normal_flow: Active connections properly closed + - already_closed: Closed connections skipped + - missing_attribute: Gracefully handles missing _closed attribute + - exception_handling: Exceptions caught, cleanup continues + - multiple_connections: All connections processed + - weakset_behavior: Auto-removes GC'd connections + - empty_list: No errors with empty set + - mixed_scenario: Mixed connection states handled correctly + """ + script = textwrap.dedent( + f""" + import mssql_python + + # Verify cleanup infrastructure exists + assert hasattr(mssql_python, '_active_connections'), "Missing _active_connections" + assert hasattr(mssql_python, '_cleanup_connections'), "Missing _cleanup_connections" + assert hasattr(mssql_python, '_register_connection'), "Missing _register_connection" + + {test_code} - print("Mixed scenario: PASSED") + print("{expected_msg}") """ ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" - assert "Mixed scenario: PASSED" in result.stdout - print(f"PASS: Cleanup connections mixed scenario") + assert expected_msg in result.stdout + print(f"PASS: Cleanup connections scenario '{scenario}'") def test_active_connections_thread_safety(self, conn_str): """ @@ -1171,7 +1057,7 @@ def register_connections(thread_id, count): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" @@ -1270,7 +1156,7 @@ def close(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" @@ -1362,7 +1248,7 @@ def close(self): ) result = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, timeout=10 + [sys.executable, "-c", script], capture_output=True, text=True, timeout=3 ) assert result.returncode == 0, f"Test failed. stderr: {result.stderr}" From 1d9bcddc281c4fcf3b2ea5f6b7669fd41d8c5db5 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 16:42:15 +0530 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_015_utf8_path_handling.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_015_utf8_path_handling.py b/tests/test_015_utf8_path_handling.py index 2432d659..32920167 100644 --- a/tests/test_015_utf8_path_handling.py +++ b/tests/test_015_utf8_path_handling.py @@ -22,9 +22,6 @@ import pytest import platform import sys -import os -import tempfile -import shutil import subprocess @@ -168,12 +165,11 @@ class TestWindowsSpecificPathHandling: def test_module_loads_on_windows(self): """Verify module loads correctly on Windows.""" import mssql_python - from mssql_python import ddbc_bindings # If we get here, LoadLibraryW succeeded for: # - msodbcsql18.dll # - mssql-auth.dll (if exists) - assert ddbc_bindings is not None + assert mssql_python.ddbc_bindings is not None def test_libs_directory_exists(self): """Verify the libs/windows directory structure exists.""" @@ -199,7 +195,6 @@ def test_auth_dll_exists_if_libs_present(self): # Determine architecture arch = "x64" if struct.calcsize("P") * 8 == 64 else "x86" # Check for ARM64 - import platform if platform.machine().lower() in ("arm64", "aarch64"): arch = "arm64" From e78c8c4d37173aefa0d215b62f6571c571843507 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 11:14:50 +0000 Subject: [PATCH 10/11] Refactor import statements for mssql_python in UTF-8 path handling tests --- tests/test_015_utf8_path_handling.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/tests/test_015_utf8_path_handling.py b/tests/test_015_utf8_path_handling.py index 32920167..350aa17c 100644 --- a/tests/test_015_utf8_path_handling.py +++ b/tests/test_015_utf8_path_handling.py @@ -24,6 +24,9 @@ import sys import subprocess +import mssql_python +from mssql_python import ddbc_bindings + class TestPathHandlingCodePaths: """ @@ -44,16 +47,12 @@ def test_module_import_exercises_path_handling(self): If any of these fail due to path encoding issues, import fails. """ - import mssql_python - assert mssql_python is not None assert hasattr(mssql_python, "__file__") assert isinstance(mssql_python.__file__, str) def test_module_path_is_valid_utf8(self): """Verify module path is valid UTF-8 string.""" - import mssql_python - module_path = mssql_python.__file__ # Should be encodable/decodable as UTF-8 without errors @@ -63,22 +62,16 @@ def test_module_path_is_valid_utf8(self): def test_connect_function_available(self): """Verify connect function is available (proves ddbc_bindings loaded).""" - import mssql_python - assert hasattr(mssql_python, "connect") assert callable(mssql_python.connect) def test_ddbc_bindings_loaded(self): """Verify ddbc_bindings C++ module loaded successfully.""" - from mssql_python import ddbc_bindings - assert ddbc_bindings is not None def test_connection_class_available(self): """Verify Connection class from C++ bindings is accessible.""" - from mssql_python.ddbc_bindings import Connection - - assert Connection is not None + assert ddbc_bindings.Connection is not None class TestPathWithNonAsciiCharacters: @@ -173,7 +166,6 @@ def test_module_loads_on_windows(self): def test_libs_directory_exists(self): """Verify the libs/windows directory structure exists.""" - import mssql_python from pathlib import Path module_dir = Path(mssql_python.__file__).parent @@ -186,7 +178,6 @@ def test_libs_directory_exists(self): def test_auth_dll_exists_if_libs_present(self): """Verify mssql-auth.dll exists in the libs directory.""" - import mssql_python from pathlib import Path import struct @@ -320,15 +311,11 @@ class TestPathEncodingEdgeCases: def test_ascii_only_path_still_works(self): """Verify ASCII-only paths continue to work (regression test).""" - import mssql_python - # If we got here, module loaded successfully assert mssql_python is not None def test_path_with_spaces(self): """Verify paths with spaces work (common Windows scenario).""" - import mssql_python - # Common Windows paths like "Program Files" have spaces # Module should load regardless assert mssql_python.__file__ is not None From 13cbda17b4992a8dc8b32dadd1c41c229f894714 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 24 Dec 2025 12:26:33 +0000 Subject: [PATCH 11/11] Remove integration tests for non-ASCII path handling and clean up related comments --- tests/test_015_utf8_path_handling.py | 114 --------------------------- 1 file changed, 114 deletions(-) diff --git a/tests/test_015_utf8_path_handling.py b/tests/test_015_utf8_path_handling.py index 350aa17c..542b3b16 100644 --- a/tests/test_015_utf8_path_handling.py +++ b/tests/test_015_utf8_path_handling.py @@ -12,11 +12,6 @@ Fix: - Use std::filesystem::path which handles encoding correctly on all platforms - fs::path::c_str() returns wchar_t* on Windows with proper UTF-16 encoding - -Test Categories: -================ -1. DEFAULT TESTS (run always) - Exercise the fixed code paths -2. SLOW TESTS (run with -m slow) - Full integration with non-ASCII venv paths """ import pytest @@ -197,115 +192,6 @@ def test_auth_dll_exists_if_libs_present(self): assert auth_dll.exists(), f"mssql-auth.dll not found at {auth_dll}" -@pytest.mark.skipif(platform.system() != "Windows", reason="Integration test requires Windows") -class TestNonAsciiPathIntegration: - """ - Full integration tests for non-ASCII path handling. - - These tests create actual virtual environments in paths with non-ASCII - characters to verify the fix works end-to-end (Issue #370). - - NOTE: These tests take ~30-60s each as they: - 1. Create a virtual environment - 2. Install mssql-python via pip - 3. Verify import works - - This is acceptable overhead for a critical bug fix that affects users - with non-ASCII characters in their Windows username or installation path. - """ - - @pytest.fixture - def clean_test_dir(self, tmp_path): - """Create a clean test directory and cleanup after.""" - yield tmp_path - # Cleanup is handled automatically by tmp_path fixture - - def test_venv_in_swedish_username_path(self, clean_test_dir): - """ - Reproduce Issue #370: venv in path with Swedish character 'é'. - - This is the exact scenario reported by the user with username 'Thalén'. - """ - self._test_venv_in_non_ascii_path(clean_test_dir, "Thalén_test") - - def test_venv_in_german_umlaut_path(self, clean_test_dir): - """Test venv in path with German umlaut 'ü'.""" - self._test_venv_in_non_ascii_path(clean_test_dir, "Müller_projekt") - - def test_venv_in_japanese_path(self, clean_test_dir): - """Test venv in path with Japanese characters.""" - self._test_venv_in_non_ascii_path(clean_test_dir, "日本語プロジェクト") - - def test_venv_in_chinese_path(self, clean_test_dir): - """Test venv in path with Chinese characters.""" - self._test_venv_in_non_ascii_path(clean_test_dir, "中文项目") - - def _test_venv_in_non_ascii_path(self, base_path, folder_name): - """ - Helper method to test venv creation and mssql-python import in non-ASCII path. - - Args: - base_path: Base temporary directory - folder_name: Folder name containing non-ASCII characters - """ - # Create directory with non-ASCII character - test_dir = base_path / folder_name - test_dir.mkdir() - - venv_path = test_dir / ".venv" - - # Create virtual environment - result = subprocess.run( - [sys.executable, "-m", "venv", str(venv_path)], - capture_output=True, - text=True, - timeout=60, - ) - assert result.returncode == 0, f"venv creation failed: {result.stderr}" - - # Get the Python executable in the venv - python_exe = venv_path / "Scripts" / "python.exe" - assert python_exe.exists(), f"Python not found at {python_exe}" - - # Install mssql-python in the venv - pip_result = subprocess.run( - [str(python_exe), "-m", "pip", "install", "mssql-python", "--quiet"], - capture_output=True, - text=True, - timeout=120, - ) - - if pip_result.returncode != 0: - pytest.skip(f"pip install failed (may not be published): {pip_result.stderr}") - - # Try to import mssql_python - this is where the bug manifests - # The C++ code calls LoadLibraryW with the path, and if UTF-8→UTF-16 - # conversion is broken, it fails with "mssql-auth.dll not found" - test_script = """ -import sys -try: - import mssql_python - from mssql_python import ddbc_bindings - print("SUCCESS") - print(f"Module: {mssql_python.__file__}") -except Exception as e: - print(f"FAILED: {e}") - sys.exit(1) -""" - - import_result = subprocess.run( - [str(python_exe), "-c", test_script], capture_output=True, text=True, timeout=30 - ) - - assert import_result.returncode == 0, ( - f"Import failed in non-ASCII path '{folder_name}'.\n" - f"stdout: {import_result.stdout}\n" - f"stderr: {import_result.stderr}\n" - f"This indicates the UTF-8 path encoding fix is not working." - ) - assert "SUCCESS" in import_result.stdout - - class TestPathEncodingEdgeCases: """Test edge cases in path encoding handling."""