summaryrefslogtreecommitdiff
path: root/tests/unit/components/test_readlinecommands.py
blob: 334e2ef9eb76a3870f3e9021e97e6b5b3f8d170d (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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

import re
import inspect

from PyQt5.QtWidgets import QLineEdit, QApplication
import pytest

from qutebrowser.components import readlinecommands


# Some functions aren't 100% readline compatible:
# https://github.com/qutebrowser/qutebrowser/issues/678
# Those are marked with fixme and have another value marked with '# wrong'
# which marks the current behavior.

fixme = pytest.mark.xfail(reason='readline compatibility - see #678')


class LineEdit(QLineEdit):

    """QLineEdit with some methods to make testing easier."""

    def _get_index(self, haystack, needle):
        """Get the index of a char (needle) in a string (haystack).

        Return:
            The position where needle was found, or None if it wasn't found.
        """
        try:
            return haystack.index(needle)
        except ValueError:
            return None

    def set_aug_text(self, text):
        """Set a text with </> markers for selected text and | as cursor."""
        real_text = re.sub('[<>|]', '', text)
        self.setText(real_text)

        cursor_pos = self._get_index(text, '|')
        sel_start_pos = self._get_index(text, '<')
        sel_end_pos = self._get_index(text, '>')

        if sel_start_pos is not None and sel_end_pos is None:
            raise ValueError("< given without >!")
        if sel_start_pos is None and sel_end_pos is not None:
            raise ValueError("> given without <!")

        if cursor_pos is not None:
            if sel_start_pos is not None or sel_end_pos is not None:
                raise ValueError("Can't mix | and </>!")
            self.setCursorPosition(cursor_pos)
        elif sel_start_pos is not None:
            if sel_start_pos > sel_end_pos:
                raise ValueError("< given after >!")
            sel_len = sel_end_pos - sel_start_pos - 1
            self.setSelection(sel_start_pos, sel_len)

    def aug_text(self):
        """Get a text with </> markers for selected text and | as cursor."""
        text = self.text()
        chars = list(text)
        cur_pos = self.cursorPosition()
        assert cur_pos >= 0
        chars.insert(cur_pos, '|')
        if self.hasSelectedText():
            selected_text = self.selectedText()
            sel_start = self.selectionStart()
            sel_end = sel_start + len(selected_text)
            assert sel_start > 0
            assert sel_end > 0
            assert sel_end > sel_start
            assert cur_pos == sel_end
            assert text[sel_start:sel_end] == selected_text
            chars.insert(sel_start, '<')
            chars.insert(sel_end + 1, '>')
        return ''.join(chars)


def _validate_deletion(lineedit, method, text, deleted, rest):
    """Run and validate a text deletion method on the ReadLine bridge.

    Args:
        lineedit: The LineEdit instance.
        method: Reference to the method on the bridge to test.
        text: The starting 'augmented' text (see LineEdit.set_aug_text)
        deleted: The text that should be deleted when the method is invoked.
        rest: The augmented text that should remain after method is invoked.
    """
    lineedit.set_aug_text(text)
    method()
    assert readlinecommands.bridge._deleted[lineedit] == deleted
    assert lineedit.aug_text() == rest
    lineedit.clear()
    readlinecommands.rl_yank()
    assert lineedit.aug_text() == deleted + '|'


@pytest.fixture
def lineedit(qtbot, monkeypatch):
    """Fixture providing a LineEdit."""
    le = LineEdit()
    qtbot.add_widget(le)
    monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le)
    return le


def test_none(qtbot):
    """Call each rl_* method with a None focusWidget."""
    assert QApplication.instance().focusWidget() is None
    for name, method in inspect.getmembers(readlinecommands,
                                           inspect.isfunction):
        if name.startswith('rl_'):
            method()


@pytest.mark.parametrize('text, expected', [('f<oo>bar', 'fo|obar'),
                                            ('|foobar', '|foobar')])
def test_rl_backward_char(text, expected, lineedit):
    """Test rl_backward_char."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_backward_char()
    assert lineedit.aug_text() == expected


@pytest.mark.parametrize('text, expected', [('f<oo>bar', 'foob|ar'),
                                            ('foobar|', 'foobar|')])
def test_rl_forward_char(text, expected, lineedit):
    """Test rl_forward_char."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_forward_char()
    assert lineedit.aug_text() == expected


@pytest.mark.parametrize('text, expected', [('one <tw>o', 'one |two'),
                                            ('<one >two', '|one two'),
                                            ('|one two', '|one two')])
def test_rl_backward_word(text, expected, lineedit):
    """Test rl_backward_word."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_backward_word()
    assert lineedit.aug_text() == expected


@pytest.mark.parametrize('text, expected', [
    pytest.param('<o>ne two', 'one| two', marks=fixme),
    ('<o>ne two', 'one |two'),  # wrong
    pytest.param('<one> two', 'one two|', marks=fixme),
    ('<one> two', 'one |two'),  # wrong
    ('one t<wo>', 'one two|')
])
def test_rl_forward_word(text, expected, lineedit):
    """Test rl_forward_word."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_forward_word()
    assert lineedit.aug_text() == expected


def test_rl_beginning_of_line(lineedit):
    """Test rl_beginning_of_line."""
    lineedit.set_aug_text('f<oo>bar')
    readlinecommands.rl_beginning_of_line()
    assert lineedit.aug_text() == '|foobar'


def test_rl_end_of_line(lineedit):
    """Test rl_end_of_line."""
    lineedit.set_aug_text('f<oo>bar')
    readlinecommands.rl_end_of_line()
    assert lineedit.aug_text() == 'foobar|'


@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'),
                                            ('foobar|', 'foobar|'),
                                            ('|foobar', '|oobar'),
                                            ('f<oo>bar', 'f|bar')])
def test_rl_delete_char(text, expected, lineedit):
    """Test rl_delete_char."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_delete_char()
    assert lineedit.aug_text() == expected


@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'),
                                            ('foobar|', 'fooba|'),
                                            ('|foobar', '|foobar'),
                                            ('f<oo>bar', 'f|bar')])
def test_rl_backward_delete_char(text, expected, lineedit):
    """Test rl_backward_delete_char."""
    lineedit.set_aug_text(text)
    readlinecommands.rl_backward_delete_char()
    assert lineedit.aug_text() == expected


@pytest.mark.parametrize('text, deleted, rest', [
    ('delete this| test', 'delete this', '| test'),
    pytest.param('delete <this> test', 'delete this', '| test', marks=fixme),
    ('delete <this> test', 'delete ', '|this test'),  # wrong
    pytest.param('f<oo>bar', 'foo', '|bar', marks=fixme),
    ('f<oo>bar', 'f', '|oobar'),  # wrong
])
def test_rl_unix_line_discard(lineedit, text, deleted, rest):
    """Delete from the cursor to the beginning of the line and yank back."""
    _validate_deletion(lineedit, readlinecommands.rl_unix_line_discard,
                       text, deleted, rest)


@pytest.mark.parametrize('text, deleted, rest', [
    ('test |delete this', 'delete this', 'test |'),
    pytest.param('<test >delete this', 'test delete this', 'test |',
                 marks=fixme),
    ('<test >delete this', 'test delete this', '|'),  # wrong
])
def test_rl_kill_line(lineedit, text, deleted, rest):
    """Delete from the cursor to the end of line and yank back."""
    _validate_deletion(lineedit, readlinecommands.rl_kill_line,
                       text, deleted, rest)


@pytest.mark.parametrize('text, deleted, rest', [
    ('test delete|foobar', 'delete', 'test |foobar'),
    ('test delete |foobar', 'delete ', 'test |foobar'),
    ('open -t github.com/foo/bar  |', 'github.com/foo/bar  ', 'open -t |'),
    ('open -t |github.com/foo/bar', '-t ', 'open |github.com/foo/bar'),
    pytest.param('test del<ete>foobar', 'delete', 'test |foobar',
                 marks=fixme),
    ('test del<ete >foobar', 'del', 'test |ete foobar'),  # wrong
])
def test_rl_unix_word_rubout(lineedit, text, deleted, rest):
    """Delete to word beginning and see if it comes back with yank."""
    _validate_deletion(lineedit, readlinecommands.rl_unix_word_rubout,
                       text, deleted, rest)


@pytest.mark.parametrize('text, deleted, rest', [
    ('test delete|foobar', 'delete', 'test |foobar'),
    ('test delete |foobar', 'delete ', 'test |foobar'),
    ('open -t github.com/foo/bar  |', 'bar  ', 'open -t github.com/foo/|'),
    ('open -t |github.com/foo/bar', '-t ', 'open |github.com/foo/bar'),
    ('open foo/bar.baz|', 'bar.baz', 'open foo/|'),
])
def test_rl_unix_filename_rubout(lineedit, text, deleted, rest):
    """Delete filename segment and see if it comes back with yank."""
    _validate_deletion(lineedit, readlinecommands.rl_unix_filename_rubout,
                       text, deleted, rest)


@pytest.mark.parametrize('text, deleted, rest', [
    pytest.param('test foobar| delete', ' delete', 'test foobar|',
                 marks=fixme),
    ('test foobar| delete', ' ', 'test foobar|delete'),  # wrong
    pytest.param('test foo|delete bar', 'delete', 'test foo| bar',
                 marks=fixme),
    ('test foo|delete bar', 'delete ', 'test foo|bar'),  # wrong
    pytest.param('test foo<bar> delete', ' delete', 'test foobar|',
                 marks=fixme),
    ('test foo<bar>delete', 'bardelete', 'test foo|'),  # wrong
])
def test_rl_kill_word(lineedit, text, deleted, rest):
    """Delete to word end and see if it comes back with yank."""
    _validate_deletion(lineedit, readlinecommands.rl_kill_word,
                       text, deleted, rest)


@pytest.mark.parametrize('text, deleted, rest', [
    ('test delete|foobar', 'delete', 'test |foobar'),
    ('test delete |foobar', 'delete ', 'test |foobar'),
    ('open -t github.com/foo/bar  |', 'bar  ', 'open -t github.com/foo/|'),
    ('open -t |github.com/foo/bar', 't ', 'open -|github.com/foo/bar'),
    pytest.param('test del<ete>foobar', 'delete', 'test |foobar', marks=fixme),
    ('test del<ete >foobar', 'del', 'test |ete foobar'),  # wrong
    ('open foo/bar.baz|', 'baz', 'open foo/bar.|'),
])
def test_rl_backward_kill_word(lineedit, text, deleted, rest):
    """Delete to word beginning and see if it comes back with yank."""
    _validate_deletion(lineedit, readlinecommands.rl_backward_kill_word,
                       text, deleted, rest)


def test_rl_yank_no_text(lineedit):
    """Test yank without having deleted anything."""
    lineedit.clear()
    readlinecommands.rl_yank()
    assert lineedit.aug_text() == '|'