RLTest 0.7.24__tar.gz → 0.7.26__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.
- {rltest-0.7.24 → rltest-0.7.26}/PKG-INFO +1 -1
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/__init__.py +3 -1
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/__main__.py +79 -43
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/env.py +6 -1
- rltest-0.7.26/RLTest/env_spec.py +141 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/loader.py +20 -6
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/redis_std.py +4 -3
- {rltest-0.7.24 → rltest-0.7.26}/pyproject.toml +1 -1
- {rltest-0.7.24 → rltest-0.7.26}/LICENSE +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/README.md +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/Enterprise/CcsMock.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/Enterprise/Dmc.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/Enterprise/EnterpriseClusterEnv.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/Enterprise/__init__.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/Enterprise/binaryrepo.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/_version.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/debuggers.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/exists_redis.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/random_port.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/redis_cluster.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/redis_enterprise_cluster.py +0 -0
- {rltest-0.7.24 → rltest-0.7.26}/RLTest/utils.py +0 -0
|
@@ -712,14 +712,23 @@ class RLTest:
|
|
|
712
712
|
except:
|
|
713
713
|
test_args = inspect.getfullargspec(test.target).args
|
|
714
714
|
|
|
715
|
-
|
|
715
|
+
# Only function-style tests receive ``env`` as a parameter. Class
|
|
716
|
+
# methods access env via ``self`` (the class stashes it in
|
|
717
|
+
# ``__init__``); declaring ``env`` on a method will surface as a
|
|
718
|
+
# natural ``TypeError`` through the failure path below.
|
|
719
|
+
env = None
|
|
720
|
+
if test_args and not test.is_method:
|
|
721
|
+
spec = getattr(test, 'env_spec', None)
|
|
716
722
|
try:
|
|
717
|
-
|
|
718
|
-
|
|
723
|
+
if spec is not None:
|
|
724
|
+
env = Defaults.env_factory(testName=test.name, **spec)
|
|
725
|
+
else:
|
|
726
|
+
env = Defaults.env_factory(testName=test.name)
|
|
719
727
|
except Exception as e:
|
|
720
728
|
self.handleFailure(testFullName=testFullName, exception=e, prefix=msgPrefix, testname=test.name)
|
|
721
729
|
return 0
|
|
722
730
|
|
|
731
|
+
if env is not None:
|
|
723
732
|
fn = lambda: test.target(env)
|
|
724
733
|
before_func = lambda: before(env)
|
|
725
734
|
after_func = lambda: after(env)
|
|
@@ -832,7 +841,17 @@ class RLTest:
|
|
|
832
841
|
|
|
833
842
|
Defaults.curr_test_name = test.name
|
|
834
843
|
try:
|
|
835
|
-
|
|
844
|
+
# If the class declared an env_spec, build the env up
|
|
845
|
+
# front and pass it to ``__init__``. What the class
|
|
846
|
+
# does with it after that is its own business — the
|
|
847
|
+
# runner never reads attributes off the instance.
|
|
848
|
+
# Test methods access env through ``self``.
|
|
849
|
+
spec = getattr(test, 'env_spec', None)
|
|
850
|
+
if spec is not None:
|
|
851
|
+
env = Defaults.env_factory(testName=test.name, **spec)
|
|
852
|
+
obj = test.create_instance(env)
|
|
853
|
+
else:
|
|
854
|
+
obj = test.create_instance()
|
|
836
855
|
|
|
837
856
|
except unittest.SkipTest:
|
|
838
857
|
self.printSkip(test.name)
|
|
@@ -941,44 +960,60 @@ class RLTest:
|
|
|
941
960
|
|
|
942
961
|
self.takeEnvDown(fullShutDown=True)
|
|
943
962
|
|
|
944
|
-
def run_jobs(jobs, results,
|
|
963
|
+
def run_jobs(jobs, results, port):
|
|
945
964
|
Defaults.port = port
|
|
946
|
-
done = 0
|
|
947
965
|
while True:
|
|
948
966
|
try:
|
|
949
967
|
test = jobs.get(timeout=0.1)
|
|
950
968
|
except Exception as e:
|
|
951
969
|
break
|
|
952
970
|
|
|
971
|
+
# Reset per-test: addFailure() in this worker writes into this
|
|
972
|
+
# dict, which is shipped as-is. The coordinator owns the
|
|
973
|
+
# cumulative testsFailed; the worker keeps no state across tests.
|
|
974
|
+
self.testsFailed = {}
|
|
953
975
|
output = io.StringIO()
|
|
954
976
|
with redirect_stdout(output):
|
|
955
977
|
def on_timeout():
|
|
956
|
-
nonlocal done
|
|
957
978
|
try:
|
|
958
|
-
done += 1
|
|
959
979
|
self.killEnvWithSegFault()
|
|
960
980
|
self.handleFailure(testFullName=test.name, testname=test.name, error_msg=Colors.Bred('Test timeout'))
|
|
961
981
|
except Exception as e:
|
|
962
982
|
self.handleFailure(testFullName=test.name, testname=test.name, error_msg=Colors.Bred('Exception on timeout function %s' % str(e)))
|
|
963
983
|
finally:
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
#
|
|
984
|
+
# The watcher thread calls os._exit(1) right after
|
|
985
|
+
# this returns, bypassing Python finalization and
|
|
986
|
+
# the normal post-loop shutdown put. Ship both the
|
|
987
|
+
# per-test result and the shutdown sentinel here so
|
|
988
|
+
# the coordinator's bounded count of
|
|
989
|
+
# n_jobs + parallelism remains accurate. close() +
|
|
990
|
+
# join_thread() flushes both puts to the pipe first.
|
|
991
|
+
results.put({'test_name': test.name, 'output': output.getvalue(),
|
|
992
|
+
'done': 1, 'failures': self.testsFailed,
|
|
993
|
+
'shutdown': False}, block=False)
|
|
994
|
+
results.put({'test_name': '<worker shutdown>', 'output': '',
|
|
995
|
+
'done': 0, 'failures': {},
|
|
996
|
+
'shutdown': True}, block=False)
|
|
967
997
|
results.close()
|
|
968
|
-
summary.close()
|
|
969
|
-
summary.join_thread()
|
|
970
998
|
results.join_thread()
|
|
971
|
-
done += self.run_single_test(test, on_timeout)
|
|
972
999
|
|
|
973
|
-
|
|
1000
|
+
done_delta = self.run_single_test(test, on_timeout)
|
|
974
1001
|
|
|
975
|
-
|
|
1002
|
+
results.put({'test_name': test.name, 'output': output.getvalue(),
|
|
1003
|
+
'done': done_delta, 'failures': self.testsFailed,
|
|
1004
|
+
'shutdown': False}, block=False)
|
|
976
1005
|
|
|
977
|
-
#
|
|
978
|
-
|
|
1006
|
+
# Always ship one shutdown message per worker so the coordinator
|
|
1007
|
+
# reads a known total of n_jobs + parallelism messages. Captures
|
|
1008
|
+
# failures raised during final shutdown (e.g. "redis did not exit
|
|
1009
|
+
# cleanly" when env_reuse=True).
|
|
1010
|
+
self.testsFailed = {}
|
|
1011
|
+
self.takeEnvDown(fullShutDown=True)
|
|
1012
|
+
results.put({'test_name': '<worker shutdown>', 'output': '',
|
|
1013
|
+
'done': 0, 'failures': self.testsFailed,
|
|
1014
|
+
'shutdown': True}, block=False)
|
|
979
1015
|
|
|
980
1016
|
results = Queue()
|
|
981
|
-
summary = Queue()
|
|
982
1017
|
# Open group for all tests at the start (parallel execution)
|
|
983
1018
|
self._openGitHubActionsTestsGroup()
|
|
984
1019
|
if self.parallelism == 1:
|
|
@@ -987,39 +1022,40 @@ class RLTest:
|
|
|
987
1022
|
processes = []
|
|
988
1023
|
currPort = Defaults.port
|
|
989
1024
|
for i in range(self.parallelism):
|
|
990
|
-
p = Process(target=run_jobs, args=(jobs,results,
|
|
1025
|
+
p = Process(target=run_jobs, args=(jobs,results,currPort))
|
|
991
1026
|
currPort += 30 # safe distance for cluster and replicas
|
|
992
1027
|
processes.append(p)
|
|
993
1028
|
p.start()
|
|
994
|
-
|
|
1029
|
+
# Workers send exactly n_jobs per-test messages plus one shutdown
|
|
1030
|
+
# message each, for a known total. The single shared queue does
|
|
1031
|
+
# not preserve per-worker ordering, so a fast worker's shutdown
|
|
1032
|
+
# may arrive before a slow worker's last test. We read every
|
|
1033
|
+
# message in one bounded loop and tick the progressbar only on
|
|
1034
|
+
# per-test ones. The has_live_processor guard turns a worker
|
|
1035
|
+
# crash before it ships its shutdown message into a clean error
|
|
1036
|
+
# instead of an indefinite hang.
|
|
1037
|
+
def _get_result():
|
|
995
1038
|
while True:
|
|
996
|
-
# check if we have some lives executors
|
|
997
|
-
has_live_processor = False
|
|
998
|
-
for p in processes:
|
|
999
|
-
if p.is_alive():
|
|
1000
|
-
has_live_processor = True
|
|
1001
|
-
break
|
|
1002
1039
|
try:
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1040
|
+
return results.get(timeout=1)
|
|
1041
|
+
except Exception:
|
|
1042
|
+
if not any(p.is_alive() for p in processes):
|
|
1043
|
+
raise Exception('Failed to get job result and no more processors are alive')
|
|
1044
|
+
|
|
1045
|
+
bar_iter = iter(self.progressbar(n_jobs))
|
|
1046
|
+
for _ in range(n_jobs + self.parallelism):
|
|
1047
|
+
res = _get_result()
|
|
1048
|
+
if res['output']:
|
|
1049
|
+
print('%s' % res['output'], end="")
|
|
1050
|
+
done += res['done']
|
|
1051
|
+
self.testsFailed.update(res['failures'])
|
|
1052
|
+
if not res['shutdown']:
|
|
1053
|
+
next(bar_iter, None)
|
|
1054
|
+
next(bar_iter, None) # finalize bar.update(n_jobs)
|
|
1010
1055
|
|
|
1011
1056
|
for p in processes:
|
|
1012
1057
|
p.join()
|
|
1013
1058
|
|
|
1014
|
-
# join results
|
|
1015
|
-
while True:
|
|
1016
|
-
try:
|
|
1017
|
-
res = summary.get(timeout=1)
|
|
1018
|
-
except Exception as e:
|
|
1019
|
-
break
|
|
1020
|
-
done += res['done']
|
|
1021
|
-
self.testsFailed.update(res['failures'])
|
|
1022
|
-
|
|
1023
1059
|
endTime = time.time()
|
|
1024
1060
|
|
|
1025
1061
|
# Close group after all tests complete (parallel execution)
|
|
@@ -153,6 +153,7 @@ class Defaults:
|
|
|
153
153
|
protocol = 2
|
|
154
154
|
redis_config_file = None
|
|
155
155
|
dualTLS = False
|
|
156
|
+
startup_grace_secs = 0.1
|
|
156
157
|
|
|
157
158
|
def getKwargs(self):
|
|
158
159
|
kwargs = {
|
|
@@ -201,7 +202,8 @@ class Env:
|
|
|
201
202
|
tlsCaCertFile=None, tlsPassphrase=None, logDir=None, redisBinaryPath=None, dmcBinaryPath=None,
|
|
202
203
|
redisEnterpriseBinaryPath=None, noDefaultModuleArgs=False, clusterNodeTimeout = None,
|
|
203
204
|
freshEnv=False, enableDebugCommand=None, enableModuleCommand=None, enableProtectedConfigs=None, protocol=None,
|
|
204
|
-
terminateRetries=None, terminateRetrySecs=None, redisConfigFile=None, dualTLS=False
|
|
205
|
+
terminateRetries=None, terminateRetrySecs=None, redisConfigFile=None, dualTLS=False,
|
|
206
|
+
startupGraceSecs=None):
|
|
205
207
|
|
|
206
208
|
self.testName = testName if testName else Defaults.curr_test_name
|
|
207
209
|
if self.testName is None:
|
|
@@ -255,6 +257,8 @@ class Env:
|
|
|
255
257
|
|
|
256
258
|
self.dualTLS = dualTLS if dualTLS else Defaults.dualTLS
|
|
257
259
|
|
|
260
|
+
self.startupGraceSecs = startupGraceSecs if startupGraceSecs is not None else Defaults.startup_grace_secs
|
|
261
|
+
|
|
258
262
|
if not freshEnv and Env.RTestInstance and Env.RTestInstance.currEnv and self.compareEnvs(Env.RTestInstance.currEnv):
|
|
259
263
|
self.envRunner = Env.RTestInstance.currEnv.envRunner
|
|
260
264
|
else:
|
|
@@ -372,6 +376,7 @@ class Env:
|
|
|
372
376
|
'terminateRetrySecs': self.terminateRetrySecs,
|
|
373
377
|
'redisConfigFile': self.redisConfigFile,
|
|
374
378
|
'dualTLS': self.dualTLS,
|
|
379
|
+
'startupGraceSecs': self.startupGraceSecs,
|
|
375
380
|
}
|
|
376
381
|
return kwargs
|
|
377
382
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Declarative environment requirements for RLTest tests.
|
|
2
|
+
|
|
3
|
+
A test can declare the Env parameters it needs *before* it runs, so the runner
|
|
4
|
+
can construct the env on its behalf and inject it as a parameter. Two benefits:
|
|
5
|
+
|
|
6
|
+
1. Single source of truth: the declared spec is exactly the shape of the env
|
|
7
|
+
that gets injected, eliminating drift between a "what env I need" hint and
|
|
8
|
+
the in-body ``Env(...)`` call.
|
|
9
|
+
2. Future schedulers can read each test's spec at discovery time and route
|
|
10
|
+
same-spec tests adjacently to maximize Redis-instance reuse via
|
|
11
|
+
``Env.compareEnvs`` (env.py:191).
|
|
12
|
+
|
|
13
|
+
A spec is declared by applying ``@env_spec(...)`` to a test function or to a
|
|
14
|
+
test class. A class-level spec applies to every method of that class;
|
|
15
|
+
method-level decoration is not supported (see ``env_spec`` below).
|
|
16
|
+
|
|
17
|
+
For file-wide defaults, define a local dict and spread it into each
|
|
18
|
+
decoration::
|
|
19
|
+
|
|
20
|
+
BASE = dict(moduleArgs='DEFAULT_DIALECT 2')
|
|
21
|
+
|
|
22
|
+
@env_spec(**BASE, shardsCount=3)
|
|
23
|
+
def test_cluster(env):
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
How env is delivered:
|
|
27
|
+
|
|
28
|
+
- Function tests receive the constructed env as a parameter (``def
|
|
29
|
+
test_x(env):``).
|
|
30
|
+
- Class tests receive it once, through ``__init__(self, env)``, and are
|
|
31
|
+
responsible for stashing it for their methods to use. By convention that
|
|
32
|
+
attribute is ``self.env``, but the runner does not enforce the name — it
|
|
33
|
+
hands env to ``__init__`` and then forgets about it. Test methods **never**
|
|
34
|
+
receive env as a parameter; they reach it through ``self``.
|
|
35
|
+
|
|
36
|
+
Example::
|
|
37
|
+
|
|
38
|
+
@env_spec(shardsCount=3)
|
|
39
|
+
def test_cluster(env):
|
|
40
|
+
env.expect('FT.SEARCH', 'idx', '*').noError()
|
|
41
|
+
|
|
42
|
+
@env_spec(moduleArgs='WORKERS 1')
|
|
43
|
+
class TestWorkers:
|
|
44
|
+
def __init__(self, env):
|
|
45
|
+
self.env = env # required: methods access env via ``self``
|
|
46
|
+
|
|
47
|
+
def test_x(self):
|
|
48
|
+
self.env.expect(...)
|
|
49
|
+
"""
|
|
50
|
+
import inspect
|
|
51
|
+
|
|
52
|
+
from RLTest.env import Env
|
|
53
|
+
|
|
54
|
+
_SPEC_KEYS = frozenset(Env.EnvCompareParams)
|
|
55
|
+
_ATTR = '_rltest_env_spec'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _looks_like_class_method(target):
|
|
59
|
+
"""Heuristic: is ``target`` a function defined inside a class body?
|
|
60
|
+
|
|
61
|
+
At decoration time the function isn't bound to the class yet, but Python
|
|
62
|
+
has already populated ``__qualname__`` with the enclosing scope. Examples:
|
|
63
|
+
|
|
64
|
+
f -> top-level function (not a method)
|
|
65
|
+
outer.<locals>.g -> nested function (not a method)
|
|
66
|
+
C.m -> class method
|
|
67
|
+
outer.<locals>.C.m -> class defined inside a function; still a method
|
|
68
|
+
|
|
69
|
+
The rule: take whatever follows the last ``<locals>.`` (the path *inside*
|
|
70
|
+
the innermost enclosing function scope, or the whole qualname if there's
|
|
71
|
+
no ``<locals>``). If that trailing segment contains a dot, the target is
|
|
72
|
+
qualified by a class name and is therefore a method.
|
|
73
|
+
"""
|
|
74
|
+
qn = getattr(target, '__qualname__', '')
|
|
75
|
+
if not qn:
|
|
76
|
+
return False
|
|
77
|
+
trailing = qn.rsplit('<locals>.', 1)[-1]
|
|
78
|
+
return '.' in trailing
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def env_spec(**kwargs):
|
|
82
|
+
"""Declare the env requirements of a test function or test class.
|
|
83
|
+
|
|
84
|
+
Allowed keys are the entries of ``Env.EnvCompareParams``; unknown keys
|
|
85
|
+
raise ``ValueError`` at decoration time so typos can't silently disable
|
|
86
|
+
spec-driven behaviour.
|
|
87
|
+
|
|
88
|
+
Applying ``@env_spec`` to a method inside a class is rejected: class tests
|
|
89
|
+
share a single env across all their methods (that's the whole point of a
|
|
90
|
+
class test). If one method needs a different env, lift it out into a
|
|
91
|
+
standalone function or its own class. To declare a class-wide spec,
|
|
92
|
+
decorate the class itself.
|
|
93
|
+
"""
|
|
94
|
+
unknown = set(kwargs) - _SPEC_KEYS
|
|
95
|
+
if unknown:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"unknown env_spec keys: {}; allowed keys are: {}".format(
|
|
98
|
+
sorted(unknown), sorted(_SPEC_KEYS)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
spec = dict(kwargs)
|
|
103
|
+
|
|
104
|
+
def deco(target):
|
|
105
|
+
if inspect.isfunction(target) and _looks_like_class_method(target):
|
|
106
|
+
raise TypeError(
|
|
107
|
+
"@env_spec is not supported on class methods (got {}). "
|
|
108
|
+
"Class tests share one env across all methods; decorate the "
|
|
109
|
+
"class itself, or move the test out of the class.".format(
|
|
110
|
+
target.__qualname__
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
setattr(target, _ATTR, spec)
|
|
114
|
+
return target
|
|
115
|
+
|
|
116
|
+
return deco
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_spec(target):
|
|
120
|
+
"""Return the declared env spec for ``target``, or ``None`` if none was
|
|
121
|
+
declared via ``@env_spec(...)``.
|
|
122
|
+
|
|
123
|
+
``target`` is a test function or test class. The ``None`` return is the
|
|
124
|
+
sentinel callers use for "no declared spec — fall back to default env
|
|
125
|
+
construction."
|
|
126
|
+
"""
|
|
127
|
+
spec = getattr(target, _ATTR, None)
|
|
128
|
+
return dict(spec) if spec is not None else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def spec_key(spec):
|
|
132
|
+
"""Canonical hashable key for spec equivalence.
|
|
133
|
+
|
|
134
|
+
Two tests with the same ``spec_key`` produce envs that satisfy
|
|
135
|
+
``Env.compareEnvs``, so they're eligible to share a Redis instance via
|
|
136
|
+
RLTest's opportunistic-reuse path (env.py:262). Future schedulers can use
|
|
137
|
+
this as a grouping key.
|
|
138
|
+
"""
|
|
139
|
+
if spec is None:
|
|
140
|
+
return ()
|
|
141
|
+
return tuple(sorted(spec.items()))
|
|
@@ -3,18 +3,22 @@ import os
|
|
|
3
3
|
import sys
|
|
4
4
|
import importlib.util
|
|
5
5
|
import inspect
|
|
6
|
+
from RLTest.env_spec import resolve_spec
|
|
6
7
|
from RLTest.utils import Colors
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TestFunction(object):
|
|
10
11
|
is_class = False
|
|
11
12
|
|
|
12
|
-
def __init__(self, filename, symbol, modulename):
|
|
13
|
+
def __init__(self, filename, symbol, modulename, env_spec=None):
|
|
13
14
|
self.filename = filename
|
|
14
15
|
self.symbol = symbol
|
|
15
16
|
self.modulename = modulename
|
|
16
17
|
self.is_method = False
|
|
17
18
|
self.name = '{}:{}'.format(self.modulename, symbol)
|
|
19
|
+
# Resolved env requirements (dict or None). None means "no declared
|
|
20
|
+
# spec — fall back to legacy behaviour".
|
|
21
|
+
self.env_spec = env_spec
|
|
18
22
|
|
|
19
23
|
def initialize(self):
|
|
20
24
|
module_spec = importlib.util.spec_from_file_location(self.modulename, self.filename)
|
|
@@ -30,10 +34,12 @@ class TestFunction(object):
|
|
|
30
34
|
class TestMethod(object):
|
|
31
35
|
is_class = False
|
|
32
36
|
|
|
33
|
-
def __init__(self, obj, name):
|
|
37
|
+
def __init__(self, obj, name, env_spec=None):
|
|
34
38
|
self.target = obj
|
|
35
39
|
self.name = name
|
|
36
40
|
self.is_method = True
|
|
41
|
+
# Methods inherit their class's env_spec; they cannot override it.
|
|
42
|
+
self.env_spec = env_spec
|
|
37
43
|
|
|
38
44
|
def initialize(self):
|
|
39
45
|
pass
|
|
@@ -44,12 +50,13 @@ class TestMethod(object):
|
|
|
44
50
|
class TestClass(object):
|
|
45
51
|
is_class = True
|
|
46
52
|
|
|
47
|
-
def __init__(self, filename, symbol, modulename, functions):
|
|
53
|
+
def __init__(self, filename, symbol, modulename, functions, env_spec=None):
|
|
48
54
|
self.filename = filename
|
|
49
55
|
self.symbol = symbol
|
|
50
56
|
self.modulename = modulename
|
|
51
57
|
self.functions = functions
|
|
52
58
|
self.name = '{}:{}'.format(self.modulename, symbol)
|
|
59
|
+
self.env_spec = env_spec
|
|
53
60
|
|
|
54
61
|
def initialize(self):
|
|
55
62
|
module_spec = importlib.util.spec_from_file_location(self.modulename, self.filename)
|
|
@@ -70,7 +77,8 @@ class TestClass(object):
|
|
|
70
77
|
if not callable(bound):
|
|
71
78
|
continue
|
|
72
79
|
fns.append(TestMethod(bound,
|
|
73
|
-
name='{}:{}.{}'.format(self.modulename, self.clsname, mname)
|
|
80
|
+
name='{}:{}.{}'.format(self.modulename, self.clsname, mname),
|
|
81
|
+
env_spec=self.env_spec))
|
|
74
82
|
return fns
|
|
75
83
|
|
|
76
84
|
|
|
@@ -129,9 +137,15 @@ class TestLoader(object):
|
|
|
129
137
|
if inspect.isclass(obj):
|
|
130
138
|
methnames = [mname for mname in dir(obj)
|
|
131
139
|
if self.filter_method(mname, subfilter)]
|
|
132
|
-
|
|
140
|
+
spec = resolve_spec(obj)
|
|
141
|
+
self.tests.append(
|
|
142
|
+
TestClass(filename, symbol, module_name, methnames, env_spec=spec)
|
|
143
|
+
)
|
|
133
144
|
elif inspect.isfunction(obj):
|
|
134
|
-
|
|
145
|
+
spec = resolve_spec(obj)
|
|
146
|
+
self.tests.append(
|
|
147
|
+
TestFunction(filename, symbol, module_name, env_spec=spec)
|
|
148
|
+
)
|
|
135
149
|
except OSError as e:
|
|
136
150
|
print(Colors.Red("Can't access file %s." % filename))
|
|
137
151
|
raise e
|
|
@@ -23,7 +23,7 @@ class StandardEnv(object):
|
|
|
23
23
|
useAof=False, useRdbPreamble=True, debugger=None, sanitizer=None, noCatch=False, noLog=False, unix=False, verbose=False, useTLS=False,
|
|
24
24
|
tlsCertFile=None, tlsKeyFile=None, tlsCaCertFile=None, clusterNodeTimeout=None, tlsPassphrase=None, enableDebugCommand=False, protocol=2,
|
|
25
25
|
terminateRetries=None, terminateRetrySecs=None, enableProtectedConfigs=False, enableModuleCommand=False, loglevel=None,
|
|
26
|
-
redisConfigFile=None, dualTLS=False
|
|
26
|
+
redisConfigFile=None, dualTLS=False, startupGraceSecs=0.1
|
|
27
27
|
):
|
|
28
28
|
self.uuid = uuid.uuid4().hex
|
|
29
29
|
self.redisBinaryPath = os.path.expanduser(redisBinaryPath) if redisBinaryPath.startswith(
|
|
@@ -72,6 +72,7 @@ class StandardEnv(object):
|
|
|
72
72
|
self.terminateRetrySecs = terminateRetrySecs
|
|
73
73
|
self.redisConfigFile = redisConfigFile
|
|
74
74
|
self.dualTLS = dualTLS
|
|
75
|
+
self.startupGraceSecs = startupGraceSecs
|
|
75
76
|
|
|
76
77
|
if port > 0:
|
|
77
78
|
self.port = port
|
|
@@ -381,7 +382,7 @@ class StandardEnv(object):
|
|
|
381
382
|
if masters and self.masterProcess is None:
|
|
382
383
|
self.masterProcess = subprocess.Popen(args=self.masterCmdArgs, env=self.masterOSEnv, cwd=self.dbDirPath,
|
|
383
384
|
**options)
|
|
384
|
-
time.sleep(
|
|
385
|
+
time.sleep(self.startupGraceSecs)
|
|
385
386
|
if self._isAlive(self.masterProcess):
|
|
386
387
|
con = self.getConnection()
|
|
387
388
|
self.waitForRedisToStart(con, self.masterProcess)
|
|
@@ -392,7 +393,7 @@ class StandardEnv(object):
|
|
|
392
393
|
print(Colors.Green("Redis slave command: " + ' '.join(self.slaveCmdArgs)))
|
|
393
394
|
self.slaveProcess = subprocess.Popen(args=self.slaveCmdArgs, env=self.slaveOSEnv, cwd=self.dbDirPath,
|
|
394
395
|
**options)
|
|
395
|
-
time.sleep(
|
|
396
|
+
time.sleep(self.startupGraceSecs)
|
|
396
397
|
if self._isAlive(self.slaveProcess):
|
|
397
398
|
con = self.getSlaveConnection()
|
|
398
399
|
self.waitForRedisToStart(con, self.slaveProcess)
|
|
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
|