Merge branch 'completion_tests' of https://github.com/rcorre/qutebrowser into rcorre-completion_tests

This commit is contained in:
Florian Bruhin 2016-07-01 14:36:32 +02:00
commit b178099f44
7 changed files with 584 additions and 9 deletions

View File

@ -54,7 +54,7 @@ class Section:
def __iter__(self): def __iter__(self):
"""Iterate over all set values.""" """Iterate over all set values."""
return self.values.__iter__() return iter(self.values)
def __bool__(self): def __bool__(self):
"""Get boolean state of section.""" """Get boolean state of section."""

View File

@ -143,6 +143,9 @@ PERFECT_FILES = [
'qutebrowser/utils/error.py'), 'qutebrowser/utils/error.py'),
('tests/unit/utils/test_typing.py', ('tests/unit/utils/test_typing.py',
'qutebrowser/utils/typing.py'), 'qutebrowser/utils/typing.py'),
('tests/unit/completion/test_models.py',
'qutebrowser/completion/models/base.py'),
] ]

View File

@ -198,6 +198,63 @@ def host_blocker_stub(stubs):
objreg.delete('host-blocker') objreg.delete('host-blocker')
@pytest.yield_fixture
def quickmark_manager_stub(stubs):
"""Fixture which provides a fake quickmark manager object."""
stub = stubs.QuickmarkManagerStub()
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.BookmarkManagerStub()
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.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.yield_fixture
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
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') @pytest.fixture(scope='session')
def stubs(): def stubs():
"""Provide access to stub objects useful for testing.""" """Provide access to stub objects useful for testing."""

View File

@ -21,6 +21,7 @@
"""Fake objects/stubs.""" """Fake objects/stubs."""
import collections
from unittest import mock from unittest import mock
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
@ -28,8 +29,9 @@ from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData) QNetworkCacheMetaData)
from PyQt5.QtWidgets import QCommonStyle, QWidget 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.config import configexc
from qutebrowser.mainwindow import mainwindow
class FakeNetworkCache(QAbstractNetworkCache): class FakeNetworkCache(QAbstractNetworkCache):
@ -218,13 +220,20 @@ class FakeWebView(QWidget):
"""Fake WebView which can be added to a tab.""" """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__() super().__init__()
self.progress = 0 self.progress = 0
self.scroll_pos = (-1, -1) self.scroll_pos = (-1, -1)
self.load_status = webview.LoadStatus.none self.load_status = webview.LoadStatus.none
self.tab_id = 0 self.tab_id = tab_id
self.cur_url = FakeUrl() self.cur_url = url
self.title = title
def url(self):
return self.cur_url
class FakeSignal: class FakeSignal:
@ -283,8 +292,13 @@ class FakeCommand:
"""A simple command stub which has a description.""" """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.desc = desc
self.name = name
self.hide = hide
self.debug = debug
self.deprecated = deprecated
class FakeTimer(QObject): class FakeTimer(QObject):
@ -335,6 +349,16 @@ class FakeTimer(QObject):
return self._started return self._started
class FakeConfigType:
"""A stub to provide valid_values for typ attribute of a SettingValue."""
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): class ConfigStub(QObject):
"""Stub for the config module. """Stub for the config module.
@ -368,7 +392,7 @@ class ConfigStub(QObject):
""" """
return self.data[name] return self.data[name]
def get(self, sect, opt): def get(self, sect, opt, raw=True):
"""Get a value from the config.""" """Get a value from the config."""
data = self.data[sect] data = self.data[sect]
try: try:
@ -400,9 +424,103 @@ class KeyConfigStub:
self.bindings[section] = bindings self.bindings[section] = bindings
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 = {}
def delete(self, key):
del self.marks[key]
self.removed.emit(key)
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)
class WebHistoryStub(QObject):
"""Stub for the web-history object."""
add_completion_item = pyqtSignal(history.Entry)
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: class HostBlockerStub:
"""Stub for the host-blocker object.""" """Stub for the host-blocker object."""
def __init__(self): def __init__(self):
self.blocked_hosts = set() self.blocked_hosts = set()
class SessionManagerStub:
"""Stub for the session-manager object."""
def __init__(self):
self.sessions = []
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
def on_tab_close_requested(self, idx):
del self.tabs[idx]
class ApplicationStub(QObject):
"""Stub to insert as the app object in objreg."""
new_window = pyqtSignal(mainwindow.MainWindow)
def __init__(self):
super().__init__()

View File

@ -391,3 +391,4 @@ def test_init(qapp, tmpdir, monkeypatch, fake_save_manager):
assert hist.parent() is qapp assert hist.parent() is qapp
assert QWebHistoryInterface.defaultInterface()._history is hist assert QWebHistoryInterface.defaultInterface()._history is hist
assert fake_save_manager.add_saveable.called assert fake_save_manager.add_saveable.called
objreg.delete('web-history')

View File

@ -0,0 +1,395 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for completion models."""
import collections
from datetime import datetime
import pytest
from PyQt5.QtCore import QUrl
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):
"""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', sections.KeyValue(
('time',
value.SettingValue(stubs.FakeConfigType('fast', 'slow'),
default='slow'),
'Is an illusion.\n\nLunchtime doubly so.'),
('volume',
value.SettingValue(stubs.FakeConfigType('0', '11'),
default='11'),
'Goes to 11'))),
('ui', sections.KeyValue(
('gesture',
value.SettingValue(stubs.FakeConfigType(('on', 'off')),
default='off'),
'Waggle your hands to control qutebrowser'),
('mind',
value.SettingValue(stubs.FakeConfigType(('on', 'off')),
default='off'),
'Enable mind-control ui (experimental)'),
('voice',
value.SettingValue(stubs.FakeConfigType(('on', 'off')),
default='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, qtbot):
"""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()
qtbot.add_widget(view)
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."""
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', history.Entry(
datetime(2015, 9, 5).timestamp(),
QUrl('http://qutebrowser.org'), 'qutebrowser | qutebrowser')),
('https://python.org', history.Entry(
datetime(2016, 3, 8).timestamp(),
QUrl('https://python.org'), 'Welcome to Python.org')),
('https://github.com', history.Entry(
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):
"""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 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 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 test_url_completion_delete_bookmark(config_stub, web_history, quickmarks,
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()
# delete item (1, 0) -> (bookmarks, 'https://github.com' )
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
assert 'http://qutebrowser.org' in bookmarks.marks
def test_url_completion_delete_quickmark(config_stub, web_history, quickmarks,
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()
# delete item (0, 1) -> (quickmarks, 'ddg' )
view = _mock_view_index(model, 0, 1, qtbot)
model.delete_cur_item(view)
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())
assert actual == [
("Sessions", [('default', '', ''), ('1', '', ''), ('2', '', '')])
]
def test_tab_completion(stubs, qtbot, app_stub, win_registry,
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_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),
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()
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]
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')
_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 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 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', '', ''),
])
]

View File

@ -823,8 +823,9 @@ class TestCommand:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch(self, monkeypatch, stubs): def patch(self, monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands.""" """Patch the cmdutils module to provide fake commands."""
cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"), cmd_utils = stubs.FakeCmdUtils({
'cmd2': stubs.FakeCommand("desc 2")}) 'cmd1': stubs.FakeCommand(desc="desc 1"),
'cmd2': stubs.FakeCommand(desc="desc 2")})
monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils', monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils',
cmd_utils) cmd_utils)