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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RLTest
3
- Version: 0.7.25
3
+ Version: 0.7.26
4
4
  Summary: Redis Modules Test Framework, allow to run tests on redis and modules on a variety of environments
5
5
  License: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -1,10 +1,12 @@
1
1
  from RLTest.env import Env, Defaults
2
+ from RLTest.env_spec import env_spec
2
3
  from RLTest.redis_std import StandardEnv
3
4
  from ._version import __version__
4
5
 
5
6
  __all__ = [
6
7
  'Defaults',
7
8
  'Env',
8
- 'StandardEnv'
9
+ 'StandardEnv',
10
+ 'env_spec',
9
11
  ]
10
12
 
@@ -712,14 +712,23 @@ class RLTest:
712
712
  except:
713
713
  test_args = inspect.getfullargspec(test.target).args
714
714
 
715
- if len(test_args) > 0 and not test.is_method:
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
- # env = Env(testName=test.name)
718
- env = Defaults.env_factory(testName=test.name)
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
- obj = test.create_instance()
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
- self.tests.append(TestClass(filename, symbol, module_name, methnames))
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
- self.tests.append(TestFunction(filename, symbol, module_name))
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "RLTest"
3
- version = "0.7.25"
3
+ version = "0.7.26"
4
4
  description="Redis Modules Test Framework, allow to run tests on redis and modules on a variety of environments"
5
5
  authors = ["Redis, Inc. <oss@redis.com>"]
6
6
  license = "BSD-3-Clause"
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