Decouple categories from completionmodel.

Instead of add_list and add_sqltable, the completion model now supports
add_category, and callees either pass in a SqlCategory or ListCategory. This
makes unit testing much easier.

This also folds CompletionFilterModel into the ListCategory class.
This commit is contained in:
Ryan Roden-Corrent 2017-03-03 08:31:28 -05:00
parent ce3c555712
commit f95dff4d9e
19 changed files with 484 additions and 775 deletions

View File

@ -41,7 +41,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing) objreg, utils, typing)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import sortfilter, urlmodel, miscmodels from qutebrowser.completion.models import urlmodel, miscmodels
class CommandDispatcher: class CommandDispatcher:
@ -1024,10 +1024,9 @@ class CommandDispatcher:
int(part) int(part)
except ValueError: except ValueError:
model = miscmodels.buffer() model = miscmodels.buffer()
sf = sortfilter.CompletionFilterModel(source=model) model.set_pattern(index)
sf.set_pattern(index) if model.count() > 0:
if sf.count() > 0: index = model.data(model.first_item())
index = sf.data(sf.first_item())
index_parts = index.split('/', 1) index_parts = index.split('/', 1)
else: else:
raise cmdexc.CommandError( raise cmdexc.CommandError(

View File

@ -275,6 +275,7 @@ class CompletionView(QTreeView):
self.hide() self.hide()
return return
model.setParent(self)
old_model = self.model() old_model = self.model()
if model is not old_model: if model is not old_model:
sel_model = self.selectionModel() sel_model = self.selectionModel()

View File

@ -24,7 +24,6 @@ import re
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.utils import log, qtutils from qutebrowser.utils import log, qtutils
from qutebrowser.completion.models import sortfilter, listcategory, sqlcategory
class CompletionModel(QAbstractItemModel): class CompletionModel(QAbstractItemModel):
@ -65,33 +64,8 @@ class CompletionModel(QAbstractItemModel):
return self._categories[index.row()] return self._categories[index.row()]
return None return None
def add_list(self, name, items): def add_category(self, cat):
"""Add a list of items as a completion category. """Add a completion category to the model."""
Args:
name: Title of the category.
items: List of tuples.
"""
cat = listcategory.ListCategory(name, items, parent=self)
filtermodel = sortfilter.CompletionFilterModel(cat,
self.columns_to_filter)
self._categories.append(filtermodel)
def add_sqltable(self, name, *, select='*', where=None, sort_by=None,
sort_order=None):
"""Create a new completion category and add it to this model.
Args:
name: Name of category, and the table in the database.
select: A custom result column expression for the select statement.
where: An optional clause to filter out some rows.
sort_by: The name of the field to sort by, or None for no sorting.
sort_order: Either 'asc' or 'desc', if sort_by is non-None
"""
cat = sqlcategory.SqlCategory(name, parent=self, sort_by=sort_by,
sort_order=sort_order,
select=select, where=where,
columns_to_filter=self.columns_to_filter)
self._categories.append(cat) self._categories.append(cat)
def data(self, index, role=Qt.DisplayRole): def data(self, index, role=Qt.DisplayRole):
@ -210,7 +184,7 @@ class CompletionModel(QAbstractItemModel):
# TODO: should pattern be saved in the view layer instead? # TODO: should pattern be saved in the view layer instead?
self.pattern = pattern self.pattern = pattern
for cat in self._categories: for cat in self._categories:
cat.set_pattern(pattern) cat.set_pattern(pattern, self.columns_to_filter)
def first_item(self): def first_item(self):
"""Return the index of the first child (non-category) in the model.""" """Return the index of the first child (non-category) in the model."""

View File

@ -20,7 +20,7 @@
"""Functions that return config-related completion models.""" """Functions that return config-related completion models."""
from qutebrowser.config import configdata, configexc from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.utils import objreg from qutebrowser.utils import objreg
@ -29,7 +29,7 @@ def section():
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip()) sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
for name in configdata.DATA) for name in configdata.DATA)
model.add_list("Sections", sections) model.add_category(listcategory.ListCategory("Sections", sections))
return model return model
@ -57,7 +57,7 @@ def option(sectname):
config = objreg.get('config') config = objreg.get('config')
val = config.get(sectname, name, raw=True) val = config.get(sectname, name, raw=True)
options.append((name, desc, val)) options.append((name, desc, val))
model.add_list(sectname, options) model.add_category(listcategory.ListCategory(sectname, options))
return model return model
@ -88,8 +88,9 @@ def value(sectname, optname):
# Different type for each value (KeyValue) # Different type for each value (KeyValue)
vals = configdata.DATA[sectname][optname].typ.complete() vals = configdata.DATA[sectname][optname].typ.complete()
model.add_list("Current/Default", [(current, "Current value"), cur_cat = listcategory.ListCategory("Current/Default",
(default, "Default value")]) [(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat)
if vals is not None: if vals is not None:
model.add_list("Completions", vals) model.add_category(listcategory.ListCategory("Completions", vals))
return model return model

View File

@ -23,44 +23,100 @@ Module attributes:
Role: An enum of user defined model roles. Role: An enum of user defined model roles.
""" """
from PyQt5.QtCore import Qt import re
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QModelIndex
from PyQt5.QtGui import QStandardItem, QStandardItemModel from PyQt5.QtGui import QStandardItem, QStandardItemModel
from qutebrowser.utils import qtutils, debug, log
class ListCategory(QStandardItemModel):
class ListCategory(QSortFilterProxyModel):
"""Expose a list of items as a category for the CompletionModel.""" """Expose a list of items as a category for the CompletionModel."""
def __init__(self, name, items, parent=None): def __init__(self, name, items, parent=None):
super().__init__(parent) super().__init__(parent)
self.name = name self.name = name
# self.setColumnCount(3) TODO needed? self.srcmodel = QStandardItemModel(parent=self)
# TODO: batch insert? self.pattern = ''
# TODO: can I just insert a tuple instead of a list? self.pattern_re = None
for item in items: for item in items:
self.appendRow([QStandardItem(x) for x in item]) self.srcmodel.appendRow([QStandardItem(x) for x in item])
self.setSourceModel(self.srcmodel)
def flags(self, index): def set_pattern(self, val, columns_to_filter):
"""Return the item flags for index. """Setter for pattern.
Override QAbstractItemModel::flags.
Args: Args:
index: The QModelIndex to get item flags for. val: The value to set.
"""
with debug.log_time(log.completion, 'Setting filter pattern'):
self.columns_to_filter = columns_to_filter
self.pattern = val
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
self.pattern_re = re.compile(val, re.IGNORECASE)
self.invalidate()
sortcol = 0
self.sort(sortcol)
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
Override QSortFilterProxyModel::filterAcceptsRow.
Args:
row: The row of the item.
parent: The parent item QModelIndex.
Return: Return:
The item flags, or Qt.NoItemFlags on error. True if self.pattern is contained in item, or if it's a root item
(category). False in all other cases
""" """
if not index.isValid(): if not self.pattern:
return return True
if index.parent().isValid(): for col in self.columns_to_filter:
# item idx = self.srcmodel.index(row, col, parent)
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | if not idx.isValid(): # pragma: no cover
Qt.ItemNeverHasChildren) # this is a sanity check not hit by any test case
continue
data = self.srcmodel.data(idx)
if not data:
continue
elif self.pattern_re.search(data):
return True
return False
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self.pattern. Other than that, uses
normal Python string sorting.
Args:
lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*)
Return:
True if left < right, else False
"""
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
leftstart = left.startswith(self.pattern)
rightstart = right.startswith(self.pattern)
if leftstart and rightstart:
return left < right
elif leftstart:
return True
elif rightstart:
return False
else: else:
# category return left < right
return Qt.NoItemFlags
def set_pattern(self, pattern):
pass

View File

@ -22,14 +22,14 @@
from qutebrowser.config import config, configdata from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils from qutebrowser.utils import objreg, log, qtutils
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import completionmodel from qutebrowser.completion.models import completionmodel, listcategory
def command(): def command():
"""A CompletionModel filled with non-hidden commands and descriptions.""" """A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False) cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
model.add_list("Commands", cmdlist) model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model return model
@ -53,22 +53,24 @@ def helptopic():
name = '{}->{}'.format(sectname, optname) name = '{}->{}'.format(sectname, optname)
settings.append((name, desc)) settings.append((name, desc))
model.add_list("Commands", cmdlist) model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_list("Settings", settings) model.add_category(listcategory.ListCategory("Settings", settings))
return model return model
def quickmark(): def quickmark():
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
model.add_list('Quickmarks', objreg.get('quickmark-manager').marks.items()) marks = objreg.get('quickmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Quickmarks', marks))
return model return model
def bookmark(): def bookmark():
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
model.add_list('Bookmarks', objreg.get('bookmark-manager').marks.items()) marks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Bookmarks', marks))
return model return model
@ -79,7 +81,7 @@ def session():
manager = objreg.get('session-manager') manager = objreg.get('session-manager')
sessions = ((name,) for name in manager.list_sessions() sessions = ((name,) for name in manager.list_sessions()
if not name.startswith('_')) if not name.startswith('_'))
model.add_list("Sessions", sessions) model.add_category(listcategory.ListCategory("Sessions", sessions))
except OSError: except OSError:
log.completion.exception("Failed to list sessions!") log.completion.exception("Failed to list sessions!")
return model return model
@ -122,7 +124,8 @@ def buffer():
tabs.append(("{}/{}".format(win_id, idx + 1), tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(), tab.url().toDisplayString(),
tabbed_browser.page_title(idx))) tabbed_browser.page_title(idx)))
model.add_list("{}".format(win_id), tabs) cat = listcategory.ListCategory("{}".format(win_id), tabs)
model.add_category(cat)
return model return model
@ -135,7 +138,7 @@ def bind(_key):
# TODO: offer a 'Current binding' completion based on the key. # TODO: offer a 'Current binding' completion based on the key.
model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True) cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
model.add_list("Commands", cmdlist) model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model return model

View File

@ -1,133 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
"""A filtering/sorting base model for completions.
Contains:
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
"""
import re
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
from qutebrowser.utils import log, qtutils, debug
class CompletionFilterModel(QSortFilterProxyModel):
"""Subclass of QSortFilterProxyModel with custom filtering.
Attributes:
pattern: The pattern to filter with.
srcmodel: The current source model.
Kept as attribute because calling `sourceModel` takes quite
a long time for some reason.
"""
def __init__(self, source, columns_to_filter, parent=None):
super().__init__(parent)
super().setSourceModel(source)
self.srcmodel = source
self.pattern = ''
self.pattern_re = None
self.columns_to_filter = columns_to_filter
self.name = source.name
def set_pattern(self, val):
"""Setter for pattern.
Args:
val: The value to set.
"""
with debug.log_time(log.completion, 'Setting filter pattern'):
self.pattern = val
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
self.pattern_re = re.compile(val, re.IGNORECASE)
self.invalidate()
sortcol = 0
self.sort(sortcol)
def setSourceModel(self, model):
"""Override QSortFilterProxyModel's setSourceModel to clear pattern."""
log.completion.debug("Setting source model: {}".format(model))
self.set_pattern('')
super().setSourceModel(model)
self.srcmodel = model
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
Override QSortFilterProxyModel::filterAcceptsRow.
Args:
row: The row of the item.
parent: The parent item QModelIndex.
Return:
True if self.pattern is contained in item, or if it's a root item
(category). False in all other cases
"""
if not self.pattern:
return True
for col in self.columns_to_filter:
idx = self.srcmodel.index(row, col, parent)
if not idx.isValid(): # pragma: no cover
# this is a sanity check not hit by any test case
continue
data = self.srcmodel.data(idx)
if not data:
continue
elif self.pattern_re.search(data):
return True
return False
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self.pattern. Other than that, uses
normal Python string sorting.
Args:
lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*)
Return:
True if left < right, else False
"""
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
leftstart = left.startswith(self.pattern)
rightstart = right.startswith(self.pattern)
if leftstart and rightstart:
return left < right
elif leftstart:
return True
elif rightstart:
return False
else:
return left < right

View File

@ -29,36 +29,48 @@ from qutebrowser.misc import sql
class SqlCategory(QSqlQueryModel): class SqlCategory(QSqlQueryModel):
"""Wraps a SqlQuery for use as a completion category.""" """Wraps a SqlQuery for use as a completion category."""
def __init__(self, name, *, sort_by, sort_order, select, where, def __init__(self, name, *, sort_by=None, sort_order=None, select='*',
columns_to_filter, parent=None): where=None, parent=None):
"""Create a new completion category backed by a sql table.
Args:
name: Name of category, and the table in the database.
select: A custom result column expression for the select statement.
where: An optional clause to filter out some rows.
sort_by: The name of the field to sort by, or None for no sorting.
sort_order: Either 'asc' or 'desc', if sort_by is non-None
"""
super().__init__(parent=parent) super().__init__(parent=parent)
self.name = name self.name = name
self._sort_by = sort_by
self._sort_order = sort_order
self._select = select
self._where = where
self.set_pattern('', [0])
query = sql.run_query('select * from {} limit 1'.format(name)) def set_pattern(self, pattern, columns_to_filter):
self._fields = [query.record().fieldName(i) for i in columns_to_filter] query = sql.run_query('select * from {} limit 1'.format(self.name))
fields = [query.record().fieldName(i) for i in columns_to_filter]
querystr = 'select {} from {} where ('.format(select, name) querystr = 'select {} from {} where ('.format(self._select, self.name)
# the incoming pattern will have literal % and _ escaped with '\' # the incoming pattern will have literal % and _ escaped with '\'
# we need to tell sql to treat '\' as an escape character # we need to tell sql to treat '\' as an escape character
querystr += ' or '.join("{} like ? escape '\\'".format(f) querystr += ' or '.join("{} like ? escape '\\'".format(f)
for f in self._fields) for f in fields)
querystr += ')' querystr += ')'
if where: if self._where:
querystr += ' and ' + where querystr += ' and ' + self._where
if sort_by: if self._sort_by:
assert sort_order == 'asc' or sort_order == 'desc' assert self._sort_order in ['asc', 'desc']
querystr += ' order by {} {}'.format(sort_by, sort_order) querystr += ' order by {} {}'.format(self._sort_by,
self._sort_order)
self._querystr = querystr
self.set_pattern('')
def set_pattern(self, pattern):
# escape to treat a user input % or _ as a literal, not a wildcard # escape to treat a user input % or _ as a literal, not a wildcard
pattern = pattern.replace('%', '\\%') pattern = pattern.replace('%', '\\%')
pattern = pattern.replace('_', '\\_') pattern = pattern.replace('_', '\\_')
# treat spaces as wildcards to match any of the typed words # treat spaces as wildcards to match any of the typed words
pattern = re.sub(r' +', '%', pattern) pattern = re.sub(r' +', '%', pattern)
pattern = '%{}%'.format(pattern) pattern = '%{}%'.format(pattern)
query = sql.run_query(self._querystr, [pattern for _ in self._fields]) query = sql.run_query(querystr, [pattern for _ in fields])
self.setQuery(query) self.setQuery(query)

View File

@ -19,7 +19,8 @@
"""Function to return the url completion model for the `open` command.""" """Function to return the url completion model for the `open` command."""
from qutebrowser.completion.models import completionmodel from qutebrowser.completion.models import (completionmodel, listcategory,
sqlcategory)
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import qtutils, log, objreg from qutebrowser.utils import qtutils, log, objreg
@ -46,8 +47,7 @@ def _delete_url(completion):
log.completion.debug('Deleting bookmark {}'.format(urlstr)) log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager') bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr) bookmark_manager.delete(urlstr)
else: elif catname == 'Quickmarks':
assert catname == 'Quickmarks', 'Unknown category {}'.format(catname)
quickmark_manager = objreg.get('quickmark-manager') quickmark_manager = objreg.get('quickmark-manager')
sibling = index.sibling(index.row(), _TEXTCOL) sibling = index.sibling(index.row(), _TEXTCOL)
qtutils.ensure_valid(sibling) qtutils.ensure_valid(sibling)
@ -66,15 +66,17 @@ def url():
columns_to_filter=[_URLCOL, _TEXTCOL], columns_to_filter=[_URLCOL, _TEXTCOL],
delete_cur_item=_delete_url) delete_cur_item=_delete_url)
quickmarks = objreg.get('quickmark-manager').marks.items() quickmarks = ((url, name) for (name, url)
model.add_list('Quickmarks', ((url, name) for (name, url) in quickmarks)) in objreg.get('quickmark-manager').marks.items())
bookmarks = objreg.get('bookmark-manager').marks.items()
model.add_list('Bookmarks', objreg.get('bookmark-manager').marks.items()) model.add_category(listcategory.ListCategory('Quickmarks', quickmarks))
model.add_category(listcategory.ListCategory('Bookmarks', bookmarks))
timefmt = config.get('completion', 'timestamp-format') timefmt = config.get('completion', 'timestamp-format')
select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt) select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt)
model.add_sqltable('History', hist_cat = sqlcategory.SqlCategory(
sort_order='desc', sort_by='atime', 'History', sort_order='desc', sort_by='atime',
select='url, title, {}'.format(select_time), select='url, title, {}'.format(select_time), where='not redirect')
where='not redirect') model.add_category(hist_cat)
return model return model

View File

@ -158,8 +158,6 @@ PERFECT_FILES = [
'completion/models/base.py'), 'completion/models/base.py'),
('tests/unit/completion/test_models.py', ('tests/unit/completion/test_models.py',
'completion/models/urlmodel.py'), 'completion/models/urlmodel.py'),
('tests/unit/completion/test_sortfilter.py',
'completion/models/sortfilter.py'),
] ]

View File

@ -239,6 +239,24 @@ def host_blocker_stub(stubs):
objreg.delete('host-blocker') objreg.delete('host-blocker')
@pytest.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.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.fixture @pytest.fixture
def session_manager_stub(stubs): def session_manager_stub(stubs):
"""Fixture which provides a fake web-history object.""" """Fixture which provides a fake web-history object."""

View File

@ -193,11 +193,10 @@ def test_update_completion(txt, kind, pattern, pos_args, status_command_stub,
completer_obj.schedule_completion_update() completer_obj.schedule_completion_update()
assert completion_widget_stub.set_model.call_count == 1 assert completion_widget_stub.set_model.call_count == 1
args = completion_widget_stub.set_model.call_args[0] args = completion_widget_stub.set_model.call_args[0]
# the outer model is just for sorting; srcmodel is the completion model
if kind is None: if kind is None:
assert args[0] is None assert args[0] is None
else: else:
model = args[0].srcmodel model = args[0]
assert model.kind == kind assert model.kind == kind
assert model.pos_args == pos_args assert model.pos_args == pos_args
assert args[1] == pattern assert args[1] == pattern

View File

@ -19,128 +19,59 @@
"""Tests for CompletionModel.""" """Tests for CompletionModel."""
import sys
import pytest import pytest
import hypothesis
from unittest import mock
from hypothesis import strategies
from qutebrowser.completion.models import completionmodel, sortfilter from qutebrowser.completion.models import completionmodel
def _create_model(data, filter_cols=None): @hypothesis.given(strategies.lists(min_size=0, max_size=3,
"""Create a completion model populated with the given data. elements=strategies.integers(min_value=0, max_value=2**31)))
def test_first_last_item(counts):
data: A list of lists, where each sub-list represents a category, each """Test that first() and last() index to the first and last items."""
tuple in the sub-list represents an item, and each value in the model = completionmodel.CompletionModel()
tuple represents the item data for that column for c in counts:
filter_cols: Columns to filter, or None for default. cat = mock.Mock()
""" cat.rowCount = mock.Mock(return_value=c)
model = completionmodel.CompletionModel(columns_to_filter=filter_cols) model.add_category(cat)
for catdata in data: nonempty = [i for i, rowCount in enumerate(counts) if rowCount > 0]
model.add_list('', catdata) if not nonempty:
return model # with no items, first and last should be an invalid index
assert not model.first_item().isValid()
assert not model.last_item().isValid()
else:
first = nonempty[0]
last = nonempty[-1]
# first item of the first nonempty category
assert model.first_item().row() == 0
assert model.first_item().parent().row() == first
# last item of the last nonempty category
assert model.last_item().row() == counts[last] - 1
assert model.last_item().parent().row() == last
def _extract_model_data(model): @hypothesis.given(strategies.lists(elements=strategies.integers(),
"""Express a model's data as a list for easier comparison. min_size=0, max_size=3))
def test_count(counts):
Return: A list of lists, where each sub-list represents a category, each model = completionmodel.CompletionModel()
tuple in the sub-list represents an item, and each value in the for c in counts:
tuple represents the item data for that column cat = mock.Mock(spec=['rowCount'])
""" cat.rowCount = mock.Mock(return_value=c)
data = [] model.add_category(cat)
for i in range(0, model.rowCount()): assert model.count() == sum(counts)
cat_idx = model.index(i, 0)
row = []
for j in range(0, model.rowCount(cat_idx)):
row.append((model.data(cat_idx.child(j, 0)),
model.data(cat_idx.child(j, 1)),
model.data(cat_idx.child(j, 2))))
data.append(row)
return data
@pytest.mark.parametrize('tree, first, last', [ @hypothesis.given(strategies.text())
([[('Aa',)]], 'Aa', 'Aa'), def test_set_pattern(pat):
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
'Aa', 'Ca'),
([[], [('Ba',)]], 'Ba', 'Ba'),
([[], [], [('Ca',)]], 'Ca', 'Ca'),
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], [], []], 'Aa', 'Aa'),
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
([[], []], None, None),
])
def test_first_last_item(tree, first, last):
"""Test that first() and last() return indexes to the first and last items.
Args:
tree: Each list represents a completion category, with each string
being an item under that category.
first: text of the first item
last: text of the last item
"""
model = _create_model(tree)
assert model.data(model.first_item()) == first
assert model.data(model.last_item()) == last
@pytest.mark.parametrize('tree, expected', [
([[('Aa',)]], 1),
([[('Aa',)], [('Ba',)]], 2),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
([[], [('Ba',)]], 1),
([[], [], [('Ca',)]], 1),
([[], [], [('Ca',), ('Cb',)]], 2),
([[('Aa',)], []], 1),
([[('Aa',)], []], 1),
([[('Aa',)], [], []], 1),
([[('Aa',)], [], [('Ca',)]], 2),
])
def test_count(tree, expected):
model = _create_model(tree)
assert model.count() == expected
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[[('foo', '', ''), ('bar', '', '')]],
[[('foo', '', '')]]),
('foo', [0],
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
('foo', [0],
[[('foo', '', '')], [('bar', '', '')]],
[[('foo', '', '')], []]),
# prefer foobar as it starts with the pattern
('foo', [0],
[[('barfoo', '', ''), ('foobar', '', '')]],
[[('foobar', '', ''), ('barfoo', '', '')]]),
# however, don't rearrange categories
('foo', [0],
[[('barfoo', '', '')], [('foobar', '', '')]],
[[('barfoo', '', '')], [('foobar', '', '')]]),
('foo', [1],
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
[[('bar', 'foo', '')]]),
('foo', [0, 1],
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
('foo', [0, 1, 2],
[[('foo', '', ''), ('bar', '')]],
[[('foo', '', '')]]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern.""" """Validate the filtering and sorting results of set_pattern."""
# TODO: just test that it calls the mock on its child categories cols = [1, 2, 3]
model = _create_model(before, filter_cols) model = completionmodel.CompletionModel(columns_to_filter=cols)
model.set_pattern(pattern) cats = [mock.Mock(spec=['set_pattern'])] * 3
actual = _extract_model_data(model) for c in cats:
assert actual == after c.set_pattern = mock.Mock()
model.add_category(c)
model.set_pattern(pat)
assert all(c.set_pattern.called_with([pat, cols]) for c in cats)

View File

@ -19,13 +19,13 @@
"""Tests for the CompletionView Object.""" """Tests for the CompletionView Object."""
import unittest.mock from unittest import mock
import pytest import pytest
from PyQt5.QtGui import QStandardItem, QColor from PyQt5.QtGui import QStandardItem, QColor
from qutebrowser.completion import completionwidget from qutebrowser.completion import completionwidget
from qutebrowser.completion.models import completionmodel from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.commands import cmdexc from qutebrowser.commands import cmdexc
@ -70,19 +70,11 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
return view return view
@pytest.fixture
def simplemodel(completionview):
"""A completion model with one item."""
model = completionmodel.CompletionModel()
model.add_list('', [('foo'),])
return model
def test_set_model(completionview): def test_set_model(completionview):
"""Ensure set_model actually sets the model and expands all categories.""" """Ensure set_model actually sets the model and expands all categories."""
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
for i in range(3): for i in range(3):
model.add_list(str(i), [('foo',)]) cat = listcategory.ListCategory('', [('foo',)])
completionview.set_model(model) completionview.set_model(model)
assert completionview.model() is model assert completionview.model() is model
for i in range(model.rowCount()): for i in range(model.rowCount()):
@ -91,7 +83,7 @@ def test_set_model(completionview):
def test_set_pattern(completionview): def test_set_pattern(completionview):
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
model.set_pattern = unittest.mock.Mock() model.set_pattern = mock.Mock()
completionview.set_model(model, 'foo') completionview.set_model(model, 'foo')
model.set_pattern.assert_called_with('foo') model.set_pattern.assert_called_with('foo')
@ -158,7 +150,8 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
""" """
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
for catdata in tree: for catdata in tree:
model.add_list('', (x,) for x in catdata) cat = listcategory.ListCategory('', ((x,) for x in catdata))
model.add_category(cat)
completionview.set_model(model) completionview.set_model(model)
for entry in expected: for entry in expected:
if entry is None: if entry is None:
@ -203,7 +196,8 @@ def test_completion_show(show, rows, quick_complete, completionview,
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
for name in rows: for name in rows:
model.add_list('', [(name,)]) cat = listcategory.ListCategory('', [(name,)])
model.add_category(cat)
assert not completionview.isVisible() assert not completionview.isVisible()
completionview.set_model(model) completionview.set_model(model)
@ -217,27 +211,33 @@ def test_completion_show(show, rows, quick_complete, completionview,
assert not completionview.isVisible() assert not completionview.isVisible()
def test_completion_item_del(completionview, simplemodel): def test_completion_item_del(completionview):
"""Test that completion_item_del invokes delete_cur_item in the model.""" """Test that completion_item_del invokes delete_cur_item in the model."""
simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock() func = mock.Mock()
completionview.set_model(simplemodel) model = completionmodel.CompletionModel(delete_cur_item=func)
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
completionview.completion_item_focus('next') completionview.completion_item_focus('next')
completionview.completion_item_del() completionview.completion_item_del()
assert simplemodel.srcmodel.delete_cur_item.called assert func.called
def test_completion_item_del_no_selection(completionview, simplemodel): def test_completion_item_del_no_selection(completionview):
"""Test that completion_item_del with no selected index.""" """Test that completion_item_del with no selected index."""
simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock() func = mock.Mock()
completionview.set_model(simplemodel) model = completionmodel.CompletionModel(delete_cur_item=func)
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
with pytest.raises(cmdexc.CommandError): with pytest.raises(cmdexc.CommandError):
completionview.completion_item_del() completionview.completion_item_del()
assert not simplemodel.srcmodel.delete_cur_item.called assert not func.called
def test_completion_item_del_no_func(completionview, simplemodel): def test_completion_item_del_no_func(completionview):
"""Test completion_item_del with no delete_cur_item in the model.""" """Test completion_item_del with no delete_cur_item in the model."""
completionview.set_model(simplemodel) model = completionmodel.CompletionModel()
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
completionview.completion_item_focus('next') completionview.completion_item_focus('next')
with pytest.raises(cmdexc.CommandError): with pytest.raises(cmdexc.CommandError):
completionview.completion_item_del() completionview.completion_item_del()

View File

@ -0,0 +1,70 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 CompletionFilterModel."""
import pytest
from qutebrowser.completion.models import listcategory
def _validate(cat, expected):
"""Check that a category contains the expected items in the given order.
Args:
cat: The category to inspect.
expected: A list of tuples containing the expected items.
"""
assert cat.rowCount() == len(expected)
for row, items in enumerate(expected):
for col, item in enumerate(items):
assert cat.data(cat.index(row, col)) == item
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('foo', '', ''), ('bar', '', '')],
[('foo', '', '')]),
('foo', [0],
[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]),
# prefer foobar as it starts with the pattern
('foo', [0],
[('barfoo', '', ''), ('foobar', '', '')],
[('foobar', '', ''), ('barfoo', '', '')]),
('foo', [1],
[('foo', 'bar', ''), ('bar', 'foo', '')],
[('bar', 'foo', '')]),
('foo', [0, 1],
[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')],
[('foo', 'bar', ''), ('bar', 'foo', '')]),
('foo', [0, 1, 2],
[('foo', '', ''), ('bar', '')],
[('foo', '', '')]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
cat = listcategory.ListCategory('Foo', before)
cat.set_pattern(pattern, filter_cols)
_validate(cat, after)

View File

@ -134,27 +134,25 @@ def _mock_view_index(model, category_num, child_num, qtbot):
@pytest.fixture @pytest.fixture
def quickmarks(init_sql): def quickmarks(quickmark_manager_stub):
"""Pre-populate the quickmark database.""" """Pre-populate the quickmark-manager stub with some quickmarks."""
table = sql.SqlTable('Quickmarks', ['name', 'url'], primary_key='name') quickmark_manager_stub.marks = collections.OrderedDict([
table.insert(['aw', 'https://wiki.archlinux.org']) ('aw', 'https://wiki.archlinux.org'),
table.insert(['ddg', 'https://duckduckgo.com']) ('ddg', 'https://duckduckgo.com'),
table.insert(['wiki', 'https://wikipedia.org']) ('wiki', 'https://wikipedia.org'),
objreg.register('quickmark-manager', table) ])
yield table return quickmark_manager_stub
objreg.delete('quickmark-manager')
@pytest.fixture @pytest.fixture
def bookmarks(init_sql): def bookmarks(bookmark_manager_stub):
"""Pre-populate the bookmark database.""" """Pre-populate the bookmark-manager stub with some quickmarks."""
table = sql.SqlTable('Bookmarks', ['url', 'title'], primary_key='url') bookmark_manager_stub.marks = collections.OrderedDict([
table.insert(['https://github.com', 'GitHub']) ('https://github.com', 'GitHub'),
table.insert(['https://python.org', 'Welcome to Python.org']) ('https://python.org', 'Welcome to Python.org'),
table.insert(['http://qutebrowser.org', 'qutebrowser | qutebrowser']) ('http://qutebrowser.org', 'qutebrowser | qutebrowser'),
objreg.register('bookmark-manager', table) ])
yield table return bookmark_manager_stub
objreg.delete('bookmark-manager')
@pytest.fixture @pytest.fixture
@ -315,9 +313,9 @@ def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
# delete item (1, 0) -> (bookmarks, 'https://github.com' ) # delete item (1, 0) -> (bookmarks, 'https://github.com' )
view = _mock_view_index(model, 1, 0, qtbot) view = _mock_view_index(model, 1, 0, qtbot)
model.delete_cur_item(view) model.delete_cur_item(view)
assert 'https://github.com' not in bookmarks assert 'https://github.com' not in bookmarks.marks
assert 'https://python.org' in bookmarks assert 'https://python.org' in bookmarks.marks
assert 'http://qutebrowser.org' in bookmarks assert 'http://qutebrowser.org' in bookmarks.marks
def test_url_completion_delete_quickmark(qtmodeltester, config_stub, def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
@ -332,9 +330,9 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
# delete item (0, 1) -> (quickmarks, 'ddg' ) # delete item (0, 1) -> (quickmarks, 'ddg' )
view = _mock_view_index(model, 0, 1, qtbot) view = _mock_view_index(model, 0, 1, qtbot)
model.delete_cur_item(view) model.delete_cur_item(view)
assert 'aw' in quickmarks assert 'aw' in quickmarks.marks
assert 'ddg' not in quickmarks assert 'ddg' not in quickmarks.marks
assert 'wiki' in quickmarks assert 'wiki' in quickmarks.marks
def test_session_completion(qtmodeltester, session_manager_stub): def test_session_completion(qtmodeltester, session_manager_stub):

View File

@ -1,146 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 CompletionFilterModel."""
import pytest
from qutebrowser.completion.models import listcategory, sortfilter
# TODO: merge listcategory and sortfilter
def _create_model(data):
"""Create a completion model populated with the given data.
data: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
model = completionmodel.CompletionModel()
for catdata in data:
cat = model.add_list(itemdata)
return model
def _extract_model_data(model):
"""Express a model's data as a list for easier comparison.
Return: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
data = []
for i in range(0, model.rowCount()):
cat_idx = model.index(i, 0)
row = []
for j in range(0, model.rowCount(cat_idx)):
row.append((model.data(cat_idx.child(j, 0)),
model.data(cat_idx.child(j, 1)),
model.data(cat_idx.child(j, 2))))
data.append(row)
return data
@pytest.mark.parametrize('pattern, data, expected', [
('foo', 'barfoobar', True),
('foo bar', 'barfoobar', True),
('foo bar', 'barfoobar', True),
('foo bar', 'barfoobazbar', True),
('foo bar', 'barfoobazbar', True),
('foo', 'barFOObar', True),
('Foo', 'barfOObar', True),
('ab', 'aonebtwo', False),
('33', 'l33t', True),
('x', 'blah', False),
('4', 'blah', False),
])
def test_filter_accepts_row(pattern, data, expected):
source_model = completionmodel.CompletionModel()
cat = source_model.new_category('test')
source_model.new_item(cat, data)
filter_model = sortfilter.CompletionFilterModel(source_model)
filter_model.set_pattern(pattern)
assert filter_model.rowCount() == 1 # "test" category
idx = filter_model.index(0, 0)
assert idx.isValid()
row_count = filter_model.rowCount(idx)
assert row_count == (1 if expected else 0)
def test_set_source_model():
"""Ensure setSourceModel sets source_model and clears the pattern."""
model1 = base.CompletionModel()
model2 = base.CompletionModel()
filter_model = sortfilter.CompletionFilterModel(model1)
filter_model.set_pattern('foo')
# sourceModel() is cached as srcmodel, so make sure both match
assert filter_model.srcmodel is model1
assert filter_model.sourceModel() is model1
assert filter_model.pattern == 'foo'
filter_model.setSourceModel(model2)
assert filter_model.srcmodel is model2
assert filter_model.sourceModel() is model2
assert not filter_model.pattern
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[[('foo', '', ''), ('bar', '', '')]],
[[('foo', '', '')]]),
('foo', [0],
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
('foo', [0],
[[('foo', '', '')], [('bar', '', '')]],
[[('foo', '', '')], []]),
# prefer foobar as it starts with the pattern
('foo', [0],
[[('barfoo', '', ''), ('foobar', '', '')]],
[[('foobar', '', ''), ('barfoo', '', '')]]),
# however, don't rearrange categories
('foo', [0],
[[('barfoo', '', '')], [('foobar', '', '')]],
[[('barfoo', '', '')], [('foobar', '', '')]]),
('foo', [1],
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
[[('bar', 'foo', '')]]),
('foo', [0, 1],
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
('foo', [0, 1, 2],
[[('foo', '', ''), ('bar', '')]],
[[('foo', '', '')]]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
model = _create_model(before)
model.columns_to_filter = filter_cols
filter_model = sortfilter.CompletionFilterModel(model)
filter_model.set_pattern(pattern)
actual = _extract_model_data(filter_model)
assert actual == after

View File

@ -0,0 +1,156 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 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/>.
"""Test SQL-based completions."""
import pytest
from qutebrowser.misc import sql
from qutebrowser.completion.models import sqlcategory
pytestmark = pytest.mark.usefixtures('init_sql')
def _validate(cat, expected):
"""Check that a category contains the expected items in the given order.
Args:
cat: The category to inspect.
expected: A list of tuples containing the expected items.
"""
assert cat.rowCount() == len(expected)
for row, items in enumerate(expected):
for col, item in enumerate(items):
assert cat.data(cat.index(row, col)) == item
@pytest.mark.parametrize('sort_by, sort_order, data, expected', [
(None, 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')]),
('a', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('a', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('c', 'asc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('A', 'F', 0), ('C', 'A', 1), ('B', 'C', 2)]),
('c', 'desc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('B', 'C', 2), ('C', 'A', 1), ('A', 'F', 0)]),
])
def test_sorting(sort_by, sort_order, data, expected):
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in data:
table.insert(row)
cat = sqlcategory.SqlCategory('Foo', sort_by=sort_by,
sort_order=sort_order)
_validate(cat, expected)
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('foo', '', ''), ('bar', '', ''), ('aafobbb', '', '')],
[('foo',)]),
('foo', [0],
[('baz', 'bar', 'foo'), ('foo', '', ''), ('bar', 'foo', '')],
[('foo', '', '')]),
('foo', [0],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]),
('foo', [1],
[('foo', 'bar', ''), ('bar', 'foo', '')],
[('bar', 'foo', '')]),
('foo', [0, 1],
[('foo', 'bar', ''), ('bar', 'foo', ''), ('biz', 'baz', 'foo')],
[('foo', 'bar', ''), ('bar', 'foo', '')]),
('foo', [0, 1, 2],
[('foo', '', ''), ('bar', '', ''), ('baz', 'bar', 'foo')],
[('foo', '', ''), ('baz', 'bar', 'foo')]),
('foo bar', [0],
[('foo', '', ''), ('bar foo', '', ''), ('xfooyybarz', '', '')],
[('xfooyybarz', '', '')]),
('foo%bar', [0],
[('foo%bar', '', ''), ('foo bar', '', ''), ('foobar', '', '')],
[('foo%bar', '', '')]),
('_', [0],
[('a_b', '', ''), ('__a', '', ''), ('abc', '', '')],
[('a_b', '', ''), ('__a', '', '')]),
('%', [0, 1],
[('\\foo', '\\bar', '')],
[]),
("can't", [0],
[("can't touch this", '', ''), ('a', '', '')],
[("can't touch this", '', '')]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in before:
table.insert(row)
cat = sqlcategory.SqlCategory('Foo')
cat.set_pattern(pattern, filter_cols)
_validate(cat, after)
def test_select():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', 'baz'])
cat = sqlcategory.SqlCategory('Foo', select='b, c, a')
_validate(cat, [('bar', 'baz', 'foo')])
def test_where():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', False])
table.insert(['baz', 'biz', True])
cat = sqlcategory.SqlCategory('Foo', where='not c')
_validate(cat, [('foo', 'bar', False)])
def test_entry():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
assert hasattr(table.Entry, 'a')
assert hasattr(table.Entry, 'b')
assert hasattr(table.Entry, 'c')

View File

@ -1,230 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 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 the base sql completion model."""
import pytest
from qutebrowser.misc import sql
from qutebrowser.completion.models import sqlmodel
pytestmark = pytest.mark.usefixtures('init_sql')
def _check_model(model, expected):
"""Check that a model contains the expected items in the given order.
Args:
expected: A list of form
[
(cat, [(name, desc, misc), (name, desc, misc), ...]),
(cat, [(name, desc, misc), (name, desc, misc), ...]),
...
]
"""
assert model.rowCount() == len(expected)
for i, (expected_title, expected_items) in enumerate(expected):
catidx = model.index(i, 0)
assert model.data(catidx) == expected_title
assert model.rowCount(catidx) == len(expected_items)
for j, (name, desc, misc) in enumerate(expected_items):
assert model.data(model.index(j, 0, catidx)) == name
assert model.data(model.index(j, 1, catidx)) == desc
assert model.data(model.index(j, 2, catidx)) == misc
@pytest.mark.parametrize('rowcounts, expected', [
([0], 0),
([1], 1),
([2], 2),
([0, 0], 0),
([0, 0, 0], 0),
([1, 1], 2),
([3, 2, 1], 6),
([0, 2, 0], 2),
])
def test_count(rowcounts, expected):
model = sqlmodel.SqlCompletionModel()
for i, rowcount in enumerate(rowcounts):
name = 'Foo' + str(i)
table = sql.SqlTable(name, ['a'], primary_key='a')
for rownum in range(rowcount):
table.insert([rownum])
model.new_category(name)
assert model.count() == expected
@pytest.mark.parametrize('sort_by, sort_order, data, expected', [
(None, 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')]),
('a', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('a', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('c', 'asc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('A', 'F', 0), ('C', 'A', 1), ('B', 'C', 2)]),
('c', 'desc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('B', 'C', 2), ('C', 'A', 1), ('A', 'F', 0)]),
])
def test_sorting(sort_by, sort_order, data, expected):
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in data:
table.insert(row)
model = sqlmodel.SqlCompletionModel()
model.new_category('Foo', sort_by=sort_by, sort_order=sort_order)
_check_model(model, [('Foo', expected)])
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('A', [('foo', '', ''), ('bar', '', ''), ('aafobbb', '', '')])],
[('A', [('foo', '', '')])]),
('foo', [0],
[('A', [('baz', 'bar', 'foo'), ('foo', '', ''), ('bar', 'foo', '')])],
[('A', [('foo', '', '')])]),
('foo', [0],
[('A', [('foo', '', ''), ('bar', '', '')]),
('B', [('foo', '', ''), ('bar', '', '')])],
[('A', [('foo', '', '')]), ('B', [('foo', '', '')])]),
('foo', [0],
[('A', [('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')])],
[('A', [('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')])]),
('foo', [0],
[('A', [('foo', '', '')]), ('B', [('bar', '', '')])],
[('A', [('foo', '', '')]), ('B', [])]),
('foo', [1],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])],
[('A', [('bar', 'foo', '')])]),
('foo', [0, 1],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])]),
('foo', [0, 1, 2],
[('A', [('foo', '', ''), ('bar', '', '')])],
[('A', [('foo', '', '')])]),
('foo bar', [0],
[('A', [('foo', '', ''), ('bar foo', '', ''), ('xfooyybarz', '', '')])],
[('A', [('xfooyybarz', '', '')])]),
('foo%bar', [0],
[('A', [('foo%bar', '', ''), ('foo bar', '', ''), ('foobar', '', '')])],
[('A', [('foo%bar', '', '')])]),
('_', [0],
[('A', [('a_b', '', ''), ('__a', '', ''), ('abc', '', '')])],
[('A', [('a_b', '', ''), ('__a', '', '')])]),
('%', [0, 1],
[('A', [('\\foo', '\\bar', '')])],
[('A', [])]),
("can't", [0],
[('A', [("can't touch this", '', ''), ('a', '', '')])],
[('A', [("can't touch this", '', '')])]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
model = sqlmodel.SqlCompletionModel(columns_to_filter=filter_cols)
for name, rows in before:
table = sql.SqlTable(name, ['a', 'b', 'c'], primary_key='a')
for row in rows:
table.insert(row)
model.new_category(name)
model.set_pattern(pattern)
_check_model(model, after)
@pytest.mark.parametrize('data, first, last', [
([('A', ['Aa'])], 'Aa', 'Aa'),
([('A', ['Aa', 'Ba'])], 'Aa', 'Ba'),
([('A', ['Aa', 'Ab', 'Ac']), ('B', ['Ba', 'Bb']),
('C', ['Ca'])], 'Aa', 'Ca'),
([('A', []), ('B', ['Ba'])], 'Ba', 'Ba'),
([('A', []), ('B', []), ('C', ['Ca'])], 'Ca', 'Ca'),
([('A', []), ('B', []), ('C', ['Ca', 'Cb'])], 'Ca', 'Cb'),
([('A', ['Aa']), ('B', [])], 'Aa', 'Aa'),
([('A', ['Aa']), ('B', []), ('C', [])], 'Aa', 'Aa'),
([('A', ['Aa']), ('B', []), ('C', ['Ca'])], 'Aa', 'Ca'),
([('A', []), ('B', [])], None, None),
])
def test_first_last_item(data, first, last):
"""Test that first() and last() return indexes to the first and last items.
Args:
data: Input to _make_model
first: text of the first item
last: text of the last item
"""
model = sqlmodel.SqlCompletionModel()
for name, rows in data:
table = sql.SqlTable(name, ['a'], primary_key='a')
for row in rows:
table.insert([row])
model.new_category(name)
assert model.data(model.first_item()) == first
assert model.data(model.last_item()) == last
def test_select():
table = sql.SqlTable('test_select', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', 'baz'])
model = sqlmodel.SqlCompletionModel()
model.new_category('test_select', select='b, c, a')
_check_model(model, [('test_select', [('bar', 'baz', 'foo')])])
def test_where():
table = sql.SqlTable('test_where', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', False])
table.insert(['baz', 'biz', True])
model = sqlmodel.SqlCompletionModel()
model.new_category('test_where', where='not c')
_check_model(model, [('test_where', [('foo', 'bar', False)])])
def test_entry():
table = sql.SqlTable('test_entry', ['a', 'b', 'c'], primary_key='a')
assert hasattr(table.Entry, 'a')
assert hasattr(table.Entry, 'b')
assert hasattr(table.Entry, 'c')