Skip to content

Commit efa4633

Browse files
committed
feat: add Windows compatibility fixes for PostgreSQL executor
1 parent 2b244db commit efa4633

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

pytest_postgresql/executor.py

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

20+
import os
2021
import os.path
2122
import platform
2223
import re
2324
import shutil
25+
import signal
2426
import subprocess
2527
import tempfile
2628
import time
@@ -48,13 +50,20 @@ class PostgreSQLExecutor(TCPExecutor):
4850
<http://www.postgresql.org/docs/current/static/app-pg-ctl.html>`_
4951
"""
5052

51-
BASE_PROC_START_COMMAND = (
52-
'{executable} start -D "{datadir}" '
53-
"-o \"-F -p {port} -c log_destination='stderr' "
54-
"-c logging_collector=off "
55-
"-c unix_socket_directories='{unixsocketdir}' {postgres_options}\" "
56-
'-l "{logfile}" {startparams}'
57-
)
53+
def _get_base_command(self) -> str:
54+
"""Get the base PostgreSQL command, cross-platform compatible."""
55+
# Use unified format without single quotes around values
56+
# This format works on both Windows and Unix systems since PostgreSQL
57+
# configuration values without spaces don't require quotes
58+
return (
59+
'{executable} start -D "{datadir}" '
60+
'-o "-F -p {port} -c log_destination=stderr '
61+
"-c logging_collector=off "
62+
'-c unix_socket_directories={unixsocketdir} {postgres_options}" '
63+
'-l "{logfile}" {startparams}'
64+
)
65+
66+
BASE_PROC_START_COMMAND = "" # Will be set dynamically
5867

5968
VERSION_RE = re.compile(r".* (?P<version>\d+(?:\.\d+)?)")
6069
MIN_SUPPORTED_VERSION = parse("14")
@@ -108,7 +117,7 @@ def __init__(
108117
self.logfile = logfile
109118
self.startparams = startparams
110119
self.postgres_options = postgres_options
111-
command = self.BASE_PROC_START_COMMAND.format(
120+
command = self._get_base_command().format(
112121
executable=self.executable,
113122
datadir=self.datadir,
114123
port=port,
@@ -219,17 +228,58 @@ def running(self) -> bool:
219228
status_code = subprocess.getstatusoutput(f'{self.executable} status -D "{self.datadir}"')[0]
220229
return status_code == 0
221230

231+
def _windows_terminate_process(self, sig: Optional[int] = None) -> None:
232+
"""Terminate process on Windows."""
233+
if self.process is None:
234+
return
235+
236+
try:
237+
# On Windows, try to terminate gracefully first
238+
self.process.terminate()
239+
# Give it a chance to terminate gracefully
240+
try:
241+
self.process.wait(timeout=5)
242+
except subprocess.TimeoutExpired:
243+
# If it doesn't terminate gracefully, force kill
244+
self.process.kill()
245+
self.process.wait()
246+
except (OSError, AttributeError):
247+
# Process might already be dead or other issues
248+
pass
249+
250+
def _unix_terminate_process(self, sig: Optional[int] = None) -> None:
251+
"""Terminate process on Unix systems."""
252+
if self.process is None:
253+
return
254+
255+
try:
256+
# On Unix systems, use the signal
257+
actual_sig = sig or signal.SIGTERM
258+
os.killpg(self.process.pid, actual_sig)
259+
except (OSError, AttributeError):
260+
# Process might already be dead or other issues
261+
pass
262+
222263
def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T:
223264
"""Issue a stop request to executable."""
224265
subprocess.check_output(
225266
f'{self.executable} stop -D "{self.datadir}" -m f',
226267
shell=True,
227268
)
228269
try:
229-
super().stop(sig, exp_sig)
270+
if platform.system() == "Windows":
271+
self._windows_terminate_process(sig)
272+
else:
273+
super().stop(sig, exp_sig)
230274
except ProcessFinishedWithError:
231275
# Finished, leftovers ought to be cleaned afterwards anyway
232276
pass
277+
except AttributeError as e:
278+
# Handle case where os.killpg doesn't exist (shouldn't happen now)
279+
if "killpg" in str(e):
280+
self._windows_terminate_process(sig)
281+
else:
282+
raise
233283
return self
234284

235285
def __del__(self) -> None:
15.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)