diff --git a/.github/workflows/dockerised-postgres.yml b/.github/workflows/dockerised-postgres.yml
index 8da5bc8e..678a988a 100644
--- a/.github/workflows/dockerised-postgres.yml
+++ b/.github/workflows/dockerised-postgres.yml
@@ -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:
diff --git a/.github/workflows/oldest-postgres.yml b/.github/workflows/oldest-postgres.yml
index 56be172d..2dc5b00e 100644
--- a/.github/workflows/oldest-postgres.yml
+++ b/.github/workflows/oldest-postgres.yml
@@ -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 }}
@@ -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
diff --git a/.github/workflows/single-postgres.yml b/.github/workflows/single-postgres.yml
index 870e500b..13d9c930 100644
--- a/.github/workflows/single-postgres.yml
+++ b/.github/workflows/single-postgres.yml
@@ -36,14 +36,55 @@ 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
@@ -51,21 +92,21 @@ 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: 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:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1c33cdee..12910def 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -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
diff --git a/pytest_postgresql/executor.py b/pytest_postgresql/executor.py
index ef027739..53ab5fce 100644
--- a/pytest_postgresql/executor.py
+++ b/pytest_postgresql/executor.py
@@ -17,6 +17,8 @@
# along with pytest-postgresql. If not, see .
"""PostgreSQL executor crafter around pg_ctl."""
+import logging
+import os
import os.path
import platform
import re
@@ -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")
@@ -48,7 +56,11 @@ class PostgreSQLExecutor(TCPExecutor):
`_
"""
- 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 "
@@ -56,6 +68,17 @@ class PostgreSQLExecutor(TCPExecutor):
'-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\d+(?:\.\d+)?)")
MIN_SUPPORTED_VERSION = parse("14")
@@ -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,
@@ -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:
diff --git a/tests/conftest.py b/tests/conftest.py
index 784b8905..10b5f39d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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")
diff --git a/tests/test_executor.py b/tests/test_executor.py
index 25c6aa24..27e22749 100644
--- a/tests/test_executor.py
+++ b/tests/test_executor.py
@@ -1,6 +1,8 @@
"""Test various executor behaviours."""
+import platform
from typing import Any
+from unittest.mock import patch
import psycopg
import pytest
@@ -119,6 +121,112 @@ def test_executor_init_bad_tmp_path(
password="some password",
dbname="some database",
)
+
+ # Verify the correct template was selected based on platform
+ current_platform = platform.system()
+ if current_platform == "Windows":
+ # Windows template should not have unix_socket_directories
+ assert "unix_socket_directories" not in executor.command
+ assert "log_destination=stderr" in executor.command
+ else:
+ # Unix/Darwin template should have unix_socket_directories with single quotes
+ assert "unix_socket_directories=" in executor.command
+ assert "log_destination='stderr'" in executor.command
+
+ assert_executor_start_stop(executor)
+
+
+@pytest.mark.parametrize(
+ "platform_name",
+ ["Windows", "Linux", "Darwin"],
+)
+def test_executor_platform_template_selection(
+ request: FixtureRequest,
+ tmp_path_factory: pytest.TempPathFactory,
+ platform_name: str,
+) -> None:
+ """Test that correct template is selected for each platform.
+
+ This parametrized test verifies that the executor selects the appropriate
+ command template based on the platform.
+ """
+ config = get_config(request)
+ pg_exe = process._pg_exe(None, config)
+ port = process._pg_port(-1, config, [])
+ tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}")
+ datadir, logfile_path = process._prepare_dir(tmpdir, port)
+
+ with patch("pytest_postgresql.executor.platform.system", return_value=platform_name):
+ executor = PostgreSQLExecutor(
+ executable=pg_exe,
+ host=config.host,
+ port=port,
+ datadir=str(datadir),
+ unixsocketdir=config.unixsocketdir,
+ logfile=str(logfile_path),
+ startparams=config.startparams,
+ dbname="test",
+ )
+
+ # Verify correct template was selected
+ if platform_name == "Windows":
+ # Windows template
+ assert "unix_socket_directories" not in executor.command
+ assert "log_destination=stderr" in executor.command
+ else:
+ # Unix/Darwin template
+ assert "unix_socket_directories=" in executor.command
+ assert "log_destination='stderr'" in executor.command
+
+
+def test_executor_with_special_chars_in_all_paths(
+ request: FixtureRequest,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ """Test executor with special characters in multiple paths simultaneously.
+
+ This integration test verifies that the executor can handle special
+ characters (spaces, Unicode) in datadir, logfile, unixsocketdir, and
+ postgres_options all at the same time.
+ """
+ config = get_config(request)
+ pg_exe = process._pg_exe(None, config)
+ port = process._pg_port(-1, config, [])
+ # Create a tmpdir with spaces in the name
+ tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}") / "my test dir"
+ tmpdir.mkdir(exist_ok=True)
+ datadir, logfile_path = process._prepare_dir(tmpdir, port)
+
+ # Create the socket directory for Unix systems
+ socket_dir = tmpdir / "socket dir"
+ socket_dir.mkdir(parents=True, exist_ok=True)
+
+ executor = PostgreSQLExecutor(
+ executable=pg_exe,
+ host=config.host,
+ port=port,
+ datadir=str(datadir),
+ unixsocketdir=str(socket_dir),
+ logfile=str(logfile_path),
+ startparams=config.startparams,
+ password="test pass",
+ dbname="test db",
+ postgres_options="-N 50",
+ )
+
+ # Verify the command contains properly quoted paths
+ command = executor.command
+ assert str(datadir) in command or f'"{datadir}"' in command
+ assert str(logfile_path) in command or f'"{logfile_path}"' in command
+
+ # Verify correct template was selected based on actual platform
+ current_platform = platform.system()
+ if current_platform == "Windows":
+ assert "unix_socket_directories" not in executor.command
+ else:
+ assert "unix_socket_directories=" in executor.command
+
+ # Start and stop the executor to verify it works
assert_executor_start_stop(executor)
@@ -173,3 +281,119 @@ def test_custom_isolation_level(postgres_isolation_level: Connection) -> None:
cur = postgres_isolation_level.cursor()
cur.execute("SELECT 1")
assert cur.fetchone() == (1,)
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-specific test")
+def test_actual_postgresql_start_windows(
+ request: FixtureRequest,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ """Test that PostgreSQL actually starts on Windows with the new template.
+
+ This integration test verifies that the Windows-specific command template
+ correctly starts PostgreSQL on actual Windows systems.
+ """
+ config = get_config(request)
+ pg_exe = process._pg_exe(None, config)
+ port = process._pg_port(-1, config, [])
+ tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}")
+ datadir, logfile_path = process._prepare_dir(tmpdir, port)
+
+ executor = PostgreSQLExecutor(
+ executable=pg_exe,
+ host=config.host,
+ port=port,
+ datadir=str(datadir),
+ unixsocketdir=config.unixsocketdir,
+ logfile=str(logfile_path),
+ startparams=config.startparams,
+ password="testpass",
+ dbname="test",
+ )
+
+ # Verify Windows template is used
+ assert "unix_socket_directories" not in executor.command
+ assert "log_destination=stderr" in executor.command
+
+ # Start and stop PostgreSQL to verify it works
+ assert_executor_start_stop(executor)
+
+
+@pytest.mark.skipif(
+ platform.system() not in ("Linux", "FreeBSD"),
+ reason="Unix/Linux-specific test",
+)
+def test_actual_postgresql_start_unix(
+ request: FixtureRequest,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ """Test that PostgreSQL actually starts on Unix/Linux with the new template.
+
+ This integration test verifies that the Unix-specific command template
+ correctly starts PostgreSQL on actual Unix/Linux systems.
+ """
+ config = get_config(request)
+ pg_exe = process._pg_exe(None, config)
+ port = process._pg_port(-1, config, [])
+ tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}")
+ datadir, logfile_path = process._prepare_dir(tmpdir, port)
+
+ executor = PostgreSQLExecutor(
+ executable=pg_exe,
+ host=config.host,
+ port=port,
+ datadir=str(datadir),
+ unixsocketdir=config.unixsocketdir,
+ logfile=str(logfile_path),
+ startparams=config.startparams,
+ password="testpass",
+ dbname="test",
+ )
+
+ # Verify Unix template is used
+ assert "unix_socket_directories=" in executor.command
+ assert "log_destination='stderr'" in executor.command
+
+ # Start and stop PostgreSQL to verify it works
+ assert_executor_start_stop(executor)
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="Darwin/macOS-specific test")
+def test_actual_postgresql_start_darwin(
+ request: FixtureRequest,
+ tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+ """Test that PostgreSQL actually starts on Darwin/macOS with the new template.
+
+ This integration test verifies that the Unix template correctly starts
+ PostgreSQL on actual Darwin/macOS systems and uses the correct locale.
+ """
+ config = get_config(request)
+ pg_exe = process._pg_exe(None, config)
+ port = process._pg_port(-1, config, [])
+ tmpdir = tmp_path_factory.mktemp(f"pytest-postgresql-{request.node.name}")
+ datadir, logfile_path = process._prepare_dir(tmpdir, port)
+
+ executor = PostgreSQLExecutor(
+ executable=pg_exe,
+ host=config.host,
+ port=port,
+ datadir=str(datadir),
+ unixsocketdir=config.unixsocketdir,
+ logfile=str(logfile_path),
+ startparams=config.startparams,
+ password="testpass",
+ dbname="test",
+ )
+
+ # Verify Unix template is used
+ assert "unix_socket_directories=" in executor.command
+ assert "log_destination='stderr'" in executor.command
+
+ # Verify Darwin-specific locale is set
+ assert executor.envvars["LC_ALL"] == "en_US.UTF-8"
+ assert executor.envvars["LC_CTYPE"] == "en_US.UTF-8"
+ assert executor.envvars["LANG"] == "en_US.UTF-8"
+
+ # Start and stop PostgreSQL to verify it works
+ assert_executor_start_stop(executor)
diff --git a/tests/test_postgres_options_plugin.py b/tests/test_postgres_options_plugin.py
index 59bc132c..20962a0d 100644
--- a/tests/test_postgres_options_plugin.py
+++ b/tests/test_postgres_options_plugin.py
@@ -19,7 +19,7 @@ def pointed_pytester(pytester: Pytester) -> Pytester:
pytest_postgresql_path = Path(pytest_postgresql.__file__)
root_path = pytest_postgresql_path.parent.parent
pytester.syspathinsert(root_path)
- pytester.makeconftest("from pytest_postgresql.plugin import *\n")
+ pytester.makeconftest('pytest_plugins = ["pytest_postgresql.plugin"]\n')
return pytester
diff --git a/tests/test_windows_compatibility.py b/tests/test_windows_compatibility.py
new file mode 100644
index 00000000..e8f52967
--- /dev/null
+++ b/tests/test_windows_compatibility.py
@@ -0,0 +1,729 @@
+"""Test Windows compatibility fixes for pytest-postgresql."""
+
+import subprocess
+from unittest.mock import MagicMock, patch
+
+from pytest_postgresql.executor import PostgreSQLExecutor
+
+
+class TestCommandTemplates:
+ """Test platform-specific command templates."""
+
+ def test_unix_command_template_has_single_quotes(self) -> None:
+ """Test that Unix template uses single quotes for PostgreSQL config values.
+
+ Single quotes are PostgreSQL config-level quoting that protects paths
+ with spaces in unix_socket_directories. On Unix, mirakuru uses
+ shlex.split() which properly handles single quotes inside double-quoted strings.
+ """
+ template = PostgreSQLExecutor.UNIX_PROC_START_COMMAND
+
+ # Unix template should use single quotes around config values
+ assert "log_destination='stderr'" in template
+ assert "unix_socket_directories='{unixsocketdir}'" in template
+
+ def test_windows_command_template_no_single_quotes(self) -> None:
+ """Test that Windows template has no single quotes.
+
+ Windows cmd.exe treats single quotes as literal characters, not
+ delimiters, which causes errors when passed to pg_ctl.
+ """
+ template = PostgreSQLExecutor.WINDOWS_PROC_START_COMMAND
+
+ # Windows template should NOT use single quotes
+ assert "log_destination=stderr" in template
+ assert "log_destination='stderr'" not in template
+ assert "'" not in template
+
+ def test_windows_command_template_omits_unix_socket_directories(self) -> None:
+ """Test that Windows template does not include unix_socket_directories.
+
+ PostgreSQL ignores unix_socket_directories on Windows entirely, so
+ including it is unnecessary and avoids any quoting complexity.
+ """
+ template = PostgreSQLExecutor.WINDOWS_PROC_START_COMMAND
+
+ assert "unix_socket_directories" not in template
+ assert "{unixsocketdir}" not in template
+
+ def test_unix_command_template_includes_unix_socket_directories(self) -> None:
+ """Test that Unix template includes unix_socket_directories."""
+ template = PostgreSQLExecutor.UNIX_PROC_START_COMMAND
+
+ assert "unix_socket_directories='{unixsocketdir}'" in template
+
+ def test_unix_template_protects_paths_with_spaces(self) -> None:
+ """Test that Unix template properly quotes paths containing spaces.
+
+ When unixsocketdir contains spaces (e.g., custom temp directories),
+ the single quotes in the Unix template protect the path from being
+ split by PostgreSQL's argument parser.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/my socket dir",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # The path with spaces should be enclosed in single quotes
+ assert "unix_socket_directories='/tmp/my socket dir'" in command
+
+ def test_windows_template_selected_on_windows(self) -> None:
+ """Test that Windows template is selected when platform is Windows."""
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/temp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Windows template should not have unix_socket_directories
+ assert "unix_socket_directories" not in command
+ # Windows template should not have single quotes
+ assert "log_destination=stderr" in command
+ assert "log_destination='stderr'" not in command
+
+ def test_unix_template_selected_on_linux(self) -> None:
+ """Test that Unix template is selected when platform is Linux."""
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Unix template should have unix_socket_directories with single quotes
+ assert "unix_socket_directories='/tmp/socket'" in command
+ assert "log_destination='stderr'" in command
+
+ def test_darwin_template_selection(self) -> None:
+ """Test that Darwin/macOS uses Unix template.
+
+ macOS should use the same Unix template as Linux since it's a Unix-like
+ system and supports unix_socket_directories.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Darwin"):
+ executor = PostgreSQLExecutor(
+ executable="/opt/homebrew/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Darwin should use Unix template with unix_socket_directories and single quotes
+ assert "unix_socket_directories='/tmp/socket'" in command
+ assert "log_destination='stderr'" in command
+
+ def test_darwin_locale_setting(self) -> None:
+ """Test that Darwin/macOS sets en_US.UTF-8 locale.
+
+ Darwin requires en_US.UTF-8 instead of C.UTF-8 which is used on Linux.
+ This test verifies the locale environment variables are set correctly.
+ """
+ # Patch the _LOCALE variable to simulate Darwin environment
+ with patch("pytest_postgresql.executor._LOCALE", "en_US.UTF-8"):
+ executor = PostgreSQLExecutor(
+ executable="/opt/homebrew/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Darwin should set en_US.UTF-8 locale
+ assert executor.envvars["LC_ALL"] == "en_US.UTF-8"
+ assert executor.envvars["LC_CTYPE"] == "en_US.UTF-8"
+ assert executor.envvars["LANG"] == "en_US.UTF-8"
+
+ def test_postgres_options_with_single_quotes_unix(self) -> None:
+ """Test postgres_options containing single quotes on Unix.
+
+ Single quotes in postgres_options should be preserved and passed through
+ to PostgreSQL on Unix systems.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ postgres_options="-c shared_buffers='128MB' -c work_mem='64MB'",
+ )
+
+ command = executor.command
+ # postgres_options should be included as-is with single quotes preserved
+ assert "-c shared_buffers='128MB' -c work_mem='64MB'" in command
+
+ def test_postgres_options_with_single_quotes_windows(self) -> None:
+ """Test postgres_options containing single quotes on Windows.
+
+ Single quotes in postgres_options should work on Windows since they're
+ inside the -o parameter's double quotes.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/temp/log",
+ startparams="-w",
+ dbname="test",
+ postgres_options="-c shared_buffers='128MB' -c work_mem='64MB'",
+ )
+
+ command = executor.command
+ # postgres_options should be included with single quotes preserved
+ assert "-c shared_buffers='128MB' -c work_mem='64MB'" in command
+
+ def test_postgres_options_with_double_quotes(self) -> None:
+ """Test postgres_options containing double quotes.
+
+ Double quotes in postgres_options need careful handling as they interact
+ with the shell's quote parsing.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ postgres_options='-c search_path="public,other"',
+ )
+
+ command = executor.command
+ # postgres_options with double quotes should be preserved
+ assert '-c search_path="public,other"' in command
+
+ def test_postgres_options_with_paths_containing_spaces(self) -> None:
+ """Test postgres_options with file paths containing spaces.
+
+ Config options that reference file paths with spaces should be properly
+ quoted within postgres_options.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ postgres_options="""-c config_file='/etc/postgres/my config.conf'""",
+ )
+
+ command = executor.command
+ # postgres_options with paths containing spaces should be preserved
+ assert """-c config_file='/etc/postgres/my config.conf'""" in command
+
+ def test_empty_postgres_options(self) -> None:
+ """Test command generation with empty postgres_options.
+
+ When postgres_options is empty (default), the command should still be
+ properly formatted without extra spaces or malformed syntax.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ postgres_options="",
+ )
+
+ command = executor.command
+ # Command should still be valid with empty postgres_options
+ assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command
+ assert '-D "/tmp/data"' in command
+ assert "unix_socket_directories='/tmp/socket'" in command
+ # Should not have trailing space before closing quote in -o parameter
+ expected_opts = (
+ '-o "-F -p 5432 -c log_destination=\'stderr\' '
+ '-c logging_collector=off -c unix_socket_directories=\'/tmp/socket\' "'
+ )
+ assert expected_opts in command
+
+ def test_empty_startparams(self) -> None:
+ """Test command generation with empty startparams.
+
+ When startparams is empty (default), the command should still be
+ properly formatted at the end.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Command should be valid with empty startparams
+ assert "/usr/lib/postgresql/16/bin/pg_ctl start" in command
+ assert '-l "/tmp/log"' in command
+ # Command should not have trailing spaces at the end
+ assert not command.endswith(" ")
+
+ def test_both_empty_postgres_options_and_startparams(self) -> None:
+ """Test command generation with both postgres_options and startparams empty.
+
+ When both optional parameters are empty, the command should still
+ be properly formatted.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/temp/log",
+ startparams="",
+ dbname="test",
+ postgres_options="",
+ )
+
+ command = executor.command
+ # Command should be valid with both empty
+ assert "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start" in command
+ assert '-D "C:/temp/data"' in command
+ assert '-l "C:/temp/log"' in command
+ # Windows template should not have unix_socket_directories
+ assert "unix_socket_directories" not in command
+
+ def test_unixsocketdir_ignored_on_windows_in_command(self) -> None:
+ """Test that unixsocketdir value doesn't appear in Windows command.
+
+ Even when unixsocketdir is passed to the executor on Windows, its value
+ should not appear anywhere in the generated command since Windows doesn't
+ use unix_socket_directories.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/this/should/not/appear",
+ logfile="C:/temp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # The unixsocketdir value should NOT appear in the Windows command
+ assert "C:/this/should/not/appear" not in command
+ assert "unix_socket_directories" not in command
+
+ def test_paths_with_multiple_consecutive_spaces(self) -> None:
+ """Test paths with multiple consecutive spaces.
+
+ Paths with multiple spaces should be properly quoted and preserved.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/my socket dir",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Multiple spaces should be preserved
+ assert "unix_socket_directories='/tmp/my socket dir'" in command
+
+ def test_paths_with_special_shell_characters(self) -> None:
+ """Test paths with special shell characters.
+
+ Paths with shell metacharacters should be properly quoted to prevent
+ shell interpretation. Testing with ampersand, semicolon, and pipe.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket&test",
+ logfile="/tmp/log;file",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Special characters should be inside quotes
+ assert "unix_socket_directories='/tmp/socket&test'" in command
+ assert '-l "/tmp/log;file"' in command
+
+ def test_paths_with_unicode_characters(self) -> None:
+ """Test paths with Unicode characters.
+
+ Unicode characters in paths should be properly handled and preserved.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/sóckét_dïr_日本語",
+ logfile="/tmp/lög_文件.log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Unicode characters should be preserved
+ assert "unix_socket_directories='/tmp/sóckét_dïr_日本語'" in command
+ assert '-l "/tmp/lög_文件.log"' in command
+
+ def test_command_with_all_special_characters_combined(self) -> None:
+ """Test command with multiple types of special characters.
+
+ This comprehensive test combines spaces, quotes, special shell chars,
+ and Unicode to ensure the command handles complex real-world scenarios.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Linux"):
+ executor = PostgreSQLExecutor(
+ executable="/usr/lib/postgresql/16/bin/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/my data & files",
+ unixsocketdir="/tmp/sóckét dir (test)",
+ logfile="/tmp/log file; output.log",
+ startparams="-w -t 30",
+ dbname="test",
+ postgres_options="-c shared_buffers='256MB' -c config_file='/etc/pg/main.conf'",
+ )
+
+ command = executor.command
+ # All special characters should be properly handled
+ assert '-D "/tmp/my data & files"' in command
+ assert "unix_socket_directories='/tmp/sóckét dir (test)'" in command
+ assert '-l "/tmp/log file; output.log"' in command
+ assert "-c shared_buffers='256MB'" in command
+ assert "-c config_file='/etc/pg/main.conf'" in command
+ assert "-w -t 30" in command
+
+
+class TestWindowsCompatibility:
+ """Test Windows-specific process management functionality."""
+
+ def test_windows_terminate_process(self) -> None:
+ """Test Windows process termination."""
+ executor = PostgreSQLExecutor(
+ executable="/path/to/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Mock process
+ mock_process = MagicMock()
+ executor.process = mock_process
+
+ # No need to mock platform.system() since the method doesn't check it anymore
+ executor._windows_terminate_process()
+
+ # Should call terminate first
+ mock_process.terminate.assert_called_once()
+ mock_process.wait.assert_called()
+
+ def test_windows_terminate_process_force_kill(self) -> None:
+ """Test Windows process termination with force kill on timeout."""
+ executor = PostgreSQLExecutor(
+ executable="/path/to/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Mock process that times out
+ mock_process = MagicMock()
+ mock_process.wait.side_effect = [subprocess.TimeoutExpired(cmd="test", timeout=5), None]
+ executor.process = mock_process
+
+ # No need to mock platform.system() since the method doesn't check it anymore
+ executor._windows_terminate_process()
+
+ # Should call terminate, wait (timeout), then kill, then wait again
+ mock_process.terminate.assert_called_once()
+ mock_process.kill.assert_called_once()
+ assert mock_process.wait.call_count == 2
+
+ def test_stop_method_windows(self) -> None:
+ """Test stop method on Windows."""
+ executor = PostgreSQLExecutor(
+ executable="/path/to/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Mock subprocess and process
+ with (
+ patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess,
+ patch("platform.system", return_value="Windows"),
+ patch.object(executor, "_windows_terminate_process") as mock_terminate,
+ ):
+ result = executor.stop()
+
+ # Should call pg_ctl stop and Windows terminate
+ mock_subprocess.assert_called_once_with(
+ ["/path/to/pg_ctl", "stop", "-D", "/tmp/data", "-m", "f"],
+ )
+ mock_terminate.assert_called_once_with(None)
+ assert result is executor
+
+ def test_stop_method_unix(self) -> None:
+ """Test stop method on Unix systems."""
+ executor = PostgreSQLExecutor(
+ executable="/path/to/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Mock subprocess and super().stop
+ with (
+ patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess,
+ patch("platform.system", return_value="Linux"),
+ patch("pytest_postgresql.executor.TCPExecutor.stop") as mock_super_stop,
+ ):
+ mock_super_stop.return_value = executor
+ result = executor.stop()
+
+ # Should call pg_ctl stop and parent class stop
+ mock_subprocess.assert_called_once()
+ mock_super_stop.assert_called_once_with(None, None)
+ assert result is executor
+
+ def test_stop_method_fallback_on_killpg_error(self) -> None:
+ """Test stop method falls back to Windows termination on killpg AttributeError."""
+ import pytest_postgresql.executor
+
+ executor = PostgreSQLExecutor(
+ executable="/path/to/pg_ctl",
+ host="localhost",
+ port=5432,
+ datadir="/tmp/data",
+ unixsocketdir="/tmp/socket",
+ logfile="/tmp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ # Mock subprocess and super().stop to raise AttributeError
+ with (
+ patch("pytest_postgresql.executor.subprocess.check_output") as mock_subprocess,
+ patch("platform.system", return_value="Linux"),
+ patch(
+ "pytest_postgresql.executor.TCPExecutor.stop",
+ side_effect=AttributeError("module 'os' has no attribute 'killpg'"),
+ ),
+ patch.object(executor, "_windows_terminate_process") as mock_terminate,
+ ):
+ # Temporarily remove os.killpg so hasattr(os, "killpg") returns False
+ real_killpg = getattr(pytest_postgresql.executor.os, "killpg", None)
+ try:
+ if real_killpg is not None:
+ delattr(pytest_postgresql.executor.os, "killpg")
+ result = executor.stop()
+ finally:
+ if real_killpg is not None:
+ pytest_postgresql.executor.os.killpg = real_killpg
+
+ # Should call pg_ctl stop, fail on super().stop, then use Windows terminate
+ mock_subprocess.assert_called_once()
+ mock_terminate.assert_called_once()
+ assert result is executor
+
+ def test_command_formatting_windows(self) -> None:
+ """Test that command is properly formatted for Windows paths."""
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5555,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/temp/log.txt",
+ startparams="-w -s",
+ dbname="testdb",
+ postgres_options="-c shared_preload_libraries=test",
+ )
+
+ # The command should be properly formatted without single quotes
+ # and without unix_socket_directories (irrelevant on Windows)
+ expected_parts = [
+ "C:/Program Files/PostgreSQL/bin/pg_ctl.exe start",
+ '-D "C:/temp/data"',
+ '-o "-F -p 5555 -c log_destination=stderr',
+ "-c logging_collector=off",
+ '-c shared_preload_libraries=test"',
+ '-l "C:/temp/log.txt"',
+ "-w -s",
+ ]
+
+ command = executor.command
+ for part in expected_parts:
+ assert part in command, f"Expected '{part}' in command: {command}"
+
+ # Verify unix_socket_directories is NOT in the Windows command
+ assert "unix_socket_directories" not in command, (
+ f"unix_socket_directories should not be in Windows command: {command}"
+ )
+
+ def test_windows_datadir_with_spaces(self) -> None:
+ """Test Windows datadir with spaces in path.
+
+ Windows paths with spaces should be properly quoted with double quotes.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/Program Files/PostgreSQL/my data dir",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/temp/log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # datadir with spaces should be quoted
+ assert '-D "C:/Program Files/PostgreSQL/my data dir"' in command
+
+ def test_windows_logfile_with_spaces(self) -> None:
+ """Test Windows logfile with spaces in path.
+
+ Windows log file paths with spaces should be properly quoted with double quotes.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:/temp/data",
+ unixsocketdir="C:/temp/socket",
+ logfile="C:/Program Files/PostgreSQL/logs/my log file.log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # logfile with spaces should be quoted
+ assert '-l "C:/Program Files/PostgreSQL/logs/my log file.log"' in command
+
+ def test_windows_unc_paths(self) -> None:
+ """Test Windows UNC (Universal Naming Convention) paths.
+
+ UNC paths like \\\\server\\share should be properly handled on Windows.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:/Program Files/PostgreSQL/bin/pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="//server/share/postgres/data",
+ unixsocketdir="//server/share/postgres/socket",
+ logfile="//server/share/postgres/logs/postgresql.log",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # UNC paths should be properly quoted (using forward slashes in Python)
+ assert '-D "//server/share/postgres/data"' in command
+ assert '-l "//server/share/postgres/logs/postgresql.log"' in command
+
+ def test_windows_mixed_slashes(self) -> None:
+ """Test Windows paths with mixed forward and backslashes.
+
+ Windows accepts both forward slashes and backslashes, and the command
+ should handle both properly.
+ """
+ with patch("pytest_postgresql.executor.platform.system", return_value="Windows"):
+ executor = PostgreSQLExecutor(
+ executable="C:\\Program Files\\PostgreSQL\\bin\\pg_ctl.exe",
+ host="localhost",
+ port=5432,
+ datadir="C:\\temp\\data",
+ unixsocketdir="C:\\temp\\socket",
+ logfile="C:\\temp\\log.txt",
+ startparams="-w",
+ dbname="test",
+ )
+
+ command = executor.command
+ # Paths with backslashes should be properly quoted
+ assert 'C:\\Program Files\\PostgreSQL\\bin\\pg_ctl.exe start' in command
+ assert '-D "C:\\temp\\data"' in command
+ assert '-l "C:\\temp\\log.txt"' in command