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

QApplication.processEvents() blocks qtbot on MacOS

Open impact27 opened this issue 5 years ago • 12 comments

If qtbot tries to stop waiting while QApplication.processEvents() is being called, the test will hang indefinitely. What happens is that:

  • qtbot creates a QEventLoop
  • an event calling QApplication.processEvents() is processed by QEventLoop
  • The qtbot timer event that calls _quit_loop_by_timeout is processed by QApplication.processEvents()
  • Somehow self._loop.quit() doesn't quit the loop if it is called from QApplication.processEvents()

Code to reproduce (save as test_qtbot.py):

import pytest
import time
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QEventLoop, QTimer


class MyWidget(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._timer = QTimer(self)
        self._timer.setSingleShot(True)
        self._timer.setInterval(10)
        self._timer.timeout.connect(self._wait_process_events)
        self._timer.start()

    def _wait_process_events(self):
        time.sleep(1)
        QApplication.processEvents()


def test_qtbot(qtbot):
    """
    qtbot does the following:
    1. start QEventLoop
    2. process an event that calls QApplication.processEvents()
    3. this processes an event that calls loop.quit()
        which doesn't quit the wait loop
    """
    widget = MyWidget()
    qtbot.addWidget(widget)
    widget.show()
    qtbot.wait(500)

if __name__ == "__main__":
    pytest.main()

Versions I am using:

PyQt5                                   5.12.3
pytest                                  5.3.1         
pytest-qt                               3.2.2  

impact27 avatar Jan 17 '20 09:01 impact27

Sorry for the late answer! FWIW your code runs fine for me, with the same versions you're running:

============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-5.3.1, py-1.8.1, pluggy-0.13.1
PyQt5 5.12.3 -- Qt runtime 5.12.8 -- Qt compiled 5.12.8
rootdir: /home/florian/proj/pytest-qt, inifile: setup.cfg
plugins: qt-3.2.2
collected 1 item                                                               

test_qtbot.py .                                                          [100%]

============================== 1 passed in 1.13s ===============================

The-Compiler avatar May 08 '20 14:05 The-Compiler

I tested it again and still have the problem. Maybe it is OSX specific?

% python3 test_qtbot.py 
============================= test session starts ==============================
platform darwin -- Python 3.7.7, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
PyQt5 5.12.3 -- Qt runtime 5.12.6 -- Qt compiled 5.12.6
rootdir: /Users/quentinpeter/Desktop
plugins: mock-1.12.1, lazy-fixture-0.6.2, flaky-3.6.1, ordering-0.6, qt-3.2.2
collected 1 item                                                               

test_qtbot.py 

impact27 avatar May 08 '20 18:05 impact27

Yep, I can reproduce on macOS, even with pytest 5.4.2, pytest-qt 3.3.0 and PyQt5 5.14.2. Using -o faulthandler_timeout=10 confirms that it hangs in qtbot.wait:

Thread 0x00007fffa5eb8380 (most recent call first):
  File "/Users/florian/tmp/.venv/lib/python3.7/site-packages/pytestqt/wait_signal.py", line 51 in wait
  File "/Users/florian/tmp/.venv/lib/python3.7/site-packages/pytestqt/qtbot.py", line 448 in wait
  File "/Users/florian/tmp/test_qtbot.py", line 32 in test_qtbot
  [...]

Works fine on Windows as well FWIW.

The-Compiler avatar May 09 '20 21:05 The-Compiler

Yeah works fine for me too. Not sure how to proceed here I'm afraid, it might be something in your system. 😕

nicoddemus avatar May 14 '20 19:05 nicoddemus

But @The-Compiler replicated the bug on macOS?

impact27 avatar May 15 '20 06:05 impact27

on a similar note (apologies if I missed it in the docs), qtbot hangs if itemClicked signals are emitted directly:

My case was a QListWidgetItem, which I triggered manually (leading to hanging), and then fixed with the proper qtbot usage.

list_widget.itemClicked.emit(item)

FIx:

center = list_widget.visualItemRect(item).center()
qtbot.mouseClick(list_widget.viewport(), qc.Qt.LeftButton, pos=center)

(I'm running inside a x86 debian docker container with xvfb on for CI reasons)

Update: It was my mistake, a message_prompt blocked the entire process, and in headless mode it was not obvious.

mskoenz avatar Jul 30 '22 11:07 mskoenz

@nicoddemus should the “question” label be removed as this is a reproduced bug rather than a question? (Reproduced on macOS by @The-Compiler)

impact27 avatar Jul 31 '22 05:07 impact27

Indeed, changed!

However we need a volunteer with access to MacOS to investigate this.

nicoddemus avatar Jul 31 '22 13:07 nicoddemus

Calling QEventLoop.quit() inside QApplication.processEvents has no effect on macOS.

Code to reproduce:

import time
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QEventLoop, QTimer
# from PyQt6.QtWidgets import QApplication
# from PyQt6.QtCore import QEventLoop, QTimer


if __name__ == "__main__":
    app = QApplication([])

    loop = QEventLoop()

    def _wait_process_events():
        # While we wait here, _quitloop_by_timeout is triggered
        time.sleep(1)
        # The timeout event is triggered
        QApplication.processEvents()

    def _quitloop_by_timeout():
        # Ask the loop to quit
        loop.quit()
        print("Requesting loop to quit")

    # Call _wait_process_events in the event loop
    timer_1 = QTimer(loop)
    timer_1.setSingleShot(True)
    timer_1.setInterval(10)
    timer_1.timeout.connect(_wait_process_events)
    timer_1.start()

    # Call _quitloop_by_timeout in the event loop after _wait_process_events
    timer_2 = QTimer(loop)
    timer_2.setSingleShot(True)
    timer_2.setInterval(500)
    timer_2.timeout.connect(_quitloop_by_timeout)
    timer_2.start()

    # Enter eventloop
    loop.exec()
    print("loop exited")

Executing this code with Python 3.9.12 under macOS 12.3.1 with either

  • PyQt5-5.15.7 PyQt5-Qt5-5.15.2 PyQt5-sip-12.11.0
  • PyQt6-6.3.1 PyQt6-Qt6-6.3.1 PyQt6-sip-13.4.0

Results in "Requesting loop to quit" being printed and the program freezing.

From the discussion here I understand that this does not affect linux or window

impact27 avatar Jul 31 '22 14:07 impact27

I will send a bug report on the pyqt mailing list

impact27 avatar Jul 31 '22 14:07 impact27

Have you tried with PySide rather than PyQt? If it happens there as well, it's likely a Qt bug.

The-Compiler avatar Jul 31 '22 15:07 The-Compiler