pytest-qt icon indicating copy to clipboard operation
pytest-qt copied to clipboard

Incorrect waitSignal + connect

Open danicc097 opened this issue 6 months ago • 4 comments

import pytest
from PySide6.QtCore import QObject, Signal, QThread, QTimer
from PySide6.QtWidgets import QApplication
from pytestqt.qtbot import QtBot


@pytest.fixture(scope="session", autouse=True)
def qapp_instance():
    app = QApplication.instance()
    if app is None:
        app = QApplication([])
    return app


class MySignalEmitter(QObject):
    signal_A_emitted = Signal(str, int)
    signal_B_emitted = Signal(bool, float)

    def __init__(self):
        super().__init__()
        self._thread = QThread()
        self.moveToThread(self._thread)
        self._thread.started.connect(self._keep_thread_alive)

    def _keep_thread_alive(self):

        pass

    def start(self):
        if not self._thread.isRunning():
            self._thread.start()

    def stop(self):
        if self._thread.isRunning():
            self._thread.quit()
            self._thread.wait(500)

    def emit_signal_A(self, message: str, code: int):

        if self._thread.isRunning():
            QTimer.singleShot(0, lambda: self.signal_A_emitted.emit(message, code))
        else:
            self.signal_A_emitted.emit(message, code)


    def emit_signal_B(self, status: bool, value: float):
        if self._thread.isRunning():
            QTimer.singleShot(0, lambda: self.signal_B_emitted.emit(status, value))
        else:
            self.signal_B_emitted.emit(status, value)




def test_signals(qtbot: QtBot):
    emitter = MySignalEmitter()
    emitter.start()

    with qtbot.waitSignal((emitter.signal_A_emitted, "A"), timeout=3000, raising=True) as blocker:
        blocker.connect((emitter.signal_B_emitted, "B"))
        emitter.emit_signal_B(True, 3.14)

    assert blocker.signal_triggered is True
    assert blocker.args == [True, 3.14]
    assert blocker.signal_name == "B"

    with qtbot.waitSignal((emitter.signal_A_emitted, "A"), timeout=3000, raising=True) as blocker:
        blocker.connect((emitter.signal_B_emitted, "B"))
        emitter.emit_signal_A("message", 0)

    assert blocker.signal_triggered is True
    assert blocker.args == ["message", 0]
    assert blocker.signal_name == "A"

    emitter.stop()

will fail with:

>       assert blocker.signal_name == "A"
E       AssertionError: assert 'B' == 'A'
E
E         - A
E         + B

src\tests\issue_pytestqt_test.py:73: AssertionError

what is the workaround to have an OR condition?

danicc097 avatar Jun 13 '25 10:06 danicc097

Hi @danicc097,

I'm not sure I understand why emitter.emit_signal_A ends up emitting signal_B_emitted. 🤔

nicoddemus avatar Jun 17 '25 13:06 nicoddemus

Ok, looking at connect I see it overrides self.signal_name, so thats the issue. MultiSignalBlocker waits for all signals so I don't know if it'd be simpler to allow it to wait for the first n signals instead of updating the SignalBlocker

danicc097 avatar Jun 17 '25 14:06 danicc097

Ok, looking at connect I see it overrides self.signal_name, so thats the issue.

I see thanks.

MultiSignalBlocker waits for all signals

There's blocker.all_signals_and_args, perhaps that helps?

nicoddemus avatar Jun 17 '25 16:06 nicoddemus

Ok, looking at connect I see it overrides self.signal_name, so thats the issue.

I see thanks.

MultiSignalBlocker waits for all signals

There's blocker.all_signals_and_args, perhaps that helps?

Unfortunately MultiSignalBlocker would still require waiting for both signals.

For my tests the only reason I wanted this OR condition was to skip waiting for long timeouts when tests fail and the wrong signals are triggered (say error instead of success), so assertNotEmitted fits more in that case.

Regarding the original issue, I think the signal_name should be corrected once any of the connected signals is triggered

danicc097 avatar Jun 17 '25 16:06 danicc097