1 | """Attribute selector plugin. |
---|
2 | |
---|
3 | Oftentimes when testing you will want to select tests based on |
---|
4 | criteria rather then simply by filename. For example, you might want |
---|
5 | to run all tests except for the slow ones. You can do this with the |
---|
6 | Attribute selector plugin by setting attributes on your test methods. |
---|
7 | Here is an example: |
---|
8 | |
---|
9 | .. code-block:: python |
---|
10 | |
---|
11 | def test_big_download(): |
---|
12 | import urllib |
---|
13 | # commence slowness... |
---|
14 | |
---|
15 | test_big_download.slow = 1 |
---|
16 | |
---|
17 | Once you've assigned an attribute ``slow = 1`` you can exclude that |
---|
18 | test and all other tests having the slow attribute by running :: |
---|
19 | |
---|
20 | $ nosetests -a '!slow' |
---|
21 | |
---|
22 | There is also a decorator available for you that will set attributes. |
---|
23 | Here's how to set ``slow=1`` like above with the decorator: |
---|
24 | |
---|
25 | .. code-block:: python |
---|
26 | |
---|
27 | from nose.plugins.attrib import attr |
---|
28 | @attr('slow') |
---|
29 | def test_big_download(): |
---|
30 | import urllib |
---|
31 | # commence slowness... |
---|
32 | |
---|
33 | And here's how to set an attribute with a specific value: |
---|
34 | |
---|
35 | .. code-block:: python |
---|
36 | |
---|
37 | from nose.plugins.attrib import attr |
---|
38 | @attr(speed='slow') |
---|
39 | def test_big_download(): |
---|
40 | import urllib |
---|
41 | # commence slowness... |
---|
42 | |
---|
43 | This test could be run with :: |
---|
44 | |
---|
45 | $ nosetests -a speed=slow |
---|
46 | |
---|
47 | Below is a reference to the different syntaxes available. |
---|
48 | |
---|
49 | Simple syntax |
---|
50 | ------------- |
---|
51 | |
---|
52 | Examples of using the ``-a`` and ``--attr`` options: |
---|
53 | |
---|
54 | * ``nosetests -a status=stable`` |
---|
55 | Only runs tests with attribute "status" having value "stable" |
---|
56 | |
---|
57 | * ``nosetests -a priority=2,status=stable`` |
---|
58 | Runs tests having both attributes and values |
---|
59 | |
---|
60 | * ``nosetests -a priority=2 -a slow`` |
---|
61 | Runs tests that match either attribute |
---|
62 | |
---|
63 | * ``nosetests -a tags=http`` |
---|
64 | If a test's ``tags`` attribute was a list and it contained the value |
---|
65 | ``http`` then it would be run |
---|
66 | |
---|
67 | * ``nosetests -a slow`` |
---|
68 | Runs tests with the attribute ``slow`` if its value does not equal False |
---|
69 | (False, [], "", etc...) |
---|
70 | |
---|
71 | * ``nosetests -a '!slow'`` |
---|
72 | Runs tests that do NOT have the attribute ``slow`` or have a ``slow`` |
---|
73 | attribute that is equal to False |
---|
74 | **NOTE**: |
---|
75 | if your shell (like bash) interprets '!' as a special character make sure to |
---|
76 | put single quotes around it. |
---|
77 | |
---|
78 | Expression Evaluation |
---|
79 | --------------------- |
---|
80 | |
---|
81 | Examples using the ``-A`` and ``--eval-attr`` options: |
---|
82 | |
---|
83 | * ``nosetests -A "not slow"`` |
---|
84 | Evaluates the Python expression "not slow" and runs the test if True |
---|
85 | |
---|
86 | * ``nosetests -A "(priority > 5) and not slow"`` |
---|
87 | Evaluates a complex Python expression and runs the test if True |
---|
88 | |
---|
89 | """ |
---|
90 | import logging |
---|
91 | import os |
---|
92 | import sys |
---|
93 | from inspect import isfunction |
---|
94 | from nose.plugins.base import Plugin |
---|
95 | from nose.util import tolist |
---|
96 | |
---|
97 | log = logging.getLogger('nose.plugins.attrib') |
---|
98 | compat_24 = sys.version_info >= (2, 4) |
---|
99 | |
---|
100 | def attr(*args, **kwargs): |
---|
101 | """Decorator that adds attributes to objects |
---|
102 | for use with the Attribute (-a) plugin. |
---|
103 | """ |
---|
104 | def wrap(func): |
---|
105 | for name in args: |
---|
106 | # these are just True flags: |
---|
107 | setattr(func, name, 1) |
---|
108 | func.__dict__.update(kwargs) |
---|
109 | return func |
---|
110 | return wrap |
---|
111 | |
---|
112 | class ContextHelper: |
---|
113 | """Returns default values for dictionary lookups.""" |
---|
114 | def __init__(self, obj): |
---|
115 | self.obj = obj |
---|
116 | |
---|
117 | def __getitem__(self, name): |
---|
118 | return self.obj.get(name, False) |
---|
119 | |
---|
120 | |
---|
121 | class AttributeGetter: |
---|
122 | """Helper for looking up attributes |
---|
123 | |
---|
124 | First we check the method, and if the attribute is not present, |
---|
125 | we check the method's class. |
---|
126 | """ |
---|
127 | missing = object() |
---|
128 | |
---|
129 | def __init__(self, cls, method): |
---|
130 | self.cls = cls |
---|
131 | self.method = method |
---|
132 | |
---|
133 | def get(self, name, default=None): |
---|
134 | log.debug('Get %s from %s.%s', name, self.cls, self.method) |
---|
135 | val = self.method.__dict__.get(name, self.missing) |
---|
136 | if val is self.missing: |
---|
137 | log.debug('No attribute %s in method, getting from class', |
---|
138 | name) |
---|
139 | val = getattr(self.cls, name, default) |
---|
140 | log.debug('Class attribute %s value: %s', name, val) |
---|
141 | return val |
---|
142 | |
---|
143 | class AttributeSelector(Plugin): |
---|
144 | """Selects test cases to be run based on their attributes. |
---|
145 | """ |
---|
146 | |
---|
147 | def __init__(self): |
---|
148 | Plugin.__init__(self) |
---|
149 | self.attribs = [] |
---|
150 | |
---|
151 | def options(self, parser, env): |
---|
152 | """Register command line options""" |
---|
153 | parser.add_option("-a", "--attr", |
---|
154 | dest="attr", action="append", |
---|
155 | default=env.get('NOSE_ATTR'), |
---|
156 | metavar="ATTR", |
---|
157 | help="Run only tests that have attributes " |
---|
158 | "specified by ATTR [NOSE_ATTR]") |
---|
159 | # disable in < 2.4: eval can't take needed args |
---|
160 | if compat_24: |
---|
161 | parser.add_option("-A", "--eval-attr", |
---|
162 | dest="eval_attr", metavar="EXPR", action="append", |
---|
163 | default=env.get('NOSE_EVAL_ATTR'), |
---|
164 | help="Run only tests for whose attributes " |
---|
165 | "the Python expression EXPR evaluates " |
---|
166 | "to True [NOSE_EVAL_ATTR]") |
---|
167 | |
---|
168 | def configure(self, options, config): |
---|
169 | """Configure the plugin and system, based on selected options. |
---|
170 | |
---|
171 | attr and eval_attr may each be lists. |
---|
172 | |
---|
173 | self.attribs will be a list of lists of tuples. In that list, each |
---|
174 | list is a group of attributes, all of which must match for the rule to |
---|
175 | match. |
---|
176 | """ |
---|
177 | self.attribs = [] |
---|
178 | |
---|
179 | # handle python eval-expression parameter |
---|
180 | if compat_24 and options.eval_attr: |
---|
181 | eval_attr = tolist(options.eval_attr) |
---|
182 | for attr in eval_attr: |
---|
183 | # "<python expression>" |
---|
184 | # -> eval(expr) in attribute context must be True |
---|
185 | def eval_in_context(expr, attribs): |
---|
186 | return eval(expr, None, ContextHelper(attribs)) |
---|
187 | self.attribs.append([(attr, eval_in_context)]) |
---|
188 | |
---|
189 | # attribute requirements are a comma separated list of |
---|
190 | # 'key=value' pairs |
---|
191 | if options.attr: |
---|
192 | std_attr = tolist(options.attr) |
---|
193 | for attr in std_attr: |
---|
194 | # all attributes within an attribute group must match |
---|
195 | attr_group = [] |
---|
196 | for attrib in attr.strip().split(","): |
---|
197 | # don't die on trailing comma |
---|
198 | if not attrib: |
---|
199 | continue |
---|
200 | items = attrib.split("=", 1) |
---|
201 | if len(items) > 1: |
---|
202 | # "name=value" |
---|
203 | # -> 'str(obj.name) == value' must be True |
---|
204 | key, value = items |
---|
205 | else: |
---|
206 | key = items[0] |
---|
207 | if key[0] == "!": |
---|
208 | # "!name" |
---|
209 | # 'bool(obj.name)' must be False |
---|
210 | key = key[1:] |
---|
211 | value = False |
---|
212 | else: |
---|
213 | # "name" |
---|
214 | # -> 'bool(obj.name)' must be True |
---|
215 | value = True |
---|
216 | attr_group.append((key, value)) |
---|
217 | self.attribs.append(attr_group) |
---|
218 | if self.attribs: |
---|
219 | self.enabled = True |
---|
220 | |
---|
221 | def validateAttrib(self, attribs): |
---|
222 | # TODO: is there a need for case-sensitive value comparison? |
---|
223 | # within each group, all must match for the group to match |
---|
224 | # if any group matches, then the attribute set as a whole |
---|
225 | # has matched |
---|
226 | any = False |
---|
227 | for group in self.attribs: |
---|
228 | match = True |
---|
229 | for key, value in group: |
---|
230 | obj_value = attribs.get(key) |
---|
231 | if callable(value): |
---|
232 | if not value(key, attribs): |
---|
233 | match = False |
---|
234 | break |
---|
235 | elif value is True: |
---|
236 | # value must exist and be True |
---|
237 | if not bool(obj_value): |
---|
238 | match = False |
---|
239 | break |
---|
240 | elif value is False: |
---|
241 | # value must not exist or be False |
---|
242 | if bool(obj_value): |
---|
243 | match = False |
---|
244 | break |
---|
245 | elif type(obj_value) in (list, tuple): |
---|
246 | # value must be found in the list attribute |
---|
247 | |
---|
248 | if not str(value).lower() in [str(x).lower() |
---|
249 | for x in obj_value]: |
---|
250 | match = False |
---|
251 | break |
---|
252 | else: |
---|
253 | # value must match, convert to string and compare |
---|
254 | if (value != obj_value |
---|
255 | and str(value).lower() != str(obj_value).lower()): |
---|
256 | match = False |
---|
257 | break |
---|
258 | any = any or match |
---|
259 | if any: |
---|
260 | # not True because we don't want to FORCE the selection of the |
---|
261 | # item, only say that it is acceptable |
---|
262 | return None |
---|
263 | return False |
---|
264 | |
---|
265 | def wantClass(self, cls): |
---|
266 | """Accept the class if the class or any method is wanted. |
---|
267 | """ |
---|
268 | cls_attr = cls.__dict__ |
---|
269 | if self.validateAttrib(cls_attr) is not False: |
---|
270 | return None |
---|
271 | # Methods in __dict__.values() are functions, oddly enough. |
---|
272 | methods = filter(isfunction, cls_attr.values()) |
---|
273 | wanted = filter(lambda m: m is not False, |
---|
274 | map(self.wantFunction, methods)) |
---|
275 | if wanted: |
---|
276 | return None |
---|
277 | return False |
---|
278 | |
---|
279 | def wantFunction(self, function): |
---|
280 | """Accept the function if its attributes match. |
---|
281 | """ |
---|
282 | return self.validateAttrib(function.__dict__) |
---|
283 | |
---|
284 | def wantMethod(self, method): |
---|
285 | """Accept the method if its attributes match. |
---|
286 | """ |
---|
287 | attribs = AttributeGetter(method.im_class, method) |
---|
288 | return self.validateAttrib(attribs) |
---|