summaryrefslogtreecommitdiff
path: root/src/leap/testing/qunittest.py
blob: b89ccec3329db79ab509fe96f2fe912d7042058f (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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# -*- coding: utf-8 -*-

# **qunittest** is an standard Python `unittest` enhancement for PyQt4,
# allowing
# you to test asynchronous code using standard synchronous testing facility.
#
# The source for `qunittest` is available on [GitHub][gh], and released under
# the MIT license.
#
# Slightly modified by The Leap Project.

### Prerequisites

# Import unittest2 or unittest
try:
    import unittest2 as unittest
except ImportError:
    import unittest

# ... and some standard Python libraries
import sys
import functools
import contextlib
import re

# ... and several PyQt classes
from PyQt4.QtCore import QTimer
from PyQt4.QtTest import QTest
from PyQt4 import QtGui

### The code


# Override standard main method, by invoking it inside PyQt event loop

def main(*args, **kwargs):
    qapplication = QtGui.QApplication(sys.argv)

    QTimer.singleShot(0, unittest.main(*args, **kwargs))
    qapplication.exec_()

"""
This main substitute does not integrate with unittest.

Note about mixing the event loop and unittests:

Unittest will fail if we keep more than one reference to a QApplication.
(pyqt expects to be  and only one).
So, for the things that need a QApplication to exist, do something like:

    self.app = QApplication()
    QtGui.qApp = self.app

in the class setUp, and::

    QtGui.qApp = None
    self.app = None

in the class tearDown.

For some explanation about this, see
  http://stuvel.eu/blog/127/multiple-instances-of-qapplication-in-one-process
and
  http://www.riverbankcomputing.com/pipermail/pyqt/2010-September/027705.html
"""


# Helper returning the name of a given signal

def _signal_name(signal):
    s = repr(signal)
    name_re = "signal (\w+) of (\w+)"
    match = re.search(name_re, s, re.I)
    if not match:
        return "??"
    return "%s#%s" % (match.group(2), match.group(1))


class _SignalConnector(object):
    """ Encapsulates signal assertion testing """
    def __init__(self, test, signal, callable_):
        self.test = test
        self.callable_ = callable_
        self.called_with = None
        self.emited = False
        self.signal = signal
        self._asserted = False

        signal.connect(self.on_signal_emited)

    # Store given parameters and mark signal as `emited`
    def on_signal_emited(self, *args, **kwargs):
        self.called_with = (args, kwargs)
        self.emited = True

    def assertEmission(self):
        # Assert once wheter signal was emited or not
        was_asserted = self._asserted
        self._asserted = True

        if not was_asserted:
            if not self.emited:
                self.test.fail(
                    "signal %s not emited" % (_signal_name(self.signal)))

            # Call given callable is necessary
            if self.callable_:
                args, kwargs = self.called_with
                self.callable_(*args, **kwargs)

    def __enter__(self):
        # Assert emission when context is entered
        self.assertEmission()
        return self.called_with

    def __exit__(self, *_):
        return False

### Unit Testing

# `qunittest` does not force much abould how test should look - it just adds
# several helpers for asynchronous code testing.
#
# Common test case may look like this:
#
#     import qunittest
#     from calculator import Calculator
#
#     class TestCalculator(qunittest.TestCase):
#         def setUp(self):
#             self.calc = Calculator()
#
#         def test_should_add_two_numbers_synchronously(self):
#             # given
#             a, b = 2, 3
#
#             # when
#             r = self.calc.add(a, b)
#
#             # then
#             self.assertEqual(5, r)
#
#         def test_should_calculate_factorial_in_background(self):
#             # given
#
#             # when
#             self.calc.factorial(20)
#
#             # then
#             self.assertEmited(self.calc.done) with (args, kwargs):
#                 self.assertEqual([2432902008176640000], args)
#
#     if __name__ == "__main__":
#         main()
#
# Test can be run by typing:
#
#     python test_calculator.py
#
# Automatic test discovery is not supported now, because testing PyQt needs
# an instance of `QApplication` and its `exec_` method is blocking.
#


### TestCase class

class TestCase(unittest.TestCase):
    """
    Extends standard `unittest.TestCase` with several PyQt4 testing features
    useful for asynchronous testing.
    """
    def __init__(self, *args, **kwargs):
        super(TestCase, self).__init__(*args, **kwargs)

        self._clearSignalConnectors()
        self._succeeded = False
        self.addCleanup(self._clearSignalConnectors)
        self.tearDown = self._decorateTearDown(self.tearDown)

    ### Protected methods

    def _clearSignalConnectors(self):
        self._connectedSignals = []

    def _decorateTearDown(self, tearDown):
        @functools.wraps(tearDown)
        def decorator():
            self._ensureEmitedSignals()
            return tearDown()
        return decorator

    def _ensureEmitedSignals(self):
        """
        Checks if signals were acually emited. Raises AssertionError if no.
        """
        # TODO: add information about line
        for signal in self._connectedSignals:
            signal.assertEmission()

    ### Assertions

    def assertEmited(self, signal, callable_=None, timeout=1):
        """
        Asserts if given `signal` was emited. Waits 1 second by default,
        before asserts signal emission.

        If `callable_` is given, it should be a function which takes two
        arguments: `args` and `kwargs`. It will be called after blocking
        operation or when assertion about signal emission is made and
        signal was emited.

        When timeout is not `False`, method call is blocking, and ends
        after `timeout` seconds. After that time, it validates wether
        signal was emited.

        When timeout is `False`, method is non blocking, and test should wait
        for signals afterwards. Otherwise, at the end of the test, all
        signal emissions are checked if appeared.

        Function returns context, which yields to list of parameters given
        to signal. It can be useful for testing given parameters. Following
        code:

            with self.assertEmited(widget.signal) as (args, kwargs):
                self.assertEqual(1, len(args))
                self.assertEqual("Hello World!", args[0])

        will wait 1 second and test for correct parameters, is signal was
        emtied.

        Note that code:

            with self.assertEmited(widget.signal, timeout=False) as (a, k):
                # Will not be invoked

        will always fail since signal cannot be emited in the time of its
        connection - code inside the context will not be invoked at all.
        """

        connector = _SignalConnector(self, signal, callable_)
        self._connectedSignals.append(connector)
        if timeout:
            self.waitFor(timeout)
            connector.assertEmission()

        return connector

    ### Helper methods

    @contextlib.contextmanager
    def invokeAfter(self, seconds, callable_=None):
        """
        Waits given amount of time and executes the context.

        If `callable_` is given, executes it, instead of context.
        """
        self.waitFor(seconds)
        if callable_:
            callable_()
        else:
            yield

    def waitFor(self, seconds):
        """
        Waits given amount of time.

            self.widget.loadImage(url)
            self.waitFor(seconds=10)
        """
        QTest.qWait(seconds * 1000)

    def succeed(self, bool_=True):
        """ Marks test as suceeded for next `failAfter()` invocation. """
        self._succeeded = self._succeeded or bool_

    def failAfter(self, seconds, message=None):
        """
        Waits given amount of time, and fails the test if `succeed(bool)`
        is not called - in most common case, `succeed(bool)` should be called
        asynchronously (in signal handler):

            self.widget.signal.connect(lambda: self.succeed())
            self.failAfter(1, "signal not emited?")

        After invocation, test is no longer consider as succeeded.
        """
        self.waitFor(seconds)
        if not self._succeeded:
            self.fail(message)

        self._succeeded = False

### Credits
#
# * **Who is responsible:** [Dawid Fatyga][df]
# * **Source:** [GitHub][gh]
# * **Doc. generator:** [rocco][ro]
#
# [gh]: https://www.github.com/dejw/qunittest
# [df]: https://github.com/dejw
# [ro]: http://rtomayko.github.com/rocco/
#