Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Qt: How to wait for multiple signals?

I'm developing a GUI test library of sorts using PySide and Qt. So far it works quite nicely when the test case requires waiting for only one condition to happen (such as a signal or a timeout), but my problem is having to wait for multiple conditions to happen before proceeding with data verification.

The test runner works in its own thread so as not to disturb the main thread too much. Waiting for signals/timeouts happens with an event loop, and this is the part that works nicely (simplified example):

# Create a  simple event loop and fail timer (to prevent infinite waiting)
loop = QtCore.QEventLoop()
failtimer = QtCore.QTimer()
failtimer.setInterval(MAX_DELAY)
failtimer.setSingleShot(True)
failtimer.timeout.connect(loop.quit)

# Connect waitable signal to event loop
condition.connect(loop.quit) # condition is for example QLineEdit.textChanged() signal

# Perform test action
testwidget.doStuff.emit() # Function not called directly, but via signals

# Wait for condition, or fail timeout, to happen
loop.exec_()

# Verify data
assert expectedvalue == testwidget.readValue()

The waiting has to be synchronous, so an event loop is the way to go, but it does not work for multiple signals. Waiting for any of multiple conditions is of course possible, but not waiting for multiple conditions/signals to all have happened. So any advice on how to proceed with this?

I was thinking about a helper class that counts the number of signals received and then emits a ready()-signal once the required count is reached. But is this really the best way to go? The helper would also have to check each sender so that only one 'instance' of a specific signal is accounted for.

like image 408
Teemu Karimerto Avatar asked Mar 21 '23 23:03

Teemu Karimerto


2 Answers

I would personally have all the necessary signals connected to their corresponding signal handlers, aka. slots.

They would all mark their emission is "done", and there could be a check for the overall condition whether it is "done" and after each signal handler sets its own "done", there could be a global "done" check, and if that suffices, they would emit a "global done" signal.

Then you could also connect to that "global done" signal initially, and when the corresponding signal handler is triggered, you would know that is done unless the conditions changed in the meantime.

After the theoretical design, you would have something like this (pseudo code)

connect_signal1_to_slot1();
connect_signal2_to_slot2();
...
connect_global_done_signal_to_global_done_slot();

slotX: mark_conditionX_done(); if global_done: emit global_done_signal();
global_done_slot: do_foo();

You could probably also simplify by having only two signals and slots, namely: one for the local done operation that "marks" local signal done based on the argument passed, and then there would be the "global done" signal and slots.

The difference would be then the semantics, whether to use arguments with one signal and slot or many signals and slots without arguments, but it is the same theory in principle.

like image 145
lpapp Avatar answered Apr 02 '23 13:04

lpapp


I ended up implementing a rather straightforward helper class. It has a set for waitable signals and another for received signals. Each waitable signal is connected to a single slot. The slot adds the sender() to the ready-set, and once the set sizes match, emit a ready signal.

If anyone is interested, here is what I ended up doing:

from PySide.QtCore import QObject, Signal, Slot

class QMultiWait(QObject):
    ready = Signal()

    def __init__(self, parent=None):
        super(QMultiWait, self).__init__(parent)
        self._waitable = set()
        self._waitready = set()

    def addWaitableSignal(self, signal):
        if signal not in self._waitable:
            self._waitable.add(signal)
            signal.connect(self._checkSignal)

    @Slot()
    def _checkSignal(self):
        sender = self.sender()
        self._waitready.add(sender)
        if len(self._waitready) == len(self._waitable):
            self.ready.emit()

    def clear(self):
        for signal in self._waitable:
            signal.disconnect(self._checkSignal)

The clear function is hardly necessary, but allows for the class instance to be reused.

like image 37
Teemu Karimerto Avatar answered Apr 02 '23 13:04

Teemu Karimerto