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
2022import os .path
2123import platform
2224import re
3234
3335from pytest_postgresql .exceptions import ExecutableMissingException , PostgreSQLUnsupported
3436
37+ logger = logging .getLogger (__name__ )
38+
3539_LOCALE = "C.UTF-8"
3640
3741if 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
4149T = 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 :
0 commit comments