From f95dff4d9e39abc3c29c514a3783ce632f96c369 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Fri, 3 Mar 2017 08:31:28 -0500 Subject: [PATCH] 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. --- qutebrowser/browser/commands.py | 9 +- qutebrowser/completion/completionwidget.py | 1 + .../completion/models/completionmodel.py | 32 +-- qutebrowser/completion/models/configmodel.py | 13 +- qutebrowser/completion/models/listcategory.py | 102 ++++++-- qutebrowser/completion/models/miscmodels.py | 21 +- qutebrowser/completion/models/sortfilter.py | 133 ---------- qutebrowser/completion/models/sqlcategory.py | 44 ++-- qutebrowser/completion/models/urlmodel.py | 22 +- scripts/dev/check_coverage.py | 2 - tests/helpers/fixtures.py | 18 ++ tests/unit/completion/test_completer.py | 3 +- tests/unit/completion/test_completionmodel.py | 163 ++++--------- .../unit/completion/test_completionwidget.py | 48 ++-- tests/unit/completion/test_listcategory.py | 70 ++++++ tests/unit/completion/test_models.py | 46 ++-- tests/unit/completion/test_sortfilter.py | 146 ----------- tests/unit/completion/test_sqlcateogry.py | 156 ++++++++++++ tests/unit/completion/test_sqlmodel.py | 230 ------------------ 19 files changed, 484 insertions(+), 775 deletions(-) delete mode 100644 qutebrowser/completion/models/sortfilter.py create mode 100644 tests/unit/completion/test_listcategory.py delete mode 100644 tests/unit/completion/test_sortfilter.py create mode 100644 tests/unit/completion/test_sqlcateogry.py delete mode 100644 tests/unit/completion/test_sqlmodel.py diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8914d4ac2..3ec4c63c0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -41,7 +41,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils, typing) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess -from qutebrowser.completion.models import sortfilter, urlmodel, miscmodels +from qutebrowser.completion.models import urlmodel, miscmodels class CommandDispatcher: @@ -1024,10 +1024,9 @@ class CommandDispatcher: int(part) except ValueError: model = miscmodels.buffer() - sf = sortfilter.CompletionFilterModel(source=model) - sf.set_pattern(index) - if sf.count() > 0: - index = sf.data(sf.first_item()) + model.set_pattern(index) + if model.count() > 0: + index = model.data(model.first_item()) index_parts = index.split('/', 1) else: raise cmdexc.CommandError( diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index f6a980612..5e207d68d 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -275,6 +275,7 @@ class CompletionView(QTreeView): self.hide() return + model.setParent(self) old_model = self.model() if model is not old_model: sel_model = self.selectionModel() diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py index b8dcc4601..84dd74b8f 100644 --- a/qutebrowser/completion/models/completionmodel.py +++ b/qutebrowser/completion/models/completionmodel.py @@ -24,7 +24,6 @@ import re from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel from qutebrowser.utils import log, qtutils -from qutebrowser.completion.models import sortfilter, listcategory, sqlcategory class CompletionModel(QAbstractItemModel): @@ -65,33 +64,8 @@ class CompletionModel(QAbstractItemModel): return self._categories[index.row()] return None - def add_list(self, name, items): - """Add a list of items as a completion category. - - 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) + def add_category(self, cat): + """Add a completion category to the model.""" self._categories.append(cat) def data(self, index, role=Qt.DisplayRole): @@ -210,7 +184,7 @@ class CompletionModel(QAbstractItemModel): # TODO: should pattern be saved in the view layer instead? self.pattern = pattern for cat in self._categories: - cat.set_pattern(pattern) + cat.set_pattern(pattern, self.columns_to_filter) def first_item(self): """Return the index of the first child (non-category) in the model.""" diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 3cb46d42d..663a0b7f7 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -20,7 +20,7 @@ """Functions that return config-related completion models.""" from qutebrowser.config import configdata, configexc -from qutebrowser.completion.models import completionmodel +from qutebrowser.completion.models import completionmodel, listcategory from qutebrowser.utils import objreg @@ -29,7 +29,7 @@ def section(): model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip()) for name in configdata.DATA) - model.add_list("Sections", sections) + model.add_category(listcategory.ListCategory("Sections", sections)) return model @@ -57,7 +57,7 @@ def option(sectname): config = objreg.get('config') val = config.get(sectname, name, raw=True) options.append((name, desc, val)) - model.add_list(sectname, options) + model.add_category(listcategory.ListCategory(sectname, options)) return model @@ -88,8 +88,9 @@ def value(sectname, optname): # Different type for each value (KeyValue) vals = configdata.DATA[sectname][optname].typ.complete() - model.add_list("Current/Default", [(current, "Current value"), - (default, "Default value")]) + cur_cat = listcategory.ListCategory("Current/Default", + [(current, "Current value"), (default, "Default value")]) + model.add_category(cur_cat) if vals is not None: - model.add_list("Completions", vals) + model.add_category(listcategory.ListCategory("Completions", vals)) return model diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py index 7b222c3c0..6251a7cac 100644 --- a/qutebrowser/completion/models/listcategory.py +++ b/qutebrowser/completion/models/listcategory.py @@ -23,44 +23,100 @@ Module attributes: 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 qutebrowser.utils import qtutils, debug, log -class ListCategory(QStandardItemModel): + +class ListCategory(QSortFilterProxyModel): """Expose a list of items as a category for the CompletionModel.""" def __init__(self, name, items, parent=None): super().__init__(parent) self.name = name - # self.setColumnCount(3) TODO needed? - # TODO: batch insert? - # TODO: can I just insert a tuple instead of a list? + self.srcmodel = QStandardItemModel(parent=self) + self.pattern = '' + self.pattern_re = None 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): - """Return the item flags for index. - - Override QAbstractItemModel::flags. + def set_pattern(self, val, columns_to_filter): + """Setter for pattern. 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: - 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(): - return + if not self.pattern: + return True - if index.parent().isValid(): - # item - return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | - Qt.ItemNeverHasChildren) + 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: - # category - return Qt.NoItemFlags - - def set_pattern(self, pattern): - pass + return left < right diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 64a467a6b..368e1e6c6 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -22,14 +22,14 @@ from qutebrowser.config import config, configdata from qutebrowser.utils import objreg, log, qtutils from qutebrowser.commands import cmdutils -from qutebrowser.completion.models import completionmodel +from qutebrowser.completion.models import completionmodel, listcategory def command(): """A CompletionModel filled with non-hidden commands and descriptions.""" model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False) - model.add_list("Commands", cmdlist) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) return model @@ -53,22 +53,24 @@ def helptopic(): name = '{}->{}'.format(sectname, optname) settings.append((name, desc)) - model.add_list("Commands", cmdlist) - model.add_list("Settings", settings) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) + model.add_category(listcategory.ListCategory("Settings", settings)) return model def quickmark(): """A CompletionModel filled with all quickmarks.""" 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 def bookmark(): """A CompletionModel filled with all bookmarks.""" 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 @@ -79,7 +81,7 @@ def session(): manager = objreg.get('session-manager') sessions = ((name,) for name in manager.list_sessions() if not name.startswith('_')) - model.add_list("Sessions", sessions) + model.add_category(listcategory.ListCategory("Sessions", sessions)) except OSError: log.completion.exception("Failed to list sessions!") return model @@ -122,7 +124,8 @@ def buffer(): tabs.append(("{}/{}".format(win_id, idx + 1), tab.url().toDisplayString(), 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 @@ -135,7 +138,7 @@ def bind(_key): # TODO: offer a 'Current binding' completion based on the key. model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True) - model.add_list("Commands", cmdlist) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) return model diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py deleted file mode 100644 index fb79d002c..000000000 --- a/qutebrowser/completion/models/sortfilter.py +++ /dev/null @@ -1,133 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""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 diff --git a/qutebrowser/completion/models/sqlcategory.py b/qutebrowser/completion/models/sqlcategory.py index 12e0534c1..4f767abbf 100644 --- a/qutebrowser/completion/models/sqlcategory.py +++ b/qutebrowser/completion/models/sqlcategory.py @@ -29,36 +29,48 @@ from qutebrowser.misc import sql class SqlCategory(QSqlQueryModel): """Wraps a SqlQuery for use as a completion category.""" - def __init__(self, name, *, sort_by, sort_order, select, where, - columns_to_filter, parent=None): + def __init__(self, name, *, sort_by=None, sort_order=None, select='*', + 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) 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)) - self._fields = [query.record().fieldName(i) for i in columns_to_filter] + def set_pattern(self, pattern, 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 '\' # we need to tell sql to treat '\' as an escape character querystr += ' or '.join("{} like ? escape '\\'".format(f) - for f in self._fields) + for f in fields) querystr += ')' - if where: - querystr += ' and ' + where + if self._where: + querystr += ' and ' + self._where - if sort_by: - assert sort_order == 'asc' or sort_order == 'desc' - querystr += ' order by {} {}'.format(sort_by, sort_order) + if self._sort_by: + assert self._sort_order in ['asc', 'desc'] + 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 pattern = pattern.replace('%', '\\%') pattern = pattern.replace('_', '\\_') # treat spaces as wildcards to match any of the typed words pattern = re.sub(r' +', '%', 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) diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 5b738c503..ea3f3f610 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -19,7 +19,8 @@ """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.utils import qtutils, log, objreg @@ -46,8 +47,7 @@ def _delete_url(completion): log.completion.debug('Deleting bookmark {}'.format(urlstr)) bookmark_manager = objreg.get('bookmark-manager') bookmark_manager.delete(urlstr) - else: - assert catname == 'Quickmarks', 'Unknown category {}'.format(catname) + elif catname == 'Quickmarks': quickmark_manager = objreg.get('quickmark-manager') sibling = index.sibling(index.row(), _TEXTCOL) qtutils.ensure_valid(sibling) @@ -66,15 +66,17 @@ def url(): columns_to_filter=[_URLCOL, _TEXTCOL], delete_cur_item=_delete_url) - quickmarks = objreg.get('quickmark-manager').marks.items() - model.add_list('Quickmarks', ((url, name) for (name, url) in quickmarks)) + quickmarks = ((url, name) for (name, url) + 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') select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt) - model.add_sqltable('History', - sort_order='desc', sort_by='atime', - select='url, title, {}'.format(select_time), - where='not redirect') + hist_cat = sqlcategory.SqlCategory( + 'History', sort_order='desc', sort_by='atime', + select='url, title, {}'.format(select_time), where='not redirect') + model.add_category(hist_cat) return model diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 3b1611a1c..47e304a73 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -158,8 +158,6 @@ PERFECT_FILES = [ 'completion/models/base.py'), ('tests/unit/completion/test_models.py', 'completion/models/urlmodel.py'), - ('tests/unit/completion/test_sortfilter.py', - 'completion/models/sortfilter.py'), ] diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 0b69ad89f..87da45ce6 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -239,6 +239,24 @@ def host_blocker_stub(stubs): 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 def session_manager_stub(stubs): """Fixture which provides a fake web-history object.""" diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index dce105c27..c407aeab8 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -193,11 +193,10 @@ def test_update_completion(txt, kind, pattern, pos_args, status_command_stub, completer_obj.schedule_completion_update() assert completion_widget_stub.set_model.call_count == 1 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: assert args[0] is None else: - model = args[0].srcmodel + model = args[0] assert model.kind == kind assert model.pos_args == pos_args assert args[1] == pattern diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index 4c1241d12..7a61c0b84 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -19,128 +19,59 @@ """Tests for CompletionModel.""" +import sys 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): - """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 - filter_cols: Columns to filter, or None for default. - """ - model = completionmodel.CompletionModel(columns_to_filter=filter_cols) - for catdata in data: - model.add_list('', catdata) - return model +@hypothesis.given(strategies.lists(min_size=0, max_size=3, + elements=strategies.integers(min_value=0, max_value=2**31))) +def test_first_last_item(counts): + """Test that first() and last() index to the first and last items.""" + model = completionmodel.CompletionModel() + for c in counts: + cat = mock.Mock() + cat.rowCount = mock.Mock(return_value=c) + model.add_category(cat) + nonempty = [i for i, rowCount in enumerate(counts) if rowCount > 0] + if not nonempty: + # 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): - """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 +@hypothesis.given(strategies.lists(elements=strategies.integers(), + min_size=0, max_size=3)) +def test_count(counts): + model = completionmodel.CompletionModel() + for c in counts: + cat = mock.Mock(spec=['rowCount']) + cat.rowCount = mock.Mock(return_value=c) + model.add_category(cat) + assert model.count() == sum(counts) -@pytest.mark.parametrize('tree, first, last', [ - ([[('Aa',)]], 'Aa', 'Aa'), - ([[('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): +@hypothesis.given(strategies.text()) +def test_set_pattern(pat): """Validate the filtering and sorting results of set_pattern.""" - # TODO: just test that it calls the mock on its child categories - model = _create_model(before, filter_cols) - model.set_pattern(pattern) - actual = _extract_model_data(model) - assert actual == after + cols = [1, 2, 3] + model = completionmodel.CompletionModel(columns_to_filter=cols) + cats = [mock.Mock(spec=['set_pattern'])] * 3 + for c in cats: + 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) diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 3db4dca5a..43722af42 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -19,13 +19,13 @@ """Tests for the CompletionView Object.""" -import unittest.mock +from unittest import mock import pytest from PyQt5.QtGui import QStandardItem, QColor from qutebrowser.completion import completionwidget -from qutebrowser.completion.models import completionmodel +from qutebrowser.completion.models import completionmodel, listcategory from qutebrowser.commands import cmdexc @@ -70,19 +70,11 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry, 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): """Ensure set_model actually sets the model and expands all categories.""" model = completionmodel.CompletionModel() for i in range(3): - model.add_list(str(i), [('foo',)]) + cat = listcategory.ListCategory('', [('foo',)]) completionview.set_model(model) assert completionview.model() is model for i in range(model.rowCount()): @@ -91,7 +83,7 @@ def test_set_model(completionview): def test_set_pattern(completionview): model = completionmodel.CompletionModel() - model.set_pattern = unittest.mock.Mock() + model.set_pattern = mock.Mock() completionview.set_model(model, '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() 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) for entry in expected: if entry is None: @@ -203,7 +196,8 @@ def test_completion_show(show, rows, quick_complete, completionview, model = completionmodel.CompletionModel() for name in rows: - model.add_list('', [(name,)]) + cat = listcategory.ListCategory('', [(name,)]) + model.add_category(cat) assert not completionview.isVisible() completionview.set_model(model) @@ -217,27 +211,33 @@ def test_completion_show(show, rows, quick_complete, completionview, 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.""" - simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock() - completionview.set_model(simplemodel) + func = mock.Mock() + 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_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.""" - simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock() - completionview.set_model(simplemodel) + func = mock.Mock() + model = completionmodel.CompletionModel(delete_cur_item=func) + model.add_category(listcategory.ListCategory('', [('foo',)])) + completionview.set_model(model) with pytest.raises(cmdexc.CommandError): 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.""" - completionview.set_model(simplemodel) + model = completionmodel.CompletionModel() + model.add_category(listcategory.ListCategory('', [('foo',)])) + completionview.set_model(model) completionview.completion_item_focus('next') with pytest.raises(cmdexc.CommandError): completionview.completion_item_del() diff --git a/tests/unit/completion/test_listcategory.py b/tests/unit/completion/test_listcategory.py new file mode 100644 index 000000000..0b4426bdb --- /dev/null +++ b/tests/unit/completion/test_listcategory.py @@ -0,0 +1,70 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for 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) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 6e7e60958..48b3646bf 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -134,27 +134,25 @@ def _mock_view_index(model, category_num, child_num, qtbot): @pytest.fixture -def quickmarks(init_sql): - """Pre-populate the quickmark database.""" - table = sql.SqlTable('Quickmarks', ['name', 'url'], primary_key='name') - table.insert(['aw', 'https://wiki.archlinux.org']) - table.insert(['ddg', 'https://duckduckgo.com']) - table.insert(['wiki', 'https://wikipedia.org']) - objreg.register('quickmark-manager', table) - yield table - objreg.delete('quickmark-manager') +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(init_sql): - """Pre-populate the bookmark database.""" - table = sql.SqlTable('Bookmarks', ['url', 'title'], primary_key='url') - table.insert(['https://github.com', 'GitHub']) - table.insert(['https://python.org', 'Welcome to Python.org']) - table.insert(['http://qutebrowser.org', 'qutebrowser | qutebrowser']) - objreg.register('bookmark-manager', table) - yield table - objreg.delete('bookmark-manager') +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 @@ -315,9 +313,9 @@ def test_url_completion_delete_bookmark(qtmodeltester, config_stub, # 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 - assert 'https://python.org' in bookmarks - assert 'http://qutebrowser.org' in bookmarks + 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(qtmodeltester, config_stub, @@ -332,9 +330,9 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub, # delete item (0, 1) -> (quickmarks, 'ddg' ) view = _mock_view_index(model, 0, 1, qtbot) model.delete_cur_item(view) - assert 'aw' in quickmarks - assert 'ddg' not in quickmarks - assert 'wiki' in quickmarks + assert 'aw' in quickmarks.marks + assert 'ddg' not in quickmarks.marks + assert 'wiki' in quickmarks.marks def test_session_completion(qtmodeltester, session_manager_stub): diff --git a/tests/unit/completion/test_sortfilter.py b/tests/unit/completion/test_sortfilter.py deleted file mode 100644 index 7dadaa9dd..000000000 --- a/tests/unit/completion/test_sortfilter.py +++ /dev/null @@ -1,146 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Tests for 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 diff --git a/tests/unit/completion/test_sqlcateogry.py b/tests/unit/completion/test_sqlcateogry.py new file mode 100644 index 000000000..1d25c5954 --- /dev/null +++ b/tests/unit/completion/test_sqlcateogry.py @@ -0,0 +1,156 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016-2017 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 . + +"""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') diff --git a/tests/unit/completion/test_sqlmodel.py b/tests/unit/completion/test_sqlmodel.py deleted file mode 100644 index 8b9727dfd..000000000 --- a/tests/unit/completion/test_sqlmodel.py +++ /dev/null @@ -1,230 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Tests for the 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')