RLTest 0.7.14__py3-none-any.whl
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.
- RLTest/Enterprise/CcsMock.py +183 -0
- RLTest/Enterprise/Dmc.py +47 -0
- RLTest/Enterprise/EnterpriseClusterEnv.py +158 -0
- RLTest/Enterprise/__init__.py +5 -0
- RLTest/Enterprise/binaryrepo.py +71 -0
- RLTest/__init__.py +10 -0
- RLTest/__main__.py +1026 -0
- RLTest/_version.py +8 -0
- RLTest/debuggers.py +81 -0
- RLTest/env.py +646 -0
- RLTest/exists_redis.py +122 -0
- RLTest/loader.py +174 -0
- RLTest/random_port.py +63 -0
- RLTest/redis_cluster.py +241 -0
- RLTest/redis_enterprise_cluster.py +127 -0
- RLTest/redis_std.py +649 -0
- RLTest/utils.py +181 -0
- rltest-0.7.14.dist-info/LICENSE +29 -0
- rltest-0.7.14.dist-info/METADATA +296 -0
- rltest-0.7.14.dist-info/RECORD +22 -0
- rltest-0.7.14.dist-info/WHEEL +4 -0
- rltest-0.7.14.dist-info/entry_points.txt +3 -0
RLTest/__main__.py
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
from __future__ import print_function
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import cmd
|
|
7
|
+
import traceback
|
|
8
|
+
import sys
|
|
9
|
+
import shutil
|
|
10
|
+
import inspect
|
|
11
|
+
import unittest
|
|
12
|
+
import time
|
|
13
|
+
import shlex
|
|
14
|
+
import json
|
|
15
|
+
from multiprocessing import Process, Queue, set_start_method
|
|
16
|
+
|
|
17
|
+
from RLTest.env import Env, TestAssertionFailure, Defaults
|
|
18
|
+
from RLTest.utils import Colors, fix_modules, fix_modulesArgs
|
|
19
|
+
from RLTest.loader import TestLoader
|
|
20
|
+
from RLTest.Enterprise import binaryrepo
|
|
21
|
+
from RLTest import debuggers
|
|
22
|
+
from RLTest._version import __version__
|
|
23
|
+
from contextlib import redirect_stdout
|
|
24
|
+
from progressbar import progressbar, ProgressBar
|
|
25
|
+
import threading
|
|
26
|
+
import signal
|
|
27
|
+
|
|
28
|
+
import warnings
|
|
29
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
30
|
+
|
|
31
|
+
RLTest_CONFIG_FILE_PREFIX = '@'
|
|
32
|
+
RLTest_CONFIG_FILE_NAME = 'config.txt'
|
|
33
|
+
|
|
34
|
+
class CustomArgumentParser(argparse.ArgumentParser):
|
|
35
|
+
def __init__(self, *args, **kwrags):
|
|
36
|
+
super(CustomArgumentParser, self).__init__(*args, **kwrags)
|
|
37
|
+
|
|
38
|
+
def convert_arg_line_to_args(self, line):
|
|
39
|
+
for arg in shlex.split(line):
|
|
40
|
+
if not arg.strip():
|
|
41
|
+
continue
|
|
42
|
+
if arg[0] == '#':
|
|
43
|
+
break
|
|
44
|
+
yield arg
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MyCmd(cmd.Cmd):
|
|
48
|
+
|
|
49
|
+
def __init__(self, env):
|
|
50
|
+
cmd.Cmd.__init__(self)
|
|
51
|
+
self.env = env
|
|
52
|
+
self.prompt = '> '
|
|
53
|
+
try:
|
|
54
|
+
commands_reply = env.cmd('command')
|
|
55
|
+
except Exception:
|
|
56
|
+
return
|
|
57
|
+
commands = [c[0] for c in commands_reply]
|
|
58
|
+
for c in commands:
|
|
59
|
+
if type(c)==bytes:
|
|
60
|
+
c=c.decode('utf-8')
|
|
61
|
+
setattr(MyCmd, 'do_' + c, self._create_functio(c))
|
|
62
|
+
|
|
63
|
+
def _exec(self, command):
|
|
64
|
+
self.env.expect(*command).prettyPrint()
|
|
65
|
+
|
|
66
|
+
def _create_functio(self, command):
|
|
67
|
+
c = command
|
|
68
|
+
return lambda self, x: self._exec([c] + shlex.split(x))
|
|
69
|
+
|
|
70
|
+
def do_exec(self, line):
|
|
71
|
+
self.env.expect(*shlex.split(line)).prettyPrint()
|
|
72
|
+
|
|
73
|
+
def do_print(self, line):
|
|
74
|
+
'''
|
|
75
|
+
print
|
|
76
|
+
'''
|
|
77
|
+
print('print')
|
|
78
|
+
|
|
79
|
+
def do_stop(self, line):
|
|
80
|
+
'''
|
|
81
|
+
print
|
|
82
|
+
'''
|
|
83
|
+
print('BYE BYE')
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def do_cluster_conn(self, line):
|
|
87
|
+
'''
|
|
88
|
+
move to oss-cluster connection
|
|
89
|
+
'''
|
|
90
|
+
if self.env.env == 'oss-cluster':
|
|
91
|
+
self.env.con = self.env.envRunner.getClusterConnection()
|
|
92
|
+
print('moved to cluster connection')
|
|
93
|
+
else:
|
|
94
|
+
print('cluster connection only available on oss-cluster env')
|
|
95
|
+
|
|
96
|
+
def do_normal_conn(self, line):
|
|
97
|
+
'''
|
|
98
|
+
move to normal connection (will connect to the first shard on oss-cluster)
|
|
99
|
+
'''
|
|
100
|
+
self.env.con = self.env.envRunner.getConnection()
|
|
101
|
+
print('moved to normal connection (first shard on oss-cluster)')
|
|
102
|
+
|
|
103
|
+
do_exit = do_stop
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
parser = CustomArgumentParser(fromfile_prefix_chars=RLTest_CONFIG_FILE_PREFIX,
|
|
107
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
108
|
+
description='Test Framework for redis and redis module')
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
'--version', action='store_const', const=True, default=False,
|
|
111
|
+
help='Print RLTest version and exit')
|
|
112
|
+
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
'--module', default=None, action='append',
|
|
115
|
+
help='path to the module file. '
|
|
116
|
+
'You can use `--module` more than once but it imples that you explicitly specify `--module-args` as well. '
|
|
117
|
+
'Notice that on enterprise the file should be a zip file packed with [RAMP](https://github.com/RedisLabs/RAMP).')
|
|
118
|
+
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
'--module-args', default=None, action='append', nargs='*',
|
|
121
|
+
help='arguments to give to the module on loading')
|
|
122
|
+
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
'--env', '-e', default='oss', choices=['oss', 'oss-cluster', 'enterprise', 'enterprise-cluster', 'existing-env', 'cluster_existing-env'],
|
|
125
|
+
help='env on which to run the test')
|
|
126
|
+
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
'-p', '--redis-port', type=int, default=6379,
|
|
129
|
+
help='Redis server port')
|
|
130
|
+
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
'--existing-env-addr', default='localhost:6379',
|
|
133
|
+
help='Address of existing env, relevent only when running with existing-env, cluster_existing-env')
|
|
134
|
+
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
'--shards_ports',
|
|
137
|
+
help=' list of ports, the shards are listening to, relevent only when running with cluster_existing-env')
|
|
138
|
+
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
'--cluster_address',
|
|
141
|
+
help='enterprise cluster ip, relevent only when running with cluster_existing-env')
|
|
142
|
+
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
'--oss_password', default=None,
|
|
145
|
+
help='set redis password, relevant for oss and oss-cluster environment')
|
|
146
|
+
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
'--cluster_node_timeout', default=5000,
|
|
149
|
+
help='sets the node timeout on cluster in milliseconds')
|
|
150
|
+
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
'--cluster_credentials',
|
|
153
|
+
help='enterprise cluster cluster_credentials "username:password", relevent only when running with cluster_existing-env')
|
|
154
|
+
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
'--internal_password', default='',
|
|
157
|
+
help='Give an ability to execute commands on shards directly, relevent only when running with cluster_existing-env')
|
|
158
|
+
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
'--oss-redis-path', default='redis-server',
|
|
161
|
+
help='path to the oss redis binary')
|
|
162
|
+
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
'--enterprise-redis-path', default=os.path.join(binaryrepo.REPO_ROOT, 'opt/redislabs/bin/redis-server'),
|
|
165
|
+
help='path to the enterprise redis binary')
|
|
166
|
+
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
'--redis-config-file', default=None,
|
|
169
|
+
help='path to the redis configuration file')
|
|
170
|
+
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
'--stop-on-failure', action='store_const', const=True, default=False,
|
|
173
|
+
help='stop running on failure')
|
|
174
|
+
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
'-x', '--exit-on-failure', action='store_true',
|
|
177
|
+
help='Stop test execution and exit on first assertion failure')
|
|
178
|
+
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
'--verbose', '-v', action='count', default=0,
|
|
181
|
+
help='print more information about the test')
|
|
182
|
+
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
'--debug', action='store_const', const=True, default=False,
|
|
185
|
+
help='stop before each test allow gdb attachment')
|
|
186
|
+
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
'-t', '--test', metavar='TEST', action='append', help='test to run, in the form of "file:test"')
|
|
189
|
+
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
'-f', '--tests-file', metavar='FILE', action='append', help='file containing test to run, in the form of "file:test"')
|
|
192
|
+
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
'-F', '--failed-tests-file', metavar='FILE', help='destination file for failed tests')
|
|
195
|
+
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
'--env-only', action='store_const', const=True, default=False,
|
|
198
|
+
help='start the env but do not run any tests')
|
|
199
|
+
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
'--clear-logs', action='store_const', const=True, default=False,
|
|
202
|
+
help='deleting the log directory before the execution')
|
|
203
|
+
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
'--log-dir', default='./logs',
|
|
206
|
+
help='directory to write logs to')
|
|
207
|
+
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
'--log-level', default=None, metavar='LEVEL', choices=['debug', 'verbose', 'notice', 'warning'],
|
|
210
|
+
help='sets the server log level')
|
|
211
|
+
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
'--use-slaves', action='store_const', const=True, default=False,
|
|
214
|
+
help='run env with slaves enabled')
|
|
215
|
+
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
'--shards-count', default=1, type=int,
|
|
218
|
+
help='Number shards in bdb')
|
|
219
|
+
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
'--test-timeout', default=0, type=int,
|
|
222
|
+
help='Test timeout, 0 means no timeout.')
|
|
223
|
+
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
'--download-enterprise-binaries', action='store_const', const=True, default=False,
|
|
226
|
+
help='run env with slaves enabled')
|
|
227
|
+
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
'--proxy-binary-path', default=os.path.join(binaryrepo.REPO_ROOT, 'opt/redislabs/bin/dmcproxy'),
|
|
230
|
+
help='dmc proxy binary path')
|
|
231
|
+
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
'--enterprise-lib-path', default=os.path.join(binaryrepo.REPO_ROOT, 'opt/redislabs/lib/'),
|
|
234
|
+
help='path of needed libraries to run enterprise binaries')
|
|
235
|
+
|
|
236
|
+
parser.add_argument(
|
|
237
|
+
'-r', '--env-reuse', action='store_const', const=True, default=False,
|
|
238
|
+
help='reuse exists env, this feature is based on best efforts, if the env can not be reused then it will be taken down.')
|
|
239
|
+
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
'--use-aof', action='store_const', const=True, default=False,
|
|
242
|
+
help='use aof instead of rdb')
|
|
243
|
+
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
'--use-rdb-preamble', action='store_const', const=True, default=True,
|
|
246
|
+
help='use rdb preamble when rewriting aof file')
|
|
247
|
+
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
'--debug-print', action='store_const', const=True, default=False,
|
|
250
|
+
help='print debug messages')
|
|
251
|
+
|
|
252
|
+
parser.add_argument(
|
|
253
|
+
'-V', '--vg', '--use-valgrind', action='store_const', const=True, default=False,
|
|
254
|
+
dest='use_valgrind',
|
|
255
|
+
help='running redis under valgrind (assuming valgrind is install on the machine)')
|
|
256
|
+
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
'--vg-suppressions', default=None, help='path valgrind suppressions file')
|
|
259
|
+
parser.add_argument(
|
|
260
|
+
'--vg-options', default=None, dest='vg_options', help='valgrind [options]')
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
'--vg-no-leakcheck', action='store_true', help="Don't perform a leak check")
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
'--vg-verbose', action='store_true', help="Don't log valgrind output. "
|
|
265
|
+
"Output to screen directly")
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
'--vg-no-fail-on-errors', action='store_true', dest='vg_no_fail_on_errors', help="Dont Fail test when valgrind reported any errors in the run."
|
|
268
|
+
"By default on RLTest the return value from Valgrind will be used to fail the tests."
|
|
269
|
+
"Use this option when you wish to dry-run valgrind but not fail the test on valgrind reported errors."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
'--sanitizer', default=None, help='type of CLang sanitizer (addr|mem)')
|
|
274
|
+
|
|
275
|
+
parser.add_argument(
|
|
276
|
+
'-i', '--interactive-debugger', action='store_const', const=True, default=False,
|
|
277
|
+
help='runs the redis on a debuger (gdb/lldb) interactivly.'
|
|
278
|
+
'debugger interactive mode is only possible on a single process and so unsupported on cluste or with slaves.'
|
|
279
|
+
'it is also not possible to use valgrind on interactive mode.'
|
|
280
|
+
'interactive mode direcly applies: --no-output-catch and --stop-on-failure.'
|
|
281
|
+
'it is also implies that only one test will be run (if --env-only was not specify), an error will be raise otherwise.')
|
|
282
|
+
|
|
283
|
+
parser.add_argument('--debugger', help='Run specified command line as the debugger')
|
|
284
|
+
|
|
285
|
+
parser.add_argument(
|
|
286
|
+
'-s', '--no-output-catch', action='store_const', const=True, default=False,
|
|
287
|
+
help='all output will be written to the stdout, no log files. Implies --no-progress.')
|
|
288
|
+
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
'--no-progress', action='store_const', const=True, default=False,
|
|
291
|
+
help='Do not show progress bar.')
|
|
292
|
+
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
'--verbose-information-on-failure', action='store_const', const=True, default=False,
|
|
295
|
+
help='Print a verbose information on test failure')
|
|
296
|
+
|
|
297
|
+
parser.add_argument(
|
|
298
|
+
'--enable-debug-command', action='store_const', const=True, default=False,
|
|
299
|
+
help='On Redis 7, debug command need to be enabled in order to be used.')
|
|
300
|
+
|
|
301
|
+
parser.add_argument(
|
|
302
|
+
'--enable-protected-configs', action='store_const', const=True, default=False,
|
|
303
|
+
help='On Redis 7, this option needs to be enabled in order to change protected configuration in runtime.')
|
|
304
|
+
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
'--enable-module-command', action='store_const', const=True, default=False,
|
|
307
|
+
help='On Redis 7, this option needs to be enabled in order to use module command (load/unload modules in runtime).')
|
|
308
|
+
|
|
309
|
+
parser.add_argument(
|
|
310
|
+
'--allow-unsafe', action='store_const', const=True, default=False,
|
|
311
|
+
help='On Redis 7, allow the three unsafe modes above (debug and module commands and protected configs)')
|
|
312
|
+
|
|
313
|
+
parser.add_argument('--check-exitcode', help='Check redis process exit code',
|
|
314
|
+
default=False, action='store_true')
|
|
315
|
+
|
|
316
|
+
parser.add_argument('--unix', help='Use Unix domain sockets instead of TCP',
|
|
317
|
+
default=False, action='store_true')
|
|
318
|
+
|
|
319
|
+
parser.add_argument('--randomize-ports',
|
|
320
|
+
help='Randomize Redis listening port assignment rather than'
|
|
321
|
+
'using default port',
|
|
322
|
+
default=False, action='store_true')
|
|
323
|
+
|
|
324
|
+
parser.add_argument('--parallelism', help='Run tests in parallel', default=1, type=int)
|
|
325
|
+
|
|
326
|
+
parser.add_argument(
|
|
327
|
+
'--collect-only', action='store_true',
|
|
328
|
+
help='Collect the tests and exit')
|
|
329
|
+
|
|
330
|
+
parser.add_argument('--tls', help='Enable TLS Support and disable the non-TLS port completely. TLS connections will be available at the default non-TLS ports.',
|
|
331
|
+
default=False, action='store_true')
|
|
332
|
+
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
'--tls-cert-file', default=None, help='/path/to/redis.crt')
|
|
335
|
+
|
|
336
|
+
parser.add_argument(
|
|
337
|
+
'--tls-key-file', default=None, help='/path/to/redis.key')
|
|
338
|
+
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
'--tls-ca-cert-file', default=None, help='/path/to/ca.crt')
|
|
341
|
+
|
|
342
|
+
parser.add_argument(
|
|
343
|
+
'--tls-passphrase', default=None, help='passphrase to use on decript key file')
|
|
344
|
+
|
|
345
|
+
class EnvScopeGuard:
|
|
346
|
+
def __init__(self, runner):
|
|
347
|
+
self.runner = runner
|
|
348
|
+
|
|
349
|
+
def __enter__(self):
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
def __exit__(self, type, value, traceback):
|
|
353
|
+
self.runner.takeEnvDown()
|
|
354
|
+
|
|
355
|
+
class TestTimeLimit(object):
|
|
356
|
+
"""
|
|
357
|
+
A test timeout watcher. The watcher opens thread that sleep for the
|
|
358
|
+
required timeout and then wake up and send SIGUSR1 signal to the main thread
|
|
359
|
+
causing it to enter a timeout phase. When enter a timeout phase, the main thread
|
|
360
|
+
prints its trace and enter a deep sleep. The watcher thread continue collecting
|
|
361
|
+
environment stats and when done kills the processes.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def __init__(self, timeout, timeout_func):
|
|
365
|
+
self.timeout = timeout
|
|
366
|
+
self.timeout_time = time.time() + self.timeout
|
|
367
|
+
self.timeout_func = timeout_func
|
|
368
|
+
self.condition = threading.Condition()
|
|
369
|
+
self.thread = None
|
|
370
|
+
self.is_done = False
|
|
371
|
+
self.trace_printed = False
|
|
372
|
+
|
|
373
|
+
def on_timeout(self, signum, frame):
|
|
374
|
+
for line in traceback.format_stack():
|
|
375
|
+
print(line.strip())
|
|
376
|
+
self.trace_printed = True
|
|
377
|
+
time.sleep(1000) # sleep forever process will be killed soon
|
|
378
|
+
|
|
379
|
+
def watcher_thread(self):
|
|
380
|
+
self.condition.acquire()
|
|
381
|
+
while not self.is_done and self.timeout_time > time.time():
|
|
382
|
+
self.condition.wait(timeout=0.1)
|
|
383
|
+
if not self.is_done:
|
|
384
|
+
print(Colors.Bred('Test Timeout, printing trace.'))
|
|
385
|
+
os.kill(os.getpid(), signal.SIGUSR1)
|
|
386
|
+
while not self.trace_printed:
|
|
387
|
+
time.sleep(0.1)
|
|
388
|
+
try:
|
|
389
|
+
self.timeout_func()
|
|
390
|
+
except Exception as e:
|
|
391
|
+
print(Colors.Bred("Failed on timeout function, %s" % str(e)))
|
|
392
|
+
os._exit(1)
|
|
393
|
+
|
|
394
|
+
def reset(self):
|
|
395
|
+
self.timeout_time = time.time() + self.timeout
|
|
396
|
+
|
|
397
|
+
def __enter__(self):
|
|
398
|
+
if self.timeout == 0:
|
|
399
|
+
return self
|
|
400
|
+
signal.signal(signal.SIGUSR1, self.on_timeout)
|
|
401
|
+
self.thread = threading.Thread(target=self.watcher_thread)
|
|
402
|
+
self.thread.start()
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
406
|
+
if self.timeout == 0:
|
|
407
|
+
return
|
|
408
|
+
self.condition.acquire()
|
|
409
|
+
self.is_done = True
|
|
410
|
+
self.condition.notify(1)
|
|
411
|
+
self.condition.release()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class RLTest:
|
|
415
|
+
def __init__(self):
|
|
416
|
+
# adding the current path to sys.path for test import puspused
|
|
417
|
+
sys.path.append(os.getcwd())
|
|
418
|
+
|
|
419
|
+
configFilePath = './%s' % RLTest_CONFIG_FILE_NAME
|
|
420
|
+
if os.path.exists(configFilePath):
|
|
421
|
+
args = ['%s%s' % (RLTest_CONFIG_FILE_PREFIX, RLTest_CONFIG_FILE_NAME)] + sys.argv[1:]
|
|
422
|
+
else:
|
|
423
|
+
args = sys.argv[1:]
|
|
424
|
+
self.args = parser.parse_args(args=args)
|
|
425
|
+
|
|
426
|
+
if self.args.version:
|
|
427
|
+
print(Colors.Green('RLTest version {}'.format(__version__)))
|
|
428
|
+
sys.exit(0)
|
|
429
|
+
|
|
430
|
+
if self.args.redis_port not in range(1, pow(2, 16)):
|
|
431
|
+
print(Colors.Bred(f'requested port {self.args.redis_port} is not valid'))
|
|
432
|
+
sys.exit(1)
|
|
433
|
+
|
|
434
|
+
if self.args.interactive_debugger:
|
|
435
|
+
if self.args.env != 'oss' and not (self.args.env == 'oss-cluster' and Defaults.num_shards == 1) and self.args.env != 'enterprise':
|
|
436
|
+
print(Colors.Bred('interactive debugger can only be used on non cluster env'))
|
|
437
|
+
sys.exit(1)
|
|
438
|
+
if self.args.use_valgrind:
|
|
439
|
+
print(Colors.Bred('can not use valgrind with interactive debugger'))
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
if self.args.use_slaves:
|
|
442
|
+
print(Colors.Bred('can not use slaves with interactive debugger'))
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
|
|
445
|
+
self.args.no_output_catch = True
|
|
446
|
+
self.args.stop_on_failure = True
|
|
447
|
+
|
|
448
|
+
if self.args.download_enterprise_binaries:
|
|
449
|
+
br = binaryrepo.BinaryRepository()
|
|
450
|
+
br.download_binaries()
|
|
451
|
+
|
|
452
|
+
if self.args.clear_logs:
|
|
453
|
+
if os.path.exists(self.args.log_dir):
|
|
454
|
+
try:
|
|
455
|
+
shutil.rmtree(self.args.log_dir)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
print(e, file=sys.stderr)
|
|
458
|
+
|
|
459
|
+
debugger = None
|
|
460
|
+
if self.args.debugger:
|
|
461
|
+
if self.args.env.endswith('existing-env'):
|
|
462
|
+
print(Colors.Bred('can not use debug with existing-env'))
|
|
463
|
+
sys.exit(1)
|
|
464
|
+
debuggers.set_interactive_debugger(self.args.debugger)
|
|
465
|
+
self.args.interactive_debugger = True
|
|
466
|
+
if self.args.use_valgrind:
|
|
467
|
+
if self.args.env.endswith('existing-env'):
|
|
468
|
+
print(Colors.Bred('can not use valgrind with existing-env'))
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
if self.args.vg_options is None:
|
|
471
|
+
self.args.vg_options = os.getenv('VG_OPTIONS', '--leak-check=full --errors-for-leak-kinds=definite')
|
|
472
|
+
vg_debugger = debuggers.Valgrind(options=self.args.vg_options,
|
|
473
|
+
suppressions=self.args.vg_suppressions,
|
|
474
|
+
fail_on_errors=not(self.args.vg_no_fail_on_errors),
|
|
475
|
+
leakcheck=not(self.args.vg_no_leakcheck)
|
|
476
|
+
)
|
|
477
|
+
if self.args.vg_no_leakcheck:
|
|
478
|
+
vg_debugger.leakcheck = False
|
|
479
|
+
if self.args.no_output_catch or self.args.vg_verbose:
|
|
480
|
+
vg_debugger.verbose = True
|
|
481
|
+
debugger = vg_debugger
|
|
482
|
+
elif self.args.interactive_debugger:
|
|
483
|
+
debugger = debuggers.default_interactive_debugger
|
|
484
|
+
|
|
485
|
+
sanitizer = None
|
|
486
|
+
if self.args.sanitizer:
|
|
487
|
+
sanitizer = self.args.sanitizer
|
|
488
|
+
|
|
489
|
+
if self.args.env.endswith('existing-env'):
|
|
490
|
+
# when running on existing env we always reuse it
|
|
491
|
+
self.args.env_reuse = True
|
|
492
|
+
|
|
493
|
+
# unless None, they must match in length
|
|
494
|
+
if self.args.module_args:
|
|
495
|
+
len_module_args = len(self.args.module_args)
|
|
496
|
+
modules = self.args.module
|
|
497
|
+
if type(modules) == list:
|
|
498
|
+
if (len(modules) != len_module_args):
|
|
499
|
+
print(Colors.Bred('Using `--module` multiple time implies that you specify the `--module-args` in the the same number'))
|
|
500
|
+
sys.exit(1)
|
|
501
|
+
|
|
502
|
+
if self.args.no_output_catch and self.args.parallelism > 1:
|
|
503
|
+
print(Colors.Bred('--no-output-catch can not be combined with --parallelism.'))
|
|
504
|
+
sys.exit(1)
|
|
505
|
+
|
|
506
|
+
Defaults.module = fix_modules(self.args.module)
|
|
507
|
+
Defaults.module_args = fix_modulesArgs(Defaults.module, self.args.module_args)
|
|
508
|
+
Defaults.env = self.args.env
|
|
509
|
+
Defaults.binary = self.args.oss_redis_path
|
|
510
|
+
Defaults.verbose = self.args.verbose
|
|
511
|
+
Defaults.logdir = self.args.log_dir
|
|
512
|
+
Defaults.loglevel = self.args.log_level
|
|
513
|
+
Defaults.use_slaves = self.args.use_slaves
|
|
514
|
+
Defaults.num_shards = self.args.shards_count
|
|
515
|
+
Defaults.shards_ports = self.args.shards_ports.split(',') if self.args.shards_ports is not None else None
|
|
516
|
+
Defaults.cluster_address = self.args.cluster_address
|
|
517
|
+
Defaults.cluster_credentials = self.args.cluster_credentials
|
|
518
|
+
Defaults.internal_password = self.args.internal_password
|
|
519
|
+
Defaults.proxy_binary = self.args.proxy_binary_path
|
|
520
|
+
Defaults.re_binary = self.args.enterprise_redis_path
|
|
521
|
+
Defaults.re_libdir = self.args.enterprise_lib_path
|
|
522
|
+
Defaults.use_aof = self.args.use_aof
|
|
523
|
+
Defaults.debug_pause = self.args.debug
|
|
524
|
+
Defaults.debug_print = self.args.debug_print
|
|
525
|
+
Defaults.no_capture_output = self.args.no_output_catch
|
|
526
|
+
Defaults.print_verbose_information_on_failure = self.args.verbose_information_on_failure
|
|
527
|
+
Defaults.debugger = debugger
|
|
528
|
+
Defaults.sanitizer = sanitizer
|
|
529
|
+
Defaults.exit_on_failure = self.args.exit_on_failure
|
|
530
|
+
Defaults.port = self.args.redis_port
|
|
531
|
+
Defaults.external_addr = self.args.existing_env_addr
|
|
532
|
+
Defaults.use_unix = self.args.unix
|
|
533
|
+
Defaults.randomize_ports = self.args.randomize_ports
|
|
534
|
+
Defaults.use_TLS = self.args.tls
|
|
535
|
+
Defaults.tls_cert_file = self.args.tls_cert_file
|
|
536
|
+
Defaults.tls_key_file = self.args.tls_key_file
|
|
537
|
+
Defaults.tls_ca_cert_file = self.args.tls_ca_cert_file
|
|
538
|
+
Defaults.tls_passphrase = self.args.tls_passphrase
|
|
539
|
+
Defaults.oss_password = self.args.oss_password
|
|
540
|
+
Defaults.cluster_node_timeout = self.args.cluster_node_timeout
|
|
541
|
+
Defaults.enable_debug_command = True if self.args.allow_unsafe else self.args.enable_debug_command
|
|
542
|
+
Defaults.enable_protected_configs = True if self.args.allow_unsafe else self.args.enable_protected_configs
|
|
543
|
+
Defaults.enable_module_command = True if self.args.allow_unsafe else self.args.enable_module_command
|
|
544
|
+
Defaults.redis_config_file = self.args.redis_config_file
|
|
545
|
+
|
|
546
|
+
if Defaults.use_unix and Defaults.use_slaves:
|
|
547
|
+
raise Exception('Cannot use unix sockets with slaves')
|
|
548
|
+
|
|
549
|
+
if Defaults.env == 'enterprise-cluster' and Defaults.redis_config_file is not None:
|
|
550
|
+
raise Exception('Redis configuration file is not supported with enterprise-cluster env')
|
|
551
|
+
|
|
552
|
+
self.tests = []
|
|
553
|
+
self.testsFailed = {}
|
|
554
|
+
self.currEnv = None
|
|
555
|
+
self.loader = TestLoader()
|
|
556
|
+
if self.args.test is not None:
|
|
557
|
+
self.loader.load_spec(self.args.test)
|
|
558
|
+
if self.args.tests_file is not None:
|
|
559
|
+
for fname in self.args.tests_file:
|
|
560
|
+
try:
|
|
561
|
+
with open(fname, 'r') as file:
|
|
562
|
+
for line in file.readlines():
|
|
563
|
+
line = line.strip()
|
|
564
|
+
if line.startswith('#') or line == "":
|
|
565
|
+
continue
|
|
566
|
+
try:
|
|
567
|
+
self.loader.load_spec(line)
|
|
568
|
+
except:
|
|
569
|
+
print(Colors.Red('Invalid test {TEST} in file {FILE}'.format(TEST=line, FILE=fname)))
|
|
570
|
+
except:
|
|
571
|
+
print(Colors.Red('Test file {} not found'.format(fname)))
|
|
572
|
+
if self.args.test is None and self.args.tests_file is None:
|
|
573
|
+
self.loader.scan_dir(os.getcwd())
|
|
574
|
+
|
|
575
|
+
if self.args.collect_only:
|
|
576
|
+
self.loader.print_tests()
|
|
577
|
+
sys.exit(0)
|
|
578
|
+
if self.args.use_valgrind or self.args.check_exitcode:
|
|
579
|
+
self.require_clean_exit = True
|
|
580
|
+
else:
|
|
581
|
+
self.require_clean_exit = False
|
|
582
|
+
|
|
583
|
+
self.parallelism = self.args.parallelism
|
|
584
|
+
|
|
585
|
+
def _convertArgsType(self):
|
|
586
|
+
pass
|
|
587
|
+
|
|
588
|
+
def stopEnvWithSegFault(self):
|
|
589
|
+
if not self.currEnv:
|
|
590
|
+
return
|
|
591
|
+
self.currEnv.stopEnvWithSegFault()
|
|
592
|
+
|
|
593
|
+
def takeEnvDown(self, fullShutDown=False):
|
|
594
|
+
if not self.currEnv:
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
needShutdown = True
|
|
598
|
+
if self.args.env_reuse and not fullShutDown:
|
|
599
|
+
try:
|
|
600
|
+
self.currEnv.flush()
|
|
601
|
+
needShutdown = False
|
|
602
|
+
except Exception as e:
|
|
603
|
+
self.currEnv.stop()
|
|
604
|
+
self.handleFailure(exception=e, testname='[env dtor]',
|
|
605
|
+
env=self.currEnv)
|
|
606
|
+
|
|
607
|
+
if needShutdown:
|
|
608
|
+
if self.currEnv.isUp():
|
|
609
|
+
try:
|
|
610
|
+
self.currEnv.flush()
|
|
611
|
+
flush_ok = True
|
|
612
|
+
except:
|
|
613
|
+
flush_ok = False
|
|
614
|
+
self.currEnv.stop()
|
|
615
|
+
if self.require_clean_exit and self.currEnv and (not self.currEnv.checkExitCode() or not flush_ok):
|
|
616
|
+
print(Colors.Bred('\tRedis did not exit cleanly'))
|
|
617
|
+
self.addFailure(self.currEnv.testName, ['redis process failure'])
|
|
618
|
+
if self.args.check_exitcode:
|
|
619
|
+
raise Exception('Process exited dirty')
|
|
620
|
+
self.currEnv = None
|
|
621
|
+
|
|
622
|
+
def printException(self, err):
|
|
623
|
+
msg = 'Unhandled exception: {}'.format(err)
|
|
624
|
+
print('\t' + Colors.Bred(msg))
|
|
625
|
+
traceback.print_exc(file=sys.stdout)
|
|
626
|
+
|
|
627
|
+
def addFailuresFromEnv(self, name, env):
|
|
628
|
+
"""
|
|
629
|
+
Extract the list of failures from the given test Env
|
|
630
|
+
:param name: The name of the test that failed
|
|
631
|
+
:param env: The Environment which contains the failures
|
|
632
|
+
"""
|
|
633
|
+
if not env:
|
|
634
|
+
self.addFailure(name, ['<unknown (environment destroyed)>'])
|
|
635
|
+
else:
|
|
636
|
+
self.addFailure(name, failures=env.assertionFailedSummary)
|
|
637
|
+
|
|
638
|
+
def addFailure(self, name, failures=None):
|
|
639
|
+
"""
|
|
640
|
+
Adds a list of failures to the report
|
|
641
|
+
:param name: The name of the test that has failures
|
|
642
|
+
:param failures: A string or of strings describing the individual failures
|
|
643
|
+
"""
|
|
644
|
+
if failures and not isinstance(failures, (list, tuple)):
|
|
645
|
+
failures = [failures]
|
|
646
|
+
if not failures:
|
|
647
|
+
failures = []
|
|
648
|
+
self.testsFailed.setdefault(name, []).extend(failures)
|
|
649
|
+
|
|
650
|
+
def getFailedTestsCount(self):
|
|
651
|
+
return len(self.testsFailed)
|
|
652
|
+
|
|
653
|
+
def handleFailure(self, testFullName=None, exception=None, prefix='', testname=None, env=None, error_msg=None):
|
|
654
|
+
"""
|
|
655
|
+
Failure omni-function.
|
|
656
|
+
|
|
657
|
+
This function handles failures given a set of input parameters.
|
|
658
|
+
At least one of these must not be empty
|
|
659
|
+
:param exception: The exception to report, of any
|
|
660
|
+
:param prefix: The prefix to use for logging.
|
|
661
|
+
This is usually the test name
|
|
662
|
+
:param testname: The test name, use for recording the failures
|
|
663
|
+
:param env: The environment, used for extracting failed assertions
|
|
664
|
+
"""
|
|
665
|
+
if not testname and env:
|
|
666
|
+
testname = env.testName
|
|
667
|
+
elif not testname:
|
|
668
|
+
if prefix:
|
|
669
|
+
testname = prefix
|
|
670
|
+
else:
|
|
671
|
+
testname = '<unknown>'
|
|
672
|
+
|
|
673
|
+
if exception:
|
|
674
|
+
self.printError(testFullName if testFullName is not None else '')
|
|
675
|
+
self.printException(exception)
|
|
676
|
+
else:
|
|
677
|
+
self.printFail(testFullName if testFullName is not None else '')
|
|
678
|
+
|
|
679
|
+
if env:
|
|
680
|
+
self.addFailuresFromEnv(testname, env)
|
|
681
|
+
elif exception:
|
|
682
|
+
self.addFailure(testname, str(exception))
|
|
683
|
+
elif error_msg:
|
|
684
|
+
self.addFailure(testname, str(error_msg))
|
|
685
|
+
else:
|
|
686
|
+
self.addFailure(testname, '<No exception or environment>')
|
|
687
|
+
|
|
688
|
+
def _runTest(self, test, numberOfAssertionFailed=0, prefix='', before=lambda x=None: None, after=lambda x=None: None):
|
|
689
|
+
test.initialize()
|
|
690
|
+
|
|
691
|
+
msgPrefix = test.name
|
|
692
|
+
|
|
693
|
+
testFullName = prefix + test.name
|
|
694
|
+
|
|
695
|
+
if not test.is_method:
|
|
696
|
+
Defaults.curr_test_name = testFullName
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
# Python < 3.11
|
|
700
|
+
test_args = inspect.getargspec(test.target).args
|
|
701
|
+
except:
|
|
702
|
+
test_args = inspect.getfullargspec(test.target).args
|
|
703
|
+
|
|
704
|
+
if len(test_args) > 0 and not test.is_method:
|
|
705
|
+
try:
|
|
706
|
+
# env = Env(testName=test.name)
|
|
707
|
+
env = Defaults.env_factory(testName=test.name)
|
|
708
|
+
except Exception as e:
|
|
709
|
+
self.handleFailure(testFullName=testFullName, exception=e, prefix=msgPrefix, testname=test.name)
|
|
710
|
+
return 0
|
|
711
|
+
|
|
712
|
+
fn = lambda: test.target(env)
|
|
713
|
+
before_func = lambda: before(env)
|
|
714
|
+
after_func = lambda: after(env)
|
|
715
|
+
else:
|
|
716
|
+
fn = test.target
|
|
717
|
+
before_func = before
|
|
718
|
+
after_func = after
|
|
719
|
+
|
|
720
|
+
hasException = False
|
|
721
|
+
try:
|
|
722
|
+
before_func()
|
|
723
|
+
fn()
|
|
724
|
+
passed = True
|
|
725
|
+
except unittest.SkipTest:
|
|
726
|
+
self.printSkip(testFullName)
|
|
727
|
+
return 0
|
|
728
|
+
except TestAssertionFailure:
|
|
729
|
+
if self.args.exit_on_failure:
|
|
730
|
+
self.takeEnvDown(fullShutDown=True)
|
|
731
|
+
|
|
732
|
+
# Don't fall-through
|
|
733
|
+
raise
|
|
734
|
+
except Exception as err:
|
|
735
|
+
if self.args.exit_on_failure:
|
|
736
|
+
self.takeEnvDown(fullShutDown=True)
|
|
737
|
+
after_func = lambda x=None: None
|
|
738
|
+
raise
|
|
739
|
+
|
|
740
|
+
self.handleFailure(testFullName=testFullName, exception=err, prefix=msgPrefix,
|
|
741
|
+
testname=test.name, env=self.currEnv)
|
|
742
|
+
hasException = True
|
|
743
|
+
passed = False
|
|
744
|
+
finally:
|
|
745
|
+
after_func()
|
|
746
|
+
|
|
747
|
+
numFailed = 0
|
|
748
|
+
if self.currEnv:
|
|
749
|
+
numFailed = self.currEnv.getNumberOfFailedAssertion()
|
|
750
|
+
if numFailed > numberOfAssertionFailed:
|
|
751
|
+
self.handleFailure(testFullName=testFullName, prefix=msgPrefix,
|
|
752
|
+
testname=test.name, env=self.currEnv)
|
|
753
|
+
passed = False
|
|
754
|
+
elif not hasException:
|
|
755
|
+
self.addFailure(test.name, '<Environment destroyed>')
|
|
756
|
+
passed = False
|
|
757
|
+
|
|
758
|
+
# Handle debugger, if needed
|
|
759
|
+
if self.args.stop_on_failure and not passed:
|
|
760
|
+
if self.args.interactive_debugger:
|
|
761
|
+
while self.currEnv.isUp():
|
|
762
|
+
time.sleep(1)
|
|
763
|
+
input('press any button to move to the next test')
|
|
764
|
+
|
|
765
|
+
if passed:
|
|
766
|
+
self.printPass(testFullName)
|
|
767
|
+
|
|
768
|
+
if hasException:
|
|
769
|
+
numFailed += 1 # exception should be counted as failure
|
|
770
|
+
return numFailed
|
|
771
|
+
|
|
772
|
+
def printSkip(self, name):
|
|
773
|
+
print('%s:\r\n\t%s' % (Colors.Cyan(name), Colors.Green('[SKIP]')))
|
|
774
|
+
|
|
775
|
+
def printFail(self, name):
|
|
776
|
+
print('%s:\r\n\t%s' % (Colors.Cyan(name), Colors.Bred('[FAIL]')))
|
|
777
|
+
|
|
778
|
+
def printError(self, name):
|
|
779
|
+
print('%s:\r\n\t%s' % (Colors.Cyan(name), Colors.Bred('[ERROR]')))
|
|
780
|
+
|
|
781
|
+
def printPass(self, name):
|
|
782
|
+
print('%s:\r\n\t%s' % (Colors.Cyan(name), Colors.Green('[PASS]')))
|
|
783
|
+
|
|
784
|
+
def envScopeGuard(self):
|
|
785
|
+
return EnvScopeGuard(self)
|
|
786
|
+
|
|
787
|
+
def killEnvWithSegFault(self):
|
|
788
|
+
if self.currEnv and Defaults.print_verbose_information_on_failure:
|
|
789
|
+
try:
|
|
790
|
+
verboseInfo = {}
|
|
791
|
+
# It is not safe to get the information before dispose, Redis might be stack and will not reply.
|
|
792
|
+
# It will cause us to hand here forever. We will only get the information after dispose, this should be
|
|
793
|
+
# enough as we kill Redis with segfualt which means that it should provide use with all the required details.
|
|
794
|
+
self.stopEnvWithSegFault()
|
|
795
|
+
verboseInfo['after_dispose'] = self.currEnv.getInformationAfterDispose()
|
|
796
|
+
self.currEnv.debugPrint(json.dumps(verboseInfo, indent=2).replace('\\n', '\n'), force=True)
|
|
797
|
+
except Exception as e:
|
|
798
|
+
print('Failed %s' % str(e))
|
|
799
|
+
else:
|
|
800
|
+
self.stopEnvWithSegFault()
|
|
801
|
+
|
|
802
|
+
def run_single_test(self, test, on_timeout_func):
|
|
803
|
+
done = 0
|
|
804
|
+
with TestTimeLimit(self.args.test_timeout, on_timeout_func) as timeout_handler:
|
|
805
|
+
with self.envScopeGuard():
|
|
806
|
+
if test.is_class:
|
|
807
|
+
test.initialize()
|
|
808
|
+
|
|
809
|
+
Defaults.curr_test_name = test.name
|
|
810
|
+
try:
|
|
811
|
+
obj = test.create_instance()
|
|
812
|
+
|
|
813
|
+
except unittest.SkipTest:
|
|
814
|
+
self.printSkip(test.name)
|
|
815
|
+
return 0
|
|
816
|
+
|
|
817
|
+
except Exception as e:
|
|
818
|
+
self.printException(e)
|
|
819
|
+
self.addFailure(test.name + " [__init__]")
|
|
820
|
+
return 0
|
|
821
|
+
|
|
822
|
+
failures = 0
|
|
823
|
+
before = getattr(obj, 'setUp', lambda x=None: None)
|
|
824
|
+
after = getattr(obj, 'tearDown', lambda x=None: None)
|
|
825
|
+
for subtest in test.get_functions(obj):
|
|
826
|
+
timeout_handler.reset()
|
|
827
|
+
failures += self._runTest(subtest, prefix='\t',
|
|
828
|
+
numberOfAssertionFailed=failures,
|
|
829
|
+
before=before, after=after)
|
|
830
|
+
done += 1
|
|
831
|
+
|
|
832
|
+
else:
|
|
833
|
+
failures = self._runTest(test)
|
|
834
|
+
done += 1
|
|
835
|
+
|
|
836
|
+
verboseInfo = {}
|
|
837
|
+
if failures > 0 and Defaults.print_verbose_information_on_failure:
|
|
838
|
+
lastEnv = self.currEnv
|
|
839
|
+
verboseInfo['before_dispose'] = lastEnv.getInformationBeforeDispose()
|
|
840
|
+
|
|
841
|
+
# here the env is down so lets collect more info and print it
|
|
842
|
+
if failures > 0 and Defaults.print_verbose_information_on_failure:
|
|
843
|
+
verboseInfo['after_dispose'] = lastEnv.getInformationAfterDispose()
|
|
844
|
+
lastEnv.debugPrint(json.dumps(verboseInfo, indent=2).replace('\\n', '\n'), force=True)
|
|
845
|
+
return done
|
|
846
|
+
|
|
847
|
+
def print_failures(self):
|
|
848
|
+
for group, failures in self.testsFailed.items():
|
|
849
|
+
print('\t' + Colors.Bold(group))
|
|
850
|
+
if not failures:
|
|
851
|
+
print('\t\t' + Colors.Bred('Exception raised during test execution. See logs'))
|
|
852
|
+
for failure in failures:
|
|
853
|
+
print('\t\t' + failure)
|
|
854
|
+
|
|
855
|
+
def disable_progress_bar(self):
|
|
856
|
+
return self.args.no_output_catch or self.args.no_progress or not sys.stdout.isatty()
|
|
857
|
+
|
|
858
|
+
def progressbar(self, num_elements):
|
|
859
|
+
bar = None
|
|
860
|
+
if not self.disable_progress_bar():
|
|
861
|
+
bar = ProgressBar(max_value=num_elements, redirect_stdout=True)
|
|
862
|
+
for i in range(num_elements):
|
|
863
|
+
bar.update(i)
|
|
864
|
+
yield i
|
|
865
|
+
bar.update(num_elements)
|
|
866
|
+
else:
|
|
867
|
+
yield from range(num_elements)
|
|
868
|
+
|
|
869
|
+
def execute(self):
|
|
870
|
+
Env.RTestInstance = self
|
|
871
|
+
if self.args.env_only:
|
|
872
|
+
Defaults.verbose = 2
|
|
873
|
+
# env = Env(testName='manual test env')
|
|
874
|
+
env = Defaults.env_factory(testName='manual test env')
|
|
875
|
+
if self.args.interactive_debugger:
|
|
876
|
+
while env.isUp():
|
|
877
|
+
time.sleep(1)
|
|
878
|
+
else:
|
|
879
|
+
cmd = MyCmd(env)
|
|
880
|
+
cmd.cmdloop()
|
|
881
|
+
env.stop()
|
|
882
|
+
return
|
|
883
|
+
done = 0
|
|
884
|
+
startTime = time.time()
|
|
885
|
+
if self.args.interactive_debugger and len(self.loader.tests) != 1:
|
|
886
|
+
print(self.tests)
|
|
887
|
+
print(Colors.Bred('only one test can be run on interactive-debugger use -t'))
|
|
888
|
+
sys.exit(1)
|
|
889
|
+
|
|
890
|
+
jobs = Queue()
|
|
891
|
+
n_jobs = 0
|
|
892
|
+
for test in self.loader:
|
|
893
|
+
jobs.put(test, block=False)
|
|
894
|
+
n_jobs += 1
|
|
895
|
+
|
|
896
|
+
def run_jobs_main_thread(jobs):
|
|
897
|
+
nonlocal done
|
|
898
|
+
bar = self.progressbar(n_jobs)
|
|
899
|
+
for _ in bar:
|
|
900
|
+
try:
|
|
901
|
+
test = jobs.get(timeout=0.1)
|
|
902
|
+
except Exception as e:
|
|
903
|
+
break
|
|
904
|
+
|
|
905
|
+
def on_timeout():
|
|
906
|
+
nonlocal done
|
|
907
|
+
try:
|
|
908
|
+
done += 1
|
|
909
|
+
self.killEnvWithSegFault()
|
|
910
|
+
self.handleFailure(testFullName=test.name, testname=test.name, error_msg=Colors.Bred('Test timeout'))
|
|
911
|
+
self.print_failures()
|
|
912
|
+
finally:
|
|
913
|
+
# we must update the bar anyway to see output
|
|
914
|
+
bar.__next__()
|
|
915
|
+
|
|
916
|
+
done += self.run_single_test(test, on_timeout)
|
|
917
|
+
|
|
918
|
+
self.takeEnvDown(fullShutDown=True)
|
|
919
|
+
|
|
920
|
+
def run_jobs(jobs, results, summary, port):
|
|
921
|
+
Defaults.port = port
|
|
922
|
+
done = 0
|
|
923
|
+
while True:
|
|
924
|
+
try:
|
|
925
|
+
test = jobs.get(timeout=0.1)
|
|
926
|
+
except Exception as e:
|
|
927
|
+
break
|
|
928
|
+
|
|
929
|
+
output = io.StringIO()
|
|
930
|
+
with redirect_stdout(output):
|
|
931
|
+
def on_timeout():
|
|
932
|
+
nonlocal done
|
|
933
|
+
try:
|
|
934
|
+
done += 1
|
|
935
|
+
self.killEnvWithSegFault()
|
|
936
|
+
self.handleFailure(testFullName=test.name, testname=test.name, error_msg=Colors.Bred('Test timeout'))
|
|
937
|
+
except Exception as e:
|
|
938
|
+
self.handleFailure(testFullName=test.name, testname=test.name, error_msg=Colors.Bred('Exception on timeout function %s' % str(e)))
|
|
939
|
+
finally:
|
|
940
|
+
results.put({'test_name': test.name, "output": output.getvalue()}, block=False)
|
|
941
|
+
summary.put({'done': done, 'failures': self.testsFailed}, block=False)
|
|
942
|
+
# After we return the processes will be killed, so we must make sure the queues are drained properly.
|
|
943
|
+
results.close()
|
|
944
|
+
summary.close()
|
|
945
|
+
summary.join_thread()
|
|
946
|
+
results.join_thread()
|
|
947
|
+
done += self.run_single_test(test, on_timeout)
|
|
948
|
+
|
|
949
|
+
results.put({'test_name': test.name, "output": output.getvalue()}, block=False)
|
|
950
|
+
|
|
951
|
+
self.takeEnvDown(fullShutDown=True)
|
|
952
|
+
|
|
953
|
+
# serialized the results back
|
|
954
|
+
summary.put({'done': done, 'failures': self.testsFailed}, block=False)
|
|
955
|
+
|
|
956
|
+
results = Queue()
|
|
957
|
+
summary = Queue()
|
|
958
|
+
if self.parallelism == 1:
|
|
959
|
+
run_jobs_main_thread(jobs)
|
|
960
|
+
else :
|
|
961
|
+
processes = []
|
|
962
|
+
currPort = Defaults.port
|
|
963
|
+
for i in range(self.parallelism):
|
|
964
|
+
p = Process(target=run_jobs, args=(jobs,results,summary,currPort))
|
|
965
|
+
currPort += 30 # safe distance for cluster and replicas
|
|
966
|
+
processes.append(p)
|
|
967
|
+
p.start()
|
|
968
|
+
for _ in self.progressbar(n_jobs):
|
|
969
|
+
while True:
|
|
970
|
+
# check if we have some lives executors
|
|
971
|
+
has_live_processor = False
|
|
972
|
+
for p in processes:
|
|
973
|
+
if p.is_alive():
|
|
974
|
+
has_live_processor = True
|
|
975
|
+
break
|
|
976
|
+
try:
|
|
977
|
+
res = results.get(timeout=1)
|
|
978
|
+
break
|
|
979
|
+
except Exception as e:
|
|
980
|
+
if not has_live_processor:
|
|
981
|
+
raise Exception('Failed to get job result and no more processors is alive')
|
|
982
|
+
output = res['output']
|
|
983
|
+
print('%s' % output, end="")
|
|
984
|
+
|
|
985
|
+
for p in processes:
|
|
986
|
+
p.join()
|
|
987
|
+
|
|
988
|
+
# join results
|
|
989
|
+
while True:
|
|
990
|
+
try:
|
|
991
|
+
res = summary.get(timeout=1)
|
|
992
|
+
except Exception as e:
|
|
993
|
+
break
|
|
994
|
+
done += res['done']
|
|
995
|
+
self.testsFailed.update(res['failures'])
|
|
996
|
+
|
|
997
|
+
endTime = time.time()
|
|
998
|
+
|
|
999
|
+
print(Colors.Bold('\nTest Took: %d sec' % (endTime - startTime)))
|
|
1000
|
+
print(Colors.Bold('Total Tests Run: %d, Total Tests Failed: %d, Total Tests Passed: %d' % (done, self.getFailedTestsCount(), done - self.getFailedTestsCount())))
|
|
1001
|
+
if self.testsFailed:
|
|
1002
|
+
if self.args.failed_tests_file:
|
|
1003
|
+
with open(self.args.failed_tests_file, 'w') as file:
|
|
1004
|
+
for test, _ in self.testsFailed:
|
|
1005
|
+
file.write(test.split(' ')[0] + "\n")
|
|
1006
|
+
|
|
1007
|
+
print(Colors.Bold('Failed Tests Summary:'))
|
|
1008
|
+
self.print_failures()
|
|
1009
|
+
sys.exit(1)
|
|
1010
|
+
else:
|
|
1011
|
+
if self.args.failed_tests_file:
|
|
1012
|
+
with open(self.args.failed_tests_file, 'w') as file:
|
|
1013
|
+
pass
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def main():
|
|
1017
|
+
# Avoid "UnicodeEncodeError: 'ascii' codec can't encode character" errors
|
|
1018
|
+
sys.stdout = io.open(sys.stdout.fileno(), 'w', encoding='utf8')
|
|
1019
|
+
sys.stderr = io.open(sys.stderr.fileno(), 'w', encoding='utf8')
|
|
1020
|
+
# Set multiprocessing start method to fork, we have unserializable objects in the env
|
|
1021
|
+
set_start_method('fork')
|
|
1022
|
+
RLTest().execute()
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
if __name__ == '__main__':
|
|
1026
|
+
main()
|