# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) # # SPDX-License-Identifier: GPL-3.0-or-later """Test the web history completion category.""" import datetime import logging import hypothesis from hypothesis import strategies import pytest from qutebrowser.misc import sql from qutebrowser.completion.models import histcategory from qutebrowser.utils import usertypes @pytest.fixture def hist(data_tmpdir, config_stub): db = sql.Database(str(data_tmpdir / 'test_histcategory.db')) config_stub.val.completion.timestamp_format = '%Y-%m-%d' config_stub.val.completion.web_history.max_items = -1 yield sql.SqlTable(db, 'CompletionHistory', ['url', 'title', 'last_atime']) db.close() # pytest could re-use the filename @pytest.mark.parametrize('pattern, before, after', [ ('foo', [('foo', ''), ('bar', ''), ('aafobbb', '')], [('foo',)]), ('FOO', [('foo', ''), ('bar', ''), ('aafobbb', '')], [('foo',)]), ('foo', [('FOO', ''), ('BAR', ''), ('AAFOBBB', '')], [('FOO',)]), ('foo', [('baz', 'bar'), ('foo', ''), ('bar', 'foo')], [('foo', ''), ('bar', 'foo')]), ('foo', [('fooa', ''), ('foob', ''), ('fooc', '')], [('fooa', ''), ('foob', ''), ('fooc', '')]), ('foo', [('foo', 'bar'), ('bar', 'foo'), ('biz', 'baz')], [('foo', 'bar'), ('bar', 'foo')]), ('foo bar', [('foo', ''), ('bar foo', ''), ('xfooyybarz', '')], [('bar foo', ''), ('xfooyybarz', '')]), ('foo%bar', [('foo%bar', ''), ('foo bar', ''), ('foobar', '')], [('foo%bar', '')]), ('_', [('a_b', ''), ('__a', ''), ('abc', '')], [('a_b', ''), ('__a', '')]), ('%', [('\\foo', '\\bar')], []), ("can't", [("can't touch this", ''), ('a', '')], [("can't touch this", '')]), ("ample itle", [('example.com', 'title'), ('example.com', 'nope')], [('example.com', 'title')]), # https://github.com/qutebrowser/qutebrowser/issues/4411 ("mlfreq", [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')], []), ("ml freq", [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')], [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')]), ]) def test_set_pattern(pattern, before, after, model_validator, hist): """Validate the filtering and sorting results of set_pattern.""" for row in before: hist.insert({'url': row[0], 'title': row[1], 'last_atime': 1}) cat = histcategory.HistoryCategory(database=hist.database) model_validator.set_model(cat) cat.set_pattern(pattern) model_validator.validate(after) def test_set_pattern_repeated(model_validator, hist): """Validate multiple subsequent calls to set_pattern.""" hist.insert({'url': 'example.com/foo', 'title': 'title1', 'last_atime': 1}) hist.insert({'url': 'example.com/bar', 'title': 'title2', 'last_atime': 1}) hist.insert({'url': 'example.com/baz', 'title': 'title3', 'last_atime': 1}) cat = histcategory.HistoryCategory(database=hist.database) model_validator.set_model(cat) cat.set_pattern('b') model_validator.validate([ ('example.com/bar', 'title2'), ('example.com/baz', 'title3'), ]) cat.set_pattern('ba') model_validator.validate([ ('example.com/bar', 'title2'), ('example.com/baz', 'title3'), ]) cat.set_pattern('ba ') model_validator.validate([ ('example.com/bar', 'title2'), ('example.com/baz', 'title3'), ]) cat.set_pattern('ba z') model_validator.validate([ ('example.com/baz', 'title3'), ]) @pytest.mark.parametrize('pattern', [ ' '.join(map(str, range(10000))), 'x' * 50000, ], ids=['numbers', 'characters']) def test_set_pattern_long(hist, message_mock, caplog, pattern): hist.insert({'url': 'example.com/foo', 'title': 'title1', 'last_atime': 1}) cat = histcategory.HistoryCategory(database=hist.database) with caplog.at_level(logging.ERROR): cat.set_pattern(pattern) msg = message_mock.getmsg(usertypes.MessageLevel.error) assert msg.text.startswith("Error with SQL query:") @hypothesis.given(pat=strategies.text()) def test_set_pattern_hypothesis(hist, pat, caplog): hist.insert({'url': 'example.com/foo', 'title': 'title1', 'last_atime': 1}) cat = histcategory.HistoryCategory(database=hist.database) with caplog.at_level(logging.ERROR): cat.set_pattern(pat) @pytest.mark.parametrize('max_items, before, after', [ (-1, [ ('a', 'a', '2017-04-16'), ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ], [ ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ('a', 'a', '2017-04-16'), ]), (3, [ ('a', 'a', '2017-04-16'), ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ], [ ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ('a', 'a', '2017-04-16'), ]), (2 ** 63 - 1, [ # Maximum value sqlite can handle for LIMIT ('a', 'a', '2017-04-16'), ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ], [ ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ('a', 'a', '2017-04-16'), ]), (2, [ ('a', 'a', '2017-04-16'), ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ], [ ('b', 'b', '2017-06-16'), ('c', 'c', '2017-05-16'), ]), (1, [], []), # issue 2849 (crash with empty history) ]) def test_sorting(max_items, before, after, model_validator, hist, config_stub): """Validate the filtering and sorting results of set_pattern.""" config_stub.val.completion.web_history.max_items = max_items for url, title, atime in before: timestamp = datetime.datetime.strptime(atime, '%Y-%m-%d').timestamp() hist.insert({'url': url, 'title': title, 'last_atime': timestamp}) cat = histcategory.HistoryCategory(database=hist.database) model_validator.set_model(cat) cat.set_pattern('') model_validator.validate(after) def test_remove_rows(hist, model_validator): hist.insert({'url': 'foo', 'title': 'Foo', 'last_atime': 0}) hist.insert({'url': 'bar', 'title': 'Bar', 'last_atime': 0}) cat = histcategory.HistoryCategory(database=hist.database) model_validator.set_model(cat) cat.set_pattern('') hist.delete('url', 'foo') cat.removeRows(0, 1) model_validator.validate([('bar', 'Bar')]) def test_remove_rows_fetch(hist): """removeRows should fetch enough data to make the current index valid.""" # we cannot use model_validator as it will fetch everything up front hist.insert_batch({ 'url': [str(i) for i in range(300)], 'title': [str(i) for i in range(300)], 'last_atime': [0] * 300, }) cat = histcategory.HistoryCategory(database=hist.database) cat.set_pattern('') # sanity check that we didn't fetch everything up front assert cat.rowCount() < 300 cat.fetchMore() assert cat.rowCount() == 300 hist.delete('url', '298') cat.removeRows(297, 1) assert cat.rowCount() == 299 @pytest.mark.parametrize('fmt, expected', [ ('%Y-%m-%d', '2018-02-27'), ('%m/%d/%Y %H:%M', '02/27/2018 08:30'), ('', ''), ]) def test_timestamp_fmt(fmt, expected, model_validator, config_stub, data_tmpdir): """Validate the filtering and sorting results of set_pattern.""" config_stub.val.completion.timestamp_format = fmt db = sql.Database(str(data_tmpdir / 'test_timestamp_fmt.db')) hist = sql.SqlTable(db, 'CompletionHistory', ['url', 'title', 'last_atime']) atime = datetime.datetime(2018, 2, 27, 8, 30) hist.insert({'url': 'foo', 'title': '', 'last_atime': atime.timestamp()}) cat = histcategory.HistoryCategory(database=hist.database) model_validator.set_model(cat) cat.set_pattern('') model_validator.validate([('foo', '', expected)]) def test_skip_duplicate_set(message_mock, caplog, hist): cat = histcategory.HistoryCategory(database=hist.database) cat.set_pattern('foo') cat.set_pattern('foobarbaz') msg = caplog.messages[-1] assert msg.startswith( "Skipping query on foobarbaz due to prefix foo returning nothing.")