testgres 1.9.2__tar.gz → 1.9.3__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 (36) hide show
  1. {testgres-1.9.2/testgres.egg-info → testgres-1.9.3}/PKG-INFO +3 -3
  2. {testgres-1.9.2 → testgres-1.9.3}/setup.py +4 -4
  3. {testgres-1.9.2 → testgres-1.9.3}/testgres/__init__.py +3 -1
  4. testgres-1.9.3/testgres/helpers/port_manager.py +40 -0
  5. {testgres-1.9.2 → testgres-1.9.3}/testgres/node.py +7 -5
  6. testgres-1.9.3/testgres/operations/__init__.py +0 -0
  7. {testgres-1.9.2 → testgres-1.9.3}/testgres/operations/local_ops.py +75 -60
  8. {testgres-1.9.2 → testgres-1.9.3}/testgres/operations/os_ops.py +7 -1
  9. {testgres-1.9.2 → testgres-1.9.3}/testgres/operations/remote_ops.py +35 -36
  10. {testgres-1.9.2 → testgres-1.9.3}/testgres/utils.py +9 -4
  11. {testgres-1.9.2 → testgres-1.9.3/testgres.egg-info}/PKG-INFO +3 -3
  12. {testgres-1.9.2 → testgres-1.9.3}/testgres.egg-info/SOURCES.txt +2 -0
  13. {testgres-1.9.2 → testgres-1.9.3}/tests/test_remote.py +4 -5
  14. {testgres-1.9.2 → testgres-1.9.3}/tests/test_simple.py +35 -12
  15. {testgres-1.9.2 → testgres-1.9.3}/tests/test_simple_remote.py +3 -4
  16. {testgres-1.9.2 → testgres-1.9.3}/LICENSE +0 -0
  17. {testgres-1.9.2 → testgres-1.9.3}/MANIFEST.in +0 -0
  18. {testgres-1.9.2 → testgres-1.9.3}/README.md +0 -0
  19. {testgres-1.9.2 → testgres-1.9.3}/setup.cfg +0 -0
  20. {testgres-1.9.2 → testgres-1.9.3}/testgres/api.py +0 -0
  21. {testgres-1.9.2 → testgres-1.9.3}/testgres/backup.py +0 -0
  22. {testgres-1.9.2 → testgres-1.9.3}/testgres/cache.py +0 -0
  23. {testgres-1.9.2 → testgres-1.9.3}/testgres/config.py +0 -0
  24. {testgres-1.9.2 → testgres-1.9.3}/testgres/connection.py +0 -0
  25. {testgres-1.9.2 → testgres-1.9.3}/testgres/consts.py +0 -0
  26. {testgres-1.9.2 → testgres-1.9.3}/testgres/decorators.py +0 -0
  27. {testgres-1.9.2 → testgres-1.9.3}/testgres/defaults.py +0 -0
  28. {testgres-1.9.2 → testgres-1.9.3}/testgres/enums.py +0 -0
  29. {testgres-1.9.2 → testgres-1.9.3}/testgres/exceptions.py +0 -0
  30. {testgres-1.9.2/testgres/operations → testgres-1.9.3/testgres/helpers}/__init__.py +0 -0
  31. {testgres-1.9.2 → testgres-1.9.3}/testgres/logger.py +0 -0
  32. {testgres-1.9.2 → testgres-1.9.3}/testgres/pubsub.py +0 -0
  33. {testgres-1.9.2 → testgres-1.9.3}/testgres/standby.py +0 -0
  34. {testgres-1.9.2 → testgres-1.9.3}/testgres.egg-info/dependency_links.txt +0 -0
  35. {testgres-1.9.2 → testgres-1.9.3}/testgres.egg-info/requires.txt +0 -0
  36. {testgres-1.9.2 → testgres-1.9.3}/testgres.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: testgres
3
- Version: 1.9.2
3
+ Version: 1.9.3
4
4
  Summary: Testing utility for PostgreSQL and its extensions
5
5
  Home-page: https://github.com/postgrespro/testgres
6
- Author: Ildar Musin
7
- Author-email: zildermann@gmail.com
6
+ Author: Postgres Professional
7
+ Author-email: testgres@postgrespro.ru
8
8
  License: PostgreSQL
9
9
  Keywords: test,testing,postgresql
10
10
  Platform: UNKNOWN
@@ -27,16 +27,16 @@ with open('README.md', 'r') as f:
27
27
  readme = f.read()
28
28
 
29
29
  setup(
30
- version='1.9.2',
30
+ version='1.9.3',
31
31
  name='testgres',
32
- packages=['testgres', 'testgres.operations'],
32
+ packages=['testgres', 'testgres.operations', 'testgres.helpers'],
33
33
  description='Testing utility for PostgreSQL and its extensions',
34
34
  url='https://github.com/postgrespro/testgres',
35
35
  long_description=readme,
36
36
  long_description_content_type='text/markdown',
37
37
  license='PostgreSQL',
38
- author='Ildar Musin',
39
- author_email='zildermann@gmail.com',
38
+ author='Postgres Professional',
39
+ author_email='testgres@postgrespro.ru',
40
40
  keywords=['test', 'testing', 'postgresql'],
41
41
  install_requires=install_requires,
42
42
  classifiers=[],
@@ -52,6 +52,8 @@ from .operations.os_ops import OsOperations, ConnectionParams
52
52
  from .operations.local_ops import LocalOperations
53
53
  from .operations.remote_ops import RemoteOperations
54
54
 
55
+ from .helpers.port_manager import PortManager
56
+
55
57
  __all__ = [
56
58
  "get_new_node",
57
59
  "get_remote_node",
@@ -62,6 +64,6 @@ __all__ = [
62
64
  "XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
63
65
  "PostgresNode", "NodeApp",
64
66
  "reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
65
- "First", "Any",
67
+ "First", "Any", "PortManager",
66
68
  "OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
67
69
  ]
@@ -0,0 +1,40 @@
1
+ import socket
2
+ import random
3
+ from typing import Set, Iterable, Optional
4
+
5
+
6
+ class PortForException(Exception):
7
+ pass
8
+
9
+
10
+ class PortManager:
11
+ def __init__(self, ports_range=(1024, 65535)):
12
+ self.ports_range = ports_range
13
+
14
+ @staticmethod
15
+ def is_port_free(port: int) -> bool:
16
+ """Check if a port is free to use."""
17
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
18
+ try:
19
+ s.bind(("", port))
20
+ return True
21
+ except OSError:
22
+ return False
23
+
24
+ def find_free_port(self, ports: Optional[Set[int]] = None, exclude_ports: Optional[Iterable[int]] = None) -> int:
25
+ """Return a random unused port number."""
26
+ if ports is None:
27
+ ports = set(range(1024, 65535))
28
+
29
+ if exclude_ports is None:
30
+ exclude_ports = set()
31
+
32
+ ports.difference_update(set(exclude_ports))
33
+
34
+ sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
35
+
36
+ for port in sampled_ports:
37
+ if self.is_port_free(port):
38
+ return port
39
+
40
+ raise PortForException("Can't select a port")
@@ -623,8 +623,8 @@ class PostgresNode(object):
623
623
  "-D", self.data_dir,
624
624
  "status"
625
625
  ] # yapf: disable
626
- status_code, out, err = execute_utility(_params, self.utils_log_file, verbose=True)
627
- if 'does not exist' in err:
626
+ status_code, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
627
+ if error and 'does not exist' in error:
628
628
  return NodeStatus.Uninitialized
629
629
  elif 'no server running' in out:
630
630
  return NodeStatus.Stopped
@@ -659,7 +659,7 @@ class PostgresNode(object):
659
659
 
660
660
  return out_dict
661
661
 
662
- def slow_start(self, replica=False, dbname='template1', username=default_username(), max_attempts=0):
662
+ def slow_start(self, replica=False, dbname='template1', username=None, max_attempts=0):
663
663
  """
664
664
  Starts the PostgreSQL instance and then polls the instance
665
665
  until it reaches the expected state (primary or replica). The state is checked
@@ -672,6 +672,8 @@ class PostgresNode(object):
672
672
  If False, waits for the instance to be in primary mode. Default is False.
673
673
  max_attempts:
674
674
  """
675
+ if not username:
676
+ username = default_username()
675
677
  self.start()
676
678
 
677
679
  if replica:
@@ -715,7 +717,7 @@ class PostgresNode(object):
715
717
 
716
718
  try:
717
719
  exit_status, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
718
- if 'does not exist' in error:
720
+ if error and 'does not exist' in error:
719
721
  raise Exception
720
722
  except Exception as e:
721
723
  msg = 'Cannot start node'
@@ -789,7 +791,7 @@ class PostgresNode(object):
789
791
 
790
792
  try:
791
793
  error_code, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
792
- if 'could not start server' in error:
794
+ if error and 'could not start server' in error:
793
795
  raise ExecUtilException
794
796
  except ExecUtilException as e:
795
797
  msg = 'Cannot restart node'
File without changes
@@ -8,8 +8,7 @@ import tempfile
8
8
  import psutil
9
9
 
10
10
  from ..exceptions import ExecUtilException
11
- from .os_ops import ConnectionParams, OsOperations
12
- from .os_ops import pglib
11
+ from .os_ops import ConnectionParams, OsOperations, pglib, get_default_encoding
13
12
 
14
13
  try:
15
14
  from shutil import which as find_executable
@@ -22,6 +21,14 @@ CMD_TIMEOUT_SEC = 60
22
21
  error_markers = [b'error', b'Permission denied', b'fatal']
23
22
 
24
23
 
24
+ def has_errors(output):
25
+ if output:
26
+ if isinstance(output, str):
27
+ output = output.encode(get_default_encoding())
28
+ return any(marker in output for marker in error_markers)
29
+ return False
30
+
31
+
25
32
  class LocalOperations(OsOperations):
26
33
  def __init__(self, conn_params=None):
27
34
  if conn_params is None:
@@ -33,72 +40,80 @@ class LocalOperations(OsOperations):
33
40
  self.remote = False
34
41
  self.username = conn_params.username or self.get_user()
35
42
 
36
- # Command execution
37
- def exec_command(self, cmd, wait_exit=False, verbose=False,
38
- expect_error=False, encoding=None, shell=False, text=False,
39
- input=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
40
- get_process=None, timeout=None):
41
- """
42
- Execute a command in a subprocess.
43
-
44
- Args:
45
- - cmd: The command to execute.
46
- - wait_exit: Whether to wait for the subprocess to exit before returning.
47
- - verbose: Whether to return verbose output.
48
- - expect_error: Whether to raise an error if the subprocess exits with an error status.
49
- - encoding: The encoding to use for decoding the subprocess output.
50
- - shell: Whether to use shell when executing the subprocess.
51
- - text: Whether to return str instead of bytes for the subprocess output.
52
- - input: The input to pass to the subprocess.
53
- - stdout: The stdout to use for the subprocess.
54
- - stderr: The stderr to use for the subprocess.
55
- - proc: The process to use for subprocess creation.
56
- :return: The output of the subprocess.
57
- """
58
- if os.name == 'nt':
59
- with tempfile.NamedTemporaryFile() as buf:
60
- process = subprocess.Popen(cmd, stdout=buf, stderr=subprocess.STDOUT)
61
- process.communicate()
62
- buf.seek(0)
63
- result = buf.read().decode(encoding)
64
- return result
65
- else:
43
+ @staticmethod
44
+ def _raise_exec_exception(message, command, exit_code, output):
45
+ """Raise an ExecUtilException."""
46
+ raise ExecUtilException(message=message.format(output),
47
+ command=command,
48
+ exit_code=exit_code,
49
+ out=output)
50
+
51
+ @staticmethod
52
+ def _process_output(encoding, temp_file_path):
53
+ """Process the output of a command from a temporary file."""
54
+ with open(temp_file_path, 'rb') as temp_file:
55
+ output = temp_file.read()
56
+ if encoding:
57
+ output = output.decode(encoding)
58
+ return output, None # In Windows stderr writing in stdout
59
+
60
+ def _run_command(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
61
+ """Execute a command and return the process and its output."""
62
+ if os.name == 'nt' and stdout is None: # Windows
63
+ with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
64
+ stdout = temp_file
65
+ stderr = subprocess.STDOUT
66
+ process = subprocess.Popen(
67
+ cmd,
68
+ shell=shell,
69
+ stdin=stdin or subprocess.PIPE if input is not None else None,
70
+ stdout=stdout,
71
+ stderr=stderr,
72
+ )
73
+ if get_process:
74
+ return process, None, None
75
+ temp_file_path = temp_file.name
76
+
77
+ # Wait process finished
78
+ process.wait()
79
+
80
+ output, error = self._process_output(encoding, temp_file_path)
81
+ return process, output, error
82
+ else: # Other OS
66
83
  process = subprocess.Popen(
67
84
  cmd,
68
85
  shell=shell,
69
- stdout=stdout,
70
- stderr=stderr,
86
+ stdin=stdin or subprocess.PIPE if input is not None else None,
87
+ stdout=stdout or subprocess.PIPE,
88
+ stderr=stderr or subprocess.PIPE,
71
89
  )
72
90
  if get_process:
73
- return process
74
-
91
+ return process, None, None
75
92
  try:
76
- result, error = process.communicate(input, timeout=timeout)
93
+ output, error = process.communicate(input=input.encode(encoding) if input else None, timeout=timeout)
94
+ if encoding:
95
+ output = output.decode(encoding)
96
+ error = error.decode(encoding)
97
+ return process, output, error
77
98
  except subprocess.TimeoutExpired:
78
99
  process.kill()
79
100
  raise ExecUtilException("Command timed out after {} seconds.".format(timeout))
80
- exit_status = process.returncode
81
-
82
- error_found = exit_status != 0 or any(marker in error for marker in error_markers)
83
101
 
84
- if encoding:
85
- result = result.decode(encoding)
86
- error = error.decode(encoding)
87
-
88
- if expect_error:
89
- raise Exception(result, error)
90
-
91
- if exit_status != 0 or error_found:
92
- if exit_status == 0:
93
- exit_status = 1
94
- raise ExecUtilException(message='Utility exited with non-zero code. Error `{}`'.format(error),
95
- command=cmd,
96
- exit_code=exit_status,
97
- out=result)
98
- if verbose:
99
- return exit_status, result, error
100
- else:
101
- return result
102
+ def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False, encoding=None, shell=False,
103
+ text=False, input=None, stdin=None, stdout=None, stderr=None, get_process=False, timeout=None):
104
+ """
105
+ Execute a command in a subprocess and handle the output based on the provided parameters.
106
+ """
107
+ process, output, error = self._run_command(cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding)
108
+ if get_process:
109
+ return process
110
+ if process.returncode != 0 or (has_errors(error) and not expect_error):
111
+ self._raise_exec_exception('Utility exited with non-zero code. Error `{}`', cmd, process.returncode, error)
112
+
113
+ if verbose:
114
+ return process.returncode, output, error
115
+ else:
116
+ return output
102
117
 
103
118
  # Environment setup
104
119
  def environ(self, var_name):
@@ -117,7 +132,7 @@ class LocalOperations(OsOperations):
117
132
 
118
133
  # Get environment variables
119
134
  def get_user(self):
120
- return getpass.getuser()
135
+ return self.username or getpass.getuser()
121
136
 
122
137
  def get_name(self):
123
138
  return os.name
@@ -210,7 +225,7 @@ class LocalOperations(OsOperations):
210
225
  if binary:
211
226
  return content
212
227
  if isinstance(content, bytes):
213
- return content.decode(encoding or 'utf-8')
228
+ return content.decode(encoding or get_default_encoding())
214
229
  return content
215
230
 
216
231
  def readlines(self, filename, num_lines=0, binary=False, encoding=None):
@@ -1,3 +1,5 @@
1
+ import locale
2
+
1
3
  try:
2
4
  import psycopg2 as pglib # noqa: F401
3
5
  except ImportError:
@@ -14,6 +16,10 @@ class ConnectionParams:
14
16
  self.username = username
15
17
 
16
18
 
19
+ def get_default_encoding():
20
+ return locale.getdefaultlocale()[1] or 'UTF-8'
21
+
22
+
17
23
  class OsOperations:
18
24
  def __init__(self, username=None):
19
25
  self.ssh_key = None
@@ -75,7 +81,7 @@ class OsOperations:
75
81
  def touch(self, filename):
76
82
  raise NotImplementedError()
77
83
 
78
- def read(self, filename):
84
+ def read(self, filename, encoding, binary):
79
85
  raise NotImplementedError()
80
86
 
81
87
  def readlines(self, filename):
@@ -1,8 +1,8 @@
1
- import locale
2
1
  import logging
3
2
  import os
4
3
  import subprocess
5
4
  import tempfile
5
+ import platform
6
6
 
7
7
  # we support both pg8000 and psycopg2
8
8
  try:
@@ -14,12 +14,7 @@ except ImportError:
14
14
  raise ImportError("You must have psycopg2 or pg8000 modules installed")
15
15
 
16
16
  from ..exceptions import ExecUtilException
17
-
18
- from .os_ops import OsOperations, ConnectionParams
19
-
20
- ConsoleEncoding = locale.getdefaultlocale()[1]
21
- if not ConsoleEncoding:
22
- ConsoleEncoding = 'UTF-8'
17
+ from .os_ops import OsOperations, ConnectionParams, get_default_encoding
23
18
 
24
19
  error_markers = [b'error', b'Permission denied', b'fatal', b'No such file or directory']
25
20
 
@@ -35,20 +30,25 @@ class PsUtilProcessProxy:
35
30
 
36
31
  def cmdline(self):
37
32
  command = "ps -p {} -o cmd --no-headers".format(self.pid)
38
- stdin, stdout, stderr = self.ssh.exec_command(command, verbose=True, encoding=ConsoleEncoding)
33
+ stdin, stdout, stderr = self.ssh.exec_command(command, verbose=True, encoding=get_default_encoding())
39
34
  cmdline = stdout.strip()
40
35
  return cmdline.split()
41
36
 
42
37
 
43
38
  class RemoteOperations(OsOperations):
44
39
  def __init__(self, conn_params: ConnectionParams):
45
- if os.name != "posix":
40
+
41
+ if not platform.system().lower() == "linux":
46
42
  raise EnvironmentError("Remote operations are supported only on Linux!")
47
43
 
48
44
  super().__init__(conn_params.username)
49
45
  self.conn_params = conn_params
50
46
  self.host = conn_params.host
51
47
  self.ssh_key = conn_params.ssh_key
48
+ if self.ssh_key:
49
+ self.ssh_cmd = ["-i", self.ssh_key]
50
+ else:
51
+ self.ssh_cmd = []
52
52
  self.remote = True
53
53
  self.username = conn_params.username or self.get_user()
54
54
  self.add_known_host(self.host)
@@ -76,16 +76,14 @@ class RemoteOperations(OsOperations):
76
76
  print("No active tunnel to close.")
77
77
 
78
78
  def add_known_host(self, host):
79
- cmd = 'ssh-keyscan -H %s >> /home/%s/.ssh/known_hosts' % (host, os.getlogin())
79
+ known_hosts_path = os.path.expanduser("~/.ssh/known_hosts")
80
+ cmd = 'ssh-keyscan -H %s >> %s' % (host, known_hosts_path)
81
+
80
82
  try:
81
- subprocess.check_call(
82
- cmd,
83
- shell=True,
84
- )
83
+ subprocess.check_call(cmd, shell=True)
85
84
  logging.info("Successfully added %s to known_hosts." % host)
86
85
  except subprocess.CalledProcessError as e:
87
- raise ExecUtilException(message="Failed to add %s to known_hosts. Error: %s" % (host, str(e)), command=cmd,
88
- exit_code=e.returncode, out=e.stderr)
86
+ raise Exception("Failed to add %s to known_hosts. Error: %s" % (host, str(e)))
89
87
 
90
88
  def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False,
91
89
  encoding=None, shell=True, text=False, input=None, stdin=None, stdout=None,
@@ -97,9 +95,9 @@ class RemoteOperations(OsOperations):
97
95
  """
98
96
  ssh_cmd = []
99
97
  if isinstance(cmd, str):
100
- ssh_cmd = ['ssh', f"{self.username}@{self.host}", '-i', self.ssh_key, cmd]
98
+ ssh_cmd = ['ssh', f"{self.username}@{self.host}"] + self.ssh_cmd + [cmd]
101
99
  elif isinstance(cmd, list):
102
- ssh_cmd = ['ssh', f"{self.username}@{self.host}", '-i', self.ssh_key] + cmd
100
+ ssh_cmd = ['ssh', f"{self.username}@{self.host}"] + self.ssh_cmd + cmd
103
101
  process = subprocess.Popen(ssh_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
104
102
  if get_process:
105
103
  return process
@@ -145,7 +143,7 @@ class RemoteOperations(OsOperations):
145
143
  - var_name (str): The name of the environment variable.
146
144
  """
147
145
  cmd = "echo ${}".format(var_name)
148
- return self.exec_command(cmd, encoding=ConsoleEncoding).strip()
146
+ return self.exec_command(cmd, encoding=get_default_encoding()).strip()
149
147
 
150
148
  def find_executable(self, executable):
151
149
  search_paths = self.environ("PATH")
@@ -176,11 +174,11 @@ class RemoteOperations(OsOperations):
176
174
 
177
175
  # Get environment variables
178
176
  def get_user(self):
179
- return self.exec_command("echo $USER", encoding=ConsoleEncoding).strip()
177
+ return self.exec_command("echo $USER", encoding=get_default_encoding()).strip()
180
178
 
181
179
  def get_name(self):
182
180
  cmd = 'python3 -c "import os; print(os.name)"'
183
- return self.exec_command(cmd, encoding=ConsoleEncoding).strip()
181
+ return self.exec_command(cmd, encoding=get_default_encoding()).strip()
184
182
 
185
183
  # Work with dirs
186
184
  def makedirs(self, path, remove_existing=False):
@@ -227,7 +225,7 @@ class RemoteOperations(OsOperations):
227
225
  return result.splitlines()
228
226
 
229
227
  def path_exists(self, path):
230
- result = self.exec_command("test -e {}; echo $?".format(path), encoding=ConsoleEncoding)
228
+ result = self.exec_command("test -e {}; echo $?".format(path), encoding=get_default_encoding())
231
229
  return int(result.strip()) == 0
232
230
 
233
231
  @property
@@ -248,9 +246,9 @@ class RemoteOperations(OsOperations):
248
246
  - prefix (str): The prefix of the temporary directory name.
249
247
  """
250
248
  if prefix:
251
- command = ["ssh", "-i", self.ssh_key, f"{self.username}@{self.host}", f"mktemp -d {prefix}XXXXX"]
249
+ command = ["ssh"] + self.ssh_cmd + [f"{self.username}@{self.host}", f"mktemp -d {prefix}XXXXX"]
252
250
  else:
253
- command = ["ssh", "-i", self.ssh_key, f"{self.username}@{self.host}", "mktemp -d"]
251
+ command = ["ssh"] + self.ssh_cmd + [f"{self.username}@{self.host}", "mktemp -d"]
254
252
 
255
253
  result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
256
254
 
@@ -264,9 +262,9 @@ class RemoteOperations(OsOperations):
264
262
 
265
263
  def mkstemp(self, prefix=None):
266
264
  if prefix:
267
- temp_dir = self.exec_command("mktemp {}XXXXX".format(prefix), encoding=ConsoleEncoding)
265
+ temp_dir = self.exec_command("mktemp {}XXXXX".format(prefix), encoding=get_default_encoding())
268
266
  else:
269
- temp_dir = self.exec_command("mktemp", encoding=ConsoleEncoding)
267
+ temp_dir = self.exec_command("mktemp", encoding=get_default_encoding())
270
268
 
271
269
  if temp_dir:
272
270
  if not os.path.isabs(temp_dir):
@@ -283,7 +281,9 @@ class RemoteOperations(OsOperations):
283
281
  return self.exec_command("cp -r {} {}".format(src, dst))
284
282
 
285
283
  # Work with files
286
- def write(self, filename, data, truncate=False, binary=False, read_and_write=False, encoding=ConsoleEncoding):
284
+ def write(self, filename, data, truncate=False, binary=False, read_and_write=False, encoding=None):
285
+ if not encoding:
286
+ encoding = get_default_encoding()
287
287
  mode = "wb" if binary else "w"
288
288
  if not truncate:
289
289
  mode = "ab" if binary else "a"
@@ -292,7 +292,7 @@ class RemoteOperations(OsOperations):
292
292
 
293
293
  with tempfile.NamedTemporaryFile(mode=mode, delete=False) as tmp_file:
294
294
  if not truncate:
295
- scp_cmd = ['scp', '-i', self.ssh_key, f"{self.username}@{self.host}:{filename}", tmp_file.name]
295
+ scp_cmd = ['scp'] + self.ssh_cmd + [f"{self.username}@{self.host}:{filename}", tmp_file.name]
296
296
  subprocess.run(scp_cmd, check=False) # The file might not exist yet
297
297
  tmp_file.seek(0, os.SEEK_END)
298
298
 
@@ -302,18 +302,17 @@ class RemoteOperations(OsOperations):
302
302
  data = data.encode(encoding)
303
303
 
304
304
  if isinstance(data, list):
305
- data = [(s if isinstance(s, str) else s.decode(ConsoleEncoding)).rstrip('\n') + '\n' for s in data]
305
+ data = [(s if isinstance(s, str) else s.decode(get_default_encoding())).rstrip('\n') + '\n' for s in data]
306
306
  tmp_file.writelines(data)
307
307
  else:
308
308
  tmp_file.write(data)
309
309
 
310
310
  tmp_file.flush()
311
-
312
- scp_cmd = ['scp', '-i', self.ssh_key, tmp_file.name, f"{self.username}@{self.host}:{filename}"]
311
+ scp_cmd = ['scp'] + self.ssh_cmd + [tmp_file.name, f"{self.username}@{self.host}:{filename}"]
313
312
  subprocess.run(scp_cmd, check=True)
314
313
 
315
314
  remote_directory = os.path.dirname(filename)
316
- mkdir_cmd = ['ssh', '-i', self.ssh_key, f"{self.username}@{self.host}", f"mkdir -p {remote_directory}"]
315
+ mkdir_cmd = ['ssh'] + self.ssh_cmd + [f"{self.username}@{self.host}", f"mkdir -p {remote_directory}"]
317
316
  subprocess.run(mkdir_cmd, check=True)
318
317
 
319
318
  os.remove(tmp_file.name)
@@ -334,7 +333,7 @@ class RemoteOperations(OsOperations):
334
333
  result = self.exec_command(cmd, encoding=encoding)
335
334
 
336
335
  if not binary and result:
337
- result = result.decode(encoding or ConsoleEncoding)
336
+ result = result.decode(encoding or get_default_encoding())
338
337
 
339
338
  return result
340
339
 
@@ -347,7 +346,7 @@ class RemoteOperations(OsOperations):
347
346
  result = self.exec_command(cmd, encoding=encoding)
348
347
 
349
348
  if not binary and result:
350
- lines = result.decode(encoding or ConsoleEncoding).splitlines()
349
+ lines = result.decode(encoding or get_default_encoding()).splitlines()
351
350
  else:
352
351
  lines = result.splitlines()
353
352
 
@@ -375,10 +374,10 @@ class RemoteOperations(OsOperations):
375
374
 
376
375
  def get_pid(self):
377
376
  # Get current process id
378
- return int(self.exec_command("echo $$", encoding=ConsoleEncoding))
377
+ return int(self.exec_command("echo $$", encoding=get_default_encoding()))
379
378
 
380
379
  def get_process_children(self, pid):
381
- command = ["ssh", "-i", self.ssh_key, f"{self.username}@{self.host}", f"pgrep -P {pid}"]
380
+ command = ["ssh"] + self.ssh_cmd + [f"{self.username}@{self.host}", f"pgrep -P {pid}"]
382
381
 
383
382
  result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
384
383
 
@@ -4,7 +4,7 @@ from __future__ import division
4
4
  from __future__ import print_function
5
5
 
6
6
  import os
7
- import port_for
7
+
8
8
  import sys
9
9
 
10
10
  from contextlib import contextmanager
@@ -13,6 +13,7 @@ import re
13
13
 
14
14
  from six import iteritems
15
15
 
16
+ from .helpers.port_manager import PortManager
16
17
  from .exceptions import ExecUtilException
17
18
  from .config import testgres_config as tconf
18
19
 
@@ -37,8 +38,8 @@ def reserve_port():
37
38
  """
38
39
  Generate a new port and add it to 'bound_ports'.
39
40
  """
40
-
41
- port = port_for.select_random(exclude_ports=bound_ports)
41
+ port_mng = PortManager()
42
+ port = port_mng.find_free_port(exclude_ports=bound_ports)
42
43
  bound_ports.add(port)
43
44
 
44
45
  return port
@@ -80,7 +81,8 @@ def execute_utility(args, logfile=None, verbose=False):
80
81
  lines = [u'\n'] + ['# ' + line for line in out.splitlines()] + [u'\n']
81
82
  tconf.os_ops.write(filename=logfile, data=lines)
82
83
  except IOError:
83
- raise ExecUtilException("Problem with writing to logfile `{}` during run command `{}`".format(logfile, args))
84
+ raise ExecUtilException(
85
+ "Problem with writing to logfile `{}` during run command `{}`".format(logfile, args))
84
86
  if verbose:
85
87
  return exit_status, out, error
86
88
  else:
@@ -179,6 +181,9 @@ def get_pg_version():
179
181
  _params = [get_bin_path('postgres'), '--version']
180
182
  raw_ver = tconf.os_ops.exec_command(_params, encoding='utf-8')
181
183
 
184
+ # Remove "(Homebrew)" if present
185
+ raw_ver = raw_ver.replace('(Homebrew)', '').strip()
186
+
182
187
  # cook version of PostgreSQL
183
188
  version = raw_ver.strip().split(' ')[-1] \
184
189
  .partition('devel')[0] \
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: testgres
3
- Version: 1.9.2
3
+ Version: 1.9.3
4
4
  Summary: Testing utility for PostgreSQL and its extensions
5
5
  Home-page: https://github.com/postgrespro/testgres
6
- Author: Ildar Musin
7
- Author-email: zildermann@gmail.com
6
+ Author: Postgres Professional
7
+ Author-email: testgres@postgrespro.ru
8
8
  License: PostgreSQL
9
9
  Keywords: test,testing,postgresql
10
10
  Platform: UNKNOWN
@@ -24,6 +24,8 @@ testgres.egg-info/SOURCES.txt
24
24
  testgres.egg-info/dependency_links.txt
25
25
  testgres.egg-info/requires.txt
26
26
  testgres.egg-info/top_level.txt
27
+ testgres/helpers/__init__.py
28
+ testgres/helpers/port_manager.py
27
29
  testgres/operations/__init__.py
28
30
  testgres/operations/local_ops.py
29
31
  testgres/operations/os_ops.py
@@ -11,10 +11,9 @@ class TestRemoteOperations:
11
11
 
12
12
  @pytest.fixture(scope="function", autouse=True)
13
13
  def setup(self):
14
- conn_params = ConnectionParams(host=os.getenv('RDBMS_TESTPOOL1_HOST') or '172.18.0.3',
15
- username='dev',
16
- ssh_key=os.getenv(
17
- 'RDBMS_TESTPOOL_SSHKEY') or '../../container_files/postgres/ssh/id_ed25519')
14
+ conn_params = ConnectionParams(host=os.getenv('RDBMS_TESTPOOL1_HOST') or '127.0.0.1',
15
+ username=os.getenv('USER'),
16
+ ssh_key=os.getenv('RDBMS_TESTPOOL_SSHKEY'))
18
17
  self.operations = RemoteOperations(conn_params)
19
18
 
20
19
  def test_exec_command_success(self):
@@ -41,7 +40,7 @@ class TestRemoteOperations:
41
40
  """
42
41
  Test is_executable for an existing executable.
43
42
  """
44
- cmd = "postgres"
43
+ cmd = os.getenv('PG_CONFIG')
45
44
  response = self.operations.is_executable(cmd)
46
45
 
47
46
  assert response is True
@@ -74,6 +74,24 @@ def util_exists(util):
74
74
  return True
75
75
 
76
76
 
77
+ def rm_carriage_returns(out):
78
+ """
79
+ In Windows we have additional '\r' symbols in output.
80
+ Let's get rid of them.
81
+ """
82
+ if os.name == 'nt':
83
+ if isinstance(out, (int, float, complex)):
84
+ return out
85
+ elif isinstance(out, tuple):
86
+ return tuple(rm_carriage_returns(item) for item in out)
87
+ elif isinstance(out, bytes):
88
+ return out.replace(b'\r', b'')
89
+ else:
90
+ return out.replace('\r', '')
91
+ else:
92
+ return out
93
+
94
+
77
95
  @contextmanager
78
96
  def removing(f):
79
97
  try:
@@ -123,7 +141,7 @@ class TestgresTests(unittest.TestCase):
123
141
  node.cleanup()
124
142
  node.init().start().execute('select 1')
125
143
 
126
- @unittest.skipUnless(util_exists('pg_resetwal'), 'might be missing')
144
+ @unittest.skipUnless(util_exists('pg_resetwal.exe' if os.name == 'nt' else 'pg_resetwal'), 'pgbench might be missing')
127
145
  @unittest.skipUnless(pg_version_ge('9.6'), 'requires 9.6+')
128
146
  def test_init_unique_system_id(self):
129
147
  # this function exists in PostgreSQL 9.6+
@@ -254,34 +272,34 @@ class TestgresTests(unittest.TestCase):
254
272
 
255
273
  # check returned values (1 arg)
256
274
  res = node.psql('select 1')
257
- self.assertEqual(res, (0, b'1\n', b''))
275
+ self.assertEqual(rm_carriage_returns(res), (0, b'1\n', b''))
258
276
 
259
277
  # check returned values (2 args)
260
278
  res = node.psql('postgres', 'select 2')
261
- self.assertEqual(res, (0, b'2\n', b''))
279
+ self.assertEqual(rm_carriage_returns(res), (0, b'2\n', b''))
262
280
 
263
281
  # check returned values (named)
264
282
  res = node.psql(query='select 3', dbname='postgres')
265
- self.assertEqual(res, (0, b'3\n', b''))
283
+ self.assertEqual(rm_carriage_returns(res), (0, b'3\n', b''))
266
284
 
267
285
  # check returned values (1 arg)
268
286
  res = node.safe_psql('select 4')
269
- self.assertEqual(res, b'4\n')
287
+ self.assertEqual(rm_carriage_returns(res), b'4\n')
270
288
 
271
289
  # check returned values (2 args)
272
290
  res = node.safe_psql('postgres', 'select 5')
273
- self.assertEqual(res, b'5\n')
291
+ self.assertEqual(rm_carriage_returns(res), b'5\n')
274
292
 
275
293
  # check returned values (named)
276
294
  res = node.safe_psql(query='select 6', dbname='postgres')
277
- self.assertEqual(res, b'6\n')
295
+ self.assertEqual(rm_carriage_returns(res), b'6\n')
278
296
 
279
297
  # check feeding input
280
298
  node.safe_psql('create table horns (w int)')
281
299
  node.safe_psql('copy horns from stdin (format csv)',
282
300
  input=b"1\n2\n3\n\\.\n")
283
301
  _sum = node.safe_psql('select sum(w) from horns')
284
- self.assertEqual(_sum, b'6\n')
302
+ self.assertEqual(rm_carriage_returns(_sum), b'6\n')
285
303
 
286
304
  # check psql's default args, fails
287
305
  with self.assertRaises(QueryException):
@@ -455,7 +473,7 @@ class TestgresTests(unittest.TestCase):
455
473
  master.safe_psql(
456
474
  'insert into abc select generate_series(1, 1000000)')
457
475
  res = standby1.safe_psql('select count(*) from abc')
458
- self.assertEqual(res, b'1000000\n')
476
+ self.assertEqual(rm_carriage_returns(res), b'1000000\n')
459
477
 
460
478
  @unittest.skipUnless(pg_version_ge('10'), 'requires 10+')
461
479
  def test_logical_replication(self):
@@ -589,7 +607,7 @@ class TestgresTests(unittest.TestCase):
589
607
  # make standby becomes writable master
590
608
  replica.safe_psql('insert into abc values (1)')
591
609
  res = replica.safe_psql('select * from abc')
592
- self.assertEqual(res, b'1\n')
610
+ self.assertEqual(rm_carriage_returns(res), b'1\n')
593
611
 
594
612
  def test_dump(self):
595
613
  query_create = 'create table test as select generate_series(1, 2) as val'
@@ -614,6 +632,7 @@ class TestgresTests(unittest.TestCase):
614
632
  with get_new_node().init().start() as node:
615
633
  node.psql('create role test_user login')
616
634
  value = node.safe_psql('select 1', username='test_user')
635
+ value = rm_carriage_returns(value)
617
636
  self.assertEqual(value, b'1\n')
618
637
 
619
638
  def test_poll_query_until(self):
@@ -728,7 +747,7 @@ class TestgresTests(unittest.TestCase):
728
747
  master.restart()
729
748
  self.assertTrue(master._logger.is_alive())
730
749
 
731
- @unittest.skipUnless(util_exists('pgbench'), 'might be missing')
750
+ @unittest.skipUnless(util_exists('pgbench.exe' if os.name == 'nt' else 'pgbench'), 'pgbench might be missing')
732
751
  def test_pgbench(self):
733
752
  with get_new_node().init().start() as node:
734
753
 
@@ -744,6 +763,8 @@ class TestgresTests(unittest.TestCase):
744
763
  out, _ = proc.communicate()
745
764
  out = out.decode('utf-8')
746
765
 
766
+ proc.stdout.close()
767
+
747
768
  self.assertTrue('tps' in out)
748
769
 
749
770
  def test_pg_config(self):
@@ -977,7 +998,9 @@ class TestgresTests(unittest.TestCase):
977
998
 
978
999
  def test_child_process_dies(self):
979
1000
  # test for FileNotFound exception during child_processes() function
980
- with subprocess.Popen(["sleep", "60"]) as process:
1001
+ cmd = ["timeout", "60"] if os.name == 'nt' else ["sleep", "60"]
1002
+
1003
+ with subprocess.Popen(cmd, shell=True) as process: # shell=True might be needed on Windows
981
1004
  self.assertEqual(process.poll(), None)
982
1005
  # collect list of processes currently running
983
1006
  children = psutil.Process(os.getpid()).children()
@@ -52,10 +52,9 @@ from testgres import bound_ports
52
52
  from testgres.utils import PgVer
53
53
  from testgres.node import ProcessProxy, ConnectionParams
54
54
 
55
- conn_params = ConnectionParams(host=os.getenv('RDBMS_TESTPOOL1_HOST') or '172.18.0.3',
56
- username='dev',
57
- ssh_key=os.getenv(
58
- 'RDBMS_TESTPOOL_SSHKEY') or '../../container_files/postgres/ssh/id_ed25519')
55
+ conn_params = ConnectionParams(host=os.getenv('RDBMS_TESTPOOL1_HOST') or '127.0.0.1',
56
+ username=os.getenv('USER'),
57
+ ssh_key=os.getenv('RDBMS_TESTPOOL_SSHKEY'))
59
58
  os_ops = RemoteOperations(conn_params)
60
59
  testgres_config.set_os_ops(os_ops=os_ops)
61
60
 
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