# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015-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 qutebrowser.misc.sessions.""" import logging import pytest import yaml from PyQt5.QtCore import QUrl, QPoint, QByteArray, QObject QWebView = pytest.importorskip('PyQt5.QtWebKitWidgets').QWebView from qutebrowser.misc import sessions from qutebrowser.misc.sessions import TabHistoryItem as Item from qutebrowser.utils import objreg, qtutils from qutebrowser.browser.webkit import tabhistory pytestmark = pytest.mark.qt_log_ignore('QIODevice::read.*: device not open') webengine_refactoring_xfail = pytest.mark.xfail( True, reason='Broke during QtWebEngine refactoring, will be fixed after ' 'sessions are refactored too.') @pytest.fixture def sess_man(tmpdir): """Fixture providing a SessionManager.""" return sessions.SessionManager(base_path=str(tmpdir)) class TestInit: @pytest.fixture(autouse=True) def cleanup(self): yield objreg.delete('session-manager') @pytest.mark.parametrize('create_dir', [True, False]) def test_with_standarddir(self, tmpdir, monkeypatch, create_dir): monkeypatch.setattr(sessions.standarddir, 'data', lambda: str(tmpdir)) session_dir = tmpdir / 'sessions' if create_dir: session_dir.ensure(dir=True) sessions.init() manager = objreg.get('session-manager') assert session_dir.exists() assert manager._base_path == str(session_dir) def test_did_not_load(sess_man): assert not sess_man.did_load class TestExists: @pytest.mark.parametrize('absolute', [True, False]) def test_existent(self, tmpdir, absolute): session_dir = tmpdir / 'sessions' abs_session = tmpdir / 'foo.yml' rel_session = session_dir / 'foo.yml' session_dir.ensure(dir=True) abs_session.ensure() rel_session.ensure() man = sessions.SessionManager(str(session_dir)) if absolute: name = str(abs_session) else: name = 'foo' assert man.exists(name) @pytest.mark.parametrize('absolute', [True, False]) def test_inexistent(self, tmpdir, absolute): man = sessions.SessionManager(str(tmpdir)) if absolute: name = str(tmpdir / 'foo') else: name = 'foo' assert not man.exists(name) @webengine_refactoring_xfail class TestSaveTab: @pytest.mark.parametrize('is_active', [True, False]) def test_active(self, sess_man, webview, is_active): data = sess_man._save_tab(webview, is_active) if is_active: assert data['active'] else: assert 'active' not in data def test_no_history(self, sess_man, webview): data = sess_man._save_tab(webview, active=False) assert not data['history'] class FakeMainWindow(QObject): """Helper class for the fake_main_window fixture. A fake MainWindow which provides a saveGeometry method. Needs to be a QObject so sip.isdeleted works. """ def __init__(self, geometry, win_id, parent=None): super().__init__(parent) self._geometry = QByteArray(geometry) self.win_id = win_id def saveGeometry(self): return self._geometry @pytest.fixture def fake_window(win_registry, stubs, monkeypatch, qtbot): """Fixture which provides a fake main windows with a tabbedbrowser.""" win0 = FakeMainWindow(b'fake-geometry-0', win_id=0) objreg.register('main-window', win0, scope='window', window=0) webview = QWebView() qtbot.add_widget(webview) browser = stubs.TabbedBrowserStub([webview]) objreg.register('tabbed-browser', browser, scope='window', window=0) yield objreg.delete('main-window', scope='window', window=0) objreg.delete('tabbed-browser', scope='window', window=0) class TestSaveAll: def test_no_history(self, sess_man): # FIXME can this ever actually happen? assert not objreg.window_registry data = sess_man._save_all() assert not data['windows'] @webengine_refactoring_xfail def test_no_active_window(self, sess_man, fake_window, stubs, monkeypatch): qapp = stubs.FakeQApplication(active_window=None) monkeypatch.setattr(sessions, 'QApplication', qapp) sess_man._save_all() @pytest.mark.parametrize('arg, config, current, expected', [ ('foo', None, None, 'foo'), (sessions.default, 'foo', None, 'foo'), (sessions.default, None, 'foo', 'foo'), (sessions.default, None, None, 'default'), ]) def test_get_session_name(config_stub, sess_man, arg, config, current, expected): config_stub.val.session_default_name = config sess_man._current = current assert sess_man._get_session_name(arg) == expected class TestSave: @pytest.fixture def state_config(self, monkeypatch): state = {'general': {}} monkeypatch.setattr(sessions.configfiles, 'state', state) return state @pytest.fixture def fake_history(self, win_registry, stubs, monkeypatch, webview): """Fixture which provides a window with a fake history.""" win = FakeMainWindow(b'fake-geometry-0', win_id=0) objreg.register('main-window', win, scope='window', window=0) browser = stubs.TabbedBrowserStub([webview]) objreg.register('tabbed-browser', browser, scope='window', window=0) qapp = stubs.FakeQApplication(active_window=win) monkeypatch.setattr(sessions, 'QApplication', qapp) def set_data(items): history = browser.widgets()[0].page().history() stream, _data, user_data = tabhistory.serialize(items) qtutils.deserialize_stream(stream, history) for i, data in enumerate(user_data): history.itemAt(i).setUserData(data) yield set_data objreg.delete('main-window', scope='window', window=0) objreg.delete('tabbed-browser', scope='window', window=0) def test_no_state_config(self, sess_man, tmpdir, state_config): session_path = tmpdir / 'foo.yml' sess_man.save(str(session_path)) assert 'session' not in state_config['general'] def test_last_window_session_none(self, caplog, sess_man, tmpdir): session_path = tmpdir / 'foo.yml' with caplog.at_level(logging.ERROR): sess_man.save(str(session_path), last_window=True) assert len(caplog.records) == 1 msg = "last_window_session is None while saving!" assert caplog.records[0].msg == msg assert not session_path.exists() def test_last_window_session(self, sess_man, tmpdir): sess_man.save_last_window_session() session_path = tmpdir / 'foo.yml' sess_man.save(str(session_path), last_window=True) data = session_path.read_text('utf-8') assert data == 'windows: []\n' @pytest.mark.parametrize('exception', [ OSError('foo'), UnicodeEncodeError('ascii', '', 0, 2, 'foo'), yaml.YAMLError('foo')]) def test_fake_exception(self, mocker, sess_man, tmpdir, exception): mocker.patch('qutebrowser.misc.sessions.yaml.dump', side_effect=exception) with pytest.raises(sessions.SessionError, match=str(exception)): sess_man.save(str(tmpdir / 'foo.yml')) assert not tmpdir.listdir() def test_load_next_time(self, tmpdir, state_config, sess_man): session_path = tmpdir / 'foo.yml' sess_man.save(str(session_path), load_next_time=True) assert state_config['general']['session'] == str(session_path) @webengine_refactoring_xfail def test_utf_8_invalid(self, tmpdir, sess_man, fake_history): """Make sure data containing invalid UTF8 raises SessionError.""" session_path = tmpdir / 'foo.yml' fake_history([Item(QUrl('http://www.qutebrowser.org/'), '\ud800', active=True)]) try: sess_man.save(str(session_path)) except sessions.SessionError: # This seems to happen on some systems only?! pass else: data = session_path.read_text('utf-8') assert r'title: "\uD800"' in data def _set_data(self, browser, tab_id, items): """Helper function for test_long_output.""" history = browser.widgets()[tab_id].page().history() stream, _data, user_data = tabhistory.serialize(items) qtutils.deserialize_stream(stream, history) for i, data in enumerate(user_data): history.itemAt(i).setUserData(data) class FakeWebView: """A QWebView fake which provides a "page" with a load_history method. Attributes: loaded_history: The history which has been loaded by load_history, or None. raise_error: The exception to raise on load_history, or None. """ def __init__(self): self.loaded_history = None self.raise_error = None def page(self): return self def load_history(self, data): self.loaded_history = data if self.raise_error is not None: raise self.raise_error # pylint: disable=raising-bad-type @pytest.fixture def fake_webview(): return FakeWebView() @webengine_refactoring_xfail class TestLoadTab: def test_no_history(self, sess_man, fake_webview): sess_man._load_tab(fake_webview, {'history': []}) assert fake_webview.loaded_history == [] def test_load_fail(self, sess_man, fake_webview): fake_webview.raise_error = ValueError with pytest.raises(sessions.SessionError): sess_man._load_tab(fake_webview, {'history': []}) @pytest.mark.parametrize('key, val, expected', [ ('zoom', 1.23, 1.23), ('scroll-pos', {'x': 23, 'y': 42}, QPoint(23, 42)), ]) @pytest.mark.parametrize('in_main_data', [True, False]) def test_user_data(self, sess_man, fake_webview, key, val, expected, in_main_data): item = {'url': 'http://www.example.com/', 'title': 'foo'} if in_main_data: # This information got saved in the main data instead of saving it # per item - make sure the old format can still be read # https://github.com/qutebrowser/qutebrowser/issues/728 d = {'history': [item], key: val} else: item[key] = val d = {'history': [item]} sess_man._load_tab(fake_webview, d) assert len(fake_webview.loaded_history) == 1 assert fake_webview.loaded_history[0].user_data[key] == expected @pytest.mark.parametrize('original_url', ['http://example.org/', None]) def test_urls(self, sess_man, fake_webview, original_url): url = 'http://www.example.com/' item = {'url': url, 'title': 'foo'} if original_url is None: expected = QUrl(url) else: item['original-url'] = original_url expected = QUrl(original_url) d = {'history': [item]} sess_man._load_tab(fake_webview, d) assert len(fake_webview.loaded_history) == 1 loaded_item = fake_webview.loaded_history[0] assert loaded_item.url == QUrl(url) assert loaded_item.original_url == expected class TestListSessions: def test_no_sessions(self, tmpdir): sess_man = sessions.SessionManager(str(tmpdir)) assert not sess_man.list_sessions() def test_with_sessions(self, tmpdir): (tmpdir / 'foo.yml').ensure() (tmpdir / 'bar.yml').ensure() sess_man = sessions.SessionManager(str(tmpdir)) assert sess_man.list_sessions() == ['bar', 'foo'] def test_with_other_files(self, tmpdir): (tmpdir / 'foo.yml').ensure() (tmpdir / 'bar.html').ensure() sess_man = sessions.SessionManager(str(tmpdir)) assert sess_man.list_sessions() == ['foo']