# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2016-2021 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 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 return sql.SqlTable(db, 'CompletionHistory', ['url', 'title', 'last_atime']) @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.")