diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py index ffa44b82e..56905bbec 100644 --- a/qutebrowser/completion/models/sortfilter.py +++ b/qutebrowser/completion/models/sortfilter.py @@ -135,8 +135,8 @@ class CompletionFilterModel(QSortFilterProxyModel): for col in self.srcmodel.columns_to_filter: idx = self.srcmodel.index(row, col, parent) - if not idx.isValid(): - # No entries in parent model + 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: diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index c8db7e03e..4b1b9378a 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -150,6 +150,8 @@ PERFECT_FILES = [ ('tests/unit/completion/test_models.py', 'qutebrowser/completion/models/base.py'), + ('tests/unit/completion/test_sortfilter.py', + 'qutebrowser/completion/models/sortfilter.py'), ] diff --git a/tests/unit/completion/test_sortfilter.py b/tests/unit/completion/test_sortfilter.py index 86a132e1d..c63d08ed5 100644 --- a/tests/unit/completion/test_sortfilter.py +++ b/tests/unit/completion/test_sortfilter.py @@ -21,9 +21,44 @@ import pytest +from PyQt5.QtCore import Qt + from qutebrowser.completion.models import base, 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 = base.BaseCompletionModel() + for catdata in data: + cat = model.new_category('') + for itemdata in catdata: + model.new_item(cat, *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', 'barFOObar', True), @@ -46,3 +81,145 @@ def test_filter_accepts_row(pattern, data, expected): row_count = filter_model.rowCount(idx) assert row_count == (1 if expected else 0) + + +@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) + filter_model = sortfilter.CompletionFilterModel(model) + assert filter_model.data(filter_model.first_item()) == first + assert filter_model.data(filter_model.last_item()) == last + + +def test_set_source_model(): + """Ensure setSourceModel sets source_model and clears the pattern.""" + model1 = base.BaseCompletionModel() + model2 = base.BaseCompletionModel() + 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('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) + filter_model = sortfilter.CompletionFilterModel(model) + assert filter_model.count() == expected + + +@pytest.mark.parametrize('pattern, dumb_sort, filter_cols, before, after', [ + ('foo', None, [0], + [[('foo', '', ''), ('bar', '', '')]], + [[('foo', '', '')]]), + + ('foo', None, [0], + [[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]], + [[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]), + + ('foo', None, [0], + [[('foo', '', '')], [('bar', '', '')]], + [[('foo', '', '')], []]), + + # prefer foobar as it starts with the pattern + ('foo', None, [0], + [[('barfoo', '', ''), ('foobar', '', '')]], + [[('foobar', '', ''), ('barfoo', '', '')]]), + + # however, don't rearrange categories + ('foo', None, [0], + [[('barfoo', '', '')], [('foobar', '', '')]], + [[('barfoo', '', '')], [('foobar', '', '')]]), + + ('foo', None, [1], + [[('foo', 'bar', ''), ('bar', 'foo', '')]], + [[('bar', 'foo', '')]]), + + ('foo', None, [0, 1], + [[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]], + [[('foo', 'bar', ''), ('bar', 'foo', '')]]), + + ('foo', None, [0, 1, 2], + [[('foo', '', ''), ('bar', '')]], + [[('foo', '', '')]]), + + # the fourth column is the sort role, which overrides data-based sorting + ('', None, [0], + [[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]], + [[('one', '', ''), ('two', '', ''), ('three', '', '')]]), + + ('', Qt.AscendingOrder, [0], + [[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]], + [[('one', '', ''), ('two', '', ''), ('three', '', '')]]), + + ('', Qt.DescendingOrder, [0], + [[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]], + [[('three', '', ''), ('two', '', ''), ('one', '', '')]]), +]) +def test_set_pattern(pattern, dumb_sort, filter_cols, before, after): + """Validate the filtering and sorting results of set_pattern.""" + model = _create_model(before) + model.DUMB_SORT = dumb_sort + 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 + + +def test_sort(): + """Ensure that a sort argument passed to sort overrides DUMB_SORT. + + While test_set_pattern above covers most of the sorting logic, this + particular case is easier to test separately. + """ + model = _create_model([[('B', '', '', 1), + ('C', '', '', 2), + ('A', '', '', 0)]]) + filter_model = sortfilter.CompletionFilterModel(model) + + filter_model.sort(0, Qt.AscendingOrder) + actual = _extract_model_data(filter_model) + assert actual == [[('A', '', ''), ('B', '', ''), ('C', '', '')]] + + filter_model.sort(0, Qt.DescendingOrder) + actual = _extract_model_data(filter_model) + assert actual == [[('C', '', ''), ('B', '', ''), ('A', '', '')]]