RLTest 0.7.25__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.25 → rltest-0.7.26}/PKG-INFO +1 -1
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/__init__.py +3 -1
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/__main__.py +23 -4
- rltest-0.7.26/RLTest/env_spec.py +141 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/loader.py +20 -6
- {rltest-0.7.25 → rltest-0.7.26}/pyproject.toml +1 -1
- {rltest-0.7.25 → rltest-0.7.26}/LICENSE +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/README.md +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/Enterprise/CcsMock.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/Enterprise/Dmc.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/Enterprise/EnterpriseClusterEnv.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/Enterprise/__init__.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/Enterprise/binaryrepo.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/_version.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/debuggers.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/env.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/exists_redis.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/random_port.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/redis_cluster.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/redis_enterprise_cluster.py +0 -0
- {rltest-0.7.25 → rltest-0.7.26}/RLTest/redis_std.py +0 -0
- {rltest-0.7.25 → 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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|