From f1f6d3030b4f93aa5b4ed3d1ae27d92b5d4b8066 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Thu, 29 Jan 2026 12:46:45 +0700 Subject: [PATCH 01/23] Refactor PostgreSQLExecutor to support Windows compatibility for process management - Convert _get_base_command method to BASE_PROC_START_COMMAND class attribute - Use unified command format without single quotes around PostgreSQL config values - Add _windows_terminate_process method for graceful Windows process termination - Update stop() method to use list args instead of shell=True for security - Add platform-specific termination logic with killpg fallback - Add comprehensive Windows compatibility test suite - Rename parameter in _windows_terminate_process method for clarity --- pytest_postgresql/executor.py | 44 ++++++- tests/test_windows_compatibility.py | 186 ++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 tests/test_windows_compatibility.py diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index 17cf0bd3..725dc87e 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -17,6 +17,7 @@ # along with pytest-postgresql. If not, see . """PostgreSQL executor crafter around pg_ctl.""" +import os import os.path import platform import re @@ -48,11 +49,14 @@ class PostgreSQLExecutor(TCPExecutor): `_ """ + # Base PostgreSQL start command template - cross-platform compatible + # Use unified format without single quotes around values + # This format works on both Windows and Unix systems BASE_PROC_START_COMMAND = ( '{executable} start -D "{datadir}" ' - "-o \"-F -p {port} -c log_destination='stderr' " + '-o "-F -p {port} -c log_destination=stderr ' "-c logging_collector=off " - "-c unix_socket_directories='{unixsocketdir}' {postgres_options}\" " + '-c unix_socket_directories={unixsocketdir} {postgres_options}" ' '-l "{logfile}" {startparams}' ) @@ -219,17 +223,47 @@ def running(self) -> bool: status_code = subprocess.getstatusoutput(f'{self.executable} status -D "{self.datadir}"')[0] return status_code == 0 + def _windows_terminate_process(self, _sig: Optional[int] = None) -> None: + """Terminate process on Windows. + + :param _sig: Signal parameter (unused on Windows but included for consistency) + """ + if self.process is None: + return + + try: + # On Windows, try to terminate gracefully first + self.process.terminate() + # Give it a chance to terminate gracefully + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + # If it doesn't terminate gracefully, force kill + self.process.kill() + self.process.wait() + except (OSError, AttributeError): + # Process might already be dead or other issues + pass + def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T: """Issue a stop request to executable.""" subprocess.check_output( - f'{self.executable} stop -D "{self.datadir}" -m f', - shell=True, + [self.executable, "stop", "-D", self.datadir, "-m", "f"], ) try: - super().stop(sig, exp_sig) + if platform.system() == "Windows": + self._windows_terminate_process(sig) + else: + super().stop(sig, exp_sig) except ProcessFinishedWithError: # Finished, leftovers ought to be cleaned afterwards anyway pass + except AttributeError as e: + # Fallback for edge cases where os.killpg doesn't exist + if "killpg" in str(e): + self._windows_terminate_process(sig) + else: + raise return self def __del__(self) -> None: diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py new file mode 100644 index 00000000..dee2837d --- /dev/null +++ b/tests/test_windows_compatibility.py @@ -0,0 +1,186 @@ +"""Test Windows compatibility fixes for pytest-postgresql.""" + +import subprocess +from unittest.mock import MagicMock, patch + +from pytest_postgresql.executor import PostgreSQLExecutor + + +class TestWindowsCompatibility: + """Test Windows-specific functionality.""" + + def test_base_command_unified(self) -> None: + """Test that base command template is unified and cross-platform compatible.""" + # The BASE_PROC_START_COMMAND should use the simplified format without single quotes + # around configuration values, which works on both Windows and Unix systems + command_template = PostgreSQLExecutor.BASE_PROC_START_COMMAND + + # Should use simplified format without single quotes + assert "log_destination=stderr" in command_template + assert "log_destination='stderr'" not in command_template + assert "unix_socket_directories={unixsocketdir}" in command_template + assert "unix_socket_directories='{unixsocketdir}'" not in command_template + + def test_windows_terminate_process(self) -> None: + """Test Windows process termination.""" + executor = PostgreSQLExecutor( + executable="/path/to/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Mock process + mock_process = MagicMock() + executor.process = mock_process + + # No need to mock platform.system() since the method doesn't check it anymore + executor._windows_terminate_process() + + # Should call terminate first + mock_process.terminate.assert_called_once() + mock_process.wait.assert_called() + + def test_windows_terminate_process_force_kill(self) -> None: + """Test Windows process termination with force kill on timeout.""" + executor = PostgreSQLExecutor( + executable="/path/to/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Mock process that times out + mock_process = MagicMock() + mock_process.wait.side_effect = [subprocess.TimeoutExpired(cmd="test", timeout=5), None] + executor.process = mock_process + + # No need to mock platform.system() since the method doesn't check it anymore + executor._windows_terminate_process() + + # Should call terminate, wait (timeout), then kill, then wait again + mock_process.terminate.assert_called_once() + mock_process.kill.assert_called_once() + assert mock_process.wait.call_count == 2 + + def test_stop_method_windows(self) -> None: + """Test stop method on Windows.""" + executor = PostgreSQLExecutor( + executable="/path/to/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Mock subprocess and process + with ( + patch("subprocess.check_output") as mock_subprocess, + patch("platform.system", return_value="Windows"), + patch.object(executor, "_windows_terminate_process") as mock_terminate, + ): + result = executor.stop() + + # Should call pg_ctl stop and Windows terminate + mock_subprocess.assert_called_once() + mock_terminate.assert_called_once() + assert result is executor + + def test_stop_method_unix(self) -> None: + """Test stop method on Unix systems.""" + executor = PostgreSQLExecutor( + executable="/path/to/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Mock subprocess and super().stop + with ( + patch("subprocess.check_output") as mock_subprocess, + patch("platform.system", return_value="Linux"), + patch("pytest_postgresql.executor.TCPExecutor.stop") as mock_super_stop, + ): + mock_super_stop.return_value = executor + result = executor.stop() + + # Should call pg_ctl stop and parent class stop + mock_subprocess.assert_called_once() + mock_super_stop.assert_called_once_with(None, None) + assert result is executor + + def test_stop_method_fallback_on_killpg_error(self) -> None: + """Test stop method falls back to Windows termination on killpg AttributeError.""" + executor = PostgreSQLExecutor( + executable="/path/to/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Mock subprocess and super().stop to raise AttributeError + with ( + patch("subprocess.check_output") as mock_subprocess, + patch("platform.system", return_value="Linux"), + patch( + "pytest_postgresql.executor.TCPExecutor.stop", + side_effect=AttributeError("module 'os' has no attribute 'killpg'"), + ), + patch.object(executor, "_windows_terminate_process") as mock_terminate, + ): + result = executor.stop() + + # Should call pg_ctl stop, fail on super().stop, then use Windows terminate + mock_subprocess.assert_called_once() + mock_terminate.assert_called_once() + assert result is executor + + def test_command_formatting_windows(self) -> None: + """Test that command is properly formatted for Windows paths.""" + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5555, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log.txt", + startparams="-w -s", + dbname="testdb", + postgres_options="-c shared_preload_libraries=test", + ) + + # The command should be properly formatted without single quotes around values + expected_parts = [ + "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start", + '-D "C:/temp/data"', + '-o "-F -p 5555 -c log_destination=stderr', + "-c logging_collector=off", + "-c unix_socket_directories=C:/temp/socket", + '-c shared_preload_libraries=test"', + '-l "C:/temp/log.txt"', + "-w -s", + ] + + # Check if all expected parts are in the command + command = executor.command + for part in expected_parts: + assert part in command, f"Expected '{part}' in command: {command}" From 13be8021c3ed65d8ae6f83325bb1ca0b6129f8ce Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Wed, 11 Feb 2026 20:45:50 +0700 Subject: [PATCH 02/23] Enhance PostgreSQL workflow for Windows compatibility and streamline environment variable usage - Added detection of PostgreSQL path for Windows runners. - Set PostgreSQL path for Unix/macOS environments. - Updated test commands to utilize the dynamically set PostgreSQL executable path. - Introduced new Windows job configurations for PostgreSQL versions 16, 17, and 18 in tests.yml. - Removed unnecessary import in conftest.py as the plugin is registered via entry point. --- .github/workflows/single-postgres.yml | 24 +++++++++++++++++++++--- .github/workflows/tests.yml | 21 +++++++++++++++++++++ tests/conftest.py | 3 ++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 1a86ac80..03395f91 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -43,7 +43,25 @@ jobs: - uses: ankane/setup-postgres@v1 with: postgres-version: ${{ inputs.postgresql }} + - name: Detect PostgreSQL path on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + $pgPath = "C:\Program Files\PostgreSQL\${{ inputs.postgresql }}\bin\pg_ctl" + if (Test-Path $pgPath) { + echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV + } else { + $pgPath = (Get-Command pg_ctl -ErrorAction SilentlyContinue).Source + if ($pgPath) { + echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV + } + } + - name: Set PostgreSQL path for Unix/macOS + if: runner.os != 'Windows' + run: | + echo "POSTGRESQL_EXEC=/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV - name: Check installed locales + if: runner.os != 'Windows' run: | locale -a - name: update locale for tests @@ -51,16 +69,16 @@ jobs: run: | sudo locale-gen de_DE.UTF-8 - name: install libpq - if: ${{ contains(inputs.python-versions, 'pypy') }} + if: ${{ contains(inputs.python-versions, 'pypy') && runner.os == 'Linux' }} run: sudo apt install libpq5 - name: Run test uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: - command: pytest -svv -p no:xdist --postgresql-exec="/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" -k "not docker" --cov-report=xml + command: pytest -svv -p no:xdist --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml - name: Run xdist test uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: - command: pytest -n auto --dist loadgroup --max-worker-restart 0 --postgresql-exec="/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" -k "not docker" --cov-report=xml:coverage-xdist.xml + command: pytest -n auto --dist loadgroup --max-worker-restart 0 --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml:coverage-xdist.xml - uses: actions/upload-artifact@v6 if: failure() with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1c33cdee..12910def 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,6 +59,27 @@ jobs: postgresql: 16 os: macos-latest python-versions: '["3.13", "3.14"]' + windows_postgres_18: + needs: [postgresql_18] + uses: ./.github/workflows/single-postgres.yml + with: + postgresql: 18 + os: windows-latest + python-versions: '["3.12", "3.13", "3.14"]' + windows_postgres_17: + needs: [postgresql_17, windows_postgres_18] + uses: ./.github/workflows/single-postgres.yml + with: + postgresql: 17 + os: windows-latest + python-versions: '["3.12", "3.13", "3.14"]' + windows_postgres_16: + needs: [postgresql_16, windows_postgres_17] + uses: ./.github/workflows/single-postgres.yml + with: + postgresql: 16 + os: windows-latest + python-versions: '["3.13", "3.14"]' docker_postgresql_18: needs: [postgresql_18] uses: ./.github/workflows/dockerised-postgres.yml diff --git a/tests/conftest.py b/tests/conftest.py index 784b8905..045d7003 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,8 @@ from pathlib import Path from pytest_postgresql import factories -from pytest_postgresql.plugin import * # noqa: F403,F401 +# Plugin is registered via entry point in pyproject.toml - no need to import here +# from pytest_postgresql.plugin import * # noqa: F403,F401 pytest_plugins = ["pytester"] POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13") From 909961f7094de04ef97e1933a0cab0d342525249 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:46:05 +0000 Subject: [PATCH 03/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 045d7003..51eca992 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from pathlib import Path from pytest_postgresql import factories + # Plugin is registered via entry point in pyproject.toml - no need to import here # from pytest_postgresql.plugin import * # noqa: F403,F401 From 4e3594b5777df2dbeecfcedaf932149f03f0a721 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Wed, 11 Feb 2026 21:15:58 +0700 Subject: [PATCH 04/23] Enhance PostgreSQL workflow and executor for improved cross-platform compatibility - Implement dynamic detection of pg_ctl path for Unix/macOS environments in the GitHub Actions workflow. - Add logging for exception handling during Windows process termination in PostgreSQLExecutor. - Update test mocks to reference the correct subprocess path for better compatibility. - Remove unnecessary import in conftest.py to streamline the codebase. --- .github/workflows/single-postgres.yml | 20 ++++++++++++++++++-- pytest_postgresql/executor.py | 16 ++++++++++++---- tests/conftest.py | 1 - tests/test_windows_compatibility.py | 6 +++--- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 03395f91..72c5e695 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -59,7 +59,23 @@ jobs: - name: Set PostgreSQL path for Unix/macOS if: runner.os != 'Windows' run: | - echo "POSTGRESQL_EXEC=/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV + # Try to find pg_ctl dynamically for cross-platform compatibility + if command -v pg_ctl >/dev/null 2>&1; then + PG_CTL_PATH=$(command -v pg_ctl) + echo "POSTGRESQL_EXEC=$PG_CTL_PATH" >> $GITHUB_ENV + elif [ -f "/opt/homebrew/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" ]; then + # macOS Apple Silicon Homebrew path + echo "POSTGRESQL_EXEC=/opt/homebrew/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV + elif [ -f "/usr/local/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" ]; then + # macOS Intel Homebrew path + echo "POSTGRESQL_EXEC=/usr/local/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV + elif [ -f "/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" ]; then + # Debian/Ubuntu path (fallback) + echo "POSTGRESQL_EXEC=/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV + else + echo "Error: pg_ctl not found in expected locations" + exit 1 + fi - name: Check installed locales if: runner.os != 'Windows' run: | @@ -83,7 +99,7 @@ jobs: if: failure() with: name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }} - path: /tmp/pytest-of-runner/** + path: ${{ runner.temp }}/pytest-of-runner/** - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index 725dc87e..30bf451c 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -17,6 +17,7 @@ # along with pytest-postgresql. If not, see . """PostgreSQL executor crafter around pg_ctl.""" +import logging import os import os.path import platform @@ -28,6 +29,8 @@ from typing import Any, Optional, TypeVar from mirakuru import TCPExecutor + +logger = logging.getLogger(__name__) from mirakuru.exceptions import ProcessFinishedWithError from packaging.version import parse @@ -241,8 +244,13 @@ def _windows_terminate_process(self, _sig: Optional[int] = None) -> None: # If it doesn't terminate gracefully, force kill self.process.kill() self.process.wait() - except (OSError, AttributeError): + except (OSError, AttributeError) as e: # Process might already be dead or other issues + logger.debug( + "Exception during Windows process termination: %s: %s", + type(e).__name__, + e, + ) pass def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T: @@ -258,9 +266,9 @@ def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T except ProcessFinishedWithError: # Finished, leftovers ought to be cleaned afterwards anyway pass - except AttributeError as e: - # Fallback for edge cases where os.killpg doesn't exist - if "killpg" in str(e): + except AttributeError: + # Fallback for edge cases where os.killpg doesn't exist (e.g., Windows) + if not hasattr(os, "killpg"): self._windows_terminate_process(sig) else: raise diff --git a/tests/conftest.py b/tests/conftest.py index 045d7003..14e9dcfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ from pytest_postgresql import factories # Plugin is registered via entry point in pyproject.toml - no need to import here -# from pytest_postgresql.plugin import * # noqa: F403,F401 pytest_plugins = ["pytester"] POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13") diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py index dee2837d..c6550a02 100644 --- a/tests/test_windows_compatibility.py +++ b/tests/test_windows_compatibility.py @@ -86,7 +86,7 @@ def test_stop_method_windows(self) -> None: # Mock subprocess and process with ( - patch("subprocess.check_output") as mock_subprocess, + patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess, patch("platform.system", return_value="Windows"), patch.object(executor, "_windows_terminate_process") as mock_terminate, ): @@ -112,7 +112,7 @@ def test_stop_method_unix(self) -> None: # Mock subprocess and super().stop with ( - patch("subprocess.check_output") as mock_subprocess, + patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess, patch("platform.system", return_value="Linux"), patch("pytest_postgresql.executor.TCPExecutor.stop") as mock_super_stop, ): @@ -139,7 +139,7 @@ def test_stop_method_fallback_on_killpg_error(self) -> None: # Mock subprocess and super().stop to raise AttributeError with ( - patch("subprocess.check_output") as mock_subprocess, + patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess, patch("platform.system", return_value="Linux"), patch( "pytest_postgresql.executor.TCPExecutor.stop", From 5611d227d4ed1d3f93eacaf74f59367ad53b4a29 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Wed, 11 Feb 2026 21:27:18 +0700 Subject: [PATCH 05/23] Refactor PostgreSQLExecutor and enhance Windows compatibility in tests - Reintroduce logger initialization in executor.py for improved logging. - Remove unnecessary pass statement in exception handling within the stop method. - Update test assertions to ensure correct subprocess call format for Windows compatibility. - Temporarily modify os.killpg attribute in tests to validate fallback behavior on Windows termination. --- pytest_postgresql/executor.py | 5 ++--- tests/test_windows_compatibility.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index 30bf451c..dbe19746 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -29,13 +29,13 @@ from typing import Any, Optional, TypeVar from mirakuru import TCPExecutor - -logger = logging.getLogger(__name__) from mirakuru.exceptions import ProcessFinishedWithError from packaging.version import parse from pytest_postgresql.exceptions import ExecutableMissingException, PostgreSQLUnsupported +logger = logging.getLogger(__name__) + _LOCALE = "C.UTF-8" if platform.system() == "Darwin": @@ -251,7 +251,6 @@ def _windows_terminate_process(self, _sig: Optional[int] = None) -> None: type(e).__name__, e, ) - pass def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T: """Issue a stop request to executable.""" diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py index c6550a02..df0a881c 100644 --- a/tests/test_windows_compatibility.py +++ b/tests/test_windows_compatibility.py @@ -93,7 +93,9 @@ def test_stop_method_windows(self) -> None: result = executor.stop() # Should call pg_ctl stop and Windows terminate - mock_subprocess.assert_called_once() + mock_subprocess.assert_called_once_with( + ["/path/to/pg_ctl", "stop", "-D", "/tmp/data", "-m", "f"], + ) mock_terminate.assert_called_once() assert result is executor @@ -126,6 +128,8 @@ def test_stop_method_unix(self) -> None: def test_stop_method_fallback_on_killpg_error(self) -> None: """Test stop method falls back to Windows termination on killpg AttributeError.""" + import pytest_postgresql.executor + executor = PostgreSQLExecutor( executable="/path/to/pg_ctl", host="localhost", @@ -147,7 +151,15 @@ def test_stop_method_fallback_on_killpg_error(self) -> None: ), patch.object(executor, "_windows_terminate_process") as mock_terminate, ): - result = executor.stop() + # Temporarily remove os.killpg so hasattr(os, "killpg") returns False + real_killpg = getattr(pytest_postgresql.executor.os, "killpg", None) + try: + if real_killpg is not None: + delattr(pytest_postgresql.executor.os, "killpg") + result = executor.stop() + finally: + if real_killpg is not None: + pytest_postgresql.executor.os.killpg = real_killpg # Should call pg_ctl stop, fail on super().stop, then use Windows terminate mock_subprocess.assert_called_once() From bb03b4b654f62a85fc3c570b04b22107700b2e03 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Wed, 11 Feb 2026 22:12:14 +0700 Subject: [PATCH 06/23] Improve process termination handling in PostgreSQLExecutor and refine test assertions - Add timeout handling for process cleanup in the stop method to prevent zombie processes. - Update test to assert correct argument usage for mock_terminate in Windows compatibility tests. --- pytest_postgresql/executor.py | 8 +++++++- tests/test_windows_compatibility.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index dbe19746..05330734 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -243,7 +243,13 @@ def _windows_terminate_process(self, _sig: Optional[int] = None) -> None: except subprocess.TimeoutExpired: # If it doesn't terminate gracefully, force kill self.process.kill() - self.process.wait() + try: + self.process.wait(timeout=10) + except subprocess.TimeoutExpired: + logger.warning( + "Process %s could not be cleaned up after kill() and may be a zombie process", + self.process.pid if self.process else "unknown", + ) except (OSError, AttributeError) as e: # Process might already be dead or other issues logger.debug( diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py index df0a881c..4da0674f 100644 --- a/tests/test_windows_compatibility.py +++ b/tests/test_windows_compatibility.py @@ -96,7 +96,7 @@ def test_stop_method_windows(self) -> None: mock_subprocess.assert_called_once_with( ["/path/to/pg_ctl", "stop", "-D", "/tmp/data", "-m", "f"], ) - mock_terminate.assert_called_once() + mock_terminate.assert_called_once_with(None) assert result is executor def test_stop_method_unix(self) -> None: From 7a9e6a036ac2f315ad3665e222269c33de803639 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Thu, 12 Feb 2026 10:35:53 +0700 Subject: [PATCH 07/23] Remove unnecessary import in conftest.py as the plugin is registered via entry point, streamlining the codebase. --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2414f6cd..10b5f39d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,6 @@ from pytest_postgresql import factories -# Plugin is registered via entry point in pyproject.toml - no need to import here - pytest_plugins = ["pytester"] POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13") From 91e8f2b3ffec63c47abee556b9a192af471f0b77 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 13:58:24 +0700 Subject: [PATCH 08/23] Update GitHub workflows to include editable package installation - Added steps to install the package in editable mode before running tests in dockerised-postgres.yml, oldest-postgres.yml, and single-postgres.yml. - Streamlined the testing process by ensuring the package is available in the current environment for all workflows. --- .github/workflows/dockerised-postgres.yml | 13 ++++++++++--- .github/workflows/oldest-postgres.yml | 4 ++++ .github/workflows/single-postgres.yml | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dockerised-postgres.yml b/.github/workflows/dockerised-postgres.yml index 7f4902dd..d64ca977 100644 --- a/.github/workflows/dockerised-postgres.yml +++ b/.github/workflows/dockerised-postgres.yml @@ -49,12 +49,19 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Run test noproc fixture on docker - uses: fizyk/actions-reuse/.github/actions/pipenv@v4.1.1 + - name: Set up Pipenv on python ${{ matrix.python-version }} + uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.1.1 with: python-version: ${{ matrix.python-version }} - command: pytest -n 0 --max-worker-restart 0 -k docker --postgresql-host=localhost --postgresql-port 5433 --postgresql-password=postgres --cov-report=xml:coverage-docker.xml allow-prereleases: true + - name: Install package in editable mode + uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 + with: + command: pip install -e . + - name: Run test noproc fixture on docker + uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 + with: + command: pytest -n 0 --max-worker-restart 0 -k docker --postgresql-host=localhost --postgresql-port 5433 --postgresql-password=postgres --cov-report=xml:coverage-docker.xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: diff --git a/.github/workflows/oldest-postgres.yml b/.github/workflows/oldest-postgres.yml index 1866f2c2..226cc94c 100644 --- a/.github/workflows/oldest-postgres.yml +++ b/.github/workflows/oldest-postgres.yml @@ -58,6 +58,10 @@ jobs: uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: command: pip install -r oldest/requirements.txt + - name: Install package in editable mode + uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 + with: + command: pip install -e . - name: Run tests without xdist uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 72c5e695..09f5ff27 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -40,6 +40,10 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install package in editable mode + uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 + with: + command: pip install -e . - uses: ankane/setup-postgres@v1 with: postgres-version: ${{ inputs.postgresql }} From 2a8e575da7df7aa5c318152dc630a63f67b8b673 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 14:02:30 +0700 Subject: [PATCH 09/23] Update oldest-postgres.yml to install package without dependencies - Modified the package installation command to include the --no-deps flag, ensuring that only the editable package is installed without its dependencies before running tests. --- .github/workflows/oldest-postgres.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/oldest-postgres.yml b/.github/workflows/oldest-postgres.yml index 226cc94c..6f573c68 100644 --- a/.github/workflows/oldest-postgres.yml +++ b/.github/workflows/oldest-postgres.yml @@ -61,7 +61,7 @@ jobs: - name: Install package in editable mode uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: - command: pip install -e . + command: pip install -e . --no-deps - name: Run tests without xdist uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: From 035d2954149def1f4f829a14be92b1bdbbf08991 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 14:12:54 +0700 Subject: [PATCH 10/23] Enhance PostgreSQL workflow error handling - Added verification step to ensure pg_ctl is found in expected locations during the GitHub Actions workflow for single-postgres.yml. - Implemented error logging and exit strategy if PostgreSQL is not detected, improving robustness of the setup process. --- .github/workflows/single-postgres.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 09f5ff27..33e84bf9 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -60,6 +60,12 @@ jobs: echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV } } + + # Verify that PostgreSQL was found + if (-not $pgPath) { + Write-Error "Error: pg_ctl not found in expected locations. Checked hardcoded path and system PATH." + exit 1 + } - name: Set PostgreSQL path for Unix/macOS if: runner.os != 'Windows' run: | From 58f0d8df1b207f23dab2c20b502b2bda74efb614 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 14:28:24 +0700 Subject: [PATCH 11/23] Refactor PostgreSQLExecutor command templates for platform compatibility - Introduced separate command templates for Unix and Windows to handle quoting and configuration differences. - Updated tests to validate the correct command template is used based on the operating system. - Enhanced test coverage for command formatting, ensuring proper handling of paths with spaces on Unix and omission of unnecessary parameters on Windows. --- pytest_postgresql/executor.py | 30 ++++-- tests/test_windows_compatibility.py | 155 +++++++++++++++++++++++----- 2 files changed, 152 insertions(+), 33 deletions(-) diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index 05330734..dc2cb641 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -52,14 +52,26 @@ class PostgreSQLExecutor(TCPExecutor): `_ """ - # Base PostgreSQL start command template - cross-platform compatible - # Use unified format without single quotes around values - # This format works on both Windows and Unix systems - BASE_PROC_START_COMMAND = ( + # Unix command template - uses single quotes for PostgreSQL config value quoting + # which protects paths with spaces in unix_socket_directories. + # On Unix, mirakuru uses shlex.split() with shell=False, so single quotes + # inside double-quoted strings are preserved and passed to PostgreSQL's config parser. + UNIX_PROC_START_COMMAND = ( '{executable} start -D "{datadir}" ' - '-o "-F -p {port} -c log_destination=stderr ' + "-o \"-F -p {port} -c log_destination='stderr' " "-c logging_collector=off " - '-c unix_socket_directories={unixsocketdir} {postgres_options}" ' + "-c unix_socket_directories='{unixsocketdir}' {postgres_options}\" " + '-l "{logfile}" {startparams}' + ) + + # Windows command template - no single quotes (cmd.exe treats them as literals, + # not delimiters) and unix_socket_directories is omitted entirely since PostgreSQL + # ignores it on Windows. On Windows, mirakuru forces shell=True so the command + # goes through cmd.exe. + WINDOWS_PROC_START_COMMAND = ( + '{executable} start -D "{datadir}" ' + '-o "-F -p {port} -c log_destination=stderr ' + '-c logging_collector=off {postgres_options}" ' '-l "{logfile}" {startparams}' ) @@ -115,7 +127,11 @@ def __init__( self.logfile = logfile self.startparams = startparams self.postgres_options = postgres_options - command = self.BASE_PROC_START_COMMAND.format( + if platform.system() == "Windows": + command_template = self.WINDOWS_PROC_START_COMMAND + else: + command_template = self.UNIX_PROC_START_COMMAND + command = command_template.format( executable=self.executable, datadir=self.datadir, port=port, diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py index 4da0674f..5e76a1a4 100644 --- a/tests/test_windows_compatibility.py +++ b/tests/test_windows_compatibility.py @@ -6,20 +6,118 @@ from pytest_postgresql.executor import PostgreSQLExecutor -class TestWindowsCompatibility: - """Test Windows-specific functionality.""" +class TestCommandTemplates: + """Test platform-specific command templates.""" + + def test_unix_command_template_has_single_quotes(self) -> None: + """Test that Unix template uses single quotes for PostgreSQL config values. + + Single quotes are PostgreSQL config-level quoting that protects paths + with spaces in unix_socket_directories. On Unix, mirakuru uses + shlex.split() which properly handles single quotes inside double-quoted strings. + """ + template = PostgreSQLExecutor.UNIX_PROC_START_COMMAND + + # Unix template should use single quotes around config values + assert "log_destination='stderr'" in template + assert "unix_socket_directories='{unixsocketdir}'" in template + + def test_windows_command_template_no_single_quotes(self) -> None: + """Test that Windows template has no single quotes. + + Windows cmd.exe treats single quotes as literal characters, not + delimiters, which causes errors when passed to pg_ctl. + """ + template = PostgreSQLExecutor.WINDOWS_PROC_START_COMMAND + + # Windows template should NOT use single quotes + assert "log_destination=stderr" in template + assert "log_destination='stderr'" not in template + assert "'" not in template + + def test_windows_command_template_omits_unix_socket_directories(self) -> None: + """Test that Windows template does not include unix_socket_directories. + + PostgreSQL ignores unix_socket_directories on Windows entirely, so + including it is unnecessary and avoids any quoting complexity. + """ + template = PostgreSQLExecutor.WINDOWS_PROC_START_COMMAND + + assert "unix_socket_directories" not in template + assert "{unixsocketdir}" not in template + + def test_unix_command_template_includes_unix_socket_directories(self) -> None: + """Test that Unix template includes unix_socket_directories.""" + template = PostgreSQLExecutor.UNIX_PROC_START_COMMAND + + assert "unix_socket_directories='{unixsocketdir}'" in template + + def test_unix_template_protects_paths_with_spaces(self) -> None: + """Test that Unix template properly quotes paths containing spaces. + + When unixsocketdir contains spaces (e.g., custom temp directories), + the single quotes in the Unix template protect the path from being + split by PostgreSQL's argument parser. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/my socket dir", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # The path with spaces should be enclosed in single quotes + assert "unix_socket_directories='/tmp/my socket dir'" in command + + def test_windows_template_selected_on_windows(self) -> None: + """Test that Windows template is selected when platform is Windows.""" + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Windows template should not have unix_socket_directories + assert "unix_socket_directories" not in command + # Windows template should not have single quotes + assert "log_destination=stderr" in command + assert "log_destination='stderr'" not in command + + def test_unix_template_selected_on_linux(self) -> None: + """Test that Unix template is selected when platform is Linux.""" + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Unix template should have unix_socket_directories with single quotes + assert "unix_socket_directories='/tmp/socket'" in command + assert "log_destination='stderr'" in command - def test_base_command_unified(self) -> None: - """Test that base command template is unified and cross-platform compatible.""" - # The BASE_PROC_START_COMMAND should use the simplified format without single quotes - # around configuration values, which works on both Windows and Unix systems - command_template = PostgreSQLExecutor.BASE_PROC_START_COMMAND - # Should use simplified format without single quotes - assert "log_destination=stderr" in command_template - assert "log_destination='stderr'" not in command_template - assert "unix_socket_directories={unixsocketdir}" in command_template - assert "unix_socket_directories='{unixsocketdir}'" not in command_template +class TestWindowsCompatibility: + """Test Windows-specific process management functionality.""" def test_windows_terminate_process(self) -> None: """Test Windows process termination.""" @@ -168,31 +266,36 @@ def test_stop_method_fallback_on_killpg_error(self) -> None: def test_command_formatting_windows(self) -> None: """Test that command is properly formatted for Windows paths.""" - executor = PostgreSQLExecutor( - executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", - host="localhost", - port=5555, - datadir="C:/temp/data", - unixsocketdir="C:/temp/socket", - logfile="C:/temp/log.txt", - startparams="-w -s", - dbname="testdb", - postgres_options="-c shared_preload_libraries=test", - ) + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5555, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log.txt", + startparams="-w -s", + dbname="testdb", + postgres_options="-c shared_preload_libraries=test", + ) - # The command should be properly formatted without single quotes around values + # The command should be properly formatted without single quotes + # and without unix_socket_directories (irrelevant on Windows) expected_parts = [ "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start", '-D "C:/temp/data"', '-o "-F -p 5555 -c log_destination=stderr', "-c logging_collector=off", - "-c unix_socket_directories=C:/temp/socket", '-c shared_preload_libraries=test"', '-l "C:/temp/log.txt"', "-w -s", ] - # Check if all expected parts are in the command command = executor.command for part in expected_parts: assert part in command, f"Expected '{part}' in command: {command}" + + # Verify unix_socket_directories is NOT in the Windows command + assert "unix_socket_directories" not in command, ( + f"unix_socket_directories should not be in Windows command: {command}" + ) From 14eb2fbbccc6e736f67361ede4e4c25ed4b3fab0 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 14:29:59 +0700 Subject: [PATCH 12/23] Fix PostgreSQL path in Windows workflow - Updated the pg_ctl path in single-postgres.yml to include the .exe extension for compatibility with Windows environments, ensuring proper execution of PostgreSQL commands. --- .github/workflows/single-postgres.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 33e84bf9..a2181e98 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -51,7 +51,7 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - $pgPath = "C:\Program Files\PostgreSQL\${{ inputs.postgresql }}\bin\pg_ctl" + $pgPath = "C:\Program Files\PostgreSQL\${{ inputs.postgresql }}\bin\pg_ctl.exe" if (Test-Path $pgPath) { echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV } else { From ca053a7175f98064219bc9c1f1c6ff6e91ea46c0 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 15:36:49 +0700 Subject: [PATCH 13/23] Update pytest configuration in test_postgres_options_plugin.py - Changed the conftest file generation to use a more explicit pytest_plugins declaration for better clarity and compatibility with pytest's plugin system. --- tests/test_postgres_options_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_postgres_options_plugin.py b/tests/test_postgres_options_plugin.py index 59bc132c..f8df8164 100644 --- a/tests/test_postgres_options_plugin.py +++ b/tests/test_postgres_options_plugin.py @@ -19,7 +19,7 @@ def pointed_pytester(pytester: Pytester) -> Pytester: pytest_postgresql_path = Path(pytest_postgresql.__file__) root_path = pytest_postgresql_path.parent.parent pytester.syspathinsert(root_path) - pytester.makeconftest("from pytest_postgresql.plugin import *\n") + pytester.makeconftest('pytest_plugins = ["pytest_postgresql"]\n') return pytester From 4498275de11218d68ac6ee1343004aa07fae78a0 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Sun, 15 Feb 2026 15:45:41 +0700 Subject: [PATCH 14/23] Update pytest_plugins declaration in test_postgres_options_plugin.py for improved clarity - Changed the pytest_plugins declaration in the conftest file generation to specify the plugin path explicitly, enhancing compatibility with pytest's plugin system. --- tests/test_postgres_options_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_postgres_options_plugin.py b/tests/test_postgres_options_plugin.py index f8df8164..20962a0d 100644 --- a/tests/test_postgres_options_plugin.py +++ b/tests/test_postgres_options_plugin.py @@ -19,7 +19,7 @@ def pointed_pytester(pytester: Pytester) -> Pytester: pytest_postgresql_path = Path(pytest_postgresql.__file__) root_path = pytest_postgresql_path.parent.parent pytester.syspathinsert(root_path) - pytester.makeconftest('pytest_plugins = ["pytest_postgresql"]\n') + pytester.makeconftest('pytest_plugins = ["pytest_postgresql.plugin"]\n') return pytester From 9344cc43c87bb5d620f87a2920220675fef49db4 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Mon, 16 Feb 2026 19:09:07 +0700 Subject: [PATCH 15/23] Enhance platform-specific command templates and test coverage for PostgreSQLExecutor - Added tests to verify correct command templates for Windows, Unix, and Darwin platforms, ensuring proper handling of `unix_socket_directories` and `log_destination`. - Implemented checks for locale settings on Darwin and preserved quoting for `postgres_options` across different platforms. - Improved test assertions for command generation with special characters and empty parameters, enhancing overall test robustness. --- tests/test_executor.py | 217 ++++++++++++++ tests/test_windows_compatibility.py | 428 ++++++++++++++++++++++++++++ 2 files changed, 645 insertions(+) diff --git a/tests/test_executor.py b/tests/test_executor.py index 25c6aa24..f1973f97 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -1,6 +1,8 @@ """Test various executor behaviours.""" +import platform from typing import Any +from unittest.mock import patch import psycopg import pytest @@ -119,6 +121,108 @@ def test_executor_init_bad_tmp_path( password="some password", dbname="some database", ) + + # Verify the correct template was selected based on platform + current_platform = platform.system() + if current_platform == "Windows": + # Windows template should not have unix_socket_directories + assert "unix_socket_directories" not in executor.command + assert "log_destination=stderr" in executor.command + else: + # Unix/Darwin template should have unix_socket_directories with single quotes + assert "unix_socket_directories=" in executor.command + assert "log_destination='stderr'" in executor.command + + assert_executor_start_stop(executor) + + +@pytest.mark.parametrize( + "platform_name", + ["Windows", "Linux", "Darwin"], +) +def test_executor_platform_template_selection( + request: FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, + platform_name: str, +) -> None: + """Test that correct template is selected for each platform. + + This parametrized test verifies that the executor selects the appropriate + command template based on the platform. + """ + config = get_config(request) + pg_exe = process._pg_exe(None, config) + port = process._pg_port(-1, config, []) + tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") + datadir, logfile_path = process._prepare_dir(tmpdir, port) + + with patch("pytest_postgresql.executor.platform.system", return_value=platform_name): + executor = PostgreSQLExecutor( + executable=pg_exe, + host=config.host, + port=port, + datadir=str(datadir), + unixsocketdir=config.unixsocketdir, + logfile=str(logfile_path), + startparams=config.startparams, + dbname="test", + ) + + # Verify correct template was selected + if platform_name == "Windows": + # Windows template + assert "unix_socket_directories" not in executor.command + assert "log_destination=stderr" in executor.command + else: + # Unix/Darwin template + assert "unix_socket_directories=" in executor.command + assert "log_destination='stderr'" in executor.command + + +def test_executor_with_special_chars_in_all_paths( + request: FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + """Test executor with special characters in multiple paths simultaneously. + + This integration test verifies that the executor can handle special + characters (spaces, Unicode) in datadir, logfile, unixsocketdir, and + postgres_options all at the same time. + """ + config = get_config(request) + pg_exe = process._pg_exe(None, config) + port = process._pg_port(-1, config, []) + # Create a tmpdir with spaces in the name + tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") / "my test dir" + tmpdir.mkdir(exist_ok=True) + datadir, logfile_path = process._prepare_dir(tmpdir, port) + + executor = PostgreSQLExecutor( + executable=pg_exe, + host=config.host, + port=port, + datadir=str(datadir), + unixsocketdir=str(tmpdir / "socket dir"), + logfile=str(logfile_path), + startparams=config.startparams, + password="test pass", + dbname="test db", + postgres_options="-N 50", + ) + + # Verify the command contains properly quoted paths + command = executor.command + assert str(datadir) in command or f'"{datadir}"' in command + assert str(logfile_path) in command or f'"{logfile_path}"' in command + + # Verify correct template was selected based on actual platform + current_platform = platform.system() + if current_platform == "Windows": + assert "unix_socket_directories" not in executor.command + else: + assert "unix_socket_directories=" in executor.command + + # Start and stop the executor to verify it works assert_executor_start_stop(executor) @@ -173,3 +277,116 @@ def test_custom_isolation_level(postgres_isolation_level: Connection) -> None: cur = postgres_isolation_level.cursor() cur.execute("SELECT 1") assert cur.fetchone() == (1,) + + +@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-specific test") +def test_actual_postgresql_start_windows( + request: FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + """Test that PostgreSQL actually starts on Windows with the new template. + + This integration test verifies that the Windows-specific command template + correctly starts PostgreSQL on actual Windows systems. + """ + config = get_config(request) + pg_exe = process._pg_exe(None, config) + port = process._pg_port(-1, config, []) + tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") + datadir, logfile_path = process._prepare_dir(tmpdir, port) + + executor = PostgreSQLExecutor( + executable=pg_exe, + host=config.host, + port=port, + datadir=str(datadir), + unixsocketdir=config.unixsocketdir, + logfile=str(logfile_path), + startparams=config.startparams, + dbname="test", + ) + + # Verify Windows template is used + assert "unix_socket_directories" not in executor.command + assert "log_destination=stderr" in executor.command + + # Start and stop PostgreSQL to verify it works + assert_executor_start_stop(executor) + + +@pytest.mark.skipif( + platform.system() not in ("Linux", "FreeBSD"), + reason="Unix/Linux-specific test", +) +def test_actual_postgresql_start_unix( + request: FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + """Test that PostgreSQL actually starts on Unix/Linux with the new template. + + This integration test verifies that the Unix-specific command template + correctly starts PostgreSQL on actual Unix/Linux systems. + """ + config = get_config(request) + pg_exe = process._pg_exe(None, config) + port = process._pg_port(-1, config, []) + tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") + datadir, logfile_path = process._prepare_dir(tmpdir, port) + + executor = PostgreSQLExecutor( + executable=pg_exe, + host=config.host, + port=port, + datadir=str(datadir), + unixsocketdir=config.unixsocketdir, + logfile=str(logfile_path), + startparams=config.startparams, + dbname="test", + ) + + # Verify Unix template is used + assert "unix_socket_directories=" in executor.command + assert "log_destination='stderr'" in executor.command + + # Start and stop PostgreSQL to verify it works + assert_executor_start_stop(executor) + + +@pytest.mark.skipif(platform.system() != "Darwin", reason="Darwin/macOS-specific test") +def test_actual_postgresql_start_darwin( + request: FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + """Test that PostgreSQL actually starts on Darwin/macOS with the new template. + + This integration test verifies that the Unix template correctly starts + PostgreSQL on actual Darwin/macOS systems and uses the correct locale. + """ + config = get_config(request) + pg_exe = process._pg_exe(None, config) + port = process._pg_port(-1, config, []) + tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") + datadir, logfile_path = process._prepare_dir(tmpdir, port) + + executor = PostgreSQLExecutor( + executable=pg_exe, + host=config.host, + port=port, + datadir=str(datadir), + unixsocketdir=config.unixsocketdir, + logfile=str(logfile_path), + startparams=config.startparams, + dbname="test", + ) + + # Verify Unix template is used + assert "unix_socket_directories=" in executor.command + assert "log_destination='stderr'" in executor.command + + # Verify Darwin-specific locale is set + assert executor.envvars["LC_ALL"] == "en_US.UTF-8" + assert executor.envvars["LC_CTYPE"] == "en_US.UTF-8" + assert executor.envvars["LANG"] == "en_US.UTF-8" + + # Start and stop PostgreSQL to verify it works + assert_executor_start_stop(executor) diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py index 5e76a1a4..e8f52967 100644 --- a/tests/test_windows_compatibility.py +++ b/tests/test_windows_compatibility.py @@ -115,6 +115,346 @@ def test_unix_template_selected_on_linux(self) -> None: assert "unix_socket_directories='/tmp/socket'" in command assert "log_destination='stderr'" in command + def test_darwin_template_selection(self) -> None: + """Test that Darwin/macOS uses Unix template. + + macOS should use the same Unix template as Linux since it's a Unix-like + system and supports unix_socket_directories. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Darwin"): + executor = PostgreSQLExecutor( + executable="/opt/homebrew/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Darwin should use Unix template with unix_socket_directories and single quotes + assert "unix_socket_directories='/tmp/socket'" in command + assert "log_destination='stderr'" in command + + def test_darwin_locale_setting(self) -> None: + """Test that Darwin/macOS sets en_US.UTF-8 locale. + + Darwin requires en_US.UTF-8 instead of C.UTF-8 which is used on Linux. + This test verifies the locale environment variables are set correctly. + """ + # Patch the _LOCALE variable to simulate Darwin environment + with patch("pytest_postgresql.executor._LOCALE", "en_US.UTF-8"): + executor = PostgreSQLExecutor( + executable="/opt/homebrew/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + # Darwin should set en_US.UTF-8 locale + assert executor.envvars["LC_ALL"] == "en_US.UTF-8" + assert executor.envvars["LC_CTYPE"] == "en_US.UTF-8" + assert executor.envvars["LANG"] == "en_US.UTF-8" + + def test_postgres_options_with_single_quotes_unix(self) -> None: + """Test postgres_options containing single quotes on Unix. + + Single quotes in postgres_options should be preserved and passed through + to PostgreSQL on Unix systems. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + postgres_options="-c shared_buffers='128MB' -c work_mem='64MB'", + ) + + command = executor.command + # postgres_options should be included as-is with single quotes preserved + assert "-c shared_buffers='128MB' -c work_mem='64MB'" in command + + def test_postgres_options_with_single_quotes_windows(self) -> None: + """Test postgres_options containing single quotes on Windows. + + Single quotes in postgres_options should work on Windows since they're + inside the -o parameter's double quotes. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log", + startparams="-w", + dbname="test", + postgres_options="-c shared_buffers='128MB' -c work_mem='64MB'", + ) + + command = executor.command + # postgres_options should be included with single quotes preserved + assert "-c shared_buffers='128MB' -c work_mem='64MB'" in command + + def test_postgres_options_with_double_quotes(self) -> None: + """Test postgres_options containing double quotes. + + Double quotes in postgres_options need careful handling as they interact + with the shell's quote parsing. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + postgres_options='-c search_path="public,other"', + ) + + command = executor.command + # postgres_options with double quotes should be preserved + assert '-c search_path="public,other"' in command + + def test_postgres_options_with_paths_containing_spaces(self) -> None: + """Test postgres_options with file paths containing spaces. + + Config options that reference file paths with spaces should be properly + quoted within postgres_options. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + postgres_options="""-c config_file='/etc/postgres/my config.conf'""", + ) + + command = executor.command + # postgres_options with paths containing spaces should be preserved + assert """-c config_file='/etc/postgres/my config.conf'""" in command + + def test_empty_postgres_options(self) -> None: + """Test command generation with empty postgres_options. + + When postgres_options is empty (default), the command should still be + properly formatted without extra spaces or malformed syntax. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="-w", + dbname="test", + postgres_options="", + ) + + command = executor.command + # Command should still be valid with empty postgres_options + assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command + assert '-D "/tmp/data"' in command + assert "unix_socket_directories='/tmp/socket'" in command + # Should not have trailing space before closing quote in -o parameter + expected_opts = ( + '-o "-F -p 5432 -c log_destination=\'stderr\' ' + '-c logging_collector=off -c unix_socket_directories=\'/tmp/socket\' "' + ) + assert expected_opts in command + + def test_empty_startparams(self) -> None: + """Test command generation with empty startparams. + + When startparams is empty (default), the command should still be + properly formatted at the end. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket", + logfile="/tmp/log", + startparams="", + dbname="test", + ) + + command = executor.command + # Command should be valid with empty startparams + assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command + assert '-l "/tmp/log"' in command + # Command should not have trailing spaces at the end + assert not command.endswith(" ") + + def test_both_empty_postgres_options_and_startparams(self) -> None: + """Test command generation with both postgres_options and startparams empty. + + When both optional parameters are empty, the command should still + be properly formatted. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log", + startparams="", + dbname="test", + postgres_options="", + ) + + command = executor.command + # Command should be valid with both empty + assert "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start" in command + assert '-D "C:/temp/data"' in command + assert '-l "C:/temp/log"' in command + # Windows template should not have unix_socket_directories + assert "unix_socket_directories" not in command + + def test_unixsocketdir_ignored_on_windows_in_command(self) -> None: + """Test that unixsocketdir value doesn't appear in Windows command. + + Even when unixsocketdir is passed to the executor on Windows, its value + should not appear anywhere in the generated command since Windows doesn't + use unix_socket_directories. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/temp/data", + unixsocketdir="C:/this/should/not/appear", + logfile="C:/temp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # The unixsocketdir value should NOT appear in the Windows command + assert "C:/this/should/not/appear" not in command + assert "unix_socket_directories" not in command + + def test_paths_with_multiple_consecutive_spaces(self) -> None: + """Test paths with multiple consecutive spaces. + + Paths with multiple spaces should be properly quoted and preserved. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/my socket dir", + logfile="/tmp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Multiple spaces should be preserved + assert "unix_socket_directories='/tmp/my socket dir'" in command + + def test_paths_with_special_shell_characters(self) -> None: + """Test paths with special shell characters. + + Paths with shell metacharacters should be properly quoted to prevent + shell interpretation. Testing with ampersand, semicolon, and pipe. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/socket&test", + logfile="/tmp/log;file", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Special characters should be inside quotes + assert "unix_socket_directories='/tmp/socket&test'" in command + assert '-l "/tmp/log;file"' in command + + def test_paths_with_unicode_characters(self) -> None: + """Test paths with Unicode characters. + + Unicode characters in paths should be properly handled and preserved. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/data", + unixsocketdir="/tmp/sóckét_dïr_日本語", + logfile="/tmp/lög_文件.log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Unicode characters should be preserved + assert "unix_socket_directories='/tmp/sóckét_dïr_日本語'" in command + assert '-l "/tmp/lög_文件.log"' in command + + def test_command_with_all_special_characters_combined(self) -> None: + """Test command with multiple types of special characters. + + This comprehensive test combines spaces, quotes, special shell chars, + and Unicode to ensure the command handles complex real-world scenarios. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Linux"): + executor = PostgreSQLExecutor( + executable="/usr/lib/postgresql/16/bin/pg_ctl", + host="localhost", + port=5432, + datadir="/tmp/my data & files", + unixsocketdir="/tmp/sóckét dir (test)", + logfile="/tmp/log file; output.log", + startparams="-w -t 30", + dbname="test", + postgres_options="-c shared_buffers='256MB' -c config_file='/etc/pg/main.conf'", + ) + + command = executor.command + # All special characters should be properly handled + assert '-D "/tmp/my data & files"' in command + assert "unix_socket_directories='/tmp/sóckét dir (test)'" in command + assert '-l "/tmp/log file; output.log"' in command + assert "-c shared_buffers='256MB'" in command + assert "-c config_file='/etc/pg/main.conf'" in command + assert "-w -t 30" in command + class TestWindowsCompatibility: """Test Windows-specific process management functionality.""" @@ -299,3 +639,91 @@ def test_command_formatting_windows(self) -> None: assert "unix_socket_directories" not in command, ( f"unix_socket_directories should not be in Windows command: {command}" ) + + def test_windows_datadir_with_spaces(self) -> None: + """Test Windows datadir with spaces in path. + + Windows paths with spaces should be properly quoted with double quotes. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/Program Files/PostgreSQL/my data dir", + unixsocketdir="C:/temp/socket", + logfile="C:/temp/log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # datadir with spaces should be quoted + assert '-D "C:/Program Files/PostgreSQL/my data dir"' in command + + def test_windows_logfile_with_spaces(self) -> None: + """Test Windows logfile with spaces in path. + + Windows log file paths with spaces should be properly quoted with double quotes. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:/temp/data", + unixsocketdir="C:/temp/socket", + logfile="C:/Program Files/PostgreSQL/logs/my log file.log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # logfile with spaces should be quoted + assert '-l "C:/Program Files/PostgreSQL/logs/my log file.log"' in command + + def test_windows_unc_paths(self) -> None: + """Test Windows UNC (Universal Naming Convention) paths. + + UNC paths like \\\\server\\share should be properly handled on Windows. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe", + host="localhost", + port=5432, + datadir="//server/share/postgres/data", + unixsocketdir="//server/share/postgres/socket", + logfile="//server/share/postgres/logs/postgresql.log", + startparams="-w", + dbname="test", + ) + + command = executor.command + # UNC paths should be properly quoted (using forward slashes in Python) + assert '-D "//server/share/postgres/data"' in command + assert '-l "//server/share/postgres/logs/postgresql.log"' in command + + def test_windows_mixed_slashes(self) -> None: + """Test Windows paths with mixed forward and backslashes. + + Windows accepts both forward slashes and backslashes, and the command + should handle both properly. + """ + with patch("pytest_postgresql.executor.platform.system", return_value="Windows"): + executor = PostgreSQLExecutor( + executable="C:\\Program Files\\PostgreSQL\\bin\\pg_ctl.exe", + host="localhost", + port=5432, + datadir="C:\\temp\\data", + unixsocketdir="C:\\temp\\socket", + logfile="C:\\temp\\log.txt", + startparams="-w", + dbname="test", + ) + + command = executor.command + # Paths with backslashes should be properly quoted + assert 'C:\\Program Files\\PostgreSQL\\bin\\pg_ctl.exe start' in command + assert '-D "C:\\temp\\data"' in command + assert '-l "C:\\temp\\log.txt"' in command From ed614ee24a3d6461815ff78929daa0a169b606d2 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Mon, 16 Feb 2026 20:39:03 +0700 Subject: [PATCH 16/23] Add Windows locale setup fixture and update test cases for password handling - Introduced a pytest fixture to set Windows-compatible locale environment variables, ensuring compatibility with PostgreSQL's initdb on Windows. - Updated test cases for PostgreSQL start commands across Windows, Unix, and Darwin platforms to include a password parameter, enhancing test coverage and consistency. --- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_executor.py | 3 +++ 2 files changed, 23 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 10b5f39d..e3c21211 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,34 @@ """Tests main conftest file.""" import os +import platform from pathlib import Path +import pytest + from pytest_postgresql import factories pytest_plugins = ["pytester"] POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13") +@pytest.fixture(scope="session", autouse=True) +def setup_windows_locale() -> None: + """Set Windows-compatible locale environment variables. + + Windows doesn't support Unix-style locales like C.UTF-8 or en_US.UTF-8. + PostgreSQL's initdb requires valid locale settings, so we set the 'C' + locale which is supported on Windows. + + This fixture runs automatically for all test sessions on Windows. + """ + if platform.system() == "Windows": + # Set Windows-compatible locale (C locale is supported on Windows) + os.environ["LC_ALL"] = "C" + os.environ["LC_CTYPE"] = "C" + os.environ["LANG"] = "C" + + TEST_SQL_DIR = os.path.dirname(os.path.abspath(__file__)) + "/test_sql/" TEST_SQL_FILE = Path(TEST_SQL_DIR + "test.sql") TEST_SQL_FILE2 = Path(TEST_SQL_DIR + "test2.sql") diff --git a/tests/test_executor.py b/tests/test_executor.py index f1973f97..e11d1c15 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -303,6 +303,7 @@ def test_actual_postgresql_start_windows( unixsocketdir=config.unixsocketdir, logfile=str(logfile_path), startparams=config.startparams, + password="testpass", dbname="test", ) @@ -341,6 +342,7 @@ def test_actual_postgresql_start_unix( unixsocketdir=config.unixsocketdir, logfile=str(logfile_path), startparams=config.startparams, + password="testpass", dbname="test", ) @@ -376,6 +378,7 @@ def test_actual_postgresql_start_darwin( unixsocketdir=config.unixsocketdir, logfile=str(logfile_path), startparams=config.startparams, + password="testpass", dbname="test", ) From 679c4d4129d98d446da71e1eda2c1d3f2c442723 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Mon, 16 Feb 2026 20:50:11 +0700 Subject: [PATCH 17/23] Update locale handling in executor.py and remove Windows locale setup fixture from conftest.py - Modified locale settings in executor.py to ensure compatibility across Darwin and Windows platforms. - Removed the Windows locale setup fixture from conftest.py as the locale handling is now managed directly in executor.py, simplifying the test configuration. --- pytest_postgresql/executor.py | 4 ++++ tests/conftest.py | 20 -------------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py index dc2cb641..d12aef17 100644 --- a/pytest_postgresql/executor.py +++ b/pytest_postgresql/executor.py @@ -39,7 +39,11 @@ _LOCALE = "C.UTF-8" if platform.system() == "Darwin": + # Darwin does not have C.UTF-8, but en_US.UTF-8 is always available _LOCALE = "en_US.UTF-8" +elif platform.system() == "Windows": + # Windows doesn't support C.UTF-8 or en_US.UTF-8, use plain "C" locale + _LOCALE = "C" T = TypeVar("T", bound="PostgreSQLExecutor") diff --git a/tests/conftest.py b/tests/conftest.py index e3c21211..10b5f39d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,34 +1,14 @@ """Tests main conftest file.""" import os -import platform from pathlib import Path -import pytest - from pytest_postgresql import factories pytest_plugins = ["pytester"] POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13") -@pytest.fixture(scope="session", autouse=True) -def setup_windows_locale() -> None: - """Set Windows-compatible locale environment variables. - - Windows doesn't support Unix-style locales like C.UTF-8 or en_US.UTF-8. - PostgreSQL's initdb requires valid locale settings, so we set the 'C' - locale which is supported on Windows. - - This fixture runs automatically for all test sessions on Windows. - """ - if platform.system() == "Windows": - # Set Windows-compatible locale (C locale is supported on Windows) - os.environ["LC_ALL"] = "C" - os.environ["LC_CTYPE"] = "C" - os.environ["LANG"] = "C" - - TEST_SQL_DIR = os.path.dirname(os.path.abspath(__file__)) + "/test_sql/" TEST_SQL_FILE = Path(TEST_SQL_DIR + "test.sql") TEST_SQL_FILE2 = Path(TEST_SQL_DIR + "test2.sql") From 5f754550d1c804339ecd82c05f375260c1ce83a4 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Mon, 16 Feb 2026 21:26:45 +0700 Subject: [PATCH 18/23] Refactor socket directory handling in test_executor.py for PostgreSQLExecutor - Updated the test case to create a dedicated socket directory for Unix systems, improving clarity and organization in the test setup. - Adjusted the path for the `unixsocketdir` parameter to use the newly created socket directory, ensuring proper configuration for PostgreSQLExecutor. --- tests/test_executor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_executor.py b/tests/test_executor.py index e11d1c15..27e22749 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -196,13 +196,17 @@ def test_executor_with_special_chars_in_all_paths( tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") / "my test dir" tmpdir.mkdir(exist_ok=True) datadir, logfile_path = process._prepare_dir(tmpdir, port) + + # Create the socket directory for Unix systems + socket_dir = tmpdir / "socket dir" + socket_dir.mkdir(parents=True, exist_ok=True) executor = PostgreSQLExecutor( executable=pg_exe, host=config.host, port=port, datadir=str(datadir), - unixsocketdir=str(tmpdir / "socket dir"), + unixsocketdir=str(socket_dir), logfile=str(logfile_path), startparams=config.startparams, password="test pass", From 9de133ba03e9914f618b051a9d060c1e63c60f1d Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 17 Feb 2026 01:24:55 +0700 Subject: [PATCH 19/23] Update path handling for pytest uploads in single-postgres.yml - Modified the upload path for pytest results to include both the temporary runner directory and a specific /tmp directory, ensuring comprehensive coverage of test artifacts during the workflow execution. --- .github/workflows/single-postgres.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index a2181e98..a305ed24 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -109,7 +109,9 @@ jobs: if: failure() with: name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }} - path: ${{ runner.temp }}/pytest-of-runner/** + path: | + /tmp/pytest-of-runner/** + ${{ runner.temp }}/pytest-of-runner/** - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: From 0b77a5c8f6a44b7a3eb3e4a33a09af7ba2a041fb Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 17 Feb 2026 02:27:29 +0700 Subject: [PATCH 20/23] Update pytest command options in single-postgres.yml to include --basetemp for improved temporary directory handling - Added the --basetemp option to pytest commands to specify a base temporary directory, enhancing the management of test artifacts during execution. - Simplified the upload path for pytest results to focus on the temporary runner directory, ensuring efficient artifact collection. --- .github/workflows/single-postgres.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index a305ed24..7acfde23 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -100,18 +100,16 @@ jobs: - name: Run test uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: - command: pytest -svv -p no:xdist --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml + command: pytest -svv -p no:xdist --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml --basetemp="${{ runner.temp }}" - name: Run xdist test uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: - command: pytest -n auto --dist loadgroup --max-worker-restart 0 --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml:coverage-xdist.xml + command: pytest -n auto --dist loadgroup --max-worker-restart 0 --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml:coverage-xdist.xml --basetemp="${{ runner.temp }}" - uses: actions/upload-artifact@v6 if: failure() with: name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }} - path: | - /tmp/pytest-of-runner/** - ${{ runner.temp }}/pytest-of-runner/** + path: ${{ runner.temp }}/pytest-of-*/** - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: From f872e7f931d8406b2f94c3bd74a018ce85177280 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 17 Feb 2026 04:43:32 +0700 Subject: [PATCH 21/23] Refine pytest upload path in single-postgres.yml for improved artifact collection - Simplified the upload path for pytest results by removing the specific wildcard in the temporary directory, ensuring more efficient collection of test artifacts during the workflow execution. --- .github/workflows/single-postgres.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index 7acfde23..8c1d4669 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -109,7 +109,7 @@ jobs: if: failure() with: name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }} - path: ${{ runner.temp }}/pytest-of-*/** + path: ${{ runner.temp }}/pytest-*/** - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: From 048ed28fb5e0b744f8285aa74872c8c725217e1a Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 17 Feb 2026 05:41:09 +0700 Subject: [PATCH 22/23] Update workflows to use pipenv-setup@v4.4.0 with editable flag Upgraded fizyk/actions-reuse pipenv-setup action from v4.2.1 to v4.4.0 which includes built-in support for editable package installation via the editable flag. This simplifies the workflows by removing the explicit pip install -e . steps. Changes: - Updated pipenv-setup to v4.4.0 in all workflow files - Added editable: true parameter to pipenv-setup steps - Removed separate "Install package in editable mode" steps from dockerised-postgres.yml, single-postgres.yml, and oldest-postgres.yml This aligns with the upstream pattern established in pytest-mongo and reduces workflow complexity while maintaining the same functionality. Co-authored-by: Cursor --- .github/workflows/dockerised-postgres.yml | 7 ++----- .github/workflows/oldest-postgres.yml | 7 ++----- .github/workflows/single-postgres.yml | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dockerised-postgres.yml b/.github/workflows/dockerised-postgres.yml index b3346f10..4b913836 100644 --- a/.github/workflows/dockerised-postgres.yml +++ b/.github/workflows/dockerised-postgres.yml @@ -50,14 +50,11 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Pipenv on python ${{ matrix.python-version }} - uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.2.1 + uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install package in editable mode - uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 - with: - command: pip install -e . + editable: true - name: Run test noproc fixture on docker uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 with: diff --git a/.github/workflows/oldest-postgres.yml b/.github/workflows/oldest-postgres.yml index e1b62778..9f1ce95b 100644 --- a/.github/workflows/oldest-postgres.yml +++ b/.github/workflows/oldest-postgres.yml @@ -36,11 +36,12 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Pipenv on python ${{ matrix.python-version }} - uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.2.1 + uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0 with: python-version: ${{ matrix.python-version }} cache: false allow-prereleases: true + editable: true - uses: ankane/setup-postgres@v1 with: postgres-version: ${{ inputs.postgresql }} @@ -58,10 +59,6 @@ jobs: uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1 with: command: pip install -r oldest/requirements.txt - - name: Install package in editable mode - uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 - with: - command: pip install -e . --no-deps - name: Run tests without xdist uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1 with: diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index f9c82496..c11411a2 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -36,14 +36,11 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Pipenv on python ${{ matrix.python-version }} - uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.2.1 + uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install package in editable mode - uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 - with: - command: pip install -e . + editable: true - uses: ankane/setup-postgres@v1 with: postgres-version: ${{ inputs.postgresql }} From 2b2c75c30129f0300b712715dcb869090491ff84 Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 17 Feb 2026 06:18:10 +0700 Subject: [PATCH 23/23] Update workflow files to use pipenv-run@v4.2.1 and refine conditional checks - Upgraded the pipenv-run action to v4.2.1 in dockerised-postgres.yml, single-postgres.yml, and oldest-postgres.yml for consistency across workflows. - Refined the conditional check for installing libpq to use matrix.python-version instead of inputs.python-versions in oldest-postgres.yml and single-postgres.yml, improving clarity and accuracy. These changes enhance the maintainability and consistency of the workflow configurations. --- .github/workflows/dockerised-postgres.yml | 2 +- .github/workflows/oldest-postgres.yml | 2 +- .github/workflows/single-postgres.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dockerised-postgres.yml b/.github/workflows/dockerised-postgres.yml index 4b913836..678a988a 100644 --- a/.github/workflows/dockerised-postgres.yml +++ b/.github/workflows/dockerised-postgres.yml @@ -56,7 +56,7 @@ jobs: allow-prereleases: true editable: true - name: Run test noproc fixture on docker - uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.1.1 + uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1 with: command: pytest -n 0 --max-worker-restart 0 -k docker --postgresql-host=localhost --postgresql-port 5433 --postgresql-password=postgres --cov-report=xml:coverage-docker.xml - name: Upload coverage to Codecov diff --git a/.github/workflows/oldest-postgres.yml b/.github/workflows/oldest-postgres.yml index 9f1ce95b..2dc5b00e 100644 --- a/.github/workflows/oldest-postgres.yml +++ b/.github/workflows/oldest-postgres.yml @@ -53,7 +53,7 @@ jobs: run: | sudo locale-gen de_DE.UTF-8 - name: install libpq - if: ${{ contains(inputs.python-versions, 'pypy') }} + if: ${{ contains(matrix.python-version, 'pypy') && runner.os == 'Linux' }} run: sudo apt install libpq5 - name: Install oldest supported versions uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1 diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml index c11411a2..13d9c930 100644 --- a/.github/workflows/single-postgres.yml +++ b/.github/workflows/single-postgres.yml @@ -92,7 +92,7 @@ jobs: run: | sudo locale-gen de_DE.UTF-8 - name: install libpq - if: ${{ contains(inputs.python-versions, 'pypy') && runner.os == 'Linux' }} + if: ${{ contains(matrix.python-version, 'pypy') && runner.os == 'Linux' }} run: sudo apt install libpq5 - name: Run test uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1