testgres 1.16.1__tar.gz → 1.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {testgres-1.16.1/testgres.egg-info → testgres-1.16.2}/PKG-INFO +2 -2
- {testgres-1.16.1 → testgres-1.16.2}/pyproject.toml +1 -1
- {testgres-1.16.1 → testgres-1.16.2}/src/__init__.py +1 -1
- testgres-1.16.2/src/impl/platforms/darwin/internal_platform_utils.py +456 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/platforms/internal_platform_utils.py +1 -1
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/platforms/internal_platform_utils_factory.py +4 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/platforms/linux/internal_platform_utils.py +3 -2
- {testgres-1.16.1 → testgres-1.16.2}/src/node_app.py +1 -1
- {testgres-1.16.1 → testgres-1.16.2/testgres.egg-info}/PKG-INFO +2 -2
- {testgres-1.16.1 → testgres-1.16.2}/testgres.egg-info/SOURCES.txt +1 -0
- {testgres-1.16.1 → testgres-1.16.2}/testgres.egg-info/requires.txt +1 -1
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_os_ops_common.py +181 -10
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_testgres_common.py +1 -1
- {testgres-1.16.1 → testgres-1.16.2}/LICENSE +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/README.md +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/setup.cfg +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/api.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/backup.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/cache.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/config.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/connection.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/consts.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/decorators.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/defaults.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/enums.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/exceptions.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/file_line_reader.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/internal_utils.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/platforms/win32/internal_platform_utils.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/port_manager__generic.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/port_manager__generic2.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/impl/port_manager__this_host.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/logger.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/node.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/port_manager.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/pubsub.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/raise_error.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/standby.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/src/utils.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/testgres.egg-info/dependency_links.txt +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/testgres.egg-info/top_level.txt +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_api.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_config.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_os_ops_local.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_os_ops_remote.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_raise_error.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_testgres_local.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_testgres_remote.py +0 -0
- {testgres-1.16.1 → testgres-1.16.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testgres
|
|
3
|
-
Version: 1.16.
|
|
3
|
+
Version: 1.16.2
|
|
4
4
|
Summary: Testing utility for PostgreSQL and its extensions
|
|
5
5
|
Author-email: Postgres Professional <testgres@postgrespro.ru>
|
|
6
6
|
License: PostgreSQL
|
|
@@ -27,7 +27,7 @@ Requires-Dist: port-for>=0.4
|
|
|
27
27
|
Requires-Dist: six>=1.9.0
|
|
28
28
|
Requires-Dist: psutil
|
|
29
29
|
Requires-Dist: packaging
|
|
30
|
-
Requires-Dist: testgres.os_ops<4.0.0,>=3.3.
|
|
30
|
+
Requires-Dist: testgres.os_ops<4.0.0,>=3.3.3
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
33
|
[](https://github.com/postgrespro/testgres/actions/workflows/ci.yml)
|
|
@@ -60,7 +60,7 @@ from testgres.operations.os_ops import OsProcessController
|
|
|
60
60
|
from testgres.operations.local_ops import LocalOperations
|
|
61
61
|
from testgres.operations.remote_ops import RemoteOperations
|
|
62
62
|
|
|
63
|
-
__version__ = "1.16.
|
|
63
|
+
__version__ = "1.16.2"
|
|
64
64
|
|
|
65
65
|
__all__ = [
|
|
66
66
|
"get_new_node",
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .. import internal_platform_utils as base
|
|
4
|
+
from ... import internal_utils
|
|
5
|
+
from ....raise_error import RaiseError
|
|
6
|
+
|
|
7
|
+
from testgres.operations.os_ops import OsOperations
|
|
8
|
+
from testgres.operations.os_ops import OsCommandResult
|
|
9
|
+
from testgres.operations.exceptions import ExecUtilException
|
|
10
|
+
from testgres.operations.types import T_OS_EXEC_ENV
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
14
|
+
import typing
|
|
15
|
+
import time
|
|
16
|
+
import subprocess
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InternalPlatformUtils(base.InternalPlatformUtils):
|
|
20
|
+
C_MAX_FIND_POSTMASTER_ATTEMPTS = 5
|
|
21
|
+
C_BASH_EXE = "/bin/bash"
|
|
22
|
+
|
|
23
|
+
sm_exec_env: T_OS_EXEC_ENV = {
|
|
24
|
+
"LANG": "en_US.UTF-8",
|
|
25
|
+
"LC_ALL": "en_US.UTF-8",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------
|
|
29
|
+
def FindPostmaster(
|
|
30
|
+
self,
|
|
31
|
+
os_ops: OsOperations,
|
|
32
|
+
bin_dir: str,
|
|
33
|
+
data_dir: str
|
|
34
|
+
) -> InternalPlatformUtils.FindPostmasterResult:
|
|
35
|
+
assert isinstance(os_ops, OsOperations)
|
|
36
|
+
assert type(bin_dir) is str
|
|
37
|
+
assert type(data_dir) is str
|
|
38
|
+
assert type(__class__.C_BASH_EXE) is str
|
|
39
|
+
assert type(__class__.sm_exec_env) is dict
|
|
40
|
+
assert len(__class__.C_BASH_EXE) > 0
|
|
41
|
+
assert len(bin_dir) > 0
|
|
42
|
+
assert len(data_dir) > 0
|
|
43
|
+
|
|
44
|
+
failures: typing.List[Exception] = []
|
|
45
|
+
|
|
46
|
+
postmaster_pid: typing.Optional[int] = None
|
|
47
|
+
|
|
48
|
+
nAttempts = 0
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
nAttempts += 1
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
postmaster_pid = __class__._FindPostmaster(
|
|
55
|
+
os_ops,
|
|
56
|
+
bin_dir,
|
|
57
|
+
data_dir,
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
failures.append(e)
|
|
61
|
+
|
|
62
|
+
log_msg = "FindPostmaster (bin_dir={!r}, data_dir={!r}) detects a problem. Exception {}:\n{}".format(
|
|
63
|
+
bin_dir,
|
|
64
|
+
data_dir,
|
|
65
|
+
type(e).__name__,
|
|
66
|
+
e,
|
|
67
|
+
)
|
|
68
|
+
internal_utils.send_log_debug(log_msg)
|
|
69
|
+
|
|
70
|
+
if nAttempts < __class__.C_MAX_FIND_POSTMASTER_ATTEMPTS:
|
|
71
|
+
time.sleep(0.05)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
__class__._find_postmaster__throw_error__fail(
|
|
75
|
+
bin_dir=bin_dir,
|
|
76
|
+
data_dir=data_dir,
|
|
77
|
+
failures=failures,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if postmaster_pid is None:
|
|
83
|
+
return InternalPlatformUtils.FindPostmasterResult.create_not_found()
|
|
84
|
+
|
|
85
|
+
assert type(postmaster_pid) is int
|
|
86
|
+
|
|
87
|
+
return InternalPlatformUtils.FindPostmasterResult.create_ok(postmaster_pid)
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _FindPostmaster(
|
|
92
|
+
os_ops: OsOperations,
|
|
93
|
+
bin_dir: str,
|
|
94
|
+
data_dir: str
|
|
95
|
+
) -> typing.Optional[int]:
|
|
96
|
+
assert isinstance(os_ops, OsOperations)
|
|
97
|
+
assert type(bin_dir) is str
|
|
98
|
+
assert type(data_dir) is str
|
|
99
|
+
assert type(__class__.C_BASH_EXE) is str
|
|
100
|
+
assert type(__class__.sm_exec_env) is dict
|
|
101
|
+
assert len(__class__.C_BASH_EXE) > 0
|
|
102
|
+
assert len(bin_dir) > 0
|
|
103
|
+
assert len(data_dir) > 0
|
|
104
|
+
|
|
105
|
+
pg_path_e = re.escape(os_ops.build_path(bin_dir, "postgres"))
|
|
106
|
+
data_dir_e = re.escape(data_dir)
|
|
107
|
+
|
|
108
|
+
assert type(pg_path_e) is str
|
|
109
|
+
assert type(data_dir_e) is str
|
|
110
|
+
|
|
111
|
+
regexp = r"^\s*[0-9]+\s+[0-9]+\s+" + pg_path_e + r"(\s+.*)?\s+\-[D]\s+" + data_dir_e + r"(\s+.*)?"
|
|
112
|
+
|
|
113
|
+
cmd = [
|
|
114
|
+
__class__.C_BASH_EXE,
|
|
115
|
+
"-c",
|
|
116
|
+
"ps -ewwo \"pid=,ppid=,args=\" | grep -E " + shlex.quote(regexp),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
exec_r = os_ops.run(
|
|
120
|
+
cmd=cmd,
|
|
121
|
+
check=False,
|
|
122
|
+
exec_env=__class__.sm_exec_env,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assert type(exec_r) is OsCommandResult
|
|
126
|
+
assert type(exec_r.returncode) is int
|
|
127
|
+
assert type(exec_r.stdout) is bytes
|
|
128
|
+
assert type(exec_r.stderr) is bytes
|
|
129
|
+
|
|
130
|
+
if exec_r.returncode == 1:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
output = exec_r.stdout.decode("utf-8")
|
|
134
|
+
error = exec_r.stderr.decode("utf-8")
|
|
135
|
+
|
|
136
|
+
assert type(output) is str
|
|
137
|
+
assert type(error) is str
|
|
138
|
+
|
|
139
|
+
if exec_r.returncode != 0:
|
|
140
|
+
errMsg = f"test command returned an unexpected exit code: {exec_r.returncode}"
|
|
141
|
+
raise ExecUtilException(
|
|
142
|
+
message=errMsg,
|
|
143
|
+
command=cmd,
|
|
144
|
+
exit_code=exec_r.returncode,
|
|
145
|
+
out=output,
|
|
146
|
+
error=error,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
lines = output.splitlines()
|
|
150
|
+
assert type(lines) is list
|
|
151
|
+
|
|
152
|
+
if len(lines) == 0:
|
|
153
|
+
# ACHTUNG!
|
|
154
|
+
raise RuntimeError("Command returns 0 error code without output.")
|
|
155
|
+
|
|
156
|
+
# parse result lines
|
|
157
|
+
pid_to_ppid: __class__.T_PID_TO_PPID = {}
|
|
158
|
+
|
|
159
|
+
for i_line in range(len(lines)):
|
|
160
|
+
assert type(lines[i_line]) is str
|
|
161
|
+
|
|
162
|
+
parts = lines[i_line].split()
|
|
163
|
+
assert type(parts) is list
|
|
164
|
+
|
|
165
|
+
if len(parts) < 2:
|
|
166
|
+
__class__._find_postmaster__throw_error__bad_line_format(
|
|
167
|
+
lines,
|
|
168
|
+
i_line,
|
|
169
|
+
"no usefull data",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not parts[0].isdigit():
|
|
173
|
+
__class__._find_postmaster__throw_error__bad_line_format(
|
|
174
|
+
lines,
|
|
175
|
+
i_line,
|
|
176
|
+
"bad pid",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if not parts[1].isdigit():
|
|
180
|
+
__class__._find_postmaster__throw_error__bad_line_format(
|
|
181
|
+
lines,
|
|
182
|
+
i_line,
|
|
183
|
+
"bad ppid",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
pid = int(parts[0])
|
|
187
|
+
ppid = int(parts[1])
|
|
188
|
+
|
|
189
|
+
if pid not in pid_to_ppid:
|
|
190
|
+
pid_to_ppid[pid] = ppid
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
other_ppid = pid_to_ppid[pid]
|
|
194
|
+
assert type(other_ppid) is int
|
|
195
|
+
|
|
196
|
+
if ppid == other_ppid:
|
|
197
|
+
log_msg = "FindPostmaster (data_dir={!r}) get pid ({}) with ppid ({}) more than one time.".format(
|
|
198
|
+
data_dir,
|
|
199
|
+
pid,
|
|
200
|
+
ppid,
|
|
201
|
+
)
|
|
202
|
+
internal_utils.send_log_debug(log_msg)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# ACTUNG ppid is changed --> restart
|
|
206
|
+
__class__._find_postmaster__throw_error__ppid_is_changed(
|
|
207
|
+
pid,
|
|
208
|
+
ppid,
|
|
209
|
+
other_ppid,
|
|
210
|
+
lines,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
assert len(pid_to_ppid) <= len(lines)
|
|
214
|
+
|
|
215
|
+
true_postmasters = [
|
|
216
|
+
pid for pid, ppid in pid_to_ppid.items()
|
|
217
|
+
if ppid not in pid_to_ppid
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
if len(true_postmasters) == 0:
|
|
221
|
+
__class__._find_postmaster__throw_error__cycle(
|
|
222
|
+
pid_to_ppid,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if len(true_postmasters) == 1:
|
|
226
|
+
true_pid = true_postmasters[0]
|
|
227
|
+
|
|
228
|
+
if len(pid_to_ppid) > 1:
|
|
229
|
+
msg = "Many processes like a postmaster for data dir [{}] are found ({}).".format(
|
|
230
|
+
data_dir,
|
|
231
|
+
len(true_postmasters),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
msg += " List (ppid->pid): {}.".format(
|
|
235
|
+
__class__._make_text_from_pid_to_ppid(pid_to_ppid),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
msg += " True postmaster PID is {}.".format(true_pid)
|
|
239
|
+
internal_utils.send_log_debug(msg)
|
|
240
|
+
|
|
241
|
+
return true_pid
|
|
242
|
+
|
|
243
|
+
assert len(true_postmasters) > 1
|
|
244
|
+
|
|
245
|
+
__class__._find_postmaster__throw_error__many_postmasters(
|
|
246
|
+
true_postmasters,
|
|
247
|
+
pid_to_ppid,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# --------------------------------------------------------------------
|
|
251
|
+
def ProcessIsZombi_soft_check(
|
|
252
|
+
self,
|
|
253
|
+
os_ops: OsOperations,
|
|
254
|
+
pid: int,
|
|
255
|
+
) -> typing.Optional[bool]:
|
|
256
|
+
assert isinstance(os_ops, OsOperations)
|
|
257
|
+
assert type(pid) is int
|
|
258
|
+
|
|
259
|
+
# Change for macOS: Instead of reading non-existent /proc/PID/stat,
|
|
260
|
+
# we make a native call to the system ps and request the status (state) of the process.
|
|
261
|
+
cmd = ["ps", "-p", str(pid), "-wwo", "state="]
|
|
262
|
+
|
|
263
|
+
exec_r = os_ops.run(
|
|
264
|
+
cmd=cmd,
|
|
265
|
+
check=False,
|
|
266
|
+
exec_env=__class__.sm_exec_env,
|
|
267
|
+
stdout=subprocess.PIPE,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
assert type(exec_r) is OsCommandResult
|
|
271
|
+
assert type(exec_r.stdout) is bytes
|
|
272
|
+
|
|
273
|
+
# If the process is not found (already terminated and deleted), ps will return code 1
|
|
274
|
+
if exec_r.returncode == 1:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
if exec_r.returncode != 0:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
proc_status = exec_r.stdout.decode("utf-8").rstrip()
|
|
281
|
+
|
|
282
|
+
if not proc_status:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
assert len(proc_status) > 0
|
|
286
|
+
|
|
287
|
+
# state The state is given by a sequence of letters, for example,
|
|
288
|
+
# "RWNA". The first letter indicates the run state of the process:
|
|
289
|
+
#
|
|
290
|
+
# D Marks a process in disk (or other short term, uninterruptible) wait.[legacy option]
|
|
291
|
+
# I Marks a process that is idle (sleeping for longer than about 20 seconds).
|
|
292
|
+
# R Marks a runnable process.
|
|
293
|
+
# S Marks a process that is sleeping for less than about 20 seconds.
|
|
294
|
+
# T Marks a stopped process.
|
|
295
|
+
# U Marks a process in uninterruptible wait.
|
|
296
|
+
# Z Marks a dead process (a 'zombie').
|
|
297
|
+
#
|
|
298
|
+
# Additional characters after these, if any, indicate additional
|
|
299
|
+
# state information:
|
|
300
|
+
#
|
|
301
|
+
# + The process is in the foreground process group of its control terminal.
|
|
302
|
+
# < The process has raised CPU scheduling priority.
|
|
303
|
+
# > The process has specified a soft limit on memory requirements and is currently
|
|
304
|
+
# exceeding that limit; such a process is (necessarily) not swapped.
|
|
305
|
+
# A the process has asked for random page replacement
|
|
306
|
+
# (VA_ANOM, from vadvise(2), for example, lisp(1) in a garbage collect).
|
|
307
|
+
# E The process is trying to exit.
|
|
308
|
+
# L The process has pages locked in core (for example, for raw I/O).
|
|
309
|
+
# N The process has reduced CPU scheduling priority (see setpriority(2)).
|
|
310
|
+
# S The process has asked for FIFO page replacement (VA_SEQL,
|
|
311
|
+
# from vadvise(2), for example, a large image processing
|
|
312
|
+
# program using virtual memory to sequentially address
|
|
313
|
+
# voluminous data).
|
|
314
|
+
# s The process is a session leader.
|
|
315
|
+
# V The process is suspended during a vfork.
|
|
316
|
+
# W The process is swapped out.
|
|
317
|
+
# X The process is being traced or debugged.
|
|
318
|
+
|
|
319
|
+
ch1 = proc_status[0]
|
|
320
|
+
|
|
321
|
+
if ch1 == "Z":
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
if ch1 in "DIRSTU":
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
T_PID_TO_PPID = typing.Dict[int, int]
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _make_text_from_pid_to_ppid(pid_to_ppid: T_PID_TO_PPID) -> str:
|
|
333
|
+
result = ""
|
|
334
|
+
sep = ""
|
|
335
|
+
for pid, ppid in pid_to_ppid.items():
|
|
336
|
+
result += sep + " {}->{}".format(ppid, pid)
|
|
337
|
+
sep = ", "
|
|
338
|
+
continue
|
|
339
|
+
return result
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _find_postmaster__throw_error__bad_line_format(
|
|
343
|
+
lines: typing.List[str],
|
|
344
|
+
i_line: int,
|
|
345
|
+
hint: str,
|
|
346
|
+
) -> typing.NoReturn:
|
|
347
|
+
assert type(lines) is list
|
|
348
|
+
assert type(i_line) is int
|
|
349
|
+
assert type(hint) is str
|
|
350
|
+
|
|
351
|
+
error_lines: typing.List[str] = []
|
|
352
|
+
error_lines.append(
|
|
353
|
+
"Line {} has bad format. Hint: {}.".format(
|
|
354
|
+
i_line + 1,
|
|
355
|
+
hint,
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
error_lines.append(
|
|
359
|
+
"Problem line is:"
|
|
360
|
+
)
|
|
361
|
+
error_lines.append(
|
|
362
|
+
" " + repr(lines[i_line]),
|
|
363
|
+
)
|
|
364
|
+
error_lines.append(
|
|
365
|
+
"All the lines is:",
|
|
366
|
+
)
|
|
367
|
+
for i in range(len(lines)):
|
|
368
|
+
error_lines.append(
|
|
369
|
+
" {}. {!r}".format(i + 1, lines[i]),
|
|
370
|
+
)
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
raise RuntimeError("\n".join(error_lines))
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def _find_postmaster__throw_error__ppid_is_changed(
|
|
377
|
+
pid: int,
|
|
378
|
+
ppid: int,
|
|
379
|
+
other_ppid: int,
|
|
380
|
+
lines: typing.List[str],
|
|
381
|
+
) -> typing.NoReturn:
|
|
382
|
+
assert type(pid) is int
|
|
383
|
+
assert type(ppid) is int
|
|
384
|
+
assert type(other_ppid) is int
|
|
385
|
+
assert type(lines) is list
|
|
386
|
+
|
|
387
|
+
error_lines: typing.List[str] = []
|
|
388
|
+
error_lines.append(
|
|
389
|
+
"Parent of process ({}) is changed from {} to {}.".format(
|
|
390
|
+
pid,
|
|
391
|
+
other_ppid,
|
|
392
|
+
ppid,
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
error_lines.append(
|
|
396
|
+
"All the lines is:",
|
|
397
|
+
)
|
|
398
|
+
for i in range(len(lines)):
|
|
399
|
+
error_lines.append(
|
|
400
|
+
" {}. {!r}".format(i + 1, lines[i]),
|
|
401
|
+
)
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
raise RuntimeError("\n".join(error_lines))
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def _find_postmaster__throw_error__cycle(
|
|
408
|
+
pid_to_ppid: T_PID_TO_PPID,
|
|
409
|
+
) -> typing.NoReturn:
|
|
410
|
+
msg = "Cycle in processes postgres process tree. "
|
|
411
|
+
|
|
412
|
+
msg += " List (ppid->pid): {},".format(
|
|
413
|
+
__class__._make_text_from_pid_to_ppid(pid_to_ppid),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
msg += " List size is {}.".format(
|
|
417
|
+
len(pid_to_ppid),
|
|
418
|
+
)
|
|
419
|
+
raise RuntimeError(msg)
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def _find_postmaster__throw_error__many_postmasters(
|
|
423
|
+
postmaster_pids: typing.List[int],
|
|
424
|
+
pid_to_ppid: T_PID_TO_PPID,
|
|
425
|
+
) -> typing.NoReturn:
|
|
426
|
+
msg = "Many processes like a postmaster are found ({}): {}.".format(
|
|
427
|
+
len(postmaster_pids),
|
|
428
|
+
", ".join(map(str, postmaster_pids)),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
msg += " Trees (ppid->pid): {}.".format(
|
|
432
|
+
__class__._make_text_from_pid_to_ppid(pid_to_ppid),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
msg += " Total process count is {}.".format(len(pid_to_ppid))
|
|
436
|
+
raise RuntimeError(msg)
|
|
437
|
+
|
|
438
|
+
@staticmethod
|
|
439
|
+
def _find_postmaster__throw_error__fail(
|
|
440
|
+
bin_dir: str,
|
|
441
|
+
data_dir: str,
|
|
442
|
+
failures: typing.List[Exception],
|
|
443
|
+
) -> typing.NoReturn:
|
|
444
|
+
assert type(bin_dir) is str
|
|
445
|
+
assert type(data_dir) is str
|
|
446
|
+
assert type(failures) is list
|
|
447
|
+
|
|
448
|
+
method_name = "InternalPlatformUtils::FindPostmaster(bin_dir={!r}, data_dir={!r})".format(
|
|
449
|
+
bin_dir,
|
|
450
|
+
data_dir,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
RaiseError.function_did_multiple_attempts_without_stable_result(
|
|
454
|
+
method_name,
|
|
455
|
+
failures,
|
|
456
|
+
)
|
|
@@ -19,5 +19,9 @@ def create_internal_platform_utils(
|
|
|
19
19
|
from .win32 import internal_platform_utils as x
|
|
20
20
|
return x.InternalPlatformUtils()
|
|
21
21
|
|
|
22
|
+
if platform_name == "darwin":
|
|
23
|
+
from .darwin import internal_platform_utils as x
|
|
24
|
+
return x.InternalPlatformUtils()
|
|
25
|
+
|
|
22
26
|
# not implemented
|
|
23
27
|
return InternalPlatformUtils()
|
|
@@ -113,7 +113,7 @@ class InternalPlatformUtils(base.InternalPlatformUtils):
|
|
|
113
113
|
__class__.C_BASH_EXE,
|
|
114
114
|
"-c",
|
|
115
115
|
"ps -ewwo \"pid=,ppid=,args=\" | grep -E " + shlex.quote(regexp),
|
|
116
|
-
|
|
116
|
+
]
|
|
117
117
|
|
|
118
118
|
exec_r = os_ops.run(
|
|
119
119
|
cmd=cmd,
|
|
@@ -246,6 +246,7 @@ class InternalPlatformUtils(base.InternalPlatformUtils):
|
|
|
246
246
|
pid_to_ppid,
|
|
247
247
|
)
|
|
248
248
|
|
|
249
|
+
# --------------------------------------------------------------------
|
|
249
250
|
def ProcessIsZombi_soft_check(
|
|
250
251
|
self,
|
|
251
252
|
os_ops: OsOperations,
|
|
@@ -316,7 +317,7 @@ class InternalPlatformUtils(base.InternalPlatformUtils):
|
|
|
316
317
|
) -> typing.NoReturn:
|
|
317
318
|
assert type(lines) is list
|
|
318
319
|
assert type(i_line) is int
|
|
319
|
-
assert type(hint) is
|
|
320
|
+
assert type(hint) is str
|
|
320
321
|
|
|
321
322
|
error_lines: typing.List[str] = []
|
|
322
323
|
error_lines.append(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testgres
|
|
3
|
-
Version: 1.16.
|
|
3
|
+
Version: 1.16.2
|
|
4
4
|
Summary: Testing utility for PostgreSQL and its extensions
|
|
5
5
|
Author-email: Postgres Professional <testgres@postgrespro.ru>
|
|
6
6
|
License: PostgreSQL
|
|
@@ -27,7 +27,7 @@ Requires-Dist: port-for>=0.4
|
|
|
27
27
|
Requires-Dist: six>=1.9.0
|
|
28
28
|
Requires-Dist: psutil
|
|
29
29
|
Requires-Dist: packaging
|
|
30
|
-
Requires-Dist: testgres.os_ops<4.0.0,>=3.3.
|
|
30
|
+
Requires-Dist: testgres.os_ops<4.0.0,>=3.3.3
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
33
|
[](https://github.com/postgrespro/testgres/actions/workflows/ci.yml)
|
|
@@ -27,6 +27,7 @@ src/impl/port_manager__generic2.py
|
|
|
27
27
|
src/impl/port_manager__this_host.py
|
|
28
28
|
src/impl/platforms/internal_platform_utils.py
|
|
29
29
|
src/impl/platforms/internal_platform_utils_factory.py
|
|
30
|
+
src/impl/platforms/darwin/internal_platform_utils.py
|
|
30
31
|
src/impl/platforms/linux/internal_platform_utils.py
|
|
31
32
|
src/impl/platforms/win32/internal_platform_utils.py
|
|
32
33
|
testgres.egg-info/PKG-INFO
|
|
@@ -7,6 +7,9 @@ from tests.helpers.global_data import OsOperations
|
|
|
7
7
|
from tests.helpers.run_conditions import RunConditions
|
|
8
8
|
from tests.helpers.local_check import LocalCheck
|
|
9
9
|
from tests.helpers.local_check import OsOpsHelpers
|
|
10
|
+
from tests.helpers.multi_try_call import MultiTryCall
|
|
11
|
+
|
|
12
|
+
from tests.conftest_helpers import TestServices
|
|
10
13
|
|
|
11
14
|
from testgres.operations.os_ops import OsProcessController
|
|
12
15
|
from testgres.operations.os_ops import OsCommandResult
|
|
@@ -225,7 +228,7 @@ class TestOsOpsCommon:
|
|
|
225
228
|
p = os_ops.get_platform()
|
|
226
229
|
assert p is not None
|
|
227
230
|
assert type(p) is str
|
|
228
|
-
assert p in {"win32", "linux"}
|
|
231
|
+
assert p in {"win32", "linux", "darwin"}
|
|
229
232
|
return
|
|
230
233
|
|
|
231
234
|
def test_create_clone(
|
|
@@ -422,11 +425,12 @@ class TestOsOpsCommon:
|
|
|
422
425
|
assert not os_ops.path_exists(tmp_file)
|
|
423
426
|
return
|
|
424
427
|
|
|
425
|
-
def
|
|
428
|
+
def test_exec_command_with_cwd__linux(self, os_ops_descr: OsOpsDescr):
|
|
426
429
|
assert type(os_ops_descr) is OsOpsDescr
|
|
427
430
|
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
428
431
|
|
|
429
432
|
RunConditions.skip_if_windows()
|
|
433
|
+
RunConditions.skip_if_darwin()
|
|
430
434
|
|
|
431
435
|
os_ops = os_ops_descr.os_ops
|
|
432
436
|
assert isinstance(os_ops, OsOperations)
|
|
@@ -616,7 +620,7 @@ class TestOsOpsCommon:
|
|
|
616
620
|
assert not os_ops.path_exists(path)
|
|
617
621
|
return
|
|
618
622
|
|
|
619
|
-
def
|
|
623
|
+
def test_makedirs_failure__linux(
|
|
620
624
|
self,
|
|
621
625
|
os_ops_descr: OsOpsDescr,
|
|
622
626
|
name_with_surprize: tagNameWithSurprize,
|
|
@@ -633,6 +637,7 @@ class TestOsOpsCommon:
|
|
|
633
637
|
assert isinstance(os_ops, OsOperations)
|
|
634
638
|
|
|
635
639
|
RunConditions.skip_if_windows()
|
|
640
|
+
RunConditions.skip_if_darwin()
|
|
636
641
|
|
|
637
642
|
path = "/root/test_dir-{}-{}".format(
|
|
638
643
|
name_with_surprize.value,
|
|
@@ -651,6 +656,42 @@ class TestOsOpsCommon:
|
|
|
651
656
|
__class__.helper__bug_check__unknown_os_ops_type(os_ops)
|
|
652
657
|
return
|
|
653
658
|
|
|
659
|
+
def test_makedirs_failure__darwin(
|
|
660
|
+
self,
|
|
661
|
+
os_ops_descr: OsOpsDescr,
|
|
662
|
+
name_with_surprize: tagNameWithSurprize,
|
|
663
|
+
):
|
|
664
|
+
"""
|
|
665
|
+
Test makedirs for failure.
|
|
666
|
+
"""
|
|
667
|
+
# Try to create a directory in a read-only location
|
|
668
|
+
assert type(os_ops_descr) is OsOpsDescr
|
|
669
|
+
assert type(name_with_surprize) is __class__.tagNameWithSurprize
|
|
670
|
+
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
671
|
+
|
|
672
|
+
os_ops = os_ops_descr.os_ops
|
|
673
|
+
assert isinstance(os_ops, OsOperations)
|
|
674
|
+
|
|
675
|
+
RunConditions.skip_if_windows()
|
|
676
|
+
RunConditions.skip_if_linux()
|
|
677
|
+
|
|
678
|
+
path = "/root/test_dir-{}-{}".format(
|
|
679
|
+
name_with_surprize.value,
|
|
680
|
+
uuid.uuid4().bytes.hex(),
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Test makedirs
|
|
684
|
+
with pytest.raises(Exception) as x:
|
|
685
|
+
os_ops.makedirs(path)
|
|
686
|
+
|
|
687
|
+
if type(os_ops).__name__ == "LocalOperations":
|
|
688
|
+
assert type(x.value) is OSError
|
|
689
|
+
elif type(os_ops).__name__ == "RemoteOperations":
|
|
690
|
+
assert type(x.value) is ExecUtilException
|
|
691
|
+
else:
|
|
692
|
+
__class__.helper__bug_check__unknown_os_ops_type(os_ops)
|
|
693
|
+
return
|
|
694
|
+
|
|
654
695
|
def test_listdir(
|
|
655
696
|
self,
|
|
656
697
|
os_ops_descr: OsOpsDescr,
|
|
@@ -2559,13 +2600,16 @@ print('b', file=sys.stderr)
|
|
|
2559
2600
|
return
|
|
2560
2601
|
|
|
2561
2602
|
# --------------------------------------------------------------------
|
|
2562
|
-
def
|
|
2603
|
+
def test_get_abs_path__linux(
|
|
2563
2604
|
self,
|
|
2564
2605
|
os_ops_descr: OsOpsDescr,
|
|
2565
2606
|
):
|
|
2566
2607
|
assert type(os_ops_descr) is OsOpsDescr
|
|
2567
2608
|
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
2568
2609
|
|
|
2610
|
+
RunConditions.skip_if_windows()
|
|
2611
|
+
RunConditions.skip_if_darwin()
|
|
2612
|
+
|
|
2569
2613
|
os_ops = os_ops_descr.os_ops
|
|
2570
2614
|
assert isinstance(os_ops, OsOperations)
|
|
2571
2615
|
|
|
@@ -2887,7 +2931,10 @@ print('b', file=sys.stderr)
|
|
|
2887
2931
|
|
|
2888
2932
|
assert actual_child_pid == expected_child_pid
|
|
2889
2933
|
|
|
2890
|
-
child_cmdline =
|
|
2934
|
+
child_cmdline = __class__.helper__wait_for_not_empty_proc_cmdline(
|
|
2935
|
+
childs[0],
|
|
2936
|
+
)
|
|
2937
|
+
assert type(child_cmdline) is list
|
|
2891
2938
|
|
|
2892
2939
|
logging.info("child cmdline: {}".format(
|
|
2893
2940
|
child_cmdline,
|
|
@@ -2970,7 +3017,10 @@ print('b', file=sys.stderr)
|
|
|
2970
3017
|
assert actual_child_pids == expected_child_pids
|
|
2971
3018
|
|
|
2972
3019
|
for i in range(len(childs)):
|
|
2973
|
-
child_cmdline =
|
|
3020
|
+
child_cmdline = __class__.helper__wait_for_not_empty_proc_cmdline(
|
|
3021
|
+
childs[i],
|
|
3022
|
+
)
|
|
3023
|
+
assert type(child_cmdline) is list
|
|
2974
3024
|
|
|
2975
3025
|
logging.info("child cmdline: {}".format(
|
|
2976
3026
|
child_cmdline,
|
|
@@ -3154,7 +3204,7 @@ print('b', file=sys.stderr)
|
|
|
3154
3204
|
assert lines == result_bin
|
|
3155
3205
|
return
|
|
3156
3206
|
|
|
3157
|
-
def
|
|
3207
|
+
def test_prove_environment_isolation__linux(
|
|
3158
3208
|
self,
|
|
3159
3209
|
os_ops_descr: OsOpsDescr,
|
|
3160
3210
|
):
|
|
@@ -3166,6 +3216,9 @@ print('b', file=sys.stderr)
|
|
|
3166
3216
|
os_ops = os_ops_descr.os_ops
|
|
3167
3217
|
assert isinstance(os_ops, OsOperations)
|
|
3168
3218
|
|
|
3219
|
+
RunConditions.skip_if_windows()
|
|
3220
|
+
RunConditions.skip_if_darwin()
|
|
3221
|
+
|
|
3169
3222
|
logging.info("=================== COKANUM PROOF START ===================")
|
|
3170
3223
|
logging.info(f"Target environment type: [{os_ops_descr.sign}]")
|
|
3171
3224
|
|
|
@@ -4666,7 +4719,98 @@ print('b', file=sys.stderr)
|
|
|
4666
4719
|
type(os_ops).__name__,
|
|
4667
4720
|
))
|
|
4668
4721
|
pass
|
|
4669
|
-
|
|
4722
|
+
return
|
|
4723
|
+
|
|
4724
|
+
def test_popen_terminate_mt(
|
|
4725
|
+
self,
|
|
4726
|
+
os_ops_descr: OsOpsDescr,
|
|
4727
|
+
):
|
|
4728
|
+
assert type(os_ops_descr) is OsOpsDescr
|
|
4729
|
+
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
4730
|
+
|
|
4731
|
+
RunConditions.skip_if_windows()
|
|
4732
|
+
os_ops = os_ops_descr.os_ops
|
|
4733
|
+
|
|
4734
|
+
controller: typing.Optional[OsProcessController] = None
|
|
4735
|
+
|
|
4736
|
+
try:
|
|
4737
|
+
N_WORKERS = 100
|
|
4738
|
+
|
|
4739
|
+
logging.info("Process is creating ...")
|
|
4740
|
+
cmd1 = ["sleep", "100"]
|
|
4741
|
+
controller = os_ops.popen(cmd1)
|
|
4742
|
+
assert isinstance(controller, OsProcessController)
|
|
4743
|
+
|
|
4744
|
+
logging.info("Worker are creating ...")
|
|
4745
|
+
threadPool = ThreadPoolExecutor(
|
|
4746
|
+
max_workers=N_WORKERS,
|
|
4747
|
+
thread_name_prefix="ex_creator",
|
|
4748
|
+
)
|
|
4749
|
+
|
|
4750
|
+
class tadWorkerData:
|
|
4751
|
+
future: ThreadFuture
|
|
4752
|
+
|
|
4753
|
+
workerDatas: typing.List[tadWorkerData] = list()
|
|
4754
|
+
|
|
4755
|
+
nErrors = 0
|
|
4756
|
+
|
|
4757
|
+
try:
|
|
4758
|
+
for n in range(N_WORKERS):
|
|
4759
|
+
logging.info("worker #{} is creating ...".format(n))
|
|
4760
|
+
|
|
4761
|
+
workerDatas.append(tadWorkerData())
|
|
4762
|
+
|
|
4763
|
+
workerDatas[n].future = threadPool.submit(
|
|
4764
|
+
controller.terminate,
|
|
4765
|
+
)
|
|
4766
|
+
|
|
4767
|
+
assert workerDatas[n].future is not None
|
|
4768
|
+
|
|
4769
|
+
logging.info("OK. All the workers were created!")
|
|
4770
|
+
except BaseException as e:
|
|
4771
|
+
nErrors += 1
|
|
4772
|
+
logging.error("A problem is detected ({}): {}".format(
|
|
4773
|
+
type(e).__name__,
|
|
4774
|
+
TestServices.ExceptionToHumanText(e),
|
|
4775
|
+
))
|
|
4776
|
+
|
|
4777
|
+
logging.info("Will wait for stop of all the workers...")
|
|
4778
|
+
|
|
4779
|
+
nWorkers = 0
|
|
4780
|
+
|
|
4781
|
+
assert type(workerDatas) is list
|
|
4782
|
+
|
|
4783
|
+
for i in range(len(workerDatas)):
|
|
4784
|
+
worker = workerDatas[i].future
|
|
4785
|
+
|
|
4786
|
+
if worker is None:
|
|
4787
|
+
break
|
|
4788
|
+
|
|
4789
|
+
nWorkers += 1
|
|
4790
|
+
|
|
4791
|
+
assert isinstance(worker, ThreadFuture)
|
|
4792
|
+
|
|
4793
|
+
try:
|
|
4794
|
+
logging.info("Wait for worker #{}".format(i))
|
|
4795
|
+
worker.result()
|
|
4796
|
+
except BaseException as e:
|
|
4797
|
+
nErrors += 1
|
|
4798
|
+
logging.error("Worker #{} finished with error ({}): {}".format(
|
|
4799
|
+
i,
|
|
4800
|
+
type(e).__name__,
|
|
4801
|
+
TestServices.ExceptionToHumanText(e),
|
|
4802
|
+
))
|
|
4803
|
+
continue
|
|
4804
|
+
|
|
4805
|
+
assert nWorkers == N_WORKERS
|
|
4806
|
+
|
|
4807
|
+
if nErrors != 0:
|
|
4808
|
+
raise RuntimeError("Some problems were detected. Please examine the log messages.")
|
|
4809
|
+
|
|
4810
|
+
finally:
|
|
4811
|
+
if controller is not None:
|
|
4812
|
+
controller.close()
|
|
4813
|
+
|
|
4670
4814
|
return
|
|
4671
4815
|
|
|
4672
4816
|
def test_popen_kill(self, os_ops_descr: OsOpsDescr):
|
|
@@ -5130,7 +5274,7 @@ print('b', file=sys.stderr)
|
|
|
5130
5274
|
|
|
5131
5275
|
return
|
|
5132
5276
|
|
|
5133
|
-
def
|
|
5277
|
+
def test_popen_cwd__linux(
|
|
5134
5278
|
self,
|
|
5135
5279
|
os_ops_descr: OsOpsDescr,
|
|
5136
5280
|
):
|
|
@@ -5138,6 +5282,7 @@ print('b', file=sys.stderr)
|
|
|
5138
5282
|
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
5139
5283
|
|
|
5140
5284
|
RunConditions.skip_if_windows()
|
|
5285
|
+
RunConditions.skip_if_darwin()
|
|
5141
5286
|
|
|
5142
5287
|
os_ops = os_ops_descr.os_ops
|
|
5143
5288
|
|
|
@@ -5908,7 +6053,7 @@ print('b', file=sys.stderr)
|
|
|
5908
6053
|
|
|
5909
6054
|
return
|
|
5910
6055
|
|
|
5911
|
-
def
|
|
6056
|
+
def test_run_cwd__linux(
|
|
5912
6057
|
self,
|
|
5913
6058
|
os_ops_descr: OsOpsDescr,
|
|
5914
6059
|
):
|
|
@@ -5916,6 +6061,7 @@ print('b', file=sys.stderr)
|
|
|
5916
6061
|
assert isinstance(os_ops_descr.os_ops, OsOperations)
|
|
5917
6062
|
|
|
5918
6063
|
RunConditions.skip_if_windows()
|
|
6064
|
+
RunConditions.skip_if_darwin()
|
|
5919
6065
|
|
|
5920
6066
|
os_ops = os_ops_descr.os_ops
|
|
5921
6067
|
|
|
@@ -6621,3 +6767,28 @@ print('b', file=sys.stderr)
|
|
|
6621
6767
|
type(os_ops).__name__,
|
|
6622
6768
|
)
|
|
6623
6769
|
raise RuntimeError(err_msg)
|
|
6770
|
+
|
|
6771
|
+
@staticmethod
|
|
6772
|
+
def helper__wait_for_not_empty_proc_cmdline(
|
|
6773
|
+
proc_info,
|
|
6774
|
+
) -> typing.List[str]:
|
|
6775
|
+
assert proc_info is not None
|
|
6776
|
+
|
|
6777
|
+
def LOCAL__not_empty(v: typing.List[str]) -> bool:
|
|
6778
|
+
assert v is not None
|
|
6779
|
+
assert type(v) is list
|
|
6780
|
+
return len(v) != 0
|
|
6781
|
+
|
|
6782
|
+
r = MultiTryCall.exec__until(
|
|
6783
|
+
proc_info.cmdline,
|
|
6784
|
+
LOCAL__not_empty,
|
|
6785
|
+
"wait not empty read proc cmdline",
|
|
6786
|
+
MultiTryCall.tagSETTINGS(
|
|
6787
|
+
10,
|
|
6788
|
+
1,
|
|
6789
|
+
),
|
|
6790
|
+
)
|
|
6791
|
+
|
|
6792
|
+
assert type(r) is list
|
|
6793
|
+
assert len(r) > 0
|
|
6794
|
+
return r
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|