# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2016-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 the global page history.""" import logging import pytest from PyQt5.QtCore import QUrl from qutebrowser.browser import history from qutebrowser.utils import objreg, urlutils, usertypes from qutebrowser.commands import cmdexc @pytest.fixture(autouse=True) def prerequisites(config_stub, fake_save_manager, init_sql): """Make sure everything is ready to initialize a WebHistory.""" config_stub.data = {'general': {'private-browsing': False}} @pytest.fixture() def hist(tmpdir): return history.WebHistory() @pytest.fixture() def mock_time(mocker): m = mocker.patch('qutebrowser.browser.history.time') m.time.return_value = 12345 return 12345 def test_iter(hist): urlstr = 'http://www.example.com/' url = QUrl(urlstr) hist.add_url(url, atime=12345) assert list(hist) == [(urlstr, '', 12345, False)] def test_len(hist): assert len(hist) == 0 url = QUrl('http://www.example.com/') hist.add_url(url) assert len(hist) == 1 def test_contains(hist): hist.add_url(QUrl('http://www.example.com/'), title='Title', atime=12345) assert 'http://www.example.com/' in hist assert 'www.example.com' not in hist assert 'Title' not in hist assert 12345 not in hist def test_get_recent(hist): hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) hist.add_url(QUrl('http://example.com/'), atime=12345) assert list(hist.get_recent()) == [ ('http://www.qutebrowser.org/', '', 67890, False), ('http://example.com/', '', 12345, False), ] def test_entries_between(hist): hist.add_url(QUrl('http://www.example.com/1'), atime=12345) hist.add_url(QUrl('http://www.example.com/2'), atime=12346) hist.add_url(QUrl('http://www.example.com/3'), atime=12347) hist.add_url(QUrl('http://www.example.com/4'), atime=12348) hist.add_url(QUrl('http://www.example.com/5'), atime=12348) hist.add_url(QUrl('http://www.example.com/6'), atime=12349) hist.add_url(QUrl('http://www.example.com/7'), atime=12350) times = [x.atime for x in hist.entries_between(12346, 12349)] assert times == [12349, 12348, 12348, 12347] def test_entries_before(hist): hist.add_url(QUrl('http://www.example.com/1'), atime=12346) hist.add_url(QUrl('http://www.example.com/2'), atime=12346) hist.add_url(QUrl('http://www.example.com/3'), atime=12347) hist.add_url(QUrl('http://www.example.com/4'), atime=12348) hist.add_url(QUrl('http://www.example.com/5'), atime=12348) hist.add_url(QUrl('http://www.example.com/6'), atime=12348) hist.add_url(QUrl('http://www.example.com/7'), atime=12349) hist.add_url(QUrl('http://www.example.com/8'), atime=12349) times = [x.atime for x in hist.entries_before(12348, limit=3, offset=2)] assert times == [12348, 12347, 12346] def test_clear(qtbot, tmpdir, hist, mocker): hist.add_url(QUrl('http://example.com/')) hist.add_url(QUrl('http://www.qutebrowser.org/')) m = mocker.patch('qutebrowser.browser.history.message.confirm_async', new=mocker.Mock, spec=[]) hist.clear() assert m.called def test_clear_force(qtbot, tmpdir, hist): hist.add_url(QUrl('http://example.com/')) hist.add_url(QUrl('http://www.qutebrowser.org/')) hist.clear(force=True) assert not len(hist) assert not len(hist.completion) def test_delete_url(hist): hist.add_url(QUrl('http://example.com/'), atime=0) hist.add_url(QUrl('http://example.com/1'), atime=0) hist.add_url(QUrl('http://example.com/2'), atime=0) before = set(hist) completion_before = set(hist.completion) hist.delete_url(QUrl('http://example.com/1')) diff = before.difference(set(hist)) assert diff == {('http://example.com/1', '', 0, False)} completion_diff = completion_before.difference(set(hist.completion)) assert completion_diff == {('http://example.com/1', '', 0)} @pytest.mark.parametrize( 'url, atime, title, redirect, history_url, completion_url', [ ('http://www.example.com', 12346, 'the title', False, 'http://www.example.com', 'http://www.example.com'), ('http://www.example.com', 12346, 'the title', True, 'http://www.example.com', None), ('http://www.example.com/sp ce', 12346, 'the title', False, 'http://www.example.com/sp%20ce', 'http://www.example.com/sp ce'), ('https://user:pass@example.com', 12346, 'the title', False, 'https://user@example.com', 'https://user@example.com'), ] ) def test_add_url(qtbot, hist, url, atime, title, redirect, history_url, completion_url): hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect) assert list(hist) == [(history_url, title, atime, redirect)] if completion_url is None: assert not len(hist.completion) else: assert list(hist.completion) == [(completion_url, title, atime)] def test_add_url_invalid(qtbot, hist, caplog): with caplog.at_level(logging.WARNING): hist.add_url(QUrl()) assert not list(hist) assert not list(hist.completion) @pytest.mark.parametrize('level, url, req_url, expected', [ (logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]), (logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False), ('b.com', 'title', 12345, True)]), (logging.WARNING, 'a.com', '', [('a.com', 'title', 12345, False)]), (logging.WARNING, '', '', []), (logging.WARNING, 'data:foo', '', []), (logging.WARNING, 'a.com', 'data:foo', []), ]) def test_add_from_tab(hist, level, url, req_url, expected, mock_time, caplog): with caplog.at_level(level): hist.add_from_tab(QUrl(url), QUrl(req_url), 'title') assert set(hist) == set(expected) @pytest.fixture def hist_interface(hist): # pylint: disable=invalid-name QtWebKit = pytest.importorskip('PyQt5.QtWebKit') from qutebrowser.browser.webkit import webkithistory QWebHistoryInterface = QtWebKit.QWebHistoryInterface # pylint: enable=invalid-name hist.add_url(url=QUrl('http://www.example.com/'), title='example') interface = webkithistory.WebHistoryInterface(hist) QWebHistoryInterface.setDefaultInterface(interface) yield QWebHistoryInterface.setDefaultInterface(None) def test_history_interface(qtbot, webview, hist_interface): html = b"foo" url = urlutils.data_url('text/html', html) with qtbot.waitSignal(webview.loadFinished): webview.load(url) @pytest.fixture def cleanup_init(): # prevent test_init from leaking state yield hist = objreg.get('web-history', None) if hist is not None: hist.setParent(None) objreg.delete('web-history') try: from PyQt5.QtWebKit import QWebHistoryInterface QWebHistoryInterface.setDefaultInterface(None) except ImportError: pass @pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebKit]) def test_init(backend, qapp, tmpdir, monkeypatch, cleanup_init): if backend == usertypes.Backend.QtWebKit: pytest.importorskip('PyQt5.QtWebKitWidgets') else: assert backend == usertypes.Backend.QtWebEngine monkeypatch.setattr(history.objects, 'backend', backend) history.init(qapp) hist = objreg.get('web-history') assert hist.parent() is qapp try: from PyQt5.QtWebKit import QWebHistoryInterface except ImportError: QWebHistoryInterface = None if backend == usertypes.Backend.QtWebKit: default_interface = QWebHistoryInterface.defaultInterface() assert default_interface._history is hist else: assert backend == usertypes.Backend.QtWebEngine if QWebHistoryInterface is None: default_interface = None else: default_interface = QWebHistoryInterface.defaultInterface() # For this to work, nothing can ever have called setDefaultInterface # before (so we need to test webengine before webkit) assert default_interface is None def test_import_txt(hist, data_tmpdir, monkeypatch, stubs): monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) histfile = data_tmpdir / 'history' # empty line is deliberate, to test skipping empty lines histfile.write('''12345 http://example.com/ title 12346 http://qutebrowser.org/ 67890 http://example.com/path 68891-r http://example.com/path/other ''') hist.import_txt() assert list(hist) == [ ('http://example.com/', 'title', 12345, False), ('http://qutebrowser.org/', '', 12346, False), ('http://example.com/path', '', 67890, False), ('http://example.com/path/other', '', 68891, True) ] assert not histfile.exists() assert (data_tmpdir / 'history.bak').exists() @pytest.mark.parametrize('line', [ '', '#12345 http://example.com/commented', # https://bugreports.qt.io/browse/QTBUG-60364 '12345 http://.com/', '12345 https://.com/', '12345 http://www..com/', '12345 https://www..com/', # issue #2646 '12345 data:text/html;charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-', ]) def test_import_txt_skip(hist, data_tmpdir, line, monkeypatch, stubs): """import_txt should skip certain lines silently.""" monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) histfile = data_tmpdir / 'history' histfile.write(line) hist.import_txt() assert not histfile.exists() assert not len(hist) @pytest.mark.parametrize('line', [ 'xyz http://example.com/bad-timestamp', '12345', 'http://example.com/no-timestamp', '68891-r-r http://example.com/double-flag', '68891-x http://example.com/bad-flag', '68891 http://.com', ]) def test_import_txt_invalid(hist, data_tmpdir, line, monkeypatch, stubs, caplog): """import_txt should fail on certain lines.""" monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) histfile = data_tmpdir / 'history' histfile.write(line) with caplog.at_level(logging.ERROR): hist.import_txt() assert any(rec.msg.startswith("Failed to import history:") for rec in caplog.records) assert histfile.exists() def test_import_txt_nonexistent(hist, data_tmpdir, monkeypatch, stubs): """import_txt should do nothing if the history file doesn't exist.""" monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) hist.import_txt() def test_debug_dump_history(hist, tmpdir): hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345) hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346) hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347) hist.add_url(QUrl('http://example.com/4'), title="Title4", atime=12348, redirect=True) histfile = tmpdir / 'history' hist.debug_dump_history(str(histfile)) expected = ['12345 http://example.com/1 Title1', '12346 http://example.com/2 Title2', '12347 http://example.com/3 Title3', '12348-r http://example.com/4 Title4'] assert histfile.read() == '\n'.join(expected) def test_debug_dump_history_nonexistent(hist, tmpdir): histfile = tmpdir / 'nonexistent' / 'history' with pytest.raises(cmdexc.CommandError): hist.debug_dump_history(str(histfile)) def test_rebuild_completion(hist): hist.insert({'url': 'example.com/1', 'title': 'example1', 'redirect': False, 'atime': 1}) hist.insert({'url': 'example.com/1', 'title': 'example1', 'redirect': False, 'atime': 2}) hist.insert({'url': 'example.com/2%203', 'title': 'example2', 'redirect': False, 'atime': 3}) hist.insert({'url': 'example.com/3', 'title': 'example3', 'redirect': True, 'atime': 4}) hist.insert({'url': 'example.com/2 3', 'title': 'example2', 'redirect': False, 'atime': 5}) hist.completion.delete_all() hist2 = history.WebHistory() assert list(hist2.completion) == [ ('example.com/1', 'example1', 2), ('example.com/2 3', 'example2', 5), ] def test_no_rebuild_completion(hist): """Ensure that completion is not regenerated unless completely empty.""" hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) hist.completion.delete('url', 'example.com/2') hist2 = history.WebHistory() assert list(hist2.completion) == [('example.com/1', '', 1)] def test_user_version(hist, monkeypatch): """Ensure that completion is regenerated if user_version is incremented.""" hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) hist.completion.delete('url', 'example.com/2') hist2 = history.WebHistory() assert list(hist2.completion) == [('example.com/1', '', 1)] monkeypatch.setattr(history, '_USER_VERSION', history._USER_VERSION + 1) hist3 = history.WebHistory() assert list(hist3.completion) == [ ('example.com/1', '', 1), ('example.com/2', '', 2), ]