From 09dab3139a3cdc88afc1f3a1e39c07a4aaeedbc4 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Thu, 3 Apr 2025 15:10:54 +0300
Subject: [PATCH 01/31] PostgresNode refactoring [PostgresNodePortManager and
 RO-properties]

PostgresNode uses PostgresNodePortManager to reserve/release port number

New PostgresNode RO-properties are added:
 - name
 - host
 - port
 - ssh_key
---
 testgres/node.py | 163 ++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 141 insertions(+), 22 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 1f8fca6e..ddf7e48d 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -10,6 +10,7 @@
 from queue import Queue
 
 import time
+import typing
 
 try:
     from collections.abc import Iterable
@@ -131,12 +132,47 @@ def __repr__(self):
                                                  repr(self.process))
 
 
+class PostgresNodePortManager:
+    def reserve_port(self) -> int:
+        raise NotImplementedError("PostManager::reserve_port is not implemented.")
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+        raise NotImplementedError("PostManager::release_port is not implemented.")
+
+
+class PostgresNodePortManager__Global(PostgresNodePortManager):
+    def __init__(self):
+        pass
+
+    def reserve_port(self) -> int:
+        return utils.reserve_port()
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+        return utils.release_port(number)
+
+
 class PostgresNode(object):
     # a max number of node start attempts
     _C_MAX_START_ATEMPTS = 5
 
-    def __init__(self, name=None, base_dir=None, port=None, conn_params: ConnectionParams = ConnectionParams(),
-                 bin_dir=None, prefix=None, os_ops=None):
+    _name: typing.Optional[str]
+    _host: typing.Optional[str]
+    _port: typing.Optional[int]
+    _should_free_port: bool
+    _os_ops: OsOperations
+    _port_manager: PostgresNodePortManager
+
+    def __init__(self,
+                 name=None,
+                 base_dir=None,
+                 port: typing.Optional[int] = None,
+                 conn_params: ConnectionParams = ConnectionParams(),
+                 bin_dir=None,
+                 prefix=None,
+                 os_ops: typing.Optional[OsOperations] = None,
+                 port_manager: typing.Optional[PostgresNodePortManager] = None):
         """
         PostgresNode constructor.
 
@@ -145,21 +181,26 @@ def __init__(self, name=None, base_dir=None, port=None, conn_params: ConnectionP
             port: port to accept connections.
             base_dir: path to node's data directory.
             bin_dir: path to node's binary directory.
+            os_ops: None or correct OS operation object.
+            port_manager: None or correct port manager object.
         """
+        assert port is None or type(port) == int  # noqa: E721
+        assert os_ops is None or isinstance(os_ops, OsOperations)
+        assert port_manager is None or isinstance(port_manager, PostgresNodePortManager)
 
         # private
         if os_ops is None:
-            os_ops = __class__._get_os_ops(conn_params)
+            self._os_ops = __class__._get_os_ops(conn_params)
         else:
             assert conn_params is None
+            assert isinstance(os_ops, OsOperations)
+            self._os_ops = os_ops
             pass
 
-        assert os_ops is not None
-        assert isinstance(os_ops, OsOperations)
-        self._os_ops = os_ops
+        assert self._os_ops is not None
+        assert isinstance(self._os_ops, OsOperations)
 
-        self._pg_version = PgVer(get_pg_version2(os_ops, bin_dir))
-        self._should_free_port = port is None
+        self._pg_version = PgVer(get_pg_version2(self._os_ops, bin_dir))
         self._base_dir = base_dir
         self._bin_dir = bin_dir
         self._prefix = prefix
@@ -167,12 +208,30 @@ def __init__(self, name=None, base_dir=None, port=None, conn_params: ConnectionP
         self._master = None
 
         # basic
-        self.name = name or generate_app_name()
+        self._name = name or generate_app_name()
+        self._host = self._os_ops.host
 
-        self.host = os_ops.host
-        self.port = port or utils.reserve_port()
+        if port is not None:
+            assert type(port) == int  # noqa: E721
+            assert port_manager is None
+            self._port = port
+            self._should_free_port = False
+            self._port_manager = None
+        else:
+            if port_manager is not None:
+                assert isinstance(port_manager, PostgresNodePortManager)
+                self._port_manager = port_manager
+            else:
+                self._port_manager = __class__._get_port_manager(self._os_ops)
+
+            assert self._port_manager is not None
+            assert isinstance(self._port_manager, PostgresNodePortManager)
+
+            self._port = self._port_manager.reserve_port()  # raises
+            assert type(self._port) == int  # noqa: E721
+            self._should_free_port = True
 
-        self.ssh_key = os_ops.ssh_key
+        assert type(self._port) == int  # noqa: E721
 
         # defaults for __exit__()
         self.cleanup_on_good_exit = testgres_config.node_cleanup_on_good_exit
@@ -207,7 +266,11 @@ def __exit__(self, type, value, traceback):
 
     def __repr__(self):
         return "{}(name='{}', port={}, base_dir='{}')".format(
-            self.__class__.__name__, self.name, self.port, self.base_dir)
+            self.__class__.__name__,
+            self.name,
+            str(self._port) if self._port is not None else "None",
+            self.base_dir
+        )
 
     @staticmethod
     def _get_os_ops(conn_params: ConnectionParams) -> OsOperations:
@@ -221,12 +284,28 @@ def _get_os_ops(conn_params: ConnectionParams) -> OsOperations:
 
         return LocalOperations(conn_params)
 
+    @staticmethod
+    def _get_port_manager(os_ops: OsOperations) -> PostgresNodePortManager:
+        assert os_ops is not None
+        assert isinstance(os_ops, OsOperations)
+
+        # [2025-04-03] It is our old, incorrected behaviour
+        return PostgresNodePortManager__Global()
+
     def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
         assert name is None or type(name) == str  # noqa: E721
         assert base_dir is None or type(base_dir) == str  # noqa: E721
 
         assert __class__ == PostgresNode
 
+        if self._port_manager is None:
+            raise InvalidOperationException("PostgresNode without PortManager can't be cloned.")
+
+        assert self._port_manager is not None
+        assert isinstance(self._port_manager, PostgresNodePortManager)
+        assert self._os_ops is not None
+        assert isinstance(self._os_ops, OsOperations)
+
         node = PostgresNode(
             name=name,
             base_dir=base_dir,
@@ -243,6 +322,34 @@ def os_ops(self) -> OsOperations:
         assert isinstance(self._os_ops, OsOperations)
         return self._os_ops
 
+    @property
+    def name(self) -> str:
+        if self._name is None:
+            raise InvalidOperationException("PostgresNode name is not defined.")
+        assert type(self._name) == str  # noqa: E721
+        return self._name
+
+    @property
+    def host(self) -> str:
+        if self._host is None:
+            raise InvalidOperationException("PostgresNode host is not defined.")
+        assert type(self._host) == str  # noqa: E721
+        return self._host
+
+    @property
+    def port(self) -> int:
+        if self._port is None:
+            raise InvalidOperationException("PostgresNode port is not defined.")
+
+        assert type(self._port) == int  # noqa: E721
+        return self._port
+
+    @property
+    def ssh_key(self) -> typing.Optional[str]:
+        assert self._os_ops is not None
+        assert isinstance(self._os_ops, OsOperations)
+        return self._os_ops.ssh_key
+
     @property
     def pid(self):
         """
@@ -993,6 +1100,11 @@ def start(self, params=[], wait=True):
         if self.is_started:
             return self
 
+        if self._port is None:
+            raise InvalidOperationException("Can't start PostgresNode. Port is node defined.")
+
+        assert type(self._port) == int  # noqa: E721
+
         _params = [self._get_bin_path("pg_ctl"),
                    "-D", self.data_dir,
                    "-l", self.pg_log_file,
@@ -1023,6 +1135,8 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
                 LOCAL__raise_cannot_start_node__std(e)
         else:
             assert self._should_free_port
+            assert self._port_manager is not None
+            assert isinstance(self._port_manager, PostgresNodePortManager)
             assert __class__._C_MAX_START_ATEMPTS > 1
 
             log_files0 = self._collect_log_files()
@@ -1048,20 +1162,20 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
 
                     log_files0 = log_files1
                     logging.warning(
-                        "Detected a conflict with using the port {0}. Trying another port after a {1}-second sleep...".format(self.port, timeout)
+                        "Detected a conflict with using the port {0}. Trying another port after a {1}-second sleep...".format(self._port, timeout)
                     )
                     time.sleep(timeout)
                     timeout = min(2 * timeout, 5)
-                    cur_port = self.port
-                    new_port = utils.reserve_port()  # can raise
+                    cur_port = self._port
+                    new_port = self._port_manager.reserve_port()  # can raise
                     try:
                         options = {'port': new_port}
                         self.set_auto_conf(options)
                     except:  # noqa: E722
-                        utils.release_port(new_port)
+                        self._port_manager.release_port(new_port)
                         raise
-                    self.port = new_port
-                    utils.release_port(cur_port)
+                    self._port = new_port
+                    self._port_manager.release_port(cur_port)
                     continue
                 break
         self._maybe_start_logger()
@@ -1226,10 +1340,15 @@ def free_port(self):
         """
 
         if self._should_free_port:
-            port = self.port
+            assert type(self._port) == int  # noqa: E721
+
+            assert self._port_manager is not None
+            assert isinstance(self._port_manager, PostgresNodePortManager)
+
+            port = self._port
             self._should_free_port = False
-            self.port = None
-            utils.release_port(port)
+            self._port = None
+            self._port_manager.release_port(port)
 
     def cleanup(self, max_attempts=3, full=False):
         """

From ef095d39a8a0b14bcc32368faf43eeb708b924a9 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:17:03 +0300
Subject: [PATCH 02/31] [FIX] clone_with_new_name_and_base_dir did not respect
 port_manager

---
 testgres/node.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/testgres/node.py b/testgres/node.py
index ddf7e48d..32ebca6e 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -312,7 +312,8 @@ def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
             conn_params=None,
             bin_dir=self._bin_dir,
             prefix=self._prefix,
-            os_ops=self._os_ops)
+            os_ops=self._os_ops,
+            port_manager=self._port_manager)
 
         return node
 

From 0f842fdbd7eaf2944a88047cfeb78e1a543f2923 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:21:49 +0300
Subject: [PATCH 03/31] PostgresNodePortManager__ThisHost is defined (was:
 PostgresNodePortManager__Global)

It is a singleton.
---
 testgres/node.py | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 32ebca6e..1802bff5 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -1,4 +1,6 @@
 # coding: utf-8
+from __future__ import annotations
+
 import logging
 import os
 import random
@@ -133,6 +135,9 @@ def __repr__(self):
 
 
 class PostgresNodePortManager:
+    def __init__(self):
+        super().__init__()
+
     def reserve_port(self) -> int:
         raise NotImplementedError("PostManager::reserve_port is not implemented.")
 
@@ -141,10 +146,24 @@ def release_port(self, number: int) -> None:
         raise NotImplementedError("PostManager::release_port is not implemented.")
 
 
-class PostgresNodePortManager__Global(PostgresNodePortManager):
+class PostgresNodePortManager__ThisHost(PostgresNodePortManager):
+    sm_single_instance: PostgresNodePortManager = None
+    sm_single_instance_guard = threading.Lock()
+
     def __init__(self):
         pass
 
+    def __new__(cls) -> PostgresNodePortManager:
+        assert __class__ == PostgresNodePortManager__ThisHost
+        assert __class__.sm_single_instance_guard is not None
+
+        if __class__.sm_single_instance is None:
+            with __class__.sm_single_instance_guard:
+                __class__.sm_single_instance = super().__new__(cls)
+        assert __class__.sm_single_instance
+        assert type(__class__.sm_single_instance) == __class__  # noqa: E721
+        return __class__.sm_single_instance
+
     def reserve_port(self) -> int:
         return utils.reserve_port()
 
@@ -290,7 +309,7 @@ def _get_port_manager(os_ops: OsOperations) -> PostgresNodePortManager:
         assert isinstance(os_ops, OsOperations)
 
         # [2025-04-03] It is our old, incorrected behaviour
-        return PostgresNodePortManager__Global()
+        return PostgresNodePortManager__ThisHost()
 
     def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
         assert name is None or type(name) == str  # noqa: E721

From e1e609e6dd5fe705c6c63c9bc6f3fb045992c268 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:25:23 +0300
Subject: [PATCH 04/31] PostgresNodePortManager__Generic is added

---
 testgres/node.py                  | 55 ++++++++++++++++++++++++-
 testgres/operations/local_ops.py  | 11 +++++
 testgres/operations/os_ops.py     |  4 ++
 testgres/operations/remote_ops.py | 38 ++++++++++++++++++
 tests/test_testgres_remote.py     | 67 -------------------------------
 5 files changed, 106 insertions(+), 69 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 1802bff5..7e112751 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -100,6 +100,8 @@
     options_string, \
     clean_on_error
 
+from .helpers.port_manager import PortForException
+
 from .backup import NodeBackup
 
 from .operations.os_ops import ConnectionParams
@@ -172,6 +174,52 @@ def release_port(self, number: int) -> None:
         return utils.release_port(number)
 
 
+class PostgresNodePortManager__Generic(PostgresNodePortManager):
+    _os_ops: OsOperations
+    _allocated_ports_guard: object
+    _allocated_ports: set[int]
+
+    def __init__(self, os_ops: OsOperations):
+        assert os_ops is not None
+        assert isinstance(os_ops, OsOperations)
+        self._os_ops = os_ops
+        self._allocated_ports_guard = threading.Lock()
+        self._allocated_ports = set[int]()
+
+    def reserve_port(self) -> int:
+        ports = set(range(1024, 65535))
+        assert type(ports) == set  # noqa: E721
+
+        assert self._allocated_ports_guard is not None
+        assert type(self._allocated_ports) == set  # noqa: E721
+
+        with self._allocated_ports_guard:
+            ports.difference_update(self._allocated_ports)
+
+            sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
+
+            for port in sampled_ports:
+                assert not (port in self._allocated_ports)
+
+                if not self._os_ops.is_port_free(port):
+                    continue
+
+                self._allocated_ports.add(port)
+                return port
+
+        raise PortForException("Can't select a port")
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+
+        assert self._allocated_ports_guard is not None
+        assert type(self._allocated_ports) == set  # noqa: E721
+
+        with self._allocated_ports_guard:
+            assert number in self._allocated_ports
+            self._allocated_ports.discard(number)
+
+
 class PostgresNode(object):
     # a max number of node start attempts
     _C_MAX_START_ATEMPTS = 5
@@ -308,8 +356,11 @@ def _get_port_manager(os_ops: OsOperations) -> PostgresNodePortManager:
         assert os_ops is not None
         assert isinstance(os_ops, OsOperations)
 
-        # [2025-04-03] It is our old, incorrected behaviour
-        return PostgresNodePortManager__ThisHost()
+        if isinstance(os_ops, LocalOperations):
+            return PostgresNodePortManager__ThisHost()
+
+        # TODO: Throw exception "Please define a port manager."
+        return PostgresNodePortManager__Generic(os_ops)
 
     def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
         assert name is None or type(name) == str  # noqa: E721
diff --git a/testgres/operations/local_ops.py b/testgres/operations/local_ops.py
index 35e94210..39c81405 100644
--- a/testgres/operations/local_ops.py
+++ b/testgres/operations/local_ops.py
@@ -6,6 +6,7 @@
 import subprocess
 import tempfile
 import time
+import socket
 
 import psutil
 
@@ -436,6 +437,16 @@ def get_process_children(self, pid):
         assert type(pid) == int  # noqa: E721
         return psutil.Process(pid).children()
 
+    def is_port_free(self, number: int) -> bool:
+        assert type(number) == int  # noqa: E721
+
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            try:
+                s.bind(("", number))
+                return True
+            except OSError:
+                return False
+
     # Database control
     def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
         conn = pglib.connect(
diff --git a/testgres/operations/os_ops.py b/testgres/operations/os_ops.py
index 3c606871..489a7cb2 100644
--- a/testgres/operations/os_ops.py
+++ b/testgres/operations/os_ops.py
@@ -127,6 +127,10 @@ def get_pid(self):
     def get_process_children(self, pid):
         raise NotImplementedError()
 
+    def is_port_free(self, number: int):
+        assert type(number) == int  # noqa: E721
+        raise NotImplementedError()
+
     # Database control
     def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
         raise NotImplementedError()
diff --git a/testgres/operations/remote_ops.py b/testgres/operations/remote_ops.py
index e1ad6dac..0547a262 100644
--- a/testgres/operations/remote_ops.py
+++ b/testgres/operations/remote_ops.py
@@ -629,6 +629,44 @@ def get_process_children(self, pid):
 
         raise ExecUtilException(f"Error in getting process children. Error: {result.stderr}")
 
+    def is_port_free(self, number: int) -> bool:
+        assert type(number) == int  # noqa: E721
+
+        cmd = ["nc", "-w", "5", "-z", "-v", "localhost", str(number)]
+
+        exit_status, output, error = self.exec_command(cmd=cmd, encoding=get_default_encoding(), ignore_errors=True, verbose=True)
+
+        assert type(output) == str  # noqa: E721
+        assert type(error) == str  # noqa: E721
+
+        if exit_status == 0:
+            return __class__.helper__is_port_free__process_0(output)
+
+        if exit_status == 1:
+            return __class__.helper__is_port_free__process_1(error)
+
+        errMsg = "nc returns an unknown result code: {0}".format(exit_status)
+
+        RaiseError.CommandExecutionError(
+            cmd=cmd,
+            exit_code=exit_status,
+            msg_arg=errMsg,
+            error=error,
+            out=output
+        )
+
+    @staticmethod
+    def helper__is_port_free__process_0(output: str) -> bool:
+        assert type(output) == str  # noqa: E721
+        # TODO: check output message
+        return False
+
+    @staticmethod
+    def helper__is_port_free__process_1(error: str) -> bool:
+        assert type(error) == str  # noqa: E721
+        # TODO: check error message
+        return True
+
     # Database control
     def db_connect(self, dbname, user, password=None, host="localhost", port=5432):
         conn = pglib.connect(
diff --git a/tests/test_testgres_remote.py b/tests/test_testgres_remote.py
index 2142e5ba..a2aaa18e 100755
--- a/tests/test_testgres_remote.py
+++ b/tests/test_testgres_remote.py
@@ -4,7 +4,6 @@
 import subprocess
 
 import pytest
-import psutil
 import logging
 
 from .helpers.os_ops_descrs import OsOpsDescrs
@@ -27,8 +26,6 @@
     get_pg_config
 
 # NOTE: those are ugly imports
-from ..testgres import bound_ports
-from ..testgres.node import ProcessProxy
 
 
 def util_exists(util):
@@ -259,70 +256,6 @@ def test_unix_sockets(self):
                 assert (res_exec == [(1,)])
                 assert (res_psql == b'1\n')
 
-    def test_ports_management(self):
-        assert bound_ports is not None
-        assert type(bound_ports) == set  # noqa: E721
-
-        if len(bound_ports) != 0:
-            logging.warning("bound_ports is not empty: {0}".format(bound_ports))
-
-        stage0__bound_ports = bound_ports.copy()
-
-        with __class__.helper__get_node() as node:
-            assert bound_ports is not None
-            assert type(bound_ports) == set  # noqa: E721
-
-            assert node.port is not None
-            assert type(node.port) == int  # noqa: E721
-
-            logging.info("node port is {0}".format(node.port))
-
-            assert node.port in bound_ports
-            assert node.port not in stage0__bound_ports
-
-            assert stage0__bound_ports <= bound_ports
-            assert len(stage0__bound_ports) + 1 == len(bound_ports)
-
-            stage1__bound_ports = stage0__bound_ports.copy()
-            stage1__bound_ports.add(node.port)
-
-            assert stage1__bound_ports == bound_ports
-
-        # check that port has been freed successfully
-        assert bound_ports is not None
-        assert type(bound_ports) == set  # noqa: E721
-        assert bound_ports == stage0__bound_ports
-
-    # TODO: Why does not this test work with remote host?
-    def test_child_process_dies(self):
-        nAttempt = 0
-
-        while True:
-            if nAttempt == 5:
-                raise Exception("Max attempt number is exceed.")
-
-            nAttempt += 1
-
-            logging.info("Attempt #{0}".format(nAttempt))
-
-            # test for FileNotFound exception during child_processes() function
-            with subprocess.Popen(["sleep", "60"]) as process:
-                r = process.poll()
-
-                if r is not None:
-                    logging.warning("process.pool() returns an unexpected result: {0}.".format(r))
-                    continue
-
-                assert r is None
-                # collect list of processes currently running
-                children = psutil.Process(os.getpid()).children()
-                # kill a process, so received children dictionary becomes invalid
-                process.kill()
-                process.wait()
-                # try to handle children list -- missing processes will have ptype "ProcessType.Unknown"
-                [ProcessProxy(p) for p in children]
-                break
-
     @staticmethod
     def helper__get_node(name=None):
         assert isinstance(__class__.sm_os_ops, OsOperations)

From 285e5b7dd1b35fbfd6d94829a9e3d282bebfef73 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:30:31 +0300
Subject: [PATCH 05/31] PostgresNodePortManager is added in public API

---
 testgres/__init__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/testgres/__init__.py b/testgres/__init__.py
index 665548d6..f8df8c24 100644
--- a/testgres/__init__.py
+++ b/testgres/__init__.py
@@ -34,6 +34,7 @@
     DumpFormat
 
 from .node import PostgresNode, NodeApp
+from .node import PostgresNodePortManager
 
 from .utils import \
     reserve_port, \
@@ -64,6 +65,7 @@
     "TestgresException", "ExecUtilException", "QueryException", "TimeoutException", "CatchUpException", "StartNodeException", "InitNodeException", "BackupException", "InvalidOperationException",
     "XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
     "PostgresNode", "NodeApp",
+    "PostgresNodePortManager",
     "reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
     "First", "Any", "PortManager",
     "OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"

From a97486675d5189f6c96adb368fd043cf646513a0 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:32:02 +0300
Subject: [PATCH 06/31] Test structures were refactored (local, local2, remote)

---
 tests/helpers/global_data.py   |  78 +++++++++
 tests/helpers/os_ops_descrs.py |  32 ----
 tests/test_os_ops_common.py    |   6 +-
 tests/test_os_ops_local.py     |   4 +-
 tests/test_os_ops_remote.py    |   4 +-
 tests/test_testgres_common.py  | 301 +++++++++++++++++----------------
 tests/test_testgres_remote.py  |  29 ++--
 7 files changed, 262 insertions(+), 192 deletions(-)
 create mode 100644 tests/helpers/global_data.py
 delete mode 100644 tests/helpers/os_ops_descrs.py

diff --git a/tests/helpers/global_data.py b/tests/helpers/global_data.py
new file mode 100644
index 00000000..ea7b2385
--- /dev/null
+++ b/tests/helpers/global_data.py
@@ -0,0 +1,78 @@
+from ...testgres.operations.os_ops import OsOperations
+from ...testgres.operations.os_ops import ConnectionParams
+from ...testgres.operations.local_ops import LocalOperations
+from ...testgres.operations.remote_ops import RemoteOperations
+
+from ...testgres.node import PostgresNodePortManager
+from ...testgres.node import PostgresNodePortManager__ThisHost
+from ...testgres.node import PostgresNodePortManager__Generic
+
+import os
+
+
+class OsOpsDescr:
+    sign: str
+    os_ops: OsOperations
+
+    def __init__(self, sign: str, os_ops: OsOperations):
+        assert type(sign) == str  # noqa: E721
+        assert isinstance(os_ops, OsOperations)
+        self.sign = sign
+        self.os_ops = os_ops
+
+
+class OsOpsDescrs:
+    sm_remote_conn_params = ConnectionParams(
+        host=os.getenv('RDBMS_TESTPOOL1_HOST') or '127.0.0.1',
+        username=os.getenv('USER'),
+        ssh_key=os.getenv('RDBMS_TESTPOOL_SSHKEY'))
+
+    sm_remote_os_ops = RemoteOperations(sm_remote_conn_params)
+
+    sm_remote_os_ops_descr = OsOpsDescr("remote_ops", sm_remote_os_ops)
+
+    sm_local_os_ops = LocalOperations()
+
+    sm_local_os_ops_descr = OsOpsDescr("local_ops", sm_local_os_ops)
+
+
+class PortManagers:
+    sm_remote_port_manager = PostgresNodePortManager__Generic(OsOpsDescrs.sm_remote_os_ops)
+
+    sm_local_port_manager = PostgresNodePortManager__ThisHost()
+
+    sm_local2_port_manager = PostgresNodePortManager__Generic(OsOpsDescrs.sm_local_os_ops)
+
+
+class PostgresNodeService:
+    sign: str
+    os_ops: OsOperations
+    port_manager: PostgresNodePortManager
+
+    def __init__(self, sign: str, os_ops: OsOperations, port_manager: PostgresNodePortManager):
+        assert type(sign) == str  # noqa: E721
+        assert isinstance(os_ops, OsOperations)
+        assert isinstance(port_manager, PostgresNodePortManager)
+        self.sign = sign
+        self.os_ops = os_ops
+        self.port_manager = port_manager
+
+
+class PostgresNodeServices:
+    sm_remote = PostgresNodeService(
+        "remote",
+        OsOpsDescrs.sm_remote_os_ops,
+        PortManagers.sm_remote_port_manager
+    )
+
+    sm_local = PostgresNodeService(
+        "local",
+        OsOpsDescrs.sm_local_os_ops,
+        PortManagers.sm_local_port_manager
+    )
+
+    sm_local2 = PostgresNodeService(
+        "local2",
+        OsOpsDescrs.sm_local_os_ops,
+        PortManagers.sm_local2_port_manager
+    )
diff --git a/tests/helpers/os_ops_descrs.py b/tests/helpers/os_ops_descrs.py
deleted file mode 100644
index 02297adb..00000000
--- a/tests/helpers/os_ops_descrs.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from ...testgres.operations.os_ops import OsOperations
-from ...testgres.operations.os_ops import ConnectionParams
-from ...testgres.operations.local_ops import LocalOperations
-from ...testgres.operations.remote_ops import RemoteOperations
-
-import os
-
-
-class OsOpsDescr:
-    os_ops: OsOperations
-    sign: str
-
-    def __init__(self, os_ops: OsOperations, sign: str):
-        assert isinstance(os_ops, OsOperations)
-        assert type(sign) == str  # noqa: E721
-        self.os_ops = os_ops
-        self.sign = sign
-
-
-class OsOpsDescrs:
-    sm_remote_conn_params = ConnectionParams(
-        host=os.getenv('RDBMS_TESTPOOL1_HOST') or '127.0.0.1',
-        username=os.getenv('USER'),
-        ssh_key=os.getenv('RDBMS_TESTPOOL_SSHKEY'))
-
-    sm_remote_os_ops = RemoteOperations(sm_remote_conn_params)
-
-    sm_remote_os_ops_descr = OsOpsDescr(sm_remote_os_ops, "remote_ops")
-
-    sm_local_os_ops = LocalOperations()
-
-    sm_local_os_ops_descr = OsOpsDescr(sm_local_os_ops, "local_ops")
diff --git a/tests/test_os_ops_common.py b/tests/test_os_ops_common.py
index c3944c3b..1bcc054c 100644
--- a/tests/test_os_ops_common.py
+++ b/tests/test_os_ops_common.py
@@ -1,7 +1,7 @@
 # coding: utf-8
-from .helpers.os_ops_descrs import OsOpsDescr
-from .helpers.os_ops_descrs import OsOpsDescrs
-from .helpers.os_ops_descrs import OsOperations
+from .helpers.global_data import OsOpsDescr
+from .helpers.global_data import OsOpsDescrs
+from .helpers.global_data import OsOperations
 from .helpers.run_conditions import RunConditions
 
 import os
diff --git a/tests/test_os_ops_local.py b/tests/test_os_ops_local.py
index 2e3c30b7..f60c3fc9 100644
--- a/tests/test_os_ops_local.py
+++ b/tests/test_os_ops_local.py
@@ -1,6 +1,6 @@
 # coding: utf-8
-from .helpers.os_ops_descrs import OsOpsDescrs
-from .helpers.os_ops_descrs import OsOperations
+from .helpers.global_data import OsOpsDescrs
+from .helpers.global_data import OsOperations
 
 import os
 
diff --git a/tests/test_os_ops_remote.py b/tests/test_os_ops_remote.py
index 58b09242..338e49f3 100755
--- a/tests/test_os_ops_remote.py
+++ b/tests/test_os_ops_remote.py
@@ -1,7 +1,7 @@
 # coding: utf-8
 
-from .helpers.os_ops_descrs import OsOpsDescrs
-from .helpers.os_ops_descrs import OsOperations
+from .helpers.global_data import OsOpsDescrs
+from .helpers.global_data import OsOperations
 
 from ..testgres import ExecUtilException
 
diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 4e23c4af..5f88acd0 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1,6 +1,7 @@
-from .helpers.os_ops_descrs import OsOpsDescr
-from .helpers.os_ops_descrs import OsOpsDescrs
-from .helpers.os_ops_descrs import OsOperations
+from .helpers.global_data import PostgresNodeService
+from .helpers.global_data import PostgresNodeServices
+from .helpers.global_data import OsOperations
+from .helpers.global_data import PostgresNodePortManager
 
 from ..testgres.node import PgVer
 from ..testgres.node import PostgresNode
@@ -54,22 +55,25 @@ def removing(os_ops: OsOperations, f):
 
 
 class TestTestgresCommon:
-    sm_os_ops_descrs: list[OsOpsDescr] = [
-        OsOpsDescrs.sm_local_os_ops_descr,
-        OsOpsDescrs.sm_remote_os_ops_descr
+    sm_node_svcs: list[PostgresNodeService] = [
+        PostgresNodeServices.sm_local,
+        PostgresNodeServices.sm_local2,
+        PostgresNodeServices.sm_remote,
     ]
 
     @pytest.fixture(
-        params=[descr.os_ops for descr in sm_os_ops_descrs],
-        ids=[descr.sign for descr in sm_os_ops_descrs]
+        params=sm_node_svcs,
+        ids=[descr.sign for descr in sm_node_svcs]
     )
-    def os_ops(self, request: pytest.FixtureRequest) -> OsOperations:
+    def node_svc(self, request: pytest.FixtureRequest) -> PostgresNodeService:
         assert isinstance(request, pytest.FixtureRequest)
-        assert isinstance(request.param, OsOperations)
+        assert isinstance(request.param, PostgresNodeService)
+        assert isinstance(request.param.os_ops, OsOperations)
+        assert isinstance(request.param.port_manager, PostgresNodePortManager)
         return request.param
 
-    def test_version_management(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_version_management(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
         a = PgVer('10.0')
         b = PgVer('10')
@@ -93,42 +97,42 @@ def test_version_management(self, os_ops: OsOperations):
         assert (g == k)
         assert (g > h)
 
-        version = get_pg_version2(os_ops)
+        version = get_pg_version2(node_svc.os_ops)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             assert (isinstance(version, six.string_types))
             assert (isinstance(node.version, PgVer))
             assert (node.version == PgVer(version))
 
-    def test_double_init(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_double_init(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops).init() as node:
+        with __class__.helper__get_node(node_svc).init() as node:
             # can't initialize node more than once
             with pytest.raises(expected_exception=InitNodeException):
                 node.init()
 
-    def test_init_after_cleanup(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_init_after_cleanup(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             node.init().start().execute('select 1')
             node.cleanup()
             node.init().start().execute('select 1')
 
-    def test_init_unique_system_id(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_init_unique_system_id(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
         # this function exists in PostgreSQL 9.6+
-        current_version = get_pg_version2(os_ops)
+        current_version = get_pg_version2(node_svc.os_ops)
 
-        __class__.helper__skip_test_if_util_not_exist(os_ops, "pg_resetwal")
+        __class__.helper__skip_test_if_util_not_exist(node_svc.os_ops, "pg_resetwal")
         __class__.helper__skip_test_if_pg_version_is_not_ge(current_version, '9.6')
 
         query = 'select system_identifier from pg_control_system()'
 
         with scoped_config(cache_initdb=False):
-            with __class__.helper__get_node(os_ops).init().start() as node0:
+            with __class__.helper__get_node(node_svc).init().start() as node0:
                 id0 = node0.execute(query)[0]
 
         with scoped_config(cache_initdb=True,
@@ -137,8 +141,8 @@ def test_init_unique_system_id(self, os_ops: OsOperations):
             assert (config.cached_initdb_unique)
 
             # spawn two nodes; ids must be different
-            with __class__.helper__get_node(os_ops).init().start() as node1, \
-                    __class__.helper__get_node(os_ops).init().start() as node2:
+            with __class__.helper__get_node(node_svc).init().start() as node1, \
+                    __class__.helper__get_node(node_svc).init().start() as node2:
                 id1 = node1.execute(query)[0]
                 id2 = node2.execute(query)[0]
 
@@ -146,44 +150,44 @@ def test_init_unique_system_id(self, os_ops: OsOperations):
                 assert (id1 > id0)
                 assert (id2 > id1)
 
-    def test_node_exit(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_node_exit(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
         with pytest.raises(expected_exception=QueryException):
-            with __class__.helper__get_node(os_ops).init() as node:
+            with __class__.helper__get_node(node_svc).init() as node:
                 base_dir = node.base_dir
                 node.safe_psql('select 1')
 
         # we should save the DB for "debugging"
-        assert (os_ops.path_exists(base_dir))
-        os_ops.rmdirs(base_dir, ignore_errors=True)
+        assert (node_svc.os_ops.path_exists(base_dir))
+        node_svc.os_ops.rmdirs(base_dir, ignore_errors=True)
 
-        with __class__.helper__get_node(os_ops).init() as node:
+        with __class__.helper__get_node(node_svc).init() as node:
             base_dir = node.base_dir
 
         # should have been removed by default
-        assert not (os_ops.path_exists(base_dir))
+        assert not (node_svc.os_ops.path_exists(base_dir))
 
-    def test_double_start(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_double_start(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops).init().start() as node:
+        with __class__.helper__get_node(node_svc).init().start() as node:
             # can't start node more than once
             node.start()
             assert (node.is_started)
 
-    def test_uninitialized_start(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_uninitialized_start(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             # node is not initialized yet
             with pytest.raises(expected_exception=StartNodeException):
                 node.start()
 
-    def test_restart(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_restart(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             node.init().start()
 
             # restart, ok
@@ -198,10 +202,10 @@ def test_restart(self, os_ops: OsOperations):
                 node.append_conf('pg_hba.conf', 'DUMMY')
                 node.restart()
 
-    def test_reload(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_reload(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             node.init().start()
 
             # change client_min_messages and save old value
@@ -216,24 +220,24 @@ def test_reload(self, os_ops: OsOperations):
             assert ('debug1' == cmm_new[0][0].lower())
             assert (cmm_old != cmm_new)
 
-    def test_pg_ctl(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_pg_ctl(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             node.init().start()
 
             status = node.pg_ctl(['status'])
             assert ('PID' in status)
 
-    def test_status(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_status(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
         assert (NodeStatus.Running)
         assert not (NodeStatus.Stopped)
         assert not (NodeStatus.Uninitialized)
 
         # check statuses after each operation
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             assert (node.pid == 0)
             assert (node.status() == NodeStatus.Uninitialized)
 
@@ -257,8 +261,8 @@ def test_status(self, os_ops: OsOperations):
             assert (node.pid == 0)
             assert (node.status() == NodeStatus.Uninitialized)
 
-    def test_child_pids(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_child_pids(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
         master_processes = [
             ProcessType.AutovacuumLauncher,
@@ -269,7 +273,7 @@ def test_child_pids(self, os_ops: OsOperations):
             ProcessType.WalWriter,
         ]
 
-        postgresVersion = get_pg_version2(os_ops)
+        postgresVersion = get_pg_version2(node_svc.os_ops)
 
         if __class__.helper__pg_version_ge(postgresVersion, '10'):
             master_processes.append(ProcessType.LogicalReplicationLauncher)
@@ -338,7 +342,7 @@ def LOCAL__check_auxiliary_pids__multiple_attempts(
                 absenceList
             ))
 
-        with __class__.helper__get_node(os_ops).init().start() as master:
+        with __class__.helper__get_node(node_svc).init().start() as master:
 
             # master node doesn't have a source walsender!
             with pytest.raises(expected_exception=testgres_TestgresException):
@@ -380,10 +384,10 @@ def test_exceptions(self):
         str(ExecUtilException('msg', 'cmd', 1, 'out'))
         str(QueryException('msg', 'query'))
 
-    def test_auto_name(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_auto_name(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        with __class__.helper__get_node(os_ops).init(allow_streaming=True).start() as m:
+        with __class__.helper__get_node(node_svc).init(allow_streaming=True).start() as m:
             with m.replicate().start() as r:
                 # check that nodes are running
                 assert (m.status())
@@ -417,9 +421,9 @@ def test_file_tail(self):
             lines = file_tail(f, 1)
             assert (lines[0] == s3)
 
-    def test_isolation_levels(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops).init().start() as node:
+    def test_isolation_levels(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc).init().start() as node:
             with node.connect() as con:
                 # string levels
                 con.begin('Read Uncommitted').commit()
@@ -437,17 +441,17 @@ def test_isolation_levels(self, os_ops: OsOperations):
                 with pytest.raises(expected_exception=QueryException):
                     con.begin('Garbage').commit()
 
-    def test_users(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops).init().start() as node:
+    def test_users(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc).init().start() as node:
             node.psql('create role test_user login')
             value = node.safe_psql('select 1', username='test_user')
             value = __class__.helper__rm_carriage_returns(value)
             assert (value == b'1\n')
 
-    def test_poll_query_until(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_poll_query_until(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init().start()
 
             get_time = 'select extract(epoch from now())'
@@ -507,8 +511,8 @@ def test_poll_query_until(self, os_ops: OsOperations):
             # check 1 arg, ok
             node.poll_query_until('select true')
 
-    def test_logging(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_logging(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
         C_MAX_ATTEMPTS = 50
         # This name is used for testgres logging, too.
         C_NODE_NAME = "testgres_tests." + __class__.__name__ + "test_logging-master-" + uuid.uuid4().hex
@@ -529,7 +533,7 @@ def test_logging(self, os_ops: OsOperations):
                 logger.addHandler(handler)
 
                 with scoped_config(use_python_logging=True):
-                    with __class__.helper__get_node(os_ops, name=C_NODE_NAME) as master:
+                    with __class__.helper__get_node(node_svc, name=C_NODE_NAME) as master:
                         logging.info("Master node is initilizing")
                         master.init()
 
@@ -599,9 +603,9 @@ def LOCAL__test_lines():
         # GO HOME!
         return
 
-    def test_psql(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops).init().start() as node:
+    def test_psql(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc).init().start() as node:
 
             # check returned values (1 arg)
             res = node.psql('select 1')
@@ -636,17 +640,20 @@ def test_psql(self, os_ops: OsOperations):
 
             # check psql's default args, fails
             with pytest.raises(expected_exception=QueryException):
-                node.psql()
+                r = node.psql()  # raises!
+                logging.error("node.psql returns [{}]".format(r))
 
             node.stop()
 
             # check psql on stopped node, fails
             with pytest.raises(expected_exception=QueryException):
-                node.safe_psql('select 1')
+                # [2025-04-03] This call does not raise exception! I do not know why.
+                r = node.safe_psql('select 1')  # raises!
+                logging.error("node.safe_psql returns [{}]".format(r))
 
-    def test_safe_psql__expect_error(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops).init().start() as node:
+    def test_safe_psql__expect_error(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc).init().start() as node:
             err = node.safe_psql('select_or_not_select 1', expect_error=True)
             assert (type(err) == str)  # noqa: E721
             assert ('select_or_not_select' in err)
@@ -663,9 +670,9 @@ def test_safe_psql__expect_error(self, os_ops: OsOperations):
             res = node.safe_psql("select 1;", expect_error=False)
             assert (__class__.helper__rm_carriage_returns(res) == b'1\n')
 
-    def test_transactions(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops).init().start() as node:
+    def test_transactions(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc).init().start() as node:
 
             with node.connect() as con:
                 con.begin()
@@ -688,9 +695,9 @@ def test_transactions(self, os_ops: OsOperations):
                 con.execute('drop table test')
                 con.commit()
 
-    def test_control_data(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_control_data(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
 
             # node is not initialized yet
             with pytest.raises(expected_exception=ExecUtilException):
@@ -703,9 +710,9 @@ def test_control_data(self, os_ops: OsOperations):
             assert data is not None
             assert (any('pg_control' in s for s in data.keys()))
 
-    def test_backup_simple(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as master:
+    def test_backup_simple(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as master:
 
             # enable streaming for backups
             master.init(allow_streaming=True)
@@ -725,9 +732,9 @@ def test_backup_simple(self, os_ops: OsOperations):
                     res = slave.execute('select * from test order by i asc')
                     assert (res == [(1, ), (2, ), (3, ), (4, )])
 
-    def test_backup_multiple(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_backup_multiple(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             with node.backup(xlog_method='fetch') as backup1, \
@@ -739,9 +746,9 @@ def test_backup_multiple(self, os_ops: OsOperations):
                         backup.spawn_primary('node2', destroy=False) as node2:
                     assert (node1.base_dir != node2.base_dir)
 
-    def test_backup_exhaust(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_backup_exhaust(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             with node.backup(xlog_method='fetch') as backup:
@@ -753,9 +760,9 @@ def test_backup_exhaust(self, os_ops: OsOperations):
                 with pytest.raises(expected_exception=BackupException):
                     backup.spawn_primary()
 
-    def test_backup_wrong_xlog_method(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_backup_wrong_xlog_method(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             with pytest.raises(
@@ -764,11 +771,11 @@ def test_backup_wrong_xlog_method(self, os_ops: OsOperations):
             ):
                 node.backup(xlog_method='wrong')
 
-    def test_pg_ctl_wait_option(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_pg_ctl_wait_option(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
         C_MAX_ATTEMPTS = 50
 
-        node = __class__.helper__get_node(os_ops)
+        node = __class__.helper__get_node(node_svc)
         assert node.status() == NodeStatus.Uninitialized
         node.init()
         assert node.status() == NodeStatus.Stopped
@@ -835,9 +842,9 @@ def test_pg_ctl_wait_option(self, os_ops: OsOperations):
         logging.info("OK. Node is stopped.")
         node.cleanup()
 
-    def test_replicate(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_replicate(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             with node.replicate().start() as replica:
@@ -851,14 +858,14 @@ def test_replicate(self, os_ops: OsOperations):
                 res = node.execute('select * from test')
                 assert (res == [])
 
-    def test_synchronous_replication(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_synchronous_replication(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        current_version = get_pg_version2(os_ops)
+        current_version = get_pg_version2(node_svc.os_ops)
 
         __class__.helper__skip_test_if_pg_version_is_not_ge(current_version, "9.6")
 
-        with __class__.helper__get_node(os_ops) as master:
+        with __class__.helper__get_node(node_svc) as master:
             old_version = not __class__.helper__pg_version_ge(current_version, '9.6')
 
             master.init(allow_streaming=True).start()
@@ -897,14 +904,14 @@ def test_synchronous_replication(self, os_ops: OsOperations):
                     res = standby1.safe_psql('select count(*) from abc')
                     assert (__class__.helper__rm_carriage_returns(res) == b'1000000\n')
 
-    def test_logical_replication(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_logical_replication(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        current_version = get_pg_version2(os_ops)
+        current_version = get_pg_version2(node_svc.os_ops)
 
         __class__.helper__skip_test_if_pg_version_is_not_ge(current_version, "10")
 
-        with __class__.helper__get_node(os_ops) as node1, __class__.helper__get_node(os_ops) as node2:
+        with __class__.helper__get_node(node_svc) as node1, __class__.helper__get_node(node_svc) as node2:
             node1.init(allow_logical=True)
             node1.start()
             node2.init().start()
@@ -971,15 +978,15 @@ def test_logical_replication(self, os_ops: OsOperations):
             res = node2.execute('select * from test2')
             assert (res == [('a', ), ('b', )])
 
-    def test_logical_catchup(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_logical_catchup(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
         """ Runs catchup for 100 times to be sure that it is consistent """
 
-        current_version = get_pg_version2(os_ops)
+        current_version = get_pg_version2(node_svc.os_ops)
 
         __class__.helper__skip_test_if_pg_version_is_not_ge(current_version, "10")
 
-        with __class__.helper__get_node(os_ops) as node1, __class__.helper__get_node(os_ops) as node2:
+        with __class__.helper__get_node(node_svc) as node1, __class__.helper__get_node(node_svc) as node2:
             node1.init(allow_logical=True)
             node1.start()
             node2.init().start()
@@ -999,20 +1006,20 @@ def test_logical_catchup(self, os_ops: OsOperations):
                 assert (res == [(i, i, )])
                 node1.execute('delete from test')
 
-    def test_logical_replication_fail(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_logical_replication_fail(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
 
-        current_version = get_pg_version2(os_ops)
+        current_version = get_pg_version2(node_svc.os_ops)
 
         __class__.helper__skip_test_if_pg_version_is_ge(current_version, "10")
 
-        with __class__.helper__get_node(os_ops) as node:
+        with __class__.helper__get_node(node_svc) as node:
             with pytest.raises(expected_exception=InitNodeException):
                 node.init(allow_logical=True)
 
-    def test_replication_slots(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_replication_slots(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             with node.replicate(slot='slot1').start() as replica:
@@ -1022,18 +1029,18 @@ def test_replication_slots(self, os_ops: OsOperations):
                 with pytest.raises(expected_exception=testgres_TestgresException):
                     node.replicate(slot='slot1')
 
-    def test_incorrect_catchup(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as node:
+    def test_incorrect_catchup(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as node:
             node.init(allow_streaming=True).start()
 
             # node has no master, can't catch up
             with pytest.raises(expected_exception=testgres_TestgresException):
                 node.catchup()
 
-    def test_promotion(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
-        with __class__.helper__get_node(os_ops) as master:
+    def test_promotion(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+        with __class__.helper__get_node(node_svc) as master:
             master.init().start()
             master.safe_psql('create table abc(id serial)')
 
@@ -1046,17 +1053,17 @@ def test_promotion(self, os_ops: OsOperations):
                 res = replica.safe_psql('select * from abc')
                 assert (__class__.helper__rm_carriage_returns(res) == b'1\n')
 
-    def test_dump(self, os_ops: OsOperations):
-        assert isinstance(os_ops, OsOperations)
+    def test_dump(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
         query_create = 'create table test as select generate_series(1, 2) as val'
         query_select = 'select * from test order by val asc'
 
-        with __class__.helper__get_node(os_ops).init().start() as node1:
+        with __class__.helper__get_node(node_svc).init().start() as node1:
 
             node1.execute(query_create)
             for format in ['plain', 'custom', 'directory', 'tar']:
-                with removing(os_ops, node1.dump(format=format)) as dump:
-                    with __class__.helper__get_node(os_ops).init().start() as node3:
+                with removing(node_svc.os_ops, node1.dump(format=format)) as dump:
+                    with __class__.helper__get_node(node_svc).init().start() as node3:
                         if format == 'directory':
                             assert (os.path.isdir(dump))
                         else:
@@ -1066,14 +1073,16 @@ def test_dump(self, os_ops: OsOperations):
                         res = node3.execute(query_select)
                         assert (res == [(1, ), (2, )])
 
-    def test_get_pg_config2(self, os_ops: OsOperations):
+    def test_get_pg_config2(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+
         # check same instances
-        a = get_pg_config2(os_ops, None)
-        b = get_pg_config2(os_ops, None)
+        a = get_pg_config2(node_svc.os_ops, None)
+        b = get_pg_config2(node_svc.os_ops, None)
         assert (id(a) == id(b))
 
         # save right before config change
-        c1 = get_pg_config2(os_ops, None)
+        c1 = get_pg_config2(node_svc.os_ops, None)
 
         # modify setting for this scope
         with scoped_config(cache_pg_config=False) as config:
@@ -1081,20 +1090,26 @@ def test_get_pg_config2(self, os_ops: OsOperations):
             assert not (config.cache_pg_config)
 
             # save right after config change
-            c2 = get_pg_config2(os_ops, None)
+            c2 = get_pg_config2(node_svc.os_ops, None)
 
             # check different instances after config change
             assert (id(c1) != id(c2))
 
             # check different instances
-            a = get_pg_config2(os_ops, None)
-            b = get_pg_config2(os_ops, None)
+            a = get_pg_config2(node_svc.os_ops, None)
+            b = get_pg_config2(node_svc.os_ops, None)
             assert (id(a) != id(b))
 
     @staticmethod
-    def helper__get_node(os_ops: OsOperations, name=None):
-        assert isinstance(os_ops, OsOperations)
-        return PostgresNode(name, conn_params=None, os_ops=os_ops)
+    def helper__get_node(node_svc: PostgresNodeService, name=None):
+        assert isinstance(node_svc, PostgresNodeService)
+        assert isinstance(node_svc.os_ops, OsOperations)
+        assert isinstance(node_svc.port_manager, PostgresNodePortManager)
+        return PostgresNode(
+            name,
+            conn_params=None,
+            os_ops=node_svc.os_ops,
+            port_manager=node_svc.port_manager)
 
     @staticmethod
     def helper__skip_test_if_pg_version_is_not_ge(ver1: str, ver2: str):
diff --git a/tests/test_testgres_remote.py b/tests/test_testgres_remote.py
index a2aaa18e..7e777330 100755
--- a/tests/test_testgres_remote.py
+++ b/tests/test_testgres_remote.py
@@ -6,8 +6,8 @@
 import pytest
 import logging
 
-from .helpers.os_ops_descrs import OsOpsDescrs
-from .helpers.os_ops_descrs import OsOperations
+from .helpers.global_data import PostgresNodeService
+from .helpers.global_data import PostgresNodeServices
 
 from .. import testgres
 
@@ -45,17 +45,17 @@ def good_properties(f):
 
 
 class TestTestgresRemote:
-    sm_os_ops = OsOpsDescrs.sm_remote_os_ops
-
     @pytest.fixture(autouse=True, scope="class")
     def implicit_fixture(self):
+        cur_os_ops = PostgresNodeServices.sm_remote.os_ops
+        assert cur_os_ops is not None
+
         prev_ops = testgres_config.os_ops
         assert prev_ops is not None
-        assert __class__.sm_os_ops is not None
-        testgres_config.set_os_ops(os_ops=__class__.sm_os_ops)
-        assert testgres_config.os_ops is __class__.sm_os_ops
+        testgres_config.set_os_ops(os_ops=cur_os_ops)
+        assert testgres_config.os_ops is cur_os_ops
         yield
-        assert testgres_config.os_ops is __class__.sm_os_ops
+        assert testgres_config.os_ops is cur_os_ops
         testgres_config.set_os_ops(os_ops=prev_ops)
         assert testgres_config.os_ops is prev_ops
 
@@ -258,8 +258,17 @@ def test_unix_sockets(self):
 
     @staticmethod
     def helper__get_node(name=None):
-        assert isinstance(__class__.sm_os_ops, OsOperations)
-        return testgres.PostgresNode(name, conn_params=None, os_ops=__class__.sm_os_ops)
+        svc = PostgresNodeServices.sm_remote
+
+        assert isinstance(svc, PostgresNodeService)
+        assert isinstance(svc.os_ops, testgres.OsOperations)
+        assert isinstance(svc.port_manager, testgres.PostgresNodePortManager)
+
+        return testgres.PostgresNode(
+            name,
+            conn_params=None,
+            os_ops=svc.os_ops,
+            port_manager=svc.port_manager)
 
     @staticmethod
     def helper__restore_envvar(name, prev_value):

From 110947d88334fdcdbd0c898b7245b7fffaafbad5 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 16:42:09 +0300
Subject: [PATCH 07/31] CI files are updated

---
 Dockerfile--altlinux_10.tmpl | 2 +-
 Dockerfile--altlinux_11.tmpl | 2 +-
 run_tests.sh                 | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Dockerfile--altlinux_10.tmpl b/Dockerfile--altlinux_10.tmpl
index a75e35a0..d78b05f5 100644
--- a/Dockerfile--altlinux_10.tmpl
+++ b/Dockerfile--altlinux_10.tmpl
@@ -115,4 +115,4 @@ ssh-keygen -t rsa -f ~/.ssh/id_rsa -q -N ''; \
 cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys; \
 chmod 600 ~/.ssh/authorized_keys; \
 ls -la ~/.ssh/; \
-TEST_FILTER=\"TestTestgresLocal or TestOsOpsLocal or local_ops\" bash ./run_tests.sh;"
+TEST_FILTER=\"TestTestgresLocal or TestOsOpsLocal or local\" bash ./run_tests.sh;"
diff --git a/Dockerfile--altlinux_11.tmpl b/Dockerfile--altlinux_11.tmpl
index 5b43da20..5c88585d 100644
--- a/Dockerfile--altlinux_11.tmpl
+++ b/Dockerfile--altlinux_11.tmpl
@@ -115,4 +115,4 @@ ssh-keygen -t rsa -f ~/.ssh/id_rsa -q -N ''; \
 cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys; \
 chmod 600 ~/.ssh/authorized_keys; \
 ls -la ~/.ssh/; \
-TEST_FILTER=\"TestTestgresLocal or TestOsOpsLocal or local_ops\" bash ./run_tests.sh;"
+TEST_FILTER=\"TestTestgresLocal or TestOsOpsLocal or local\" bash ./run_tests.sh;"
diff --git a/run_tests.sh b/run_tests.sh
index 8202aff5..65c17dbf 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -5,7 +5,7 @@
 set -eux
 
 if [ -z ${TEST_FILTER+x} ]; \
-then export TEST_FILTER="TestTestgresLocal or (TestTestgresCommon and (not remote_ops))"; \
+then export TEST_FILTER="TestTestgresLocal or (TestTestgresCommon and (not remote))"; \
 fi
 
 # fail early

From 4f49dde2709e7ca9c75608bc7f020d95e351a8c5 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 18:03:24 +0300
Subject: [PATCH 08/31] TestTestgresCommon.test_pgbench is added

- [del] TestTestgresLocal.test_pgbench
- [del] TestTestgresRemote.test_pgbench
---
 tests/test_testgres_common.py | 21 +++++++++++++++++++++
 tests/test_testgres_local.py  | 21 ---------------------
 tests/test_testgres_remote.py | 16 ----------------
 3 files changed, 21 insertions(+), 37 deletions(-)

diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 5f88acd0..b65f8870 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -38,6 +38,7 @@
 import uuid
 import os
 import re
+import subprocess
 
 
 @contextmanager
@@ -1100,6 +1101,26 @@ def test_get_pg_config2(self, node_svc: PostgresNodeService):
             b = get_pg_config2(node_svc.os_ops, None)
             assert (id(a) != id(b))
 
+    def test_pgbench(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+
+        __class__.helper__skip_test_if_util_not_exist(node_svc.os_ops, "pgbench")
+
+        with __class__.helper__get_node(node_svc).init().start() as node:
+            # initialize pgbench DB and run benchmarks
+            node.pgbench_init(
+                scale=2,
+                foreign_keys=True,
+                options=['-q']
+            ).pgbench_run(time=2)
+
+            # run TPC-B benchmark
+            proc = node.pgbench(stdout=subprocess.PIPE,
+                                stderr=subprocess.STDOUT,
+                                options=['-T3'])
+            out = proc.communicate()[0]
+            assert (b'tps = ' in out)
+
     @staticmethod
     def helper__get_node(node_svc: PostgresNodeService, name=None):
         assert isinstance(node_svc, PostgresNodeService)
diff --git a/tests/test_testgres_local.py b/tests/test_testgres_local.py
index 01f975a0..d326dd9e 100644
--- a/tests/test_testgres_local.py
+++ b/tests/test_testgres_local.py
@@ -100,27 +100,6 @@ def test_custom_init(self):
                 # there should be no trust entries at all
                 assert not (any('trust' in s for s in lines))
 
-    def test_pgbench(self):
-        __class__.helper__skip_test_if_util_not_exist("pgbench")
-
-        with get_new_node().init().start() as node:
-
-            # initialize pgbench DB and run benchmarks
-            node.pgbench_init(scale=2, foreign_keys=True,
-                              options=['-q']).pgbench_run(time=2)
-
-            # run TPC-B benchmark
-            proc = node.pgbench(stdout=subprocess.PIPE,
-                                stderr=subprocess.STDOUT,
-                                options=['-T3'])
-
-            out, _ = proc.communicate()
-            out = out.decode('utf-8')
-
-            proc.stdout.close()
-
-            assert ('tps' in out)
-
     def test_pg_config(self):
         # check same instances
         a = get_pg_config()
diff --git a/tests/test_testgres_remote.py b/tests/test_testgres_remote.py
index 7e777330..34257b23 100755
--- a/tests/test_testgres_remote.py
+++ b/tests/test_testgres_remote.py
@@ -1,7 +1,6 @@
 # coding: utf-8
 import os
 import re
-import subprocess
 
 import pytest
 import logging
@@ -169,21 +168,6 @@ def test_init__unk_LANG_and_LC_CTYPE(self):
             __class__.helper__restore_envvar("LC_CTYPE", prev_LC_CTYPE)
             __class__.helper__restore_envvar("LC_COLLATE", prev_LC_COLLATE)
 
-    def test_pgbench(self):
-        __class__.helper__skip_test_if_util_not_exist("pgbench")
-
-        with __class__.helper__get_node().init().start() as node:
-            # initialize pgbench DB and run benchmarks
-            node.pgbench_init(scale=2, foreign_keys=True,
-                              options=['-q']).pgbench_run(time=2)
-
-            # run TPC-B benchmark
-            proc = node.pgbench(stdout=subprocess.PIPE,
-                                stderr=subprocess.STDOUT,
-                                options=['-T3'])
-            out = proc.communicate()[0]
-            assert (b'tps = ' in out)
-
     def test_pg_config(self):
         # check same instances
         a = get_pg_config()

From 4fbf51daa0fcbb8ac33131722af698433177a864 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 18:08:17 +0300
Subject: [PATCH 09/31] PostgresNodePortManager is updated [error messages]

---
 testgres/node.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 7e112751..59cf26f1 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -141,11 +141,11 @@ def __init__(self):
         super().__init__()
 
     def reserve_port(self) -> int:
-        raise NotImplementedError("PostManager::reserve_port is not implemented.")
+        raise NotImplementedError("PostgresNodePortManager::reserve_port is not implemented.")
 
     def release_port(self, number: int) -> None:
         assert type(number) == int  # noqa: E721
-        raise NotImplementedError("PostManager::release_port is not implemented.")
+        raise NotImplementedError("PostgresNodePortManager::release_port is not implemented.")
 
 
 class PostgresNodePortManager__ThisHost(PostgresNodePortManager):

From 17c73cbd262510920d71ce23a6c3efa3cfbb1ed0 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 18:33:11 +0300
Subject: [PATCH 10/31] PostgresNodePortManager(+company) was moved in own file
 - port_manager.py

---
 testgres/node.py         | 99 ++++------------------------------------
 testgres/port_manager.py | 91 ++++++++++++++++++++++++++++++++++++
 2 files changed, 99 insertions(+), 91 deletions(-)
 create mode 100644 testgres/port_manager.py

diff --git a/testgres/node.py b/testgres/node.py
index 59cf26f1..c080c3dd 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -83,14 +83,16 @@
     BackupException,    \
     InvalidOperationException
 
+from .port_manager import PostgresNodePortManager
+from .port_manager import PostgresNodePortManager__ThisHost
+from .port_manager import PostgresNodePortManager__Generic
+
 from .logger import TestgresLogger
 
 from .pubsub import Publication, Subscription
 
 from .standby import First
 
-from . import utils
-
 from .utils import \
     PgVer, \
     eprint, \
@@ -100,8 +102,6 @@
     options_string, \
     clean_on_error
 
-from .helpers.port_manager import PortForException
-
 from .backup import NodeBackup
 
 from .operations.os_ops import ConnectionParams
@@ -131,93 +131,10 @@ def __getattr__(self, name):
         return getattr(self.process, name)
 
     def __repr__(self):
-        return '{}(ptype={}, process={})'.format(self.__class__.__name__,
-                                                 str(self.ptype),
-                                                 repr(self.process))
-
-
-class PostgresNodePortManager:
-    def __init__(self):
-        super().__init__()
-
-    def reserve_port(self) -> int:
-        raise NotImplementedError("PostgresNodePortManager::reserve_port is not implemented.")
-
-    def release_port(self, number: int) -> None:
-        assert type(number) == int  # noqa: E721
-        raise NotImplementedError("PostgresNodePortManager::release_port is not implemented.")
-
-
-class PostgresNodePortManager__ThisHost(PostgresNodePortManager):
-    sm_single_instance: PostgresNodePortManager = None
-    sm_single_instance_guard = threading.Lock()
-
-    def __init__(self):
-        pass
-
-    def __new__(cls) -> PostgresNodePortManager:
-        assert __class__ == PostgresNodePortManager__ThisHost
-        assert __class__.sm_single_instance_guard is not None
-
-        if __class__.sm_single_instance is None:
-            with __class__.sm_single_instance_guard:
-                __class__.sm_single_instance = super().__new__(cls)
-        assert __class__.sm_single_instance
-        assert type(__class__.sm_single_instance) == __class__  # noqa: E721
-        return __class__.sm_single_instance
-
-    def reserve_port(self) -> int:
-        return utils.reserve_port()
-
-    def release_port(self, number: int) -> None:
-        assert type(number) == int  # noqa: E721
-        return utils.release_port(number)
-
-
-class PostgresNodePortManager__Generic(PostgresNodePortManager):
-    _os_ops: OsOperations
-    _allocated_ports_guard: object
-    _allocated_ports: set[int]
-
-    def __init__(self, os_ops: OsOperations):
-        assert os_ops is not None
-        assert isinstance(os_ops, OsOperations)
-        self._os_ops = os_ops
-        self._allocated_ports_guard = threading.Lock()
-        self._allocated_ports = set[int]()
-
-    def reserve_port(self) -> int:
-        ports = set(range(1024, 65535))
-        assert type(ports) == set  # noqa: E721
-
-        assert self._allocated_ports_guard is not None
-        assert type(self._allocated_ports) == set  # noqa: E721
-
-        with self._allocated_ports_guard:
-            ports.difference_update(self._allocated_ports)
-
-            sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
-
-            for port in sampled_ports:
-                assert not (port in self._allocated_ports)
-
-                if not self._os_ops.is_port_free(port):
-                    continue
-
-                self._allocated_ports.add(port)
-                return port
-
-        raise PortForException("Can't select a port")
-
-    def release_port(self, number: int) -> None:
-        assert type(number) == int  # noqa: E721
-
-        assert self._allocated_ports_guard is not None
-        assert type(self._allocated_ports) == set  # noqa: E721
-
-        with self._allocated_ports_guard:
-            assert number in self._allocated_ports
-            self._allocated_ports.discard(number)
+        return '{}(ptype={}, process={})'.format(
+            self.__class__.__name__,
+            str(self.ptype),
+            repr(self.process))
 
 
 class PostgresNode(object):
diff --git a/testgres/port_manager.py b/testgres/port_manager.py
new file mode 100644
index 00000000..32c5db5d
--- /dev/null
+++ b/testgres/port_manager.py
@@ -0,0 +1,91 @@
+from .operations.os_ops import OsOperations
+
+from .helpers.port_manager import PortForException
+
+from . import utils
+
+import threading
+import random
+
+class PostgresNodePortManager:
+    def __init__(self):
+        super().__init__()
+
+    def reserve_port(self) -> int:
+        raise NotImplementedError("PostgresNodePortManager::reserve_port is not implemented.")
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+        raise NotImplementedError("PostgresNodePortManager::release_port is not implemented.")
+
+
+class PostgresNodePortManager__ThisHost(PostgresNodePortManager):
+    sm_single_instance: PostgresNodePortManager = None
+    sm_single_instance_guard = threading.Lock()
+
+    def __init__(self):
+        pass
+
+    def __new__(cls) -> PostgresNodePortManager:
+        assert __class__ == PostgresNodePortManager__ThisHost
+        assert __class__.sm_single_instance_guard is not None
+
+        if __class__.sm_single_instance is None:
+            with __class__.sm_single_instance_guard:
+                __class__.sm_single_instance = super().__new__(cls)
+        assert __class__.sm_single_instance
+        assert type(__class__.sm_single_instance) == __class__  # noqa: E721
+        return __class__.sm_single_instance
+
+    def reserve_port(self) -> int:
+        return utils.reserve_port()
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+        return utils.release_port(number)
+
+
+class PostgresNodePortManager__Generic(PostgresNodePortManager):
+    _os_ops: OsOperations
+    _allocated_ports_guard: object
+    _allocated_ports: set[int]
+
+    def __init__(self, os_ops: OsOperations):
+        assert os_ops is not None
+        assert isinstance(os_ops, OsOperations)
+        self._os_ops = os_ops
+        self._allocated_ports_guard = threading.Lock()
+        self._allocated_ports = set[int]()
+
+    def reserve_port(self) -> int:
+        ports = set(range(1024, 65535))
+        assert type(ports) == set  # noqa: E721
+
+        assert self._allocated_ports_guard is not None
+        assert type(self._allocated_ports) == set  # noqa: E721
+
+        with self._allocated_ports_guard:
+            ports.difference_update(self._allocated_ports)
+
+            sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
+
+            for port in sampled_ports:
+                assert not (port in self._allocated_ports)
+
+                if not self._os_ops.is_port_free(port):
+                    continue
+
+                self._allocated_ports.add(port)
+                return port
+
+        raise PortForException("Can't select a port")
+
+    def release_port(self, number: int) -> None:
+        assert type(number) == int  # noqa: E721
+
+        assert self._allocated_ports_guard is not None
+        assert type(self._allocated_ports) == set  # noqa: E721
+
+        with self._allocated_ports_guard:
+            assert number in self._allocated_ports
+            self._allocated_ports.discard(number)

From b1cee194c7ad9696de67944f10cf8521ff5f1f45 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 18:46:22 +0300
Subject: [PATCH 11/31] PortManager was deleted [amen]

---
 testgres/__init__.py             |  4 +---
 testgres/exceptions.py           |  4 ++++
 testgres/helpers/__init__.py     |  0
 testgres/helpers/port_manager.py | 41 --------------------------------
 testgres/port_manager.py         |  3 ++-
 testgres/utils.py                | 31 +++++++++++++++++++-----
 6 files changed, 32 insertions(+), 51 deletions(-)
 delete mode 100644 testgres/helpers/__init__.py
 delete mode 100644 testgres/helpers/port_manager.py

diff --git a/testgres/__init__.py b/testgres/__init__.py
index f8df8c24..e76518f5 100644
--- a/testgres/__init__.py
+++ b/testgres/__init__.py
@@ -54,8 +54,6 @@
 from .operations.local_ops import LocalOperations
 from .operations.remote_ops import RemoteOperations
 
-from .helpers.port_manager import PortManager
-
 __all__ = [
     "get_new_node",
     "get_remote_node",
@@ -67,6 +65,6 @@
     "PostgresNode", "NodeApp",
     "PostgresNodePortManager",
     "reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
-    "First", "Any", "PortManager",
+    "First", "Any",
     "OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
 ]
diff --git a/testgres/exceptions.py b/testgres/exceptions.py
index d61d4691..20c1a8cf 100644
--- a/testgres/exceptions.py
+++ b/testgres/exceptions.py
@@ -7,6 +7,10 @@ class TestgresException(Exception):
     pass
 
 
+class PortForException(TestgresException):
+    pass
+
+
 @six.python_2_unicode_compatible
 class ExecUtilException(TestgresException):
     def __init__(self, message=None, command=None, exit_code=0, out=None, error=None):
diff --git a/testgres/helpers/__init__.py b/testgres/helpers/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/testgres/helpers/port_manager.py b/testgres/helpers/port_manager.py
deleted file mode 100644
index cfc5c096..00000000
--- a/testgres/helpers/port_manager.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import socket
-import random
-from typing import Set, Iterable, Optional
-
-
-class PortForException(Exception):
-    pass
-
-
-class PortManager:
-    def __init__(self, ports_range=(1024, 65535)):
-        self.ports_range = ports_range
-
-    @staticmethod
-    def is_port_free(port: int) -> bool:
-        """Check if a port is free to use."""
-        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-            try:
-                s.bind(("", port))
-                return True
-            except OSError:
-                return False
-
-    def find_free_port(self, ports: Optional[Set[int]] = None, exclude_ports: Optional[Iterable[int]] = None) -> int:
-        """Return a random unused port number."""
-        if ports is None:
-            ports = set(range(1024, 65535))
-
-        assert type(ports) == set  # noqa: E721
-
-        if exclude_ports is not None:
-            assert isinstance(exclude_ports, Iterable)
-            ports.difference_update(exclude_ports)
-
-        sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
-
-        for port in sampled_ports:
-            if self.is_port_free(port):
-                return port
-
-        raise PortForException("Can't select a port")
diff --git a/testgres/port_manager.py b/testgres/port_manager.py
index 32c5db5d..e27c64a2 100644
--- a/testgres/port_manager.py
+++ b/testgres/port_manager.py
@@ -1,12 +1,13 @@
 from .operations.os_ops import OsOperations
 
-from .helpers.port_manager import PortForException
+from .exceptions import PortForException
 
 from . import utils
 
 import threading
 import random
 
+
 class PostgresNodePortManager:
     def __init__(self):
         super().__init__()
diff --git a/testgres/utils.py b/testgres/utils.py
index 92383571..cb0a6f19 100644
--- a/testgres/utils.py
+++ b/testgres/utils.py
@@ -6,6 +6,8 @@
 import os
 
 import sys
+import socket
+import random
 
 from contextlib import contextmanager
 from packaging.version import Version, InvalidVersion
@@ -13,7 +15,7 @@
 
 from six import iteritems
 
-from .helpers.port_manager import PortManager
+from .exceptions import PortForException
 from .exceptions import ExecUtilException
 from .config import testgres_config as tconf
 from .operations.os_ops import OsOperations
@@ -41,11 +43,28 @@ def internal__reserve_port():
     """
     Generate a new port and add it to 'bound_ports'.
     """
-    port_mng = PortManager()
-    port = port_mng.find_free_port(exclude_ports=bound_ports)
-    bound_ports.add(port)
-
-    return port
+    def LOCAL__is_port_free(port: int) -> bool:
+        """Check if a port is free to use."""
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            try:
+                s.bind(("", port))
+                return True
+            except OSError:
+                return False
+
+    ports = set(range(1024, 65535))
+    assert type(ports) == set  # noqa: E721
+    assert type(bound_ports) == set  # noqa: E721
+    ports.difference_update(bound_ports)
+
+    sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
+
+    for port in sampled_ports:
+        if LOCAL__is_port_free(port):
+            bound_ports.add(port)
+            return port
+
+    raise PortForException("Can't select a port")
 
 
 def internal__release_port(port):

From c5ad9078e98495ae904f23cba8b86a6c6e9ddceb Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 18:58:17 +0300
Subject: [PATCH 12/31] PostgresNodePortManager was renamed with PortManager

---
 testgres/__init__.py          |  4 ++--
 testgres/node.py              | 28 ++++++++++++++--------------
 testgres/port_manager.py      | 16 ++++++++--------
 testgres/utils.py             |  3 +++
 tests/helpers/global_data.py  | 18 +++++++++---------
 tests/test_testgres_common.py |  6 +++---
 tests/test_testgres_remote.py |  2 +-
 7 files changed, 40 insertions(+), 37 deletions(-)

diff --git a/testgres/__init__.py b/testgres/__init__.py
index e76518f5..339ae62e 100644
--- a/testgres/__init__.py
+++ b/testgres/__init__.py
@@ -34,7 +34,7 @@
     DumpFormat
 
 from .node import PostgresNode, NodeApp
-from .node import PostgresNodePortManager
+from .node import PortManager
 
 from .utils import \
     reserve_port, \
@@ -63,7 +63,7 @@
     "TestgresException", "ExecUtilException", "QueryException", "TimeoutException", "CatchUpException", "StartNodeException", "InitNodeException", "BackupException", "InvalidOperationException",
     "XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
     "PostgresNode", "NodeApp",
-    "PostgresNodePortManager",
+    "PortManager",
     "reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
     "First", "Any",
     "OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
diff --git a/testgres/node.py b/testgres/node.py
index c080c3dd..99ec2032 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -83,9 +83,9 @@
     BackupException,    \
     InvalidOperationException
 
-from .port_manager import PostgresNodePortManager
-from .port_manager import PostgresNodePortManager__ThisHost
-from .port_manager import PostgresNodePortManager__Generic
+from .port_manager import PortManager
+from .port_manager import PortManager__ThisHost
+from .port_manager import PortManager__Generic
 
 from .logger import TestgresLogger
 
@@ -146,7 +146,7 @@ class PostgresNode(object):
     _port: typing.Optional[int]
     _should_free_port: bool
     _os_ops: OsOperations
-    _port_manager: PostgresNodePortManager
+    _port_manager: PortManager
 
     def __init__(self,
                  name=None,
@@ -156,7 +156,7 @@ def __init__(self,
                  bin_dir=None,
                  prefix=None,
                  os_ops: typing.Optional[OsOperations] = None,
-                 port_manager: typing.Optional[PostgresNodePortManager] = None):
+                 port_manager: typing.Optional[PortManager] = None):
         """
         PostgresNode constructor.
 
@@ -170,7 +170,7 @@ def __init__(self,
         """
         assert port is None or type(port) == int  # noqa: E721
         assert os_ops is None or isinstance(os_ops, OsOperations)
-        assert port_manager is None or isinstance(port_manager, PostgresNodePortManager)
+        assert port_manager is None or isinstance(port_manager, PortManager)
 
         # private
         if os_ops is None:
@@ -203,13 +203,13 @@ def __init__(self,
             self._port_manager = None
         else:
             if port_manager is not None:
-                assert isinstance(port_manager, PostgresNodePortManager)
+                assert isinstance(port_manager, PortManager)
                 self._port_manager = port_manager
             else:
                 self._port_manager = __class__._get_port_manager(self._os_ops)
 
             assert self._port_manager is not None
-            assert isinstance(self._port_manager, PostgresNodePortManager)
+            assert isinstance(self._port_manager, PortManager)
 
             self._port = self._port_manager.reserve_port()  # raises
             assert type(self._port) == int  # noqa: E721
@@ -269,15 +269,15 @@ def _get_os_ops(conn_params: ConnectionParams) -> OsOperations:
         return LocalOperations(conn_params)
 
     @staticmethod
-    def _get_port_manager(os_ops: OsOperations) -> PostgresNodePortManager:
+    def _get_port_manager(os_ops: OsOperations) -> PortManager:
         assert os_ops is not None
         assert isinstance(os_ops, OsOperations)
 
         if isinstance(os_ops, LocalOperations):
-            return PostgresNodePortManager__ThisHost()
+            return PortManager__ThisHost()
 
         # TODO: Throw exception "Please define a port manager."
-        return PostgresNodePortManager__Generic(os_ops)
+        return PortManager__Generic(os_ops)
 
     def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
         assert name is None or type(name) == str  # noqa: E721
@@ -289,7 +289,7 @@ def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):
             raise InvalidOperationException("PostgresNode without PortManager can't be cloned.")
 
         assert self._port_manager is not None
-        assert isinstance(self._port_manager, PostgresNodePortManager)
+        assert isinstance(self._port_manager, PortManager)
         assert self._os_ops is not None
         assert isinstance(self._os_ops, OsOperations)
 
@@ -1124,7 +1124,7 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
         else:
             assert self._should_free_port
             assert self._port_manager is not None
-            assert isinstance(self._port_manager, PostgresNodePortManager)
+            assert isinstance(self._port_manager, PortManager)
             assert __class__._C_MAX_START_ATEMPTS > 1
 
             log_files0 = self._collect_log_files()
@@ -1331,7 +1331,7 @@ def free_port(self):
             assert type(self._port) == int  # noqa: E721
 
             assert self._port_manager is not None
-            assert isinstance(self._port_manager, PostgresNodePortManager)
+            assert isinstance(self._port_manager, PortManager)
 
             port = self._port
             self._should_free_port = False
diff --git a/testgres/port_manager.py b/testgres/port_manager.py
index e27c64a2..e4c2180c 100644
--- a/testgres/port_manager.py
+++ b/testgres/port_manager.py
@@ -8,27 +8,27 @@
 import random
 
 
-class PostgresNodePortManager:
+class PortManager:
     def __init__(self):
         super().__init__()
 
     def reserve_port(self) -> int:
-        raise NotImplementedError("PostgresNodePortManager::reserve_port is not implemented.")
+        raise NotImplementedError("PortManager::reserve_port is not implemented.")
 
     def release_port(self, number: int) -> None:
         assert type(number) == int  # noqa: E721
-        raise NotImplementedError("PostgresNodePortManager::release_port is not implemented.")
+        raise NotImplementedError("PortManager::release_port is not implemented.")
 
 
-class PostgresNodePortManager__ThisHost(PostgresNodePortManager):
-    sm_single_instance: PostgresNodePortManager = None
+class PortManager__ThisHost(PortManager):
+    sm_single_instance: PortManager = None
     sm_single_instance_guard = threading.Lock()
 
     def __init__(self):
         pass
 
-    def __new__(cls) -> PostgresNodePortManager:
-        assert __class__ == PostgresNodePortManager__ThisHost
+    def __new__(cls) -> PortManager:
+        assert __class__ == PortManager__ThisHost
         assert __class__.sm_single_instance_guard is not None
 
         if __class__.sm_single_instance is None:
@@ -46,7 +46,7 @@ def release_port(self, number: int) -> None:
         return utils.release_port(number)
 
 
-class PostgresNodePortManager__Generic(PostgresNodePortManager):
+class PortManager__Generic(PortManager):
     _os_ops: OsOperations
     _allocated_ports_guard: object
     _allocated_ports: set[int]
diff --git a/testgres/utils.py b/testgres/utils.py
index cb0a6f19..10ae81b6 100644
--- a/testgres/utils.py
+++ b/testgres/utils.py
@@ -72,6 +72,9 @@ def internal__release_port(port):
     Free port provided by reserve_port().
     """
 
+    assert type(port) == int  # noqa: E721
+    assert port in bound_ports
+
     bound_ports.discard(port)
 
 
diff --git a/tests/helpers/global_data.py b/tests/helpers/global_data.py
index ea7b2385..c21d7dd8 100644
--- a/tests/helpers/global_data.py
+++ b/tests/helpers/global_data.py
@@ -3,9 +3,9 @@
 from ...testgres.operations.local_ops import LocalOperations
 from ...testgres.operations.remote_ops import RemoteOperations
 
-from ...testgres.node import PostgresNodePortManager
-from ...testgres.node import PostgresNodePortManager__ThisHost
-from ...testgres.node import PostgresNodePortManager__Generic
+from ...testgres.node import PortManager
+from ...testgres.node import PortManager__ThisHost
+from ...testgres.node import PortManager__Generic
 
 import os
 
@@ -37,22 +37,22 @@ class OsOpsDescrs:
 
 
 class PortManagers:
-    sm_remote_port_manager = PostgresNodePortManager__Generic(OsOpsDescrs.sm_remote_os_ops)
+    sm_remote_port_manager = PortManager__Generic(OsOpsDescrs.sm_remote_os_ops)
 
-    sm_local_port_manager = PostgresNodePortManager__ThisHost()
+    sm_local_port_manager = PortManager__ThisHost()
 
-    sm_local2_port_manager = PostgresNodePortManager__Generic(OsOpsDescrs.sm_local_os_ops)
+    sm_local2_port_manager = PortManager__Generic(OsOpsDescrs.sm_local_os_ops)
 
 
 class PostgresNodeService:
     sign: str
     os_ops: OsOperations
-    port_manager: PostgresNodePortManager
+    port_manager: PortManager
 
-    def __init__(self, sign: str, os_ops: OsOperations, port_manager: PostgresNodePortManager):
+    def __init__(self, sign: str, os_ops: OsOperations, port_manager: PortManager):
         assert type(sign) == str  # noqa: E721
         assert isinstance(os_ops, OsOperations)
-        assert isinstance(port_manager, PostgresNodePortManager)
+        assert isinstance(port_manager, PortManager)
         self.sign = sign
         self.os_ops = os_ops
         self.port_manager = port_manager
diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index b65f8870..f2d9c074 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1,7 +1,7 @@
 from .helpers.global_data import PostgresNodeService
 from .helpers.global_data import PostgresNodeServices
 from .helpers.global_data import OsOperations
-from .helpers.global_data import PostgresNodePortManager
+from .helpers.global_data import PortManager
 
 from ..testgres.node import PgVer
 from ..testgres.node import PostgresNode
@@ -70,7 +70,7 @@ def node_svc(self, request: pytest.FixtureRequest) -> PostgresNodeService:
         assert isinstance(request, pytest.FixtureRequest)
         assert isinstance(request.param, PostgresNodeService)
         assert isinstance(request.param.os_ops, OsOperations)
-        assert isinstance(request.param.port_manager, PostgresNodePortManager)
+        assert isinstance(request.param.port_manager, PortManager)
         return request.param
 
     def test_version_management(self, node_svc: PostgresNodeService):
@@ -1125,7 +1125,7 @@ def test_pgbench(self, node_svc: PostgresNodeService):
     def helper__get_node(node_svc: PostgresNodeService, name=None):
         assert isinstance(node_svc, PostgresNodeService)
         assert isinstance(node_svc.os_ops, OsOperations)
-        assert isinstance(node_svc.port_manager, PostgresNodePortManager)
+        assert isinstance(node_svc.port_manager, PortManager)
         return PostgresNode(
             name,
             conn_params=None,
diff --git a/tests/test_testgres_remote.py b/tests/test_testgres_remote.py
index 34257b23..2f92679e 100755
--- a/tests/test_testgres_remote.py
+++ b/tests/test_testgres_remote.py
@@ -246,7 +246,7 @@ def helper__get_node(name=None):
 
         assert isinstance(svc, PostgresNodeService)
         assert isinstance(svc.os_ops, testgres.OsOperations)
-        assert isinstance(svc.port_manager, testgres.PostgresNodePortManager)
+        assert isinstance(svc.port_manager, testgres.PortManager)
 
         return testgres.PostgresNode(
             name,

From 09670578fad1a2f25cad96b0cba7f4a5c07c3230 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 19:22:56 +0300
Subject: [PATCH 13/31] TestTestgresCommon.test_unix_sockets is added

---
 tests/test_testgres_common.py | 20 ++++++++++++++++++++
 tests/test_testgres_local.py  | 12 ------------
 tests/test_testgres_remote.py | 16 ----------------
 3 files changed, 20 insertions(+), 28 deletions(-)

diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index f2d9c074..27ebf23c 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1121,6 +1121,26 @@ def test_pgbench(self, node_svc: PostgresNodeService):
             out = proc.communicate()[0]
             assert (b'tps = ' in out)
 
+    def test_unix_sockets(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+
+        with __class__.helper__get_node(node_svc) as node:
+            node.init(unix_sockets=False, allow_streaming=True)
+            node.start()
+
+            res_exec = node.execute('select 1')
+            assert (res_exec == [(1,)])
+            res_psql = node.safe_psql('select 1')
+            assert (res_psql == b'1\n')
+
+            with node.replicate() as r:
+                assert type(r) == PostgresNode  # noqa: E721
+                r.start()
+                res_exec = r.execute('select 1')
+                assert (res_exec == [(1,)])
+                res_psql = r.safe_psql('select 1')
+                assert (res_psql == b'1\n')
+
     @staticmethod
     def helper__get_node(node_svc: PostgresNodeService, name=None):
         assert isinstance(node_svc, PostgresNodeService)
diff --git a/tests/test_testgres_local.py b/tests/test_testgres_local.py
index d326dd9e..45bade42 100644
--- a/tests/test_testgres_local.py
+++ b/tests/test_testgres_local.py
@@ -156,18 +156,6 @@ def test_config_stack(self):
 
         assert (TestgresConfig.cached_initdb_dir == d0)
 
-    def test_unix_sockets(self):
-        with get_new_node() as node:
-            node.init(unix_sockets=False, allow_streaming=True)
-            node.start()
-
-            node.execute('select 1')
-            node.safe_psql('select 1')
-
-            with node.replicate().start() as r:
-                r.execute('select 1')
-                r.safe_psql('select 1')
-
     def test_ports_management(self):
         assert bound_ports is not None
         assert type(bound_ports) == set  # noqa: E721
diff --git a/tests/test_testgres_remote.py b/tests/test_testgres_remote.py
index 2f92679e..ef4bd0c8 100755
--- a/tests/test_testgres_remote.py
+++ b/tests/test_testgres_remote.py
@@ -224,22 +224,6 @@ def test_config_stack(self):
 
         assert (TestgresConfig.cached_initdb_dir == d0)
 
-    def test_unix_sockets(self):
-        with __class__.helper__get_node() as node:
-            node.init(unix_sockets=False, allow_streaming=True)
-            node.start()
-
-            res_exec = node.execute('select 1')
-            res_psql = node.safe_psql('select 1')
-            assert (res_exec == [(1,)])
-            assert (res_psql == b'1\n')
-
-            with node.replicate().start() as r:
-                res_exec = r.execute('select 1')
-                res_psql = r.safe_psql('select 1')
-                assert (res_exec == [(1,)])
-                assert (res_psql == b'1\n')
-
     @staticmethod
     def helper__get_node(name=None):
         svc = PostgresNodeServices.sm_remote

From 1d450b2a71f15c9a7a4a471c7213f6a326b3e02b Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 19:39:56 +0300
Subject: [PATCH 14/31] TestTestgresCommon.test_the_same_port is added

---
 tests/test_testgres_common.py | 34 ++++++++++++++++++++++++++++++++--
 tests/test_testgres_local.py  | 24 ------------------------
 2 files changed, 32 insertions(+), 26 deletions(-)

diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 27ebf23c..1f0c070d 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1141,16 +1141,46 @@ def test_unix_sockets(self, node_svc: PostgresNodeService):
                 res_psql = r.safe_psql('select 1')
                 assert (res_psql == b'1\n')
 
+    def test_the_same_port(self, node_svc: PostgresNodeService):
+        assert isinstance(node_svc, PostgresNodeService)
+
+        with __class__.helper__get_node(node_svc) as node:
+            node.init().start()
+            assert (node._should_free_port)
+            assert (type(node.port) == int)  # noqa: E721
+            node_port_copy = node.port
+            r = node.safe_psql("SELECT 1;")
+            assert (__class__.helper__rm_carriage_returns(r) == b'1\n')
+
+            with __class__.helper__get_node(node_svc, port=node.port) as node2:
+                assert (type(node2.port) == int)  # noqa: E721
+                assert (node2.port == node.port)
+                assert not (node2._should_free_port)
+
+                with pytest.raises(
+                    expected_exception=StartNodeException,
+                    match=re.escape("Cannot start node")
+                ):
+                    node2.init().start()
+
+            # node is still working
+            assert (node.port == node_port_copy)
+            assert (node._should_free_port)
+            r = node.safe_psql("SELECT 3;")
+            assert (__class__.helper__rm_carriage_returns(r) == b'3\n')
+
     @staticmethod
-    def helper__get_node(node_svc: PostgresNodeService, name=None):
+    def helper__get_node(node_svc: PostgresNodeService, name=None, port=None):
         assert isinstance(node_svc, PostgresNodeService)
         assert isinstance(node_svc.os_ops, OsOperations)
         assert isinstance(node_svc.port_manager, PortManager)
         return PostgresNode(
             name,
+            port=port,
             conn_params=None,
             os_ops=node_svc.os_ops,
-            port_manager=node_svc.port_manager)
+            port_manager=node_svc.port_manager if port is None else None
+        )
 
     @staticmethod
     def helper__skip_test_if_pg_version_is_not_ge(ver1: str, ver2: str):
diff --git a/tests/test_testgres_local.py b/tests/test_testgres_local.py
index 45bade42..bef80d0f 100644
--- a/tests/test_testgres_local.py
+++ b/tests/test_testgres_local.py
@@ -244,30 +244,6 @@ def test_parse_pg_version(self):
         # Macos
         assert parse_pg_version("postgres (PostgreSQL) 14.9 (Homebrew)") == "14.9"
 
-    def test_the_same_port(self):
-        with get_new_node() as node:
-            node.init().start()
-            assert (node._should_free_port)
-            assert (type(node.port) == int)  # noqa: E721
-            node_port_copy = node.port
-            assert (rm_carriage_returns(node.safe_psql("SELECT 1;")) == b'1\n')
-
-            with get_new_node(port=node.port) as node2:
-                assert (type(node2.port) == int)  # noqa: E721
-                assert (node2.port == node.port)
-                assert not (node2._should_free_port)
-
-                with pytest.raises(
-                    expected_exception=StartNodeException,
-                    match=re.escape("Cannot start node")
-                ):
-                    node2.init().start()
-
-            # node is still working
-            assert (node.port == node_port_copy)
-            assert (node._should_free_port)
-            assert (rm_carriage_returns(node.safe_psql("SELECT 3;")) == b'3\n')
-
     class tagPortManagerProxy:
         sm_prev_testgres_reserve_port = None
         sm_prev_testgres_release_port = None

From 4a38b35dda82360a59e6c73f7614998810bcf248 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 20:39:12 +0300
Subject: [PATCH 15/31] [TestTestgresCommon] New tests are added

- test_port_rereserve_during_node_start
- test_port_conflict
---
 tests/test_testgres_common.py | 167 +++++++++++++++++++++++++++++++++-
 1 file changed, 165 insertions(+), 2 deletions(-)

diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 1f0c070d..85368824 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -13,6 +13,8 @@
 from ..testgres import NodeStatus
 from ..testgres import IsolationLevel
 
+import testgres
+
 # New name prevents to collect test-functions in TestgresException and fixes
 # the problem with pytest warning.
 from ..testgres import TestgresException as testgres_TestgresException
@@ -39,6 +41,7 @@
 import os
 import re
 import subprocess
+import typing
 
 
 @contextmanager
@@ -1169,17 +1172,177 @@ def test_the_same_port(self, node_svc: PostgresNodeService):
             r = node.safe_psql("SELECT 3;")
             assert (__class__.helper__rm_carriage_returns(r) == b'3\n')
 
+    class tagPortManagerProxy(PortManager):
+        m_PrevPortManager: PortManager
+
+        m_DummyPortNumber: int
+        m_DummyPortMaxUsage: int
+
+        m_DummyPortCurrentUsage: int
+        m_DummyPortTotalUsage: int
+
+        def __init__(self, prevPortManager: PortManager, dummyPortNumber: int, dummyPortMaxUsage: int):
+            assert isinstance(prevPortManager, PortManager)
+            assert type(dummyPortNumber) == int  # noqa: E721
+            assert type(dummyPortMaxUsage) == int  # noqa: E721
+            assert dummyPortNumber >= 0
+            assert dummyPortMaxUsage >= 0
+
+            super().__init__()
+
+            self.m_PrevPortManager = prevPortManager
+
+            self.m_DummyPortNumber = dummyPortNumber
+            self.m_DummyPortMaxUsage = dummyPortMaxUsage
+
+            self.m_DummyPortCurrentUsage = 0
+            self.m_DummyPortTotalUsage = 0
+
+        def __enter__(self):
+            return self
+
+        def __exit__(self, type, value, traceback):
+            assert self.m_DummyPortCurrentUsage == 0
+
+            assert self.m_PrevPortManager is not None
+
+        def reserve_port(self) -> int:
+            assert type(self.m_DummyPortMaxUsage) == int  # noqa: E721
+            assert type(self.m_DummyPortTotalUsage) == int  # noqa: E721
+            assert type(self.m_DummyPortCurrentUsage) == int  # noqa: E721
+            assert self.m_DummyPortTotalUsage >= 0
+            assert self.m_DummyPortCurrentUsage >= 0
+
+            assert self.m_DummyPortTotalUsage <= self.m_DummyPortMaxUsage
+            assert self.m_DummyPortCurrentUsage <= self.m_DummyPortTotalUsage
+
+            assert self.m_PrevPortManager is not None
+            assert isinstance(self.m_PrevPortManager, PortManager)
+
+            if self.m_DummyPortTotalUsage == self.m_DummyPortMaxUsage:
+                return self.m_PrevPortManager.reserve_port()
+
+            self.m_DummyPortTotalUsage += 1
+            self.m_DummyPortCurrentUsage += 1
+            return self.m_DummyPortNumber
+
+        def release_port(self, dummyPortNumber: int):
+            assert type(dummyPortNumber) == int  # noqa: E721
+
+            assert type(self.m_DummyPortMaxUsage) == int  # noqa: E721
+            assert type(self.m_DummyPortTotalUsage) == int  # noqa: E721
+            assert type(self.m_DummyPortCurrentUsage) == int  # noqa: E721
+            assert self.m_DummyPortTotalUsage >= 0
+            assert self.m_DummyPortCurrentUsage >= 0
+
+            assert self.m_DummyPortTotalUsage <= self.m_DummyPortMaxUsage
+            assert self.m_DummyPortCurrentUsage <= self.m_DummyPortTotalUsage
+
+            assert self.m_PrevPortManager is not None
+            assert isinstance(self.m_PrevPortManager, PortManager)
+
+            if self.m_DummyPortCurrentUsage > 0 and dummyPortNumber == self.m_DummyPortNumber:
+                assert self.m_DummyPortTotalUsage > 0
+                self.m_DummyPortCurrentUsage -= 1
+                return
+
+            return self.m_PrevPortManager.release_port(dummyPortNumber)
+
+    def test_port_rereserve_during_node_start(self, node_svc: PostgresNodeService):
+        assert type(node_svc) == PostgresNodeService  # noqa: E721
+        assert testgres.PostgresNode._C_MAX_START_ATEMPTS == 5
+
+        C_COUNT_OF_BAD_PORT_USAGE = 3
+
+        with __class__.helper__get_node(node_svc) as node1:
+            node1.init().start()
+            assert node1._should_free_port
+            assert type(node1.port) == int  # noqa: E721
+            node1_port_copy = node1.port
+            assert __class__.helper__rm_carriage_returns(node1.safe_psql("SELECT 1;")) == b'1\n'
+
+            with __class__.tagPortManagerProxy(node_svc.port_manager, node1.port, C_COUNT_OF_BAD_PORT_USAGE) as proxy:
+                assert proxy.m_DummyPortNumber == node1.port
+                with __class__.helper__get_node(node_svc, port_manager=proxy) as node2:
+                    assert node2._should_free_port
+                    assert node2.port == node1.port
+
+                    node2.init().start()
+
+                    assert node2.port != node1.port
+                    assert node2._should_free_port
+                    assert proxy.m_DummyPortCurrentUsage == 0
+                    assert proxy.m_DummyPortTotalUsage == C_COUNT_OF_BAD_PORT_USAGE
+                    assert node2.is_started
+                    r = node2.safe_psql("SELECT 2;")
+                    assert __class__.helper__rm_carriage_returns(r) == b'2\n'
+
+            # node1 is still working
+            assert node1.port == node1_port_copy
+            assert node1._should_free_port
+            r = node1.safe_psql("SELECT 3;")
+            assert __class__.helper__rm_carriage_returns(r) == b'3\n'
+
+    def test_port_conflict(self, node_svc: PostgresNodeService):
+        assert type(node_svc) == PostgresNodeService  # noqa: E721
+        assert testgres.PostgresNode._C_MAX_START_ATEMPTS > 1
+
+        C_COUNT_OF_BAD_PORT_USAGE = testgres.PostgresNode._C_MAX_START_ATEMPTS
+
+        with __class__.helper__get_node(node_svc) as node1:
+            node1.init().start()
+            assert node1._should_free_port
+            assert type(node1.port) == int  # noqa: E721
+            node1_port_copy = node1.port
+            assert __class__.helper__rm_carriage_returns(node1.safe_psql("SELECT 1;")) == b'1\n'
+
+            with __class__.tagPortManagerProxy(node_svc.port_manager, node1.port, C_COUNT_OF_BAD_PORT_USAGE) as proxy:
+                assert proxy.m_DummyPortNumber == node1.port
+                with __class__.helper__get_node(node_svc, port_manager=proxy) as node2:
+                    assert node2._should_free_port
+                    assert node2.port == node1.port
+
+                    with pytest.raises(
+                        expected_exception=StartNodeException,
+                        match=re.escape("Cannot start node after multiple attempts.")
+                    ):
+                        node2.init().start()
+
+                    assert node2.port == node1.port
+                    assert node2._should_free_port
+                    assert proxy.m_DummyPortCurrentUsage == 1
+                    assert proxy.m_DummyPortTotalUsage == C_COUNT_OF_BAD_PORT_USAGE
+                    assert not node2.is_started
+
+                # node2 must release our dummyPort (node1.port)
+                assert (proxy.m_DummyPortCurrentUsage == 0)
+
+            # node1 is still working
+            assert node1.port == node1_port_copy
+            assert node1._should_free_port
+            r = node1.safe_psql("SELECT 3;")
+            assert __class__.helper__rm_carriage_returns(r) == b'3\n'
+
     @staticmethod
-    def helper__get_node(node_svc: PostgresNodeService, name=None, port=None):
+    def helper__get_node(
+        node_svc: PostgresNodeService,
+        name: typing.Optional[str] = None,
+        port: typing.Optional[int] = None,
+        port_manager: typing.Optional[PortManager] = None
+    ) -> PostgresNode:
         assert isinstance(node_svc, PostgresNodeService)
         assert isinstance(node_svc.os_ops, OsOperations)
         assert isinstance(node_svc.port_manager, PortManager)
+
+        if port_manager is None:
+            port_manager = node_svc.port_manager
+
         return PostgresNode(
             name,
             port=port,
             conn_params=None,
             os_ops=node_svc.os_ops,
-            port_manager=node_svc.port_manager if port is None else None
+            port_manager=port_manager if port is None else None
         )
 
     @staticmethod

From 322fb237d3ce6d43c950ee5698113fbe36806944 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 22:43:36 +0300
Subject: [PATCH 16/31] RemoteOperations::is_port_free is updated

---
 testgres/operations/remote_ops.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/testgres/operations/remote_ops.py b/testgres/operations/remote_ops.py
index 0547a262..f0a172b8 100644
--- a/testgres/operations/remote_ops.py
+++ b/testgres/operations/remote_ops.py
@@ -640,7 +640,7 @@ def is_port_free(self, number: int) -> bool:
         assert type(error) == str  # noqa: E721
 
         if exit_status == 0:
-            return __class__.helper__is_port_free__process_0(output)
+            return __class__.helper__is_port_free__process_0(error)
 
         if exit_status == 1:
             return __class__.helper__is_port_free__process_1(error)
@@ -656,15 +656,15 @@ def is_port_free(self, number: int) -> bool:
         )
 
     @staticmethod
-    def helper__is_port_free__process_0(output: str) -> bool:
-        assert type(output) == str  # noqa: E721
-        # TODO: check output message
+    def helper__is_port_free__process_0(error: str) -> bool:
+        assert type(error) == str  # noqa: E721
+        # TODO: check error message?
         return False
 
     @staticmethod
     def helper__is_port_free__process_1(error: str) -> bool:
         assert type(error) == str  # noqa: E721
-        # TODO: check error message
+        # TODO: check error message?
         return True
 
     # Database control

From 94da63ee0f0cf6cb30393f735650336f73331f5e Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 22:44:25 +0300
Subject: [PATCH 17/31] Tests for OsOps::is_port_free are added

---
 tests/test_os_ops_common.py | 99 +++++++++++++++++++++++++++++++++++++
 1 file changed, 99 insertions(+)

diff --git a/tests/test_os_ops_common.py b/tests/test_os_ops_common.py
index 1bcc054c..dfea848b 100644
--- a/tests/test_os_ops_common.py
+++ b/tests/test_os_ops_common.py
@@ -10,6 +10,8 @@
 import re
 import tempfile
 import logging
+import socket
+import threading
 
 from ..testgres import InvalidOperationException
 from ..testgres import ExecUtilException
@@ -648,3 +650,100 @@ def test_touch(self, os_ops: OsOperations):
         assert os_ops.isfile(filename)
 
         os_ops.remove_file(filename)
+
+    def test_is_port_free__true(self, os_ops: OsOperations):
+        assert isinstance(os_ops, OsOperations)
+
+        C_LIMIT = 10
+
+        ports = set(range(1024, 65535))
+        assert type(ports) == set  # noqa: E721
+
+        ok_count = 0
+        no_count = 0
+
+        for port in ports:
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                try:
+                    s.bind(("", port))
+                except OSError:
+                    continue
+
+            r = os_ops.is_port_free(port)
+
+            if r:
+                ok_count += 1
+                logging.info("OK. Port {} is free.".format(port))
+            else:
+                no_count += 1
+                logging.warning("NO. Port {} is not free.".format(port))
+
+            if ok_count == C_LIMIT:
+                return
+
+            if no_count == C_LIMIT:
+                raise RuntimeError("To many false positive test attempts.")
+
+        if ok_count == 0:
+            raise RuntimeError("No one free port was found.")
+
+    def test_is_port_free__false(self, os_ops: OsOperations):
+        assert isinstance(os_ops, OsOperations)
+
+        C_LIMIT = 10
+
+        ports = set(range(1024, 65535))
+        assert type(ports) == set  # noqa: E721
+
+        def LOCAL_server(s: socket.socket):
+            assert s is not None
+            assert type(s) == socket.socket  # noqa: E721
+
+            try:
+                while True:
+                    r = s.accept()
+
+                    if r is None:
+                        break
+            except Exception as e:
+                assert e is not None
+                pass
+
+        ok_count = 0
+        no_count = 0
+
+        for port in ports:
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                try:
+                    s.bind(("", port))
+                except OSError:
+                    continue
+
+                th = threading.Thread(target=LOCAL_server, args=[s])
+
+                s.listen(10)
+
+                assert type(th) == threading.Thread  # noqa: E721
+                th.start()
+
+                try:
+                    r = os_ops.is_port_free(port)
+                finally:
+                    s.shutdown(2)
+                    th.join()
+
+                if not r:
+                    ok_count += 1
+                    logging.info("OK. Port {} is not free.".format(port))
+                else:
+                    no_count += 1
+                    logging.warning("NO. Port {} does not accept connection.".format(port))
+
+                if ok_count == C_LIMIT:
+                    return
+
+                if no_count == C_LIMIT:
+                    raise RuntimeError("To many false positive test attempts.")
+
+        if ok_count == 0:
+            raise RuntimeError("No one free port was found.")

From 88f9b73da7f1cd5a91ecf88dba451b68ae9edc70 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Fri, 4 Apr 2025 23:14:27 +0300
Subject: [PATCH 18/31] TestTestgresCommon is corrected [python problems]

---
 tests/test_testgres_common.py | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 85368824..2fd678ef 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -13,8 +13,6 @@
 from ..testgres import NodeStatus
 from ..testgres import IsolationLevel
 
-import testgres
-
 # New name prevents to collect test-functions in TestgresException and fixes
 # the problem with pytest warning.
 from ..testgres import TestgresException as testgres_TestgresException
@@ -1250,7 +1248,7 @@ def release_port(self, dummyPortNumber: int):
 
     def test_port_rereserve_during_node_start(self, node_svc: PostgresNodeService):
         assert type(node_svc) == PostgresNodeService  # noqa: E721
-        assert testgres.PostgresNode._C_MAX_START_ATEMPTS == 5
+        assert PostgresNode._C_MAX_START_ATEMPTS == 5
 
         C_COUNT_OF_BAD_PORT_USAGE = 3
 
@@ -1285,9 +1283,9 @@ def test_port_rereserve_during_node_start(self, node_svc: PostgresNodeService):
 
     def test_port_conflict(self, node_svc: PostgresNodeService):
         assert type(node_svc) == PostgresNodeService  # noqa: E721
-        assert testgres.PostgresNode._C_MAX_START_ATEMPTS > 1
+        assert PostgresNode._C_MAX_START_ATEMPTS > 1
 
-        C_COUNT_OF_BAD_PORT_USAGE = testgres.PostgresNode._C_MAX_START_ATEMPTS
+        C_COUNT_OF_BAD_PORT_USAGE = PostgresNode._C_MAX_START_ATEMPTS
 
         with __class__.helper__get_node(node_svc) as node1:
             node1.init().start()

From d9558cedf01d3dbeac9d31f075edfa613e501e2e Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 00:17:03 +0300
Subject: [PATCH 19/31] The call of RaiseError.CommandExecutionError is fixed
 [message, not msg_arg]

- RemoteOperations::message
- RemoteOperations::path_exists
- RemoteOperations::is_port_free
---
 testgres/operations/remote_ops.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/testgres/operations/remote_ops.py b/testgres/operations/remote_ops.py
index f0a172b8..6a678745 100644
--- a/testgres/operations/remote_ops.py
+++ b/testgres/operations/remote_ops.py
@@ -193,7 +193,7 @@ def is_executable(self, file):
         RaiseError.CommandExecutionError(
             cmd=command,
             exit_code=exit_status,
-            msg_arg=errMsg,
+            message=errMsg,
             error=error,
             out=output
         )
@@ -305,7 +305,7 @@ def path_exists(self, path):
         RaiseError.CommandExecutionError(
             cmd=command,
             exit_code=exit_status,
-            msg_arg=errMsg,
+            message=errMsg,
             error=error,
             out=output
         )
@@ -650,7 +650,7 @@ def is_port_free(self, number: int) -> bool:
         RaiseError.CommandExecutionError(
             cmd=cmd,
             exit_code=exit_status,
-            msg_arg=errMsg,
+            message=errMsg,
             error=error,
             out=output
         )

From 0da4c21075cfa5dbb4b011e81b516362fd2fb77b Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 00:35:18 +0300
Subject: [PATCH 20/31] [CI] ubuntu 24.04 does not have nc

Let's install it (netcat-traditional) explicitly.
---
 Dockerfile--ubuntu_24_04.tmpl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Dockerfile--ubuntu_24_04.tmpl b/Dockerfile--ubuntu_24_04.tmpl
index 3bdc6640..7a559776 100644
--- a/Dockerfile--ubuntu_24_04.tmpl
+++ b/Dockerfile--ubuntu_24_04.tmpl
@@ -10,6 +10,7 @@ RUN apt install -y sudo curl ca-certificates
 RUN apt update
 RUN apt install -y openssh-server
 RUN apt install -y time
+RUN apt install -y netcat-traditional
 
 RUN apt update
 RUN apt install -y postgresql-common

From 0058508b12cb4354193b684cef26b3a054fcb66e Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 09:43:06 +0300
Subject: [PATCH 21/31] RemoteOperations is update [private method names]

---
 testgres/operations/remote_ops.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/testgres/operations/remote_ops.py b/testgres/operations/remote_ops.py
index 6a678745..21caa560 100644
--- a/testgres/operations/remote_ops.py
+++ b/testgres/operations/remote_ops.py
@@ -640,10 +640,10 @@ def is_port_free(self, number: int) -> bool:
         assert type(error) == str  # noqa: E721
 
         if exit_status == 0:
-            return __class__.helper__is_port_free__process_0(error)
+            return __class__._is_port_free__process_0(error)
 
         if exit_status == 1:
-            return __class__.helper__is_port_free__process_1(error)
+            return __class__._is_port_free__process_1(error)
 
         errMsg = "nc returns an unknown result code: {0}".format(exit_status)
 
@@ -656,13 +656,13 @@ def is_port_free(self, number: int) -> bool:
         )
 
     @staticmethod
-    def helper__is_port_free__process_0(error: str) -> bool:
+    def _is_port_free__process_0(error: str) -> bool:
         assert type(error) == str  # noqa: E721
         # TODO: check error message?
         return False
 
     @staticmethod
-    def helper__is_port_free__process_1(error: str) -> bool:
+    def _is_port_free__process_1(error: str) -> bool:
         assert type(error) == str  # noqa: E721
         # TODO: check error message?
         return True

From 8f3a56603ccbe931301bb96bf91718bf744793c9 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 09:44:48 +0300
Subject: [PATCH 22/31] test_is_port_free__true is updated

A number of attempts is increased to 128.

The previous value (10) is not enough and test could fail.
---
 tests/test_os_ops_common.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_os_ops_common.py b/tests/test_os_ops_common.py
index dfea848b..7d183775 100644
--- a/tests/test_os_ops_common.py
+++ b/tests/test_os_ops_common.py
@@ -654,7 +654,7 @@ def test_touch(self, os_ops: OsOperations):
     def test_is_port_free__true(self, os_ops: OsOperations):
         assert isinstance(os_ops, OsOperations)
 
-        C_LIMIT = 10
+        C_LIMIT = 128
 
         ports = set(range(1024, 65535))
         assert type(ports) == set  # noqa: E721

From d8ebdb76baba0ec36c7de9456c5e375faae54b50 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 09:53:01 +0300
Subject: [PATCH 23/31] RemoteOperations::is_port_free is updated (comments)

---
 testgres/operations/remote_ops.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/testgres/operations/remote_ops.py b/testgres/operations/remote_ops.py
index 21caa560..ee747e52 100644
--- a/testgres/operations/remote_ops.py
+++ b/testgres/operations/remote_ops.py
@@ -658,13 +658,23 @@ def is_port_free(self, number: int) -> bool:
     @staticmethod
     def _is_port_free__process_0(error: str) -> bool:
         assert type(error) == str  # noqa: E721
-        # TODO: check error message?
+        #
+        # Example of error text:
+        #  "Connection to localhost (127.0.0.1) 1024 port [tcp/*] succeeded!\n"
+        #
+        # May be here is needed to check error message?
+        #
         return False
 
     @staticmethod
     def _is_port_free__process_1(error: str) -> bool:
         assert type(error) == str  # noqa: E721
-        # TODO: check error message?
+        #
+        # Example of error text:
+        #  "nc: connect to localhost (127.0.0.1) port 1024 (tcp) failed: Connection refused\n"
+        #
+        # May be here is needed to check error message?
+        #
         return True
 
     # Database control

From 30e472c13271a7d5a851a9455b44bc735138645b Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 12:12:57 +0300
Subject: [PATCH 24/31] setup.py is updated [testgres.helpers was deleted]

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 3f2474dd..b47a1d8a 100755
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@
 setup(
     version='1.10.5',
     name='testgres',
-    packages=['testgres', 'testgres.operations', 'testgres.helpers'],
+    packages=['testgres', 'testgres.operations'],
     description='Testing utility for PostgreSQL and its extensions',
     url='https://github.com/postgrespro/testgres',
     long_description=readme,

From c94bbb58e6a19a2ad6ba94897eacb4b40179bd46 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 20:14:13 +0300
Subject: [PATCH 25/31] Comment in node.py is updated

---
 testgres/node.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/testgres/node.py b/testgres/node.py
index 99ec2032..ab516a6a 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -276,7 +276,7 @@ def _get_port_manager(os_ops: OsOperations) -> PortManager:
         if isinstance(os_ops, LocalOperations):
             return PortManager__ThisHost()
 
-        # TODO: Throw exception "Please define a port manager."
+        # TODO: Throw the exception "Please define a port manager." ?
         return PortManager__Generic(os_ops)
 
     def clone_with_new_name_and_base_dir(self, name: str, base_dir: str):

From 04f88c7a5ebc6743615b9e4656a04570665f5184 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 20:15:09 +0300
Subject: [PATCH 26/31] PostgresNode::_node was deleted [use self._os_ops.host]

---
 testgres/node.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index ab516a6a..15bf3246 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -142,7 +142,6 @@ class PostgresNode(object):
     _C_MAX_START_ATEMPTS = 5
 
     _name: typing.Optional[str]
-    _host: typing.Optional[str]
     _port: typing.Optional[int]
     _should_free_port: bool
     _os_ops: OsOperations
@@ -193,7 +192,8 @@ def __init__(self,
 
         # basic
         self._name = name or generate_app_name()
-        self._host = self._os_ops.host
+
+        assert hasattr(os_ops, "host")
 
         if port is not None:
             assert type(port) == int  # noqa: E721
@@ -319,10 +319,9 @@ def name(self) -> str:
 
     @property
     def host(self) -> str:
-        if self._host is None:
-            raise InvalidOperationException("PostgresNode host is not defined.")
-        assert type(self._host) == str  # noqa: E721
-        return self._host
+        assert self._os_ops is not None
+        assert isinstance(self._os_ops, OsOperations)
+        return self._os_ops.host
 
     @property
     def port(self) -> int:

From 0a3442afab05d0d81e18c7e0a200e4001b2b07e2 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 20:17:44 +0300
Subject: [PATCH 27/31] PostgresNode::start is corrected [error message]

---
 testgres/node.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/testgres/node.py b/testgres/node.py
index 15bf3246..9902554c 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -1088,7 +1088,7 @@ def start(self, params=[], wait=True):
             return self
 
         if self._port is None:
-            raise InvalidOperationException("Can't start PostgresNode. Port is node defined.")
+            raise InvalidOperationException("Can't start PostgresNode. Port is not defined.")
 
         assert type(self._port) == int  # noqa: E721
 

From 13e71d85b39d2f76d944c30f612bde45ddc15b23 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sat, 5 Apr 2025 20:35:22 +0300
Subject: [PATCH 28/31] [FIX] PostgresNode.__init__ must not test "os_ops.host"
 attribute.

- [del] assert hasattr(os_ops, "host")

During this test we get another exception:

<[AttributeError("'PostgresNode' object has no attribute '_port'") raised in repr()] PostgresNode object at 0x782b67d79dc0>
---
 testgres/node.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 9902554c..7c1ee136 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -193,8 +193,6 @@ def __init__(self,
         # basic
         self._name = name or generate_app_name()
 
-        assert hasattr(os_ops, "host")
-
         if port is not None:
             assert type(port) == int  # noqa: E721
             assert port_manager is None

From 9e14f4a95f5914d7e3c1003cf57d69135b4d44b4 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 6 Apr 2025 10:16:56 +0300
Subject: [PATCH 29/31] PostgresNode.free_port always set a port to None

Tests are added:
 - test_try_to_get_port_after_free_manual_port
 - test_try_to_start_node_after_free_manual_port
---
 testgres/node.py              |  7 +++--
 tests/test_testgres_common.py | 59 +++++++++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 7c1ee136..7457bf7e 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -1321,10 +1321,13 @@ def pg_ctl(self, params):
     def free_port(self):
         """
         Reclaim port owned by this node.
-        NOTE: does not free auto selected ports.
+        NOTE: this method does not release manually defined port but reset it.
         """
+        assert type(self._should_free_port) == bool
 
-        if self._should_free_port:
+        if not self._should_free_port:
+            self._port = None
+        else:
             assert type(self._port) == int  # noqa: E721
 
             assert self._port_manager is not None
diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index 2fd678ef..b52883de 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1321,6 +1321,65 @@ def test_port_conflict(self, node_svc: PostgresNodeService):
             r = node1.safe_psql("SELECT 3;")
             assert __class__.helper__rm_carriage_returns(r) == b'3\n'
 
+    def test_try_to_get_port_after_free_manual_port(self, node_svc: PostgresNodeService):
+        assert type(node_svc) == PostgresNodeService  # noqa: E721
+
+        assert node_svc.port_manager is not None
+        assert isinstance(node_svc.port_manager, PortManager)
+
+        with __class__.helper__get_node(node_svc) as node1:
+            assert node1 is not None
+            assert type(node1) == PostgresNode
+            assert node1.port is not None
+            assert type(node1.port) == int
+            with __class__.helper__get_node(node_svc, port=node1.port, port_manager=None) as node2:
+                assert node2 is not None
+                assert type(node1) == PostgresNode
+                assert node2 is not node1
+                assert node2.port is not None
+                assert type(node2.port) == int
+                assert node2.port == node1.port
+
+                logging.info("Release node2 port")
+                node2.free_port()
+
+                logging.info("try to get node2.port...")
+                with pytest.raises(
+                    InvalidOperationException,
+                    match="^" + re.escape("PostgresNode port is not defined.") + "$"
+                ):
+                    p = node2.port
+                    assert p is None
+
+    def test_try_to_start_node_after_free_manual_port(self, node_svc: PostgresNodeService):
+        assert type(node_svc) == PostgresNodeService  # noqa: E721
+
+        assert node_svc.port_manager is not None
+        assert isinstance(node_svc.port_manager, PortManager)
+
+        with __class__.helper__get_node(node_svc) as node1:
+            assert node1 is not None
+            assert type(node1) == PostgresNode
+            assert node1.port is not None
+            assert type(node1.port) == int
+            with __class__.helper__get_node(node_svc, port=node1.port, port_manager=None) as node2:
+                assert node2 is not None
+                assert type(node1) == PostgresNode
+                assert node2 is not node1
+                assert node2.port is not None
+                assert type(node2.port) == int
+                assert node2.port == node1.port
+
+                logging.info("Release node2 port")
+                node2.free_port()
+
+                logging.info("node2 is trying to start...")
+                with pytest.raises(
+                    InvalidOperationException,
+                    match="^" + re.escape("Can't start PostgresNode. Port is not defined.") + "$"
+                ):
+                    node2.start()
+
     @staticmethod
     def helper__get_node(
         node_svc: PostgresNodeService,

From 739ef617cce66cb36ff5c4db8015ee4684f7d3e7 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 6 Apr 2025 10:51:38 +0300
Subject: [PATCH 30/31] [FIX] flake8 (noqa: E721)

---
 testgres/node.py              |  2 +-
 tests/test_testgres_common.py | 16 ++++++++--------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/testgres/node.py b/testgres/node.py
index 7457bf7e..5039fc43 100644
--- a/testgres/node.py
+++ b/testgres/node.py
@@ -1323,7 +1323,7 @@ def free_port(self):
         Reclaim port owned by this node.
         NOTE: this method does not release manually defined port but reset it.
         """
-        assert type(self._should_free_port) == bool
+        assert type(self._should_free_port) == bool  # noqa: E721
 
         if not self._should_free_port:
             self._port = None
diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py
index b52883de..b286a1c6 100644
--- a/tests/test_testgres_common.py
+++ b/tests/test_testgres_common.py
@@ -1329,15 +1329,15 @@ def test_try_to_get_port_after_free_manual_port(self, node_svc: PostgresNodeServ
 
         with __class__.helper__get_node(node_svc) as node1:
             assert node1 is not None
-            assert type(node1) == PostgresNode
+            assert type(node1) == PostgresNode  # noqa: E721
             assert node1.port is not None
-            assert type(node1.port) == int
+            assert type(node1.port) == int  # noqa: E721
             with __class__.helper__get_node(node_svc, port=node1.port, port_manager=None) as node2:
                 assert node2 is not None
-                assert type(node1) == PostgresNode
+                assert type(node1) == PostgresNode  # noqa: E721
                 assert node2 is not node1
                 assert node2.port is not None
-                assert type(node2.port) == int
+                assert type(node2.port) == int  # noqa: E721
                 assert node2.port == node1.port
 
                 logging.info("Release node2 port")
@@ -1359,15 +1359,15 @@ def test_try_to_start_node_after_free_manual_port(self, node_svc: PostgresNodeSe
 
         with __class__.helper__get_node(node_svc) as node1:
             assert node1 is not None
-            assert type(node1) == PostgresNode
+            assert type(node1) == PostgresNode  # noqa: E721
             assert node1.port is not None
-            assert type(node1.port) == int
+            assert type(node1.port) == int  # noqa: E721
             with __class__.helper__get_node(node_svc, port=node1.port, port_manager=None) as node2:
                 assert node2 is not None
-                assert type(node1) == PostgresNode
+                assert type(node1) == PostgresNode  # noqa: E721
                 assert node2 is not node1
                 assert node2.port is not None
-                assert type(node2.port) == int
+                assert type(node2.port) == int  # noqa: E721
                 assert node2.port == node1.port
 
                 logging.info("Release node2 port")

From 696cc1ef8a4ba6e438057f773a9833e0fea0eb08 Mon Sep 17 00:00:00 2001
From: "d.kovalenko" <dmitry.lipetsk@gmail.com>
Date: Sun, 6 Apr 2025 10:55:24 +0300
Subject: [PATCH 31/31] PortManager__Generic is refactored

---
 testgres/port_manager.py | 52 ++++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 21 deletions(-)

diff --git a/testgres/port_manager.py b/testgres/port_manager.py
index e4c2180c..164661e7 100644
--- a/testgres/port_manager.py
+++ b/testgres/port_manager.py
@@ -48,45 +48,55 @@ def release_port(self, number: int) -> None:
 
 class PortManager__Generic(PortManager):
     _os_ops: OsOperations
-    _allocated_ports_guard: object
-    _allocated_ports: set[int]
+    _guard: object
+    # TODO: is there better to use bitmap fot _available_ports?
+    _available_ports: set[int]
+    _reserved_ports: set[int]
 
     def __init__(self, os_ops: OsOperations):
         assert os_ops is not None
         assert isinstance(os_ops, OsOperations)
         self._os_ops = os_ops
-        self._allocated_ports_guard = threading.Lock()
-        self._allocated_ports = set[int]()
+        self._guard = threading.Lock()
+        self._available_ports = set[int](range(1024, 65535))
+        self._reserved_ports = set[int]()
 
     def reserve_port(self) -> int:
-        ports = set(range(1024, 65535))
-        assert type(ports) == set  # noqa: E721
+        assert self._guard is not None
+        assert type(self._available_ports) == set  # noqa: E721t
+        assert type(self._reserved_ports) == set  # noqa: E721
 
-        assert self._allocated_ports_guard is not None
-        assert type(self._allocated_ports) == set  # noqa: E721
-
-        with self._allocated_ports_guard:
-            ports.difference_update(self._allocated_ports)
-
-            sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
+        with self._guard:
+            t = tuple(self._available_ports)
+            assert len(t) == len(self._available_ports)
+            sampled_ports = random.sample(t, min(len(t), 100))
+            t = None
 
             for port in sampled_ports:
-                assert not (port in self._allocated_ports)
+                assert not (port in self._reserved_ports)
+                assert port in self._available_ports
 
                 if not self._os_ops.is_port_free(port):
                     continue
 
-                self._allocated_ports.add(port)
+                self._reserved_ports.add(port)
+                self._available_ports.discard(port)
+                assert port in self._reserved_ports
+                assert not (port in self._available_ports)
                 return port
 
-        raise PortForException("Can't select a port")
+        raise PortForException("Can't select a port.")
 
     def release_port(self, number: int) -> None:
         assert type(number) == int  # noqa: E721
 
-        assert self._allocated_ports_guard is not None
-        assert type(self._allocated_ports) == set  # noqa: E721
+        assert self._guard is not None
+        assert type(self._reserved_ports) == set  # noqa: E721
 
-        with self._allocated_ports_guard:
-            assert number in self._allocated_ports
-            self._allocated_ports.discard(number)
+        with self._guard:
+            assert number in self._reserved_ports
+            assert not (number in self._available_ports)
+            self._available_ports.add(number)
+            self._reserved_ports.discard(number)
+            assert not (number in self._reserved_ports)
+            assert number in self._available_ports