pytest-dev / pytest-qt

pytest plugin for Qt (PyQt5/PyQt6 and PySide2/PySide6) application testing
https://pytest-qt.readthedocs.io
MIT License
401 stars 71 forks source link

QApplication.processEvents() blocks qtbot on MacOS #284

Open impact27 opened 4 years ago

impact27 commented 4 years ago

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

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  
The-Compiler commented 4 years ago

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 ===============================
impact27 commented 4 years ago

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 
The-Compiler commented 4 years ago

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.

nicoddemus commented 4 years ago

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

impact27 commented 4 years ago

But @The-Compiler replicated the bug on macOS?

mskoenz commented 2 years ago

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.

impact27 commented 2 years ago

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

nicoddemus commented 2 years ago

Indeed, changed!

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

impact27 commented 2 years ago

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

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 commented 2 years ago

I will send a bug report on the pyqt mailing list

The-Compiler commented 2 years ago

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

ankurt commented 1 year ago

@impact27 I'm also experiencing this so wanted to share my setup to see if we can find commonalities that may be causing this:

I also get hanging tests using libs:

 name         : pyside6
 version      : 6.4.0.1
 description  : Python bindings for the Qt cross-platform application and UI framework

dependencies
 - PySide6-Addons 6.4.0.1
 - PySide6-Essentials 6.4.0.1
 - shiboken6 6.4.0.1
 name         : pytest
 version      : 6.2.5
 description  : pytest: simple powerful testing with Python

dependencies
 - atomicwrites >=1.0
 - attrs >=19.2.0
 - colorama *
 - iniconfig *
 - packaging *
 - pluggy >=0.12,<2.0
 - py >=1.8.2
 - toml *

required by
 - pytest-qt >=3.0.0
 name         : pytest-qt
 version      : 4.2.0
 description  : pytest support for PyQt and PySide applications

dependencies
 - pytest >=3.0.0

on MacOS 13.1 (darwin x86_64 architecture) using python 3.9.13

For my colleagues qtbot is not hanging. We haven't been able to isolate the difference either.

I modified the original test case for my dependencies and reproduced the hanging test:

from PySide6.QtCore import QCoreApplication, QTimer
from PySide6.QtWidgets import QWidget
from time import sleep
import pytest

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):
        sleep(1)
        QCoreApplication.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()