Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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
10 changes: 7 additions & 3 deletions .github/workflows/dockerised-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,16 @@ jobs:

steps:
- uses: actions/checkout@v6
- name: Run test noproc fixture on docker
uses: fizyk/actions-reuse/.github/actions/pipenv@v4.2.1
- name: Set up Pipenv on python ${{ matrix.python-version }}
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0
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
editable: true
- name: Run test noproc fixture on docker
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
uses: codecov/codecov-action@v5.5.2
with:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/oldest-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -52,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
Expand Down
51 changes: 46 additions & 5 deletions .github/workflows/single-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,36 +36,77 @@ 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
editable: true
- 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 }}"
- 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 }}"
- uses: actions/upload-artifact@v6
if: failure()
with:
name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }}
path: /tmp/pytest-of-runner/**
path: ${{ runner.temp }}/pytest-*/**
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.5.2
with:
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 72 additions & 5 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,14 +56,29 @@ 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}\" "
'-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}'
)

VERSION_RE = re.compile(r".* (?P<version>\d+(?:\.\d+)?)")
MIN_SUPPORTED_VERSION = parse("14")

Expand Down Expand Up @@ -108,7 +131,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,
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
Loading