Skip to content

Commit 1a2f6da

Browse files
authored
Fix initdb error on Windows (#99)
1 parent 846c05f commit 1a2f6da

File tree

12 files changed

+207
-122
lines changed

12 files changed

+207
-122
lines changed

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@
2727
readme = f.read()
2828

2929
setup(
30-
version='1.9.2',
30+
version='1.9.3',
3131
name='testgres',
32-
packages=['testgres', 'testgres.operations'],
32+
packages=['testgres', 'testgres.operations', 'testgres.helpers'],
3333
description='Testing utility for PostgreSQL and its extensions',
3434
url='https://github.com/postgrespro/testgres',
3535
long_description=readme,
3636
long_description_content_type='text/markdown',
3737
license='PostgreSQL',
38-
author='Ildar Musin',
39-
author_email='zildermann@gmail.com',
38+
author='Postgres Professional',
39+
author_email='testgres@postgrespro.ru',
4040
keywords=['test', 'testing', 'postgresql'],
4141
install_requires=install_requires,
4242
classifiers=[],

testgres/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
from .operations.local_ops import LocalOperations
5353
from .operations.remote_ops import RemoteOperations
5454

55+
from .helpers.port_manager import PortManager
56+
5557
__all__ = [
5658
"get_new_node",
5759
"get_remote_node",
@@ -62,6 +64,6 @@
6264
"XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
6365
"PostgresNode", "NodeApp",
6466
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
65-
"First", "Any",
67+
"First", "Any", "PortManager",
6668
"OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
6769
]

testgres/helpers/__init__.py

Whitespace-only changes.

testgres/helpers/port_manager.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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")

testgres/node.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,8 @@ def status(self):
623623
"-D", self.data_dir,
624624
"status"
625625
] # 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:
628628
return NodeStatus.Uninitialized
629629
elif 'no server running' in out:
630630
return NodeStatus.Stopped
@@ -717,7 +717,7 @@ def start(self, params=[], wait=True):
717717

718718
try:
719719
exit_status, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
720-
if 'does not exist' in error:
720+
if error and 'does not exist' in error:
721721
raise Exception
722722
except Exception as e:
723723
msg = 'Cannot start node'
@@ -791,7 +791,7 @@ def restart(self, params=[]):
791791

792792
try:
793793
error_code, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
794-
if 'could not start server' in error:
794+
if error and 'could not start server' in error:
795795
raise ExecUtilException
796796
except ExecUtilException as e:
797797
msg = 'Cannot restart node'

testgres/operations/local_ops.py

Lines changed: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import psutil
99

1010
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
1312

1413
try:
1514
from shutil import which as find_executable
@@ -22,6 +21,14 @@
2221
error_markers = [b'error', b'Permission denied', b'fatal']
2322

2423

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+
2532
class LocalOperations(OsOperations):
2633
def __init__(self, conn_params=None):
2734
if conn_params is None:
@@ -33,72 +40,80 @@ def __init__(self, conn_params=None):
3340
self.remote = False
3441
self.username = conn_params.username or self.get_user()
3542

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
6683
process = subprocess.Popen(
6784
cmd,
6885
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,
7189
)
7290
if get_process:
73-
return process
74-
91+
return process, None, None
7592
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
7798
except subprocess.TimeoutExpired:
7899
process.kill()
79100
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)
83101

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
102117

103118
# Environment setup
104119
def environ(self, var_name):
@@ -210,7 +225,7 @@ def read(self, filename, encoding=None, binary=False):
210225
if binary:
211226
return content
212227
if isinstance(content, bytes):
213-
return content.decode(encoding or 'utf-8')
228+
return content.decode(encoding or get_default_encoding())
214229
return content
215230

216231
def readlines(self, filename, num_lines=0, binary=False, encoding=None):

testgres/operations/os_ops.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import locale
2+
13
try:
24
import psycopg2 as pglib # noqa: F401
35
except ImportError:
@@ -14,6 +16,10 @@ def __init__(self, host='127.0.0.1', ssh_key=None, username=None):
1416
self.username = username
1517

1618

19+
def get_default_encoding():
20+
return locale.getdefaultlocale()[1] or 'UTF-8'
21+
22+
1723
class OsOperations:
1824
def __init__(self, username=None):
1925
self.ssh_key = None
@@ -75,7 +81,7 @@ def write(self, filename, data, truncate=False, binary=False, read_and_write=Fal
7581
def touch(self, filename):
7682
raise NotImplementedError()
7783

78-
def read(self, filename):
84+
def read(self, filename, encoding, binary):
7985
raise NotImplementedError()
8086

8187
def readlines(self, filename):

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy