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

File tree

11 files changed

+267
-58
lines changed

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)
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