PageRenderTime 54ms CodeModel.GetById 19ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

/nose/suite.py

https://bitbucket.org/jpellerin/nose/
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.")