Giter Club home page Giter Club logo

pytest-qt's Introduction

pytest-qt

pytest-qt is a pytest plugin that allows programmers to write tests for PyQt5, PyQt6, PySide2 and PySide6 applications.

The main usage is to use the qtbot fixture, responsible for handling qApp creation as needed and provides methods to simulate user interaction, like key presses and mouse clicks:

def test_hello(qtbot):
    widget = HelloWidget()
    qtbot.addWidget(widget)

    # click in the Greet button and make sure it updates the appropriate label
    qtbot.mouseClick(widget.button_greet, qt_api.QtCore.Qt.MouseButton.LeftButton)

    assert widget.greet_label.text() == "Hello!"

This allows you to test and make sure your view layer is behaving the way you expect after each code change.

Supported Python versions version conda-forge ci coverage docs black

Features

Requirements

Since version 4.1.0, pytest-qt requires Python 3.7+.

Works with either PySide6, PySide2, PyQt6 or PyQt5.

If any of the above libraries is already imported by the time the tests execute, that library will be used.

If not, pytest-qt will try to import and use the Qt APIs, in this order:

  • PySide6
  • PySide2
  • PyQt6
  • PyQt5

To force a particular API, set the configuration variable qt_api in your pytest.ini file to pyside6, pyside2, pyqt6 or pyqt5:

[pytest]
qt_api=pyqt5

Alternatively, you can set the PYTEST_QT_API environment variable to the same values described above (the environment variable wins over the configuration if both are set).

Documentation

Full documentation and tutorial available at Read the Docs.

Change Log

Please consult the changelog page.

Bugs/Requests

Please report any issues or feature requests in the issue tracker.

Contributing

Contributions are welcome, so feel free to submit a bug or feature request.

Pull requests are highly appreciated! If you can, include some tests that exercise the new code or test that a bug has been fixed, and make sure to include yourself in the contributors list. :)

To prepare your environment, create a virtual environment and install pytest-qt in editable mode with dev extras:

$ pip install --editable .[dev]

After that install pre-commit for pre-commit checks:

$ pre-commit install

Running tests

Tests are run using tox:

$ tox -e py37-pyside2,py37-pyqt5

pytest-qt is formatted using black and uses pre-commit for linting checks before commits. You can install pre-commit locally with:

$ pip install pre-commit
$ pre-commit install

Related projects

  • pytest-xvfb allows to run a virtual xserver (Xvfb) on Linux to avoid GUI elements popping up on the screen or for easy CI testing
  • pytest-qml allows running QML tests from pytest

Contributors

Many thanks to:

Powered by

pycharm

pydev

pytest-qt's People

Contributors

altendky avatar czaki avatar ext-jmmugnes avatar eyllanesc avatar fabioz avatar fogo avatar hakonhagland avatar jdreaver avatar jgirardet avatar k-dominik avatar karlch avatar kianmeng avatar luziferius avatar m3nu avatar mara004 avatar mochick avatar montefra avatar mounten avatar namezero912 avatar nicoddemus avatar nmertsch avatar oscargus avatar pre-commit-ci[bot] avatar rexeh avatar rrzaripov avatar rth avatar sgaist avatar snorfalorpagus avatar the-compiler avatar tilmank avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pytest-qt's Issues

Handling of exception during teardown.

When doing the test teardown, pytest-qt does this:

@pytest.mark.hookwrapper
def pytest_runtest_teardown():
    """
    Hook called after each test tear down, to process any pending events and
    avoiding leaking events to the next test.
    """
    yield
    app = QApplication.instance()
    if app is not None:
        app.processEvents()

PyQt 5.5 changed its behaviour to call abort() if there is an exception in a Qt virtual method and no sys.excepthook is set.

This means if an exception happens during that part, instead of it being silently swallowed (as it's not handled by pytest-qt anymore), in my testfault I got a failure (without much info if running via tox):

$  tox -e unittests -- tests/misc/test_guiprocess.py
[...]
tests/misc/test_guiprocess.py ......ERROR: InvocationError: '/home/florian/proj/qutebrowser/git/.tox/unittests/bin/python -m py.test --strict -rfEsw tests/misc/test_guiprocess.py'

Fortunately using pytest-faulthandler, -s and -v reveals a bit more:

$ tox -e unittests -- -v -s tests/misc/test_guiprocess.py
[...]
tests/misc/test_guiprocess.py::test_cmd_args PASSEDTraceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/objreg.py", line 188, in _get_window_registry
    return win.registry
AttributeError: 'NoneType' object has no attribute 'registry'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/qutebrowser/misc/guiprocess.py", line 94, in on_error
    self._what, msg), immediately=True)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/message.py", line 121, in error
    _wrapper(win_id, 'error', message, immediately)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/message.py", line 52, in _wrapper
    bridge = _get_bridge(win_id)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/message.py", line 86, in _get_bridge
    return objreg.get('message-bridge', scope='window', window=win_id)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/objreg.py", line 215, in get
    reg = _get_registry(scope, window, tab)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/objreg.py", line 204, in _get_registry
    return _get_window_registry(window)
  File "/home/florian/proj/qutebrowser/git/qutebrowser/utils/objreg.py", line 190, in _get_window_registry
    raise RegistryUnavailableError('window')
qutebrowser.utils.objreg.RegistryUnavailableError: window
Fatal Python error: Aborted

Current thread 0x00007f8e31e78700 (most recent call first):
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/pytestqt/plugin.py", line 592 in pytest_runtest_teardown
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 109 in wrapped_call
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 393 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 123 in __init__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 107 in wrapped_call
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 393 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 528 in _docall
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 521 in __call__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 137 in <lambda>
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 149 in __init__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 137 in call_runtest_hook
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 119 in call_and_report
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 77 in runtestprotocol
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py", line 65 in pytest_runtest_protocol
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 394 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 123 in __init__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 107 in wrapped_call
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 393 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 528 in _docall
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 521 in __call__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/main.py", line 142 in pytest_runtestloop
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 394 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 528 in _docall
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 521 in __call__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/main.py", line 122 in _main
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/main.py", line 84 in wrap_session
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/main.py", line 116 in pytest_cmdline_main
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 394 in execute
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 528 in _docall
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/core.py", line 521 in __call__
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/_pytest/config.py", line 41 in main
  File "/home/florian/proj/qutebrowser/git/.tox/unittests/lib/python3.4/site-packages/py/test.py", line 4 in <module>
  File "/usr/lib64/python3.4/runpy.py", line 85 in _run_code
  File "/usr/lib64/python3.4/runpy.py", line 170 in _run_module_as_main

I can't reproduce it in a minimal example, but you can clone qutebrowser and run the commandlines above.

I'll of course now fix the exception, but pytest-qt should have some way to handle them (perhaps by using capture_exceptions() in pytest_runtest_teardown as well?).

Building docs via tox fails

From docs/conf.py:

on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if on_rtd:
    html_theme = 'default'
else:
    #html_theme = 'agogo'
    html_theme = 'sphinxdoc'
    # [...]

from tox.ini:

[testenv:docs]
# ...
setenv=
    READTHEDOCS=True

This fails when trying to build the docs locally, as it uses the default theme:

Warning, treated as error:
WARNING: 'default' html theme has been renamed to 'classic'. Please change your html_theme setting either to the new 'alabaster' default theme, or to 'classic' to keep using the old default.

ERROR: InvocationError: '/home/florian/proj/pytest-qt/.tox/docs/bin/sphinx-build -q -E -W -b html . _build'

I'm not sure what the best way to solve this is, as I assume default is valid on readthedocs and READTHEDOCS needs to be set so the shims are used so no Qt-backend is needed.

pypi release?

Interesting plugin. Makes me want to write a QT app again :)

Any chance you put a release file on pypi instead of just metadata? And do you have a twitter account?

AttributeError: 'NoneType' object has no attribute 'signal_triggered' on teardown

I have some code which looks like this:

class TestJavascriptEscape:

    def _test_escape(self, text, qtbot, webframe):
        # ...
        with qtbot.waitSignal(webframe.loadFinished, raising=True):
            webframe.setHtml(html_source)
        # ...

    @pytest.mark.parametrize('text', TESTS)
    def test_real_escape(self, webframe, qtbot, text):
        """Test javascript escaping with a real QWebPage."""
        self._test_escape(text, qtbot, webframe)

    @hypothesis.given(hypothesis.strategies.text())
    def test_real_escape_hypothesis(self, webframe, qtbot, text):
        """Test javascript escaping with a real QWebPage and hypothesis."""
        self._test_escape(text, qtbot, webframe)

With these fixtures:

@pytest.fixture(scope='session')
def qnam():
    """Session-wide QNetworkAccessManager."""
    from PyQt5.QtNetwork import QNetworkAccessManager
    nam = QNetworkAccessManager()
    nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
    return nam


@pytest.fixture
def webpage(qnam):
    """Get a new QWebPage object."""
    from PyQt5.QtWebKitWidgets import QWebPage

    page = QWebPage()
    page.networkAccessManager().deleteLater()
    page.setNetworkAccessManager(qnam)
    return page


@pytest.fixture
def webframe(webpage):
    """Convenience fixture to get a mainFrame of a QWebPage."""
    return webpage.mainFrame()

When I run this (after adding the qtbot.waitSignal call) on Ubuntu Trusty (and only there!) with PyQt 5.2.1, I get this giant log. The gist of it is this:

==================================== ERRORS ====================================
____ ERROR at teardown of TestJavascriptEscape.test_real_escape_hypothesis _____

self = <_pytest.runner.SetupState object at 0x7f991bf86898>
colitem = <Function 'test_real_escape_hypothesis'>

    def _callfinalizers(self, colitem):
        # [...]

.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py:356: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.tox/unittests/lib/python3.4/site-packages/_pytest/python.py:1868: in finish
    func()
.tox/unittests/lib/python3.4/site-packages/_pytest/python.py:1826: in teardown
    next()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

qapp = <PyQt5.QtWidgets.QApplication object at 0x7f9913d87af8>
request = <SubRequest 'qtbot' for <Function 'test_real_escape_hypothesis'>>

    @pytest.yield_fixture
    def qtbot(qapp, request):
# [...]
E               Failed: Qt exceptions in virtual methods:
E               ________________________________________________________________________________
E                 File "/home/buildbotx/slaves/slave/ubuntu-trusty/build/.tox/unittests/lib/python3.4/site-packages/pytestqt/plugin.py", line 427, in _quit_loop_by_signal
E                   self.signal_triggered = True
E               
E               AttributeError: 'NoneType' object has no attribute 'signal_triggered'
E               ________________________________________________________________________________
E                 File "/home/buildbotx/slaves/slave/ubuntu-trusty/build/.tox/unittests/lib/python3.4/site-packages/pytestqt/plugin.py", line 427, in _quit_loop_by_signal
E                   self.signal_triggered = True
E               
E               AttributeError: 'NoneType' object has no attribute 'signal_triggered'

[repeated... a lot]

Do you have any idea what's happening there ๐Ÿ˜Ÿ? It seems self is None... I've seen this in QObjects which are already deleteLater()'ed, but SignalBlocker isn't one...

I'll try to produce a minimal example later.

Behaviour of waitSignal

Problem description

I am having troubles with understanding how to use the functionality qtbot.waitSignal. I followed the documentation but there must be something I am missing.

It seems that a direct call to the emit method of a given signal does not trigger a block with qtbot.waitSignal(... to validate.

I found the problem by testing my application, checking if a button was sending a signal, and noted that the waitSignal was always hitting the timeout. I extracted what I think is a bug, but might be to my very new discovery of signals and slots.

What I expected / what I get

in a file test_sample.py:

from PySide.QtCore import QObject, Signal

class Simple(QObject):
    signal = Signal()

def test_Qt(qtbot):
    simple = Simple()

    with qtbot.waitSignal(simple.signal, timeout=1000) as waiting:
        simple.signal.emit()

    assert waiting.signal_triggered

Running py.test on this file results in an AssertionError, because waiting.signal_triggered is False. I expected the emit method to cause waiting to see the signal being emitted.

Possible things I might do wrong

Since I am new to this signal/slot business, I might be overlooking some essential facts.

  • As a zen master would ask, is the signal truly fired if no slot is there to listen to it?
  • Could it be that it is that slow to fire a signal?
  • Is the emit code somehow placed at the wrong spot?

Thank you for enlightening me, and thanks for sharing such a useful work, by the way. Testing GUI applications without this would be a massive pain.

Configuration

I am using Python 2.7.5, pytest 1.4.24, and PySide 1.2.2, on a Linux Mint 16.

New event processing changes behaviour of tests which delete objects

In #79, you switched pytest-qt's waitSignal to use busy loops because using QEventLoop caused a weird segfault.

I recently wrote a test which fails with the new way of looping without realizing it.

The test is making sure that some function behaves properly if an object is already deleted:

@pytest.mark.parametrize('delete', [True, False])
def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
    config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
    obj = Obj("{{ font['foo'] }}")

    with caplog.atLevel(9):  # VDEBUG
        style.set_register_stylesheet(obj)

    records = caplog.records()
    assert len(records) == 1
    assert records[0].message == 'stylesheet for Obj: font: bar;'

    assert obj.rendered_stylesheet == 'font: bar;'

    if delete:
        with qtbot.waitSignal(obj.destroyed):
            obj.deleteLater()

    config_stub.data = {'fonts': {'foo': 'baz'}, 'colors': {}}
    style.get_stylesheet.cache_clear()
    config_stub.changed.emit('fonts', 'foo')

    if delete:
        expected = 'font: bar;'
    else:
        expected = 'font: baz;'
    assert obj.rendered_stylesheet == expected

The code under test:

def set_register_stylesheet(obj):
    """Set the stylesheet for an object based on it's STYLESHEET attribute.

    Also, register an update when the config is changed.

    Args:
        obj: The object to set the stylesheet for and register.
             Must have a STYLESHEET attribute.
    """
    qss = get_stylesheet(obj.STYLESHEET)
    log.config.vdebug("stylesheet for {}: {}".format(
        obj.__class__.__name__, qss))
    obj.setStyleSheet(qss)
    objreg.get('config').changed.connect(
        functools.partial(update_stylesheet, obj))


def update_stylesheet(obj):
    """Update the stylesheet for obj."""
    if not sip.isdeleted(obj):
        obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))

Those tests now fail, presumably because the object never got deleted:

test_set_register_stylesheet[True]:

>       assert obj.rendered_stylesheet == expected
E       assert 'font: baz;' == 'font: bar;'
E         - font: baz;
E         ?         ^
E         + font: bar;
E         ?         ^

test_set_register_stylesheet[False]:

    @pytest.mark.parametrize('delete', [True, False])
    def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
        config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
        obj = Obj("{{ font['foo'] }}")

        with caplog.atLevel(9):  # VDEBUG
            style.set_register_stylesheet(obj)

        records = caplog.records()
        assert len(records) == 1
>       assert records[0].message == 'stylesheet for Obj: font: bar;'
E       assert 'stylesheet f...j: font: baz;' == 'stylesheet fo...j: font: bar;'
E         - stylesheet for Obj: font: baz;
E         ?                             ^
E         + stylesheet for Obj: font: bar;
E         ?                             ^

Here I can imagine I'm not the only one with such a kind of test...

I think the best way to implement SignalBlocker would actually be a QSignalSpy (which would also make #64 easier) - however it only takes one signal, so SignalBlocker.connect and MultiSignalBlocker won't work with that...

Feature: Stop execution until signal emitted

Problem

I came across the following situation today while writing a PySide test. I have a data analysis GUI that calls out to an external application in a new process (using QProcess) to run a simulation. I set out to write an integration test that runs the simulation, waits for it to complete, and then tests the output files.

I tried many things, including a lot of Python time and threading functions, but they all blocked the main thread, therefore blocking the GUI and blocking my call to the external process. I figured out that you can call QtCore.QEventLoop.exec_() to create a nested event loop. That is, qtbot has a QApplication instance already running, but calling QtCore.QEventLoop.exec_() will stop execution using a new event loop QtCore.QEventLoop.quit() is called.

Here is an example test:

def test_run_simulation(qtbot):
    gui = MainGUI()
    qtbot.addWidget(gui)

    # Set up loop and quit slot
    loop = QtCore.QEventLoop()
    gui.worker.finished.connect(loop.quit)
    gui.worker.start()  # Begin long operation
    loop.exec_()   # Execution stops here until signal is emitted

    assert_valid_data(worker.results)  # Worker is done. Check results.
    ...

My feature request is this: abstract this functionality and put it in pytest-qt.

Solution

Here is one solution I found. First we define the context manager block_until_emit:

@contextmanager
def block_until_emit(signal, timeout=10000):
    """Block loop until signal emitted, or timeout (ms) elapses."""
    loop = QtCore.QEventLoop()
    signal.connect(loop.quit)

    yield

    if timeout is not None:
        QtCore.QTimer.singleShot(timeout, loop.quit)
    loop.exec_()

This context manager can be used like this:

with block_until_emit(worker.finished, 10000):
    gui.worker.start()

Implementation

I think we can add this as a method to qtbot, so we use it as qtbot.block_until_emit. We can also keep it as a top-level definition in the pyqtestqt namespace and have users import it, since it doesn't need information from the current qtbot.

Also, an arbitrary number of signal can be passed in and all of them can be connected to loop.quit. We can do something like this:

@contextmanager
def block_until_emit(signals, timeout=10000):
    if not isinstance(signal, list):
        signals = [signals]
    for signal in signals:
        signal.connect(loop.quit)
    ...

Also, users may not want to use this as a context manager. We could define a class with __enter__ and __exit__ defined that gets returned. Then, we could use this as a function too.

Lastly, users may not want to connect a signal. They may just want to use the timeout feature. In that case, we can make signals=None by default and use a timer (making sure timeout and signals are not both None).

Let me know what you think!

behaviour with `QMessageBox.question` and other modal dialogs

I do not understand how to successfully test the behaviour of modal dialogs, and did not find an example in the documentation -- or anywhere else, for that matter.

Question

How to prevent modal dialogs from poping up in testing, and how do I simulate clicking on their buttons with qtbot?

Sample file

from PySide.QtGui import QMessageBox, QFrame

class Simple(QFrame):
    def query(self):
        self.question = QMessageBox.question(
            self, 'Message', 'Are you sure?',
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No)
        if self.question == QMessageBox.Yes:
            self.answer = True
        else:
            self.answer = False

def test_Qt(qtbot):
    simple = Simple()
    qtbot.addWidget(simple)

    simple.query()

What I tried

Because self.question is a QMessageBox.StandardButton, i.e. an enum, it has no method to allow clicking on it. I tried to set directly self.question to QMessageBox.Yes, for instance, but obviously, this does not close the window.

Also, adding self.question to qtbot with addWidget does not work, as it is no real widget.

Could this be due to a wrong design choice on my side? Should the opening of the question be in a separate function, and the rest of the logic would simply take as an input the result of the question. Or am I testing this the wrong way?

Additional but (maybe) related issues

It is seems related to me, but when calling an exec_ method on a customized QtGui.QDialog object, the same problem appears. I can not use the method qtbot.mouseClick on the object before clicking on it manually. Because the object pops out, and interrups the flow of the program.

Of course, this is the whole point of such an object in the main application, but while testing, it looks like it messes things up. Feel free to ignore this part if you deem it too unrelated (I can also open another ticket).

Splitting plugin.py into multiple files

I think plugin.py slowly gets hard to navigate with all the additions - what about splitting it into multiple files, similar to how the tests are split?

Something like this:

  • plugin.py
    • all fixtures (?)
    • pytest_*
    • imports all other things from the other modules
  • qtbot.py
    • _inject_qtest_methods
    • QtBot
    • _qapp_instance
  • wait_signal.py
    • _AbstractSignalBlocker
    • SignalBlocker
    • MultiSignalBlocker
    • SignalTimeoutError
  • exceptions.py
    • capture_exceptions
    • format_captured_exceptions
    • _exception_capture_disabled
  • logging.py
    • QtLoggingPlugin
    • _QtMessageCapture
    • Record
    • _QtLogLevelErrorRepr

Log capturing breaks existing logging tests

#40 breaks an existing test in qutebrowser which uses a custom message handler which uses logging and pytest-capturelog.

I'm not sure whether this is an issue, or the kind of breakage which is okay when updating a minor version. If you think it should be fixed, probably the message handler should be restored/left untouched when qInstallMessageHandler returns something else than None (and a pytest warning shown?).

Another possibility would perhaps be to call the old message handler from the pytest-qt one, so both work?

QApplication.exit in test makes subsequent tests fail

I am testing an application. When the application quits, it calls QApplication.exit. This could happen, because of a teardown, or because I want to test, how the application quits.
In all subsequent tests, the QApplication is still in an exit state. Things like qtbot.waitSignal will stop working. I can workaround that problem for now, but it seems like an unexpected behaviour. The qtbot fixture should make sure, that the QApplication has not already quit, for each function.

Give access to what arguments were passed with in the signal

Thanks for the great plugin!

Would be awesome if when using qtbot.waitSignal, you could access any arguments that were sent in the signal e.g.

with qtbot.waitSignal(worker.success) as blocker:
    pass
assert blocker.args == 'hello, world!'

What are your thoughts?

I'll take a stab at it if you think it's a good feature, thanks

Easing the use of QSettings

Is it possible to easily tell the great qapp fixture of pytest-qt where it should look for settings?

Say my main code has app = QApplication([]) followed by app.setOrganizationName('MyOrg') and app.setApplicationName('MyApp'). Maybe I'm doing this wrong but, since I don't want my tests to rely on (or alter) real world settings I would like my test to use app.setOrganizationName('MyOrg') and app.setApplicationName('MyAppTest').

Assuming the above is not completely wrong, I'd like the qtbot fixture of pytest-qt to use (through the qapp fixture) those settings. One solution that I see and doesn't seem to cost anything is to put in my conftest.py

def pytest_namespace():
    return {'organizationName': 'MyOrg',
            'applicationName': 'MyAppTest'
            }

and then modify the plugin in the qapp fixture to replace

    if app is None:
        global _qapp_instance
        _qapp_instance = QApplication([])
        yield _qapp_instance

by

    if app is None:
        global _qapp_instance
        _qapp_instance = QApplication([])
        _qapp_instance.setOrganizationName(
                getattr(pytest, 'organizationName', '')
                )
        _qapp_instance.setApplicationName(
                getattr(pytest, 'applicationName', '')
                )
        yield _qapp_instance

Does it make sense? Should I do something completely different?

Plugin move broke using pytestqt.plugin.SignalBlocker

I have a test where I use a pytestqt.plugin.SignalBlocker manually, because I need it in a session-scoped fixture, and qtbot is a function-scoped one.

This broke with #87 - I think every public class/function which was in plugin.py should also be imported there after the move. Who knows what other crazy stuff people did ๐Ÿ˜‰

Providing pyqt testenvs in tox.ini

What about adjusting tox.ini so it has test environments for every Python version for -pyside, -pyqt4 and -pyqt5, which then set PYTEST_QT_API correctly? For PyQt, those would probably simply use system_site_packages = True and rely on it being installed system-wide, for the -pyside envs it would install it from PyPI.

The default environment list would then be something like py{26,27,32,33,34}-{pyside,pyqt4,pyqt5}, docs (if I understood tox' generative envlist feature correctly).

After that, we could probably adjust .travis.yml to use tox as well, similar to how pytest does it, right?

Catch exceptions in other Qt threads

I recently switched to pytest-qt, and I love it so far. Before, I used my own pytest fixture to create a QApplication. I found a way to catch exceptions on all threads by overriding sys.excepthook in the fixture and checking for errors:

caught_exceptions = []


@pytest.yield_fixture
def qtbot_(request, qtbot):
    # Set excepthook to catch pyside errors in other threads.
    global caught_exceptions
    caught_exceptions = []

    def pytest_excepthook(type_, value, tback):
        global caught_exceptions
        caught_exceptions.append(''.join(traceback.format_tb(tback)) + "\n" +
                                 str(type_.__name__) + ": " + str(value))
        sys.__excepthook__(type_, value, tback)
    sys.excepthook = pytest_excepthook

    yield qtbot

    sys.excepthook = sys.__excepthook__
    if caught_exceptions:
        pytest.fail("Caught qt exceptions:\n{}".format(
            "\n".join(caught_exceptions)))

I created this so I can see when my table model methods fail. Without this, if an error happens in the data() or header() methods of a table model, like when a table view is being populated using the model, the test will not fail.

I think something similar to this would be a nice feature of pytest-qt, but a better, less hacky implementation would probably be better.

mouse position in qtbot.mouseClick

Hi,

I tried to use the mouse position in qtbot.mouseClick but it doesn't work like expected. I expected the click on the defined position. The click was always on the same position. Most of the time on 366, 599, but not all the time. The position argument is commited correctly to the click method. Did I misunderstood the pos argument or did I missed something else? Using QTest directly doesn't seem to work, too...

pytestqt/plugin.py:

    def create_qtest_proxy_method(method_name):

        if hasattr(QtTest.QTest, method_name):
            qtest_method = getattr(QtTest.QTest, method_name)

            def result(*args, **kwargs):
                print "\nDEBUGGING", args, kwargs
                return qtest_method(*args, **kwargs)

            functools.update_wrapper(result, qtest_method)
            return staticmethod(result)
        else:
            return None # pragma: no cover
import pytest
import pytestqt
from PyQt4.QtTest import QTest
from pytestqt.qt_compat import QtGui, Qt, QtCore

class MyListView(QtGui.QListView):

    def __init__(self, parent=None):
        super(MyListView, self).__init__(parent)

    def mouseReleaseEvent(self, QMouseEvent):
        cursor = QtGui.QCursor()
        print "\nmouseClick on {}, position: {}".format(self, cursor.pos())

class TestClass2(object):

    def setup_class(self):
        self.widget = MyListView()
        self.widget.resize(800, 600)
        self.widget.move(0, 0)

        model = QtGui.QStandardItemModel()
        for i in xrange(100):
            model.appendRow(QtGui.QStandardItem(str(i)))
        self.widget.setModel(model)

    def test_click(self, qtbot):
        qtbot.addWidget(self.widget)

        # click have to be on viewport() otherwise it crashes
        qtbot.mouseClick(self.widget.viewport(), QtCore.Qt.LeftButton)
        qtbot.mouseClick(self.widget.viewport(), QtCore.Qt.LeftButton, pos=QtCore.QPoint(0, 0))
        qtbot.mouseClick(self.widget.viewport(), QtCore.Qt.LeftButton, pos=QtCore.QPoint(100, 100))

        rect = self.widget.geometry()
        QTest.mouseClick(self.widget.viewport(), QtCore.Qt.LeftButton, pos=QtCore.QPoint(0, 0))
        QTest.mouseClick(self.widget.viewport(), QtCore.Qt.LeftButton, pos=rect.center())

Alias for stopForInteraction

I know it's very minor, but since no IDE can parse pytest fixtures, I get my self always letting some commented "qtbot.stopForInteraction" line on test methods, so I could do a quick debug.

I think that a short alias for stopForInteraction (like "stop" or "debug") would be nice.

How to use the scope="module"

Dear all,

I want to test my PyQt software with pytest-qt. I want to use "module" scope, but the qtbot limit is "function", My testing code is as bellows:

#!/usr/bin/env python

import pytest

from mainwindow import *
from productive_maintenance import *
from PyQt4 import QtGui, QtCore

class Window:
    def __init__(self, qtbot):
        app = QApplication(sys.argv)
        connx = connectDB()
        logDialog = LoginDialog(connx)  # login dialog
        qtbot.addWidget(logDialog)
        qtbot.keyClicks(logDialog.lineEditUsername, "huang")
        qtbot.keyClicks(logDialog.lineEditPassword, "123456")
        qtbot.mouseClick(logDialog.pushButtonLogin, QtCore.Qt.LeftButton)
        assert logDialog.result() == True
        self.window = MainWindow(connx)  # main window
        self.window.show()
        qtbot.addWidget(self.window)


@pytest.fixture(scope='module')
def ui(qtbot):
    return Window(qtbot).window

class TestProductiveMaintenance:
    def test_username(self,ui):
        assert ui.userInfo['userid'] == "huang"

    def test_user_group(self, ui):
        assert ui.userInfo['user_group'] == 'administrator'

After i run py.test, i got the errors:
ScopeMismatchError: you tried to access the "function" scope fixture "qtbot".....

How to use the qtbot fixture to module? Thanks.

Reports for unexpected qWarnings should show correct line number

Currently, if I have a file like this:

96 # [...]
97
98 def test_warning():
99    qWarning("hello world")

pytest-qt shows a warning on line 97 (which is empty):

_______________________________ test_warning ________________________________
tests/utils/test_qtutils.py:97: Failure: Qt messages with level WARNING or above emitted
--------------------------- Captured Qt messages ----------------------------
QtWarningMsg: hello world

At the very least, I think the line number should be the function where the issue actually occured.

However, on Qt5 (with qInstallMessageHandler support) it'd also be possible to report the exact location in the captured log message, which would be useful.

I might submit a PR for the latter somewhen this or next week, but if you have the time, feel free to do it ๐Ÿ˜‰

AppVeyor

Is there any reason there is no integration with AppVeyor yet? That would probably also be a good way to easily test PyQt5 until #36 is ready.

See the script I use to install PyQt5 and my appveyor.yml - I guess something similar is also possible for PyQt4, and maybe PySide.

There's also install.ps1 from the Python Packaging Userguide which can be used to install various Python versions easily. (It comes with 3.4 and 2.7 only by default, I think)

waitSignal swallows exceptions with raising=True

When qtbot.waitSignal(..., raising=True) is used and an exception in the with-block happens (which causes the signal to not be emitted), it's silently ignored and SignalTimeoutError is raised instead.

I don't really know what the best way to fix this would be (because of the existing Qt exception capturing), but I have a test case:

diff --git a/pytestqt/_tests/test_wait_signal.py b/pytestqt/_tests/test_wait_signal.py
index 13a78c7..cd5ea57 100644
--- a/pytestqt/_tests/test_wait_signal.py
+++ b/pytestqt/_tests/test_wait_signal.py
@@ -10,6 +10,29 @@ class Signaller(QtCore.QObject):
     signal_2 = Signal()


+class TestException(Exception):
+    pass
+
+
+@pytest.mark.parametrize('multiple', [True, False])
+def test_raising_other_exception(qtbot, multiple):
+    """
+    Make sure waitSignal with raising=True handles exceptions correctly.
+    """
+    signaller = Signaller()
+
+    if multiple:
+        func = qtbot.waitSignals
+        arg = [signaller.signal, signaller.signal_2]
+    else:
+        func = qtbot.waitSignal
+        arg = signaller.signal
+
+    with pytest.raises(TestException):
+        with func(arg, timeout=10, raising=True):
+            raise TestException
+
+
 def test_signal_blocker_exception(qtbot):
     """
     Make sure waitSignal without signals and timeout doesn't hang, but raises

Understanding PyQt and raising exceptions

Hi, I have a few problems understanding the behavior of PyQt and exception handling. Why is no exception raised when zeroDivide() is called by the button click? Is there another solution except to mark the test as expected to fail? Why is the test test_buttonClick2() started two times and test_testFail() just once?

    import pytest
    from pytestqt.qt_compat import QtGui, QtCore

    class TestWidget(QtGui.QWidget):

        def __init__(self, parent=None):
            super(TestWidget, self).__init__(parent)

            self.horizontalLayout = QtGui.QHBoxLayout(self)
            self.button = QtGui.QPushButton("raise exception")
            self.horizontalLayout.addWidget(self.button)
            self.button.clicked.connect(self.zeroDivide)

        def zeroDivide(self):
            0 / 0

    class TestClass(object):

        def setup_class(self):
            self.widget = TestWidget()

        def teardown_class(self):
            pass

        def test_zeroDivide(self):
            with pytest.raises(ZeroDivisionError):
                self.widget.zeroDivide()

        def test_zeroDivide2(self, qtbot):
            qtbot.addWidget(self.widget)
            with pytest.raises(ZeroDivisionError):
                qtbot.mouseClick(self.widget.button, QtCore.Qt.LeftButton)

        @pytest.mark.xfail
        def test_testFail(self):
            assert False

        @pytest.mark.xfail
        def test_buttonClick2(self, qtbot):
            qtbot.addWidget(self.widget)
            qtbot.mouseClick(self.widget.button, QtCore.Qt.LeftButton)

Test fails with ignored Qt warning

I have no idea what's going on... Looking at e.g. this build, the tests fail because of a Qt message:

=================================== FAILURES ===================================
_____________________ test_err_windows[''-''-'exception'] ______________________
tests/unit/utils/test_error.py:75: Failure: Qt messages with level WARNING or above emitted
----------------------------- Captured Qt messages -----------------------------
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805
None:None:0:
    QtWarningMsg: virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805

However, this exact message is ignored in my pytest.ini:

qt_log_ignore =
    ^SpellCheck: .*
    ^SetProcessDpiAwareness failed: .*
    ^QWindowsWindow::setGeometryDp: Unable to set geometry .*
    ^QProcess: Destroyed while process .* is still running\.
    ^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist
    ^virtual void QSslSocketBackendPrivate::transmit\(\) SSL write failed with error: -9805
    ^virtual void QSslSocketBackendPrivate::transmit\(\) SSLRead failed with: -9805

Unfortunately I can't trigger the message reliably. When I write a test which uses the same message with a qWarning, it passes:

def test_suppressed_qwarning():
    qWarning("virtual void QSslSocketBackendPrivate::transmit() SSLRead failed with: -9805")

So I wonder what's going on... any idea?

Modeltest, take 2

Continuing the discussion from bgr/PyQt5_modeltest#3 here:

I now have a reimplementation of modeltest.py from the C++ code and from what I learned from the existing modeltest.py.

It's still very much work-in-progress, but it seems to work. As it's translated from the original .cpp code, it's licensed under LGPL 2.1 or LGPL 3, which should be fully compatible with pytest-qt's LGPL3 license.

I'm currently only running the tests from PyQt5_modeltest against it and
those seem to work - but for licensing and other reasons (quoting the author, Don't give too much attention to test.py, I never gave it too much thinking, it was just something to get the thing working while I was trying it out.) I'd like to implement proper tests for it.

I also intend to make it compatible with PyQt4 and PySide - I don't think it should be very hard once there are tests, actually.

After that, I want to add a pytest fixture and release it as a pytest plugin or as part of pytest-qt, preferrably the later. I've thought about this again, and it doesn't make sense as a standalone thing as it uses plain asserts everywhere, which makes it very hard to use without pytest anyways.

Would it be okay to add that to pytest-qt? I imagine adding the modeltest.py file as-is with the Qt copyright header (and my copyright added), and then adding the fixture to plugin.py.

Show Qt/PyQt/PySide versions in pytest_report_header

I think it'd be useful to show the Qt/PyQt/PySide versions used in the report header.

For PyQt, this would be:

  • PyQtX.QtCore.QT_VERSION_STR - Qt version (which PyQt was compiled against)
  • PyQtX.QtCore.qVersion() - Qt version (runtime)
  • PyQtX.QtCore.PYQT_VERSION_STR - PyQt version

For PySide, it seems it's:

  • PySide.QtCore.__version__ - Qt version (which PySide was compiled against)
  • PySide.QtCore.qVersion() - Qt version (runtime)
  • PySide.__version__ - PySide version

I'd submit a PR, but my TODO list gets longer and longer instead of shorter - so feel free to work on this if you feel like, otherwise it'll probably take me some weeks ๐Ÿ˜†

test_context_none fails

Sorry for all the reports! ๐Ÿ˜†

Getting this when running the tests:

============================================ test session starts =============================================
platform linux -- Python 3.4.0 -- py-1.4.30 -- pytest-2.7.2
qt-api: pyqt5
rootdir: /home/buildbotx/pytest-qt, inifile: 
plugins: qt
collected 24 items 

pytestqt/_tests/test_logging.py .......................F

================================================== FAILURES ==================================================
_____________________________________________ test_context_none ______________________________________________

testdir = <TmpTestdir local('/tmp/pytest-24/testdir/test_context_none0')>

    @pytest.mark.skipif(QT_API != 'pyqt5',
                        reason='Context information only available in PyQt5')
    def test_context_none(testdir):
        """
        Sometimes PyQt5 will emit a context with some/all attributes set as None
        instead of appropriate file, function and line number.

        Test that when this happens the plugin doesn't break.

        :type testdir: _pytest.pytester.TmpTestdir
        """
        testdir.makepyfile(
            """
            from pytestqt.qt_compat import QtWarningMsg

            def test_foo(request):
                log_capture = request.node.qt_log_capture
                context = log_capture._Context(None, None, None)
                log_capture._handle_with_context(QtWarningMsg,
                                                 context, "WARNING message")
                assert 0
            """
        )
        res = testdir.runpytest()
        res.stdout.fnmatch_lines([
            '*Failure*',
>           '*None:None:None:*',
        ])
E       Failed: remains unmatched: '*Failure*', see stderr

/home/buildbotx/pytest-qt/pytestqt/_tests/test_logging.py:417: Failed
-------------------------------------------- Captured stdout call -------------------------------------------$
running ['/home/buildbotx/pytest-qt/.venv/bin/python3', '/home/buildbotx/pytest-qt/.venv/lib/python3.4/site-p$
ckages/pytest.py', '--basetemp=/tmp/pytest-24/testdir/test_context_none0/runpytest-0'] curdir= /tmp/pytest-24$
testdir/test_context_none0
============================= test session starts ==============================
platform linux -- Python 3.4.0 -- py-1.4.30 -- pytest-2.7.2
qt-api: pyqt5
rootdir: /tmp/pytest-24/testdir/test_context_none0, inifile: 
plugins: qt
collected 1 items

test_context_none.py F

=================================== FAILURES ===================================
___________________________________ test_foo ___________________________________

request = <FixtureRequest for <Function 'test_foo'>>

    def test_foo(request):
        log_capture = request.node.qt_log_capture
        context = log_capture._Context(None, None, None)
        log_capture._handle_with_context(QtWarningMsg,
                                         context, "WARNING message")
>       assert 0
E       assert 0

test_context_none.py:8: AssertionError
----------------------------- Captured Qt messages -----------------------------
None:None:None:
    QtWarningMsg: WARNING message
=========================== 1 failed in 0.02 seconds ===========================
-------------------------------------------- Captured stderr call --------------------------------------------
nomatch: '*Failure*'
    and: '============================= test session starts =============================='
    and: 'platform linux -- Python 3.4.0 -- py-1.4.30 -- pytest-2.7.2'
    and: 'qt-api: pyqt5'
    and: 'rootdir: /tmp/pytest-24/testdir/test_context_none0, inifile: '
    and: 'plugins: qt'
    and: 'collected 1 items'
    and: ''
    and: 'test_context_none.py F'
    and: ''
    and: '=================================== FAILURES ==================================='
    and: '___________________________________ test_foo ___________________________________'
    and: ''
    and: "request = <FixtureRequest for <Function 'test_foo'>>"
    and: ''
    and: '    def test_foo(request):'
    and: '        log_capture = request.node.qt_log_capture'
    and: '        context = log_capture._Context(None, None, None)'
    and: '        log_capture._handle_with_context(QtWarningMsg,'
    and: '                                         context, "WARNING message")'
    and: '>       assert 0'
    and: 'E       assert 0'
    and: ''
    and: 'test_context_none.py:8: AssertionError'
    and: '----------------------------- Captured Qt messages -----------------------------'
    and: 'None:None:None:'
    and: '    QtWarningMsg: WARNING message'
    and: '=========================== 1 failed in 0.02 seconds ==========================='
==================================== 1 failed, 23 passed in 6.68 seconds =====================================

assertSignal

I'd like to add a qtbot.assertSignal to pytest-qt

/cc @acogneau

Motivation

Something I need to do regularily is to make sure a given code snippet calls a signal, or doesn't - but I don't want to wait for the signal.

The current way to do that works, but is unpythonic:

spy = QSignalSpy(foo.signal)
foo.do_stuff()
assert len(spy) == 1
assert spy[1] == "signal_arg"
spy = QSignalSpy(foo.error)
foo.do_stuff()
assert not spy

API

After some thinking, I think a context manager with this signature would be best:

qtbot.assertSignal(signal, count=None) -> QSignalSpy
  • signal: The signal it should listen to
  • count: Either None (in which case it expects the signal to be emitted >= 1 times), or an int. By setting count to 0, it can be ensured the signal is not emitted.
  • return value: A QSignalSpy which can be used to check the signal arguments.

Examples

with qtbot.assertSignal(foo.signal, count=1) as spy:
    foo.do_stuff()
assert spy[1] == "signal_arg"
with qtbot.assertSignal(foo.error, count=0):
    foo.do_stuff()

Alternatives

I came up with some other ideas while thinking about this, but I think all of them are worse:

  • qtbot.assertSignal(signal, emitted=True): Doesn't give you the possibility to check a signal was emitted exactly once, and isn't better in any way.
  • qtbot.assertSignal(signal, calls=[("signal arg 1", "signal arg 2")]): Too complicated, and can be done easier by checking the QSignalSpy
  • qtbot.assertNotEmitted(signal) - less functionality, and I think passing count=0 is okay if you want that (that was the usecase I had in mind originally)
  • Shoehorning this function into waitSignal - it's complex enough already, and qtbot.waitSignal(signal, do_not_actually_wait=True) just sounds wrong ๐Ÿ˜‰
  • Adding a multi-signal variant (AND): Simply use with qtbot.assertSignal(foo), qtbot.assertSignal(bar): instead - that isn't an issue because it doesn't wait for the signal.
  • Adding a multi-signal variant (OR): I don't really see the usecase for it.

Processing test events happens in hookwrapper

So I was looking into why the exception in #65 is actually happening for me: It seems the test finishes after a module I'm monkeypatching is already un-monkeypatched:

@pytest.fixture(autouse=True)
def mock_modules(monkeypatch, stubs):
    monkeypatch.setattr('qutebrowser.misc.guiprocess.message',
                        stubs.MessageModule())

I'm guessing this is because pytest_runtest_teardown() is decorated with @pytest.mark.hookwrapper - should it really be? I think it'd make more sense to run it before other fixtures are torn down.

Capturing Qt logging

Continuing the discussion from hackebrot/qutebrowser#8 (comment) here ๐Ÿ˜„

I agree it's a good idea to do a fixture similiar to caplog, with a .records() and maybe .setLevel() and .atLevel() as well.

Levels of messages

In Qt, there are the following logging "levels"/functions - I also checked how much it's used in the whole qtbase sourcecode:

  • qFatal(): An error which means any further execution of the application is useless. Calls abort() immediately by default, so probably should never be captured as we'll not get to the point to display the captured output. Used 178 times, for things like when no screen was found, or you're mixing incompatible Qt libraries.
  • qCritical(): A critical issue. Qt should IMHO use this for many things which are actually only qWarnings, but it's only used 88 times.
  • qWarning(): Used for a lot of messages which aren't really "just warnings", but Qt is (or thinks it is) able to recover from. Used 2601 times.
  • qDebug(): Used for debug output (1692 times), but only if you recompile Qt yourself with -debug.

Handling of qFatal()

As said I think this should not be captured. The Qt documentation says:

If you are using the default message handler this function will abort on Unix systems to create a core dump. On Windows, for debug builds, this function will report a _CRT_ERROR enabling you to connect a debugger to the application.

I wonder how this should be handled, as it's probably a good idea to keep that behaviour. Maybe disable the custom message handler from inside it, and then call qFatal() with the given message again?

Other levels

I think qCritical() / qWarning() / qDebug() should all be captured and printed with failed tests by default.

However as said, I'd also like to have a way to always print qCritical()/qWarning(). Maybe some --qt-log-always option, and a --qt-log-format option so I can get it in the format I want?

Context manager to disable logging handler

Add a context manager to qtlog fixture that disables the custom handler:

def test_foo(qtlog):
    with qtlog.disabled():
         # in this block the original qInstallMsgHandler function is active

Also implement a mark which does the same thing but for the entire test:

@pytest.mark.no_qt_log
def test_foo(qtlog):
     # in this test the original qInstallMsgHandler function is active

(Feature idea from @The-Compiler in #54).

Noisy output for exceptions in virtual methods

When there's an exception in a virtual method, e.g. with this code:

from PyQt5.QtCore import QTimer

def foo():
    raise Exception


def test_foo(qtbot):
    timer = QTimer()
    timer.setSingleShot(True)
    timer.setInterval(100)
    timer.timeout.connect(foo)
    with qtbot.waitSignal(timer.timeout):
        timer.start()

the output is quite noisy:

=========================================================================== ERRORS ===========================================================================
_______________________________________________________________ ERROR at teardown of test_foo ________________________________________________________________

self = <_pytest.runner.SetupState object at 0x7ffa51cd5cf8>, colitem = <Function 'test_foo'>

    def _callfinalizers(self, colitem):
        finalizers = self._finalizers.pop(colitem, None)
        exc = None
        while finalizers:
            fin = finalizers.pop()
            try:
>               fin()

.tox/unittests/lib/python3.4/site-packages/_pytest/runner.py:356: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.tox/unittests/lib/python3.4/site-packages/_pytest/python.py:1868: in finish
    func()
.tox/unittests/lib/python3.4/site-packages/_pytest/python.py:1826: in teardown
    next()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

qapp = <PyQt5.QtWidgets.QApplication object at 0x7ffa51cca708>, request = <SubRequest 'qtbot' for <Function 'test_foo'>>

    @pytest.yield_fixture
    def qtbot(qapp, request):
        """
        Fixture used to create a QtBot instance for using during testing.

        Make sure to call addWidget for each top-level widget you create to ensure
        that they are properly closed after the test ends.
        """
        result = QtBot()
        no_capture = _exception_capture_disabled(request.node)
        if no_capture:
            yield result  # pragma: no cover
        else:
            with capture_exceptions() as exceptions:
                yield result
            if exceptions:
>               pytest.fail(format_captured_exceptions(exceptions))
E               Failed: Qt exceptions in virtual methods:
E               ________________________________________________________________________________
E                 File "/home/florian/proj/qutebrowser/git/test_foo.py", line 4, in foo
E                   raise Exception
E               
E               Exception: 
E               ________________________________________________________________________________

.tox/unittests/lib/python3.4/site-packages/pytestqt/plugin.py:554: Failed
-------------------------------------------------------------------- Captured stderr call --------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/florian/proj/qutebrowser/git/test_foo.py", line 4, in foo
    raise Exception
Exception
============================================================= 1 passed, 1 error in 0.53 seconds ==============================================================
  • Exception text is duplicated in the captured stderr output as sys.__excepthook__ gets called which prints in there.
  • The source code of qtbot and internal pytest source is displayed.

Can some of this be shortened, e.g. by not calling sys.__excepthook__, using pytest.fail(..., pytrace=False) or using __tracebackhide__ somewhere?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.