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

Commit b949eb8

Browse files
Merge pull request #161 from dmitry-lipetsk/master-fix154--v02
Proposal to fix #154 (v2)
2 parents 1c73113 + cd0b5f8 commit b949eb8

11 files changed

+267
-58
lines changed

testgres/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
CatchUpException, \
2424
StartNodeException, \
2525
InitNodeException, \
26-
BackupException
26+
BackupException, \
27+
InvalidOperationException
2728

2829
from .enums import \
2930
XLogMethod, \
@@ -60,7 +61,7 @@
6061
"NodeBackup", "testgres_config",
6162
"TestgresConfig", "configure_testgres", "scoped_config", "push_config", "pop_config",
6263
"NodeConnection", "DatabaseError", "InternalError", "ProgrammingError", "OperationalError",
63-
"TestgresException", "ExecUtilException", "QueryException", "TimeoutException", "CatchUpException", "StartNodeException", "InitNodeException", "BackupException",
64+
"TestgresException", "ExecUtilException", "QueryException", "TimeoutException", "CatchUpException", "StartNodeException", "InitNodeException", "BackupException", "InvalidOperationException",
6465
"XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
6566
"PostgresNode", "NodeApp",
6667
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",

testgres/exceptions.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ class TestgresException(Exception):
99

1010
@six.python_2_unicode_compatible
1111
class ExecUtilException(TestgresException):
12-
def __init__(self, message=None, command=None, exit_code=0, out=None):
12+
def __init__(self, message=None, command=None, exit_code=0, out=None, error=None):
1313
super(ExecUtilException, self).__init__(message)
1414

1515
self.message = message
1616
self.command = command
1717
self.exit_code = exit_code
1818
self.out = out
19+
self.error = error
1920

2021
def __str__(self):
2122
msg = []
@@ -24,13 +25,17 @@ def __str__(self):
2425
msg.append(self.message)
2526

2627
if self.command:
27-
msg.append(u'Command: {}'.format(self.command))
28+
command_s = ' '.join(self.command) if isinstance(self.command, list) else self.command,
29+
msg.append(u'Command: {}'.format(command_s))
2830

2931
if self.exit_code:
3032
msg.append(u'Exit code: {}'.format(self.exit_code))
3133

34+
if self.error:
35+
msg.append(u'---- Error:\n{}'.format(self.error))
36+
3237
if self.out:
33-
msg.append(u'----\n{}'.format(self.out))
38+
msg.append(u'---- Out:\n{}'.format(self.out))
3439

3540
return self.convert_and_join(msg)
3641

@@ -98,3 +103,7 @@ class InitNodeException(TestgresException):
98103

99104
class BackupException(TestgresException):
100105
pass
106+
107+
108+
class InvalidOperationException(TestgresException):
109+
pass

testgres/node.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
TimeoutException, \
7575
InitNodeException, \
7676
TestgresException, \
77-
BackupException
77+
BackupException, \
78+
InvalidOperationException
7879

7980
from .logger import TestgresLogger
8081

@@ -987,6 +988,37 @@ def psql(self,
987988
>>> psql(query='select 3', ON_ERROR_STOP=1)
988989
"""
989990

991+
return self._psql(
992+
ignore_errors=True,
993+
query=query,
994+
filename=filename,
995+
dbname=dbname,
996+
username=username,
997+
input=input,
998+
**variables
999+
)
1000+
1001+
def _psql(
1002+
self,
1003+
ignore_errors,
1004+
query=None,
1005+
filename=None,
1006+
dbname=None,
1007+
username=None,
1008+
input=None,
1009+
**variables):
1010+
assert type(variables) == dict # noqa: E721
1011+
1012+
#
1013+
# We do not support encoding. It may be added later. Ok?
1014+
#
1015+
if input is None:
1016+
pass
1017+
elif type(input) == bytes: # noqa: E721
1018+
pass
1019+
else:
1020+
raise Exception("Input data must be None or bytes.")
1021+
9901022
dbname = dbname or default_dbname()
9911023

9921024
psql_params = [
@@ -1017,20 +1049,14 @@ def psql(self,
10171049

10181050
# should be the last one
10191051
psql_params.append(dbname)
1020-
if not self.os_ops.remote:
1021-
# start psql process
1022-
process = subprocess.Popen(psql_params,
1023-
stdin=subprocess.PIPE,
1024-
stdout=subprocess.PIPE,
1025-
stderr=subprocess.PIPE)
1026-
1027-
# wait until it finishes and get stdout and stderr
1028-
out, err = process.communicate(input=input)
1029-
return process.returncode, out, err
1030-
else:
1031-
status_code, out, err = self.os_ops.exec_command(psql_params, verbose=True, input=input)
10321052

1033-
return status_code, out, err
1053+
return self.os_ops.exec_command(
1054+
psql_params,
1055+
verbose=True,
1056+
input=input,
1057+
stderr=subprocess.PIPE,
1058+
stdout=subprocess.PIPE,
1059+
ignore_errors=ignore_errors)
10341060

10351061
@method_decorator(positional_args_hack(['dbname', 'query']))
10361062
def safe_psql(self, query=None, expect_error=False, **kwargs):
@@ -1051,22 +1077,27 @@ def safe_psql(self, query=None, expect_error=False, **kwargs):
10511077
Returns:
10521078
psql's output as str.
10531079
"""
1080+
assert type(kwargs) == dict # noqa: E721
1081+
assert not ("ignore_errors" in kwargs.keys())
1082+
assert not ("expect_error" in kwargs.keys())
10541083

10551084
# force this setting
10561085
kwargs['ON_ERROR_STOP'] = 1
10571086
try:
1058-
ret, out, err = self.psql(query=query, **kwargs)
1087+
ret, out, err = self._psql(ignore_errors=False, query=query, **kwargs)
10591088
except ExecUtilException as e:
1060-
ret = e.exit_code
1061-
out = e.out
1062-
err = e.message
1063-
if ret:
1064-
if expect_error:
1065-
out = (err or b'').decode('utf-8')
1066-
else:
1067-
raise QueryException((err or b'').decode('utf-8'), query)
1068-
elif expect_error:
1069-
assert False, "Exception was expected, but query finished successfully: `{}` ".format(query)
1089+
if not expect_error:
1090+
raise QueryException(e.message, query)
1091+
1092+
if type(e.error) == bytes: # noqa: E721
1093+
return e.error.decode("utf-8") # throw
1094+
1095+
# [2024-12-09] This situation is not expected
1096+
assert False
1097+
return e.error
1098+
1099+
if expect_error:
1100+
raise InvalidOperationException("Exception was expected, but query finished successfully: `{}`.".format(query))
10701101

10711102
return out
10721103

testgres/operations/helpers.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import locale
2+
3+
4+
class Helpers:
5+
def _make_get_default_encoding_func():
6+
# locale.getencoding is added in Python 3.11
7+
if hasattr(locale, 'getencoding'):
8+
return locale.getencoding
9+
10+
# It must exist
11+
return locale.getpreferredencoding
12+
13+
# Prepared pointer on function to get a name of system codepage
14+
_get_default_encoding_func = _make_get_default_encoding_func()
15+
16+
def GetDefaultEncoding():
17+
#
18+
# Original idea/source was:
19+
#
20+
# def os_ops.get_default_encoding():
21+
# if not hasattr(locale, 'getencoding'):
22+
# locale.getencoding = locale.getpreferredencoding
23+
# return locale.getencoding() or 'UTF-8'
24+
#
25+
26+
assert __class__._get_default_encoding_func is not None
27+
28+
r = __class__._get_default_encoding_func()
29+
30+
if r:
31+
assert r is not None
32+
assert type(r) == str # noqa: E721
33+
assert r != ""
34+
return r
35+
36+
# Is it an unexpected situation?
37+
return 'UTF-8'
38+
39+
def PrepareProcessInput(input, encoding):
40+
if not input:
41+
return None
42+
43+
if type(input) == str: # noqa: E721
44+
if encoding is None:
45+
return input.encode(__class__.GetDefaultEncoding())
46+
47+
assert type(encoding) == str # noqa: E721
48+
return input.encode(encoding)
49+
50+
# It is expected!
51+
assert type(input) == bytes # noqa: E721
52+
return input

testgres/operations/local_ops.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from ..exceptions import ExecUtilException
1313
from .os_ops import ConnectionParams, OsOperations, pglib, get_default_encoding
14+
from .raise_error import RaiseError
15+
from .helpers import Helpers
1416

1517
try:
1618
from shutil import which as find_executable
@@ -47,14 +49,6 @@ def __init__(self, conn_params=None):
4749
self.remote = False
4850
self.username = conn_params.username or getpass.getuser()
4951

50-
@staticmethod
51-
def _raise_exec_exception(message, command, exit_code, output):
52-
"""Raise an ExecUtilException."""
53-
raise ExecUtilException(message=message.format(output),
54-
command=' '.join(command) if isinstance(command, list) else command,
55-
exit_code=exit_code,
56-
out=output)
57-
5852
@staticmethod
5953
def _process_output(encoding, temp_file_path):
6054
"""Process the output of a command from a temporary file."""
@@ -65,6 +59,8 @@ def _process_output(encoding, temp_file_path):
6559
return output, None # In Windows stderr writing in stdout
6660

6761
def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
62+
# TODO: why don't we use the data from input?
63+
6864
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
6965
stdout = temp_file
7066
stderr = subprocess.STDOUT
@@ -86,25 +82,36 @@ def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process
8682
return process, output, error
8783

8884
def _run_command__generic(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
85+
input_prepared = None
86+
if not get_process:
87+
input_prepared = Helpers.PrepareProcessInput(input, encoding) # throw
88+
89+
assert input_prepared is None or (type(input_prepared) == bytes) # noqa: E721
90+
8991
process = subprocess.Popen(
9092
cmd,
9193
shell=shell,
9294
stdin=stdin or subprocess.PIPE if input is not None else None,
9395
stdout=stdout or subprocess.PIPE,
9496
stderr=stderr or subprocess.PIPE,
9597
)
98+
assert not (process is None)
9699
if get_process:
97100
return process, None, None
98101
try:
99-
output, error = process.communicate(input=input.encode(encoding) if input else None, timeout=timeout)
100-
if encoding:
101-
output = output.decode(encoding)
102-
error = error.decode(encoding)
103-
return process, output, error
102+
output, error = process.communicate(input=input_prepared, timeout=timeout)
104103
except subprocess.TimeoutExpired:
105104
process.kill()
106105
raise ExecUtilException("Command timed out after {} seconds.".format(timeout))
107106

107+
assert type(output) == bytes # noqa: E721
108+
assert type(error) == bytes # noqa: E721
109+
110+
if encoding:
111+
output = output.decode(encoding)
112+
error = error.decode(encoding)
113+
return process, output, error
114+
108115
def _run_command(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
109116
"""Execute a command and return the process and its output."""
110117
if os.name == 'nt' and stdout is None: # Windows
@@ -120,11 +127,20 @@ def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False,
120127
"""
121128
Execute a command in a subprocess and handle the output based on the provided parameters.
122129
"""
130+
assert type(expect_error) == bool # noqa: E721
131+
assert type(ignore_errors) == bool # noqa: E721
132+
123133
process, output, error = self._run_command(cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding)
124134
if get_process:
125135
return process
126136
if not ignore_errors and ((process.returncode != 0 or has_errors(output=output, error=error)) and not expect_error):
127-
self._raise_exec_exception('Utility exited with non-zero code. Error `{}`', cmd, process.returncode, error or output)
137+
RaiseError.UtilityExitedWithNonZeroCode(
138+
cmd=cmd,
139+
exit_code=process.returncode,
140+
msg_arg=error or output,
141+
error=error,
142+
out=output
143+
)
128144

129145
if verbose:
130146
return process.returncode, output, error

testgres/operations/raise_error.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from ..exceptions import ExecUtilException
2+
from .helpers import Helpers
3+
4+
5+
class RaiseError:
6+
def UtilityExitedWithNonZeroCode(cmd, exit_code, msg_arg, error, out):
7+
assert type(exit_code) == int # noqa: E721
8+
9+
msg_arg_s = __class__._TranslateDataIntoString(msg_arg).strip()
10+
assert type(msg_arg_s) == str # noqa: E721
11+
12+
if msg_arg_s == "":
13+
msg_arg_s = "#no_error_message"
14+
15+
message = "Utility exited with non-zero code. Error: `" + msg_arg_s + "`"
16+
raise ExecUtilException(
17+
message=message,
18+
command=cmd,
19+
exit_code=exit_code,
20+
out=out,
21+
error=error)
22+
23+
def _TranslateDataIntoString(data):
24+
if type(data) == bytes: # noqa: E721
25+
return __class__._TranslateDataIntoString__FromBinary(data)
26+
27+
return str(data)
28+
29+
def _TranslateDataIntoString__FromBinary(data):
30+
assert type(data) == bytes # noqa: E721
31+
32+
try:
33+
return data.decode(Helpers.GetDefaultEncoding())
34+
except UnicodeDecodeError:
35+
pass
36+
37+
return "#cannot_decode_text"
38+
39+
def _BinaryIsASCII(data):
40+
assert type(data) == bytes # noqa: E721
41+
42+
for b in data:
43+
if not (b >= 0 and b <= 127):
44+
return False
45+
46+
return True

0 commit comments

Comments
 (0)