Skip to content
Merged
6 changes: 4 additions & 2 deletions Dockerfile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ FROM postgres:${PG_VERSION}-alpine

ENV PYTHON=python${PYTHON_VERSION}
RUN if [ "${PYTHON_VERSION}" = "2" ] ; then \
apk add --no-cache curl python2 py-virtualenv py-pip; \
apk add --no-cache curl python2 python2-dev build-base musl-dev \
linux-headers py-virtualenv py-pip; \
fi
RUN if [ "${PYTHON_VERSION}" = "3" ] ; then \
apk add --no-cache curl python3 py-virtualenv; \
apk add --no-cache curl python3 python3-dev build-base musl-dev \
linux-headers py-virtualenv; \
fi
ENV LANG=C.UTF-8

Expand Down
5 changes: 3 additions & 2 deletions run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export VIRTUAL_ENV_DISABLE_PROMPT=1
source $VENV_PATH/bin/activate

# install utilities
$PIP install coverage flake8
$PIP install coverage flake8 psutil

# install testgres' dependencies
export PYTHONPATH=$(pwd)
Expand All @@ -47,7 +47,8 @@ time coverage run -a tests/test_simple.py
unset PG_BIN


# run tests (PG_CONFIG)
# run tests (PG_CONFIG), also without psutil
$PIP uninstall -y psutil
export PG_CONFIG=$(which pg_config)
time coverage run -a tests/test_simple.py
unset PG_CONFIG
Expand Down
7 changes: 7 additions & 0 deletions testgres/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, node, dbname=None, username=None, password=None):
username = username or default_username()

self._node = node
self._pid = 0

self._connection = pglib.connect(
database=dbname,
Expand All @@ -52,6 +53,12 @@ def node(self):
def connection(self):
return self._connection

@property
def pid(self):
if not self._pid:
self._pid = self.execute("select pg_backend_pid();")[0][0]
return self._pid

@property
def cursor(self):
return self._cursor
Expand Down
15 changes: 15 additions & 0 deletions testgres/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,18 @@ def __bool__(self):

# for Python 2.x
__nonzero__ = __bool__


class ProcessType(Enum):
"""
Types of postgres processes
"""
Checkpointer = 'postgres: checkpointer'
BackgroundWriter = 'postgres: background writer'
WalWriter = 'postgres: walwriter'
AutovacuumLauncher = 'postgres: autovacuum launcher'
StatsCollector = 'postgres: stats collector'
LogicalReplicationLauncher = 'postgres: logical replication launcher'
WalReceiver = 'postgres: walreceiver'
WalSender = 'postgres: walsender'
Startup = 'postgres: startup'
97 changes: 93 additions & 4 deletions testgres/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
import subprocess
import time

try:
import psutil
except ImportError:
psutil = None

from shutil import rmtree
from six import raise_from
from tempfile import mkstemp, mkdtemp

from .enums import NodeStatus
from .enums import NodeStatus, ProcessType

from .cache import cached_initdb

Expand Down Expand Up @@ -48,7 +53,8 @@
ExecUtilException, \
QueryException, \
StartNodeException, \
TimeoutException
TimeoutException, \
TestgresException

from .logger import TestgresLogger

Expand Down Expand Up @@ -116,7 +122,11 @@ def __exit__(self, type, value, traceback):

@property
def pid(self):
return self.get_pid()
return self.get_main_pid()

@property
def auxiliary_pids(self):
return self.get_auxiliary_pids()

@property
def master(self):
Expand Down Expand Up @@ -417,7 +427,7 @@ def status(self):
elif e.exit_code == 4:
return NodeStatus.Uninitialized

def get_pid(self):
def get_main_pid(self):
"""
Return postmaster's PID if node is running, else 0.
"""
Expand All @@ -430,6 +440,85 @@ def get_pid(self):
# for clarity
return 0

def get_child_processes(self):
''' Returns child processes for this node '''

if psutil is None:
raise TestgresException("psutil module is not installed")

try:
postmaster = psutil.Process(self.pid)
except psutil.NoSuchProcess:
return None

return postmaster.children(recursive=True)

def get_auxiliary_pids(self):
''' Returns dict with pids of auxiliary processes '''

alternative_names = {
ProcessType.LogicalReplicationLauncher: [
'postgres: bgworker: logical replication launcher'
],
ProcessType.BackgroundWriter: [
'postgres: writer',
],
ProcessType.WalWriter: [
'postgres: wal writer',
],
ProcessType.WalReceiver: [
'postgres: wal receiver',
],
}

children = self.get_child_processes()
if children is None:
return None

result = {}
for child in children:
line = ' '.join(child.cmdline())
for ptype in ProcessType:
if ptype == ProcessType.WalSender \
and (line.startswith(ptype.value) or
line.startswith('postgres: wal sender')):
result.setdefault(ptype, [])
result[ptype].append(child.pid)
break
elif line.startswith(ptype.value):
result[ptype] = child.pid
break
elif ptype in alternative_names:
names = alternative_names[ptype]
for name in names:
if line.startswith(name):
result[ptype] = child.pid
break

return result

def get_walsender_pid(self):
''' Returns pid of according walsender for replica '''

if not self._master:
raise TestgresException("This node is not a replica")

children = self._master.get_child_processes()
if children is None:
return None

sql = 'select application_name, client_port from pg_stat_replication'
for name, client_port in self._master.execute(sql):
if name == self.name:
for child in children:
line = ' '.join(child.cmdline())
if (line.startswith(ProcessType.WalSender.value) or
line.startswith('postgres: wal sender')) and \
str(client_port) in line:
return child.pid

return None

def get_control_data(self):
"""
Return contents of pg_control file.
Expand Down
57 changes: 56 additions & 1 deletion tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
BackupException, \
QueryException, \
CatchUpException, \
TimeoutException
TimeoutException, \
TestgresException

from testgres import \
TestgresConfig, \
Expand All @@ -40,6 +41,7 @@

from testgres import bound_ports
from testgres.utils import pg_version_ge
from testgres.enums import ProcessType


def util_exists(util):
Expand Down Expand Up @@ -702,6 +704,59 @@ def test_version_management(self):
self.assertTrue(b > c)
self.assertTrue(a > c)

def test_pids(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could write a helper function to check if psutil is available and then use it in @unittest.skipUnless().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it's tested for the case when pytest is unavailable too, so it should not be skipped.

try:
import psutil
except ImportError:
psutil = None

master_processes = [
ProcessType.Checkpointer,
ProcessType.BackgroundWriter,
ProcessType.WalWriter,
ProcessType.AutovacuumLauncher,
ProcessType.StatsCollector,
ProcessType.WalSender,
]
if pg_version_ge('10'):
master_processes.append(ProcessType.LogicalReplicationLauncher)

repl_processes = (
ProcessType.Startup,
ProcessType.Checkpointer,
ProcessType.BackgroundWriter,
ProcessType.StatsCollector,
ProcessType.WalReceiver,
)

with get_new_node('master') as master:
master.init().start()

self.assertIsNotNone(master.pid)
with master.connect() as con:
self.assertTrue(con.pid > 0)

with master.backup() as backup:
with backup.spawn_replica('repl', True) as repl:
repl.start()
if psutil is None:
with self.assertRaises(TestgresException):
master.auxiliary_pids
with self.assertRaises(TestgresException):
self.assertIsNone(repl.auxiliary_pids)
else:
master_pids = master.auxiliary_pids
for ptype in master_processes:
self.assertIn(ptype, master_pids)
self.assertTrue(len(master_pids[ProcessType.WalSender]) == 1)

repl_pids = repl.auxiliary_pids
for ptype in repl_processes:
self.assertIn(ptype, repl_pids)

sender_pid = master_pids[ProcessType.WalSender][0]
self.assertTrue(repl.get_walsender_pid() == sender_pid)


if __name__ == '__main__':
unittest.main()