Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit edb5708

Browse files
author
vshepard
committed
Fix initdb error on Windows
1 parent 846c05f commit edb5708

File tree

6 files changed

+184
-51
lines changed

6 files changed

+184
-51
lines changed

setup.py

Lines changed: 3 additions & 3 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',
3232
packages=['testgres', 'testgres.operations'],
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/operations/local_ops.py

Lines changed: 85 additions & 15 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,12 @@
2221
error_markers = [b'error', b'Permission denied', b'fatal']
2322

2423

24+
def has_errors(output):
25+
if isinstance(output, str):
26+
output = output.encode(get_default_encoding())
27+
return any(marker in output for marker in error_markers)
28+
29+
2530
class LocalOperations(OsOperations):
2631
def __init__(self, conn_params=None):
2732
if conn_params is None:
@@ -33,7 +38,38 @@ def __init__(self, conn_params=None):
3338
self.remote = False
3439
self.username = conn_params.username or self.get_user()
3540

36-
# Command execution
41+
@staticmethod
42+
def _run_command(cmd, shell, input, timeout, encoding, temp_file=None):
43+
"""Execute a command and return the process."""
44+
if temp_file is not None:
45+
stdout = temp_file
46+
stderr = subprocess.STDOUT
47+
else:
48+
stdout = subprocess.PIPE
49+
stderr = subprocess.PIPE
50+
51+
process = subprocess.Popen(
52+
cmd,
53+
shell=shell,
54+
stdin=subprocess.PIPE if input is not None else None,
55+
stdout=stdout,
56+
stderr=stderr,
57+
)
58+
59+
try:
60+
return process.communicate(input=input.encode(encoding) if input else None, timeout=timeout), process
61+
except subprocess.TimeoutExpired:
62+
process.kill()
63+
raise ExecUtilException("Command timed out after {} seconds.".format(timeout))
64+
65+
@staticmethod
66+
def _raise_exec_exception(message, command, exit_code, output):
67+
"""Raise an ExecUtilException."""
68+
raise ExecUtilException(message=message.format(output),
69+
command=command,
70+
exit_code=exit_code,
71+
out=output)
72+
3773
def exec_command(self, cmd, wait_exit=False, verbose=False,
3874
expect_error=False, encoding=None, shell=False, text=False,
3975
input=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@@ -56,16 +92,15 @@ def exec_command(self, cmd, wait_exit=False, verbose=False,
5692
:return: The output of the subprocess.
5793
"""
5894
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
95+
return self._exec_command_windows(cmd, wait_exit=wait_exit, verbose=verbose,
96+
expect_error=expect_error, encoding=encoding, shell=shell, text=text,
97+
input=input, stdin=stdin, stdout=stdout, stderr=stderr,
98+
get_process=get_process, timeout=timeout)
6599
else:
66100
process = subprocess.Popen(
67101
cmd,
68102
shell=shell,
103+
stdin=stdin,
69104
stdout=stdout,
70105
stderr=stderr,
71106
)
@@ -79,7 +114,7 @@ def exec_command(self, cmd, wait_exit=False, verbose=False,
79114
raise ExecUtilException("Command timed out after {} seconds.".format(timeout))
80115
exit_status = process.returncode
81116

82-
error_found = exit_status != 0 or any(marker in error for marker in error_markers)
117+
error_found = exit_status != 0 or has_errors(error)
83118

84119
if encoding:
85120
result = result.decode(encoding)
@@ -91,15 +126,50 @@ def exec_command(self, cmd, wait_exit=False, verbose=False,
91126
if exit_status != 0 or error_found:
92127
if exit_status == 0:
93128
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)
129+
self._raise_exec_exception('Utility exited with non-zero code. Error `{}`', cmd, exit_status, result)
98130
if verbose:
99131
return exit_status, result, error
100132
else:
101133
return result
102134

135+
@staticmethod
136+
def _process_output(process, encoding, temp_file=None):
137+
"""Process the output of a command."""
138+
if temp_file is not None:
139+
temp_file.seek(0)
140+
output = temp_file.read()
141+
else:
142+
output = process.stdout.read()
143+
144+
if encoding:
145+
output = output.decode(encoding)
146+
147+
return output
148+
149+
def _exec_command_windows(self, cmd, wait_exit=False, verbose=False,
150+
expect_error=False, encoding=None, shell=False, text=False,
151+
input=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
152+
get_process=None, timeout=None):
153+
with tempfile.NamedTemporaryFile(mode='w+b') as temp_file:
154+
_, process = self._run_command(cmd, shell, input, timeout, encoding, temp_file)
155+
if get_process:
156+
return process
157+
output = self._process_output(process, encoding, temp_file)
158+
159+
if process.returncode != 0 or has_errors(output):
160+
if process.returncode == 0:
161+
process.returncode = 1
162+
if expect_error:
163+
if verbose:
164+
return process.returncode, output, output
165+
else:
166+
return output
167+
else:
168+
self._raise_exec_exception('Utility exited with non-zero code. Error `{}`', cmd, process.returncode,
169+
output)
170+
171+
return (process.returncode, output, output) if verbose else output
172+
103173
# Environment setup
104174
def environ(self, var_name):
105175
return os.environ.get(var_name)
@@ -210,7 +280,7 @@ def read(self, filename, encoding=None, binary=False):
210280
if binary:
211281
return content
212282
if isinstance(content, bytes):
213-
return content.decode(encoding or 'utf-8')
283+
return content.decode(encoding or get_default_encoding())
214284
return content
215285

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

testgres/operations/os_ops.py

Lines changed: 6 additions & 0 deletions
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

testgres/operations/remote_ops.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import locale
21
import logging
32
import os
43
import subprocess
@@ -15,12 +14,7 @@
1514
raise ImportError("You must have psycopg2 or pg8000 modules installed")
1615

1716
from ..exceptions import ExecUtilException
18-
19-
from .os_ops import OsOperations, ConnectionParams
20-
21-
ConsoleEncoding = locale.getdefaultlocale()[1]
22-
if not ConsoleEncoding:
23-
ConsoleEncoding = 'UTF-8'
17+
from .os_ops import OsOperations, ConnectionParams, get_default_encoding
2418

2519
error_markers = [b'error', b'Permission denied', b'fatal', b'No such file or directory']
2620

@@ -36,7 +30,7 @@ def kill(self):
3630

3731
def cmdline(self):
3832
command = "ps -p {} -o cmd --no-headers".format(self.pid)
39-
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())
4034
cmdline = stdout.strip()
4135
return cmdline.split()
4236

@@ -145,7 +139,7 @@ def environ(self, var_name: str) -> str:
145139
- var_name (str): The name of the environment variable.
146140
"""
147141
cmd = "echo ${}".format(var_name)
148-
return self.exec_command(cmd, encoding=ConsoleEncoding).strip()
142+
return self.exec_command(cmd, encoding=get_default_encoding()).strip()
149143

150144
def find_executable(self, executable):
151145
search_paths = self.environ("PATH")
@@ -176,11 +170,11 @@ def set_env(self, var_name: str, var_val: str):
176170

177171
# Get environment variables
178172
def get_user(self):
179-
return self.exec_command("echo $USER", encoding=ConsoleEncoding).strip()
173+
return self.exec_command("echo $USER", encoding=get_default_encoding()).strip()
180174

181175
def get_name(self):
182176
cmd = 'python3 -c "import os; print(os.name)"'
183-
return self.exec_command(cmd, encoding=ConsoleEncoding).strip()
177+
return self.exec_command(cmd, encoding=get_default_encoding()).strip()
184178

185179
# Work with dirs
186180
def makedirs(self, path, remove_existing=False):
@@ -227,7 +221,7 @@ def listdir(self, path):
227221
return result.splitlines()
228222

229223
def path_exists(self, path):
230-
result = self.exec_command("test -e {}; echo $?".format(path), encoding=ConsoleEncoding)
224+
result = self.exec_command("test -e {}; echo $?".format(path), encoding=get_default_encoding())
231225
return int(result.strip()) == 0
232226

233227
@property
@@ -264,9 +258,9 @@ def mkdtemp(self, prefix=None):
264258

265259
def mkstemp(self, prefix=None):
266260
if prefix:
267-
temp_dir = self.exec_command("mktemp {}XXXXX".format(prefix), encoding=ConsoleEncoding)
261+
temp_dir = self.exec_command("mktemp {}XXXXX".format(prefix), encoding=get_default_encoding())
268262
else:
269-
temp_dir = self.exec_command("mktemp", encoding=ConsoleEncoding)
263+
temp_dir = self.exec_command("mktemp", encoding=get_default_encoding())
270264

271265
if temp_dir:
272266
if not os.path.isabs(temp_dir):
@@ -283,7 +277,9 @@ def copytree(self, src, dst):
283277
return self.exec_command("cp -r {} {}".format(src, dst))
284278

285279
# Work with files
286-
def write(self, filename, data, truncate=False, binary=False, read_and_write=False, encoding=ConsoleEncoding):
280+
def write(self, filename, data, truncate=False, binary=False, read_and_write=False, encoding=None):
281+
if not encoding:
282+
encoding = get_default_encoding()
287283
mode = "wb" if binary else "w"
288284
if not truncate:
289285
mode = "ab" if binary else "a"
@@ -302,7 +298,7 @@ def write(self, filename, data, truncate=False, binary=False, read_and_write=Fal
302298
data = data.encode(encoding)
303299

304300
if isinstance(data, list):
305-
data = [(s if isinstance(s, str) else s.decode(ConsoleEncoding)).rstrip('\n') + '\n' for s in data]
301+
data = [(s if isinstance(s, str) else s.decode(get_default_encoding())).rstrip('\n') + '\n' for s in data]
306302
tmp_file.writelines(data)
307303
else:
308304
tmp_file.write(data)
@@ -334,7 +330,7 @@ def read(self, filename, binary=False, encoding=None):
334330
result = self.exec_command(cmd, encoding=encoding)
335331

336332
if not binary and result:
337-
result = result.decode(encoding or ConsoleEncoding)
333+
result = result.decode(encoding or get_default_encoding())
338334

339335
return result
340336

@@ -347,7 +343,7 @@ def readlines(self, filename, num_lines=0, binary=False, encoding=None):
347343
result = self.exec_command(cmd, encoding=encoding)
348344

349345
if not binary and result:
350-
lines = result.decode(encoding or ConsoleEncoding).splitlines()
346+
lines = result.decode(encoding or get_default_encoding()).splitlines()
351347
else:
352348
lines = result.splitlines()
353349

@@ -375,7 +371,7 @@ def kill(self, pid, signal):
375371

376372
def get_pid(self):
377373
# Get current process id
378-
return int(self.exec_command("echo $$", encoding=ConsoleEncoding))
374+
return int(self.exec_command("echo $$", encoding=get_default_encoding()))
379375

380376
def get_process_children(self, pid):
381377
command = ["ssh", "-i", self.ssh_key, f"{self.username}@{self.host}", f"pgrep -P {pid}"]

testgres/utils.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
from __future__ import print_function
55

66
import os
7-
import port_for
7+
import random
8+
import socket
9+
810
import sys
911

1012
from contextlib import contextmanager
1113
from packaging.version import Version, InvalidVersion
1214
import re
1315

16+
from port_for import PortForException
1417
from six import iteritems
1518

1619
from .exceptions import ExecUtilException
@@ -37,13 +40,49 @@ def reserve_port():
3740
"""
3841
Generate a new port and add it to 'bound_ports'.
3942
"""
40-
41-
port = port_for.select_random(exclude_ports=bound_ports)
43+
port = select_random(exclude_ports=bound_ports)
4244
bound_ports.add(port)
4345

4446
return port
4547

4648

49+
def select_random(
50+
ports=None,
51+
exclude_ports=None,
52+
) -> int:
53+
"""
54+
Return random unused port number.
55+
Standard function from port_for does not work on Windows because of error
56+
'port_for.exceptions.PortForException: Can't select a port'
57+
We should update it.
58+
"""
59+
if ports is None:
60+
ports = set(range(1024, 65535))
61+
62+
if exclude_ports is None:
63+
exclude_ports = set()
64+
65+
ports.difference_update(set(exclude_ports))
66+
67+
sampled_ports = random.sample(tuple(ports), min(len(ports), 100))
68+
69+
for port in sampled_ports:
70+
if is_port_free(port):
71+
return port
72+
73+
raise PortForException("Can't select a port")
74+
75+
76+
def is_port_free(port: int) -> bool:
77+
"""Check if a port is free to use."""
78+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
79+
try:
80+
s.bind(("", port))
81+
return True
82+
except OSError:
83+
return False
84+
85+
4786
def release_port(port):
4887
"""
4988
Free port provided by reserve_port().
@@ -80,7 +119,8 @@ def execute_utility(args, logfile=None, verbose=False):
80119
lines = [u'\n'] + ['# ' + line for line in out.splitlines()] + [u'\n']
81120
tconf.os_ops.write(filename=logfile, data=lines)
82121
except IOError:
83-
raise ExecUtilException("Problem with writing to logfile `{}` during run command `{}`".format(logfile, args))
122+
raise ExecUtilException(
123+
"Problem with writing to logfile `{}` during run command `{}`".format(logfile, args))
84124
if verbose:
85125
return exit_status, out, error
86126
else:

0 commit comments

Comments
 (0)