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.
Files changed (48) hide show
  1. {testgres-1.14.4/testgres.egg-info → testgres-1.14.5}/PKG-INFO +1 -1
  2. {testgres-1.14.4 → testgres-1.14.5}/src/__init__.py +3 -2
  3. {testgres-1.14.4 → testgres-1.14.5}/src/connection.py +2 -2
  4. {testgres-1.14.4 → testgres-1.14.5}/src/defaults.py +24 -2
  5. testgres-1.14.5/src/impl/file_line_reader.py +80 -0
  6. testgres-1.14.5/src/logger.py +116 -0
  7. {testgres-1.14.4 → testgres-1.14.5}/src/node.py +131 -52
  8. {testgres-1.14.4 → testgres-1.14.5}/src/pubsub.py +154 -29
  9. {testgres-1.14.4 → testgres-1.14.5}/src/utils.py +15 -8
  10. {testgres-1.14.4 → testgres-1.14.5/testgres.egg-info}/PKG-INFO +1 -1
  11. {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/SOURCES.txt +1 -0
  12. {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_common.py +84 -5
  13. testgres-1.14.4/src/logger.py +0 -49
  14. {testgres-1.14.4 → testgres-1.14.5}/LICENSE +0 -0
  15. {testgres-1.14.4 → testgres-1.14.5}/README.md +0 -0
  16. {testgres-1.14.4 → testgres-1.14.5}/pyproject.toml +0 -0
  17. {testgres-1.14.4 → testgres-1.14.5}/setup.cfg +0 -0
  18. {testgres-1.14.4 → testgres-1.14.5}/src/api.py +0 -0
  19. {testgres-1.14.4 → testgres-1.14.5}/src/backup.py +0 -0
  20. {testgres-1.14.4 → testgres-1.14.5}/src/cache.py +0 -0
  21. {testgres-1.14.4 → testgres-1.14.5}/src/config.py +0 -0
  22. {testgres-1.14.4 → testgres-1.14.5}/src/consts.py +0 -0
  23. {testgres-1.14.4 → testgres-1.14.5}/src/decorators.py +0 -0
  24. {testgres-1.14.4 → testgres-1.14.5}/src/enums.py +0 -0
  25. {testgres-1.14.4 → testgres-1.14.5}/src/exceptions.py +0 -0
  26. {testgres-1.14.4 → testgres-1.14.5}/src/impl/internal_utils.py +0 -0
  27. {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/internal_platform_utils.py +0 -0
  28. {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/internal_platform_utils_factory.py +0 -0
  29. {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/linux/internal_platform_utils.py +0 -0
  30. {testgres-1.14.4 → testgres-1.14.5}/src/impl/platforms/win32/internal_platform_utils.py +0 -0
  31. {testgres-1.14.4 → testgres-1.14.5}/src/impl/port_manager__generic.py +0 -0
  32. {testgres-1.14.4 → testgres-1.14.5}/src/impl/port_manager__this_host.py +0 -0
  33. {testgres-1.14.4 → testgres-1.14.5}/src/node_app.py +0 -0
  34. {testgres-1.14.4 → testgres-1.14.5}/src/port_manager.py +0 -0
  35. {testgres-1.14.4 → testgres-1.14.5}/src/raise_error.py +0 -0
  36. {testgres-1.14.4 → testgres-1.14.5}/src/standby.py +0 -0
  37. {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/dependency_links.txt +0 -0
  38. {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/requires.txt +0 -0
  39. {testgres-1.14.4 → testgres-1.14.5}/testgres.egg-info/top_level.txt +0 -0
  40. {testgres-1.14.4 → testgres-1.14.5}/tests/test_api.py +0 -0
  41. {testgres-1.14.4 → testgres-1.14.5}/tests/test_config.py +0 -0
  42. {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_common.py +0 -0
  43. {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_local.py +0 -0
  44. {testgres-1.14.4 → testgres-1.14.5}/tests/test_os_ops_remote.py +0 -0
  45. {testgres-1.14.4 → testgres-1.14.5}/tests/test_raise_error.py +0 -0
  46. {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_local.py +0 -0
  47. {testgres-1.14.4 → testgres-1.14.5}/tests/test_testgres_remote.py +0 -0
  48. {testgres-1.14.4 → testgres-1.14.5}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testgres
3
- Version: 1.14.4
3
+ Version: 1.14.5
4
4
  Summary: Testing utility for PostgreSQL and its extensions
5
5
  Author-email: Postgres Professional <testgres@postgrespro.ru>
6
6
  License: PostgreSQL
@@ -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.4"
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
- default_username
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 default_username()
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
- return tconf.os_ops.get_user()
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(self.name, self.pg_log_file)
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
- postgres_conf = self._os_ops.build_path(self.data_dir, PG_CONF_FILE)
822
- hba_conf = self._os_ops.build_path(self.data_dir, HBA_CONF_FILE)
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
- # replication-related settings
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
- read_pos = 0
2400
-
2401
- if file_name in self._logs.keys():
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
- file_content_b = self._node.os_ops.read_binary(file_name, read_pos)
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
- # A POTENTIAL PROBLEM: file_content_b may contain an incompleted UTF-8 symbol.
2471
+ # We will process completed lines only
2411
2472
  #
2412
- file_content_s = file_content_b.decode()
2413
- assert type(file_content_s) is str
2473
+ completed_data_size = file_content_b.rfind(b"\n") + 1
2414
2474
 
2415
- next_read_pos = read_pos + len(file_content_b)
2475
+ assert completed_data_size >= 0
2476
+ assert completed_data_size <= len(file_content_b)
2416
2477
 
2417
- # It is a research/paranoja check.
2418
- # When we will process partial UTF-8 symbol, it must be adjusted.
2419
- assert cur_log_info.position <= next_read_pos
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
- cur_log_info.position = next_read_pos
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
- file_content_s
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, default_username
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 default_username()
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(query.format(name, t), dbname=dbname, username=username)
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
- self.node.execute("drop publication {}".format(self.name),
81
- dbname=dbname,
82
- username=username)
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(query.format(self.name, ", ".join(tables)),
97
- dbname=dbname or self.dbname,
98
- username=username or self.username)
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(query, dbname=dbname, username=username)
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(query.format(self.name), dbname=None, username=None)
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
- self.node.execute(query.format(self.name), dbname=None, username=None)
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(query.format(self.name, copy_data),
167
- dbname=dbname,
168
- username=username)
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
- self.node.execute("drop subscription {}".format(self.name),
175
- dbname=dbname,
176
- username=username)
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
- pub_lsn = self.pub.node.execute(query="select pg_current_wal_lsn()",
187
- dbname=None,
188
- username=None)[0][0] # yapf: disable
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(query="select txid_current()",
191
- dbname=None,
192
- username=None)
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=username or self.pub.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.pub.dbname,
214
- username=username or self.pub.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 = get_pg_config(pg_config, os_ops)["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 = get_pg_config(pg_config_path)["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
- bindir = get_pg_config(pg_config, os_ops)["BINDIR"]
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
- bindir = get_pg_config(pg_config_path)["BINDIR"]
197
- return bindir
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
- return cache_pg_config_data("pg_config")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testgres
3
- Version: 1.14.4
3
+ Version: 1.14.5
4
4
  Summary: Testing utility for PostgreSQL and its extensions
5
5
  Author-email: Postgres Professional <testgres@postgrespro.ru>
6
6
  License: PostgreSQL
@@ -20,6 +20,7 @@ src/pubsub.py
20
20
  src/raise_error.py
21
21
  src/standby.py
22
22
  src/utils.py
23
+ src/impl/file_line_reader.py
23
24
  src/impl/internal_utils.py
24
25
  src/impl/port_manager__generic.py
25
26
  src/impl/port_manager__this_host.py
@@ -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 == 4
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
- assert not (any('trust' in s for s in lines))
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 (os.path.isdir(dump))
1861
+ assert (node_svc.os_ops.isdir(dump))
1787
1862
  else:
1788
- assert (os.path.isfile(dump))
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 (os.path.isfile(dump))
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
@@ -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