Skip to content

Commit 0feeb26

Browse files
committed
Merge pull request #14 from nicoddemus/wait-signal
Wait signal; also fixes #12 and #13.
2 parents f2f7f91 + b5f5e04 commit 0feeb26

File tree

3 files changed

+201
-4
lines changed

3 files changed

+201
-4
lines changed

docs/index.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,37 @@ created earlier::
116116
assert window.filesTable.item(0, 0).text() == 'video1.avi'
117117
assert window.filesTable.item(1, 0).text() == 'video2.avi'
118118
119-
120-
And that's it for this quick tutorial!
119+
120+
121+
Waiting for threads, processes, etc.
122+
====================================
123+
124+
If your program has long running cumputations running in other threads or
125+
processes, you can use :meth:`qtbot.waitSignal <pytestqt.plugin.QtBot.waitSignal>`
126+
to block a test until a signal is emitted (such as ``QThread.finished``) or a
127+
timeout is reached. This makes it easy to write tests that wait until a
128+
computation running in another thread or process is completed before
129+
ensuring the results are correct::
130+
131+
def test_long_computation(qtbot):
132+
app = Application()
133+
134+
# Watch for the app.worker.finished signal, then start the worker.
135+
with qtbot.waitSignal(app.worker.finished, timeout=10000) as blocker:
136+
blocker.connect(app.worker.failed) # Can add other signals to blocker
137+
app.worker.start()
138+
# Test will wait here until either signal is emitted, or 10 seconds has elapsed
139+
140+
assert blocker.signal_triggered # Assuming the work took less than 10 seconds
141+
assert_application_results(app)
142+
121143

122144
QtBot
123145
=====
124146

125147
.. module:: pytestqt.plugin
126148
.. autoclass:: QtBot
149+
.. autoclass:: SignalBlocker
127150

128151
Versioning
129152
==========

pytestqt/_tests/test_wait_signal.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
import time
3+
4+
from pytestqt.qt_compat import QtCore, Signal
5+
6+
7+
class Signaller(QtCore.QObject):
8+
9+
signal = Signal()
10+
11+
12+
def test_signal_blocker_exception(qtbot):
13+
"""
14+
Make sure waitSignal without signals and timeout doesn't hang, but raises
15+
ValueError instead.
16+
"""
17+
with pytest.raises(ValueError):
18+
qtbot.waitSignal(None, None).wait()
19+
20+
21+
def explicit_wait(qtbot, signal, timeout):
22+
"""
23+
Explicit wait for the signal using blocker API.
24+
"""
25+
blocker = qtbot.waitSignal(signal, timeout)
26+
assert blocker.signal_triggered is None
27+
blocker.wait()
28+
return blocker
29+
30+
31+
def context_manager_wait(qtbot, signal, timeout):
32+
"""
33+
Waiting for signal using context manager API.
34+
"""
35+
with qtbot.waitSignal(signal, timeout) as blocker:
36+
pass
37+
return blocker
38+
39+
40+
@pytest.mark.parametrize(
41+
('wait_function', 'emit_delay', 'timeout', 'expected_signal_triggered'),
42+
[
43+
(explicit_wait, 500, 2000, True),
44+
(explicit_wait, 500, None, True),
45+
(context_manager_wait, 500, 2000, True),
46+
(context_manager_wait, 500, None, True),
47+
(explicit_wait, 2000, 500, False),
48+
(context_manager_wait, 2000, 500, False),
49+
]
50+
)
51+
def test_signal_triggered(qtbot, wait_function, emit_delay, timeout,
52+
expected_signal_triggered):
53+
"""
54+
Testing for a signal in different conditions, ensuring we are obtaining
55+
the expected results.
56+
"""
57+
signaller = Signaller()
58+
QtCore.QTimer.singleShot(emit_delay, signaller.signal.emit)
59+
60+
# block signal until either signal is emitted or timeout is reached
61+
start_time = time.time()
62+
blocker = wait_function(qtbot, signaller.signal, timeout)
63+
64+
# Check that event loop exited.
65+
assert not blocker._loop.isRunning()
66+
67+
# ensure that either signal was triggered or timeout occurred
68+
assert blocker.signal_triggered == expected_signal_triggered
69+
70+
# Check that we exited by the earliest parameter; timeout = None means
71+
# wait forever, so ensure we waited at most 4 times emit-delay
72+
if timeout is None:
73+
timeout = emit_delay * 4
74+
max_wait_ms = max(emit_delay, timeout)
75+
assert time.time() - start_time < (max_wait_ms / 1000.0)

pytestqt/plugin.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import pytest
66

7-
from pytestqt.qt_compat import QtGui
8-
from pytestqt.qt_compat import QtTest
7+
from pytestqt.qt_compat import QtCore, QtGui, QtTest
98

109

1110
def _inject_qtest_methods(cls):
@@ -61,6 +60,7 @@ class QtBot(object):
6160
.. automethod:: addWidget
6261
.. automethod:: waitForWindowShown
6362
.. automethod:: stopForInteraction
63+
.. automethod:: waitSignal
6464
6565
**Raw QTest API**
6666
@@ -212,6 +212,105 @@ def stopForInteraction(self):
212212

213213
stop = stopForInteraction
214214

215+
def waitSignal(self, signal=None, timeout=1000):
216+
"""
217+
Stops current test until a signal is triggered.
218+
219+
Used to stop the control flow of a test until a signal is emitted, or
220+
a number of milliseconds, specified by ``timeout``, has elapsed.
221+
222+
Best used as a context manager::
223+
224+
with qtbot.waitSignal(signal, timeout=1000):
225+
long_function_that_calls_signal()
226+
227+
Also, you can use the :class:`SignalBlocker` directly if the context
228+
manager form is not convenient::
229+
230+
blocker = qtbot.waitSignal(signal, timeout=1000)
231+
blocker.connect(other_signal)
232+
long_function_that_calls_signal()
233+
blocker.wait()
234+
235+
:param Signal signal:
236+
A signal to wait for. Set to ``None`` to just use timeout.
237+
:param int timeout:
238+
How many milliseconds to wait before resuming control flow.
239+
:returns:
240+
``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait.
241+
242+
.. note::
243+
Cannot have both ``signals`` and ``timeout`` equal ``None``, or
244+
else you will block indefinitely. We throw an error if this occurs.
245+
246+
"""
247+
blocker = SignalBlocker(timeout=timeout)
248+
if signal is not None:
249+
blocker.connect(signal)
250+
return blocker
251+
252+
253+
class SignalBlocker(object):
254+
"""
255+
Returned by :meth:`QtBot.waitSignal` method.
256+
257+
.. automethod:: wait
258+
.. automethod:: connect
259+
260+
:ivar int timeout: maximum time to wait for a signal to be triggered. Can
261+
be changed before :meth:`wait` is called.
262+
263+
:ivar bool signal_triggered: set to ``True`` if a signal was triggered, or
264+
``False`` if timeout was reached instead. Until :meth:`wait` is called,
265+
this is set to ``None``.
266+
"""
267+
268+
def __init__(self, timeout=1000):
269+
self._loop = QtCore.QEventLoop()
270+
self._signals = []
271+
self.timeout = timeout
272+
self.signal_triggered = None
273+
274+
def wait(self):
275+
"""
276+
Waits until either condition signal is triggered or
277+
timeout is reached.
278+
279+
:raise ValueError: if no signals are connected and timeout is None; in
280+
this case it would wait forever.
281+
"""
282+
if self.timeout is None and len(self._signals) == 0:
283+
raise ValueError("No signals or timeout specified.")
284+
if self.timeout is not None:
285+
QtCore.QTimer.singleShot(self.timeout, self._loop.quit)
286+
self.signal_triggered = False
287+
self._loop.exec_()
288+
289+
def connect(self, signal):
290+
"""
291+
Connects to the given signal, making :meth:`wait()` return once this signal
292+
is emitted.
293+
294+
:param signal: QtCore.Signal
295+
"""
296+
signal.connect(self._quit_loop_by_signal)
297+
self._signals.append(signal)
298+
299+
300+
def _quit_loop_by_signal(self):
301+
"""
302+
quits the event loop and marks that we finished because of a signal.
303+
"""
304+
self.signal_triggered = True
305+
self._loop.quit()
306+
307+
def __enter__(self):
308+
# Return self for testing purposes. Generally not needed.
309+
return self
310+
311+
def __exit__(self, type, value, traceback):
312+
self.wait()
313+
215314

216315
def pytest_configure(config):
217316
"""

0 commit comments

Comments
 (0)