From 07edcce697a489619a117cfdd02e1ef1ff2636d5 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 28 Jun 2016 22:55:16 -0400 Subject: [PATCH 01/10] Unit test Completer.update_completion. Validate update_completion sets the completions widget's model correctly based on the command text. --- tests/helpers/fixtures.py | 8 ++ tests/helpers/stubs.py | 31 +++++- tests/unit/completion/test_completer.py | 136 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/unit/completion/test_completer.py diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 2530a1ab6..eb8dd9a97 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -255,6 +255,14 @@ def app_stub(stubs): objreg.delete('app') +@pytest.yield_fixture +def completion_widget_stub(win_registry): + stub = unittest.mock.Mock() + objreg.register('completion', stub, scope='window', window=0) + yield stub + objreg.delete('completion', scope='window', window=0) + + @pytest.fixture(scope='session') def stubs(): """Provide access to stub objects useful for testing.""" diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index fa7f60fa5..f51ec4cf1 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -293,12 +293,13 @@ class FakeCommand: """A simple command stub which has a description.""" def __init__(self, name='', desc='', hide=False, debug=False, - deprecated=False): + deprecated=False, completion=None): self.desc = desc self.name = name self.hide = hide self.debug = debug self.deprecated = deprecated + self.completion = completion class FakeTimer(QObject): @@ -359,6 +360,34 @@ class FakeConfigType: self.complete = lambda: [(val, '') for val in valid_values] +class FakeStatusbarCommand(QObject): + + """Stub for the statusbar command prompt.""" + + got_cmd = pyqtSignal(str) + clear_completion_selection = pyqtSignal() + hide_completion = pyqtSignal() + update_completion = pyqtSignal() + show_cmd = pyqtSignal() + hide_cmd = pyqtSignal() + textChanged = pyqtSignal() + + def __init__(self, parent=None, name=None): + super().__init__(parent) + self._cursor_pos = 0 + self._text = "" + self.cursorPosition = lambda: self._cursor_pos + self.text = lambda: self._text + self.prefix = lambda: self._text[0] + self.setFocus = lambda: None + + def setText(self, x): + self._text = x + + def setCursorPosition(self, x): + self._cursor_pos = x + + class ConfigStub(QObject): """Stub for the config module. diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py new file mode 100644 index 000000000..b006a72ee --- /dev/null +++ b/tests/unit/completion/test_completer.py @@ -0,0 +1,136 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Ryan Roden-Corrent (rcorre) +# +# 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 the Completer Object.""" + +from unittest.mock import Mock + +import pytest +from PyQt5.QtGui import QStandardItemModel + +from qutebrowser.completion.completer import Completer +from qutebrowser.utils.usertypes import Completion + + +class FakeCompletionModel(QStandardItemModel): + + """Stub for a completion model.""" + + DUMB_SORT = Mock() + + def __init__(self, kind, parent=None): + super().__init__(parent) + self.kind = kind + + +@pytest.fixture +def cmd(stubs): + """Create the statusbar command prompt the completer uses.""" + return stubs.FakeStatusbarCommand() + + +@pytest.fixture +def completer(qtbot, cmd, config_stub): + """Create the completer used for testing.""" + config_stub.data = {'completion': {'auto-open': False}} + return Completer(cmd, 0) + + +@pytest.fixture +def instances(monkeypatch): + """Mock the instances module so get returns a fake completion model.""" + # populate a model for each completion type, with a nested structure for + # option and value completion + instances = {kind: FakeCompletionModel(kind) for kind in Completion} + instances[Completion.option] = { + 'general': FakeCompletionModel(Completion.option), + } + instances[Completion.value] = { + 'general': { + 'ignore-case': FakeCompletionModel(Completion.value), + } + } + monkeypatch.setattr('qutebrowser.completion.completer.instances', + instances) + return instances + + +@pytest.fixture +def cmdutils_patch(monkeypatch, stubs): + """Patch the cmdutils module to provide fake commands.""" + cmds = { + 'set': [Completion.section, Completion.option, Completion.value], + 'help': [Completion.helptopic], + 'quickmark-load': [Completion.quickmark_by_name], + 'bookmark-load': [Completion.bookmark_by_url], + 'open': [Completion.url], + 'buffer': [Completion.tab], + 'session-load': [Completion.sessions], + 'bind': [Completion.empty, Completion.command], + } + cmd_utils = stubs.FakeCmdUtils({ + name: stubs.FakeCommand(completion=compl) + for name, compl in cmds.items() + }) + monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', + cmd_utils) + return cmd_utils + + +@pytest.mark.parametrize('txt, expected', [ + (':nope|', Completion.command), + (':nope |', None), + (':set |', Completion.section), + (':set gen|', Completion.section), + (':set general |', Completion.option), + (':set what |', None), + (':set general ignore-case |', Completion.value), + (':set general huh |', None), + (':help |', Completion.helptopic), + (':quickmark-load |', Completion.quickmark_by_name), + (':bookmark-load |', Completion.bookmark_by_url), + (':open |', Completion.url), + (':buffer |', Completion.tab), + (':session-load |', Completion.sessions), + (':bind |', Completion.empty), + (':bind |', Completion.command), + (':bind foo|', Completion.command), + (':bind | foo', Completion.empty), + (':set| general ', Completion.command), + (':|set general ', Completion.command), + (':set gene|ral ignore-case', Completion.section), + (':|', Completion.command), + (': |', Completion.command), + (':bookmark-load |', Completion.bookmark_by_url), +]) +def test_update_completion(txt, expected, cmd, completer, instances, + cmdutils_patch, completion_widget_stub): + """Test setting the completion widget's model based on command text.""" + # this test uses | as a placeholder for the current cursor position + cursor_pos = txt.index('|') + cmd.setText(txt.replace('|', '')) + cmd.setCursorPosition(cursor_pos) + completer.update_completion() + if expected is None: + assert not completion_widget_stub.set_model.called + else: + assert completion_widget_stub.set_model.call_count == 1 + arg = completion_widget_stub.set_model.call_args[0][0] + # the outer model is just for sorting; srcmodel is the completion model + assert arg.srcmodel.kind == expected From 13e8ed53d633e2d45343aa525f318db3604c04ac Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jul 2016 13:03:30 -0400 Subject: [PATCH 02/10] Clean up completer unit test. Based on code review: - import modules, not classes - use methods, not lambdas for the mock command prompt class - use None rather than Mock for DUMB_SORT - autouse two fixtures and remove them from test signatures --- tests/helpers/stubs.py | 16 +++-- tests/unit/completion/test_completer.py | 94 ++++++++++++------------- 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f51ec4cf1..ad699f10f 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -376,10 +376,18 @@ class FakeStatusbarCommand(QObject): super().__init__(parent) self._cursor_pos = 0 self._text = "" - self.cursorPosition = lambda: self._cursor_pos - self.text = lambda: self._text - self.prefix = lambda: self._text[0] - self.setFocus = lambda: None + + def cursorPosition(self): + return self._cursor_pos + + def text(self): + return self._text + + def prefix(self): + return self._text[0] + + def setFocus(self): + return None def setText(self, x): self._text = x diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index b006a72ee..ecd0fbada 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -19,20 +19,18 @@ """Tests for the Completer Object.""" -from unittest.mock import Mock - import pytest from PyQt5.QtGui import QStandardItemModel -from qutebrowser.completion.completer import Completer -from qutebrowser.utils.usertypes import Completion +from qutebrowser.completion import completer +from qutebrowser.utils import usertypes class FakeCompletionModel(QStandardItemModel): """Stub for a completion model.""" - DUMB_SORT = Mock() + DUMB_SORT = None def __init__(self, kind, parent=None): super().__init__(parent) @@ -46,43 +44,44 @@ def cmd(stubs): @pytest.fixture -def completer(qtbot, cmd, config_stub): +def completer_obj(qtbot, cmd, config_stub): """Create the completer used for testing.""" config_stub.data = {'completion': {'auto-open': False}} - return Completer(cmd, 0) + return completer.Completer(cmd, 0) -@pytest.fixture +@pytest.fixture(autouse=True) def instances(monkeypatch): """Mock the instances module so get returns a fake completion model.""" # populate a model for each completion type, with a nested structure for # option and value completion - instances = {kind: FakeCompletionModel(kind) for kind in Completion} - instances[Completion.option] = { - 'general': FakeCompletionModel(Completion.option), + instances = {kind: FakeCompletionModel(kind) + for kind in usertypes.Completion} + instances[usertypes.Completion.option] = { + 'general': FakeCompletionModel(usertypes.Completion.option), } - instances[Completion.value] = { + instances[usertypes.Completion.value] = { 'general': { - 'ignore-case': FakeCompletionModel(Completion.value), + 'ignore-case': FakeCompletionModel(usertypes.Completion.value), } } monkeypatch.setattr('qutebrowser.completion.completer.instances', instances) - return instances -@pytest.fixture +@pytest.fixture(autouse=True) def cmdutils_patch(monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" cmds = { - 'set': [Completion.section, Completion.option, Completion.value], - 'help': [Completion.helptopic], - 'quickmark-load': [Completion.quickmark_by_name], - 'bookmark-load': [Completion.bookmark_by_url], - 'open': [Completion.url], - 'buffer': [Completion.tab], - 'session-load': [Completion.sessions], - 'bind': [Completion.empty, Completion.command], + 'set': [usertypes.Completion.section, usertypes.Completion.option, + usertypes.Completion.value], + 'help': [usertypes.Completion.helptopic], + 'quickmark-load': [usertypes.Completion.quickmark_by_name], + 'bookmark-load': [usertypes.Completion.bookmark_by_url], + 'open': [usertypes.Completion.url], + 'buffer': [usertypes.Completion.tab], + 'session-load': [usertypes.Completion.sessions], + 'bind': [usertypes.Completion.empty, usertypes.Completion.command], } cmd_utils = stubs.FakeCmdUtils({ name: stubs.FakeCommand(completion=compl) @@ -90,43 +89,42 @@ def cmdutils_patch(monkeypatch, stubs): }) monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils) - return cmd_utils @pytest.mark.parametrize('txt, expected', [ - (':nope|', Completion.command), + (':nope|', usertypes.Completion.command), (':nope |', None), - (':set |', Completion.section), - (':set gen|', Completion.section), - (':set general |', Completion.option), + (':set |', usertypes.Completion.section), + (':set gen|', usertypes.Completion.section), + (':set general |', usertypes.Completion.option), (':set what |', None), - (':set general ignore-case |', Completion.value), + (':set general ignore-case |', usertypes.Completion.value), (':set general huh |', None), - (':help |', Completion.helptopic), - (':quickmark-load |', Completion.quickmark_by_name), - (':bookmark-load |', Completion.bookmark_by_url), - (':open |', Completion.url), - (':buffer |', Completion.tab), - (':session-load |', Completion.sessions), - (':bind |', Completion.empty), - (':bind |', Completion.command), - (':bind foo|', Completion.command), - (':bind | foo', Completion.empty), - (':set| general ', Completion.command), - (':|set general ', Completion.command), - (':set gene|ral ignore-case', Completion.section), - (':|', Completion.command), - (': |', Completion.command), - (':bookmark-load |', Completion.bookmark_by_url), + (':help |', usertypes.Completion.helptopic), + (':quickmark-load |', usertypes.Completion.quickmark_by_name), + (':bookmark-load |', usertypes.Completion.bookmark_by_url), + (':open |', usertypes.Completion.url), + (':buffer |', usertypes.Completion.tab), + (':session-load |', usertypes.Completion.sessions), + (':bind |', usertypes.Completion.empty), + (':bind |', usertypes.Completion.command), + (':bind foo|', usertypes.Completion.command), + (':bind | foo', usertypes.Completion.empty), + (':set| general ', usertypes.Completion.command), + (':|set general ', usertypes.Completion.command), + (':set gene|ral ignore-case', usertypes.Completion.section), + (':|', usertypes.Completion.command), + (': |', usertypes.Completion.command), + (':bookmark-load |', usertypes.Completion.bookmark_by_url), ]) -def test_update_completion(txt, expected, cmd, completer, instances, - cmdutils_patch, completion_widget_stub): +def test_update_completion(txt, expected, cmd, completer_obj, + completion_widget_stub): """Test setting the completion widget's model based on command text.""" # this test uses | as a placeholder for the current cursor position cursor_pos = txt.index('|') cmd.setText(txt.replace('|', '')) cmd.setCursorPosition(cursor_pos) - completer.update_completion() + completer_obj.update_completion() if expected is None: assert not completion_widget_stub.set_model.called else: From 9bfff1c685b478ecec0818a18973b363e6c46e08 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jul 2016 16:42:29 -0400 Subject: [PATCH 03/10] Test more cases for completer.update_completion. --- tests/unit/completion/test_completer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index ecd0fbada..6f613ce7c 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -82,6 +82,7 @@ def cmdutils_patch(monkeypatch, stubs): 'buffer': [usertypes.Completion.tab], 'session-load': [usertypes.Completion.sessions], 'bind': [usertypes.Completion.empty, usertypes.Completion.command], + 'tab-detach': None, } cmd_utils = stubs.FakeCmdUtils({ name: stubs.FakeCommand(completion=compl) @@ -116,6 +117,18 @@ def cmdutils_patch(monkeypatch, stubs): (':|', usertypes.Completion.command), (': |', usertypes.Completion.command), (':bookmark-load |', usertypes.Completion.bookmark_by_url), + ('/|', None), + (':open -t|', None), + (':open --tab|', None), + (':open -t |', usertypes.Completion.url), + (':open --tab |', usertypes.Completion.url), + (':open | -t', usertypes.Completion.url), + (':--foo --bar |', None), + (':tab-detach |', None), + (':bind --mode=caret |', usertypes.Completion.command), + #(':bind --mode caret |', usertypes.Completion.command), KNOWN BUG + (':set -t -p |', usertypes.Completion.section), + (':open -- |', None), ]) def test_update_completion(txt, expected, cmd, completer_obj, completion_widget_stub): From 1ea28890b506a07c94e226f63a05fd3a17bde3d5 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jul 2016 17:35:17 -0400 Subject: [PATCH 04/10] Move completion_item_del to completion widget. It was implemented as a member of the Completer object, but registered to the CompletionWidget. This led to the weird scenario where self was actually a CompletionWidget, even though it was declared in Completer. --- qutebrowser/completion/completer.py | 15 +-------------- qutebrowser/completion/completionwidget.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 12f607b51..ebdb1d10b 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import cmdexc, cmdutils, runners +from qutebrowser.commands import cmdutils, runners from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.completion.models import instances, sortfilter @@ -485,16 +485,3 @@ class Completer(QObject): """Select the next completion item.""" self._open_completion_if_needed() self.next_prev_item.emit(False) - - @cmdutils.register(instance='completion', hide=True, - modes=[usertypes.KeyMode.command], scope='window') - def completion_item_del(self): - """Delete the current completion item.""" - completion = objreg.get('completion', scope='window', - window=self._win_id) - if not completion.currentIndex().isValid(): - raise cmdexc.CommandError("No item selected!") - try: - self.model().srcmodel.delete_cur_item(completion) - except NotImplementedError: - raise cmdexc.CommandError("Cannot delete this item.") diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 8c2b8c1fd..80e87dbe4 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -29,7 +29,8 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate, completer from qutebrowser.completion.models import base -from qutebrowser.utils import qtutils, objreg, utils +from qutebrowser.utils import qtutils, objreg, utils, usertypes +from qutebrowser.commands import cmdexc, cmdutils class CompletionView(QTreeView): @@ -277,3 +278,14 @@ class CompletionView(QTreeView): if scrollbar is not None: scrollbar.setValue(scrollbar.minimum()) super().showEvent(e) + + @cmdutils.register(instance='completion', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_del(self): + """Delete the current completion item.""" + if not self.currentIndex().isValid(): + raise cmdexc.CommandError("No item selected!") + try: + self.model().srcmodel.delete_cur_item(self) + except NotImplementedError: + raise cmdexc.CommandError("Cannot delete this item.") From 7f690c3f3f58202f4aab541350d2873d678e3aaf Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jul 2016 22:58:09 -0400 Subject: [PATCH 05/10] More unit test coverage for Completer. Test completion_item{next,prev} and change_completed_part. --- tests/helpers/stubs.py | 6 +++- tests/unit/completion/test_completer.py | 41 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index ad699f10f..f5f50a0ec 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -376,6 +376,7 @@ class FakeStatusbarCommand(QObject): super().__init__(parent) self._cursor_pos = 0 self._text = "" + self._focus = False def cursorPosition(self): return self._cursor_pos @@ -386,8 +387,11 @@ class FakeStatusbarCommand(QObject): def prefix(self): return self._text[0] + def focus(self): + return self._focus + def setFocus(self): - return None + self._focus = True def setText(self, x): self._text = x diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 6f613ce7c..bb058a9af 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -19,6 +19,8 @@ """Tests for the Completer Object.""" +import unittest.mock + import pytest from PyQt5.QtGui import QStandardItemModel @@ -145,3 +147,42 @@ def test_update_completion(txt, expected, cmd, completer_obj, arg = completion_widget_stub.set_model.call_args[0][0] # the outer model is just for sorting; srcmodel is the completion model assert arg.srcmodel.kind == expected + +def test_completion_item_prev(completer_obj, cmd, completion_widget_stub, + config_stub): + """Test that completion_item_prev emits next_prev_item.""" + cmd.setText(':') + slot = unittest.mock.Mock() + completer_obj.next_prev_item.connect(slot) + completer_obj.completion_item_prev() + slot.assert_called_with(True) + +def test_completion_item_next(completer_obj, cmd, completion_widget_stub, + config_stub): + """Test that completion_item_next emits next_prev_item.""" + cmd.setText(':') + slot = unittest.mock.Mock() + completer_obj.next_prev_item.connect(slot) + completer_obj.completion_item_next() + slot.assert_called_with(False) + +@pytest.mark.parametrize('before, newtxt, immediate, after', [ + (':foo |', 'bar', False, ':foo bar|'), + (':foo |', 'bar', True, ':foo bar |'), + (':foo | bar', 'baz', False, ':foo baz| bar'), + (':foo | bar', 'baz', True, ':foo baz |bar'), +]) +def test_change_completed_part(before, newtxt, after, immediate, completer_obj, + cmd, completion_widget_stub, config_stub): + """Test that change_completed_part modifies the cmd text properly.""" + before_pos = before.index('|') + after_pos = after.index('|') + before_txt = before.replace('|', '') + after_txt = after.replace('|', '') + cmd.setText(before_txt) + cmd.setCursorPosition(before_pos) + completer_obj.update_cursor_part() + completer_obj.change_completed_part(newtxt, immediate) + assert cmd.focus() + assert cmd.text() == after_txt + assert cmd.cursorPosition() == after_pos From 68e373df44cf19152d6037d2dc1300b30a1630c1 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jul 2016 23:06:36 -0400 Subject: [PATCH 06/10] Use QLineEdit as a base for FakeStatusbarCommand. Reduces the amount of mocking and keeps it more true to the original. --- tests/helpers/stubs.py | 30 ++++--------------------- tests/unit/completion/test_completer.py | 7 +++--- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f5f50a0ec..d08b35f60 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -27,7 +27,7 @@ from unittest import mock from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, QNetworkCacheMetaData) -from PyQt5.QtWidgets import QCommonStyle, QWidget +from PyQt5.QtWidgets import QCommonStyle, QWidget, QLineEdit from qutebrowser.browser.webkit import webview, history from qutebrowser.config import configexc @@ -360,7 +360,7 @@ class FakeConfigType: self.complete = lambda: [(val, '') for val in valid_values] -class FakeStatusbarCommand(QObject): +class FakeStatusbarCommand(QLineEdit): """Stub for the statusbar command prompt.""" @@ -370,34 +370,12 @@ class FakeStatusbarCommand(QObject): update_completion = pyqtSignal() show_cmd = pyqtSignal() hide_cmd = pyqtSignal() - textChanged = pyqtSignal() - def __init__(self, parent=None, name=None): + def __init__(self, parent=None): super().__init__(parent) - self._cursor_pos = 0 - self._text = "" - self._focus = False - - def cursorPosition(self): - return self._cursor_pos - - def text(self): - return self._text def prefix(self): - return self._text[0] - - def focus(self): - return self._focus - - def setFocus(self): - self._focus = True - - def setText(self, x): - self._text = x - - def setCursorPosition(self, x): - self._cursor_pos = x + return self.text()[0] class ConfigStub(QObject): diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index bb058a9af..f8d359a13 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -40,9 +40,11 @@ class FakeCompletionModel(QStandardItemModel): @pytest.fixture -def cmd(stubs): +def cmd(stubs, qtbot): """Create the statusbar command prompt the completer uses.""" - return stubs.FakeStatusbarCommand() + cmd = stubs.FakeStatusbarCommand() + qtbot.addWidget(cmd) + return cmd @pytest.fixture @@ -183,6 +185,5 @@ def test_change_completed_part(before, newtxt, after, immediate, completer_obj, cmd.setCursorPosition(before_pos) completer_obj.update_cursor_part() completer_obj.change_completed_part(newtxt, immediate) - assert cmd.focus() assert cmd.text() == after_txt assert cmd.cursorPosition() == after_pos From a6a8bf93040441044bd7024aa2edd1ffbc143378 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 4 Jul 2016 07:02:37 -0400 Subject: [PATCH 07/10] Clean up test_completer further. Based on code review: - Use qtbot.waitSignal to test a signal firing - Use pytest.mark.xfail for an expected test failure - Ensure there are 2 newlines between module-level functions --- tests/unit/completion/test_completer.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index f8d359a13..3775f5911 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -130,7 +130,8 @@ def cmdutils_patch(monkeypatch, stubs): (':--foo --bar |', None), (':tab-detach |', None), (':bind --mode=caret |', usertypes.Completion.command), - #(':bind --mode caret |', usertypes.Completion.command), KNOWN BUG + pytest.mark.xfail(reason='issue #74')((':bind --mode caret |', + usertypes.Completion.command)), (':set -t -p |', usertypes.Completion.section), (':open -- |', None), ]) @@ -150,23 +151,24 @@ def test_update_completion(txt, expected, cmd, completer_obj, # the outer model is just for sorting; srcmodel is the completion model assert arg.srcmodel.kind == expected + def test_completion_item_prev(completer_obj, cmd, completion_widget_stub, - config_stub): + config_stub, qtbot): """Test that completion_item_prev emits next_prev_item.""" cmd.setText(':') - slot = unittest.mock.Mock() - completer_obj.next_prev_item.connect(slot) - completer_obj.completion_item_prev() - slot.assert_called_with(True) + with qtbot.waitSignal(completer_obj.next_prev_item) as blocker: + completer_obj.completion_item_prev() + assert blocker.args == [True] + def test_completion_item_next(completer_obj, cmd, completion_widget_stub, - config_stub): + config_stub, qtbot): """Test that completion_item_next emits next_prev_item.""" cmd.setText(':') - slot = unittest.mock.Mock() - completer_obj.next_prev_item.connect(slot) - completer_obj.completion_item_next() - slot.assert_called_with(False) + with qtbot.waitSignal(completer_obj.next_prev_item) as blocker: + completer_obj.completion_item_next() + assert blocker.args == [False] + @pytest.mark.parametrize('before, newtxt, immediate, after', [ (':foo |', 'bar', False, ':foo bar|'), From 27d635a394feb782467d8a717606d118171aeab0 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 4 Jul 2016 08:22:21 -0400 Subject: [PATCH 08/10] Test selection_changed, not change_completed_part. For the Completer unit tests: Although `change_completed_part` looks like a public method, it was only used internally. Test the externally-used method `selection_changed` instead. --- tests/unit/completion/test_completer.py | 33 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 3775f5911..aae77c9d5 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -170,15 +170,29 @@ def test_completion_item_next(completer_obj, cmd, completion_widget_stub, assert blocker.args == [False] -@pytest.mark.parametrize('before, newtxt, immediate, after', [ - (':foo |', 'bar', False, ':foo bar|'), - (':foo |', 'bar', True, ':foo bar |'), - (':foo | bar', 'baz', False, ':foo baz| bar'), - (':foo | bar', 'baz', True, ':foo baz |bar'), +@pytest.mark.parametrize('before, newtxt, quick_complete, count, after', [ + (':foo |', 'bar', False, 1, ':foo bar|'), + (':foo |', 'bar', True, 2, ':foo bar|'), + (':foo |', 'bar', True, 1, ':foo bar |'), + (':foo | bar', 'baz', False, 1, ':foo baz| bar'), ]) -def test_change_completed_part(before, newtxt, after, immediate, completer_obj, - cmd, completion_widget_stub, config_stub): - """Test that change_completed_part modifies the cmd text properly.""" +def test_selection_changed(before, newtxt, count, quick_complete, after, + completer_obj, cmd, completion_widget_stub, + config_stub): + """Test that change_completed_part modifies the cmd text properly. + + The | represents the current cursor position in the cmd prompt. + If quick-complete is True and there is only 1 completion (count == 1), + then we expect a space to be appended after the current word. + """ + config_stub.data['completion']['quick-complete'] = quick_complete + model = unittest.mock.Mock() + model.data = unittest.mock.Mock(return_value=newtxt) + model.count = unittest.mock.Mock(return_value=count) + indexes = [unittest.mock.Mock()] + selection = unittest.mock.Mock() + selection.indexes = unittest.mock.Mock(return_value=indexes) + completion_widget_stub.model = unittest.mock.Mock(return_value=model) before_pos = before.index('|') after_pos = after.index('|') before_txt = before.replace('|', '') @@ -186,6 +200,7 @@ def test_change_completed_part(before, newtxt, after, immediate, completer_obj, cmd.setText(before_txt) cmd.setCursorPosition(before_pos) completer_obj.update_cursor_part() - completer_obj.change_completed_part(newtxt, immediate) + completer_obj.selection_changed(selection, None) + model.data.assert_called_with(indexes[0]) assert cmd.text() == after_txt assert cmd.cursorPosition() == after_pos From 4c9417ac6ec536834c726d773af9d64d72022f61 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 4 Jul 2016 12:31:56 -0400 Subject: [PATCH 09/10] Use helper method for cmd prompt text testing. In test_completer, introduce two helper methods to reduce duplicate code for handling fake cmd prompt text that uses '|' as a placeholder. --- tests/unit/completion/test_completer.py | 36 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index aae77c9d5..3a8bf7b3f 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -96,6 +96,28 @@ def cmdutils_patch(monkeypatch, stubs): cmd_utils) +def _set_cmd_prompt(cmd, txt): + """Set the command prompt's text and cursor position. + + Args: + cmd: The command prompt object. + txt: The prompt text, using | as a placeholder for the cursor position. + """ + cmd.setText(txt.replace('|', '')) + cmd.setCursorPosition(txt.index('|')) + + +def _validate_cmd_prompt(cmd, txt): + """Interpret fake command prompt text using | as the cursor placeholder. + + Args: + cmd: The command prompt object. + txt: The prompt text, using | as a placeholder for the cursor position. + """ + assert cmd.cursorPosition() == txt.index('|') + assert cmd.text() == txt.replace('|', '') + + @pytest.mark.parametrize('txt, expected', [ (':nope|', usertypes.Completion.command), (':nope |', None), @@ -139,9 +161,7 @@ def test_update_completion(txt, expected, cmd, completer_obj, completion_widget_stub): """Test setting the completion widget's model based on command text.""" # this test uses | as a placeholder for the current cursor position - cursor_pos = txt.index('|') - cmd.setText(txt.replace('|', '')) - cmd.setCursorPosition(cursor_pos) + _set_cmd_prompt(cmd, txt) completer_obj.update_completion() if expected is None: assert not completion_widget_stub.set_model.called @@ -193,14 +213,8 @@ def test_selection_changed(before, newtxt, count, quick_complete, after, selection = unittest.mock.Mock() selection.indexes = unittest.mock.Mock(return_value=indexes) completion_widget_stub.model = unittest.mock.Mock(return_value=model) - before_pos = before.index('|') - after_pos = after.index('|') - before_txt = before.replace('|', '') - after_txt = after.replace('|', '') - cmd.setText(before_txt) - cmd.setCursorPosition(before_pos) + _set_cmd_prompt(cmd, before) completer_obj.update_cursor_part() completer_obj.selection_changed(selection, None) model.data.assert_called_with(indexes[0]) - assert cmd.text() == after_txt - assert cmd.cursorPosition() == after_pos + _validate_cmd_prompt(cmd, after) From 6c2c9438a7a1624a18bd96126c71d0562adca342 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 4 Jul 2016 19:04:05 -0400 Subject: [PATCH 10/10] More completer unit test cases. --- tests/unit/completion/test_completer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 3a8bf7b3f..bfb1f0718 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -195,6 +195,9 @@ def test_completion_item_next(completer_obj, cmd, completion_widget_stub, (':foo |', 'bar', True, 2, ':foo bar|'), (':foo |', 'bar', True, 1, ':foo bar |'), (':foo | bar', 'baz', False, 1, ':foo baz| bar'), + (':foo |', 'bar baz', True, 1, ":foo 'bar baz' |"), + (':foo |', '', True, 1, ":foo '' |"), + (':foo |', None, True, 1, ":foo |"), ]) def test_selection_changed(before, newtxt, count, quick_complete, after, completer_obj, cmd, completion_widget_stub,