| 1 | """ |
|---|
| 2 | Test Loader |
|---|
| 3 | ----------- |
|---|
| 4 | |
|---|
| 5 | nose's test loader implements the same basic functionality as its |
|---|
| 6 | superclass, unittest.TestLoader, but extends it by more liberal |
|---|
| 7 | interpretations of what may be a test and how a test may be named. |
|---|
| 8 | """ |
|---|
| 9 | from __future__ import generators |
|---|
| 10 | |
|---|
| 11 | import logging |
|---|
| 12 | import os |
|---|
| 13 | import sys |
|---|
| 14 | import unittest |
|---|
| 15 | from inspect import isfunction, ismethod |
|---|
| 16 | from nose.case import FunctionTestCase, MethodTestCase |
|---|
| 17 | from nose.failure import Failure |
|---|
| 18 | from nose.config import Config |
|---|
| 19 | from nose.importer import Importer, add_path, remove_path |
|---|
| 20 | from nose.selector import defaultSelector, TestAddress |
|---|
| 21 | from nose.util import cmp_lineno, getpackage, isclass, isgenerator, ispackage, \ |
|---|
| 22 | match_last, resolve_name, transplant_func, transplant_class, test_address |
|---|
| 23 | from nose.suite import ContextSuiteFactory, ContextList, LazySuite |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | log = logging.getLogger(__name__) |
|---|
| 27 | #log.setLevel(logging.DEBUG) |
|---|
| 28 | |
|---|
| 29 | # for efficiency and easier mocking |
|---|
| 30 | op_normpath = os.path.normpath |
|---|
| 31 | op_abspath = os.path.abspath |
|---|
| 32 | op_join = os.path.join |
|---|
| 33 | op_isdir = os.path.isdir |
|---|
| 34 | op_isfile = os.path.isfile |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | __all__ = ['TestLoader', 'defaultTestLoader'] |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | class TestLoader(unittest.TestLoader): |
|---|
| 41 | """Test loader that extends unittest.TestLoader to: |
|---|
| 42 | |
|---|
| 43 | * Load tests from test-like functions and classes that are not |
|---|
| 44 | unittest.TestCase subclasses |
|---|
| 45 | * Find and load test modules in a directory |
|---|
| 46 | * Support tests that are generators |
|---|
| 47 | * Support easy extensions of or changes to that behavior through plugins |
|---|
| 48 | """ |
|---|
| 49 | config = None |
|---|
| 50 | importer = None |
|---|
| 51 | workingDir = None |
|---|
| 52 | selector = None |
|---|
| 53 | suiteClass = None |
|---|
| 54 | |
|---|
| 55 | def __init__(self, config=None, importer=None, workingDir=None, |
|---|
| 56 | selector=None): |
|---|
| 57 | """Initialize a test loader. |
|---|
| 58 | |
|---|
| 59 | Parameters (all optional): |
|---|
| 60 | |
|---|
| 61 | * config: provide a `nose.config.Config`_ or other config class |
|---|
| 62 | instance; if not provided a `nose.config.Config`_ with |
|---|
| 63 | default values is used. |
|---|
| 64 | * importer: provide an importer instance that implements |
|---|
| 65 | `importFromPath`. If not provided, a |
|---|
| 66 | `nose.importer.Importer`_ is used. |
|---|
| 67 | * workingDir: the directory to which file and module names are |
|---|
| 68 | relative. If not provided, assumed to be the current working |
|---|
| 69 | directory. |
|---|
| 70 | * selector: a selector class or instance. If a class is |
|---|
| 71 | provided, it will be instantiated with one argument, the |
|---|
| 72 | current config. If not provided, a `nose.selector.Selector`_ |
|---|
| 73 | is used. |
|---|
| 74 | """ |
|---|
| 75 | if config is None: |
|---|
| 76 | config = Config() |
|---|
| 77 | if importer is None: |
|---|
| 78 | importer = Importer(config=config) |
|---|
| 79 | if workingDir is None: |
|---|
| 80 | workingDir = config.workingDir |
|---|
| 81 | if selector is None: |
|---|
| 82 | selector = defaultSelector(config) |
|---|
| 83 | elif isclass(selector): |
|---|
| 84 | selector = selector(config) |
|---|
| 85 | self.config = config |
|---|
| 86 | self.importer = importer |
|---|
| 87 | self.workingDir = op_normpath(op_abspath(workingDir)) |
|---|
| 88 | self.selector = selector |
|---|
| 89 | if config.addPaths: |
|---|
| 90 | add_path(workingDir, config) |
|---|
| 91 | self.suiteClass = ContextSuiteFactory(config=config) |
|---|
| 92 | unittest.TestLoader.__init__(self) |
|---|
| 93 | |
|---|
| 94 | def getTestCaseNames(self, testCaseClass): |
|---|
| 95 | """Override to select with selector, unless |
|---|
| 96 | config.getTestCaseNamesCompat is True |
|---|
| 97 | """ |
|---|
| 98 | if self.config.getTestCaseNamesCompat: |
|---|
| 99 | return unittest.TestLoader.getTestCaseNames(self, testCaseClass) |
|---|
| 100 | |
|---|
| 101 | def wanted(attr, cls=testCaseClass, sel=self.selector): |
|---|
| 102 | item = getattr(cls, attr, None) |
|---|
| 103 | if not ismethod(item): |
|---|
| 104 | return False |
|---|
| 105 | return sel.wantMethod(item) |
|---|
| 106 | cases = filter(wanted, dir(testCaseClass)) |
|---|
| 107 | for base in testCaseClass.__bases__: |
|---|
| 108 | for case in self.getTestCaseNames(base): |
|---|
| 109 | if case not in cases: |
|---|
| 110 | cases.append(case) |
|---|
| 111 | # add runTest if nothing else picked |
|---|
| 112 | if not cases and hasattr(testCaseClass, 'runTest'): |
|---|
| 113 | cases = ['runTest'] |
|---|
| 114 | if self.sortTestMethodsUsing: |
|---|
| 115 | cases.sort(self.sortTestMethodsUsing) |
|---|
| 116 | return cases |
|---|
| 117 | |
|---|
| 118 | def loadTestsFromDir(self, path): |
|---|
| 119 | """Load tests from the directory at path. This is a generator |
|---|
| 120 | -- each suite of tests from a module or other file is yielded |
|---|
| 121 | and is expected to be executed before the next file is |
|---|
| 122 | examined. |
|---|
| 123 | """ |
|---|
| 124 | log.debug("load from dir %s", path) |
|---|
| 125 | plugins = self.config.plugins |
|---|
| 126 | plugins.beforeDirectory(path) |
|---|
| 127 | if self.config.addPaths: |
|---|
| 128 | paths_added = add_path(path, self.config) |
|---|
| 129 | |
|---|
| 130 | entries = os.listdir(path) |
|---|
| 131 | entries.sort(lambda a, b: match_last(a, b, self.config.testMatch)) |
|---|
| 132 | for entry in entries: |
|---|
| 133 | # this hard-coded initial-dot test will be removed: |
|---|
| 134 | # http://code.google.com/p/python-nose/issues/detail?id=82 |
|---|
| 135 | if entry.startswith('.'): |
|---|
| 136 | continue |
|---|
| 137 | entry_path = op_abspath(op_join(path, entry)) |
|---|
| 138 | is_file = op_isfile(entry_path) |
|---|
| 139 | wanted = False |
|---|
| 140 | if is_file: |
|---|
| 141 | is_dir = False |
|---|
| 142 | wanted = self.selector.wantFile(entry_path) |
|---|
| 143 | else: |
|---|
| 144 | is_dir = op_isdir(entry_path) |
|---|
| 145 | if is_dir: |
|---|
| 146 | # this hard-coded initial-underscore test will be removed: |
|---|
| 147 | # http://code.google.com/p/python-nose/issues/detail?id=82 |
|---|
| 148 | if entry.startswith('_'): |
|---|
| 149 | continue |
|---|
| 150 | wanted = self.selector.wantDirectory(entry_path) |
|---|
| 151 | is_package = ispackage(entry_path) |
|---|
| 152 | if wanted: |
|---|
| 153 | if is_file: |
|---|
| 154 | plugins.beforeContext() |
|---|
| 155 | if entry.endswith('.py'): |
|---|
| 156 | yield self.loadTestsFromName( |
|---|
| 157 | entry_path, discovered=True) |
|---|
| 158 | else: |
|---|
| 159 | yield self.loadTestsFromFile(entry_path) |
|---|
| 160 | plugins.afterContext() |
|---|
| 161 | elif is_package: |
|---|
| 162 | # Load the entry as a package: given the full path, |
|---|
| 163 | # loadTestsFromName() will figure it out |
|---|
| 164 | yield self.loadTestsFromName( |
|---|
| 165 | entry_path, discovered=True) |
|---|
| 166 | else: |
|---|
| 167 | # Another test dir in this one: recurse lazily |
|---|
| 168 | yield self.suiteClass( |
|---|
| 169 | lambda: self.loadTestsFromDir(entry_path)) |
|---|
| 170 | tests = [] |
|---|
| 171 | for test in plugins.loadTestsFromDir(path): |
|---|
| 172 | tests.append(test) |
|---|
| 173 | # TODO: is this try/except needed? |
|---|
| 174 | try: |
|---|
| 175 | if tests: |
|---|
| 176 | yield self.suiteClass(tests) |
|---|
| 177 | except (KeyboardInterrupt, SystemExit): |
|---|
| 178 | raise |
|---|
| 179 | except: |
|---|
| 180 | yield self.suiteClass([Failure(*sys.exc_info())]) |
|---|
| 181 | |
|---|
| 182 | # pop paths |
|---|
| 183 | if self.config.addPaths: |
|---|
| 184 | map(remove_path, paths_added) |
|---|
| 185 | plugins.afterDirectory(path) |
|---|
| 186 | |
|---|
| 187 | def loadTestsFromFile(self, filename): |
|---|
| 188 | """Load tests from a non-module file. Default is to raise a |
|---|
| 189 | ValueError; plugins may implement `loadTestsFromFile` to |
|---|
| 190 | provide a list of tests loaded from the file. |
|---|
| 191 | """ |
|---|
| 192 | log.debug("Load from non-module file %s", filename) |
|---|
| 193 | try: |
|---|
| 194 | tests = [test for test in |
|---|
| 195 | self.config.plugins.loadTestsFromFile(filename)] |
|---|
| 196 | if tests: |
|---|
| 197 | # Plugins can yield False to indicate that they were |
|---|
| 198 | # unable to load tests from a file, but it was not an |
|---|
| 199 | # error -- the file just had no tests to load. |
|---|
| 200 | tests = filter(None, tests) |
|---|
| 201 | return self.suiteClass(tests) |
|---|
| 202 | else: |
|---|
| 203 | # Nothing was able to even try to load from this file |
|---|
| 204 | open(filename, 'r').close() # trigger os error |
|---|
| 205 | raise ValueError("Unable to load tests from file %s" |
|---|
| 206 | % filename) |
|---|
| 207 | except (KeyboardInterrupt, SystemExit): |
|---|
| 208 | raise |
|---|
| 209 | except: |
|---|
| 210 | exc = sys.exc_info() |
|---|
| 211 | return self.suiteClass( |
|---|
| 212 | [Failure(exc[0], exc[1], exc[2], |
|---|
| 213 | address=(filename, None, None))]) |
|---|
| 214 | |
|---|
| 215 | def loadTestsFromGenerator(self, generator, module): |
|---|
| 216 | """Lazy-load tests from a generator function. The generator function |
|---|
| 217 | may yield either: |
|---|
| 218 | |
|---|
| 219 | * a callable, or |
|---|
| 220 | * a function name resolvable within the same module |
|---|
| 221 | """ |
|---|
| 222 | def generate(g=generator, m=module): |
|---|
| 223 | try: |
|---|
| 224 | for test in g(): |
|---|
| 225 | test_func, arg = self.parseGeneratedTest(test) |
|---|
| 226 | if not callable(test_func): |
|---|
| 227 | test_func = getattr(m, test_func) |
|---|
| 228 | yield FunctionTestCase(test_func, arg=arg, descriptor=g) |
|---|
| 229 | except KeyboardInterrupt: |
|---|
| 230 | raise |
|---|
| 231 | except: |
|---|
| 232 | exc = sys.exc_info() |
|---|
| 233 | yield Failure(exc[0], exc[1], exc[2], |
|---|
| 234 | address=test_address(generator)) |
|---|
| 235 | return self.suiteClass(generate, context=generator, can_split=False) |
|---|
| 236 | |
|---|
| 237 | def loadTestsFromGeneratorMethod(self, generator, cls): |
|---|
| 238 | """Lazy-load tests from a generator method. |
|---|
| 239 | |
|---|
| 240 | This is more complicated than loading from a generator function, |
|---|
| 241 | since a generator method may yield: |
|---|
| 242 | |
|---|
| 243 | * a function |
|---|
| 244 | * a bound or unbound method, or |
|---|
| 245 | * a method name |
|---|
| 246 | """ |
|---|
| 247 | # convert the unbound generator method |
|---|
| 248 | # into a bound method so it can be called below |
|---|
| 249 | cls = generator.im_class |
|---|
| 250 | inst = cls() |
|---|
| 251 | method = generator.__name__ |
|---|
| 252 | generator = getattr(inst, method) |
|---|
| 253 | |
|---|
| 254 | def generate(g=generator, c=cls): |
|---|
| 255 | try: |
|---|
| 256 | for test in g(): |
|---|
| 257 | test_func, arg = self.parseGeneratedTest(test) |
|---|
| 258 | if not callable(test_func): |
|---|
| 259 | test_func = getattr(c, test_func) |
|---|
| 260 | if ismethod(test_func): |
|---|
| 261 | yield MethodTestCase(test_func, arg=arg, descriptor=g) |
|---|
| 262 | elif isfunction(test_func): |
|---|
| 263 | # In this case we're forcing the 'MethodTestCase' |
|---|
| 264 | # to run the inline function as its test call, |
|---|
| 265 | # but using the generator method as the 'method of |
|---|
| 266 | # record' (so no need to pass it as the descriptor) |
|---|
| 267 | yield MethodTestCase(g, test=test_func, arg=arg) |
|---|
| 268 | else: |
|---|
| 269 | yield Failure( |
|---|
| 270 | TypeError, |
|---|
| 271 | "%s is not a function or method" % test_func) |
|---|
| 272 | except KeyboardInterrupt: |
|---|
| 273 | raise |
|---|
| 274 | except: |
|---|
| 275 | exc = sys.exc_info() |
|---|
| 276 | yield Failure(exc[0], exc[1], exc[2], |
|---|
| 277 | address=test_address(generator)) |
|---|
| 278 | return self.suiteClass(generate, context=generator, can_split=False) |
|---|
| 279 | |
|---|
| 280 | def loadTestsFromModule(self, module, path=None, discovered=False): |
|---|
| 281 | """Load all tests from module and return a suite containing |
|---|
| 282 | them. If the module has been discovered and is not test-like, |
|---|
| 283 | the suite will be empty by default, though plugins may add |
|---|
| 284 | their own tests. |
|---|
| 285 | """ |
|---|
| 286 | log.debug("Load from module %s", module) |
|---|
| 287 | tests = [] |
|---|
| 288 | test_classes = [] |
|---|
| 289 | test_funcs = [] |
|---|
| 290 | # For *discovered* modules, we only load tests when the module looks |
|---|
| 291 | # testlike. For modules we've been directed to load, we always |
|---|
| 292 | # look for tests. (discovered is set to True by loadTestsFromDir) |
|---|
| 293 | if not discovered or self.selector.wantModule(module): |
|---|
| 294 | for item in dir(module): |
|---|
| 295 | test = getattr(module, item, None) |
|---|
| 296 | # print "Check %s (%s) in %s" % (item, test, module.__name__) |
|---|
| 297 | if isclass(test): |
|---|
| 298 | if self.selector.wantClass(test): |
|---|
| 299 | test_classes.append(test) |
|---|
| 300 | elif isfunction(test) and self.selector.wantFunction(test): |
|---|
| 301 | test_funcs.append(test) |
|---|
| 302 | test_classes.sort(lambda a, b: cmp(a.__name__, b.__name__)) |
|---|
| 303 | test_funcs.sort(cmp_lineno) |
|---|
| 304 | tests = map(lambda t: self.makeTest(t, parent=module), |
|---|
| 305 | test_classes + test_funcs) |
|---|
| 306 | |
|---|
| 307 | # Now, descend into packages |
|---|
| 308 | # FIXME can or should this be lazy? |
|---|
| 309 | # is this syntax 2.2 compatible? |
|---|
| 310 | module_paths = getattr(module, '__path__', []) |
|---|
| 311 | if path: |
|---|
| 312 | path = os.path.realpath(path) |
|---|
| 313 | for module_path in module_paths: |
|---|
| 314 | if (self.config.traverseNamespace or not path) or \ |
|---|
| 315 | os.path.realpath(module_path).startswith(path): |
|---|
| 316 | tests.extend(self.loadTestsFromDir(module_path)) |
|---|
| 317 | |
|---|
| 318 | for test in self.config.plugins.loadTestsFromModule(module, path): |
|---|
| 319 | tests.append(test) |
|---|
| 320 | |
|---|
| 321 | return self.suiteClass(ContextList(tests, context=module)) |
|---|
| 322 | |
|---|
| 323 | def loadTestsFromName(self, name, module=None, discovered=False): |
|---|
| 324 | """Load tests from the entity with the given name. |
|---|
| 325 | |
|---|
| 326 | The name may indicate a file, directory, module, or any object |
|---|
| 327 | within a module. See `nose.util.split_test_name` for details on |
|---|
| 328 | test name parsing. |
|---|
| 329 | """ |
|---|
| 330 | # FIXME refactor this method into little bites? |
|---|
| 331 | log.debug("load from %s (%s)", name, module) |
|---|
| 332 | |
|---|
| 333 | suite = self.suiteClass |
|---|
| 334 | |
|---|
| 335 | # give plugins first crack |
|---|
| 336 | plug_tests = self.config.plugins.loadTestsFromName(name, module) |
|---|
| 337 | if plug_tests: |
|---|
| 338 | return suite(plug_tests) |
|---|
| 339 | |
|---|
| 340 | addr = TestAddress(name, workingDir=self.workingDir) |
|---|
| 341 | if module: |
|---|
| 342 | # Two cases: |
|---|
| 343 | # name is class.foo |
|---|
| 344 | # The addr will be incorrect, since it thinks class.foo is |
|---|
| 345 | # a dotted module name. It's actually a dotted attribute |
|---|
| 346 | # name. In this case we want to use the full submitted |
|---|
| 347 | # name as the name to load from the module. |
|---|
| 348 | # name is module:class.foo |
|---|
| 349 | # The addr will be correct. The part we want is the part after |
|---|
| 350 | # the :, which is in addr.call. |
|---|
| 351 | if addr.call: |
|---|
| 352 | name = addr.call |
|---|
| 353 | parent, obj = self.resolve(name, module) |
|---|
| 354 | if (isclass(parent) |
|---|
| 355 | and getattr(parent, '__module__', None) != module.__name__): |
|---|
| 356 | parent = transplant_class(parent, module.__name__) |
|---|
| 357 | obj = getattr(parent, obj.__name__) |
|---|
| 358 | log.debug("parent %s obj %s module %s", parent, obj, module) |
|---|
| 359 | if isinstance(obj, Failure): |
|---|
| 360 | return suite([obj]) |
|---|
| 361 | else: |
|---|
| 362 | return suite(ContextList([self.makeTest(obj, parent)], |
|---|
| 363 | context=parent)) |
|---|
| 364 | else: |
|---|
| 365 | if addr.module: |
|---|
| 366 | try: |
|---|
| 367 | if addr.filename is None: |
|---|
| 368 | module = resolve_name(addr.module) |
|---|
| 369 | else: |
|---|
| 370 | self.config.plugins.beforeImport( |
|---|
| 371 | addr.filename, addr.module) |
|---|
| 372 | # FIXME: to support module.name names, |
|---|
| 373 | # do what resolve-name does and keep trying to |
|---|
| 374 | # import, popping tail of module into addr.call, |
|---|
| 375 | # until we either get an import or run out of |
|---|
| 376 | # module parts |
|---|
| 377 | try: |
|---|
| 378 | module = self.importer.importFromPath( |
|---|
| 379 | addr.filename, addr.module) |
|---|
| 380 | finally: |
|---|
| 381 | self.config.plugins.afterImport( |
|---|
| 382 | addr.filename, addr.module) |
|---|
| 383 | except (KeyboardInterrupt, SystemExit): |
|---|
| 384 | raise |
|---|
| 385 | except: |
|---|
| 386 | exc = sys.exc_info() |
|---|
| 387 | return suite([Failure(exc[0], exc[1], exc[2], |
|---|
| 388 | address=addr.totuple())]) |
|---|
| 389 | if addr.call: |
|---|
| 390 | return self.loadTestsFromName(addr.call, module) |
|---|
| 391 | else: |
|---|
| 392 | return self.loadTestsFromModule( |
|---|
| 393 | module, addr.filename, |
|---|
| 394 | discovered=discovered) |
|---|
| 395 | elif addr.filename: |
|---|
| 396 | path = addr.filename |
|---|
| 397 | if addr.call: |
|---|
| 398 | package = getpackage(path) |
|---|
| 399 | if package is None: |
|---|
| 400 | return suite([ |
|---|
| 401 | Failure(ValueError, |
|---|
| 402 | "Can't find callable %s in file %s: " |
|---|
| 403 | "file is not a python module" % |
|---|
| 404 | (addr.call, path), |
|---|
| 405 | address=addr.totuple())]) |
|---|
| 406 | return self.loadTestsFromName(addr.call, module=package) |
|---|
| 407 | else: |
|---|
| 408 | if op_isdir(path): |
|---|
| 409 | # In this case we *can* be lazy since we know |
|---|
| 410 | # that each module in the dir will be fully |
|---|
| 411 | # loaded before its tests are executed; we |
|---|
| 412 | # also know that we're not going to be asked |
|---|
| 413 | # to load from . and ./some_module.py *as part |
|---|
| 414 | # of this named test load* |
|---|
| 415 | return LazySuite( |
|---|
| 416 | lambda: self.loadTestsFromDir(path)) |
|---|
| 417 | elif op_isfile(path): |
|---|
| 418 | return self.loadTestsFromFile(path) |
|---|
| 419 | else: |
|---|
| 420 | return suite([ |
|---|
| 421 | Failure(OSError, "No such file %s" % path, |
|---|
| 422 | address=addr.totuple())]) |
|---|
| 423 | else: |
|---|
| 424 | # just a function? what to do? I think it can only be |
|---|
| 425 | # handled when module is not None |
|---|
| 426 | return suite([ |
|---|
| 427 | Failure(ValueError, "Unresolvable test name %s" % name, |
|---|
| 428 | address=addr.totuple())]) |
|---|
| 429 | |
|---|
| 430 | def loadTestsFromNames(self, names, module=None): |
|---|
| 431 | """Load tests from all names, returning a suite containing all |
|---|
| 432 | tests. |
|---|
| 433 | """ |
|---|
| 434 | plug_res = self.config.plugins.loadTestsFromNames(names, module) |
|---|
| 435 | if plug_res: |
|---|
| 436 | suite, names = plug_res |
|---|
| 437 | if suite: |
|---|
| 438 | return self.suiteClass([ |
|---|
| 439 | self.suiteClass(suite), |
|---|
| 440 | unittest.TestLoader.loadTestsFromNames(self, names, module) |
|---|
| 441 | ]) |
|---|
| 442 | return unittest.TestLoader.loadTestsFromNames(self, names, module) |
|---|
| 443 | |
|---|
| 444 | def loadTestsFromTestCase(self, testCaseClass): |
|---|
| 445 | """Load tests from a unittest.TestCase subclass. |
|---|
| 446 | """ |
|---|
| 447 | cases = [] |
|---|
| 448 | plugins = self.config.plugins |
|---|
| 449 | for case in plugins.loadTestsFromTestCase(testCaseClass): |
|---|
| 450 | cases.append(case) |
|---|
| 451 | # For efficiency in the most common case, just call and return from |
|---|
| 452 | # super. This avoids having to extract cases and rebuild a context |
|---|
| 453 | # suite when there are no plugin-contributed cases. |
|---|
| 454 | if not cases: |
|---|
| 455 | return super(TestLoader, self).loadTestsFromTestCase(testCaseClass) |
|---|
| 456 | cases.extend( |
|---|
| 457 | [case for case in |
|---|
| 458 | super(TestLoader, self).loadTestsFromTestCase(testCaseClass)]) |
|---|
| 459 | return self.suiteClass(cases) |
|---|
| 460 | |
|---|
| 461 | def loadTestsFromTestClass(self, cls): |
|---|
| 462 | """Load tests from a test class that is *not* a unittest.TestCase |
|---|
| 463 | subclass. |
|---|
| 464 | |
|---|
| 465 | In this case, we can't depend on the class's `__init__` taking method |
|---|
| 466 | name arguments, so we have to compose a MethodTestCase for each |
|---|
| 467 | method in the class that looks testlike. |
|---|
| 468 | """ |
|---|
| 469 | def wanted(attr, cls=cls, sel=self.selector): |
|---|
| 470 | item = getattr(cls, attr, None) |
|---|
| 471 | if not ismethod(item): |
|---|
| 472 | return False |
|---|
| 473 | return sel.wantMethod(item) |
|---|
| 474 | cases = [self.makeTest(getattr(cls, case), cls) |
|---|
| 475 | for case in filter(wanted, dir(cls))] |
|---|
| 476 | for test in self.config.plugins.loadTestsFromTestClass(cls): |
|---|
| 477 | cases.append(test) |
|---|
| 478 | return self.suiteClass(ContextList(cases, context=cls)) |
|---|
| 479 | |
|---|
| 480 | def makeTest(self, obj, parent=None): |
|---|
| 481 | """Given a test object and its parent, return a test case |
|---|
| 482 | or test suite. |
|---|
| 483 | """ |
|---|
| 484 | plug_tests = [] |
|---|
| 485 | try: |
|---|
| 486 | addr = test_address(obj) |
|---|
| 487 | except KeyboardInterrupt: |
|---|
| 488 | raise |
|---|
| 489 | except: |
|---|
| 490 | addr = None |
|---|
| 491 | for test in self.config.plugins.makeTest(obj, parent): |
|---|
| 492 | plug_tests.append(test) |
|---|
| 493 | # TODO: is this try/except needed? |
|---|
| 494 | try: |
|---|
| 495 | if plug_tests: |
|---|
| 496 | return self.suiteClass(plug_tests) |
|---|
| 497 | except (KeyboardInterrupt, SystemExit): |
|---|
| 498 | raise |
|---|
| 499 | except: |
|---|
| 500 | exc = sys.exc_info() |
|---|
| 501 | return Failure(exc[0], exc[1], exc[2], address=addr) |
|---|
| 502 | |
|---|
| 503 | if isinstance(obj, unittest.TestCase): |
|---|
| 504 | return obj |
|---|
| 505 | elif isclass(obj): |
|---|
| 506 | if parent and obj.__module__ != parent.__name__: |
|---|
| 507 | obj = transplant_class(obj, parent.__name__) |
|---|
| 508 | if issubclass(obj, unittest.TestCase): |
|---|
| 509 | return self.loadTestsFromTestCase(obj) |
|---|
| 510 | else: |
|---|
| 511 | return self.loadTestsFromTestClass(obj) |
|---|
| 512 | elif ismethod(obj): |
|---|
| 513 | if parent is None: |
|---|
| 514 | parent = obj.__class__ |
|---|
| 515 | if issubclass(parent, unittest.TestCase): |
|---|
| 516 | return parent(obj.__name__) |
|---|
| 517 | else: |
|---|
| 518 | if isgenerator(obj): |
|---|
| 519 | return self.loadTestsFromGeneratorMethod(obj, parent) |
|---|
| 520 | else: |
|---|
| 521 | return MethodTestCase(obj) |
|---|
| 522 | elif isfunction(obj): |
|---|
| 523 | if parent and obj.__module__ != parent.__name__: |
|---|
| 524 | obj = transplant_func(obj, parent.__name__) |
|---|
| 525 | if isgenerator(obj): |
|---|
| 526 | return self.loadTestsFromGenerator(obj, parent) |
|---|
| 527 | else: |
|---|
| 528 | return FunctionTestCase(obj) |
|---|
| 529 | else: |
|---|
| 530 | return Failure(TypeError, |
|---|
| 531 | "Can't make a test from %s" % obj, |
|---|
| 532 | address=addr) |
|---|
| 533 | |
|---|
| 534 | def resolve(self, name, module): |
|---|
| 535 | """Resolve name within module |
|---|
| 536 | """ |
|---|
| 537 | obj = module |
|---|
| 538 | parts = name.split('.') |
|---|
| 539 | for part in parts: |
|---|
| 540 | parent, obj = obj, getattr(obj, part, None) |
|---|
| 541 | if obj is None: |
|---|
| 542 | # no such test |
|---|
| 543 | obj = Failure(ValueError, "No such test %s" % name) |
|---|
| 544 | return parent, obj |
|---|
| 545 | |
|---|
| 546 | def parseGeneratedTest(self, test): |
|---|
| 547 | """Given the yield value of a test generator, return a func and args. |
|---|
| 548 | |
|---|
| 549 | This is used in the two loadTestsFromGenerator* methods. |
|---|
| 550 | |
|---|
| 551 | """ |
|---|
| 552 | if not isinstance(test, tuple): # yield test |
|---|
| 553 | test_func, arg = (test, tuple()) |
|---|
| 554 | elif len(test) == 1: # yield (test,) |
|---|
| 555 | test_func, arg = (test[0], tuple()) |
|---|
| 556 | else: # yield test, foo, bar, ... |
|---|
| 557 | assert len(test) > 1 # sanity check |
|---|
| 558 | test_func, arg = (test[0], test[1:]) |
|---|
| 559 | return test_func, arg |
|---|
| 560 | |
|---|
| 561 | defaultTestLoader = TestLoader |
|---|
| 562 | |
|---|