summaryrefslogtreecommitdiff
path: root/tests/unit/utils/test_debug.py
blob: 08586d184b661af46e8559601bc6dc466570f976 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Tests for qutebrowser.utils.debug."""

import logging
import re
import time
import textwrap

import pytest
from qutebrowser.qt.core import pyqtSignal, Qt, QEvent, QObject, QTimer
from qutebrowser.qt.widgets import QStyle, QFrame, QSpinBox

from qutebrowser.utils import debug, qtutils
from qutebrowser.misc import objects


@debug.log_events
class EventObject(QObject):

    pass


def test_log_events(qapp, caplog):
    obj = EventObject()
    qapp.sendEvent(obj, QEvent(QEvent.Type.User))
    qapp.processEvents()
    assert caplog.messages == ['Event in test_debug.EventObject: User']


class SignalObject(QObject):

    signal1 = pyqtSignal()
    signal2 = pyqtSignal(str, str)

    def __repr__(self):
        """This is not a nice thing to do, but it makes our tests easier."""
        return '<repr>'


@debug.log_signals
class DecoratedSignalObject(SignalObject):

    pass


@pytest.fixture(params=[(SignalObject, True), (DecoratedSignalObject, False)])
def signal_obj(request):
    klass, wrap = request.param
    obj = klass()
    if wrap:
        debug.log_signals(obj)
    return obj


def test_log_signals(caplog, signal_obj):
    signal_obj.signal1.emit()
    signal_obj.signal2.emit('foo', 'bar')

    assert caplog.messages == ['Signal in <repr>: signal1()',
                               "Signal in <repr>: signal2('foo', 'bar')"]


class TestLogTime:

    def test_duration(self, caplog):
        logger_name = 'qt-tests'

        with caplog.at_level(logging.DEBUG, logger_name):
            with debug.log_time(logger_name, action='foobar'):
                time.sleep(0.1)

            assert len(caplog.records) == 1

            pattern = re.compile(r'Foobar took ([\d.]*) seconds\.')
            match = pattern.fullmatch(caplog.messages[0])
            assert match

            duration = float(match.group(1))
            assert 0 < duration < 30

    def test_logger(self, caplog):
        """Test with an explicit logger instead of a name."""
        logger_name = 'qt-tests'

        with caplog.at_level(logging.DEBUG, logger_name):
            with debug.log_time(logging.getLogger(logger_name)):
                pass

        assert len(caplog.records) == 1

    def test_decorator(self, caplog):
        logger_name = 'qt-tests'

        @debug.log_time(logger_name, action='foo')
        def func(arg, *, kwarg):
            assert arg == 1
            assert kwarg == 2

        with caplog.at_level(logging.DEBUG, logger_name):
            func(1, kwarg=2)

        assert len(caplog.records) == 1
        assert caplog.messages[0].startswith('Foo took')


class TestQEnumKey:

    def test_metaobj(self):
        """Make sure the classes we use in the tests have a metaobj or not.

        If Qt/PyQt even changes and our tests wouldn't test the full
        functionality of qenum_key because of that, this test will tell us.
        """
        assert not hasattr(QStyle.PrimitiveElement, 'staticMetaObject')
        assert hasattr(QFrame, 'staticMetaObject')

    @pytest.mark.parametrize('base, value, klass, expected', [
        (QStyle, QStyle.PrimitiveElement.PE_PanelButtonCommand, None, 'PE_PanelButtonCommand'),
        (QFrame, QFrame.Shadow.Sunken, None, 'Sunken'),
        (QFrame, 0x0030, QFrame.Shadow, 'Sunken'),
        (QFrame, 0x1337, QFrame.Shadow, '0x1337'),
        (Qt, Qt.AnchorPoint.AnchorLeft, None, 'AnchorLeft'),

        # No static meta object, passing in an int on Qt 6
        (QEvent, qtutils.extract_enum_val(QEvent.Type.User), QEvent.Type, 'User'),

        # Unknown value with IntFlags
        (Qt, Qt.AlignmentFlag(1024), None, '0x0400'),
    ])
    def test_qenum_key(self, base, value, klass, expected):
        key = debug.qenum_key(base, value, klass=klass)
        assert key == expected

    def test_int_noklass(self):
        """Test passing an int without explicit klass given."""
        with pytest.raises(TypeError):
            debug.qenum_key(QFrame, 42)


class TestQFlagsKey:

    """Tests for qutebrowser.utils.debug.qflags_key.

    https://github.com/qutebrowser/qutebrowser/issues/42
    """

    @pytest.mark.parametrize('base, value, klass, expected', [
        (Qt, Qt.AlignmentFlag.AlignTop, None, 'AlignTop'),
        pytest.param(Qt, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, None,
                     'AlignLeft|AlignTop', marks=pytest.mark.qt5_xfail(raises=AssertionError)),
        (Qt, Qt.AlignmentFlag.AlignCenter, None, 'AlignHCenter|AlignVCenter'),
        pytest.param(Qt, 0x0021, Qt.AlignmentFlag, 'AlignLeft|AlignTop',
                     marks=pytest.mark.qt5_xfail(raises=AssertionError)),
        (Qt, 0x1100, Qt.AlignmentFlag, 'AlignBaseline|0x1000'),
        (Qt, Qt.DockWidgetArea(0), Qt.DockWidgetArea, 'NoDockWidgetArea'),
        (Qt, Qt.DockWidgetArea(0), None, 'NoDockWidgetArea'),
        (Qt, Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier, 'ShiftModifier'),
        (Qt, Qt.KeyboardModifier.ShiftModifier, None, 'ShiftModifier'),
        (Qt, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, Qt.KeyboardModifier, 'ShiftModifier|ControlModifier'),
        pytest.param(Qt, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, None, 'ShiftModifier|ControlModifier', marks=pytest.mark.qt5_xfail(raises=AssertionError)),
    ])
    def test_qflags_key(self, base, value, klass, expected):
        flags = debug.qflags_key(base, value, klass=klass)
        assert flags == expected

    def test_find_flags(self):
        """Test a weird TypeError we get from PyQt.

        In exactly this constellation (happening via the "Searching with
        --reverse" BDD test), calling QMetaEnum::valueToKey without wrapping
        the flags in int() causes a TypeError.

        No idea what's happening here exactly...
        """
        qwebpage = pytest.importorskip("qutebrowser.qt.webkitwidgets").QWebPage

        flags = qwebpage.FindWrapsAroundDocument
        flags |= qwebpage.FindBackward
        flags &= ~qwebpage.FindBackward
        flags &= ~qwebpage.FindWrapsAroundDocument

        debug.qflags_key(qwebpage,
                         flags,
                         klass=qwebpage.FindFlag)

    def test_int_noklass(self):
        """Test passing an int without explicit klass given."""
        with pytest.raises(TypeError):
            debug.qflags_key(Qt, 42)


@pytest.mark.parametrize('cls, signal', [
    (SignalObject, 'signal1'),
    (SignalObject, 'signal2'),
    (QTimer, 'timeout'),
    (QSpinBox, 'valueChanged'),  # Overloaded signal
])
@pytest.mark.parametrize('bound', [True, False])
def test_signal_name(cls, signal, bound):
    base = cls() if bound else cls
    sig = getattr(base, signal)
    assert debug.signal_name(sig) == signal


@pytest.mark.parametrize('args, kwargs, expected', [
    ([], {}, ''),
    (None, None, ''),
    (['foo'], None, "'foo'"),
    (['foo', 'bar'], None, "'foo', 'bar'"),
    (None, {'foo': 'bar'}, "foo='bar'"),
    (['foo', 'bar'], {'baz': 'fish'}, "'foo', 'bar', baz='fish'"),
    (['x' * 300], None, "'{}".format('x' * 198 + '…')),
], ids=lambda val: str(val)[:20])
def test_format_args(args, kwargs, expected):
    assert debug.format_args(args, kwargs) == expected


def func():
    pass


@pytest.mark.parametrize('func, args, kwargs, full, expected', [
    (func, None, None, False, 'func()'),
    (func, [1, 2], None, False, 'func(1, 2)'),
    (func, [1, 2], None, True, 'test_debug.func(1, 2)'),
    (func, [1, 2], {'foo': 3}, False, 'func(1, 2, foo=3)'),
])
def test_format_call(func, args, kwargs, full, expected):
    assert debug.format_call(func, args, kwargs, full) == expected


@pytest.mark.parametrize('args, expected', [
    ([23, 42], 'fake(23, 42)'),
    (['x' * 201], "fake('{}\u2026)".format('x' * 198)),
    (['foo\nbar'], r"fake('foo\nbar')"),
], ids=lambda val: str(val)[:20])
def test_dbg_signal(stubs, args, expected):
    assert debug.dbg_signal(stubs.FakeSignal(), args) == expected


class TestGetAllObjects:

    class Object(QObject):

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

        def __repr__(self):
            return '<{}>'.format(self._name)

    def test_get_all_objects(self, stubs, monkeypatch):
        # pylint: disable=unused-variable
        widgets = [self.Object('Widget 1'), self.Object('Widget 2')]
        app = stubs.FakeQApplication(all_widgets=widgets)
        monkeypatch.setattr(objects, 'qapp', app)

        root = QObject()
        o1 = self.Object('Object 1', root)
        o2 = self.Object('Object 2', o1)  # noqa: F841
        o3 = self.Object('Object 3', root)  # noqa: F841

        expected = textwrap.dedent("""
            Qt widgets - 2 objects:
                <Widget 1>
                <Widget 2>

            Qt objects - 3 objects:
                <Object 1>
                    <Object 2>
                <Object 3>

            global object registry - 0 objects:
        """).rstrip('\n')

        assert debug.get_all_objects(start_obj=root) == expected

    def test_get_all_objects_qapp(self, qapp, monkeypatch):
        monkeypatch.setattr(objects, 'qapp', qapp)
        objs = debug.get_all_objects()
        event_dispatcher = 'QtCore.QAbstractEventDispatcher object at'
        session_manager = 'QtGui.QSessionManager object at'
        assert event_dispatcher in objs or session_manager in objs