Skip to content

Commit a78eece

Browse files
committed
Add waitSignals.
1 parent e66e58f commit a78eece

File tree

2 files changed

+193
-8
lines changed

2 files changed

+193
-8
lines changed

pytestqt/_tests/test_wait_signal.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
class Signaller(QtCore.QObject):
88

99
signal = Signal()
10+
signal_2 = Signal()
1011

1112

1213
def test_signal_blocker_exception(qtbot):
@@ -18,21 +19,26 @@ def test_signal_blocker_exception(qtbot):
1819
qtbot.waitSignal(None, None).wait()
1920

2021

21-
def explicit_wait(qtbot, signal, timeout):
22+
def explicit_wait(qtbot, signal, timeout, multiple):
2223
"""
2324
Explicit wait for the signal using blocker API.
2425
"""
25-
blocker = qtbot.waitSignal(signal, timeout)
26-
assert not blocker.signal_triggered
26+
func = qtbot.waitSignals if multiple else qtbot.waitSignal
27+
blocker = func(signal, timeout)
28+
if multiple:
29+
assert not blocker.signals_triggered
30+
else:
31+
assert not blocker.signal_triggered
2732
blocker.wait()
2833
return blocker
2934

3035

31-
def context_manager_wait(qtbot, signal, timeout):
36+
def context_manager_wait(qtbot, signal, timeout, multiple):
3237
"""
3338
Waiting for signal using context manager API.
3439
"""
35-
with qtbot.waitSignal(signal, timeout) as blocker:
40+
func = qtbot.waitSignals if multiple else qtbot.waitSignal
41+
with func(signal, timeout) as blocker:
3642
pass
3743
return blocker
3844

@@ -55,14 +61,15 @@ def test_signal_triggered(qtbot, wait_function, emit_delay, timeout,
5561
the expected results.
5662
"""
5763
signaller = Signaller()
64+
5865
timer = QtCore.QTimer()
5966
timer.setSingleShot(True)
6067
timer.timeout.connect(signaller.signal.emit)
6168
timer.start(emit_delay)
6269

6370
# block signal until either signal is emitted or timeout is reached
6471
start_time = time.time()
65-
blocker = wait_function(qtbot, signaller.signal, timeout)
72+
blocker = wait_function(qtbot, signaller.signal, timeout, multiple=False)
6673

6774
# Check that event loop exited.
6875
assert not blocker._loop.isRunning()
@@ -78,6 +85,60 @@ def test_signal_triggered(qtbot, wait_function, emit_delay, timeout,
7885
assert time.time() - start_time < (max_wait_ms / 1000.0)
7986

8087

88+
@pytest.mark.parametrize(
89+
('wait_function', 'emit_delay_1', 'emit_delay_2', 'timeout',
90+
'expected_signals_triggered'),
91+
[
92+
(explicit_wait, 500, 600, 2000, True),
93+
(explicit_wait, 500, 600, None, True),
94+
(context_manager_wait, 500, 600, 2000, True),
95+
(context_manager_wait, 500, 600, None, True),
96+
(explicit_wait, 2000, 2000, 500, False),
97+
(explicit_wait, 500, 2000, 1000, False),
98+
(explicit_wait, 2000, 500, 1000, False),
99+
(context_manager_wait, 2000, 2000, 500, False),
100+
(context_manager_wait, 500, 2000, 1000, False),
101+
(context_manager_wait, 2000, 500, 1000, False),
102+
]
103+
)
104+
def test_signal_triggered_multiple(qtbot, wait_function, emit_delay_1,
105+
emit_delay_2, timeout,
106+
expected_signals_triggered):
107+
"""
108+
Testing for a signal in different conditions, ensuring we are obtaining
109+
the expected results.
110+
"""
111+
signaller = Signaller()
112+
113+
timer = QtCore.QTimer()
114+
timer.setSingleShot(True)
115+
timer.timeout.connect(signaller.signal.emit)
116+
timer.start(emit_delay_1)
117+
118+
timer2 = QtCore.QTimer()
119+
timer2.setSingleShot(True)
120+
timer2.timeout.connect(signaller.signal_2.emit)
121+
timer2.start(emit_delay_2)
122+
123+
# block signal until either signal is emitted or timeout is reached
124+
start_time = time.time()
125+
blocker = wait_function(qtbot, [signaller.signal, signaller.signal_2],
126+
timeout, multiple=True)
127+
128+
# Check that event loop exited.
129+
assert not blocker._loop.isRunning()
130+
131+
# ensure that either signal was triggered or timeout occurred
132+
assert blocker.signals_triggered == expected_signals_triggered
133+
134+
# Check that we exited by the earliest parameter; timeout = None means
135+
# wait forever, so ensure we waited at most 4 times emit-delay
136+
if timeout is None:
137+
timeout = max(emit_delay_1, emit_delay_2) * 4
138+
max_wait_ms = max(emit_delay_1, emit_delay_2, timeout)
139+
assert time.time() - start_time < (max_wait_ms / 1000.0)
140+
141+
81142
def test_explicit_emit(qtbot):
82143
"""
83144
Make sure an explicit emit() inside a waitSignal block works.
@@ -87,3 +148,16 @@ def test_explicit_emit(qtbot):
87148
signaller.signal.emit()
88149

89150
assert waiting.signal_triggered
151+
152+
153+
def test_explicit_emit_multiple(qtbot):
154+
"""
155+
Make sure an explicit emit() inside a waitSignal block works.
156+
"""
157+
signaller = Signaller()
158+
with qtbot.waitSignals([signaller.signal, signaller.signal_2],
159+
timeout=5000) as waiting:
160+
signaller.signal.emit()
161+
signaller.signal_2.emit()
162+
163+
assert waiting.signals_triggered

pytestqt/plugin.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class QtBot(object):
7272
**Signals**
7373
7474
.. automethod:: waitSignal
75+
.. automethod:: waitSignals
7576
7677
**Raw QTest API**
7778
@@ -236,7 +237,7 @@ def stopForInteraction(self):
236237
def waitSignal(self, signal=None, timeout=1000):
237238
"""
238239
.. versionadded:: 1.2
239-
240+
240241
Stops current test until a signal is triggered.
241242
242243
Used to stop the control flow of a test until a signal is emitted, or
@@ -273,6 +274,50 @@ def waitSignal(self, signal=None, timeout=1000):
273274

274275
wait_signal = waitSignal # pep-8 alias
275276

277+
def waitSignals(self, signals=None, timeout=1000):
278+
"""
279+
.. versionadded:: 1.4
280+
281+
Stops current test until all given signals are triggered.
282+
283+
Used to stop the control flow of a test until all given signals are
284+
emitted, or a number of milliseconds, specified by ``timeout``, has
285+
elapsed.
286+
287+
Best used as a context manager::
288+
289+
with qtbot.waitSignals(signal, timeout=1000):
290+
long_function_that_calls_signal()
291+
292+
Also, you can use the :class:`MultiSignalBlocker` directly if the
293+
context manager form is not convenient::
294+
295+
blocker = qtbot.waitSignals(signal, timeout=1000)
296+
blocker.connect(other_signal)
297+
long_function_that_calls_signal()
298+
blocker.wait()
299+
300+
:param list signals:
301+
A list of :class:`Signal`s to wait for. Set to ``None`` to just use
302+
timeout.
303+
:param int timeout:
304+
How many milliseconds to wait before resuming control flow.
305+
:returns:
306+
``MultiSignalBlocker`` object. Call ``MultiSignalBlocker.wait()``
307+
to wait.
308+
309+
.. note::
310+
Cannot have both ``signals`` and ``timeout`` equal ``None``, or
311+
else you will block indefinitely. We throw an error if this occurs.
312+
"""
313+
blocker = MultiSignalBlocker(timeout=timeout)
314+
if signals is not None:
315+
for signal in signals:
316+
blocker.add_signal(signal)
317+
return blocker
318+
319+
wait_signals = waitSignals # pep-8 alias
320+
276321

277322
class SignalBlocker(object):
278323

@@ -335,6 +380,72 @@ def __exit__(self, type, value, traceback):
335380
self.wait()
336381

337382

383+
class MultiSignalBlocker(object):
384+
385+
"""
386+
Returned by :meth:`QtBot.waitSignals` method.
387+
388+
.. automethod:: wait
389+
.. automethod:: add_signal
390+
391+
:ivar int timeout: maximum time to wait for a signal to be triggered. Can
392+
be changed before :meth:`wait` is called.
393+
394+
:ivar bool signals_triggered: set to ``True`` if all signals were
395+
triggered, or ``False`` if timeout was reached instead. Until
396+
:meth:`wait` is called, this is set to ``None``.
397+
"""
398+
399+
def __init__(self, timeout=1000):
400+
self._loop = QtCore.QEventLoop()
401+
self._signals = {}
402+
self.timeout = timeout
403+
self.signals_triggered = False
404+
405+
def wait(self):
406+
"""
407+
Waits until either a connected signal is triggered or timeout is reached.
408+
409+
:raise ValueError: if no signals are connected and timeout is None; in
410+
this case it would wait forever.
411+
"""
412+
if self.signals_triggered:
413+
return
414+
if self.timeout is None and not self._signals:
415+
raise ValueError("No signals or timeout specified.")
416+
if self.timeout is not None:
417+
QtCore.QTimer.singleShot(self.timeout, self._loop.quit)
418+
self._loop.exec_()
419+
420+
def add_signal(self, signal):
421+
"""
422+
Adds the given signal to the list of signals which :meth:`wait()` waits
423+
for.
424+
425+
:param signal: QtCore.Signal
426+
"""
427+
self._signals[signal] = False
428+
signal.connect(functools.partial(self._signal_emitted, signal))
429+
430+
def _signal_emitted(self, signal):
431+
"""
432+
Called when a given signal is emitted.
433+
434+
If all expected signals have been emitted, quits the event loop and
435+
marks that we finished because signals.
436+
"""
437+
self._signals[signal] = True
438+
if all(self._signals.values()):
439+
self.signals_triggered = True
440+
self._loop.quit()
441+
442+
def __enter__(self):
443+
return self
444+
445+
def __exit__(self, type, value, traceback):
446+
self.wait()
447+
448+
338449
@contextmanager
339450
def capture_exceptions():
340451
"""
@@ -435,4 +546,4 @@ def pytest_configure(config):
435546

436547

437548
def pytest_report_header():
438-
return ['qt-api: %s' % QT_API]
549+
return ['qt-api: %s' % QT_API]

0 commit comments

Comments
 (0)