| 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) |
|---|