From 7d981a331880b821bbb8ebb9ff2b39a5d5fd0eab Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 2 Jan 2026 12:12:55 +0300 Subject: [PATCH 1/4] PostgresNode::child_processes works on a running node only We will raise an exception InvalidOperationException if PostgresNode::child_processes is called on a stopped or uninitialized node. --- src/node.py | 23 ++++++- src/raise_error.py | 44 ++++++++++++++ tests/test_raise_error.py | 76 ++++++++++++++++++++++++ tests/test_testgres_common.py | 109 ++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 tests/test_raise_error.py diff --git a/src/node.py b/src/node.py index 7dbe4fa..a7ab75a 100644 --- a/src/node.py +++ b/src/node.py @@ -385,14 +385,33 @@ def is_aux(process): return list(filter(is_aux, self.child_processes)) @property - def child_processes(self): + def child_processes(self) -> typing.List[ProcessProxy]: """ Returns a list of all child processes. Each process is represented by :class:`.ProcessProxy` object. """ # get a list of postmaster's children - children = self.os_ops.get_process_children(self.pid) + x = self._get_node_state() + assert type(x) == utils.PostgresNodeState # noqa: E721 + + if x.pid is None: + assert x.node_status != NodeStatus.Running + RaiseError.node_err__cant_enumerate_child_processes( + x.node_status, + x.pid, + ) + + assert x.node_status == NodeStatus.Running + assert type(x.pid) == int # noqa: E721 + return self._get_child_processes(x.pid) + + def _get_child_processes(self, pid: int) -> typing.List[ProcessProxy]: + assert type(pid) == int # noqa: E721 + assert isinstance(self._os_ops, OsOperations) + + # get a list of postmaster's children + children = self._os_ops.get_process_children(pid) return [ProcessProxy(p) for p in children] diff --git a/src/raise_error.py b/src/raise_error.py index 62a3bbf..d0ea687 100644 --- a/src/raise_error.py +++ b/src/raise_error.py @@ -1,3 +1,7 @@ +from .exceptions import InvalidOperationException +from .enums import NodeStatus + +import typing class RaiseError: @@ -25,3 +29,43 @@ def pg_ctl_returns_a_zero_pid(out, _params): errLines.append("------------") errLines.append("Command line is {0}".format(_params)) raise RuntimeError("\n".join(errLines)) + + @staticmethod + def node_err__cant_enumerate_child_processes( + node_status: NodeStatus, + node_pid: typing.Optional[int], + ): + assert type(node_status) == NodeStatus # noqa: E721 + + msg = "Can't enumerate node child processes. {}.".format( + __class__._map_node_status_to_reason( + node_status, + node_pid, + ) + ) + + raise InvalidOperationException(msg) + + @staticmethod + def _map_node_status_to_reason( + node_status: NodeStatus, + node_pid: typing.Optional[int], + ) -> str: + assert type(node_status) == NodeStatus # noqa: E721 + assert node_pid is None or type(node_pid) == int # noqa: E721 + + if node_status == NodeStatus.Uninitialized: + return "Node is not initialized" + + if node_status == NodeStatus.Stopped: + return "Node is not running" + + if node_status == NodeStatus.Running: + return "Node is running (pid: {})".format( + node_pid + ) + + # assert False + return "Node has unknown status {}".format( + node_status + ) diff --git a/tests/test_raise_error.py b/tests/test_raise_error.py new file mode 100644 index 0000000..62ed2db --- /dev/null +++ b/tests/test_raise_error.py @@ -0,0 +1,76 @@ +from src import InvalidOperationException +from src import NodeStatus +from src.raise_error import RaiseError + +import pytest +import typing + + +class TestRaiseError: + class tagData001__NodeErr_CantEnumerateChildProcesses: + node_status: NodeStatus + node_pid: typing.Optional[int] + expected_msg: str + + def __init__( + self, + node_status: NodeStatus, + node_pid: typing.Optional[int], + expected_msg: str, + ): + assert type(node_status) == NodeStatus # noqa: E721 + assert node_pid is None or type(node_pid) == int # noqa: E721 + assert type(expected_msg) == str # noqa: E721 + self.node_status = node_status + self.node_pid = node_pid + self.expected_msg = expected_msg + return + + @property + def sign(self) -> str: + assert type(self.node_status) == NodeStatus # noqa: E721 + assert self.node_pid is None or type(self.node_pid) == int # noqa: E721 + + msg = "status: {}; pid: {}".format( + self.node_status, + self.node_pid, + ) + return msg + + sm_Data001: list[tagData001__NodeErr_CantEnumerateChildProcesses] = [ + tagData001__NodeErr_CantEnumerateChildProcesses( + NodeStatus.Uninitialized, + None, + "Can't enumerate node child processes. Node is not initialized.", + ), + tagData001__NodeErr_CantEnumerateChildProcesses( + NodeStatus.Stopped, + None, + "Can't enumerate node child processes. Node is not running.", + ), + ] + + @pytest.fixture( + params=sm_Data001, + ids=[x.sign for x in sm_Data001], + ) + def data001(self, request: pytest.FixtureRequest) -> tagData001__NodeErr_CantEnumerateChildProcesses: + assert isinstance(request, pytest.FixtureRequest) + assert type(request.param).__name__ == "tagData001__NodeErr_CantEnumerateChildProcesses" + return request.param + + def test_001__node_err__cant_enumerate_child_processes( + self, + data001: tagData001__NodeErr_CantEnumerateChildProcesses, + ): + assert type(data001) == __class__.tagData001__NodeErr_CantEnumerateChildProcesses # noqa: E721 + + with pytest.raises(expected_exception=InvalidOperationException) as x: + RaiseError.node_err__cant_enumerate_child_processes( + data001.node_status, + data001.node_pid, + ) + + assert x is not None + assert str(x.value) == data001.expected_msg + return diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py index e308a95..e35e04f 100644 --- a/tests/test_testgres_common.py +++ b/tests/test_testgres_common.py @@ -9,6 +9,7 @@ from src.node import PostgresNode from src.node import PostgresNodeLogReader from src.node import PostgresNodeUtils +from src.node import ProcessProxy from src.utils import get_pg_version2 from src.utils import file_tail from src.utils import get_bin_path2 @@ -44,6 +45,7 @@ import re import subprocess import typing +import types @contextmanager @@ -293,6 +295,113 @@ def test_status(self, node_svc: PostgresNodeService): assert (node.pid == 0) assert (node.status() == NodeStatus.Uninitialized) + def test_child_processes__is_not_initialized( + self, + node_svc: PostgresNodeService + ): + assert isinstance(node_svc, PostgresNodeService) + + with __class__.helper__get_node(node_svc) as node: + assert isinstance(node, PostgresNode) + assert (node.pid == 0) + assert (node.status() == NodeStatus.Uninitialized) + + with pytest.raises(expected_exception=InvalidOperationException) as x: + node.child_processes + + assert x is not None + assert str(x.value) == "Can't enumerate node child processes. Node is not initialized." + return + + def test_child_processes__is_not_running( + self, + node_svc: PostgresNodeService + ): + assert isinstance(node_svc, PostgresNodeService) + + with __class__.helper__get_node(node_svc) as node: + assert isinstance(node, PostgresNode) + assert (node.pid == 0) + assert (node.status() == NodeStatus.Uninitialized) + + node.init() + + try: + with pytest.raises(expected_exception=InvalidOperationException) as x: + node.child_processes + + assert x is not None + assert str(x.value) == "Can't enumerate node child processes. Node is not running." + finally: + try: + node.cleanup(release_resources=True) + except Exception as e: + logging.error("Exception ({}): {}".format( + type(e).__name__, + e, + )) + return + + def test_child_processes__ok( + self, + node_svc: PostgresNodeService + ): + assert isinstance(node_svc, PostgresNodeService) + + with __class__.helper__get_node(node_svc) as node: + assert isinstance(node, PostgresNode) + assert (node.pid == 0) + assert (node.status() == NodeStatus.Uninitialized) + + node.init() + + try: + node.slow_start() + + children = node.child_processes + assert children is not None + assert type(children) == list # noqa: E721 + + logging.info("Children count is {}".format(len(children))) + logging.info("") + + for i in range(len(children)): + logging.info("------ check child [{}]".format(i)) + child = children[i] + + try: + assert child is not None + assert type(child) == ProcessProxy # noqa: E721 + assert hasattr(child, "process") + assert hasattr(child, "ptype") + assert hasattr(child, "pid") + assert hasattr(child, "cmdline") + assert child.process is not None + assert child.ptype is not None + assert child.pid is not None + assert type(child.ptype) == ProcessType # noqa: E721 + assert type(child.pid) == int # noqa: E721 + assert type(child.cmdline) == types.MethodType # noqa: E721 + + logging.info("ptype is {}".format(child.ptype)) + logging.info("pid is {}".format(child.pid)) + logging.info("cmdline is [{}]".format(child.cmdline())) + except Exception as e: + logging.error("Exception ({}): {}".format( + type(e).__name__, + e, + )) + continue + finally: + try: + node.cleanup(release_resources=True) + except Exception as e: + logging.error("Exception ({}): {}".format( + type(e).__name__, + e, + )) + return + def test_child_pids(self, node_svc: PostgresNodeService): assert isinstance(node_svc, PostgresNodeService) From f533eef84d2190949f98b84b2b32229f0cebf9b7 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 2 Jan 2026 13:23:27 +0300 Subject: [PATCH 2/4] fix: py3.7 and a lost import --- src/node.py | 2 ++ tests/test_raise_error.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node.py b/src/node.py index a7ab75a..0bef422 100644 --- a/src/node.py +++ b/src/node.py @@ -99,6 +99,8 @@ options_string, \ clean_on_error +from .raise_error import RaiseError + from .backup import NodeBackup from testgres.operations.os_ops import ConnectionParams diff --git a/tests/test_raise_error.py b/tests/test_raise_error.py index 62ed2db..fd2e565 100644 --- a/tests/test_raise_error.py +++ b/tests/test_raise_error.py @@ -37,7 +37,7 @@ def sign(self) -> str: ) return msg - sm_Data001: list[tagData001__NodeErr_CantEnumerateChildProcesses] = [ + sm_Data001: typing.List[tagData001__NodeErr_CantEnumerateChildProcesses] = [ tagData001__NodeErr_CantEnumerateChildProcesses( NodeStatus.Uninitialized, None, From 2491c220f3e962a6ac769e7e1d60816170508700 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 2 Jan 2026 13:26:20 +0300 Subject: [PATCH 3/4] RaiseError.node_err__cant_enumerate_child_processes is corrected --- src/node.py | 3 +-- src/raise_error.py | 5 ++--- tests/test_raise_error.py | 15 ++------------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/node.py b/src/node.py index 0bef422..ef93800 100644 --- a/src/node.py +++ b/src/node.py @@ -400,8 +400,7 @@ def child_processes(self) -> typing.List[ProcessProxy]: if x.pid is None: assert x.node_status != NodeStatus.Running RaiseError.node_err__cant_enumerate_child_processes( - x.node_status, - x.pid, + x.node_status ) assert x.node_status == NodeStatus.Running diff --git a/src/raise_error.py b/src/raise_error.py index d0ea687..3603e7d 100644 --- a/src/raise_error.py +++ b/src/raise_error.py @@ -32,15 +32,14 @@ def pg_ctl_returns_a_zero_pid(out, _params): @staticmethod def node_err__cant_enumerate_child_processes( - node_status: NodeStatus, - node_pid: typing.Optional[int], + node_status: NodeStatus ): assert type(node_status) == NodeStatus # noqa: E721 msg = "Can't enumerate node child processes. {}.".format( __class__._map_node_status_to_reason( node_status, - node_pid, + None, ) ) diff --git a/tests/test_raise_error.py b/tests/test_raise_error.py index fd2e565..10416e3 100644 --- a/tests/test_raise_error.py +++ b/tests/test_raise_error.py @@ -9,43 +9,33 @@ class TestRaiseError: class tagData001__NodeErr_CantEnumerateChildProcesses: node_status: NodeStatus - node_pid: typing.Optional[int] expected_msg: str def __init__( self, node_status: NodeStatus, - node_pid: typing.Optional[int], expected_msg: str, ): assert type(node_status) == NodeStatus # noqa: E721 - assert node_pid is None or type(node_pid) == int # noqa: E721 assert type(expected_msg) == str # noqa: E721 self.node_status = node_status - self.node_pid = node_pid self.expected_msg = expected_msg return @property def sign(self) -> str: assert type(self.node_status) == NodeStatus # noqa: E721 - assert self.node_pid is None or type(self.node_pid) == int # noqa: E721 - msg = "status: {}; pid: {}".format( - self.node_status, - self.node_pid, - ) + msg = "status: {}".format(self.node_status) return msg sm_Data001: typing.List[tagData001__NodeErr_CantEnumerateChildProcesses] = [ tagData001__NodeErr_CantEnumerateChildProcesses( NodeStatus.Uninitialized, - None, "Can't enumerate node child processes. Node is not initialized.", ), tagData001__NodeErr_CantEnumerateChildProcesses( NodeStatus.Stopped, - None, "Can't enumerate node child processes. Node is not running.", ), ] @@ -67,8 +57,7 @@ def test_001__node_err__cant_enumerate_child_processes( with pytest.raises(expected_exception=InvalidOperationException) as x: RaiseError.node_err__cant_enumerate_child_processes( - data001.node_status, - data001.node_pid, + data001.node_status ) assert x is not None From 79c719675b1bddca326332caa0289518c8470064 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 2 Jan 2026 17:28:30 +0300 Subject: [PATCH 4/4] fix: test_child_processes__ok is improved during iterator our child processes may finished. --- tests/test_testgres_common.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py index e35e04f..c83264f 100644 --- a/tests/test_testgres_common.py +++ b/tests/test_testgres_common.py @@ -365,6 +365,16 @@ def test_child_processes__ok( logging.info("Children count is {}".format(len(children))) logging.info("") + def LOCAL__safe_call_cmdline(p: ProcessProxy) -> str: + assert type(p) == ProcessProxy # noqa: E721 + try: + return p.cmdline() + except Exception as e: + return "Exception ({}): {}".format( + type(e).__name__, + e, + ) + for i in range(len(children)): logging.info("------ check child [{}]".format(i)) child = children[i] @@ -385,7 +395,7 @@ def test_child_processes__ok( logging.info("ptype is {}".format(child.ptype)) logging.info("pid is {}".format(child.pid)) - logging.info("cmdline is [{}]".format(child.cmdline())) + logging.info("cmdline is [{}]".format(LOCAL__safe_call_cmdline(child))) except Exception as e: logging.error("Exception ({}): {}".format( type(e).__name__,