4

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.

László Papp
  • 51,870
  • 39
  • 111
  • 135
Teemu Karimerto
  • 395
  • 5
  • 12
  • "So any advice on how to proceed with this?" I believe you want to [mock](http://en.wikipedia.org/wiki/Mock_object) the Qt library instead. Much easier, you have much more control. Also your current tests are somehow testing *the Qt library* more than your code. – Bakuriu Jan 14 '14 at 08:05
  • @LaszloPapp See [`unittest.mock`](http://docs.python.org/dev/library/unittest.mock) for example. – Bakuriu Jan 14 '14 at 08:12
  • @Bakuriu: I am not sure how mock is any relevant to the multiple signal concern. – László Papp Jan 14 '14 at 08:14
  • @LaszloPapp If you mock the Qt library then you can simply check which signals were called through the mock objects after the timeout. – Bakuriu Jan 14 '14 at 08:17
  • @Bakuriu: I had been working on a QtMock library, but it has not progessed, so there is no "Qt friendly" mocking framework yet. More importantly, I think it would be nicer to have a qt event loop based solution rather than a drastical timeout. – László Papp Jan 14 '14 at 08:21
  • @LaszloPapp I agree with you that a clean event loop solution is what I'm preferably looking for, not a whole mock library. – Teemu Karimerto Jan 14 '14 at 08:26

3 Answers3

3

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.

László Papp
  • 51,870
  • 39
  • 111
  • 135
  • This sounds like one way to go, but the problem with that approach is that I do not necessarily know beforehand how many specific slots I might need. The test cases are in a separate .xml file and are not written by me, so there may be one or half a dozen. – Teemu Karimerto Jan 14 '14 at 08:23
  • @TeemuKarimerto: I do not see the problem with that? What is your concern? You could of course put everything into one signal-slot relation as well through parameter(s), and then the settings and global check would be done in one slot. – László Papp Jan 14 '14 at 08:24
  • Now that I think about it, the multiple different slots approach can indeed work. I simply have to keep track of which slots I have already assigned and just pick the next one when adding a new condition. Fortunately this is all relatively simple with Python :) Another way I was thinking was making a dict of received signals and checking whether the size of the dict is equal to waitable condition count and then emitting the 'global' ready() signal. This however might have unforeseen problems? – Teemu Karimerto Jan 14 '14 at 08:31
  • @TeemuKarimerto: I do not see that as a different way. It seems to me the same concept. You add a local done, and check against global done, whethe it is a count (int), bool, or something else, that is just detail IMO. – László Papp Jan 14 '14 at 08:38
  • Yes you are right about that. I shall go ahead and implement this. Thank you for the advice. – Teemu Karimerto Jan 14 '14 at 08:53
  • This is one way of doing it, but it ignores the presence of sender() and senderSignalIndex(), [both present in at least the most recent PySide](http://pyside.github.io/docs/pyside/PySide/QtCore/QObject.html) ;) – Kuba hasn't forgotten Monica Jan 15 '14 at 16:40
  • @KubaOber: I do not see the difference. Sender is the "argument", so it remains the same solution. Actually, Teemu even implemented it, already, and in Python. Not sure why you submit the same (or at least very similar) in C++ to a python question, but I will leave it with you. :-) – László Papp Jan 15 '14 at 16:52
  • My solution works also in C++ where there's no notion of signal as an object. In PySide there are such, of course. – Kuba hasn't forgotten Monica Jan 15 '14 at 17:10
  • @KubaOber: the question is about pyside, not C++, and also, the solution in Teemu's answer (using "sender") works fine in C++, too. – László Papp Jan 15 '14 at 17:13
3

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.

Teemu Karimerto
  • 395
  • 5
  • 12
1

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;
    }
};
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • This is also quite an interesting approach, though similar to what I already implemented. However for future reference and especially for C++ this is indeed good stuff. I had not known about `senderSignalIndex()` before. Could you explain why registerSignal is called with `(sender, signal+1)`, as in why the `+1` to skip a character? – Teemu Karimerto Jan 16 '14 at 07:17