From 341708f54394c68b61c081de84e7d48359c4d0e0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 12:29:53 +0200 Subject: [PATCH] Refactor readline tests. They now use a real QLineEdit and verify a lot more. See #660, #678. --- tests/misc/test_readline.py | 338 ++++++++++++++++++++++++------------ 1 file changed, 225 insertions(+), 113 deletions(-) diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index 523c6f579..da2d05821 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -21,144 +21,256 @@ # pylint: disable=protected-access +import re import inspect -from unittest import mock -from PyQt5.QtWidgets import QLineEdit +from PyQt5.QtWidgets import QLineEdit, QApplication import pytest from qutebrowser.misc import readline +# Some functions aren't 100% readline compatible: +# https://github.com/The-Compiler/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 !") + 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) + + @pytest.fixture -def mocked_qapp(monkeypatch, stubs): - """Fixture that mocks readline.QApplication and returns it.""" - stub = stubs.FakeQApplication() - monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub) - return stub +def lineedit(qtbot, monkeypatch): + """Fixture providing a LineEdit.""" + le = LineEdit() + qtbot.add_widget(le) + monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le) + return le -class TestNoneWidget: - - """Test if there are no exceptions when the widget is None.""" - - def test_none(self, mocked_qapp): - """Call each rl_* method with a None focusWidget.""" - self.bridge = readline.ReadlineBridge() - mocked_qapp.focusWidget = mock.Mock(return_value=None) - for name, method in inspect.getmembers(self.bridge, inspect.ismethod): - if name.startswith('rl_'): - method() +@pytest.fixture +def bridge(): + """Fixture providing a ReadlineBridge.""" + return readline.ReadlineBridge() -class TestReadlineBridgeTest: +def test_none(bridge, qtbot): + """Call each rl_* method with a None focusWidget.""" + assert QApplication.instance().focusWidget() is None + for name, method in inspect.getmembers(bridge, inspect.ismethod): + if name.startswith('rl_'): + method() - """Tests for readline bridge.""" - @pytest.fixture(autouse=True) - def setup(self): - self.qle = mock.Mock() - self.qle.__class__ = QLineEdit - self.bridge = readline.ReadlineBridge() +@pytest.mark.parametrize('text, expected', [('fbar', 'fo|obar'), + ('|foobar', '|foobar')]) +def test_rl_backward_char(text, expected, lineedit, bridge): + """Test rl_backward_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_char() + assert lineedit.aug_text() == expected - def _set_selected_text(self, text): - """Set the value the fake QLineEdit should return for selectedText.""" - self.qle.configure_mock(**{'selectedText.return_value': text}) - def test_rl_backward_char(self, mocked_qapp): - """Test rl_backward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_char() - self.qle.cursorBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('fbar', 'foob|ar'), + ('foobar|', 'foobar|')]) +def test_rl_forward_char(text, expected, lineedit, bridge): + """Test rl_forward_char.""" + lineedit.set_aug_text(text) + bridge.rl_forward_char() + assert lineedit.aug_text() == expected - def test_rl_forward_char(self, mocked_qapp): - """Test rl_forward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_char() - self.qle.cursorForward.assert_called_with(False) - def test_rl_backward_word(self, mocked_qapp): - """Test rl_backward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_word() - self.qle.cursorWordBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('one o', 'one |two'), + ('two', '|one two'), + ('|one two', '|one two')]) +def test_rl_backward_word(text, expected, lineedit, bridge): + """Test rl_backward_word.""" + lineedit.set_aug_text(text) + bridge.rl_backward_word() + assert lineedit.aug_text() == expected - def test_rl_forward_word(self, mocked_qapp): - """Test rl_forward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_word() - self.qle.cursorWordForward.assert_called_with(False) - def test_rl_beginning_of_line(self, mocked_qapp): - """Test rl_beginning_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_beginning_of_line() - self.qle.home.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [ + fixme(('ne two', 'one| two')), + ('ne two', 'one |two'), # wrong + fixme((' two', 'one two|')), + (' two', 'one |two'), # wrong + ('one t', 'one two|') +]) +def test_rl_forward_word(text, expected, lineedit, bridge): + """Test rl_forward_word.""" + lineedit.set_aug_text(text) + bridge.rl_forward_word() + assert lineedit.aug_text() == expected - def test_rl_end_of_line(self, mocked_qapp): - """Test rl_end_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_end_of_line() - self.qle.end.assert_called_with(False) - def test_rl_delete_char(self, mocked_qapp): - """Test rl_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_delete_char() - self.qle.del_.assert_called_with() +def test_rl_beginning_of_line(lineedit, bridge): + """Test rl_beginning_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_beginning_of_line() + assert lineedit.aug_text() == '|foobar' - def test_rl_backward_delete_char(self, mocked_qapp): - """Test rl_backward_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_delete_char() - self.qle.backspace.assert_called_with() - def test_rl_unix_line_discard(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_line_discard() - self.qle.home.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +def test_rl_end_of_line(lineedit, bridge): + """Test rl_end_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_end_of_line() + assert lineedit.aug_text() == 'foobar|' - def test_rl_kill_line(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_line() - self.qle.end.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_unix_word_rubout(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_word_rubout() - self.qle.cursorWordBackward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'), + ('foobar|', 'foobar|'), + ('|foobar', '|oobar'), + ('fbar', 'f|bar')]) +def test_rl_delete_char(text, expected, lineedit, bridge): + """Test rl_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_delete_char() + assert lineedit.aug_text() == expected - def test_rl_kill_word(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_word() - self.qle.cursorWordForward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_yank_no_text(self, mocked_qapp): - """Test yank without having deleted anything.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_yank() - assert not self.qle.insert.called +@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'), + ('foobar|', 'fooba|'), + ('|foobar', '|foobar'), + ('fbar', 'f|bar')]) +def test_rl_backward_delete_char(text, expected, lineedit, bridge): + """Test rl_backward_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_delete_char() + assert lineedit.aug_text() == expected + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('delete this| test', 'delete this', '| test'), + fixme(('delete test', 'delete this', '| test')), + ('delete test', 'delete ', '|this test'), # wrong + fixme(('fbar', 'foo', '|bar')), + ('fbar', 'f', '|oobar'), # wrong +]) +def test_rl_unix_line_discard(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the beginning of the line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_unix_line_discard() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test |delete this', 'delete this', 'test |'), + fixme(('delete this', 'test delete this', 'test |')), + ('delete this', 'test delete this', '|'), # wrong +]) +def test_rl_kill_line(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the end of line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_kill_line() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test delete|foobar', 'delete', 'test |foobar'), + ('test delete |foobar', 'delete ', 'test |foobar'), + fixme(('test delfoobar', 'delete', 'test |foobar')), + ('test delfoobar', 'del', 'test |ete foobar'), # wrong +]) +def test_rl_unix_word_rubout(lineedit, bridge, text, deleted, rest): + """Delete to word beginning and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_unix_word_rubout() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + fixme(('test foobar| delete', ' delete', 'test foobar|')), + ('test foobar| delete', ' ', 'test foobar|delete'), # wrong + fixme(('test foo|delete bar', 'delete', 'test foo| bar')), + ('test foo|delete bar', 'delete ', 'test foo|bar'), # wrong + fixme(('test foo delete', ' delete', 'test foobar|')), + ('test foodelete', 'bardelete', 'test foo|'), # wrong +]) +def test_rl_kill_word(lineedit, bridge, text, deleted, rest): + """Delete to word end and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_kill_word() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +def test_rl_yank_no_text(lineedit, bridge): + """Test yank without having deleted anything.""" + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == '|'