diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py new file mode 100644 index 000000000..a87b83919 --- /dev/null +++ b/tests/unit/misc/test_sessions.py @@ -0,0 +1,579 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 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 textwrap + +import pytest +import yaml +from PyQt5.QtCore import QUrl, QPoint, QByteArray +from PyQt5.QtWebKitWidgets import QWebView + +from qutebrowser.misc import sessions +from qutebrowser.utils import objreg, qtutils +from qutebrowser.browser import tabhistory +from qutebrowser.browser.tabhistory import TabHistoryItem as Item + + + +@pytest.fixture +def sess_man(): + """Fixture providing a SessionManager with no session dir.""" + return sessions.SessionManager(base_path=None) + + +class TestInit: + + @pytest.yield_fixture(autouse=True) + def cleanup(self): + yield + objreg.delete('session-manager') + + def test_no_standarddir(self, monkeypatch): + monkeypatch.setattr('qutebrowser.misc.sessions.standarddir.data', + lambda: None) + sessions.init() + manager = objreg.get('session-manager') + assert manager._base_path is None + + @pytest.mark.parametrize('create_dir', [True, False]) + def test_with_standarddir(self, tmpdir, monkeypatch, create_dir): + monkeypatch.setattr('qutebrowser.misc.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_existant(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_inexistant(self, tmpdir, absolute): + man = sessions.SessionManager(str(tmpdir)) + + if absolute: + name = str(tmpdir / 'foo') + else: + name = 'foo' + + assert not man.exists(name) + + @pytest.mark.parametrize('absolute', [True, False]) + def test_no_datadir(self, sess_man, tmpdir, absolute): + abs_session = tmpdir / 'foo.yml' + abs_session.ensure() + + if absolute: + assert sess_man.exists(str(abs_session)) + else: + assert not sess_man.exists('foo') + + +class HistTester: + + """Helper object for the hist_tester fixture. + + Makes it possible to use tabhistory.TabHistoryItem objects to easily load + data into a QWebHistory, does some basic checks, and provides the + serialized history. + + Attributes: + sess_man: The SessionManager which is used. + webview: The WebView where the history is loaded to. + """ + + def __init__(self, webview): + self.sess_man = sessions.SessionManager(base_path=None) + self.webview = webview + + def get(self, items): + """Get the serialized history for the given items. + + Args: + items: A list of TabHistoryItems. + + Return: + A list of serialized items, as dicts. + """ + history = self.webview.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) + + d = self.sess_man._save_tab(self.webview, active=True) + new_history = d['history'] + assert len(new_history) == len(items) + return new_history + + def get_single(self, item): + """Convenience method to use get() with a single item.""" + ret = self.get([item]) + assert len(ret) == 1 + return ret[0] + + +class TestSaveTab: + + @pytest.fixture + def hist_tester(self, webview): + """Helper to test saving of history.""" + return HistTester(webview) + + @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'] + + def test_single_item(self, hist_tester): + item = Item(url=QUrl('http://www.qutebrowser.org/'), + title='Test title', + active=True) + expected = { + 'url': 'http://www.qutebrowser.org/', + 'title': 'Test title', + 'active': True, + 'scroll-pos': {'x': 0, 'y': 0}, + 'zoom': 1.0 + } + + hist = hist_tester.get_single(item) + assert hist == expected + + def test_original_url(self, hist_tester): + """Test with an original-url which differs from the URL.""" + item = Item(url=QUrl('http://www.example.com/'), + original_url=QUrl('http://www.example.org/'), + title='Test title', + active=True) + + hist = hist_tester.get_single(item) + + assert hist['url'] == 'http://www.example.com/' + assert hist['original-url'] == 'http://www.example.org/' + + def test_multiple_items(self, hist_tester): + items = [ + Item(url=QUrl('http://www.qutebrowser.org/'), title='test 1'), + Item(url=QUrl('http://www.example.com/'), title='test 2', + active=True), + Item(url=QUrl('http://www.example.org/'), title='test 3'), + ] + + expected = [ + { + 'url': 'http://www.qutebrowser.org/', + 'title': 'test 1', + }, + { + 'url': 'http://www.example.com/', + 'title': 'test 2', + 'active': True, + 'scroll-pos': {'x': 0, 'y': 0}, + 'zoom': 1.0 + }, + { + 'url': 'http://www.example.org/', + 'title': 'test 3', + }, + ] + + hist = hist_tester.get(items) + assert hist == expected + + @pytest.mark.parametrize('factor', [-1.0, 0.0, 1.5]) + def test_zoom(self, hist_tester, factor): + """Test zoom.""" + + items = [ + Item(url=QUrl('http://www.example.com/'), title='Test title', + active=True), + Item(url=QUrl('http://www.example.com/'), title='Test title', + user_data={'zoom': factor}), + ] + hist_tester.webview.setZoomFactor(factor) + hist = hist_tester.get(items) + assert hist[0]['zoom'] == factor + assert hist[1]['zoom'] == factor + + @pytest.mark.parametrize('pos_x, pos_y', [ + (0.0, 0.0), (1.0, 1.0), (0.0, 1.0), (1.0, 0.0), + ]) + def test_scroll_current(self, hist_tester, pos_x, pos_y): + """Test scroll position on the current URL.""" + items = [ + Item(url=QUrl('http://www.example.com/'), title='Test title', + active=True), + Item(url=QUrl('http://www.example.com/'), title='Test title', + user_data={'scroll-pos': QPoint(pos_x, pos_y)}), + ] + frame = hist_tester.webview.page().mainFrame() + + text = '{}\n'.format('x' * 100) * 100 + frame.setHtml('{}'.format(text)) + frame.setScrollPosition(QPoint(pos_x, pos_y)) + + hist = hist_tester.get(items) + assert hist[0]['scroll-pos'] == {'x': pos_x, 'y': pos_y} + assert hist[1]['scroll-pos'] == {'x': pos_x, 'y': pos_y} + + +class FakeMainWindow: + + """Helper class for the fake_main_window fixture. + + A fake MainWindow which provides a saveGeometry method. + """ + + def __init__(self, geometry, win_id): + self._geometry = QByteArray(geometry) + self.win_id = win_id + + def saveGeometry(self): + return self._geometry + + +class FakeTabbedBrowser: + + """A fake tabbed-browser which contains some widgets.""" + + def __init__(self, widgets): + self._widgets = widgets + + def widgets(self): + return self._widgets + + def currentIndex(self): + return 1 + + +@pytest.yield_fixture +def fake_windows(win_registry, stubs, monkeypatch, qtbot): + """Fixture which provides two fake main windows and tabbedbrowsers.""" + win_registry.add_window(1) + win0 = FakeMainWindow(b'fake-geometry-0', win_id=0) + win1 = FakeMainWindow(b'fake-geometry-1', win_id=1) + objreg.register('main-window', win0, scope='window', window=0) + objreg.register('main-window', win1, scope='window', window=1) + + webview0 = QWebView() + qtbot.add_widget(webview0) + webview1 = QWebView() + qtbot.add_widget(webview1) + webview2 = QWebView() + qtbot.add_widget(webview2) + webview3 = QWebView() + qtbot.add_widget(webview3) + + browser0 = FakeTabbedBrowser([webview0, webview1]) + browser1 = FakeTabbedBrowser([webview2, webview3]) + objreg.register('tabbed-browser', browser0, scope='window', window=0) + objreg.register('tabbed-browser', browser1, scope='window', window=1) + + qapp = stubs.FakeQApplication(active_window=win0) + monkeypatch.setattr('qutebrowser.misc.sessions.QApplication', qapp) + + yield browser0, browser1 + + objreg.delete('main-window', scope='window', window=0) + objreg.delete('main-window', scope='window', window=1) + objreg.delete('tabbed-browser', scope='window', window=0) + objreg.delete('tabbed-browser', scope='window', window=1) + + +class TestSaveAll: + + def test_no_history(self, sess_man): + assert not objreg.window_registry + data = sess_man._save_all() + assert not data['windows'] + + def test_normal(self, fake_windows, sess_man): + """Test with some windows and tabs set up.""" + data = sess_man._save_all() + + win1 = { + 'active': True, + 'geometry': b'fake-geometry-0', + 'tabs': [ + {'history': []}, + {'active': True, 'history': []}, + ], + } + win2 = { + 'geometry': b'fake-geometry-1', + 'tabs': [ + {'history': []}, + {'active': True, 'history': []}, + ], + } + expected = {'windows': [win1, win2]} + assert data == expected + + def test_no_active_window(self, sess_man, fake_windows, stubs, + monkeypatch): + qapp = stubs.FakeQApplication(active_window=None) + monkeypatch.setattr('qutebrowser.misc.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.data = {'general': {'session-default-name': config}} + sess_man._current = current + assert sess_man._get_session_name(arg) == expected + + +class TestSave: + + @pytest.yield_fixture + def state_config(self): + state = {'general': {}} + objreg.register('state-config', state) + yield state + objreg.delete('state-config') + + @pytest.yield_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 = FakeTabbedBrowser([webview]) + + objreg.register('tabbed-browser', browser, scope='window', window=0) + qapp = stubs.FakeQApplication(active_window=win) + monkeypatch.setattr('qutebrowser.misc.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_config_storage(self, sess_man): + with pytest.raises(sessions.SessionError) as excinfo: + sess_man.save('foo') + assert str(excinfo.value) == "No data storage configured." + + def test_simple_dump(self, sess_man, tmpdir): + session_path = tmpdir / 'foo.yml' + name = sess_man.save(str(session_path)) + + assert name == str(session_path) + data = session_path.read() + assert data == 'windows: []\n' + + def test_update_completion_signal(self, sess_man, tmpdir, qtbot): + session_path = tmpdir / 'foo.yml' + blocker = qtbot.waitSignal(sess_man.update_completion) + sess_man.save(str(session_path)) + assert blocker.signal_triggered + + 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, sess_man, tmpdir): + session_path = tmpdir / 'foo.yml' + with pytest.raises(AssertionError): + sess_man.save(str(session_path), last_window=True) + 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() + 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) as excinfo: + sess_man.save(str(tmpdir / 'foo.yml')) + + assert str(excinfo.value) == str(exception) + assert not tmpdir.listdir() + + def test_directory(self, sess_man, tmpdir): + """Test with a directory given as session file.""" + with pytest.raises(sessions.SessionError) as excinfo: + sess_man.save(str(tmpdir)) + + assert str(excinfo.value) == "Filename refers to a directory" + 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) + + def test_utf_8_valid(self, tmpdir, sess_man, fake_history): + """Make sure data containing valid UTF8 gets saved correctly.""" + session_path = tmpdir / 'foo.yml' + fake_history([Item(QUrl('http://www.qutebrowser.org/'), 'foo☃bar', + active=True)]) + + sess_man.save(str(session_path)) + + data = session_path.read() + assert 'title: foo☃bar' in data + + 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)]) + + with pytest.raises(sessions.SessionError): + sess_man.save(str(session_path)) + + 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) + + def test_long_output(self, fake_windows, tmpdir, sess_man): + session_path = tmpdir / 'foo.yml' + + items1 = [ + Item(QUrl('http://www.qutebrowser.org/'), 'test title 1'), + Item(QUrl('http://www.example.org/'), 'test title 2', + original_url=QUrl('http://www.example.com/'), active=True), + ] + items2 = [ + Item(QUrl('http://www.example.com/?q=foo+bar'), 'test title 3'), + Item(QUrl('http://www.example.com/?q=test%20foo'), 'test title 4', + active=True), + ] + items3 = [] + items4 = [ + Item(QUrl('http://www.github.com/The-Compiler/qutebrowser'), + 'test title 5', active=True), + ] + + self._set_data(fake_windows[0], 0, items1) + self._set_data(fake_windows[0], 1, items2) + self._set_data(fake_windows[1], 0, items3) + self._set_data(fake_windows[1], 1, items4) + + expected = """ + windows: + - active: true + geometry: !!binary | + ZmFrZS1nZW9tZXRyeS0w + tabs: + - history: + - title: test title 1 + url: http://www.qutebrowser.org/ + - active: true + original-url: http://www.example.com/ + scroll-pos: + x: 0 + y: 0 + title: test title 2 + url: http://www.example.org/ + zoom: 1.0 + - active: true + history: + - title: test title 3 + url: http://www.example.com/?q=foo+bar + - active: true + scroll-pos: + x: 0 + y: 0 + title: test title 4 + url: http://www.example.com/?q=test%20foo + zoom: 1.0 + - geometry: !!binary | + ZmFrZS1nZW9tZXRyeS0x + tabs: + - history: [] + - active: true + history: + - active: true + scroll-pos: + x: 0 + y: 0 + title: test title 5 + url: http://www.github.com/The-Compiler/qutebrowser + zoom: 1.0 + """ + + sess_man.save(str(session_path)) + data = session_path.read() + assert data == textwrap.dedent(expected.strip('\n'))