Skip to content

Commit b884e02

Browse files
Merge pull request #1 from tboy1337/edit
Add Windows Compatibility
2 parents 39bedfa + 2b2c75c commit b884e02

9 files changed

Lines changed: 1103 additions & 17 deletions

File tree

.github/workflows/dockerised-postgres.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,16 @@ jobs:
4949

5050
steps:
5151
- uses: actions/checkout@v6
52-
- name: Run test noproc fixture on docker
53-
uses: fizyk/actions-reuse/.github/actions/pipenv@v4.2.1
52+
- name: Set up Pipenv on python ${{ matrix.python-version }}
53+
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0
5454
with:
5555
python-version: ${{ matrix.python-version }}
56-
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
5756
allow-prereleases: true
57+
editable: true
58+
- name: Run test noproc fixture on docker
59+
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1
60+
with:
61+
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
5862
- name: Upload coverage to Codecov
5963
uses: codecov/codecov-action@v5.5.2
6064
with:

.github/workflows/oldest-postgres.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ jobs:
3636
steps:
3737
- uses: actions/checkout@v6
3838
- name: Set up Pipenv on python ${{ matrix.python-version }}
39-
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.2.1
39+
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0
4040
with:
4141
python-version: ${{ matrix.python-version }}
4242
cache: false
4343
allow-prereleases: true
44+
editable: true
4445
- uses: ankane/setup-postgres@v1
4546
with:
4647
postgres-version: ${{ inputs.postgresql }}
@@ -52,7 +53,7 @@ jobs:
5253
run: |
5354
sudo locale-gen de_DE.UTF-8
5455
- name: install libpq
55-
if: ${{ contains(inputs.python-versions, 'pypy') }}
56+
if: ${{ contains(matrix.python-version, 'pypy') && runner.os == 'Linux' }}
5657
run: sudo apt install libpq5
5758
- name: Install oldest supported versions
5859
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1

.github/workflows/single-postgres.yml

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,77 @@ jobs:
3636
steps:
3737
- uses: actions/checkout@v6
3838
- name: Set up Pipenv on python ${{ matrix.python-version }}
39-
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.2.1
39+
uses: fizyk/actions-reuse/.github/actions/pipenv-setup@v4.4.0
4040
with:
4141
python-version: ${{ matrix.python-version }}
4242
allow-prereleases: true
43+
editable: true
4344
- uses: ankane/setup-postgres@v1
4445
with:
4546
postgres-version: ${{ inputs.postgresql }}
47+
- name: Detect PostgreSQL path on Windows
48+
if: runner.os == 'Windows'
49+
shell: pwsh
50+
run: |
51+
$pgPath = "C:\Program Files\PostgreSQL\${{ inputs.postgresql }}\bin\pg_ctl.exe"
52+
if (Test-Path $pgPath) {
53+
echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV
54+
} else {
55+
$pgPath = (Get-Command pg_ctl -ErrorAction SilentlyContinue).Source
56+
if ($pgPath) {
57+
echo "POSTGRESQL_EXEC=$pgPath" >> $env:GITHUB_ENV
58+
}
59+
}
60+
61+
# Verify that PostgreSQL was found
62+
if (-not $pgPath) {
63+
Write-Error "Error: pg_ctl not found in expected locations. Checked hardcoded path and system PATH."
64+
exit 1
65+
}
66+
- name: Set PostgreSQL path for Unix/macOS
67+
if: runner.os != 'Windows'
68+
run: |
69+
# Try to find pg_ctl dynamically for cross-platform compatibility
70+
if command -v pg_ctl >/dev/null 2>&1; then
71+
PG_CTL_PATH=$(command -v pg_ctl)
72+
echo "POSTGRESQL_EXEC=$PG_CTL_PATH" >> $GITHUB_ENV
73+
elif [ -f "/opt/homebrew/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" ]; then
74+
# macOS Apple Silicon Homebrew path
75+
echo "POSTGRESQL_EXEC=/opt/homebrew/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV
76+
elif [ -f "/usr/local/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" ]; then
77+
# macOS Intel Homebrew path
78+
echo "POSTGRESQL_EXEC=/usr/local/opt/postgresql@${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV
79+
elif [ -f "/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" ]; then
80+
# Debian/Ubuntu path (fallback)
81+
echo "POSTGRESQL_EXEC=/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" >> $GITHUB_ENV
82+
else
83+
echo "Error: pg_ctl not found in expected locations"
84+
exit 1
85+
fi
4686
- name: Check installed locales
87+
if: runner.os != 'Windows'
4788
run: |
4889
locale -a
4990
- name: update locale for tests
5091
if: ${{ inputs.os == 'ubuntu-latest' }}
5192
run: |
5293
sudo locale-gen de_DE.UTF-8
5394
- name: install libpq
54-
if: ${{ contains(inputs.python-versions, 'pypy') }}
95+
if: ${{ contains(matrix.python-version, 'pypy') && runner.os == 'Linux' }}
5596
run: sudo apt install libpq5
5697
- name: Run test
5798
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1
5899
with:
59-
command: pytest -svv -p no:xdist --postgresql-exec="/usr/lib/postgresql/${{ inputs.postgresql }}/bin/pg_ctl" -k "not docker" --cov-report=xml
100+
command: pytest -svv -p no:xdist --postgresql-exec="${{ env.POSTGRESQL_EXEC }}" -k "not docker" --cov-report=xml --basetemp="${{ runner.temp }}"
60101
- name: Run xdist test
61102
uses: fizyk/actions-reuse/.github/actions/pipenv-run@v4.2.1
62103
with:
63-
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
104+
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 }}"
64105
- uses: actions/upload-artifact@v6
65106
if: failure()
66107
with:
67108
name: postgresql-${{ matrix.python-version }}-${{ inputs.postgresql }}
68-
path: /tmp/pytest-of-runner/**
109+
path: ${{ runner.temp }}/pytest-*/**
69110
- name: Upload coverage to Codecov
70111
uses: codecov/codecov-action@v5.5.2
71112
with:

.github/workflows/tests.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ jobs:
5959
postgresql: 16
6060
os: macos-latest
6161
python-versions: '["3.13", "3.14"]'
62+
windows_postgres_18:
63+
needs: [postgresql_18]
64+
uses: ./.github/workflows/single-postgres.yml
65+
with:
66+
postgresql: 18
67+
os: windows-latest
68+
python-versions: '["3.12", "3.13", "3.14"]'
69+
windows_postgres_17:
70+
needs: [postgresql_17, windows_postgres_18]
71+
uses: ./.github/workflows/single-postgres.yml
72+
with:
73+
postgresql: 17
74+
os: windows-latest
75+
python-versions: '["3.12", "3.13", "3.14"]'
76+
windows_postgres_16:
77+
needs: [postgresql_16, windows_postgres_17]
78+
uses: ./.github/workflows/single-postgres.yml
79+
with:
80+
postgresql: 16
81+
os: windows-latest
82+
python-versions: '["3.13", "3.14"]'
6283
docker_postgresql_18:
6384
needs: [postgresql_18]
6485
uses: ./.github/workflows/dockerised-postgres.yml

pytest_postgresql/executor.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
# along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
1818
"""PostgreSQL executor crafter around pg_ctl."""
1919

20+
import logging
21+
import os
2022
import os.path
2123
import platform
2224
import re
@@ -32,10 +34,16 @@
3234

3335
from pytest_postgresql.exceptions import ExecutableMissingException, PostgreSQLUnsupported
3436

37+
logger = logging.getLogger(__name__)
38+
3539
_LOCALE = "C.UTF-8"
3640

3741
if platform.system() == "Darwin":
42+
# Darwin does not have C.UTF-8, but en_US.UTF-8 is always available
3843
_LOCALE = "en_US.UTF-8"
44+
elif platform.system() == "Windows":
45+
# Windows doesn't support C.UTF-8 or en_US.UTF-8, use plain "C" locale
46+
_LOCALE = "C"
3947

4048

4149
T = TypeVar("T", bound="PostgreSQLExecutor")
@@ -48,14 +56,29 @@ class PostgreSQLExecutor(TCPExecutor):
4856
<http://www.postgresql.org/docs/current/static/app-pg-ctl.html>`_
4957
"""
5058

51-
BASE_PROC_START_COMMAND = (
59+
# Unix command template - uses single quotes for PostgreSQL config value quoting
60+
# which protects paths with spaces in unix_socket_directories.
61+
# On Unix, mirakuru uses shlex.split() with shell=False, so single quotes
62+
# inside double-quoted strings are preserved and passed to PostgreSQL's config parser.
63+
UNIX_PROC_START_COMMAND = (
5264
'{executable} start -D "{datadir}" '
5365
"-o \"-F -p {port} -c log_destination='stderr' "
5466
"-c logging_collector=off "
5567
"-c unix_socket_directories='{unixsocketdir}' {postgres_options}\" "
5668
'-l "{logfile}" {startparams}'
5769
)
5870

71+
# Windows command template - no single quotes (cmd.exe treats them as literals,
72+
# not delimiters) and unix_socket_directories is omitted entirely since PostgreSQL
73+
# ignores it on Windows. On Windows, mirakuru forces shell=True so the command
74+
# goes through cmd.exe.
75+
WINDOWS_PROC_START_COMMAND = (
76+
'{executable} start -D "{datadir}" '
77+
'-o "-F -p {port} -c log_destination=stderr '
78+
'-c logging_collector=off {postgres_options}" '
79+
'-l "{logfile}" {startparams}'
80+
)
81+
5982
VERSION_RE = re.compile(r".* (?P<version>\d+(?:\.\d+)?)")
6083
MIN_SUPPORTED_VERSION = parse("14")
6184

@@ -108,7 +131,11 @@ def __init__(
108131
self.logfile = logfile
109132
self.startparams = startparams
110133
self.postgres_options = postgres_options
111-
command = self.BASE_PROC_START_COMMAND.format(
134+
if platform.system() == "Windows":
135+
command_template = self.WINDOWS_PROC_START_COMMAND
136+
else:
137+
command_template = self.UNIX_PROC_START_COMMAND
138+
command = command_template.format(
112139
executable=self.executable,
113140
datadir=self.datadir,
114141
port=port,
@@ -219,17 +246,57 @@ def running(self) -> bool:
219246
status_code = subprocess.getstatusoutput(f'{self.executable} status -D "{self.datadir}"')[0]
220247
return status_code == 0
221248

249+
def _windows_terminate_process(self, _sig: Optional[int] = None) -> None:
250+
"""Terminate process on Windows.
251+
252+
:param _sig: Signal parameter (unused on Windows but included for consistency)
253+
"""
254+
if self.process is None:
255+
return
256+
257+
try:
258+
# On Windows, try to terminate gracefully first
259+
self.process.terminate()
260+
# Give it a chance to terminate gracefully
261+
try:
262+
self.process.wait(timeout=5)
263+
except subprocess.TimeoutExpired:
264+
# If it doesn't terminate gracefully, force kill
265+
self.process.kill()
266+
try:
267+
self.process.wait(timeout=10)
268+
except subprocess.TimeoutExpired:
269+
logger.warning(
270+
"Process %s could not be cleaned up after kill() and may be a zombie process",
271+
self.process.pid if self.process else "unknown",
272+
)
273+
except (OSError, AttributeError) as e:
274+
# Process might already be dead or other issues
275+
logger.debug(
276+
"Exception during Windows process termination: %s: %s",
277+
type(e).__name__,
278+
e,
279+
)
280+
222281
def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T:
223282
"""Issue a stop request to executable."""
224283
subprocess.check_output(
225-
f'{self.executable} stop -D "{self.datadir}" -m f',
226-
shell=True,
284+
[self.executable, "stop", "-D", self.datadir, "-m", "f"],
227285
)
228286
try:
229-
super().stop(sig, exp_sig)
287+
if platform.system() == "Windows":
288+
self._windows_terminate_process(sig)
289+
else:
290+
super().stop(sig, exp_sig)
230291
except ProcessFinishedWithError:
231292
# Finished, leftovers ought to be cleaned afterwards anyway
232293
pass
294+
except AttributeError:
295+
# Fallback for edge cases where os.killpg doesn't exist (e.g., Windows)
296+
if not hasattr(os, "killpg"):
297+
self._windows_terminate_process(sig)
298+
else:
299+
raise
233300
return self
234301

235302
def __del__(self) -> None:

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from pathlib import Path
55

66
from pytest_postgresql import factories
7-
from pytest_postgresql.plugin import * # noqa: F403,F401
87

98
pytest_plugins = ["pytester"]
109
POSTGRESQL_VERSION = os.environ.get("POSTGRES", "13")

0 commit comments

Comments
 (0)