• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""TestSuite"""
2
3import sys
4
5from . import case
6from . import util
7
8__unittest = True
9
10
11def _call_if_exists(parent, attr):
12    func = getattr(parent, attr, lambda: None)
13    func()
14
15
16class BaseTestSuite(object):
17    """A simple test suite that doesn't provide class or module shared fixtures.
18    """
19    _cleanup = True
20
21    def __init__(self, tests=()):
22        self._tests = []
23        self._removed_tests = 0
24        self.addTests(tests)
25
26    def __repr__(self):
27        return "<%s tests=%s>" % (util.strclass(self.__class__), list(self))
28
29    def __eq__(self, other):
30        if not isinstance(other, self.__class__):
31            return NotImplemented
32        return list(self) == list(other)
33
34    def __iter__(self):
35        return iter(self._tests)
36
37    def countTestCases(self):
38        cases = self._removed_tests
39        for test in self:
40            if test:
41                cases += test.countTestCases()
42        return cases
43
44    def addTest(self, test):
45        # sanity checks
46        if not callable(test):
47            raise TypeError("{} is not callable".format(repr(test)))
48        if isinstance(test, type) and issubclass(test,
49                                                 (case.TestCase, TestSuite)):
50            raise TypeError("TestCases and TestSuites must be instantiated "
51                            "before passing them to addTest()")
52        self._tests.append(test)
53
54    def addTests(self, tests):
55        if isinstance(tests, str):
56            raise TypeError("tests must be an iterable of tests, not a string")
57        for test in tests:
58            self.addTest(test)
59
60    def run(self, result):
61        for index, test in enumerate(self):
62            if result.shouldStop:
63                break
64            test(result)
65            if self._cleanup:
66                self._removeTestAtIndex(index)
67        return result
68
69    def _removeTestAtIndex(self, index):
70        """Stop holding a reference to the TestCase at index."""
71        try:
72            test = self._tests[index]
73        except TypeError:
74            # support for suite implementations that have overridden self._tests
75            pass
76        else:
77            # Some unittest tests add non TestCase/TestSuite objects to
78            # the suite.
79            if hasattr(test, 'countTestCases'):
80                self._removed_tests += test.countTestCases()
81            self._tests[index] = None
82
83    def __call__(self, *args, **kwds):
84        return self.run(*args, **kwds)
85
86    def debug(self):
87        """Run the tests without collecting errors in a TestResult"""
88        for test in self:
89            test.debug()
90
91
92class TestSuite(BaseTestSuite):
93    """A test suite is a composite test consisting of a number of TestCases.
94
95    For use, create an instance of TestSuite, then add test case instances.
96    When all tests have been added, the suite can be passed to a test
97    runner, such as TextTestRunner. It will run the individual test cases
98    in the order in which they were added, aggregating the results. When
99    subclassing, do not forget to call the base class constructor.
100    """
101
102    def run(self, result, debug=False):
103        topLevel = False
104        if getattr(result, '_testRunEntered', False) is False:
105            result._testRunEntered = topLevel = True
106
107        for index, test in enumerate(self):
108            if result.shouldStop:
109                break
110
111            if _isnotsuite(test):
112                self._tearDownPreviousClass(test, result)
113                self._handleModuleFixture(test, result)
114                self._handleClassSetUp(test, result)
115                result._previousTestClass = test.__class__
116
117                if (getattr(test.__class__, '_classSetupFailed', False) or
118                    getattr(result, '_moduleSetUpFailed', False)):
119                    continue
120
121            if not debug:
122                test(result)
123            else:
124                test.debug()
125
126            if self._cleanup:
127                self._removeTestAtIndex(index)
128
129        if topLevel:
130            self._tearDownPreviousClass(None, result)
131            self._handleModuleTearDown(result)
132            result._testRunEntered = False
133        return result
134
135    def debug(self):
136        """Run the tests without collecting errors in a TestResult"""
137        debug = _DebugResult()
138        self.run(debug, True)
139
140    ################################
141
142    def _handleClassSetUp(self, test, result):
143        previousClass = getattr(result, '_previousTestClass', None)
144        currentClass = test.__class__
145        if currentClass == previousClass:
146            return
147        if result._moduleSetUpFailed:
148            return
149        if getattr(currentClass, "__unittest_skip__", False):
150            return
151
152        failed = False
153        try:
154            currentClass._classSetupFailed = False
155        except TypeError:
156            # test may actually be a function
157            # so its class will be a builtin-type
158            pass
159
160        setUpClass = getattr(currentClass, 'setUpClass', None)
161        doClassCleanups = getattr(currentClass, 'doClassCleanups', None)
162        if setUpClass is not None:
163            _call_if_exists(result, '_setupStdout')
164            try:
165                try:
166                    setUpClass()
167                except Exception as e:
168                    if isinstance(result, _DebugResult):
169                        raise
170                    failed = True
171                    try:
172                        currentClass._classSetupFailed = True
173                    except TypeError:
174                        pass
175                    className = util.strclass(currentClass)
176                    self._createClassOrModuleLevelException(result, e,
177                                                            'setUpClass',
178                                                            className)
179                if failed and doClassCleanups is not None:
180                    doClassCleanups()
181                    for exc_info in currentClass.tearDown_exceptions:
182                        self._createClassOrModuleLevelException(
183                                result, exc_info[1], 'setUpClass', className,
184                                info=exc_info)
185            finally:
186                _call_if_exists(result, '_restoreStdout')
187
188    def _get_previous_module(self, result):
189        previousModule = None
190        previousClass = getattr(result, '_previousTestClass', None)
191        if previousClass is not None:
192            previousModule = previousClass.__module__
193        return previousModule
194
195
196    def _handleModuleFixture(self, test, result):
197        previousModule = self._get_previous_module(result)
198        currentModule = test.__class__.__module__
199        if currentModule == previousModule:
200            return
201
202        self._handleModuleTearDown(result)
203
204
205        result._moduleSetUpFailed = False
206        try:
207            module = sys.modules[currentModule]
208        except KeyError:
209            return
210        setUpModule = getattr(module, 'setUpModule', None)
211        if setUpModule is not None:
212            _call_if_exists(result, '_setupStdout')
213            try:
214                try:
215                    setUpModule()
216                except Exception as e:
217                    if isinstance(result, _DebugResult):
218                        raise
219                    result._moduleSetUpFailed = True
220                    self._createClassOrModuleLevelException(result, e,
221                                                            'setUpModule',
222                                                            currentModule)
223                if result._moduleSetUpFailed:
224                    try:
225                        case.doModuleCleanups()
226                    except Exception as e:
227                        self._createClassOrModuleLevelException(result, e,
228                                                                'setUpModule',
229                                                                currentModule)
230            finally:
231                _call_if_exists(result, '_restoreStdout')
232
233    def _createClassOrModuleLevelException(self, result, exc, method_name,
234                                           parent, info=None):
235        errorName = f'{method_name} ({parent})'
236        self._addClassOrModuleLevelException(result, exc, errorName, info)
237
238    def _addClassOrModuleLevelException(self, result, exception, errorName,
239                                        info=None):
240        error = _ErrorHolder(errorName)
241        addSkip = getattr(result, 'addSkip', None)
242        if addSkip is not None and isinstance(exception, case.SkipTest):
243            addSkip(error, str(exception))
244        else:
245            if not info:
246                result.addError(error, sys.exc_info())
247            else:
248                result.addError(error, info)
249
250    def _handleModuleTearDown(self, result):
251        previousModule = self._get_previous_module(result)
252        if previousModule is None:
253            return
254        if result._moduleSetUpFailed:
255            return
256
257        try:
258            module = sys.modules[previousModule]
259        except KeyError:
260            return
261
262        _call_if_exists(result, '_setupStdout')
263        try:
264            tearDownModule = getattr(module, 'tearDownModule', None)
265            if tearDownModule is not None:
266                try:
267                    tearDownModule()
268                except Exception as e:
269                    if isinstance(result, _DebugResult):
270                        raise
271                    self._createClassOrModuleLevelException(result, e,
272                                                            'tearDownModule',
273                                                            previousModule)
274            try:
275                case.doModuleCleanups()
276            except Exception as e:
277                if isinstance(result, _DebugResult):
278                    raise
279                self._createClassOrModuleLevelException(result, e,
280                                                        'tearDownModule',
281                                                        previousModule)
282        finally:
283            _call_if_exists(result, '_restoreStdout')
284
285    def _tearDownPreviousClass(self, test, result):
286        previousClass = getattr(result, '_previousTestClass', None)
287        currentClass = test.__class__
288        if currentClass == previousClass or previousClass is None:
289            return
290        if getattr(previousClass, '_classSetupFailed', False):
291            return
292        if getattr(result, '_moduleSetUpFailed', False):
293            return
294        if getattr(previousClass, "__unittest_skip__", False):
295            return
296
297        tearDownClass = getattr(previousClass, 'tearDownClass', None)
298        doClassCleanups = getattr(previousClass, 'doClassCleanups', None)
299        if tearDownClass is None and doClassCleanups is None:
300            return
301
302        _call_if_exists(result, '_setupStdout')
303        try:
304            if tearDownClass is not None:
305                try:
306                    tearDownClass()
307                except Exception as e:
308                    if isinstance(result, _DebugResult):
309                        raise
310                    className = util.strclass(previousClass)
311                    self._createClassOrModuleLevelException(result, e,
312                                                            'tearDownClass',
313                                                            className)
314            if doClassCleanups is not None:
315                doClassCleanups()
316                for exc_info in previousClass.tearDown_exceptions:
317                    if isinstance(result, _DebugResult):
318                        raise exc_info[1]
319                    className = util.strclass(previousClass)
320                    self._createClassOrModuleLevelException(result, exc_info[1],
321                                                            'tearDownClass',
322                                                            className,
323                                                            info=exc_info)
324        finally:
325            _call_if_exists(result, '_restoreStdout')
326
327
328class _ErrorHolder(object):
329    """
330    Placeholder for a TestCase inside a result. As far as a TestResult
331    is concerned, this looks exactly like a unit test. Used to insert
332    arbitrary errors into a test suite run.
333    """
334    # Inspired by the ErrorHolder from Twisted:
335    # http://twistedmatrix.com/trac/browser/trunk/twisted/trial/runner.py
336
337    # attribute used by TestResult._exc_info_to_string
338    failureException = None
339
340    def __init__(self, description):
341        self.description = description
342
343    def id(self):
344        return self.description
345
346    def shortDescription(self):
347        return None
348
349    def __repr__(self):
350        return "<ErrorHolder description=%r>" % (self.description,)
351
352    def __str__(self):
353        return self.id()
354
355    def run(self, result):
356        # could call result.addError(...) - but this test-like object
357        # shouldn't be run anyway
358        pass
359
360    def __call__(self, result):
361        return self.run(result)
362
363    def countTestCases(self):
364        return 0
365
366def _isnotsuite(test):
367    "A crude way to tell apart testcases and suites with duck-typing"
368    try:
369        iter(test)
370    except TypeError:
371        return True
372    return False
373
374
375class _DebugResult(object):
376    "Used by the TestSuite to hold previous class when running in debug."
377    _previousTestClass = None
378    _moduleSetUpFailed = False
379    shouldStop = False
380