@@ -209,11 +209,14 @@ def test_postgres_options_with_single_quotes_windows(self) -> None:
209209 # postgres_options should be included with single quotes preserved
210210 assert "-c shared_buffers='128MB' -c work_mem='64MB'" in command
211211
212- def test_postgres_options_with_double_quotes (self ) -> None :
213- """Test postgres_options containing double quotes.
214-
215- Double quotes in postgres_options need careful handling as they interact
216- with the shell's quote parsing.
212+ def test_postgres_options_with_single_quoted_search_path (self ) -> None :
213+ """Test postgres_options with a single-quoted search_path value.
214+
215+ PostgreSQL GUC values are quoted with single quotes in the config syntax.
216+ The Unix template wraps the whole -o argument in double quotes, so
217+ inner double quotes would prematurely terminate that token and produce an
218+ invalid shell command. Single-quoted values are the correct form and must
219+ be preserved verbatim inside the -o argument.
217220 """
218221 with patch ("pytest_postgresql.executor.platform.system" , return_value = "Linux" ):
219222 executor = PostgreSQLExecutor (
@@ -225,12 +228,17 @@ def test_postgres_options_with_double_quotes(self) -> None:
225228 logfile = "/tmp/log" ,
226229 startparams = "-w" ,
227230 dbname = "test" ,
228- postgres_options = ' -c search_path=" public,other"' ,
231+ postgres_options = " -c search_path=' public,other'" ,
229232 )
230233
231234 command = executor .command
232- # postgres_options with double quotes should be preserved
233- assert '-c search_path="public,other"' in command
235+ # The single-quoted search_path value must appear verbatim inside the -o argument
236+ assert "-c search_path='public,other'" in command
237+ # Verify it is embedded inside the -o "..." token (no premature " termination)
238+ o_start = command .index ('-o "' )
239+ o_end = command .index ('"' , o_start + 4 )
240+ o_argument = command [o_start : o_end + 1 ]
241+ assert "-c search_path='public,other'" in o_argument
234242
235243 def test_postgres_options_with_paths_containing_spaces (self ) -> None :
236244 """Test postgres_options with file paths containing spaces.
@@ -276,7 +284,7 @@ def test_empty_postgres_options(self) -> None:
276284
277285 command = executor .command
278286 # Command should still be valid with empty postgres_options
279- assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command
287+ assert ' "/usr/lib/postgresql/16/bin/pg_ctl" start' in command
280288 assert '-D "/tmp/data"' in command
281289 assert "unix_socket_directories='/tmp/socket'" in command
282290 # Should not have trailing space before closing quote in -o parameter
@@ -306,7 +314,7 @@ def test_empty_startparams(self) -> None:
306314
307315 command = executor .command
308316 # Command should be valid with empty startparams
309- assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command
317+ assert ' "/usr/lib/postgresql/16/bin/pg_ctl" start' in command
310318 assert '-l "/tmp/log"' in command
311319 # Command should not have trailing spaces at the end
312320 assert not command .endswith (" " )
@@ -332,7 +340,7 @@ def test_both_empty_postgres_options_and_startparams(self) -> None:
332340
333341 command = executor .command
334342 # Command should be valid with both empty
335- assert "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start" in command
343+ assert ' "C:/Program Files/PostgreSQL/bin/pg_ctl.exe" start' in command
336344 assert '-D "C:/temp/data"' in command
337345 assert '-l "C:/temp/log"' in command
338346 # Windows template should not have unix_socket_directories
@@ -621,7 +629,7 @@ def test_command_formatting_windows(self) -> None:
621629 # The command should be properly formatted without single quotes
622630 # and without unix_socket_directories (irrelevant on Windows)
623631 expected_parts = [
624- "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start" ,
632+ ' "C:/Program Files/PostgreSQL/bin/pg_ctl.exe" start' ,
625633 '-D "C:/temp/data"' ,
626634 '-o "-F -p 5555 -c log_destination=stderr' ,
627635 "-c logging_collector=off" ,
@@ -723,6 +731,96 @@ def test_windows_mixed_slashes(self) -> None:
723731
724732 command = executor .command
725733 # Paths with backslashes should be properly quoted
726- assert "C:\\ Program Files\\ PostgreSQL\\ bin\\ pg_ctl.exe start" in command
734+ assert ' "C:\\ Program Files\\ PostgreSQL\\ bin\\ pg_ctl.exe" start' in command
727735 assert '-D "C:\\ temp\\ data"' in command
728736 assert '-l "C:\\ temp\\ log.txt"' in command
737+
738+
739+ class TestRunningMethodQuoting :
740+ """Test that the running() method properly quotes the executable path."""
741+
742+ def test_running_quotes_executable_with_spaces (self ) -> None :
743+ """Test that running() quotes the executable in the status command.
744+
745+ subprocess.getstatusoutput() uses shell=True internally. On Windows,
746+ cmd.exe parses the command string and an unquoted path like
747+ C:\\ Program Files\\ ...\\ pg_ctl.exe would be split at the space,
748+ causing the status check to fail silently or error.
749+ """
750+ executor = PostgreSQLExecutor (
751+ executable = "C:/Program Files/PostgreSQL/17/bin/pg_ctl.exe" ,
752+ host = "localhost" ,
753+ port = 5432 ,
754+ datadir = "C:/temp/data" ,
755+ unixsocketdir = "C:/temp/socket" ,
756+ logfile = "C:/temp/log" ,
757+ startparams = "-w" ,
758+ dbname = "test" ,
759+ )
760+
761+ with (
762+ patch ("pytest_postgresql.executor.os.path.exists" , return_value = True ),
763+ patch ("pytest_postgresql.executor.subprocess.getstatusoutput" ) as mock_getstatusoutput ,
764+ ):
765+ mock_getstatusoutput .return_value = (0 , "" )
766+ executor .running ()
767+
768+ # The executable must be double-quoted in the shell command string
769+ called_cmd = mock_getstatusoutput .call_args [0 ][0 ]
770+ assert called_cmd .startswith ('"C:/Program Files/PostgreSQL/17/bin/pg_ctl.exe"' ), (
771+ f"Executable not quoted in status command: { called_cmd !r} "
772+ )
773+
774+ def test_running_quotes_executable_without_spaces (self ) -> None :
775+ """Test that running() still quotes executables without spaces.
776+
777+ Even when the executable path has no spaces, it should still be
778+ double-quoted for consistency and correctness.
779+ """
780+ executor = PostgreSQLExecutor (
781+ executable = "/usr/lib/postgresql/17/bin/pg_ctl" ,
782+ host = "localhost" ,
783+ port = 5432 ,
784+ datadir = "/tmp/data" ,
785+ unixsocketdir = "/tmp/socket" ,
786+ logfile = "/tmp/log" ,
787+ startparams = "-w" ,
788+ dbname = "test" ,
789+ )
790+
791+ with (
792+ patch ("pytest_postgresql.executor.os.path.exists" , return_value = True ),
793+ patch ("pytest_postgresql.executor.subprocess.getstatusoutput" ) as mock_getstatusoutput ,
794+ ):
795+ mock_getstatusoutput .return_value = (0 , "" )
796+ executor .running ()
797+
798+ called_cmd = mock_getstatusoutput .call_args [0 ][0 ]
799+ assert called_cmd .startswith ('"/usr/lib/postgresql/17/bin/pg_ctl"' ), (
800+ f"Executable not quoted in status command: { called_cmd !r} "
801+ )
802+
803+ def test_running_quotes_datadir_with_spaces (self ) -> None :
804+ """Test that running() quotes the datadir in the status command."""
805+ executor = PostgreSQLExecutor (
806+ executable = "/usr/lib/postgresql/17/bin/pg_ctl" ,
807+ host = "localhost" ,
808+ port = 5432 ,
809+ datadir = "/tmp/my data dir" ,
810+ unixsocketdir = "/tmp/socket" ,
811+ logfile = "/tmp/log" ,
812+ startparams = "-w" ,
813+ dbname = "test" ,
814+ )
815+
816+ with (
817+ patch ("pytest_postgresql.executor.os.path.exists" , return_value = True ),
818+ patch ("pytest_postgresql.executor.subprocess.getstatusoutput" ) as mock_getstatusoutput ,
819+ ):
820+ mock_getstatusoutput .return_value = (0 , "" )
821+ executor .running ()
822+
823+ called_cmd = mock_getstatusoutput .call_args [0 ][0 ]
824+ assert '-D "/tmp/my data dir"' in called_cmd , (
825+ f"Datadir not quoted in status command: { called_cmd !r} "
826+ )
0 commit comments