From 555bdb75b537ffc7d4cd39a6828b589c1f9b87a0 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 11 Jun 2016 12:43:37 -0400 Subject: [PATCH 01/20] Add unit test for CommandCompletionModel. This establishes a pattern that can probably be used to test any of the completion models. See #999. --- tests/helpers/stubs.py | 7 ++- tests/unit/completion/test_models.py | 82 +++++++++++++++++++++++++++ tests/unit/config/test_configtypes.py | 5 +- 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/unit/completion/test_models.py diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 87c5594c5..7d22f4359 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -283,8 +283,13 @@ class FakeCommand: """A simple command stub which has a description.""" - def __init__(self, desc): + def __init__(self, name='', desc='', hide=False, debug=False, + deprecated=False): self.desc = desc + self.name = name + self.hide = hide + self.debug = debug + self.deprecated = deprecated class FakeTimer(QObject): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py new file mode 100644 index 000000000..e08f82b73 --- /dev/null +++ b/tests/unit/completion/test_models.py @@ -0,0 +1,82 @@ +# 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 completion models.""" + +from qutebrowser.completion.models import miscmodels + + +def test_command_completion(monkeypatch, stubs, config_stub, key_config_stub): + """Test the results of command completion. + + Validates that: + - only non-hidden and non-deprecated commands are included + - commands are sorted by name + - the command description is shown in the desc column + - the binding (if any) is shown in the misc column + - aliases are included + """ + _patch_cmdutils(monkeypatch, stubs, + 'qutebrowser.completion.models.miscmodels.cmdutils') + config_stub.data['aliases'] = {'rock': 'roll'} + key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'}) + actual = _get_completions(miscmodels.CommandCompletionModel()) + assert actual == [ + ("Commands", [ + ('drop', 'drop all user data', ''), + ('rock', "Alias for 'roll'", ''), + ('roll', 'never gonna give you up', 'rr'), + ('stop', 'stop qutebrowser', 's') + ]) + ] + + +def _get_completions(model): + """Collect all the completion entries of a model, organized by category. + + The result is a list of form: + [ + (CategoryName: [(name, desc, misc), ...]), + (CategoryName: [(name, desc, misc), ...]), + ... + ] + """ + completions = [] + for i in range(0, model.rowCount()): + category = model.item(i) + entries = [] + for j in range(0, category.rowCount()): + name = category.child(j, 0) + desc = category.child(j, 1) + misc = category.child(j, 2) + entries.append((name.text(), desc.text(), misc.text())) + completions.append((category.text(), entries)) + return completions + + +def _patch_cmdutils(monkeypatch, stubs, symbol): + """Patch the cmdutils module to provide fake commands.""" + cmd_utils = stubs.FakeCmdUtils({ + 'stop': stubs.FakeCommand(name='stop', desc='stop qutebrowser'), + 'drop': stubs.FakeCommand(name='drop', desc='drop all user data'), + 'roll': stubs.FakeCommand(name='roll', desc='never gonna give you up'), + 'hide': stubs.FakeCommand(name='hide', hide=True), + 'depr': stubs.FakeCommand(name='depr', deprecated=True), + }) + monkeypatch.setattr(symbol, cmd_utils) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 729256f68..6998d86cd 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -823,8 +823,9 @@ class TestCommand: @pytest.fixture(autouse=True) def patch(self, monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" - cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"), - 'cmd2': stubs.FakeCommand("desc 2")}) + cmd_utils = stubs.FakeCmdUtils({ + 'cmd1': stubs.FakeCommand(desc="desc 1"), + 'cmd2': stubs.FakeCommand(desc="desc 2")}) monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils', cmd_utils) From b037ec489ff6ac87252cd587495f7de5d30b4a63 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 13 Jun 2016 22:20:25 -0400 Subject: [PATCH 02/20] Add unit tests for help completion. Also adds a FakeConfigSection stub for stubbing out configdata.DATA. --- tests/helpers/stubs.py | 16 ++++++++++ tests/unit/completion/test_models.py | 47 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 7d22f4359..57d9a4da0 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -340,6 +340,22 @@ class FakeTimer(QObject): return self._started +class FakeConfigSection: + + """A stub for a KeyValue entry in configdata.DATA.""" + + def __init__(self, *entries): + self.values = [] + self.descriptions = {} + for name, desc in entries: + self.values.append(name) + self.descriptions[name] = desc + + def __iter__(self): + """Iterate over all set values.""" + return self.values.__iter__() + + class ConfigStub(QObject): """Stub for the config module. diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index e08f82b73..2107d9128 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -19,6 +19,8 @@ """Tests for completion models.""" +import collections + from qutebrowser.completion.models import miscmodels @@ -47,6 +49,37 @@ def test_command_completion(monkeypatch, stubs, config_stub, key_config_stub): ] +def test_help_completion(monkeypatch, stubs): + """Test the results of command completion. + + Validates that: + - only non-hidden and non-deprecated commands are included + - commands are sorted by name + - the command description is shown in the desc column + - the binding (if any) is shown in the misc column + - aliases are included + - only the first line of a multiline description is shown + """ + module = 'qutebrowser.completion.models.miscmodels' + _patch_cmdutils(monkeypatch, stubs, module + '.cmdutils') + _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') + actual = _get_completions(miscmodels.HelpCompletionModel()) + assert actual == [ + ("Commands", [ + (':drop', 'drop all user data', ''), + (':roll', 'never gonna give you up', ''), + (':stop', 'stop qutebrowser', '') + ]), + ("Settings", [ + ('general->time', 'Is an illusion.', ''), + ('general->volume', 'Goes to 11', ''), + ('ui->gesture', 'Waggle your hands to control qutebrowser', ''), + ('ui->mind', 'Enable mind-control ui (experimental)', ''), + ('ui->voice', 'Whether to respond to voice commands', ''), + ]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. @@ -80,3 +113,17 @@ def _patch_cmdutils(monkeypatch, stubs, symbol): 'depr': stubs.FakeCommand(name='depr', deprecated=True), }) monkeypatch.setattr(symbol, cmd_utils) + + +def _patch_configdata(monkeypatch, stubs, symbol): + """Patch the configdata module to provide fake data.""" + data = collections.OrderedDict([ + ('general', stubs.FakeConfigSection( + ('time', 'Is an illusion.\n\nLunchtime doubly so.'), + ('volume', 'Goes to 11'))), + ('ui', stubs.FakeConfigSection( + ('gesture', 'Waggle your hands to control qutebrowser'), + ('mind', 'Enable mind-control ui (experimental)'), + ('voice', 'Whether to respond to voice commands'))), + ]) + monkeypatch.setattr(symbol, data) From baf8d00a20563f02aa1dd6d8078b02ab2723c291 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 15 Jun 2016 23:41:14 -0400 Subject: [PATCH 03/20] Unit test url/quickmark/bookmark completion. This also adds a few more stubs/fakes to mock out the quickmark-manager, bookmark-manager, and web-history objects. --- tests/helpers/fixtures.py | 27 ++++++++ tests/helpers/stubs.py | 42 ++++++++++++ tests/unit/completion/test_models.py | 98 +++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 5de771514..3c7c905a0 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -198,6 +198,33 @@ def host_blocker_stub(stubs): objreg.delete('host-blocker') +@pytest.yield_fixture +def quickmark_manager_stub(stubs): + """Fixture which provides a fake quickmark manager object.""" + stub = stubs.UrlMarkManagerStub() + objreg.register('quickmark-manager', stub) + yield stub + objreg.delete('quickmark-manager') + + +@pytest.yield_fixture +def bookmark_manager_stub(stubs): + """Fixture which provides a fake bookmark manager object.""" + stub = stubs.UrlMarkManagerStub() + objreg.register('bookmark-manager', stub) + yield stub + objreg.delete('bookmark-manager') + + +@pytest.yield_fixture +def web_history_stub(stubs): + """Fixture which provides a fake web-history object.""" + stub = stubs.WebHistoryStub() + objreg.register('web-history', stub) + yield stub + objreg.delete('web-history') + + @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 57d9a4da0..5bff97c09 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -21,6 +21,7 @@ """Fake objects/stubs.""" +import collections from unittest import mock from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject @@ -421,6 +422,47 @@ class KeyConfigStub: self.bindings[section] = bindings +class FakeHistoryEntry: + + """Mock for webkit.history.Entry.""" + + def __init__(self, atime, url, title, redirect=False): + self.atime = float(atime) + self.url = url + self.title = title + self.redirect = redirect + + +class UrlMarkManagerStub(QObject): + + """Stub for the quickmark-manager or bookmark-manager object.""" + + added = pyqtSignal(str, str) + removed = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.marks = {} + + +class WebHistoryStub(QObject): + + """Stub for the web-history object.""" + + add_completion_item = pyqtSignal(FakeHistoryEntry) + cleared = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.history_dict = collections.OrderedDict() + + def __iter__(self): + return iter(self.history_dict.values()) + + def __len__(self): + return len(self.history_dict) + + class HostBlockerStub: """Stub for the host-blocker object.""" diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 2107d9128..45e17ab52 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -20,8 +20,51 @@ """Tests for completion models.""" import collections +from datetime import datetime -from qutebrowser.completion.models import miscmodels +import pytest +from PyQt5.QtCore import QUrl + +from qutebrowser.completion.models import miscmodels, urlmodel + + +@pytest.fixture +def quickmarks(quickmark_manager_stub): + """Pre-populate the quickmark-manager stub with some quickmarks.""" + quickmark_manager_stub.marks = collections.OrderedDict([ + ('aw', 'https://wiki.archlinux.org'), + ('ddg', 'https://duckduckgo.com'), + ('wiki', 'https://wikipedia.org'), + ]) + return quickmark_manager_stub + + +@pytest.fixture +def bookmarks(bookmark_manager_stub): + """Pre-populate the bookmark-manager stub with some quickmarks.""" + bookmark_manager_stub.marks = collections.OrderedDict([ + ('https://github.com', 'GitHub'), + ('https://python.org', 'Welcome to Python.org'), + ('http://qutebrowser.org', 'qutebrowser | qutebrowser'), + ]) + return bookmark_manager_stub + + +@pytest.fixture +def web_history(stubs, web_history_stub): + """Pre-populate the web-history stub with some history entries.""" + web_history_stub.history_dict = collections.OrderedDict([ + ('http://qutebrowser.org', stubs.FakeHistoryEntry( + datetime(2015, 9, 5).timestamp(), + QUrl('http://qutebrowser.org'), 'qutebrowser | qutebrowser')), + ('https://python.org', stubs.FakeHistoryEntry( + datetime(2016, 3, 8).timestamp(), + QUrl('https://python.org'), 'Welcome to Python.org')), + ('https://github.com', stubs.FakeHistoryEntry( + datetime(2016, 5, 1).timestamp(), + QUrl('https://github.com'), 'GitHub')), + ]) + return web_history_stub def test_command_completion(monkeypatch, stubs, config_stub, key_config_stub): @@ -80,6 +123,59 @@ def test_help_completion(monkeypatch, stubs): ] +def test_quickmark_completion(quickmarks): + """Test the results of quickmark completion.""" + actual = _get_completions(miscmodels.QuickmarkCompletionModel()) + assert actual == [ + ("Quickmarks", [ + ('aw', 'https://wiki.archlinux.org', ''), + ('ddg', 'https://duckduckgo.com', ''), + ('wiki', 'https://wikipedia.org', ''), + ]) + ] + + +def test_bookmark_completion(bookmarks): + """Test the results of bookmark completion.""" + actual = _get_completions(miscmodels.BookmarkCompletionModel()) + assert actual == [ + ("Bookmarks", [ + ('https://github.com', 'GitHub', ''), + ('https://python.org', 'Welcome to Python.org', ''), + ('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''), + ]) + ] + + +def test_url_completion(config_stub, web_history, quickmarks, bookmarks): + """Test the results of url completion. + + Verify that: + - quickmarks, bookmarks, and urls are included + - no more than 'web-history-max-items' history entries are included + - the most recent entries are included + """ + config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', + 'web-history-max-items': 2} + actual = _get_completions(urlmodel.UrlCompletionModel()) + assert actual == [ + ("Quickmarks", [ + ('https://wiki.archlinux.org', 'aw', ''), + ('https://duckduckgo.com', 'ddg', ''), + ('https://wikipedia.org', 'wiki', ''), + ]), + ("Bookmarks", [ + ('https://github.com', 'GitHub', ''), + ('https://python.org', 'Welcome to Python.org', ''), + ('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''), + ]), + ("History", [ + ('https://python.org', 'Welcome to Python.org', '2016-03-08'), + ('https://github.com', 'GitHub', '2016-05-01'), + ]), + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. From 610f9b706812a8405eb2fb45cd31da4a120695e4 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 16 Jun 2016 23:12:04 -0400 Subject: [PATCH 04/20] Unit test session completion --- tests/helpers/fixtures.py | 9 +++++++++ tests/helpers/stubs.py | 11 +++++++++++ tests/unit/completion/test_models.py | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 3c7c905a0..0fcec6ab2 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -225,6 +225,15 @@ def web_history_stub(stubs): objreg.delete('web-history') +@pytest.yield_fixture +def session_manager_stub(stubs): + """Fixture which provides a fake web-history object.""" + stub = stubs.SessionManagerStub() + objreg.register('session-manager', stub) + yield stub + objreg.delete('session-manager') + + @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 5bff97c09..2d27406d4 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -469,3 +469,14 @@ class HostBlockerStub: def __init__(self): self.blocked_hosts = set() + + +class SessionManagerStub: + + """Stub for the session-manager object.""" + + def __init__(self): + self.sessions = [] + + def list_sessions(self): + return self.sessions diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 45e17ab52..6d0de25be 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -176,6 +176,14 @@ def test_url_completion(config_stub, web_history, quickmarks, bookmarks): ] +def test_session_completion(session_manager_stub): + session_manager_stub.sessions = ['default', '1', '2'] + actual = _get_completions(miscmodels.SessionCompletionModel()) + assert actual == [ + ("Sessions", [('default', '', ''), ('1', '', ''), ('2', '', '')]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. From 16f043034faa65ee55e781caf899bc0dce1e170d Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 16 Jun 2016 23:12:21 -0400 Subject: [PATCH 05/20] Unit test tab (buffer) completion. This involved adding a few stubs as well as expanding the FakeWebView. --- tests/helpers/fixtures.py | 18 +++++++++++ tests/helpers/stubs.py | 45 ++++++++++++++++++++++++++-- tests/unit/completion/test_models.py | 17 +++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 0fcec6ab2..819dda15d 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -234,6 +234,24 @@ def session_manager_stub(stubs): objreg.delete('session-manager') +@pytest.yield_fixture +def tabbed_browser_stub(stubs): + """Fixture which provides a fake tabbed-browser object.""" + stub = stubs.TabbedBrowserStub() + objreg.register('tabbed-browser', stub, scope='window', window=0) + yield stub + objreg.delete('tabbed-browser', scope='window', window=0) + + +@pytest.yield_fixture +def app_stub(stubs): + """Fixture which provides a fake app object.""" + stub = stubs.ApplicationStub() + objreg.register('app', stub) + yield stub + objreg.delete('app') + + @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 2d27406d4..f338d64ec 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -31,6 +31,7 @@ from PyQt5.QtWidgets import QCommonStyle, QWidget from qutebrowser.browser.webkit import webview from qutebrowser.config import configexc +from qutebrowser.mainwindow import mainwindow class FakeNetworkCache(QAbstractNetworkCache): @@ -219,13 +220,20 @@ class FakeWebView(QWidget): """Fake WebView which can be added to a tab.""" - def __init__(self): + url_text_changed = pyqtSignal(str) + shutting_down = pyqtSignal() + + def __init__(self, url=FakeUrl(), title='', tab_id=0): super().__init__() self.progress = 0 self.scroll_pos = (-1, -1) self.load_status = webview.LoadStatus.none - self.tab_id = 0 - self.cur_url = FakeUrl() + self.tab_id = tab_id + self.cur_url = url + self.title = title + + def url(self): + return self.cur_url class FakeSignal: @@ -480,3 +488,34 @@ class SessionManagerStub: def list_sessions(self): return self.sessions + + +class TabbedBrowserStub(QObject): + + """Stub for the tabbed-browser object.""" + + new_tab = pyqtSignal(webview.WebView, int) + + def __init__(self, parent=None): + super().__init__(parent) + self.tabs = [] + self.shutting_down = False + + def count(self): + return len(self.tabs) + + def widget(self, i): + return self.tabs[i] + + def page_title(self, i): + return self.tabs[i].title + + +class ApplicationStub(QObject): + + """Stub to insert as the app object in objreg.""" + + new_window = pyqtSignal(mainwindow.MainWindow) + + def __init__(self): + super().__init__() diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 6d0de25be..07b8dcc52 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -184,6 +184,23 @@ def test_session_completion(session_manager_stub): ] +def test_tab_completion(stubs, qtbot, app_stub, win_registry, + tabbed_browser_stub): + tabbed_browser_stub.tabs = [ + stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), + stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1), + stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) + ] + actual = _get_completions(miscmodels.TabCompletionModel()) + assert actual == [ + ('0', [ + ('0/1', 'https://github.com', 'GitHub'), + ('0/2', 'https://wikipedia.org', 'Wikipedia'), + ('0/3', 'https://duckduckgo.com', 'DuckDuckGo') + ]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. From d49fa7c4a3328a433f624e5130bb59aaf700b5c0 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 16 Jun 2016 23:22:53 -0400 Subject: [PATCH 06/20] Clean up objreg after test_history. test_init in test_history was leaving a web-history in the objreg, which was interfering with the completion tests. --- tests/unit/browser/webkit/test_history.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py index dbc2c6950..2aabd7b54 100644 --- a/tests/unit/browser/webkit/test_history.py +++ b/tests/unit/browser/webkit/test_history.py @@ -391,3 +391,4 @@ def test_init(qapp, tmpdir, monkeypatch, fake_save_manager): assert hist.parent() is qapp assert QWebHistoryInterface.defaultInterface()._history is hist assert fake_save_manager.add_saveable.called + objreg.delete('web-history') From 5255a71bc5cb11ec5dbd94407015e43cbdf66076 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 18 Jun 2016 06:25:25 -0400 Subject: [PATCH 07/20] Unit test setting section completion. --- tests/unit/completion/test_models.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 07b8dcc52..731d05b19 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -25,7 +25,7 @@ from datetime import datetime import pytest from PyQt5.QtCore import QUrl -from qutebrowser.completion.models import miscmodels, urlmodel +from qutebrowser.completion.models import miscmodels, urlmodel, configmodel @pytest.fixture @@ -201,6 +201,20 @@ def test_tab_completion(stubs, qtbot, app_stub, win_registry, ] +def test_setting_section_completion(monkeypatch, stubs): + module = 'qutebrowser.completion.models.configmodel' + _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') + _patch_config_section_desc(monkeypatch, stubs, + module + '.configdata.SECTION_DESC') + actual = _get_completions(configmodel.SettingSectionCompletionModel()) + assert actual == [ + ("Sections", [ + ('general', 'General/miscellaneous options.', ''), + ('ui', 'General options related to the user interface.', ''), + ]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. @@ -248,3 +262,11 @@ def _patch_configdata(monkeypatch, stubs, symbol): ('voice', 'Whether to respond to voice commands'))), ]) monkeypatch.setattr(symbol, data) + +def _patch_config_section_desc(monkeypatch, stubs, symbol): + """Patch the configdata module to provide fake SECTION_DESC.""" + section_desc = { + 'general': 'General/miscellaneous options.', + 'ui': 'General options related to the user interface.', + } + monkeypatch.setattr(symbol, section_desc) From f5b1019d4d2a918f2739dcbee7375804cfba3759 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 18 Jun 2016 06:55:17 -0400 Subject: [PATCH 08/20] Unit test config option completion. Had to add the `raw` parameter to ConfigStub.get as the setting option completion model passes this argument explicitly (although the docs say only raw=True is supported). --- tests/helpers/stubs.py | 2 +- tests/unit/completion/test_models.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f338d64ec..11078ca8f 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -398,7 +398,7 @@ class ConfigStub(QObject): """ return self.data[name] - def get(self, sect, opt): + def get(self, sect, opt, raw=True): """Get a value from the config.""" data = self.data[sect] try: diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 731d05b19..0fd1a1bbc 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -215,6 +215,22 @@ def test_setting_section_completion(monkeypatch, stubs): ] +def test_setting_option_completion(monkeypatch, stubs, config_stub): + module = 'qutebrowser.completion.models.configmodel' + _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') + config_stub.data = {'ui': {'gesture': 'off', + 'mind': 'on', + 'voice': 'sometimes'}} + actual = _get_completions(configmodel.SettingOptionCompletionModel('ui')) + assert actual == [ + ("ui", [ + ('gesture', 'Waggle your hands to control qutebrowser', 'off'), + ('mind', 'Enable mind-control ui (experimental)', 'on'), + ('voice', 'Whether to respond to voice commands', 'sometimes'), + ]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. From 8321c1a90ff68dbce4d1fe97aa65094594ab0a56 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 19 Jun 2016 23:15:09 -0400 Subject: [PATCH 09/20] Unit test setting value completion --- tests/helpers/stubs.py | 28 +++++++++++++++++--- tests/unit/completion/test_models.py | 38 ++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 11078ca8f..4c504af34 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -354,16 +354,38 @@ class FakeConfigSection: """A stub for a KeyValue entry in configdata.DATA.""" def __init__(self, *entries): - self.values = [] + self.values = collections.OrderedDict() self.descriptions = {} - for name, desc in entries: - self.values.append(name) + for name, value, desc in entries: + self.values[name] = value self.descriptions[name] = desc def __iter__(self): """Iterate over all set values.""" return self.values.__iter__() + def __getitem__(self, key): + return self.values[key] + + +class FakeSettingValue: + + """A stub for a SettingValue entry in configdata.DATA[section].""" + + def __init__(self, valid_values, default=None): + self.typ = FakeConfigType(valid_values) + self.default = lambda: default + + +class FakeConfigType: + + """A stub for the typ attribute of a FakeSettingValue.""" + + def __init__(self, valid_values): + # normally valid_values would be a ValidValues, but for simplicity of + # testing this can be a simple list: [(val, desc), (val, desc), ...] + self.complete = lambda: [(val, '') for val in valid_values] + class ConfigStub(QObject): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 0fd1a1bbc..4f3e61b6c 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -231,6 +231,24 @@ def test_setting_option_completion(monkeypatch, stubs, config_stub): ] +def test_setting_value_completion(monkeypatch, stubs, config_stub): + module = 'qutebrowser.completion.models.configmodel' + _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') + config_stub.data = {'general': { 'volume': '0' }} + model = configmodel.SettingValueCompletionModel('general', 'volume') + actual = _get_completions(model) + assert actual == [ + ("Current/Default", [ + ('0', 'Current value', ''), + ('11', 'Default value', ''), + ]), + ("Completions", [ + ('0', '', ''), + ('11', '', ''), + ]) + ] + + def _get_completions(model): """Collect all the completion entries of a model, organized by category. @@ -270,12 +288,22 @@ def _patch_configdata(monkeypatch, stubs, symbol): """Patch the configdata module to provide fake data.""" data = collections.OrderedDict([ ('general', stubs.FakeConfigSection( - ('time', 'Is an illusion.\n\nLunchtime doubly so.'), - ('volume', 'Goes to 11'))), + ('time', + stubs.FakeSettingValue(('fast', 'slow'), 'slow'), + 'Is an illusion.\n\nLunchtime doubly so.'), + ('volume', + stubs.FakeSettingValue(('0', '11'), '11'), + 'Goes to 11'))), ('ui', stubs.FakeConfigSection( - ('gesture', 'Waggle your hands to control qutebrowser'), - ('mind', 'Enable mind-control ui (experimental)'), - ('voice', 'Whether to respond to voice commands'))), + ('gesture', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Waggle your hands to control qutebrowser'), + ('mind', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Enable mind-control ui (experimental)'), + ('voice', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Whether to respond to voice commands'))), ]) monkeypatch.setattr(symbol, data) From 409de10fb4377593dcec8ff78f0a865ce96b8b7a Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 20 Jun 2016 21:58:12 -0400 Subject: [PATCH 10/20] Unit test delete_cur_item for tab completion model. --- tests/helpers/stubs.py | 3 +++ tests/unit/completion/test_models.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 4c504af34..c0b9c4cf3 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -532,6 +532,9 @@ class TabbedBrowserStub(QObject): def page_title(self, i): return self.tabs[i].title + def on_tab_close_requested(self, idx): + del self.tabs[idx] + class ApplicationStub(QObject): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 4f3e61b6c..19bd3c2de 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -21,6 +21,7 @@ import collections from datetime import datetime +from unittest.mock import Mock import pytest from PyQt5.QtCore import QUrl @@ -201,6 +202,29 @@ def test_tab_completion(stubs, qtbot, app_stub, win_registry, ] +def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, + tabbed_browser_stub): + """Verify closing a tab by deleting it from the completion widget.""" + tabbed_browser_stub.tabs = [ + stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), + stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1), + stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) + ] + model = miscmodels.TabCompletionModel() + index = Mock() + cat = Mock() + completion_widget = Mock() + index.isValid = Mock(return_value=True) + index.parent = Mock(return_value=cat) + index.data = Mock(return_value='0/2') + cat.child = Mock(return_value=index) + completion_widget.currentIndex = Mock(return_value=index) + model.delete_cur_item(completion_widget) + actual = [tab.url() for tab in tabbed_browser_stub.tabs] + assert actual == [QUrl('https://github.com'), + QUrl('https://duckduckgo.com')] + + def test_setting_section_completion(monkeypatch, stubs): module = 'qutebrowser.completion.models.configmodel' _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') From b6652ad6bc13e30a130dd34191ad336c67cf1157 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 21 Jun 2016 23:04:03 -0400 Subject: [PATCH 11/20] Test completion quickmark/bookmark deletion. Validate that quickmarks and bookmarks can be deleted from the url completion menu. --- tests/helpers/fixtures.py | 4 +-- tests/helpers/stubs.py | 13 ++++++++ tests/unit/completion/test_models.py | 45 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 819dda15d..5482d6e66 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -201,7 +201,7 @@ def host_blocker_stub(stubs): @pytest.yield_fixture def quickmark_manager_stub(stubs): """Fixture which provides a fake quickmark manager object.""" - stub = stubs.UrlMarkManagerStub() + stub = stubs.QuickmarkManagerStub() objreg.register('quickmark-manager', stub) yield stub objreg.delete('quickmark-manager') @@ -210,7 +210,7 @@ def quickmark_manager_stub(stubs): @pytest.yield_fixture def bookmark_manager_stub(stubs): """Fixture which provides a fake bookmark manager object.""" - stub = stubs.UrlMarkManagerStub() + stub = stubs.BookmarkManagerStub() objreg.register('bookmark-manager', stub) yield stub objreg.delete('bookmark-manager') diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index c0b9c4cf3..75ad69fbe 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -474,6 +474,19 @@ class UrlMarkManagerStub(QObject): super().__init__(parent) self.marks = {} + def delete(self, key): + del self.marks[key] + self.removed.emit(key) + + +class BookmarkManagerStub(UrlMarkManagerStub): + pass + + +class QuickmarkManagerStub(UrlMarkManagerStub): + def quickmark_del(self, key): + self.delete(key) + class WebHistoryStub(QObject): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 19bd3c2de..eaab94d01 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -177,6 +177,51 @@ def test_url_completion(config_stub, web_history, quickmarks, bookmarks): ] +def test_url_completion_delete_bookmark(config_stub, web_history, quickmarks, + bookmarks): + """Test deleting a bookmark from the url completion model.""" + config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', + 'web-history-max-items': 2} + model = urlmodel.UrlCompletionModel() + index = Mock() + cat = Mock() + completion_widget = Mock() + index.isValid = Mock(return_value=True) + index.parent = Mock(return_value=cat) + index.data = Mock(return_value='https://github.com') + cat.child = Mock(return_value=index) + cat.data = Mock(return_value='Bookmarks') + completion_widget.currentIndex = Mock(return_value=index) + model.delete_cur_item(completion_widget) + assert 'https://github.com' not in bookmarks.marks + assert 'https://python.org' in bookmarks.marks + assert 'http://qutebrowser.org' in bookmarks.marks + + +def test_url_completion_delete_quickmark(config_stub, web_history, quickmarks, + bookmarks): + """Test deleting a bookmark from the url completion model.""" + config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', + 'web-history-max-items': 2} + model = urlmodel.UrlCompletionModel() + index = Mock() + sibling = Mock() + cat = Mock() + completion_widget = Mock() + index.isValid = Mock(return_value=True) + index.parent = Mock(return_value=cat) + index.sibling = Mock(return_value=sibling) + sibling.isValid = Mock(return_value=True) + sibling.data = Mock(return_value='ddg') + cat.child = Mock(return_value=index) + cat.data = Mock(return_value='Quickmarks') + completion_widget.currentIndex = Mock(return_value=index) + model.delete_cur_item(completion_widget) + assert 'aw' in quickmarks.marks + assert 'ddg' not in quickmarks.marks + assert 'wiki' in quickmarks.marks + + def test_session_completion(session_manager_stub): session_manager_stub.sessions = ['default', '1', '2'] actual = _get_completions(miscmodels.SessionCompletionModel()) From 9d49f5a57d63f713b2326ada87295a62ed3ccc39 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 23 Jun 2016 08:21:55 -0400 Subject: [PATCH 12/20] Test tab completion with multiple tabbed browsers. --- tests/helpers/fixtures.py | 13 ++++++++----- tests/unit/completion/test_models.py | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 5482d6e66..2530a1ab6 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -235,12 +235,15 @@ def session_manager_stub(stubs): @pytest.yield_fixture -def tabbed_browser_stub(stubs): - """Fixture which provides a fake tabbed-browser object.""" - stub = stubs.TabbedBrowserStub() - objreg.register('tabbed-browser', stub, scope='window', window=0) - yield stub +def tabbed_browser_stubs(stubs, win_registry): + """Fixture providing a fake tabbed-browser object on win_id 0 and 1.""" + win_registry.add_window(1) + stubs = [stubs.TabbedBrowserStub(), stubs.TabbedBrowserStub()] + objreg.register('tabbed-browser', stubs[0], scope='window', window=0) + objreg.register('tabbed-browser', stubs[1], scope='window', window=1) + yield stubs objreg.delete('tabbed-browser', scope='window', window=0) + objreg.delete('tabbed-browser', scope='window', window=1) @pytest.yield_fixture diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index eaab94d01..f7c1e141e 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -231,30 +231,39 @@ def test_session_completion(session_manager_stub): def test_tab_completion(stubs, qtbot, app_stub, win_registry, - tabbed_browser_stub): - tabbed_browser_stub.tabs = [ + tabbed_browser_stubs): + tabbed_browser_stubs[0].tabs = [ stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1), stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) ] + tabbed_browser_stubs[1].tabs = [ + stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), + ] actual = _get_completions(miscmodels.TabCompletionModel()) assert actual == [ ('0', [ ('0/1', 'https://github.com', 'GitHub'), ('0/2', 'https://wikipedia.org', 'Wikipedia'), ('0/3', 'https://duckduckgo.com', 'DuckDuckGo') + ]), + ('1', [ + ('1/1', 'https://wiki.archlinux.org', 'ArchWiki'), ]) ] def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, - tabbed_browser_stub): + tabbed_browser_stubs): """Verify closing a tab by deleting it from the completion widget.""" - tabbed_browser_stub.tabs = [ + tabbed_browser_stubs[0].tabs = [ stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), stubs.FakeWebView(QUrl('https://wikipedia.org'), 'Wikipedia', 1), stubs.FakeWebView(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) ] + tabbed_browser_stubs[1].tabs = [ + stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), + ] model = miscmodels.TabCompletionModel() index = Mock() cat = Mock() @@ -265,7 +274,7 @@ def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, cat.child = Mock(return_value=index) completion_widget.currentIndex = Mock(return_value=index) model.delete_cur_item(completion_widget) - actual = [tab.url() for tab in tabbed_browser_stub.tabs] + actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs] assert actual == [QUrl('https://github.com'), QUrl('https://duckduckgo.com')] From d6012ad95c9e1ed21022fec75117bf3ccf80e0c3 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 21 Jun 2016 23:23:24 -0400 Subject: [PATCH 13/20] Test delete_cur_item more cleanly. Reduce duplicate code by mocking out a QTreeView around the model and setting its index. --- tests/helpers/stubs.py | 6 +++ tests/unit/completion/test_models.py | 68 ++++++++++++---------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 75ad69fbe..a144f9f98 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -480,10 +480,16 @@ class UrlMarkManagerStub(QObject): class BookmarkManagerStub(UrlMarkManagerStub): + + """Stub for the bookmark-manager object.""" + pass class QuickmarkManagerStub(UrlMarkManagerStub): + + """Stub for the quickmark-manager object.""" + def quickmark_del(self, key): self.delete(key) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index f7c1e141e..7c7e2c4f7 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -21,10 +21,10 @@ import collections from datetime import datetime -from unittest.mock import Mock import pytest from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QTreeView from qutebrowser.completion.models import miscmodels, urlmodel, configmodel @@ -178,45 +178,28 @@ def test_url_completion(config_stub, web_history, quickmarks, bookmarks): def test_url_completion_delete_bookmark(config_stub, web_history, quickmarks, - bookmarks): + bookmarks, qtbot): """Test deleting a bookmark from the url completion model.""" config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', 'web-history-max-items': 2} model = urlmodel.UrlCompletionModel() - index = Mock() - cat = Mock() - completion_widget = Mock() - index.isValid = Mock(return_value=True) - index.parent = Mock(return_value=cat) - index.data = Mock(return_value='https://github.com') - cat.child = Mock(return_value=index) - cat.data = Mock(return_value='Bookmarks') - completion_widget.currentIndex = Mock(return_value=index) - model.delete_cur_item(completion_widget) + # delete item (1, 0) -> (bookmarks, 'https://github.com' ) + view = _mock_view_index(model, 1, 0) + model.delete_cur_item(view) assert 'https://github.com' not in bookmarks.marks assert 'https://python.org' in bookmarks.marks assert 'http://qutebrowser.org' in bookmarks.marks def test_url_completion_delete_quickmark(config_stub, web_history, quickmarks, - bookmarks): + bookmarks, qtbot): """Test deleting a bookmark from the url completion model.""" config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', 'web-history-max-items': 2} model = urlmodel.UrlCompletionModel() - index = Mock() - sibling = Mock() - cat = Mock() - completion_widget = Mock() - index.isValid = Mock(return_value=True) - index.parent = Mock(return_value=cat) - index.sibling = Mock(return_value=sibling) - sibling.isValid = Mock(return_value=True) - sibling.data = Mock(return_value='ddg') - cat.child = Mock(return_value=index) - cat.data = Mock(return_value='Quickmarks') - completion_widget.currentIndex = Mock(return_value=index) - model.delete_cur_item(completion_widget) + # delete item (0, 1) -> (quickmarks, 'ddg' ) + view = _mock_view_index(model, 0, 1) + model.delete_cur_item(view) assert 'aw' in quickmarks.marks assert 'ddg' not in quickmarks.marks assert 'wiki' in quickmarks.marks @@ -265,15 +248,8 @@ def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), ] model = miscmodels.TabCompletionModel() - index = Mock() - cat = Mock() - completion_widget = Mock() - index.isValid = Mock(return_value=True) - index.parent = Mock(return_value=cat) - index.data = Mock(return_value='0/2') - cat.child = Mock(return_value=index) - completion_widget.currentIndex = Mock(return_value=index) - model.delete_cur_item(completion_widget) + view = _mock_view_index(model, 0, 1) + model.delete_cur_item(view) actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs] assert actual == [QUrl('https://github.com'), QUrl('https://duckduckgo.com')] @@ -297,8 +273,8 @@ def test_setting_option_completion(monkeypatch, stubs, config_stub): module = 'qutebrowser.completion.models.configmodel' _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') config_stub.data = {'ui': {'gesture': 'off', - 'mind': 'on', - 'voice': 'sometimes'}} + 'mind': 'on', + 'voice': 'sometimes'}} actual = _get_completions(configmodel.SettingOptionCompletionModel('ui')) assert actual == [ ("ui", [ @@ -312,7 +288,7 @@ def test_setting_option_completion(monkeypatch, stubs, config_stub): def test_setting_value_completion(monkeypatch, stubs, config_stub): module = 'qutebrowser.completion.models.configmodel' _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') - config_stub.data = {'general': { 'volume': '0' }} + config_stub.data = {'general': {'volume': '0'}} model = configmodel.SettingValueCompletionModel('general', 'volume') actual = _get_completions(model) assert actual == [ @@ -385,6 +361,7 @@ def _patch_configdata(monkeypatch, stubs, symbol): ]) monkeypatch.setattr(symbol, data) + def _patch_config_section_desc(monkeypatch, stubs, symbol): """Patch the configdata module to provide fake SECTION_DESC.""" section_desc = { @@ -392,3 +369,18 @@ def _patch_config_section_desc(monkeypatch, stubs, symbol): 'ui': 'General options related to the user interface.', } monkeypatch.setattr(symbol, section_desc) + + +def _mock_view_index(model, category_idx, child_idx): + """Create a tree view from a model and set the current index. + + Args: + model: model to create a fake view for. + category_idx: index of the category to select. + child_idx: index of the child item under that category to select. + """ + view = QTreeView() + view.setModel(model) + idx = model.indexFromItem(model.item(category_idx).child(child_idx)) + view.setCurrentIndex(idx) + return view From f94ee172c96255d61aa7bd443a21755accbc7dd9 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 26 Jun 2016 22:12:11 -0400 Subject: [PATCH 14/20] Add completion/models/base to perfect_files. It now has 100% test coverage. --- scripts/dev/check_coverage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 060c06927..39e552e3c 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -143,6 +143,9 @@ PERFECT_FILES = [ 'qutebrowser/utils/error.py'), ('tests/unit/utils/test_typing.py', 'qutebrowser/utils/typing.py'), + ('tests/unit/completion/test_models.py', + 'qutebrowser/completion/models/base.py'), + ] From f50b8bb8e82341f41af5831ba7f3169eba984d5d Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 07:11:51 -0400 Subject: [PATCH 15/20] Use iter instead of __iter__. Corrent two places this was used. --- qutebrowser/config/sections.py | 2 +- tests/helpers/stubs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/sections.py b/qutebrowser/config/sections.py index 15b6a096e..254348fe9 100644 --- a/qutebrowser/config/sections.py +++ b/qutebrowser/config/sections.py @@ -54,7 +54,7 @@ class Section: def __iter__(self): """Iterate over all set values.""" - return self.values.__iter__() + return iter(self.values) def __bool__(self): """Get boolean state of section.""" diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index a144f9f98..752a41cd8 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -362,7 +362,7 @@ class FakeConfigSection: def __iter__(self): """Iterate over all set values.""" - return self.values.__iter__() + return iter(self.values) def __getitem__(self, key): return self.values[key] From ee59b133c0f9adafb474d00a9a5c9b4b6cea68b7 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 07:17:24 -0400 Subject: [PATCH 16/20] Don't bother mocking a history entry. Use webkit.history.Entry in tests instead of FakeHistoryEntry. --- tests/helpers/stubs.py | 15 ++------------- tests/unit/completion/test_models.py | 7 ++++--- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 752a41cd8..78f134051 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -29,7 +29,7 @@ from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, QNetworkCacheMetaData) from PyQt5.QtWidgets import QCommonStyle, QWidget -from qutebrowser.browser.webkit import webview +from qutebrowser.browser.webkit import webview, history from qutebrowser.config import configexc from qutebrowser.mainwindow import mainwindow @@ -452,17 +452,6 @@ class KeyConfigStub: self.bindings[section] = bindings -class FakeHistoryEntry: - - """Mock for webkit.history.Entry.""" - - def __init__(self, atime, url, title, redirect=False): - self.atime = float(atime) - self.url = url - self.title = title - self.redirect = redirect - - class UrlMarkManagerStub(QObject): """Stub for the quickmark-manager or bookmark-manager object.""" @@ -498,7 +487,7 @@ class WebHistoryStub(QObject): """Stub for the web-history object.""" - add_completion_item = pyqtSignal(FakeHistoryEntry) + add_completion_item = pyqtSignal(history.Entry) cleared = pyqtSignal() def __init__(self, parent=None): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 7c7e2c4f7..1c39b6ea2 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QTreeView from qutebrowser.completion.models import miscmodels, urlmodel, configmodel +from qutebrowser.browser.webkit import history @pytest.fixture @@ -55,13 +56,13 @@ def bookmarks(bookmark_manager_stub): def web_history(stubs, web_history_stub): """Pre-populate the web-history stub with some history entries.""" web_history_stub.history_dict = collections.OrderedDict([ - ('http://qutebrowser.org', stubs.FakeHistoryEntry( + ('http://qutebrowser.org', history.Entry( datetime(2015, 9, 5).timestamp(), QUrl('http://qutebrowser.org'), 'qutebrowser | qutebrowser')), - ('https://python.org', stubs.FakeHistoryEntry( + ('https://python.org', history.Entry( datetime(2016, 3, 8).timestamp(), QUrl('https://python.org'), 'Welcome to Python.org')), - ('https://github.com', stubs.FakeHistoryEntry( + ('https://github.com', history.Entry( datetime(2016, 5, 1).timestamp(), QUrl('https://github.com'), 'GitHub')), ]) From 849706e310f75081255e5d979a3792986f466400 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 07:19:36 -0400 Subject: [PATCH 17/20] Clean up Qt view used in completion tests. Add the view created during the tests to qtot so it gets cleaned up after the test exits. --- tests/unit/completion/test_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 1c39b6ea2..572291eb0 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -238,7 +238,7 @@ def test_tab_completion(stubs, qtbot, app_stub, win_registry, def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, - tabbed_browser_stubs): + tabbed_browser_stubs, qtbot): """Verify closing a tab by deleting it from the completion widget.""" tabbed_browser_stubs[0].tabs = [ stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), @@ -250,6 +250,7 @@ def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, ] model = miscmodels.TabCompletionModel() view = _mock_view_index(model, 0, 1) + qtbot.add_widget(view) model.delete_cur_item(view) actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs] assert actual == [QUrl('https://github.com'), From 4178f73ed9d4367202d7844fc4e1ac0369ed562b Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 07:24:55 -0400 Subject: [PATCH 18/20] Move utility functions to top of test_completion. Just a style change. --- tests/unit/completion/test_models.py | 166 +++++++++++++-------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 572291eb0..098979e8e 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -30,6 +30,89 @@ from qutebrowser.completion.models import miscmodels, urlmodel, configmodel from qutebrowser.browser.webkit import history +def _get_completions(model): + """Collect all the completion entries of a model, organized by category. + + The result is a list of form: + [ + (CategoryName: [(name, desc, misc), ...]), + (CategoryName: [(name, desc, misc), ...]), + ... + ] + """ + completions = [] + for i in range(0, model.rowCount()): + category = model.item(i) + entries = [] + for j in range(0, category.rowCount()): + name = category.child(j, 0) + desc = category.child(j, 1) + misc = category.child(j, 2) + entries.append((name.text(), desc.text(), misc.text())) + completions.append((category.text(), entries)) + return completions + + +def _patch_cmdutils(monkeypatch, stubs, symbol): + """Patch the cmdutils module to provide fake commands.""" + cmd_utils = stubs.FakeCmdUtils({ + 'stop': stubs.FakeCommand(name='stop', desc='stop qutebrowser'), + 'drop': stubs.FakeCommand(name='drop', desc='drop all user data'), + 'roll': stubs.FakeCommand(name='roll', desc='never gonna give you up'), + 'hide': stubs.FakeCommand(name='hide', hide=True), + 'depr': stubs.FakeCommand(name='depr', deprecated=True), + }) + monkeypatch.setattr(symbol, cmd_utils) + + +def _patch_configdata(monkeypatch, stubs, symbol): + """Patch the configdata module to provide fake data.""" + data = collections.OrderedDict([ + ('general', stubs.FakeConfigSection( + ('time', + stubs.FakeSettingValue(('fast', 'slow'), 'slow'), + 'Is an illusion.\n\nLunchtime doubly so.'), + ('volume', + stubs.FakeSettingValue(('0', '11'), '11'), + 'Goes to 11'))), + ('ui', stubs.FakeConfigSection( + ('gesture', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Waggle your hands to control qutebrowser'), + ('mind', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Enable mind-control ui (experimental)'), + ('voice', + stubs.FakeSettingValue(('on', 'off'), 'off'), + 'Whether to respond to voice commands'))), + ]) + monkeypatch.setattr(symbol, data) + + +def _patch_config_section_desc(monkeypatch, stubs, symbol): + """Patch the configdata module to provide fake SECTION_DESC.""" + section_desc = { + 'general': 'General/miscellaneous options.', + 'ui': 'General options related to the user interface.', + } + monkeypatch.setattr(symbol, section_desc) + + +def _mock_view_index(model, category_idx, child_idx): + """Create a tree view from a model and set the current index. + + Args: + model: model to create a fake view for. + category_idx: index of the category to select. + child_idx: index of the child item under that category to select. + """ + view = QTreeView() + view.setModel(model) + idx = model.indexFromItem(model.item(category_idx).child(child_idx)) + view.setCurrentIndex(idx) + return view + + @pytest.fixture def quickmarks(quickmark_manager_stub): """Pre-populate the quickmark-manager stub with some quickmarks.""" @@ -303,86 +386,3 @@ def test_setting_value_completion(monkeypatch, stubs, config_stub): ('11', '', ''), ]) ] - - -def _get_completions(model): - """Collect all the completion entries of a model, organized by category. - - The result is a list of form: - [ - (CategoryName: [(name, desc, misc), ...]), - (CategoryName: [(name, desc, misc), ...]), - ... - ] - """ - completions = [] - for i in range(0, model.rowCount()): - category = model.item(i) - entries = [] - for j in range(0, category.rowCount()): - name = category.child(j, 0) - desc = category.child(j, 1) - misc = category.child(j, 2) - entries.append((name.text(), desc.text(), misc.text())) - completions.append((category.text(), entries)) - return completions - - -def _patch_cmdutils(monkeypatch, stubs, symbol): - """Patch the cmdutils module to provide fake commands.""" - cmd_utils = stubs.FakeCmdUtils({ - 'stop': stubs.FakeCommand(name='stop', desc='stop qutebrowser'), - 'drop': stubs.FakeCommand(name='drop', desc='drop all user data'), - 'roll': stubs.FakeCommand(name='roll', desc='never gonna give you up'), - 'hide': stubs.FakeCommand(name='hide', hide=True), - 'depr': stubs.FakeCommand(name='depr', deprecated=True), - }) - monkeypatch.setattr(symbol, cmd_utils) - - -def _patch_configdata(monkeypatch, stubs, symbol): - """Patch the configdata module to provide fake data.""" - data = collections.OrderedDict([ - ('general', stubs.FakeConfigSection( - ('time', - stubs.FakeSettingValue(('fast', 'slow'), 'slow'), - 'Is an illusion.\n\nLunchtime doubly so.'), - ('volume', - stubs.FakeSettingValue(('0', '11'), '11'), - 'Goes to 11'))), - ('ui', stubs.FakeConfigSection( - ('gesture', - stubs.FakeSettingValue(('on', 'off'), 'off'), - 'Waggle your hands to control qutebrowser'), - ('mind', - stubs.FakeSettingValue(('on', 'off'), 'off'), - 'Enable mind-control ui (experimental)'), - ('voice', - stubs.FakeSettingValue(('on', 'off'), 'off'), - 'Whether to respond to voice commands'))), - ]) - monkeypatch.setattr(symbol, data) - - -def _patch_config_section_desc(monkeypatch, stubs, symbol): - """Patch the configdata module to provide fake SECTION_DESC.""" - section_desc = { - 'general': 'General/miscellaneous options.', - 'ui': 'General options related to the user interface.', - } - monkeypatch.setattr(symbol, section_desc) - - -def _mock_view_index(model, category_idx, child_idx): - """Create a tree view from a model and set the current index. - - Args: - model: model to create a fake view for. - category_idx: index of the category to select. - child_idx: index of the child item under that category to select. - """ - view = QTreeView() - view.setModel(model) - idx = model.indexFromItem(model.item(category_idx).child(child_idx)) - view.setCurrentIndex(idx) - return view From 94ec712ea8e6be7ed6475ea81fbc9bfbd85349bc Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 20:12:04 -0400 Subject: [PATCH 19/20] Really clean up Qt view in completion tests. Missed a spot. --- tests/unit/completion/test_models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 098979e8e..6cb736a04 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -98,7 +98,7 @@ def _patch_config_section_desc(monkeypatch, stubs, symbol): monkeypatch.setattr(symbol, section_desc) -def _mock_view_index(model, category_idx, child_idx): +def _mock_view_index(model, category_idx, child_idx, qtbot): """Create a tree view from a model and set the current index. Args: @@ -107,6 +107,7 @@ def _mock_view_index(model, category_idx, child_idx): child_idx: index of the child item under that category to select. """ view = QTreeView() + qtbot.add_widget(view) view.setModel(model) idx = model.indexFromItem(model.item(category_idx).child(child_idx)) view.setCurrentIndex(idx) @@ -268,7 +269,7 @@ def test_url_completion_delete_bookmark(config_stub, web_history, quickmarks, 'web-history-max-items': 2} model = urlmodel.UrlCompletionModel() # delete item (1, 0) -> (bookmarks, 'https://github.com' ) - view = _mock_view_index(model, 1, 0) + view = _mock_view_index(model, 1, 0, qtbot) model.delete_cur_item(view) assert 'https://github.com' not in bookmarks.marks assert 'https://python.org' in bookmarks.marks @@ -282,7 +283,7 @@ def test_url_completion_delete_quickmark(config_stub, web_history, quickmarks, 'web-history-max-items': 2} model = urlmodel.UrlCompletionModel() # delete item (0, 1) -> (quickmarks, 'ddg' ) - view = _mock_view_index(model, 0, 1) + view = _mock_view_index(model, 0, 1, qtbot) model.delete_cur_item(view) assert 'aw' in quickmarks.marks assert 'ddg' not in quickmarks.marks @@ -321,7 +322,7 @@ def test_tab_completion(stubs, qtbot, app_stub, win_registry, def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, - tabbed_browser_stubs, qtbot): + tabbed_browser_stubs): """Verify closing a tab by deleting it from the completion widget.""" tabbed_browser_stubs[0].tabs = [ stubs.FakeWebView(QUrl('https://github.com'), 'GitHub', 0), @@ -332,7 +333,7 @@ def test_tab_completion_delete(stubs, qtbot, app_stub, win_registry, stubs.FakeWebView(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), ] model = miscmodels.TabCompletionModel() - view = _mock_view_index(model, 0, 1) + view = _mock_view_index(model, 0, 1, qtbot) qtbot.add_widget(view) model.delete_cur_item(view) actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs] From d45acb0388d8f6e8b517ae19cf0e72f24a96ab1e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 30 Jun 2016 20:12:44 -0400 Subject: [PATCH 20/20] Eliminate FakeSettingSection/Value. Don't really need to mock these out for tests as the real classes are simple enough. --- tests/helpers/stubs.py | 32 ++-------------------------- tests/unit/completion/test_models.py | 20 +++++++++++------ 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 78f134051..fa7f60fa5 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -349,39 +349,11 @@ class FakeTimer(QObject): return self._started -class FakeConfigSection: - - """A stub for a KeyValue entry in configdata.DATA.""" - - def __init__(self, *entries): - self.values = collections.OrderedDict() - self.descriptions = {} - for name, value, desc in entries: - self.values[name] = value - self.descriptions[name] = desc - - def __iter__(self): - """Iterate over all set values.""" - return iter(self.values) - - def __getitem__(self, key): - return self.values[key] - - -class FakeSettingValue: - - """A stub for a SettingValue entry in configdata.DATA[section].""" - - def __init__(self, valid_values, default=None): - self.typ = FakeConfigType(valid_values) - self.default = lambda: default - - class FakeConfigType: - """A stub for the typ attribute of a FakeSettingValue.""" + """A stub to provide valid_values for typ attribute of a SettingValue.""" - def __init__(self, valid_values): + def __init__(self, *valid_values): # normally valid_values would be a ValidValues, but for simplicity of # testing this can be a simple list: [(val, desc), (val, desc), ...] self.complete = lambda: [(val, '') for val in valid_values] diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 6cb736a04..8c19d4bee 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QTreeView from qutebrowser.completion.models import miscmodels, urlmodel, configmodel from qutebrowser.browser.webkit import history +from qutebrowser.config import sections, value def _get_completions(model): @@ -68,22 +69,27 @@ def _patch_cmdutils(monkeypatch, stubs, symbol): def _patch_configdata(monkeypatch, stubs, symbol): """Patch the configdata module to provide fake data.""" data = collections.OrderedDict([ - ('general', stubs.FakeConfigSection( + ('general', sections.KeyValue( ('time', - stubs.FakeSettingValue(('fast', 'slow'), 'slow'), + value.SettingValue(stubs.FakeConfigType('fast', 'slow'), + default='slow'), 'Is an illusion.\n\nLunchtime doubly so.'), ('volume', - stubs.FakeSettingValue(('0', '11'), '11'), + value.SettingValue(stubs.FakeConfigType('0', '11'), + default='11'), 'Goes to 11'))), - ('ui', stubs.FakeConfigSection( + ('ui', sections.KeyValue( ('gesture', - stubs.FakeSettingValue(('on', 'off'), 'off'), + value.SettingValue(stubs.FakeConfigType(('on', 'off')), + default='off'), 'Waggle your hands to control qutebrowser'), ('mind', - stubs.FakeSettingValue(('on', 'off'), 'off'), + value.SettingValue(stubs.FakeConfigType(('on', 'off')), + default='off'), 'Enable mind-control ui (experimental)'), ('voice', - stubs.FakeSettingValue(('on', 'off'), 'off'), + value.SettingValue(stubs.FakeConfigType(('on', 'off')), + default='off'), 'Whether to respond to voice commands'))), ]) monkeypatch.setattr(symbol, data)