Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

A very simple way to do this, in C++, would be to:

  1. Have a set of (object, signal index) pairs that you expect to be signaled.

  2. Copy the set before the wait is started.

  3. In the slot, remove the (sender(), senderSignalIndex()) element from the copied list. If the list is empty, you know that you're done.

The benefit of this solution is portability: the approach works in both PySide and C++.

In C++, connect() is customarily called with the method arguments wrapped in a SIGNAL or SLOT macro. Those macros prepend a method code: either '0', '1' or '2' to indicate whether it's an invokable method, a signal, or a slot. This method code is skipped when calling registerSignal, as it expects a raw method name.

Since indexOfMethod called in registerSignal needs a normalized signature, the connect method normalizes it.

class SignalMerge : public QObject {
    Q_OBJECT
#if QT_VERSION>=QT_VERSION_CHECK(5,0,0)
    typedef QMetaObject::Connection Connection;
#else
    typedef bool Connection;
#endif
    typedef QPair<QObject*, int> ObjectMethod;
    QSet<ObjectMethod> m_signals, m_pendingSignals;

    void registerSignal(QObject * obj, const char * method) {
        int index = obj->metaObject()->indexOfMethod(method);
        if (index < 0) return;
        m_signals.insert(ObjectMethod(obj, index));
    }
    Q_SLOT void merge() {
        if (m_pendingSignals.isEmpty()) m_pendingSignals = m_signals;
        m_pendingSignals.remove(ObjectMethod(sender(), senderSignalIndex()));
        if (m_pendingSignals.isEmpty()) emit merged();
    }
public:

    void clear() {
        foreach (ObjectMethod om, m_signals) {
            QMetaObject::disconnect(om.first, om.second, this, staticMetaObject.indexOfSlot("merge()"));
        }
        m_signals.clear();
        m_pendingSignals.clear();
    }
    Q_SIGNAL void merged();
    Connection connect(QObject *sender, const char *signal, Qt::ConnectionType type = Qt::AutoConnection) {
        Connection conn = QObject::connect(sender, signal, this, SLOT(merge()), type);
        if (conn) registerSignal(sender, QMetaObject::normalizedSignature(signal+1));
        return conn;
    }
};
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top