# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2014-2017 Florian Bruhin (The Compiler) # # 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 . """Tests for qutebrowser.misc.readline.""" import re import inspect from PyQt5.QtWidgets import QLineEdit, QApplication import pytest from qutebrowser.misc import readline # 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 !") 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, bridge, method, text, deleted, rest): """Run and validate a text deletion method on the ReadLine bridge. Args: lineedit: The LineEdit instance. bridge: The ReadlineBridge 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 bridge._deleted[lineedit] == deleted assert lineedit.aug_text() == rest lineedit.clear() bridge.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 @pytest.fixture def bridge(): """Fixture providing a ReadlineBridge.""" return readline.ReadlineBridge() 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() @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 @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 @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 @pytest.mark.parametrize('text, expected', [ pytest.param('ne two', 'one| two', marks=fixme), ('ne two', 'one |two'), # wrong pytest.param(' two', 'one two|', marks=fixme), (' 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_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_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|' @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 @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'), pytest.param('delete test', 'delete this', '| test', marks=fixme), ('delete test', 'delete ', '|this test'), # wrong pytest.param('fbar', 'foo', '|bar', marks=fixme), ('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.""" _validate_deletion(lineedit, bridge, bridge.rl_unix_line_discard, text, deleted, rest) @pytest.mark.parametrize('text, deleted, rest', [ ('test |delete this', 'delete this', 'test |'), pytest.param('delete this', 'test delete this', 'test |', marks=fixme), ('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.""" _validate_deletion(lineedit, bridge, bridge.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 delfoobar', 'delete', 'test |foobar', marks=fixme), ('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.""" _validate_deletion(lineedit, bridge, bridge.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, bridge, text, deleted, rest): """Delete filename segment and see if it comes back with yank.""" _validate_deletion(lineedit, bridge, bridge.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 delete', ' delete', 'test foobar|', marks=fixme), ('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.""" _validate_deletion(lineedit, bridge, bridge.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 delfoobar', 'delete', 'test |foobar', marks=fixme), ('test delfoobar', 'del', 'test |ete foobar'), # wrong ('open foo/bar.baz|', 'baz', 'open foo/bar.|'), ]) def test_rl_backward_kill_word(lineedit, bridge, text, deleted, rest): """Delete to word beginning and see if it comes back with yank.""" _validate_deletion(lineedit, bridge, bridge.rl_backward_kill_word, text, deleted, rest) def test_rl_yank_no_text(lineedit, bridge): """Test yank without having deleted anything.""" lineedit.clear() bridge.rl_yank() assert lineedit.aug_text() == '|'