diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 55a16801a..ba8486ff4 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -187,7 +187,7 @@ def qute_bookmarks(_url): def history_data(start_time): - """Return history data + """Return history data. Arguments: start_time -- select history starting from this timestamp. diff --git a/qutebrowser/completion/models/sqlcategory.py b/qutebrowser/completion/models/sqlcategory.py index 693aaa841..9f78163ca 100644 --- a/qutebrowser/completion/models/sqlcategory.py +++ b/qutebrowser/completion/models/sqlcategory.py @@ -30,7 +30,7 @@ class SqlCategory(QSqlQueryModel): """Wraps a SqlQuery for use as a completion category.""" def __init__(self, name, *, sort_by=None, sort_order=None, select='*', - where=None, parent=None): + where=None, group_by=None, parent=None): """Create a new completion category backed by a sql table. Args: @@ -46,6 +46,7 @@ class SqlCategory(QSqlQueryModel): self._sort_order = sort_order self._select = select self._where = where + self._group_by = group_by self.set_pattern('', [0]) def set_pattern(self, pattern, columns_to_filter): @@ -67,6 +68,9 @@ class SqlCategory(QSqlQueryModel): if self._where: querystr += ' and ' + self._where + if self._group_by: + querystr += ' group by {}'.format(self._group_by) + if self._sort_by: assert self._sort_order in ['asc', 'desc'] querystr += ' order by {} {}'.format(self._sort_by, diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index ea3f3f610..9e601cd39 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -74,9 +74,9 @@ def url(): model.add_category(listcategory.ListCategory('Bookmarks', bookmarks)) timefmt = config.get('completion', 'timestamp-format') - select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt) + select_time = "strftime('{}', max(atime), 'unixepoch')".format(timefmt) hist_cat = sqlcategory.SqlCategory( - 'History', sort_order='desc', sort_by='atime', + 'History', sort_order='desc', sort_by='atime', group_by='url', select='url, title, {}'.format(select_time), where='not redirect') model.add_category(hist_cat) return model diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 24087ed18..1a3dfd8ea 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -259,7 +259,7 @@ def bookmark_manager_stub(stubs): @pytest.fixture def session_manager_stub(stubs): - """Fixture which provides a fake web-history object.""" + """Fixture which provides a fake session-manager object.""" stub = stubs.SessionManagerStub() objreg.register('session-manager', stub) yield stub diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 8b2a235d3..05341103c 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -518,24 +518,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub): self.delete(key) -class WebHistoryStub(QObject): - - """Stub for the web-history object.""" - - add_completion_item = pyqtSignal(history.Entry) - cleared = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - self.history_dict = collections.OrderedDict() - - def __iter__(self): - return iter(self.history_dict.values()) - - def __len__(self): - return len(self.history_dict) - - class HostBlockerStub: """Stub for the host-blocker object.""" diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index e071f1584..cea9d0ccd 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -43,18 +43,18 @@ def _check_completions(model, expected): ... } """ + actual = {} assert model.rowCount() == len(expected) for i in range(0, model.rowCount()): catidx = model.index(i, 0) catname = model.data(catidx) - assert catname in expected - expected_cat = expected[catname] - assert model.rowCount(catidx) == len(expected_cat) + actual[catname] = [] for j in range(model.rowCount(catidx)): name = model.data(model.index(j, 0, parent=catidx)) desc = model.data(model.index(j, 1, parent=catidx)) misc = model.data(model.index(j, 2, parent=catidx)) - assert (name, desc, misc) in expected_cat + actual[catname].append((name, desc, misc)) + assert actual == expected # sanity-check the column_widths assert len(model.column_widths) == 3 assert sum(model.column_widths) == 100 @@ -155,18 +155,26 @@ def bookmarks(bookmark_manager_stub): @pytest.fixture -def web_history(stubs, init_sql): +def web_history_stub(stubs, init_sql): + return sql.SqlTable("History", ['url', 'title', 'atime', 'redirect']) + + +@pytest.fixture +def web_history(web_history_stub, init_sql): """Pre-populate the web-history database.""" - table = sql.SqlTable("History", ['url', 'title', 'atime', 'redirect']) - table.insert(['http://some-redirect.example.com', 'redirect', - datetime(2016, 9, 5).timestamp(), True]) - table.insert(['http://qutebrowser.org', 'qutebrowser', - datetime(2015, 9, 5).timestamp(), False]) - table.insert(['https://python.org', 'Welcome to Python.org', - datetime(2016, 3, 8).timestamp(), False]) - table.insert(['https://github.com', 'https://github.com', - datetime(2016, 5, 1).timestamp(), False]) - return table + web_history_stub.insert(['http://some-redirect.example.com', 'redirect', + datetime(2016, 9, 5).timestamp(), True]) + web_history_stub.insert(['http://qutebrowser.org', 'qutebrowser', + datetime(2015, 9, 5).timestamp(), False]) + web_history_stub.insert(['https://python.org', 'Welcome to Python.org', + datetime(2016, 2, 8).timestamp(), False]) + web_history_stub.insert(['https://python.org', 'Welcome to Python.org', + datetime(2016, 3, 8).timestamp(), False]) + web_history_stub.insert(['https://python.org', 'Welcome to Python.org', + datetime(2014, 3, 8).timestamp(), False]) + web_history_stub.insert(['https://github.com', 'https://github.com', + datetime(2016, 5, 1).timestamp(), False]) + return web_history_stub def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub, @@ -186,15 +194,16 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub, 'rr': 'roll', 'ro': 'rock'}) model = miscmodels.command() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Commands": [ - ('stop', 'stop qutebrowser', 's'), ('drop', 'drop all user data', ''), - ('roll', 'never gonna give you up', 'rr'), ('rock', "Alias for 'roll'", 'ro'), + ('roll', 'never gonna give you up', 'rr'), + ('stop', 'stop qutebrowser', 's'), ] }) @@ -214,23 +223,24 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub): _patch_cmdutils(monkeypatch, stubs, module + '.cmdutils') _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') model = miscmodels.helptopic() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Commands": [ - (':stop', 'stop qutebrowser', 's'), (':drop', 'drop all user data', ''), - (':roll', 'never gonna give you up', 'rr'), (':hide', '', ''), + (':roll', 'never gonna give you up', 'rr'), + (':stop', 'stop qutebrowser', 's'), ], "Settings": [ ('general->time', 'Is an illusion.', None), ('general->volume', 'Goes to 11', None), + ('searchengines->DEFAULT', '', None), ('ui->gesture', 'Waggle your hands to control qutebrowser', None), ('ui->mind', 'Enable mind-control ui (experimental)', None), ('ui->voice', 'Whether to respond to voice commands', None), - ('searchengines->DEFAULT', '', None), ] }) @@ -238,6 +248,7 @@ def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub): def test_quickmark_completion(qtmodeltester, quickmarks): """Test the results of quickmark completion.""" model = miscmodels.quickmark() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -253,14 +264,15 @@ def test_quickmark_completion(qtmodeltester, quickmarks): def test_bookmark_completion(qtmodeltester, bookmarks): """Test the results of bookmark completion.""" model = miscmodels.bookmark() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Bookmarks": [ + ('http://qutebrowser.org', 'qutebrowser | qutebrowser', None), ('https://github.com', 'GitHub', None), ('https://python.org', 'Welcome to Python.org', None), - ('http://qutebrowser.org', 'qutebrowser | qutebrowser', None), ] }) @@ -273,22 +285,24 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks, - quickmarks, bookmarks, and urls are included - entries are sorted by access time - redirect entries are not included + - only the most recent entry is included for each url """ config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} model = urlmodel.url() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Quickmarks": [ - ('https://wiki.archlinux.org', 'aw', None), ('https://duckduckgo.com', 'ddg', None), + ('https://wiki.archlinux.org', 'aw', None), ('https://wikipedia.org', 'wiki', None), ], "Bookmarks": [ + ('http://qutebrowser.org', 'qutebrowser | qutebrowser', None), ('https://github.com', 'GitHub', None), ('https://python.org', 'Welcome to Python.org', None), - ('http://qutebrowser.org', 'qutebrowser | qutebrowser', None), ], "History": [ ('https://github.com', 'https://github.com', '2016-05-01'), @@ -298,17 +312,48 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks, }) +@pytest.mark.parametrize('url, title, pattern, rowcount', [ + ('example.com', 'Site Title', '', 1), + ('example.com', 'Site Title', 'ex', 1), + ('example.com', 'Site Title', 'am', 1), + ('example.com', 'Site Title', 'com', 1), + ('example.com', 'Site Title', 'ex com', 1), + ('example.com', 'Site Title', 'com ex', 0), + ('example.com', 'Site Title', 'ex foo', 0), + ('example.com', 'Site Title', 'foo com', 0), + ('example.com', 'Site Title', 'exm', 0), + ('example.com', 'Site Title', 'Si Ti', 1), + ('example.com', 'Site Title', 'Ti Si', 0), + ('example.com', '', 'foo', 0), + ('foo_bar', '', '_', 1), + ('foobar', '', '_', 0), + ('foo%bar', '', '%', 1), + ('foobar', '', '%', 0), +]) +def test_url_completion_pattern(config_stub, web_history_stub, + quickmark_manager_stub, bookmark_manager_stub, + url, title, pattern, rowcount): + """Test that url completion filters by url and title.""" + config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} + web_history_stub.insert([url, title, 0, False]) + model = urlmodel.url() + model.set_pattern(pattern) + # 2, 0 is History + assert model.rowCount(model.index(2, 0)) == rowcount + + def test_url_completion_delete_bookmark(qtmodeltester, config_stub, web_history, quickmarks, bookmarks, qtbot): """Test deleting a bookmark from the url completion model.""" config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} model = urlmodel.url() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - # delete item (1, 0) -> (bookmarks, 'https://github.com' ) - view = _mock_view_index(model, 1, 0, qtbot) + # delete item (1, 1) -> (bookmarks, 'https://github.com') + view = _mock_view_index(model, 1, 1, qtbot) model.delete_cur_item(view) assert 'https://github.com' not in bookmarks.marks assert 'https://python.org' in bookmarks.marks @@ -321,11 +366,12 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub, """Test deleting a bookmark from the url completion model.""" config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} model = urlmodel.url() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) - # delete item (0, 1) -> (quickmarks, 'ddg' ) - view = _mock_view_index(model, 0, 1, qtbot) + # delete item (0, 0) -> (quickmarks, 'ddg' ) + view = _mock_view_index(model, 0, 0, qtbot) model.delete_cur_item(view) assert 'aw' in quickmarks.marks assert 'ddg' not in quickmarks.marks @@ -338,6 +384,7 @@ def test_url_completion_delete_history(qtmodeltester, config_stub, """Test that deleting a history entry is a noop.""" config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d'} model = urlmodel.url() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -350,13 +397,14 @@ def test_url_completion_delete_history(qtmodeltester, config_stub, def test_session_completion(qtmodeltester, session_manager_stub): session_manager_stub.sessions = ['default', '1', '2'] model = miscmodels.session() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { - "Sessions": [('default', None, None), - ('1', None, None), - ('2', None, None)] + "Sessions": [('1', None, None), + ('2', None, None), + ('default', None, None)] }) @@ -371,6 +419,7 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry, fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), ] model = miscmodels.buffer() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -398,6 +447,7 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub, fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), ] model = miscmodels.buffer() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -415,14 +465,15 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs): _patch_config_section_desc(monkeypatch, stubs, module + '.configdata.SECTION_DESC') model = configmodel.section() + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Sections": [ ('general', 'General/miscellaneous options.', None), - ('ui', 'General options related to the user interface.', None), ('searchengines', 'Definitions of search engines ...', None), + ('ui', 'General options related to the user interface.', None), ] }) @@ -435,6 +486,7 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs, 'mind': 'on', 'voice': 'sometimes'}} model = configmodel.option('ui') + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -457,6 +509,7 @@ def test_setting_option_completion_valuelist(qtmodeltester, monkeypatch, stubs, } } model = configmodel.option('searchengines') + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -471,6 +524,7 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs, _patch_configdata(monkeypatch, stubs, module + '.configdata.DATA') config_stub.data = {'general': {'volume': '0'}} model = configmodel.value('general', 'volume') + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -503,16 +557,17 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub, 'rr': 'roll', 'ro': 'rock'}) model = miscmodels.bind('s') + model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) _check_completions(model, { "Commands": [ - ('stop', 'stop qutebrowser', 's'), ('drop', 'drop all user data', ''), ('hide', '', ''), - ('roll', 'never gonna give you up', 'rr'), ('rock', "Alias for 'roll'", 'ro'), + ('roll', 'never gonna give you up', 'rr'), + ('stop', 'stop qutebrowser', 's'), ] }) diff --git a/tests/unit/completion/test_sqlcategory.py b/tests/unit/completion/test_sqlcategory.py index 03288f016..f29589717 100644 --- a/tests/unit/completion/test_sqlcategory.py +++ b/tests/unit/completion/test_sqlcategory.py @@ -149,6 +149,16 @@ def test_where(): _validate(cat, [('foo', 'bar', False)]) +def test_group(): + table = sql.SqlTable('Foo', ['a', 'b']) + table.insert(['foo', 1]) + table.insert(['bar', 3]) + table.insert(['foo', 2]) + table.insert(['bar', 0]) + cat = sqlcategory.SqlCategory('Foo', select='a, max(b)', group_by='a') + _validate(cat, [('bar', 3), ('foo', 2)]) + + def test_entry(): table = sql.SqlTable('Foo', ['a', 'b', 'c']) assert hasattr(table.Entry, 'a')