/nose/suite.py
Python | 607 lines | 469 code | 48 blank | 90 comment | 109 complexity | d902fd3c7df42cf0151aec50639c1c33 MD5 | raw file
1""" 2Test Suites 3----------- 4 5Provides a LazySuite, which is a suite whose test list is a generator 6function, and ContextSuite,which can run fixtures (setup/teardown 7functions or methods) for the context that contains its tests. 8 9""" 10from __future__ import generators 11 12import logging 13import sys 14import unittest 15from nose.case import Test 16from nose.config import Config 17from nose.proxy import ResultProxyFactory 18from nose.util import isclass, resolve_name, try_run 19 20if sys.platform == 'cli': 21 if sys.version_info[:2] < (2, 6): 22 import clr 23 clr.AddReference("IronPython") 24 from IronPython.Runtime.Exceptions import StringException 25 else: 26 class StringException(Exception): 27 pass 28 29log = logging.getLogger(__name__) 30#log.setLevel(logging.DEBUG) 31 32# Singleton for default value -- see ContextSuite.__init__ below 33_def = object() 34 35 36def _strclass(cls): 37 return "%s.%s" % (cls.__module__, cls.__name__) 38 39class MixedContextError(Exception): 40 """Error raised when a context suite sees tests from more than 41 one context. 42 """ 43 pass 44 45 46class LazySuite(unittest.TestSuite): 47 """A suite that may use a generator as its list of tests 48 """ 49 def __init__(self, tests=()): 50 """Initialize the suite. tests may be an iterable or a generator 51 """ 52 self._set_tests(tests) 53 54 def __iter__(self): 55 return iter(self._tests) 56 57 def __repr__(self): 58 return "<%s tests=generator (%s)>" % ( 59 _strclass(self.__class__), id(self)) 60 61 def __hash__(self): 62 return object.__hash__(self) 63 64 __str__ = __repr__ 65 66 def addTest(self, test): 67 self._precache.append(test) 68 69 # added to bypass run changes in 2.7's unittest 70 def run(self, result): 71 for test in self._tests: 72 if result.shouldStop: 73 break 74 test(result) 75 return result 76 77 def __nonzero__(self): 78 log.debug("tests in %s?", id(self)) 79 if self._precache: 80 return True 81 if self.test_generator is None: 82 return False 83 try: 84 test = self.test_generator.next() 85 if test is not None: 86 self._precache.append(test) 87 return True 88 except StopIteration: 89 pass 90 return False 91 92 def _get_tests(self): 93 log.debug("precache is %s", self._precache) 94 for test in self._precache: 95 yield test 96 if self.test_generator is None: 97 return 98 for test in self.test_generator: 99 yield test 100 101 def _set_tests(self, tests): 102 self._precache = [] 103 is_suite = isinstance(tests, unittest.TestSuite) 104 if callable(tests) and not is_suite: 105 self.test_generator = tests() 106 elif is_suite: 107 # Suites need special treatment: they must be called like 108 # tests for their setup/teardown to run (if any) 109 self.addTests([tests]) 110 self.test_generator = None 111 else: 112 self.addTests(tests) 113 self.test_generator = None 114 115 _tests = property(_get_tests, _set_tests, None, 116 "Access the tests in this suite. Access is through a " 117 "generator, so iteration may not be repeatable.") 118 119 120class ContextSuite(LazySuite): 121 """A suite with context. 122 123 A ContextSuite executes fixtures (setup and teardown functions or 124 methods) for the context containing its tests. 125 126 The context may be explicitly passed. If it is not, a context (or 127 nested set of contexts) will be constructed by examining the tests 128 in the suite. 129 """ 130 failureException = unittest.TestCase.failureException 131 was_setup = False 132 was_torndown = False 133 classSetup = ('setup_class', 'setup_all', 'setupClass', 'setupAll', 134 'setUpClass', 'setUpAll') 135 classTeardown = ('teardown_class', 'teardown_all', 'teardownClass', 136 'teardownAll', 'tearDownClass', 'tearDownAll') 137 moduleSetup = ('setup_module', 'setupModule', 'setUpModule', 'setup', 138 'setUp') 139 moduleTeardown = ('teardown_module', 'teardownModule', 'tearDownModule', 140 'teardown', 'tearDown') 141 packageSetup = ('setup_package', 'setupPackage', 'setUpPackage') 142 packageTeardown = ('teardown_package', 'teardownPackage', 143 'tearDownPackage') 144 145 def __init__(self, tests=(), context=None, factory=None, 146 config=None, resultProxy=None, can_split=True): 147 log.debug("Context suite for %s (%s) (%s)", tests, context, id(self)) 148 self.context = context 149 self.factory = factory 150 if config is None: 151 config = Config() 152 self.config = config 153 self.resultProxy = resultProxy 154 self.has_run = False 155 self.can_split = can_split 156 self.error_context = None 157 LazySuite.__init__(self, tests) 158 159 def __repr__(self): 160 return "<%s context=%s>" % ( 161 _strclass(self.__class__), 162 getattr(self.context, '__name__', self.context)) 163 __str__ = __repr__ 164 165 def id(self): 166 if self.error_context: 167 return '%s:%s' % (repr(self), self.error_context) 168 else: 169 return repr(self) 170 171 def __hash__(self): 172 return object.__hash__(self) 173 174 # 2.3 compat -- force 2.4 call sequence 175 def __call__(self, *arg, **kw): 176 return self.run(*arg, **kw) 177 178 def exc_info(self): 179 """Hook for replacing error tuple output 180 """ 181 return sys.exc_info() 182 183 def _exc_info(self): 184 """Bottleneck to fix up IronPython string exceptions 185 """ 186 e = self.exc_info() 187 if sys.platform == 'cli': 188 if isinstance(e[0], StringException): 189 # IronPython throws these StringExceptions, but 190 # traceback checks type(etype) == str. Make a real 191 # string here. 192 e = (str(e[0]), e[1], e[2]) 193 194 return e 195 196 def run(self, result): 197 """Run tests in suite inside of suite fixtures. 198 """ 199 # proxy the result for myself 200 log.debug("suite %s (%s) run called, tests: %s", id(self), self, self._tests) 201 #import pdb 202 #pdb.set_trace() 203 if self.resultProxy: 204 result, orig = self.resultProxy(result, self), result 205 else: 206 result, orig = result, result 207 try: 208 self.setUp() 209 except KeyboardInterrupt: 210 raise 211 except: 212 self.error_context = 'setup' 213 result.addError(self, self._exc_info()) 214 return 215 try: 216 for test in self._tests: 217 if result.shouldStop: 218 log.debug("stopping") 219 break 220 # each nose.case.Test will create its own result proxy 221 # so the cases need the original result, to avoid proxy 222 # chains 223 test(orig) 224 finally: 225 self.has_run = True 226 try: 227 self.tearDown() 228 except KeyboardInterrupt: 229 raise 230 except: 231 self.error_context = 'teardown' 232 result.addError(self, self._exc_info()) 233 234 def hasFixtures(self, ctx_callback=None): 235 context = self.context 236 if context is None: 237 return False 238 if self.implementsAnyFixture(context, ctx_callback=ctx_callback): 239 return True 240 # My context doesn't have any, but its ancestors might 241 factory = self.factory 242 if factory: 243 ancestors = factory.context.get(self, []) 244 for ancestor in ancestors: 245 if self.implementsAnyFixture( 246 ancestor, ctx_callback=ctx_callback): 247 return True 248 return False 249 250 def implementsAnyFixture(self, context, ctx_callback): 251 if isclass(context): 252 names = self.classSetup + self.classTeardown 253 else: 254 names = self.moduleSetup + self.moduleTeardown 255 if hasattr(context, '__path__'): 256 names += self.packageSetup + self.packageTeardown 257 # If my context has any fixture attribute, I have fixtures 258 fixt = False 259 for m in names: 260 if hasattr(context, m): 261 fixt = True 262 break 263 if ctx_callback is None: 264 return fixt 265 return ctx_callback(context, fixt) 266 267 def setUp(self): 268 log.debug("suite %s setUp called, tests: %s", id(self), self._tests) 269 if not self: 270 # I have no tests 271 log.debug("suite %s has no tests", id(self)) 272 return 273 if self.was_setup: 274 log.debug("suite %s already set up", id(self)) 275 return 276 context = self.context 277 if context is None: 278 return 279 # before running my own context's setup, I need to 280 # ask the factory if my context's contexts' setups have been run 281 factory = self.factory 282 if factory: 283 # get a copy, since we'll be destroying it as we go 284 ancestors = factory.context.get(self, [])[:] 285 while ancestors: 286 ancestor = ancestors.pop() 287 log.debug("ancestor %s may need setup", ancestor) 288 if ancestor in factory.was_setup: 289 continue 290 log.debug("ancestor %s does need setup", ancestor) 291 self.setupContext(ancestor) 292 if not context in factory.was_setup: 293 self.setupContext(context) 294 else: 295 self.setupContext(context) 296 self.was_setup = True 297 log.debug("completed suite setup") 298 299 def setupContext(self, context): 300 self.config.plugins.startContext(context) 301 log.debug("%s setup context %s", self, context) 302 if self.factory: 303 if context in self.factory.was_setup: 304 return 305 # note that I ran the setup for this context, so that I'll run 306 # the teardown in my teardown 307 self.factory.was_setup[context] = self 308 if isclass(context): 309 names = self.classSetup 310 else: 311 names = self.moduleSetup 312 if hasattr(context, '__path__'): 313 names = self.packageSetup + names 314 try_run(context, names) 315 316 def shortDescription(self): 317 if self.context is None: 318 return "test suite" 319 return "test suite for %s" % self.context 320 321 def tearDown(self): 322 log.debug('context teardown') 323 if not self.was_setup or self.was_torndown: 324 log.debug( 325 "No reason to teardown (was_setup? %s was_torndown? %s)" 326 % (self.was_setup, self.was_torndown)) 327 return 328 self.was_torndown = True 329 context = self.context 330 if context is None: 331 log.debug("No context to tear down") 332 return 333 334 # for each ancestor... if the ancestor was setup 335 # and I did the setup, I can do teardown 336 factory = self.factory 337 if factory: 338 ancestors = factory.context.get(self, []) + [context] 339 for ancestor in ancestors: 340 log.debug('ancestor %s may need teardown', ancestor) 341 if not ancestor in factory.was_setup: 342 log.debug('ancestor %s was not setup', ancestor) 343 continue 344 if ancestor in factory.was_torndown: 345 log.debug('ancestor %s already torn down', ancestor) 346 continue 347 setup = factory.was_setup[ancestor] 348 log.debug("%s setup ancestor %s", setup, ancestor) 349 if setup is self: 350 self.teardownContext(ancestor) 351 else: 352 self.teardownContext(context) 353 354 def teardownContext(self, context): 355 log.debug("%s teardown context %s", self, context) 356 if self.factory: 357 if context in self.factory.was_torndown: 358 return 359 self.factory.was_torndown[context] = self 360 if isclass(context): 361 names = self.classTeardown 362 else: 363 names = self.moduleTeardown 364 if hasattr(context, '__path__'): 365 names = self.packageTeardown + names 366 try_run(context, names) 367 self.config.plugins.stopContext(context) 368 369 # FIXME the wrapping has to move to the factory? 370 def _get_wrapped_tests(self): 371 for test in self._get_tests(): 372 if isinstance(test, Test) or isinstance(test, unittest.TestSuite): 373 yield test 374 else: 375 yield Test(test, 376 config=self.config, 377 resultProxy=self.resultProxy) 378 379 _tests = property(_get_wrapped_tests, LazySuite._set_tests, None, 380 "Access the tests in this suite. Tests are returned " 381 "inside of a context wrapper.") 382 383 384class ContextSuiteFactory(object): 385 """Factory for ContextSuites. Called with a collection of tests, 386 the factory decides on a hierarchy of contexts by introspecting 387 the collection or the tests themselves to find the objects 388 containing the test objects. It always returns one suite, but that 389 suite may consist of a hierarchy of nested suites. 390 """ 391 suiteClass = ContextSuite 392 def __init__(self, config=None, suiteClass=None, resultProxy=_def): 393 if config is None: 394 config = Config() 395 self.config = config 396 if suiteClass is not None: 397 self.suiteClass = suiteClass 398 # Using a singleton to represent default instead of None allows 399 # passing resultProxy=None to turn proxying off. 400 if resultProxy is _def: 401 resultProxy = ResultProxyFactory(config=config) 402 self.resultProxy = resultProxy 403 self.suites = {} 404 self.context = {} 405 self.was_setup = {} 406 self.was_torndown = {} 407 408 def __call__(self, tests, **kw): 409 """Return ``ContextSuite`` for tests. ``tests`` may either 410 be a callable (in which case the resulting ContextSuite will 411 have no parent context and be evaluated lazily) or an 412 iterable. In that case the tests will wrapped in 413 nose.case.Test, be examined and the context of each found and a 414 suite of suites returned, organized into a stack with the 415 outermost suites belonging to the outermost contexts. 416 """ 417 log.debug("Create suite for %s", tests) 418 context = kw.pop('context', getattr(tests, 'context', None)) 419 log.debug("tests %s context %s", tests, context) 420 if context is None: 421 tests = self.wrapTests(tests) 422 try: 423 context = self.findContext(tests) 424 except MixedContextError: 425 return self.makeSuite(self.mixedSuites(tests), None, **kw) 426 return self.makeSuite(tests, context, **kw) 427 428 def ancestry(self, context): 429 """Return the ancestry of the context (that is, all of the 430 packages and modules containing the context), in order of 431 descent with the outermost ancestor last. 432 This method is a generator. 433 """ 434 log.debug("get ancestry %s", context) 435 if context is None: 436 return 437 # Methods include reference to module they are defined in, we 438 # don't want that, instead want the module the class is in now 439 # (classes are re-ancestored elsewhere). 440 if hasattr(context, 'im_class'): 441 context = context.im_class 442 elif hasattr(context, '__self__'): 443 context = context.__self__.__class__ 444 if hasattr(context, '__module__'): 445 ancestors = context.__module__.split('.') 446 elif hasattr(context, '__name__'): 447 ancestors = context.__name__.split('.')[:-1] 448 else: 449 raise TypeError("%s has no ancestors?" % context) 450 while ancestors: 451 log.debug(" %s ancestors %s", context, ancestors) 452 yield resolve_name('.'.join(ancestors)) 453 ancestors.pop() 454 455 def findContext(self, tests): 456 if callable(tests) or isinstance(tests, unittest.TestSuite): 457 return None 458 context = None 459 for test in tests: 460 # Don't look at suites for contexts, only tests 461 ctx = getattr(test, 'context', None) 462 if ctx is None: 463 continue 464 if context is None: 465 context = ctx 466 elif context != ctx: 467 raise MixedContextError( 468 "Tests with different contexts in same suite! %s != %s" 469 % (context, ctx)) 470 return context 471 472 def makeSuite(self, tests, context, **kw): 473 suite = self.suiteClass( 474 tests, context=context, config=self.config, factory=self, 475 resultProxy=self.resultProxy, **kw) 476 if context is not None: 477 self.suites.setdefault(context, []).append(suite) 478 self.context.setdefault(suite, []).append(context) 479 log.debug("suite %s has context %s", suite, 480 getattr(context, '__name__', None)) 481 for ancestor in self.ancestry(context): 482 self.suites.setdefault(ancestor, []).append(suite) 483 self.context[suite].append(ancestor) 484 log.debug("suite %s has ancestor %s", suite, ancestor.__name__) 485 return suite 486 487 def mixedSuites(self, tests): 488 """The complex case where there are tests that don't all share 489 the same context. Groups tests into suites with common ancestors, 490 according to the following (essentially tail-recursive) procedure: 491 492 Starting with the context of the first test, if it is not 493 None, look for tests in the remaining tests that share that 494 ancestor. If any are found, group into a suite with that 495 ancestor as the context, and replace the current suite with 496 that suite. Continue this process for each ancestor of the 497 first test, until all ancestors have been processed. At this 498 point if any tests remain, recurse with those tests as the 499 input, returning a list of the common suite (which may be the 500 suite or test we started with, if no common tests were found) 501 plus the results of recursion. 502 """ 503 if not tests: 504 return [] 505 head = tests.pop(0) 506 if not tests: 507 return [head] # short circuit when none are left to combine 508 suite = head # the common ancestry suite, so far 509 tail = tests[:] 510 context = getattr(head, 'context', None) 511 if context is not None: 512 ancestors = [context] + [a for a in self.ancestry(context)] 513 for ancestor in ancestors: 514 common = [suite] # tests with ancestor in common, so far 515 remain = [] # tests that remain to be processed 516 for test in tail: 517 found_common = False 518 test_ctx = getattr(test, 'context', None) 519 if test_ctx is None: 520 remain.append(test) 521 continue 522 if test_ctx is ancestor: 523 common.append(test) 524 continue 525 for test_ancestor in self.ancestry(test_ctx): 526 if test_ancestor is ancestor: 527 common.append(test) 528 found_common = True 529 break 530 if not found_common: 531 remain.append(test) 532 if common: 533 suite = self.makeSuite(common, ancestor) 534 tail = self.mixedSuites(remain) 535 return [suite] + tail 536 537 def wrapTests(self, tests): 538 log.debug("wrap %s", tests) 539 if callable(tests) or isinstance(tests, unittest.TestSuite): 540 log.debug("I won't wrap") 541 return tests 542 wrapped = [] 543 for test in tests: 544 log.debug("wrapping %s", test) 545 if isinstance(test, Test) or isinstance(test, unittest.TestSuite): 546 wrapped.append(test) 547 elif isinstance(test, ContextList): 548 wrapped.append(self.makeSuite(test, context=test.context)) 549 else: 550 wrapped.append( 551 Test(test, config=self.config, resultProxy=self.resultProxy) 552 ) 553 return wrapped 554 555 556class ContextList(object): 557 """Not quite a suite -- a group of tests in a context. This is used 558 to hint the ContextSuiteFactory about what context the tests 559 belong to, in cases where it may be ambiguous or missing. 560 """ 561 def __init__(self, tests, context=None): 562 self.tests = tests 563 self.context = context 564 565 def __iter__(self): 566 return iter(self.tests) 567 568 569class FinalizingSuiteWrapper(unittest.TestSuite): 570 """Wraps suite and calls final function after suite has 571 executed. Used to call final functions in cases (like running in 572 the standard test runner) where test running is not under nose's 573 control. 574 """ 575 def __init__(self, suite, finalize): 576 self.suite = suite 577 self.finalize = finalize 578 579 def __call__(self, *arg, **kw): 580 return self.run(*arg, **kw) 581 582 # 2.7 compat 583 def __iter__(self): 584 return iter(self.suite) 585 586 def run(self, *arg, **kw): 587 try: 588 return self.suite(*arg, **kw) 589 finally: 590 self.finalize(*arg, **kw) 591 592 593# backwards compat -- sort of 594class TestDir: 595 def __init__(*arg, **kw): 596 raise NotImplementedError( 597 "TestDir is not usable with nose 0.10. The class is present " 598 "in nose.suite for backwards compatibility purposes but it " 599 "may not be used.") 600 601 602class TestModule: 603 def __init__(*arg, **kw): 604 raise NotImplementedError( 605 "TestModule is not usable with nose 0.10. The class is present " 606 "in nose.suite for backwards compatibility purposes but it " 607 "may not be used.")