Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f1f6d30
Refactor PostgreSQLExecutor to support Windows compatibility for proc…
tboy1337 Jan 29, 2026
13be802
Enhance PostgreSQL workflow for Windows compatibility and streamline …
tboy1337 Feb 11, 2026
909961f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 11, 2026
4e3594b
Enhance PostgreSQL workflow and executor for improved cross-platform …
tboy1337 Feb 11, 2026
bd83393
Merge branch 'edit' of https://github.com/tboy1337/pytest-postgresql …
tboy1337 Feb 11, 2026
5611d22
Refactor PostgreSQLExecutor and enhance Windows compatibility in tests
tboy1337 Feb 11, 2026
bb03b4b
Improve process termination handling in PostgreSQLExecutor and refine…
tboy1337 Feb 11, 2026
7a9e6a0
Remove unnecessary import in conftest.py as the plugin is registered …
tboy1337 Feb 12, 2026
91e8f2b
Update GitHub workflows to include editable package installation
tboy1337 Feb 15, 2026
2a8e575
Update oldest-postgres.yml to install package without dependencies
tboy1337 Feb 15, 2026
035d295
Enhance PostgreSQL workflow error handling
tboy1337 Feb 15, 2026
58f0d8d
Refactor PostgreSQLExecutor command templates for platform compatibility
tboy1337 Feb 15, 2026
14eb2fb
Fix PostgreSQL path in Windows workflow
tboy1337 Feb 15, 2026
ca053a7
Update pytest configuration in test_postgres_options_plugin.py
tboy1337 Feb 15, 2026
4498275
Update pytest_plugins declaration in test_postgres_options_plugin.py …
tboy1337 Feb 15, 2026
9344cc4
Enhance platform-specific command templates and test coverage for Pos…
tboy1337 Feb 16, 2026
ed614ee
Add Windows locale setup fixture and update test cases for password h…
tboy1337 Feb 16, 2026
679c4d4
Update locale handling in executor.py and remove Windows locale setup…
tboy1337 Feb 16, 2026
5f75455
Refactor socket directory handling in test_executor.py for PostgreSQL…
tboy1337 Feb 16, 2026
9de133b
Update path handling for pytest uploads in single-postgres.yml
tboy1337 Feb 16, 2026
0b77a5c
Update pytest command options in single-postgres.yml to include --bas…
tboy1337 Feb 16, 2026
f872e7f
Refine pytest upload path in single-postgres.yml for improved artifac…
tboy1337 Feb 16, 2026
b71a3c3
Merge branch 'main' into edit
tboy1337 Feb 16, 2026
048ed28
Update workflows to use pipenv-setup@v4.4.0 with editable flag
tboy1337 Feb 16, 2026
2b2c75c
Update workflow files to use pipenv-run@v4.2.1 and refine conditional…
tboy1337 Feb 16, 2026
b962c71
Refactor socket directory handling in test_executor.py and clean up t…
tboy1337 Feb 23, 2026
7f1e310
Fix formatting issues in PostgreSQL command templates and update test…
tboy1337 Feb 23, 2026
45b29e9
Update test assertions for PostgreSQL command formatting in test_exec…
tboy1337 Feb 23, 2026
7500137
Add FreeBSD to platform parameterization in test_executor.py
tboy1337 Feb 23, 2026
03911ab
Fixed trailing whitespace in single-postgres.yml and test_executor.py
tboy1337 Mar 1, 2026
39b6003
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 1, 2026
0cf1075
Refine pytest temporary directory handling in single-postgres.yml
tboy1337 Mar 1, 2026
403d31b
Merge branch 'edit' of https://github.com/tboy1337/pytest-postgresql …
tboy1337 Mar 1, 2026
d5aec7a
Merge branch 'main' into edit
fizyk Mar 1, 2026
fa890e8
Update test documentation for Windows UNC paths in test_windows_compa…
tboy1337 Mar 3, 2026
2edeb34
Merge branch 'edit' of https://github.com/tboy1337/pytest-postgresql …
tboy1337 Mar 3, 2026
f5cc98c
Remove editable package changes extracted to separate PR
tboy1337 Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/oldest-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,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
Expand Down
48 changes: 44 additions & 4 deletions .github/workflows/single-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,69 @@ 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.exe"
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
}
}

# 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: |
# 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: |
locale -a
- name: update locale for tests
if: ${{ inputs.os == 'ubuntu-latest' }}
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: Run test
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.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 --basetemp="${{ runner.temp }}/pytest-basetemp"
- name: Run xdist test
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.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 --basetemp="${{ runner.temp }}/pytest-basetemp"
- uses: actions/upload-artifact@v7
if: failure()
with:
name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }}
path: /tmp/pytest-of-runner/**
path: ${{ runner.temp }}/pytest-basetemp/**
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.5.2
with:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ jobs:
python-versions: '["3.13", "3.14"]'
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
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"]'
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
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"]'
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
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"]'
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
docker_postgresql_18:
needs: [postgresql_18]
uses: ./.github/workflows/dockerised-postgres.yml
Expand Down
1 change: 1 addition & 0 deletions newsfragments/1182.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Windows compatibility for ``PostgreSQLExecutor`` with platform-specific command templates (``UNIX_PROC_START_COMMAND`` / ``WINDOWS_PROC_START_COMMAND``) and a dedicated ``_windows_terminate_process`` method for graceful process termination on Windows.
81 changes: 74 additions & 7 deletions pytest_postgresql/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
"""PostgreSQL executor crafter around pg_ctl."""

import logging
import os
import os.path
import platform
import re
Expand All @@ -32,10 +34,16 @@

from pytest_postgresql.exceptions import ExecutableMissingException, PostgreSQLUnsupported

logger = logging.getLogger(__name__)

_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")
Expand All @@ -48,11 +56,26 @@ class PostgreSQLExecutor(TCPExecutor):
<http://www.postgresql.org/docs/current/static/app-pg-ctl.html>`_
"""

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' "
"-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}'
)

Expand Down Expand Up @@ -108,14 +131,18 @@ 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,
unixsocketdir=self.unixsocketdir,
logfile=self.logfile,
startparams=self.startparams,
postgres_options=self.postgres_options,
postgres_options=f" {self.postgres_options}" if self.postgres_options else "",
)
super().__init__(
command,
Expand Down Expand Up @@ -219,17 +246,57 @@ 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()
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(
"Exception during Windows process termination: %s: %s",
type(e).__name__,
e,
)

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:
# 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
return self

def __del__(self) -> None:
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path

from pytest_postgresql import factories
from pytest_postgresql.plugin import * # noqa: F403,F401

pytest_plugins = ["pytester"]
POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13")
Expand Down
Loading