testgres 1.14.4__tar.gz → 1.14.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {testgres-1.14.4/testgres.egg-info → testgres-1.14.5}/PKG-INFO +1 -1
- {testgres-1.14.4 → testgres-1.14.5}/src/__init__.py +3 -2
- {testgres-1.14.4 → testgres-1.14.5}/src/connection.py +2 -2
- {testgres-1.14.4 → testgres-1.14.5}/src/defaults.py +24 -2
- testgres-1.14.5/src/impl/file_line_reader.py +80 -0
- testgres-1.14.5/src/logger.py +116 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/node.py +131 -52
- {testgres-1.14.4 → testgres-1.14.5}/src/pubsub.py +154 -29
- {testgres-1.14.4 → testgres-1.14.5}/src/utils.py +15 -8
- {testgres-1.14.4 → testgres-1.14.5/testgres.egg-info}/PKG-INFO +1 -1
- {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/SOURCES.txt +1 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_common.py +84 -5
- testgres-1.14.4/src/logger.py +0 -49
- {testgres-1.14.4 → testgres-1.14.5}/LICENSE +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/README.md +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/pyproject.toml +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/setup.cfg +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/api.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/backup.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/cache.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/config.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/consts.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/decorators.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/enums.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/exceptions.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/internal_utils.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/internal_platform_utils.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/internal_platform_utils_factory.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/linux/internal_platform_utils.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/win32/internal_platform_utils.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/port_manager__generic.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/impl/port_manager__this_host.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/node_app.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/port_manager.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/raise_error.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/src/standby.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/dependency_links.txt +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/requires.txt +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/top_level.txt +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_api.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_config.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_common.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_local.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_remote.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_raise_error.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_local.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_remote.py +0 -0
- {testgres-1.14.4 → testgres-1.14.5}/tests/test_utils.py +0 -0
|
@@ -43,6 +43,7 @@ from .utils import \
|
|
|
43
43
|
release_port, \
|
|
44
44
|
bound_ports, \
|
|
45
45
|
get_bin_path, \
|
|
46
|
+
get_bin_dir, \
|
|
46
47
|
get_pg_config, \
|
|
47
48
|
get_pg_version, \
|
|
48
49
|
parse_pg_version
|
|
@@ -57,7 +58,7 @@ from testgres.operations.os_ops import OsOperations, ConnectionParams
|
|
|
57
58
|
from testgres.operations.local_ops import LocalOperations
|
|
58
59
|
from testgres.operations.remote_ops import RemoteOperations
|
|
59
60
|
|
|
60
|
-
__version__ = "1.14.
|
|
61
|
+
__version__ = "1.14.5"
|
|
61
62
|
|
|
62
63
|
__all__ = [
|
|
63
64
|
"get_new_node",
|
|
@@ -72,7 +73,7 @@ __all__ = [
|
|
|
72
73
|
"NodeApp",
|
|
73
74
|
"PostgresNode",
|
|
74
75
|
"PortManager",
|
|
75
|
-
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version", "parse_pg_version",
|
|
76
|
+
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_bin_dir", "get_pg_config", "get_pg_version", "parse_pg_version",
|
|
76
77
|
"First", "Any",
|
|
77
78
|
"OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
|
|
78
79
|
]
|
|
@@ -14,7 +14,7 @@ from .enums import IsolationLevel
|
|
|
14
14
|
|
|
15
15
|
from .defaults import \
|
|
16
16
|
default_dbname, \
|
|
17
|
-
|
|
17
|
+
default_username2
|
|
18
18
|
|
|
19
19
|
from .exceptions import QueryException
|
|
20
20
|
|
|
@@ -38,7 +38,7 @@ class NodeConnection(object):
|
|
|
38
38
|
|
|
39
39
|
# Set default arguments
|
|
40
40
|
dbname = dbname or default_dbname()
|
|
41
|
-
username = username or
|
|
41
|
+
username = username or default_username2(node.os_ops)
|
|
42
42
|
|
|
43
43
|
self._node = node
|
|
44
44
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import struct
|
|
3
3
|
import uuid
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from testgres.operations.os_ops import OsOperations
|
|
4
7
|
|
|
5
8
|
from .config import testgres_config as tconf
|
|
6
9
|
|
|
@@ -13,11 +16,30 @@ def default_dbname():
|
|
|
13
16
|
return 'postgres'
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
def default_username():
|
|
19
|
+
def default_username(os_ops: typing.Optional[OsOperations] = None) -> str:
|
|
17
20
|
"""
|
|
18
21
|
Return default username (current user).
|
|
19
22
|
"""
|
|
20
|
-
|
|
23
|
+
assert os_ops is None or isinstance(os_ops, OsOperations)
|
|
24
|
+
|
|
25
|
+
if os_ops is None:
|
|
26
|
+
os_ops = tconf.os_ops
|
|
27
|
+
|
|
28
|
+
assert isinstance(os_ops, OsOperations)
|
|
29
|
+
result = default_username2(os_ops)
|
|
30
|
+
assert type(result) is str
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_username2(os_ops: OsOperations) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Return default username (current user).
|
|
37
|
+
"""
|
|
38
|
+
assert isinstance(os_ops, OsOperations)
|
|
39
|
+
|
|
40
|
+
result = os_ops.get_user()
|
|
41
|
+
assert type(result) is str
|
|
42
|
+
return result
|
|
21
43
|
|
|
22
44
|
|
|
23
45
|
def generate_app_name():
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from testgres.operations.os_ops import OsOperations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileLineReader:
|
|
9
|
+
_os_ops: OsOperations
|
|
10
|
+
_file_name: str
|
|
11
|
+
_file_encoding: str
|
|
12
|
+
_file_pos: int
|
|
13
|
+
_buffer_pos: int
|
|
14
|
+
_buffer: bytes
|
|
15
|
+
|
|
16
|
+
# --------------------------------------------------------------------
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
os_ops: OsOperations,
|
|
20
|
+
file_name: str,
|
|
21
|
+
file_encoding: str = "utf-8",
|
|
22
|
+
):
|
|
23
|
+
assert isinstance(os_ops, OsOperations)
|
|
24
|
+
assert type(file_encoding) is str
|
|
25
|
+
self._os_ops = os_ops
|
|
26
|
+
self._file_name = file_name
|
|
27
|
+
self._file_encoding = file_encoding
|
|
28
|
+
self._file_pos = 0
|
|
29
|
+
self._buffer_pos = 0
|
|
30
|
+
self._buffer = b''
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# interface ----------------------------------------------------------
|
|
34
|
+
def read_line(self) -> typing.Optional[str]:
|
|
35
|
+
assert isinstance(self._os_ops, OsOperations)
|
|
36
|
+
assert type(self._buffer_pos) is int
|
|
37
|
+
assert type(self._buffer) is bytes
|
|
38
|
+
assert self._buffer_pos >= 0
|
|
39
|
+
assert self._buffer_pos <= len(self._buffer)
|
|
40
|
+
|
|
41
|
+
scan_pos = self._buffer_pos
|
|
42
|
+
|
|
43
|
+
while True:
|
|
44
|
+
sz1 = len(self._buffer)
|
|
45
|
+
|
|
46
|
+
if scan_pos == sz1:
|
|
47
|
+
block = self._os_ops.read_binary(
|
|
48
|
+
self._file_name,
|
|
49
|
+
self._file_pos,
|
|
50
|
+
)
|
|
51
|
+
assert type(block) is bytes
|
|
52
|
+
self._buffer += block
|
|
53
|
+
self._file_pos += len(block)
|
|
54
|
+
|
|
55
|
+
x = self._buffer.find(b'\n', scan_pos)
|
|
56
|
+
|
|
57
|
+
sz2 = len(self._buffer)
|
|
58
|
+
|
|
59
|
+
if x == -1:
|
|
60
|
+
if scan_pos == sz2:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
if self._buffer_pos == 0:
|
|
64
|
+
scan_pos = sz2
|
|
65
|
+
else:
|
|
66
|
+
assert self._buffer_pos > 0
|
|
67
|
+
self._buffer = self._buffer[self._buffer_pos:]
|
|
68
|
+
scan_pos = sz2 - self._buffer_pos
|
|
69
|
+
self._buffer_pos = 0
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
assert x >= 0
|
|
73
|
+
assert x < sz2
|
|
74
|
+
|
|
75
|
+
b = self._buffer[self._buffer_pos:(x+1)]
|
|
76
|
+
|
|
77
|
+
s = b.decode(self._file_encoding)
|
|
78
|
+
|
|
79
|
+
self._buffer_pos = x + 1
|
|
80
|
+
return s
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
from .config import testgres_config as tconf
|
|
4
|
+
from .exceptions import ExecUtilException
|
|
5
|
+
from .impl.file_line_reader import FileLineReader
|
|
6
|
+
|
|
7
|
+
from testgres.operations.os_ops import OsOperations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import typing
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestgresLogger(threading.Thread):
|
|
16
|
+
_C_SLEEP_MIN = 0.01
|
|
17
|
+
_C_SLEEP_MAX = 60
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
Helper class to implement reading from log files.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
node_name: str,
|
|
25
|
+
log_file_name: str,
|
|
26
|
+
log_file_encoding: str = "utf-8",
|
|
27
|
+
os_ops: typing.Optional[OsOperations] = None,
|
|
28
|
+
):
|
|
29
|
+
assert type(node_name) is str
|
|
30
|
+
assert type(log_file_name) is str
|
|
31
|
+
assert type(log_file_encoding) is str
|
|
32
|
+
assert os_ops is None or isinstance(os_ops, OsOperations)
|
|
33
|
+
|
|
34
|
+
threading.Thread.__init__(self)
|
|
35
|
+
|
|
36
|
+
if os_ops is None:
|
|
37
|
+
os_ops = tconf.os_ops
|
|
38
|
+
|
|
39
|
+
assert os_ops is None or isinstance(os_ops, OsOperations)
|
|
40
|
+
|
|
41
|
+
self._os_ops = os_ops
|
|
42
|
+
self._node_name = node_name
|
|
43
|
+
self._log_file_name = log_file_name
|
|
44
|
+
self._log_file_encoding = log_file_encoding
|
|
45
|
+
self._stop_event = threading.Event()
|
|
46
|
+
self._logger = logging.getLogger(node_name)
|
|
47
|
+
self._logger.setLevel(logging.INFO)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
def run(self):
|
|
51
|
+
# open log file for reading
|
|
52
|
+
file_line_reader = FileLineReader(
|
|
53
|
+
self._os_ops,
|
|
54
|
+
self._log_file_name,
|
|
55
|
+
self._log_file_encoding,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
sleep_time = __class__._C_SLEEP_MIN
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# work until we're asked to stop
|
|
62
|
+
while not self._stop_event.is_set():
|
|
63
|
+
line = None
|
|
64
|
+
|
|
65
|
+
while True:
|
|
66
|
+
try:
|
|
67
|
+
line = file_line_reader.read_line() # raise
|
|
68
|
+
except Exception as e:
|
|
69
|
+
if __class__._is_file_not_found_exception(e):
|
|
70
|
+
if not self._os_ops.path_exists(self._log_file_name):
|
|
71
|
+
break
|
|
72
|
+
raise
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if line is None:
|
|
76
|
+
time.sleep(sleep_time)
|
|
77
|
+
sleep_time = min(__class__._C_SLEEP_MAX, 2 * sleep_time)
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
assert type(line) is str
|
|
81
|
+
|
|
82
|
+
sleep_time = __class__._C_SLEEP_MIN
|
|
83
|
+
|
|
84
|
+
# do we have new lines?
|
|
85
|
+
line = line.strip()
|
|
86
|
+
|
|
87
|
+
extra = {'node': self._node_name}
|
|
88
|
+
self._logger.info(line, extra=extra)
|
|
89
|
+
continue
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self._logger.error(e)
|
|
92
|
+
raise
|
|
93
|
+
finally:
|
|
94
|
+
# don't forget to clear event
|
|
95
|
+
# [2026-07-10] legacy cargo cult, thread is single-use only.
|
|
96
|
+
# self._stop_event.clear()
|
|
97
|
+
pass
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
def stop(self, wait=True):
|
|
101
|
+
self._stop_event.set()
|
|
102
|
+
|
|
103
|
+
if wait:
|
|
104
|
+
self.join()
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _is_file_not_found_exception(e: Exception) -> bool:
|
|
109
|
+
if isinstance(e, FileNotFoundError):
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
if isinstance(e, ExecUtilException):
|
|
113
|
+
if e.exit_code == 2:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
@@ -729,7 +729,11 @@ class PostgresNode(object):
|
|
|
729
729
|
if testgres_config.use_python_logging:
|
|
730
730
|
# spawn new logger if it doesn't exist or is stopped
|
|
731
731
|
if not self._logger or not self._logger.is_alive():
|
|
732
|
-
self._logger = TestgresLogger(
|
|
732
|
+
self._logger = TestgresLogger(
|
|
733
|
+
self.name,
|
|
734
|
+
self.pg_log_file,
|
|
735
|
+
os_ops=self._os_ops,
|
|
736
|
+
)
|
|
733
737
|
self._logger.start()
|
|
734
738
|
|
|
735
739
|
def _maybe_stop_logger(self):
|
|
@@ -807,7 +811,7 @@ class PostgresNode(object):
|
|
|
807
811
|
Args:
|
|
808
812
|
fsync: should this node use fsync to keep data safe?
|
|
809
813
|
unix_sockets: should we enable UNIX sockets?
|
|
810
|
-
allow_streaming: should this node add a hba entry for replication?
|
|
814
|
+
allow_streaming: (ignored) should this node add a hba entry for replication?
|
|
811
815
|
allow_logical: can this node be used as a logical replication publisher?
|
|
812
816
|
log_statement: one of ('all', 'off', 'mod', 'ddl').
|
|
813
817
|
|
|
@@ -818,44 +822,10 @@ class PostgresNode(object):
|
|
|
818
822
|
assert self._os_ops is not None
|
|
819
823
|
assert isinstance(self._os_ops, OsOperations)
|
|
820
824
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
# filter lines in hba file
|
|
825
|
-
# get rid of comments and blank lines
|
|
826
|
-
hba_conf_file = self._os_ops.readlines(hba_conf)
|
|
827
|
-
lines = [
|
|
828
|
-
s for s in hba_conf_file
|
|
829
|
-
if len(s.strip()) > 0 and not s.startswith('#')
|
|
830
|
-
]
|
|
831
|
-
|
|
832
|
-
# write filtered lines
|
|
833
|
-
self._os_ops.write(hba_conf, lines, truncate=True)
|
|
825
|
+
# hba file is updated
|
|
826
|
+
self._default_conf__hba()
|
|
834
827
|
|
|
835
|
-
|
|
836
|
-
if allow_streaming:
|
|
837
|
-
# get auth method for host or local users
|
|
838
|
-
def get_auth_method(t):
|
|
839
|
-
return next((s.split()[-1]
|
|
840
|
-
for s in lines if s.startswith(t)), 'trust')
|
|
841
|
-
|
|
842
|
-
# get auth methods
|
|
843
|
-
auth_local = get_auth_method('local')
|
|
844
|
-
auth_host = get_auth_method('host')
|
|
845
|
-
subnet_base = ".".join(self._os_ops.host.split('.')[:-1] + ['0'])
|
|
846
|
-
|
|
847
|
-
new_lines = [
|
|
848
|
-
u"local\treplication\tall\t\t\t{}\n".format(auth_local),
|
|
849
|
-
u"host\treplication\tall\t127.0.0.1/32\t{}\n".format(auth_host),
|
|
850
|
-
u"host\treplication\tall\t::1/128\t\t{}\n".format(auth_host),
|
|
851
|
-
u"host\treplication\tall\t{}/24\t\t{}\n".format(subnet_base, auth_host),
|
|
852
|
-
u"host\tall\tall\t{}/24\t\t{}\n".format(subnet_base, auth_host),
|
|
853
|
-
u"host\tall\tall\tall\t{}\n".format(auth_host),
|
|
854
|
-
u"host\treplication\tall\tall\t{}\n".format(auth_host)
|
|
855
|
-
] # yapf: disable
|
|
856
|
-
|
|
857
|
-
# write missing lines
|
|
858
|
-
self._os_ops.write(hba_conf, new_lines)
|
|
828
|
+
postgres_conf = self._os_ops.build_path(self.data_dir, PG_CONF_FILE)
|
|
859
829
|
|
|
860
830
|
# overwrite config file
|
|
861
831
|
self._os_ops.write(postgres_conf, '', truncate=True)
|
|
@@ -901,6 +871,88 @@ class PostgresNode(object):
|
|
|
901
871
|
|
|
902
872
|
return self
|
|
903
873
|
|
|
874
|
+
def _default_conf__hba(self) -> None:
|
|
875
|
+
hba_conf = self._os_ops.build_path(self.data_dir, HBA_CONF_FILE)
|
|
876
|
+
|
|
877
|
+
# filter lines in hba file
|
|
878
|
+
# get rid of comments and blank lines
|
|
879
|
+
hba_conf_file = self._os_ops.readlines(hba_conf, binary=False)
|
|
880
|
+
|
|
881
|
+
assert type(hba_conf_file) is list
|
|
882
|
+
|
|
883
|
+
hba_conf_file_finished_with_eol = True
|
|
884
|
+
if len(hba_conf_file) > 0:
|
|
885
|
+
last_line = hba_conf_file[-1]
|
|
886
|
+
assert type(last_line) is str
|
|
887
|
+
hba_conf_file_finished_with_eol = last_line.endswith("\n")
|
|
888
|
+
|
|
889
|
+
# Normalize function: turns a string into a list of pure words
|
|
890
|
+
def normalize_line(line_str):
|
|
891
|
+
return line_str.strip().split()
|
|
892
|
+
|
|
893
|
+
# We collect a list of rules that already exist in the file (in the form of word lists)
|
|
894
|
+
existing_normalized = []
|
|
895
|
+
for s in hba_conf_file:
|
|
896
|
+
s_clean = s.strip()
|
|
897
|
+
if s_clean and not s_clean.startswith("#"):
|
|
898
|
+
existing_normalized.append(normalize_line(s_clean))
|
|
899
|
+
continue
|
|
900
|
+
|
|
901
|
+
# get auth method for host or local users
|
|
902
|
+
def get_auth_method(t):
|
|
903
|
+
for x in existing_normalized:
|
|
904
|
+
assert type(x) is list
|
|
905
|
+
if len(x) > 0 and x[0] == t:
|
|
906
|
+
return x[-1]
|
|
907
|
+
continue
|
|
908
|
+
return 'trust'
|
|
909
|
+
|
|
910
|
+
# get auth methods
|
|
911
|
+
auth_local = get_auth_method('local')
|
|
912
|
+
auth_host = get_auth_method('host')
|
|
913
|
+
|
|
914
|
+
# Basic rules that we want to see in the file
|
|
915
|
+
raw_rules = [
|
|
916
|
+
("local", "replication", "all", "", auth_local),
|
|
917
|
+
("host", "replication", "all", "0.0.0.0/0", auth_host),
|
|
918
|
+
("host", "replication", "all", "::/0", auth_host),
|
|
919
|
+
("local", "all", "all", "", auth_local),
|
|
920
|
+
("host", "all", "all", "0.0.0.0/0", auth_host),
|
|
921
|
+
("host", "all", "all", "::/0", auth_host),
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
add_rules = []
|
|
925
|
+
|
|
926
|
+
for type_hba, db, user, addr, method in raw_rules:
|
|
927
|
+
# We check if such a rule already exists in the file (by meaning, not by tabs!)
|
|
928
|
+
target_words = [type_hba, db, user, method]
|
|
929
|
+
if addr:
|
|
930
|
+
target_words.insert(3, addr)
|
|
931
|
+
|
|
932
|
+
if target_words in existing_normalized:
|
|
933
|
+
continue # Такое правило уже есть, пропускаем!
|
|
934
|
+
|
|
935
|
+
# Beautiful, smooth enterprise formatting with spaces!
|
|
936
|
+
# Text will be left-aligned and aligned strictly within columns.
|
|
937
|
+
formatted_rule = "{:<8} {:<16} {:<16} {:<24} {}\n".format(
|
|
938
|
+
type_hba, db, user, addr if addr else "", method
|
|
939
|
+
)
|
|
940
|
+
add_rules.append(formatted_rule)
|
|
941
|
+
continue
|
|
942
|
+
|
|
943
|
+
if len(add_rules) > 0:
|
|
944
|
+
add_lines = []
|
|
945
|
+
if not hba_conf_file_finished_with_eol:
|
|
946
|
+
add_lines.append("\n")
|
|
947
|
+
|
|
948
|
+
add_lines.append("\n")
|
|
949
|
+
add_lines.append("# Testgres default configuration\n")
|
|
950
|
+
add_lines += add_rules
|
|
951
|
+
|
|
952
|
+
# We add only real, beautifully formatted new items
|
|
953
|
+
self._os_ops.write(hba_conf, add_lines, truncate=False)
|
|
954
|
+
return
|
|
955
|
+
|
|
904
956
|
@method_decorator(positional_args_hack(['filename', 'line']))
|
|
905
957
|
def append_conf(self, line='', filename=PG_CONF_FILE, **kwargs):
|
|
906
958
|
"""
|
|
@@ -2318,9 +2370,11 @@ class PostgresNode(object):
|
|
|
2318
2370
|
class PostgresNodeLogReader:
|
|
2319
2371
|
class LogInfo:
|
|
2320
2372
|
position: int
|
|
2373
|
+
tail: bytes
|
|
2321
2374
|
|
|
2322
2375
|
def __init__(self, position: int):
|
|
2323
2376
|
self.position = position
|
|
2377
|
+
self.tail = b''
|
|
2324
2378
|
|
|
2325
2379
|
# --------------------------------------------------------------------
|
|
2326
2380
|
class LogDataBlock:
|
|
@@ -2396,38 +2450,63 @@ class PostgresNodeLogReader:
|
|
|
2396
2450
|
assert type(file_name) is str
|
|
2397
2451
|
assert type(cur_log_info) is __class__.LogInfo
|
|
2398
2452
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2453
|
+
if file_name not in self._logs.keys():
|
|
2454
|
+
read_pos = 0
|
|
2455
|
+
file_content_b = b''
|
|
2456
|
+
else:
|
|
2402
2457
|
prev_log_info = self._logs[file_name]
|
|
2403
2458
|
assert type(prev_log_info) is __class__.LogInfo
|
|
2404
2459
|
read_pos = prev_log_info.position # the previous size
|
|
2460
|
+
file_content_b = prev_log_info.tail
|
|
2405
2461
|
|
|
2406
|
-
|
|
2462
|
+
prev_data_sz = len(file_content_b)
|
|
2463
|
+
assert prev_data_sz <= read_pos
|
|
2464
|
+
|
|
2465
|
+
file_content_b += self._node.os_ops.read_binary(file_name, read_pos)
|
|
2407
2466
|
assert type(file_content_b) is bytes
|
|
2408
2467
|
|
|
2468
|
+
assert prev_data_sz <= len(file_content_b)
|
|
2469
|
+
|
|
2409
2470
|
#
|
|
2410
|
-
#
|
|
2471
|
+
# We will process completed lines only
|
|
2411
2472
|
#
|
|
2412
|
-
|
|
2413
|
-
assert type(file_content_s) is str
|
|
2473
|
+
completed_data_size = file_content_b.rfind(b"\n") + 1
|
|
2414
2474
|
|
|
2415
|
-
|
|
2475
|
+
assert completed_data_size >= 0
|
|
2476
|
+
assert completed_data_size <= len(file_content_b)
|
|
2416
2477
|
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
assert
|
|
2478
|
+
completed_data = file_content_b[:completed_data_size]
|
|
2479
|
+
assert type(completed_data) is bytes
|
|
2480
|
+
assert len(completed_data) == completed_data_size
|
|
2420
2481
|
|
|
2421
|
-
|
|
2482
|
+
new_tail = file_content_b[completed_data_size:]
|
|
2483
|
+
assert type(new_tail) is bytes
|
|
2484
|
+
assert len(new_tail) == len(file_content_b) - completed_data_size
|
|
2485
|
+
|
|
2486
|
+
completed_data_s = completed_data.decode()
|
|
2487
|
+
assert type(completed_data_s) is str
|
|
2488
|
+
|
|
2489
|
+
next_read_pos = read_pos - prev_data_sz + len(file_content_b)
|
|
2490
|
+
|
|
2491
|
+
assert read_pos <= next_read_pos
|
|
2492
|
+
|
|
2493
|
+
# It is a FINAL paranoja check.
|
|
2494
|
+
# [2026-07-10] Verified
|
|
2495
|
+
assert cur_log_info.position <= next_read_pos
|
|
2422
2496
|
|
|
2423
2497
|
block = __class__.LogDataBlock(
|
|
2424
2498
|
file_name,
|
|
2425
2499
|
read_pos,
|
|
2426
|
-
|
|
2500
|
+
completed_data_s,
|
|
2427
2501
|
)
|
|
2428
2502
|
|
|
2429
2503
|
result.append(block)
|
|
2430
2504
|
|
|
2505
|
+
# Save information to next iteration
|
|
2506
|
+
cur_log_info.position = next_read_pos
|
|
2507
|
+
cur_log_info.tail = new_tail
|
|
2508
|
+
continue
|
|
2509
|
+
|
|
2431
2510
|
# A new check point
|
|
2432
2511
|
self._logs = cur_logs
|
|
2433
2512
|
|
|
@@ -45,7 +45,7 @@ PostgresNode(name='...', port=..., base_dir='...')
|
|
|
45
45
|
from six import raise_from
|
|
46
46
|
|
|
47
47
|
from .consts import LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS
|
|
48
|
-
from .defaults import default_dbname,
|
|
48
|
+
from .defaults import default_dbname, default_username2
|
|
49
49
|
from .exceptions import CatchUpException
|
|
50
50
|
from .utils import options_string
|
|
51
51
|
|
|
@@ -63,23 +63,46 @@ class Publication(object):
|
|
|
63
63
|
dbname: database name used to connect and perform subscription.
|
|
64
64
|
username: username used to connect to the database.
|
|
65
65
|
"""
|
|
66
|
+
assert type(name) is str
|
|
67
|
+
assert node is not None
|
|
68
|
+
assert node.os_ops is not None
|
|
69
|
+
assert dbname is None or type(dbname) is str
|
|
70
|
+
assert username is None or type(username) is str
|
|
71
|
+
|
|
66
72
|
self.name = name
|
|
67
73
|
self.node = node
|
|
68
74
|
self.dbname = dbname or default_dbname()
|
|
69
|
-
self.username = username or
|
|
75
|
+
self.username = username or default_username2(node.os_ops)
|
|
70
76
|
|
|
71
77
|
# create publication in database
|
|
72
78
|
t = "table " + ", ".join(tables) if tables else "all tables"
|
|
73
79
|
query = "create publication {} for {}"
|
|
74
|
-
node.execute(
|
|
80
|
+
self.node.execute(
|
|
81
|
+
query.format(name, t),
|
|
82
|
+
dbname=self.dbname,
|
|
83
|
+
username=self.username,
|
|
84
|
+
)
|
|
75
85
|
|
|
76
86
|
def drop(self, dbname=None, username=None):
|
|
77
87
|
"""
|
|
78
88
|
Drop publication
|
|
79
89
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
assert dbname is None or type(dbname) is str
|
|
91
|
+
assert username is None or type(username) is str
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# [2026-07-10] [BUG FIX]
|
|
95
|
+
# dbname and username are ignored.
|
|
96
|
+
# We will use settings of our object.
|
|
97
|
+
#
|
|
98
|
+
assert dbname is None or dbname == self.dbname
|
|
99
|
+
assert username is None or username == self.username
|
|
100
|
+
|
|
101
|
+
self.node.execute(
|
|
102
|
+
"drop publication {}".format(self.name),
|
|
103
|
+
dbname=self.dbname,
|
|
104
|
+
username=self.username,
|
|
105
|
+
)
|
|
83
106
|
|
|
84
107
|
def add_tables(self, tables, dbname=None, username=None):
|
|
85
108
|
"""
|
|
@@ -89,13 +112,26 @@ class Publication(object):
|
|
|
89
112
|
Args:
|
|
90
113
|
tables: a list of tables to be added to the publication.
|
|
91
114
|
"""
|
|
115
|
+
assert dbname is None or type(dbname) is str
|
|
116
|
+
assert username is None or type(username) is str
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# [2026-07-10] [BUG FIX]
|
|
120
|
+
# dbname and username are ignored.
|
|
121
|
+
# We will use settings of our object.
|
|
122
|
+
#
|
|
123
|
+
assert dbname is None or dbname == self.dbname
|
|
124
|
+
assert username is None or username == self.username
|
|
125
|
+
|
|
92
126
|
if not tables:
|
|
93
127
|
raise ValueError("Tables list is empty")
|
|
94
128
|
|
|
95
129
|
query = "alter publication {} add table {}"
|
|
96
|
-
self.node.execute(
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
self.node.execute(
|
|
131
|
+
query.format(self.name, ", ".join(tables)),
|
|
132
|
+
dbname=self.dbname,
|
|
133
|
+
username=self.username,
|
|
134
|
+
)
|
|
99
135
|
|
|
100
136
|
|
|
101
137
|
class Subscription(object):
|
|
@@ -121,9 +157,17 @@ class Subscription(object):
|
|
|
121
157
|
<https://www.postgresql.org/docs/current/static/sql-createsubscription.html>`_
|
|
122
158
|
for details).
|
|
123
159
|
"""
|
|
160
|
+
assert type(name) is str
|
|
161
|
+
assert node is not None
|
|
162
|
+
assert node.os_ops is not None
|
|
163
|
+
assert dbname is None or type(dbname) is str
|
|
164
|
+
assert username is None or type(username) is str
|
|
165
|
+
|
|
124
166
|
self.name = name
|
|
125
167
|
self.node = node
|
|
126
168
|
self.pub = publication
|
|
169
|
+
self.dbname = dbname or default_dbname()
|
|
170
|
+
self.username = username or default_username2(node.os_ops)
|
|
127
171
|
|
|
128
172
|
# connection info
|
|
129
173
|
conninfo = {
|
|
@@ -142,38 +186,99 @@ class Subscription(object):
|
|
|
142
186
|
query += " with ({})".format(options_string(**params))
|
|
143
187
|
|
|
144
188
|
# Note: cannot run 'create subscription' query in transaction mode
|
|
145
|
-
node.execute(
|
|
189
|
+
self.node.execute(
|
|
190
|
+
query,
|
|
191
|
+
dbname=self.dbname,
|
|
192
|
+
username=self.username,
|
|
193
|
+
)
|
|
146
194
|
|
|
147
195
|
def disable(self, dbname=None, username=None):
|
|
148
196
|
"""
|
|
149
197
|
Disables the running subscription.
|
|
150
198
|
"""
|
|
199
|
+
assert dbname is None or type(dbname) is str
|
|
200
|
+
assert username is None or type(username) is str
|
|
201
|
+
|
|
202
|
+
#
|
|
203
|
+
# [2026-07-10] [BUG FIX]
|
|
204
|
+
# dbname and username are ignored.
|
|
205
|
+
# We will use settings of our object.
|
|
206
|
+
#
|
|
207
|
+
assert dbname is None or dbname == self.dbname
|
|
208
|
+
assert username is None or username == self.username
|
|
209
|
+
|
|
151
210
|
query = "alter subscription {} disable"
|
|
152
|
-
self.node.execute(
|
|
211
|
+
self.node.execute(
|
|
212
|
+
query.format(self.name),
|
|
213
|
+
dbname=self.dbname,
|
|
214
|
+
username=self.username,
|
|
215
|
+
)
|
|
153
216
|
|
|
154
217
|
def enable(self, dbname=None, username=None):
|
|
155
218
|
"""
|
|
156
219
|
Enables the previously disabled subscription.
|
|
157
220
|
"""
|
|
221
|
+
assert dbname is None or type(dbname) is str
|
|
222
|
+
assert username is None or type(username) is str
|
|
223
|
+
|
|
224
|
+
#
|
|
225
|
+
# [2026-07-10] [BUG FIX]
|
|
226
|
+
# dbname and username were and are ignored.
|
|
227
|
+
# We will use settings of our object.
|
|
228
|
+
#
|
|
229
|
+
assert dbname is None or dbname == self.dbname
|
|
230
|
+
assert username is None or username == self.username
|
|
231
|
+
|
|
158
232
|
query = "alter subscription {} enable"
|
|
159
|
-
|
|
233
|
+
|
|
234
|
+
self.node.execute(
|
|
235
|
+
query.format(self.name),
|
|
236
|
+
dbname=self.dbname,
|
|
237
|
+
username=self.username,
|
|
238
|
+
)
|
|
160
239
|
|
|
161
240
|
def refresh(self, copy_data=True, dbname=None, username=None):
|
|
162
241
|
"""
|
|
163
242
|
Disables the running subscription.
|
|
164
243
|
"""
|
|
244
|
+
assert dbname is None or type(dbname) is str
|
|
245
|
+
assert username is None or type(username) is str
|
|
246
|
+
|
|
247
|
+
#
|
|
248
|
+
# [2026-07-10] [BUG FIX]
|
|
249
|
+
# dbname and username are ignored.
|
|
250
|
+
# We will use settings of our object.
|
|
251
|
+
#
|
|
252
|
+
assert dbname is None or dbname == self.dbname
|
|
253
|
+
assert username is None or username == self.username
|
|
254
|
+
|
|
165
255
|
query = "alter subscription {} refresh publication with (copy_data={})"
|
|
166
|
-
self.node.execute(
|
|
167
|
-
|
|
168
|
-
|
|
256
|
+
self.node.execute(
|
|
257
|
+
query.format(self.name, copy_data),
|
|
258
|
+
dbname=self.dbname,
|
|
259
|
+
username=self.username,
|
|
260
|
+
)
|
|
169
261
|
|
|
170
262
|
def drop(self, dbname=None, username=None):
|
|
171
263
|
"""
|
|
172
264
|
Drops subscription
|
|
173
265
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
266
|
+
assert dbname is None or type(dbname) is str
|
|
267
|
+
assert username is None or type(username) is str
|
|
268
|
+
|
|
269
|
+
#
|
|
270
|
+
# [2026-07-10] [BUG FIX]
|
|
271
|
+
# dbname and username are ignored.
|
|
272
|
+
# We will use settings of our object.
|
|
273
|
+
#
|
|
274
|
+
assert dbname is None or dbname == self.dbname
|
|
275
|
+
assert username is None or username == self.username
|
|
276
|
+
|
|
277
|
+
self.node.execute(
|
|
278
|
+
"drop subscription {}".format(self.name),
|
|
279
|
+
dbname=self.dbname,
|
|
280
|
+
username=self.username,
|
|
281
|
+
)
|
|
177
282
|
|
|
178
283
|
def catchup(self, username=None):
|
|
179
284
|
"""
|
|
@@ -182,14 +287,32 @@ class Subscription(object):
|
|
|
182
287
|
Args:
|
|
183
288
|
username: remote node's user name.
|
|
184
289
|
"""
|
|
290
|
+
assert username is None or type(username) is str
|
|
291
|
+
|
|
292
|
+
#
|
|
293
|
+
# [2026-07-10] [BUG FIX]
|
|
294
|
+
# username is ignored.
|
|
295
|
+
# We will use settings of objects.
|
|
296
|
+
#
|
|
297
|
+
assert username is None or username == self.username
|
|
298
|
+
|
|
185
299
|
try:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
#
|
|
301
|
+
# [2026-07-10]
|
|
302
|
+
# About dbname=None and username=None
|
|
303
|
+
# We will try to use self.pub.xxx the next time. OK?
|
|
304
|
+
#
|
|
305
|
+
pub_lsn = self.pub.node.execute(
|
|
306
|
+
query="select pg_current_wal_lsn()",
|
|
307
|
+
dbname=None,
|
|
308
|
+
username=None,
|
|
309
|
+
)[0][0] # yapf: disable
|
|
189
310
|
# create dummy xact, as LR replicates only on commit.
|
|
190
|
-
self.pub.node.execute(
|
|
191
|
-
|
|
192
|
-
|
|
311
|
+
self.pub.node.execute(
|
|
312
|
+
query="select txid_current()",
|
|
313
|
+
dbname=None,
|
|
314
|
+
username=None,
|
|
315
|
+
)
|
|
193
316
|
query = """
|
|
194
317
|
select '{}'::pg_lsn - replay_lsn <= 0
|
|
195
318
|
from pg_catalog.pg_stat_replication where application_name = '{}'
|
|
@@ -199,8 +322,9 @@ class Subscription(object):
|
|
|
199
322
|
self.pub.node.poll_query_until(
|
|
200
323
|
query=query,
|
|
201
324
|
dbname=self.pub.dbname,
|
|
202
|
-
username=
|
|
203
|
-
max_attempts=LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS
|
|
325
|
+
username=self.pub.username,
|
|
326
|
+
max_attempts=LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS,
|
|
327
|
+
)
|
|
204
328
|
|
|
205
329
|
# Now, wait until there are no tablesync workers: probably
|
|
206
330
|
# replay_lsn above was sent with changes of new tables just skipped;
|
|
@@ -210,8 +334,9 @@ class Subscription(object):
|
|
|
210
334
|
"""
|
|
211
335
|
self.node.poll_query_until(
|
|
212
336
|
query=query,
|
|
213
|
-
dbname=self.
|
|
214
|
-
username=
|
|
215
|
-
max_attempts=LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS
|
|
337
|
+
dbname=self.dbname,
|
|
338
|
+
username=self.username,
|
|
339
|
+
max_attempts=LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS,
|
|
340
|
+
)
|
|
216
341
|
except Exception as e:
|
|
217
342
|
raise_from(CatchUpException("Failed to catch up"), e)
|
|
@@ -14,7 +14,7 @@ import typing
|
|
|
14
14
|
|
|
15
15
|
from six import iteritems
|
|
16
16
|
|
|
17
|
-
from .exceptions import ExecUtilException
|
|
17
|
+
from .exceptions import ExecUtilException, InvalidOperationException
|
|
18
18
|
from .config import testgres_config as tconf
|
|
19
19
|
from .raise_error import RaiseError
|
|
20
20
|
from .enums import NodeStatus
|
|
@@ -156,7 +156,7 @@ def get_bin_path2(os_ops: OsOperations, filename):
|
|
|
156
156
|
pg_config = os.environ.get("PG_CONFIG")
|
|
157
157
|
|
|
158
158
|
if pg_config:
|
|
159
|
-
bindir =
|
|
159
|
+
bindir = get_pg_config2(os_ops, pg_config)["BINDIR"]
|
|
160
160
|
return os_ops.build_path(bindir, filename)
|
|
161
161
|
|
|
162
162
|
# try PG_BIN
|
|
@@ -166,7 +166,7 @@ def get_bin_path2(os_ops: OsOperations, filename):
|
|
|
166
166
|
|
|
167
167
|
pg_config_path = os_ops.find_executable('pg_config')
|
|
168
168
|
if pg_config_path:
|
|
169
|
-
bindir =
|
|
169
|
+
bindir = get_pg_config2(os_ops, pg_config_path)["BINDIR"]
|
|
170
170
|
return os_ops.build_path(bindir, filename)
|
|
171
171
|
|
|
172
172
|
return filename
|
|
@@ -183,8 +183,7 @@ def get_bin_dir(os_ops: OsOperations) -> str:
|
|
|
183
183
|
pg_config = os.environ.get("PG_CONFIG")
|
|
184
184
|
|
|
185
185
|
if pg_config:
|
|
186
|
-
|
|
187
|
-
return bindir
|
|
186
|
+
return get_pg_config2(os_ops, pg_config)["BINDIR"]
|
|
188
187
|
|
|
189
188
|
# try PG_BIN
|
|
190
189
|
pg_bin = os_ops.environ("PG_BIN")
|
|
@@ -193,8 +192,11 @@ def get_bin_dir(os_ops: OsOperations) -> str:
|
|
|
193
192
|
|
|
194
193
|
pg_config_path = os_ops.find_executable('pg_config')
|
|
195
194
|
if pg_config_path:
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
return get_pg_config2(os_ops, pg_config_path)["BINDIR"]
|
|
196
|
+
|
|
197
|
+
postgres = os_ops.find_executable('postgres')
|
|
198
|
+
if postgres:
|
|
199
|
+
return os_ops.get_dirname(postgres)
|
|
198
200
|
|
|
199
201
|
raise RuntimeError("BinDir is not detected.")
|
|
200
202
|
|
|
@@ -261,7 +263,12 @@ def get_pg_config2(os_ops: OsOperations, pg_config_path):
|
|
|
261
263
|
return cache_pg_config_data(cmd)
|
|
262
264
|
|
|
263
265
|
# try plain name
|
|
264
|
-
|
|
266
|
+
try:
|
|
267
|
+
pg_config_data = cache_pg_config_data("pg_config")
|
|
268
|
+
except Exception:
|
|
269
|
+
raise InvalidOperationException(
|
|
270
|
+
"Failed to determine how to start pg_config. Either specify the path to pg_config in PG_CONFIG or specify the path to the Postgres directory containing pg_config in PG_BIN, or put pg_config into the system PATH.")
|
|
271
|
+
return pg_config_data
|
|
265
272
|
|
|
266
273
|
|
|
267
274
|
def get_pg_version2(os_ops: OsOperations, bin_dir=None):
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from .helpers.global_data import OsOpsDescrs
|
|
4
|
+
from .helpers.global_data import OsOpsDescr
|
|
3
5
|
from .helpers.global_data import PostgresNodeService
|
|
4
6
|
from .helpers.global_data import PostgresNodeServices
|
|
5
7
|
from .helpers.global_data import OsOperations
|
|
@@ -16,6 +18,9 @@ from src.utils import get_pg_version2
|
|
|
16
18
|
from src.utils import file_tail
|
|
17
19
|
from src.utils import get_bin_path2
|
|
18
20
|
from src.utils import execute_utility2
|
|
21
|
+
from src.defaults import default_username
|
|
22
|
+
from src.defaults import default_username2
|
|
23
|
+
from src.config import testgres_config as tconf
|
|
19
24
|
from src import ProcessType
|
|
20
25
|
from src import NodeStatus
|
|
21
26
|
from src import IsolationLevel
|
|
@@ -71,6 +76,25 @@ def removing(os_ops: OsOperations, f):
|
|
|
71
76
|
|
|
72
77
|
|
|
73
78
|
class TestTestgresCommon:
|
|
79
|
+
sm_os_ops_descrs: typing.List[OsOpsDescr] = [
|
|
80
|
+
OsOpsDescrs.sm_local_os_ops_descr,
|
|
81
|
+
OsOpsDescrs.sm_remote_os_ops_descr
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
@pytest.fixture(
|
|
85
|
+
params=[
|
|
86
|
+
pytest.param(
|
|
87
|
+
descr,
|
|
88
|
+
id=descr.sign,
|
|
89
|
+
)
|
|
90
|
+
for descr in sm_os_ops_descrs
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def os_ops_descr(self, request: pytest.FixtureRequest) -> OsOpsDescr:
|
|
94
|
+
assert isinstance(request, pytest.FixtureRequest)
|
|
95
|
+
assert isinstance(request.param, OsOpsDescr)
|
|
96
|
+
return request.param
|
|
97
|
+
|
|
74
98
|
sm_node_svcs: typing.List[PostgresNodeService] = [
|
|
75
99
|
PostgresNodeServices.sm_local,
|
|
76
100
|
PostgresNodeServices.sm_local2,
|
|
@@ -96,7 +120,7 @@ class TestTestgresCommon:
|
|
|
96
120
|
# Author: Mark G.
|
|
97
121
|
assert v.major == 1
|
|
98
122
|
assert v.minor == 14
|
|
99
|
-
assert v.micro ==
|
|
123
|
+
assert v.micro == 5
|
|
100
124
|
|
|
101
125
|
assert str(v) == testgres_version
|
|
102
126
|
return
|
|
@@ -133,6 +157,37 @@ class TestTestgresCommon:
|
|
|
133
157
|
assert (isinstance(node.version, PgVer))
|
|
134
158
|
assert (node.version == PgVer(version))
|
|
135
159
|
|
|
160
|
+
def test_default_username(
|
|
161
|
+
self,
|
|
162
|
+
os_ops_descr: OsOpsDescr,
|
|
163
|
+
):
|
|
164
|
+
assert type(os_ops_descr) is OsOpsDescr
|
|
165
|
+
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
166
|
+
|
|
167
|
+
os_ops = os_ops_descr.os_ops
|
|
168
|
+
assert isinstance(os_ops, OsOperations)
|
|
169
|
+
|
|
170
|
+
assert default_username(os_ops) == os_ops.get_user()
|
|
171
|
+
assert default_username(os_ops) == os_ops.username
|
|
172
|
+
|
|
173
|
+
assert default_username() == tconf.os_ops.username
|
|
174
|
+
assert default_username() == tconf.os_ops.get_user()
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def test_default_username2(
|
|
178
|
+
self,
|
|
179
|
+
os_ops_descr: OsOpsDescr,
|
|
180
|
+
):
|
|
181
|
+
assert type(os_ops_descr) is OsOpsDescr
|
|
182
|
+
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
183
|
+
|
|
184
|
+
os_ops = os_ops_descr.os_ops
|
|
185
|
+
assert isinstance(os_ops, OsOperations)
|
|
186
|
+
|
|
187
|
+
assert default_username2(os_ops) == os_ops.get_user()
|
|
188
|
+
assert default_username2(os_ops) == os_ops.username
|
|
189
|
+
return
|
|
190
|
+
|
|
136
191
|
def test_node_constructor__default(self):
|
|
137
192
|
node = PostgresNode()
|
|
138
193
|
assert node._os_ops is not None
|
|
@@ -190,6 +245,10 @@ class TestTestgresCommon:
|
|
|
190
245
|
with __class__.helper__get_node(node_svc) as node:
|
|
191
246
|
# enable page checksums
|
|
192
247
|
node.init(initdb_params=['-k']).start()
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
def test_custom_init__hba(self, node_svc: PostgresNodeService):
|
|
251
|
+
assert isinstance(node_svc, PostgresNodeService)
|
|
193
252
|
|
|
194
253
|
with __class__.helper__get_node(node_svc) as node:
|
|
195
254
|
node.init(
|
|
@@ -202,8 +261,24 @@ class TestTestgresCommon:
|
|
|
202
261
|
# check number of lines
|
|
203
262
|
assert (len(lines) >= 6)
|
|
204
263
|
|
|
264
|
+
# Normalize function: turns a string into a list of pure words
|
|
265
|
+
def normalize_line(line_str):
|
|
266
|
+
return line_str.strip().split()
|
|
267
|
+
|
|
268
|
+
# We collect a list of rules that already exist in the file (in the form of word lists)
|
|
269
|
+
existing_normalized = []
|
|
270
|
+
for s in lines:
|
|
271
|
+
s_clean = s.strip()
|
|
272
|
+
if s_clean and not s_clean.startswith("#"):
|
|
273
|
+
existing_normalized.append(normalize_line(s_clean))
|
|
274
|
+
continue
|
|
275
|
+
|
|
205
276
|
# there should be no trust entries at all
|
|
206
|
-
|
|
277
|
+
for s in lines:
|
|
278
|
+
if len(s) > 0 and s[0] in ["host", "local"]:
|
|
279
|
+
assert s[-1] == "reject"
|
|
280
|
+
continue
|
|
281
|
+
return
|
|
207
282
|
|
|
208
283
|
def test_double_init(self, node_svc: PostgresNodeService):
|
|
209
284
|
assert isinstance(node_svc, PostgresNodeService)
|
|
@@ -1783,9 +1858,9 @@ class TestTestgresCommon:
|
|
|
1783
1858
|
with removing(node_svc.os_ops, node1.dump(format=dump_fmt)) as dump:
|
|
1784
1859
|
with __class__.helper__get_node(node_svc).init().start() as node3:
|
|
1785
1860
|
if dump_fmt == enums.DumpFormat.Directory:
|
|
1786
|
-
assert (
|
|
1861
|
+
assert (node_svc.os_ops.isdir(dump))
|
|
1787
1862
|
else:
|
|
1788
|
-
assert (
|
|
1863
|
+
assert (node_svc.os_ops.isfile(dump))
|
|
1789
1864
|
# restore dump
|
|
1790
1865
|
node3.restore(filename=dump)
|
|
1791
1866
|
res = node3.execute(query_select)
|
|
@@ -1801,7 +1876,7 @@ class TestTestgresCommon:
|
|
|
1801
1876
|
# Test dump with --schema-only option
|
|
1802
1877
|
with removing(node_svc.os_ops, node1.dump(options=['--schema-only'])) as dump:
|
|
1803
1878
|
with __class__.helper__get_node(node_svc).init().start() as node2:
|
|
1804
|
-
assert (
|
|
1879
|
+
assert (node_svc.os_ops.isfile(dump))
|
|
1805
1880
|
# restore schema-only dump
|
|
1806
1881
|
node2.restore(filename=dump)
|
|
1807
1882
|
|
|
@@ -2680,6 +2755,7 @@ where c.relname=%s;"""
|
|
|
2680
2755
|
)
|
|
2681
2756
|
|
|
2682
2757
|
assert node_app.os_ops is node_svc.os_ops
|
|
2758
|
+
assert node_app.port_manager is not None
|
|
2683
2759
|
assert node_app.port_manager is node_svc.port_manager
|
|
2684
2760
|
assert type(node_app.nodes_to_cleanup) is list
|
|
2685
2761
|
assert len(node_app.nodes_to_cleanup) == 0
|
|
@@ -2753,6 +2829,7 @@ where c.relname=%s;"""
|
|
|
2753
2829
|
assert not node._should_free_port
|
|
2754
2830
|
break
|
|
2755
2831
|
finally:
|
|
2832
|
+
assert node_app.port_manager is not None
|
|
2756
2833
|
while len(ports) > 0:
|
|
2757
2834
|
node_app.port_manager.release_port(ports.pop())
|
|
2758
2835
|
|
|
@@ -2809,6 +2886,8 @@ where c.relname=%s;"""
|
|
|
2809
2886
|
|
|
2810
2887
|
logging.info("Node is started ...")
|
|
2811
2888
|
node.slow_start()
|
|
2889
|
+
|
|
2890
|
+
assert node.status() == NodeStatus.Running
|
|
2812
2891
|
return
|
|
2813
2892
|
|
|
2814
2893
|
@staticmethod
|
testgres-1.14.4/src/logger.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import select
|
|
5
|
-
import threading
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestgresLogger(threading.Thread):
|
|
10
|
-
"""
|
|
11
|
-
Helper class to implement reading from log files.
|
|
12
|
-
"""
|
|
13
|
-
def __init__(self, node_name, log_file_name):
|
|
14
|
-
threading.Thread.__init__(self)
|
|
15
|
-
|
|
16
|
-
self._node_name = node_name
|
|
17
|
-
self._log_file_name = log_file_name
|
|
18
|
-
self._stop_event = threading.Event()
|
|
19
|
-
self._logger = logging.getLogger(node_name)
|
|
20
|
-
self._logger.setLevel(logging.INFO)
|
|
21
|
-
|
|
22
|
-
def run(self):
|
|
23
|
-
# open log file for reading
|
|
24
|
-
with open(self._log_file_name, 'r') as fd:
|
|
25
|
-
# work until we're asked to stop
|
|
26
|
-
while not self._stop_event.is_set():
|
|
27
|
-
sleep_time = 0.1
|
|
28
|
-
new_lines = False
|
|
29
|
-
|
|
30
|
-
# do we have new lines?
|
|
31
|
-
if fd in select.select([fd], [], [], 0)[0]:
|
|
32
|
-
for line in fd.readlines():
|
|
33
|
-
line = line.strip()
|
|
34
|
-
if line:
|
|
35
|
-
new_lines = True
|
|
36
|
-
extra = {'node': self._node_name}
|
|
37
|
-
self._logger.info(line, extra=extra)
|
|
38
|
-
|
|
39
|
-
if not new_lines:
|
|
40
|
-
time.sleep(sleep_time)
|
|
41
|
-
|
|
42
|
-
# don't forget to clear event
|
|
43
|
-
self._stop_event.clear()
|
|
44
|
-
|
|
45
|
-
def stop(self, wait=True):
|
|
46
|
-
self._stop_event.set()
|
|
47
|
-
|
|
48
|
-
if wait:
|
|
49
|
-
self.join()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|