summaryrefslogtreecommitdiff
path: root/testtools/tests/samplecases.py
blob: 07447cbefef087b58fdd703dc2b31c1d6506978e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# Copyright (c) 2015 testtools developers. See LICENSE for details.

"""A collection of sample TestCases.

These are primarily of use in testing the test framework.
"""

from testscenarios import multiply_scenarios

from testtools import TestCase
from testtools.matchers import (
    AfterPreprocessing,
    Contains,
    Equals,
    MatchesDict,
    MatchesListwise,
)


def make_test_case(test_method_name, set_up=None, test_body=None,
                   tear_down=None, cleanups=(), pre_set_up=None,
                   post_tear_down=None):
    """Make a test case with the given behaviors.

    All callables are unary callables that receive this test as their argument.

    :param str test_method_name: The name of the test method.
    :param callable set_up: Implementation of setUp.
    :param callable test_body: Implementation of the actual test. Will be
        assigned to the test method.
    :param callable tear_down: Implementation of tearDown.
    :param cleanups: Iterable of callables that will be added as cleanups.
    :param callable pre_set_up: Called before the upcall to setUp().
    :param callable post_tear_down: Called after the upcall to tearDown().

    :return: A ``testtools.TestCase``.
    """
    set_up = set_up if set_up else _do_nothing
    test_body = test_body if test_body else _do_nothing
    tear_down = tear_down if tear_down else _do_nothing
    pre_set_up = pre_set_up if pre_set_up else _do_nothing
    post_tear_down = post_tear_down if post_tear_down else _do_nothing
    return _ConstructedTest(
        test_method_name, set_up, test_body, tear_down, cleanups,
        pre_set_up, post_tear_down,
    )


class _ConstructedTest(TestCase):
    """A test case defined by arguments, rather than overrides."""

    def __init__(self, test_method_name, set_up, test_body, tear_down,
                 cleanups, pre_set_up, post_tear_down):
        """Construct a test case.

        See ``make_test_case`` for full documentation.
        """
        setattr(self, test_method_name, self.test_case)
        super().__init__(test_method_name)
        self._set_up = set_up
        self._test_body = test_body
        self._tear_down = tear_down
        self._test_cleanups = cleanups
        self._pre_set_up = pre_set_up
        self._post_tear_down = post_tear_down

    def setUp(self):
        self._pre_set_up(self)
        super().setUp()
        for cleanup in self._test_cleanups:
            self.addCleanup(cleanup, self)
        self._set_up(self)

    def test_case(self):
        self._test_body(self)

    def tearDown(self):
        self._tear_down(self)
        super().tearDown()
        self._post_tear_down(self)


def _do_nothing(case):
    pass


_success = _do_nothing


def _error(case):
    1/0  # arbitrary non-failure exception


def _failure(case):
    case.fail('arbitrary failure')


def _skip(case):
    case.skipTest('arbitrary skip message')


def _expected_failure(case):
    case.expectFailure('arbitrary expected failure', _failure, case)


def _unexpected_success(case):
    case.expectFailure('arbitrary unexpected success', _success, case)


behaviors = [
    ('success', _success),
    ('fail', _failure),
    ('error',  _error),
    ('skip', _skip),
    ('xfail', _expected_failure),
    ('uxsuccess', _unexpected_success),
]


def _make_behavior_scenarios(stage):
    """Given a test stage, iterate over behavior scenarios for that stage.

    e.g.
        >>> list(_make_behavior_scenarios('set_up'))
        [('set_up=success', {'set_up_behavior': <function _success>}),
         ('set_up=fail', {'set_up_behavior': <function _failure>}),
         ('set_up=error', {'set_up_behavior': <function _error>}),
         ('set_up=skip', {'set_up_behavior': <function _skip>}),
         ('set_up=xfail', {'set_up_behavior': <function _expected_failure>),
         ('set_up=uxsuccess',
          {'set_up_behavior': <function _unexpected_success>})]

    Ordering is not consistent.
    """
    return (
        (f'{stage}={behavior}',
         {f'{stage}_behavior': function})
        for (behavior, function) in behaviors
    )


def make_case_for_behavior_scenario(case):
    """Given a test with a behavior scenario installed, make a TestCase."""
    cleanup_behavior = getattr(case, 'cleanup_behavior', None)
    cleanups = [cleanup_behavior] if cleanup_behavior else []
    return make_test_case(
        case.getUniqueString(),
        set_up=getattr(case, 'set_up_behavior', _do_nothing),
        test_body=getattr(case, 'body_behavior', _do_nothing),
        tear_down=getattr(case, 'tear_down_behavior', _do_nothing),
        cleanups=cleanups,
        pre_set_up=getattr(case, 'pre_set_up_behavior', _do_nothing),
        post_tear_down=getattr(case, 'post_tear_down_behavior', _do_nothing),
    )


class _SetUpFailsOnGlobalState(TestCase):
    """Fail to upcall setUp on first run. Fail to upcall tearDown after.

    This simulates a test that fails to upcall in ``setUp`` if some global
    state is broken, and fails to call ``tearDown`` when the global state
    breaks but works after that.
    """

    first_run = True

    def setUp(self):
        if not self.first_run:
            return
        super().setUp()

    def test_success(self):
        pass

    def tearDown(self):
        if not self.first_run:
            super().tearDown()
        self.__class__.first_run = False

    @classmethod
    def make_scenario(cls):
        case = cls('test_success')
        return {
            'case': case,
            'expected_first_result': _test_error_traceback(
                case, Contains('TestCase.tearDown was not called')),
            'expected_second_result': _test_error_traceback(
                case, Contains('TestCase.setUp was not called')),
        }


def _test_error_traceback(case, traceback_matcher):
    """Match result log of single test that errored out.

    ``traceback_matcher`` is applied to the text of the traceback.
    """
    return MatchesListwise([
        Equals(('startTest', case)),
        MatchesListwise([
            Equals('addError'),
            Equals(case),
            MatchesDict({
                'traceback': AfterPreprocessing(
                    lambda x: x.as_text(),
                    traceback_matcher,
                )
            })
        ]),
        Equals(('stopTest', case)),
    ])


"""
A list that can be used with testscenarios to test every deterministic sample
case that we have.
"""
deterministic_sample_cases_scenarios = multiply_scenarios(
    _make_behavior_scenarios('set_up'),
    _make_behavior_scenarios('body'),
    _make_behavior_scenarios('tear_down'),
    _make_behavior_scenarios('cleanup'),
) + [
    ('tear_down_fails_after_upcall', {
        'post_tear_down_behavior': _error,
    }),
]


"""
A list that can be used with testscenarios to test every non-deterministic
sample case that we have.
"""
nondeterministic_sample_cases_scenarios = [
    ('setup-fails-global-state', _SetUpFailsOnGlobalState.make_scenario()),
]