Skip to content

Commit ac782bb

Browse files
[New] OsOps::execute_command supports a transfer of environment variables (exec_env) (#239)
* [New] OsOps::execute_command supports a transfer of environment variables (exec_env) New feature allows to pass environment variables to an executed program. If variable in exec_env has None value, then this variable will be unset. PostgresNode::start and PostgresNode::slow_start supports exec_env.
1 parent 2401474 commit ac782bb

File tree

5 files changed

+177
-22
lines changed

5 files changed

+177
-22
lines changed

‎testgres/node.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ def get_control_data(self):
10201020

10211021
return out_dict
10221022

1023-
def slow_start(self, replica=False, dbname='template1', username=None, max_attempts=0):
1023+
def slow_start(self, replica=False, dbname='template1', username=None, max_attempts=0, exec_env=None):
10241024
"""
10251025
Starts the PostgreSQL instance and then polls the instance
10261026
until it reaches the expected state (primary or replica). The state is checked
@@ -1033,7 +1033,9 @@ def slow_start(self, replica=False, dbname='template1', username=None, max_attem
10331033
If False, waits for the instance to be in primary mode. Default is False.
10341034
max_attempts:
10351035
"""
1036-
self.start()
1036+
assert exec_env is None or type(exec_env) == dict # noqa: E721
1037+
1038+
self.start(exec_env=exec_env)
10371039

10381040
if replica:
10391041
query = 'SELECT pg_is_in_recovery()'
@@ -1065,7 +1067,7 @@ def _detect_port_conflict(self, log_files0, log_files1):
10651067
return True
10661068
return False
10671069

1068-
def start(self, params=[], wait=True):
1070+
def start(self, params=[], wait=True, exec_env=None):
10691071
"""
10701072
Starts the PostgreSQL node using pg_ctl if node has not been started.
10711073
By default, it waits for the operation to complete before returning.
@@ -1079,7 +1081,7 @@ def start(self, params=[], wait=True):
10791081
Returns:
10801082
This instance of :class:`.PostgresNode`.
10811083
"""
1082-
1084+
assert exec_env is None or type(exec_env) == dict # noqa: E721
10831085
assert __class__._C_MAX_START_ATEMPTS > 1
10841086

10851087
if self.is_started:
@@ -1098,7 +1100,7 @@ def start(self, params=[], wait=True):
10981100

10991101
def LOCAL__start_node():
11001102
# 'error' will be None on Windows
1101-
_, _, error = execute_utility2(self.os_ops, _params, self.utils_log_file, verbose=True)
1103+
_, _, error = execute_utility2(self.os_ops, _params, self.utils_log_file, verbose=True, exec_env=exec_env)
11021104
assert error is None or type(error) == str # noqa: E721
11031105
if error and 'does not exist' in error:
11041106
raise Exception(error)

‎testgres/operations/local_ops.py

+60-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import socket
1010

1111
import psutil
12+
import typing
1213

1314
from ..exceptions import ExecUtilException
1415
from ..exceptions import InvalidOperationException
@@ -46,9 +47,34 @@ def _process_output(encoding, temp_file_path):
4647
output = output.decode(encoding)
4748
return output, None # In Windows stderr writing in stdout
4849

49-
def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
50+
def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding, exec_env=None):
51+
assert exec_env is None or type(exec_env) == dict # noqa: E721
52+
5053
# TODO: why don't we use the data from input?
5154

55+
extParams: typing.Dict[str, str] = dict()
56+
57+
if exec_env is None:
58+
pass
59+
elif len(exec_env) == 0:
60+
pass
61+
else:
62+
env = os.environ.copy()
63+
assert type(env) == dict # noqa: E721
64+
for v in exec_env.items():
65+
assert type(v) == tuple # noqa: E721
66+
assert len(v) == 2
67+
assert type(v[0]) == str # noqa: E721
68+
assert v[0] != ""
69+
70+
if v[1] is None:
71+
env.pop(v[0], None)
72+
else:
73+
assert type(v[1]) == str # noqa: E721
74+
env[v[0]] = v[1]
75+
76+
extParams["env"] = env
77+
5278
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
5379
stdout = temp_file
5480
stderr = subprocess.STDOUT
@@ -58,6 +84,7 @@ def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process
5884
stdin=stdin or subprocess.PIPE if input is not None else None,
5985
stdout=stdout,
6086
stderr=stderr,
87+
**extParams,
6188
)
6289
if get_process:
6390
return process, None, None
@@ -69,19 +96,45 @@ def _run_command__nt(self, cmd, shell, input, stdin, stdout, stderr, get_process
6996
output, error = self._process_output(encoding, temp_file_path)
7097
return process, output, error
7198

72-
def _run_command__generic(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
99+
def _run_command__generic(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding, exec_env=None):
100+
assert exec_env is None or type(exec_env) == dict # noqa: E721
101+
73102
input_prepared = None
74103
if not get_process:
75104
input_prepared = Helpers.PrepareProcessInput(input, encoding) # throw
76105

77106
assert input_prepared is None or (type(input_prepared) == bytes) # noqa: E721
78107

108+
extParams: typing.Dict[str, str] = dict()
109+
110+
if exec_env is None:
111+
pass
112+
elif len(exec_env) == 0:
113+
pass
114+
else:
115+
env = os.environ.copy()
116+
assert type(env) == dict # noqa: E721
117+
for v in exec_env.items():
118+
assert type(v) == tuple # noqa: E721
119+
assert len(v) == 2
120+
assert type(v[0]) == str # noqa: E721
121+
assert v[0] != ""
122+
123+
if v[1] is None:
124+
env.pop(v[0], None)
125+
else:
126+
assert type(v[1]) == str # noqa: E721
127+
env[v[0]] = v[1]
128+
129+
extParams["env"] = env
130+
79131
process = subprocess.Popen(
80132
cmd,
81133
shell=shell,
82134
stdin=stdin or subprocess.PIPE if input is not None else None,
83135
stdout=stdout or subprocess.PIPE,
84136
stderr=stderr or subprocess.PIPE,
137+
**extParams
85138
)
86139
assert not (process is None)
87140
if get_process:
@@ -100,25 +153,26 @@ def _run_command__generic(self, cmd, shell, input, stdin, stdout, stderr, get_pr
100153
error = error.decode(encoding)
101154
return process, output, error
102155

103-
def _run_command(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding):
156+
def _run_command(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding, exec_env=None):
104157
"""Execute a command and return the process and its output."""
105158
if os.name == 'nt' and stdout is None: # Windows
106159
method = __class__._run_command__nt
107160
else: # Other OS
108161
method = __class__._run_command__generic
109162

110-
return method(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding)
163+
return method(self, cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding, exec_env=exec_env)
111164

112165
def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False, encoding=None, shell=False,
113166
text=False, input=None, stdin=None, stdout=None, stderr=None, get_process=False, timeout=None,
114-
ignore_errors=False):
167+
ignore_errors=False, exec_env=None):
115168
"""
116169
Execute a command in a subprocess and handle the output based on the provided parameters.
117170
"""
118171
assert type(expect_error) == bool # noqa: E721
119172
assert type(ignore_errors) == bool # noqa: E721
173+
assert exec_env is None or type(exec_env) == dict # noqa: E721
120174

121-
process, output, error = self._run_command(cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding)
175+
process, output, error = self._run_command(cmd, shell, input, stdin, stdout, stderr, get_process, timeout, encoding, exec_env=exec_env)
122176
if get_process:
123177
return process
124178

‎testgres/operations/remote_ops.py

+35-9
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ def __enter__(self):
6464

6565
def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False,
6666
encoding=None, shell=True, text=False, input=None, stdin=None, stdout=None,
67-
stderr=None, get_process=None, timeout=None, ignore_errors=False):
67+
stderr=None, get_process=None, timeout=None, ignore_errors=False,
68+
exec_env=None):
6869
"""
6970
Execute a command in the SSH session.
7071
Args:
7172
- cmd (str): The command to be executed.
7273
"""
7374
assert type(expect_error) == bool # noqa: E721
7475
assert type(ignore_errors) == bool # noqa: E721
76+
assert exec_env is None or type(exec_env) == dict # noqa: E721
7577

7678
input_prepared = None
7779
if not get_process:
@@ -88,7 +90,7 @@ def exec_command(self, cmd, wait_exit=False, verbose=False, expect_error=False,
8890

8991
assert type(cmd_s) == str # noqa: E721
9092

91-
cmd_items = __class__._make_exec_env_list()
93+
cmd_items = __class__._make_exec_env_list(exec_env=exec_env)
9294
cmd_items.append(cmd_s)
9395

9496
env_cmd_s = ';'.join(cmd_items)
@@ -670,14 +672,38 @@ def _is_port_free__process_1(error: str) -> bool:
670672
return True
671673

672674
@staticmethod
673-
def _make_exec_env_list() -> typing.List[str]:
674-
result: typing.List[str] = list()
675+
def _make_exec_env_list(exec_env: typing.Dict) -> typing.List[str]:
676+
env: typing.Dict[str, str] = dict()
677+
678+
# ---------------------------------- SYSTEM ENV
675679
for envvar in os.environ.items():
676-
if not __class__._does_put_envvar_into_exec_cmd(envvar[0]):
677-
continue
678-
qvalue = __class__._quote_envvar(envvar[1])
679-
assert type(qvalue) == str # noqa: E721
680-
result.append(envvar[0] + "=" + qvalue)
680+
if __class__._does_put_envvar_into_exec_cmd(envvar[0]):
681+
env[envvar[0]] = envvar[1]
682+
683+
# ---------------------------------- EXEC (LOCAL) ENV
684+
if exec_env is None:
685+
pass
686+
else:
687+
for envvar in exec_env.items():
688+
assert type(envvar) == tuple # noqa: E721
689+
assert len(envvar) == 2
690+
assert type(envvar[0]) == str # noqa: E721
691+
env[envvar[0]] = envvar[1]
692+
693+
# ---------------------------------- FINAL BUILD
694+
result: typing.List[str] = list()
695+
for envvar in env.items():
696+
assert type(envvar) == tuple # noqa: E721
697+
assert len(envvar) == 2
698+
assert type(envvar[0]) == str # noqa: E721
699+
700+
if envvar[1] is None:
701+
result.append("unset " + envvar[0])
702+
else:
703+
assert type(envvar[1]) == str # noqa: E721
704+
qvalue = __class__._quote_envvar(envvar[1])
705+
assert type(qvalue) == str # noqa: E721
706+
result.append(envvar[0] + "=" + qvalue)
681707
continue
682708

683709
return result

‎testgres/utils.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,26 @@ def execute_utility(args, logfile=None, verbose=False):
9696
return execute_utility2(tconf.os_ops, args, logfile, verbose)
9797

9898

99-
def execute_utility2(os_ops: OsOperations, args, logfile=None, verbose=False, ignore_errors=False):
99+
def execute_utility2(
100+
os_ops: OsOperations,
101+
args,
102+
logfile=None,
103+
verbose=False,
104+
ignore_errors=False,
105+
exec_env=None,
106+
):
100107
assert os_ops is not None
101108
assert isinstance(os_ops, OsOperations)
102109
assert type(verbose) == bool # noqa: E721
103110
assert type(ignore_errors) == bool # noqa: E721
111+
assert exec_env is None or type(exec_env) == dict # noqa: E721
104112

105113
exit_status, out, error = os_ops.exec_command(
106114
args,
107115
verbose=True,
108116
ignore_errors=ignore_errors,
109-
encoding=OsHelpers.GetDefaultEncoding())
117+
encoding=OsHelpers.GetDefaultEncoding(),
118+
exec_env=exec_env)
110119

111120
out = '' if not out else out
112121

‎tests/test_os_ops_common.py

+64
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,70 @@ def test_exec_command_failure__expect_error(self, os_ops: OsOperations):
9393
assert b"nonexistent_command" in error
9494
assert b"not found" in error
9595

96+
def test_exec_command_with_exec_env(self, os_ops: OsOperations):
97+
assert isinstance(os_ops, OsOperations)
98+
99+
RunConditions.skip_if_windows()
100+
101+
C_ENV_NAME = "TESTGRES_TEST__EXEC_ENV_20250414"
102+
103+
cmd = ["sh", "-c", "echo ${}".format(C_ENV_NAME)]
104+
105+
exec_env = {C_ENV_NAME: "Hello!"}
106+
107+
response = os_ops.exec_command(cmd, exec_env=exec_env)
108+
assert response is not None
109+
assert type(response) == bytes # noqa: E721
110+
assert response == b'Hello!\n'
111+
112+
response = os_ops.exec_command(cmd)
113+
assert response is not None
114+
assert type(response) == bytes # noqa: E721
115+
assert response == b'\n'
116+
117+
def test_exec_command__test_unset(self, os_ops: OsOperations):
118+
assert isinstance(os_ops, OsOperations)
119+
120+
RunConditions.skip_if_windows()
121+
122+
C_ENV_NAME = "LANG"
123+
124+
cmd = ["sh", "-c", "echo ${}".format(C_ENV_NAME)]
125+
126+
response1 = os_ops.exec_command(cmd)
127+
assert response1 is not None
128+
assert type(response1) == bytes # noqa: E721
129+
130+
if response1 == b'\n':
131+
logging.warning("Environment variable {} is not defined.".format(C_ENV_NAME))
132+
return
133+
134+
exec_env = {C_ENV_NAME: None}
135+
response2 = os_ops.exec_command(cmd, exec_env=exec_env)
136+
assert response2 is not None
137+
assert type(response2) == bytes # noqa: E721
138+
assert response2 == b'\n'
139+
140+
response3 = os_ops.exec_command(cmd)
141+
assert response3 is not None
142+
assert type(response3) == bytes # noqa: E721
143+
assert response3 == response1
144+
145+
def test_exec_command__test_unset_dummy_var(self, os_ops: OsOperations):
146+
assert isinstance(os_ops, OsOperations)
147+
148+
RunConditions.skip_if_windows()
149+
150+
C_ENV_NAME = "TESTGRES_TEST__DUMMY_VAR_20250414"
151+
152+
cmd = ["sh", "-c", "echo ${}".format(C_ENV_NAME)]
153+
154+
exec_env = {C_ENV_NAME: None}
155+
response2 = os_ops.exec_command(cmd, exec_env=exec_env)
156+
assert response2 is not None
157+
assert type(response2) == bytes # noqa: E721
158+
assert response2 == b'\n'
159+
96160
def test_is_executable_true(self, os_ops: OsOperations):
97161
"""
98162
Test is_executable for an existing executable.

0 commit comments

Comments
 (0)