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/__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()