pytest-dev / pytest-qt

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

Intermittently missing property of object under test #539

Closed KennethNielsen closed 9 months ago

KennethNielsen commented 9 months ago

Dear pytest-qt devs

First and foremost, thank you for an awesome tool!

I have run into an issue that I don't really know what to make of. I also do not know at all if this is even a pytest-qt issues, I'm mostly just looking for information.

I have the problem that occasionally one of my tests will fail. It happen intermittently and I have been unable so far to force the re-production of the issue. But I do know that it is more frequent in CI on GitHub Actions, than it is on local machines (Assuming that average CPU load is higher on GH Actions, I even tried stressing the CPU to see whether that would increase the failure rate locally, but without success). The failure I get is this:

qtbot = <pytestqt.qtbot.QtBot object at 0x7f5955515a50>

    @fixture
    def zilien(qtbot):
        """Create Zilien instance with the Qt App instance created by qtbot.

        Why? When Zilien is created inside the parametrized test, Qt crashed after 3 parameters.
        Also, the WebAPI thread needs to be started in some tests and when a test fails,
        is it polite to quit the thread and wait for it to finish.

        """
        # The `request_system_info` method is patched out, because it would require the mock to be
        # loaded with return value data already here in the fixture, which makes it more difficult to do
        # the same in the actual tests AND because allowing this method to run, would cause a call back
        # into Zilien which we are not interested in
        with patch("zilien_qt.web_api.WebAPI.request_system_info"):
>           zilien = Zilien()

tests/integration/core_communication_test.py:36: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/zilien_qt/zilien.py:113: in __init__
    self.pipe_diagram = PipeDiagram(self)
src/zilien_qt/pipe_diagram.py:281: in __init__
    float_input = self.scene.create_float_input_widget(input_name, input_data["coords"])
src/zilien_qt/pipe_diagram.py:131: in create_float_input_widget
    input_widget = FloatInput(input_id, scene=self)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <zilien_qt.pipe_diagram.FloatInput(0x561fb01317f0) at 0x7f5955[53](https://github.com/SpectroInlets/zilien_qt/actions/runs/6811692781/job/18523898363?pr=155#step:10:54)edc0>
widget_id = 'MFC1 setpoint'
scene = <zilien_qt.pipe_diagram.PipeDiagramGraphicsScene(0x561fb7df5c30) at 0x7f59[55](https://github.com/SpectroInlets/zilien_qt/actions/runs/6811692781/job/18523898363?pr=155#step:10:56)61f8c0>
stepper_emit_delay_ms = 500

    def __init__(self, widget_id, scene, stepper_emit_delay_ms=STEPPER_EMIT_DELAY_MS):
        """Initialize internal QDoubleSpinbox with format and signal connections"""
        QDoubleSpinBox.__init__(self)
        self.last_sent = None
        # This disables sending value_changed while editing the value
        self.setKeyboardTracking(False)
>       self.lineEdit().returnPressed.connect(self.changed)
E       AttributeError: 'PySide6.QtWidgets.QWidgetItem' object has no attribute 'returnPressed'

src/zilien_qt/pipe_diagram.py:557: AttributeError
=========================== short test summary info ============================

and a (shortened) representation of the offending code is something like this:

class FloatInput(QDoubleSpinBox):

    adapted_value_changed = Signal(str, float)

    def __init__(
        self,
        widget_id: str,
        scene: QGraphicsScene,
        stepper_emit_delay_ms: int = STEPPER_EMIT_DELAY_MS,
    ):
        """Initialize internal QDoubleSpinbox with format and signal connections"""
        QDoubleSpinBox.__init__(self)
        # SNIP
        self.lineEdit().returnPressed.connect(self.changed)
        # SNIP

The lineEdit of the QDoubleSpinBox that we inherit from, definitely is supposed to have a returnPressed property (signal really). One thing that I noticed, is that the author of the code opted for the old-style way of calling __init__ on the parent. Since it is single inheritance, the two should be the same, but it lead me down the route of thinking about whether qtbot does something asynchronously to the inheritance tree of the objects under test, possibly to inject behavior. If it did, that might explain why QDoubleSpinBox.__init__(self) might not be the same as super().__init__(). An additional interesting tidbit is that the error identifies the object as a QWidgetItem, which isn't even in the inheritance tree under normal operation. The __mro__ looks like this:

(<class 'zilien_qt.pipe_diagram.FloatInput'>, <class 'PySide6.QtWidgets.QDoubleSpinBox'>, <class 'PySide6.QtWidgets.QAbstractSpinBox'>, <class 'PySide6.QtWidgets.QWidget'>, <class 'PySide6.QtCore.QObject'>, <class 'PySide6.QtGui.QPaintDevice'>, <class 'Shiboken.Object'>, <class 'object'>)

Besides from the speculation above, I'm would be greatly thankful for any sort of insight or idea of where I might start to look.

I should mention, that I am not sure that pytest-qt is even involved in the problem. I have never seen it when running normally, but these objects are also initialized a lot more under test than under normal run, so it can also be that this is problem with my use of PySide, that I'm just only seeing under test.

Software versions:

PySide6             6.5.1.1
PySide6-Addons      6.5.1.1
PySide6-Essentials  6.5.1.1
pytest-qt           4.2.0
nicoddemus commented 9 months ago

Hi @KennethNielsen,

I have never seen that exception before.

whether qtbot does something asynchronously to the inheritance tree of the objects under test, possibly to inject behavior.

Nothing of the sort, QtBot is just a class like any other, providing methods to interact with widgets, you can see the code here:

https://github.com/pytest-dev/pytest-qt/blob/master/src/pytestqt/qtbot.py

I'm would be greatly thankful for any sort of insight or idea of where I might start to look.

I did know about PySide6.QtWidgets.QWidgetItem before, but from a glancing look at the docs, I think it might be an internal class that is used as a "placeholder" for an actual widget inside a layout, perhaps momentarily, being replaced by the actual widget later at some point?. But this is just a guess, and not sure how related to your problem is.

Can you change FloatInput to use super()?

KennethNielsen commented 9 months ago

@nicoddemus thanks for your reply.

Can you change FloatInput to use super()?

I can, and I did, unfortunately it didn't fix the problem.

I did know about PySide6.QtWidgets.QWidgetItem before, but from a glancing look at the docs, I think it might be an internal class that is used as a "placeholder" for an actual widget inside a layout, perhaps momentarily, being replaced by the actual widget later at some point?. But this is just a guess, and not sure how related to your problem is.

Yeah, that was what I got as well, the feeling that it is a placeholder. I think that may point in the direction of this being related to PySide somehow. Maybe those placeholders are being replaced by real widgets asynchronously (for fast loading possibly) or something. I think it will try the PySide community for some input on this.

In any case. From you answers I don't see any reason to suspect that pytest-qt is involved, so I will thank you for your answer and close the issue.

nicoddemus commented 9 months ago

Good luck.

If you find the source of the issue, please come back and share. 👍