diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 9f93d26bd..752885dd0 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -30,6 +30,8 @@ Breaking changes - New config system which ignores the old config file. - The depedency on PyOpenGL (when using QtWebEngine) got removed. Note that PyQt5.QtOpenGL is still a dependency. +- Migrating QtWebEngine data written by versions before 2016-11-15 (before + v0.9.0) is now not supported anymore. Major changes ~~~~~~~~~~~~~ diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index e370fd31c..54b0108a1 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -11,8 +11,8 @@ Migrating older configurations ------------------------------ qutebrowser does no automatic migration for the new configuration. However, -there's a special link:qute://configdiff[config diff page] which will show you -the changes you did in your old configuration, compared to the old defaults. +there's a special link:qute://configdiff[] page in qutebrowser, which will show +you the changes you did in your old configuration, compared to the old defaults. Other changes in default settings: @@ -38,31 +38,34 @@ interface or command line. Changes you make this way are immediately active (with the exception of a few settings, where this is pointed out in the documentation) and are persisted in an `autoconfig.yml` file. -Using the link:commands.html#set[`:set`] command and command completion, you -can quickly set settings interactively, for example `:set tabs.position left`. +The `autoconfig.yml` file is located in the "config" folder listed on the +link:qute://version[] page. On macOS, the "auto config" folder is used, which is +different from where hand-written config files are kept. -To get more help about a setting, use e.g. `:help tabs.position`. +However, **do not** edit `autoconfig.yml` by hand. Instead, see the next +section. If you want to customize many settings, you can open the link:qute://settings[] page by running `:set` without any arguments, where all settings are listed and customizable. +Using the link:commands.html#set[`:set`] command and command completion, you +can quickly set settings interactively, for example `:set tabs.position left`. + +To get more help about a setting, use e.g. `:help tabs.position`. + To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and link:commands.html#unbind[`:unbind`] commands: -- Binding a key: `:bind ,v spawn mpv {url}` -- Unbinding: `:unbind ,v` -- Changing an existing binding: `bind --force ,v message-info foo` +- Binding the key chain "`,`, `v`" to the `:spawn mpv {url}` command: `:bind ,v + spawn mpv {url}` +- Unbinding the same key chain: `:unbind ,v` +- Changing an existing binding: `bind --force ,v message-info foo`. Without + `--force`, qutebrowser will show an error because `,v` is already bound. Key chains starting with a comma are ideal for custom bindings, as the comma key will never be used in a default keybinding. -The `autoconfig.yml` file is located in the "config" folder listed on the -link:qute://version[] page. On macOS, the "auto config" folder is used, which is -different from where hand-written config files are kept. - -**Do not** edit `autoconfig.yml` by hand. Instead, see the next section. - Configuring qutebrowser via config.py ------------------------------------- @@ -77,13 +80,14 @@ link:qute://version[], which is typically `~/.config/qutebrowser/config.py` on Linux, `~/.qutebrowser/config.py` on macOS, and `%APPDATA%/qutebrowser/config.py` on Windows. -Two global objects are pre-defined when executing the file: `c` and `config`. +Two global objects are pre-defined when running `config.py`: `c` and `config`. Changing settings ~~~~~~~~~~~~~~~~~ `c` is a shorthand object to easily set settings like this: +.config.py: [source,python] ---- c.tabs.position = "left" @@ -105,9 +109,9 @@ accepted values depend on the type of the option. Commonly used are: previous elements. * `c.url.start_pages.append("https://www.python.org/")` to add a new value. -Any other config types (e.g. a color) are specified as a string, with the -exception of the `Regex` type which can take either a string (with an `r` prefix -to preserve backslashes) or a Python regex object: +Any other config types (e.g. a color) are specified as a string. The only +exception is the `Regex` type, which can take either a string (with an `r` +prefix to preserve backslashes) or a Python regex object: - `c.hints.next_regexes.append(r'\bvor\b')` - `c.hints.prev_regexes.append(re.compile(r'\bzurück\b'))` @@ -122,6 +126,7 @@ Using strings for setting names If you want to set settings based on their name as a string, use the `config.set` method: +.config.py: [source,python] ---- config.set('content.javascript.enabled', False) @@ -147,6 +152,7 @@ setting. To bind a key: +.config.py: [source,python] ---- config.bind(',v', 'spawn mpv {url}', mode='normal') @@ -179,6 +185,7 @@ If you want all customization done via `:set`, `:bind` and `:unbind` to be temporary, you can suppress loading `autoconfig.yml` in your `config.py` by doing: +.config.py: [source,python] ---- config.load_autoconfig = False diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 5c9a682e2..3dbb72dc8 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -70,6 +70,9 @@ def run(args): quitter = Quitter(args) objreg.register('quitter', quitter) + log.init.debug("Initializing directories...") + standarddir.init(args) + global qApp qApp = Application(args) qApp.setOrganizationName("qutebrowser") @@ -77,9 +80,6 @@ def run(args): qApp.setApplicationVersion(qutebrowser.__version__) qApp.lastWindowClosed.connect(quitter.on_last_window_closed) - log.init.debug("Initializing directories...") - standarddir.init(args) - if args.version: print(version.version()) sys.exit(usertypes.Exit.ok) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index c6624e157..72fca984b 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -132,7 +132,7 @@ class WebHistory(sql.SqlTable): self._do_clear() else: message.confirm_async(self._do_clear, title="Clear all browsing " - "history?") + "history?") def _do_clear(self): self.delete_all() @@ -177,6 +177,9 @@ class WebHistory(sql.SqlTable): log.misc.warning("Ignoring invalid URL being added to history") return + if 'no-sql-history' in objreg.get('args').debug_flags: + return + atime = int(atime) if (atime is not None) else int(time.time()) self.insert({'url': self._format_url(url), 'title': title, diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 2de72c615..24cd49914 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -150,7 +150,7 @@ def debug_flag_error(flag): debug-exit: Turn on debugging of late exit. pdb-postmortem: Drop into pdb on exceptions. """ - valid_flags = ['debug-exit', 'pdb-postmortem'] + valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history'] if flag in valid_flags: return flag diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 15c9e72ce..6778c30ee 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -249,12 +249,14 @@ def unset_organization(): This is primarily needed in config.py. """ qapp = QApplication.instance() - orgname = qapp.organizationName() - qapp.setOrganizationName(None) + if qapp is not None: + orgname = qapp.organizationName() + qapp.setOrganizationName(None) try: yield finally: - qapp.setOrganizationName(orgname) + if qapp is not None: + qapp.setOrganizationName(orgname) class PyQIODevice(io.BufferedIOBase): diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index dfa969328..a4f4dbd7b 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -24,7 +24,7 @@ import sys import shutil import os.path -from PyQt5.QtCore import QCoreApplication, QStandardPaths +from PyQt5.QtCore import QStandardPaths from qutebrowser.utils import log, qtutils, debug, usertypes, message @@ -37,6 +37,9 @@ Location = usertypes.enum('Location', ['config', 'auto_config', 'cache', 'download', 'runtime']) +APPNAME = 'qutebrowser' + + class EmptyValueError(Exception): """Error raised when QStandardPaths returns an empty value.""" @@ -53,11 +56,6 @@ def _init_config(args): path = os.path.join(app_data_path, 'config') else: path = _writable_location(typ) - appname = QCoreApplication.instance().applicationName() - if path.split(os.sep)[-1] != appname: # pragma: no branch - # WORKAROUND - see - # https://bugreports.qt.io/browse/QTBUG-38872 - path = os.path.join(path, appname) _create(path) _locations[Location.config] = path _locations[Location.auto_config] = path @@ -66,7 +64,7 @@ def _init_config(args): if sys.platform == 'darwin': overridden, path = _from_args(typ, args) if not overridden: # pragma: no branch - path = os.path.expanduser('~/.qutebrowser') + path = os.path.expanduser('~/.' + APPNAME) _create(path) _locations[Location.config] = path @@ -88,8 +86,7 @@ def _init_data(args): overridden, path = _from_args(typ, args) if not overridden: if os.name == 'nt': - app_data_path = _writable_location( - QStandardPaths.AppDataLocation) + app_data_path = _writable_location(QStandardPaths.AppDataLocation) path = os.path.join(app_data_path, 'data') else: path = _writable_location(typ) @@ -99,7 +96,7 @@ def _init_data(args): # system_data _locations.pop(Location.system_data, None) # Remove old state if sys.platform.startswith('linux'): - path = "/usr/share/qutebrowser" + path = '/usr/share/' + APPNAME if os.path.exists(path): _locations[Location.system_data] = path @@ -123,7 +120,12 @@ def _init_cache(args): typ = QStandardPaths.CacheLocation overridden, path = _from_args(typ, args) if not overridden: - path = _writable_location(typ) + if os.name == 'nt': + # Local, not Roaming! + data_path = _writable_location(QStandardPaths.DataLocation) + path = os.path.join(data_path, 'cache') + else: + path = _writable_location(typ) _create(path) _locations[Location.cache] = path @@ -169,6 +171,7 @@ def _init_runtime(args): path = _writable_location(QStandardPaths.TempLocation) # This is generic, but per-user. + # _writable_location makes sure we have a qutebrowser-specific subdir. # # For TempLocation: # "The returned value might be application-specific, shared among @@ -176,8 +179,7 @@ def _init_runtime(args): # # Unfortunately this path could get too long for sockets (which have a # maximum length of 104 chars), so we don't add the username here... - appname = QCoreApplication.instance().applicationName() - path = os.path.join(path, appname) + _create(path) _locations[Location.runtime] = path @@ -187,15 +189,39 @@ def runtime(): def _writable_location(typ): - """Wrapper around QStandardPaths.writableLocation.""" + """Wrapper around QStandardPaths.writableLocation. + + Arguments: + typ: A QStandardPaths::StandardLocation member. + """ + typ_str = debug.qenum_key(QStandardPaths, typ) + + # Types we are sure we handle correctly below. + assert typ in [ + QStandardPaths.ConfigLocation, QStandardPaths.DataLocation, + QStandardPaths.CacheLocation, QStandardPaths.DownloadLocation, + QStandardPaths.RuntimeLocation, QStandardPaths.TempLocation, + # FIXME old Qt + getattr(QStandardPaths, 'AppDataLocation', object())], typ_str + with qtutils.unset_organization(): path = QStandardPaths.writableLocation(typ) - typ_str = debug.qenum_key(QStandardPaths, typ) + log.misc.debug("writable location for {}: {}".format(typ_str, path)) if not path: raise EmptyValueError("QStandardPaths returned an empty value!") + # Qt seems to use '/' as path separator even on Windows... path = path.replace('/', os.sep) + + # Add the application name to the given path if needed. + # This is in order for this to work without a QApplication (and thus + # QStandardsPaths not knowing the application name), as well as a + # workaround for https://bugreports.qt.io/browse/QTBUG-38872 + if (typ != QStandardPaths.DownloadLocation and + path.split(os.sep)[-1] != APPNAME): + path = os.path.join(path, APPNAME) + return path @@ -267,7 +293,6 @@ def init(args): _init_dirs(args) _init_cachedir_tag() if args is not None and getattr(args, 'basedir', None) is None: - _move_webengine_data() if sys.platform == 'darwin': # pragma: no cover _move_macos() elif os.name == 'nt': # pragma: no cover @@ -304,7 +329,7 @@ def _move_windows(): for f in os.listdir(old_appdata_dir): if f != 'cache': _move_data(os.path.join(old_appdata_dir, f), - os.path.join(new_config_dir, f)) + os.path.join(new_config_dir, f)) def _init_cachedir_tag(): @@ -326,34 +351,6 @@ def _init_cachedir_tag(): log.init.exception("Failed to create CACHEDIR.TAG") -def _move_webengine_data(): - """Move QtWebEngine data from an older location to the new one.""" - # Do NOT use _writable_location here as that'd give us a wrong path - old_data_dir = QStandardPaths.writableLocation(QStandardPaths.DataLocation) - new_data_dir = os.path.join(data(), 'webengine') - ok = _move_data(os.path.join(old_data_dir, 'QtWebEngine', 'Default'), - new_data_dir) - if not ok: - return - - old_cache_dir = QStandardPaths.writableLocation( - QStandardPaths.CacheLocation) - new_cache_dir = os.path.join(cache(), 'webengine') - ok = _move_data(os.path.join(old_cache_dir, 'QtWebEngine', 'Default'), - new_cache_dir) - if not ok: - return - - # Remove e.g. - # ~/.local/share/qutebrowser/qutebrowser/QtWebEngine/Default - if old_data_dir.split(os.sep)[-2:] == ['qutebrowser', 'qutebrowser']: - log.init.debug("Removing {} / {}".format( - old_data_dir, old_cache_dir)) - for old_dir in old_data_dir, old_cache_dir: - os.rmdir(os.path.join(old_dir, 'QtWebEngine')) - os.rmdir(old_dir) - - def _move_data(old, new): """Migrate data from an old to a new directory. diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 732efa15a..8e7be7fec 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -54,6 +54,7 @@ Feature: Javascript stuff And I wait for "Focus object changed: *" in the log Then no crash should happen + @flaky Scenario: Opening window without user interaction with content.javascript.can_open_tabs_automatically set to true When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to true diff --git a/tests/end2end/features/test_history_bdd.py b/tests/end2end/features/test_history_bdd.py index 319e36aee..e4bfbd9e2 100644 --- a/tests/end2end/features/test_history_bdd.py +++ b/tests/end2end/features/test_history_bdd.py @@ -20,11 +20,19 @@ import logging import re +import pytest import pytest_bdd as bdd bdd.scenarios('history.feature') +@pytest.fixture(autouse=True) +def turn_on_sql_history(quteproc): + """Make sure SQL writing is enabled for tests in this module.""" + quteproc.send_cmd(":debug-pyeval objreg.get('args').debug_flags.remove('no-sql-history')") + quteproc.wait_for_load_finished_url('qute://pyeval') + + @bdd.then(bdd.parsers.parse("the history should contain:\n{expected}")) def check_history(quteproc, httpbin, tmpdir, expected): path = tmpdir / 'history' diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index f5ad4682b..eb166f5b6 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -401,7 +401,8 @@ class QuteProc(testprocess.Process): backend = 'webengine' if self.request.config.webengine else 'webkit' return ['--debug', '--no-err-windows', '--temp-basedir', '--json-logging', '--loglevel', 'vdebug', - '--backend', backend, 'about:blank'] + '--backend', backend, '--debug-flag', 'no-sql-history', + 'about:blank'] def path_to_url(self, path, *, port=None, https=False): """Get a URL based on a filename for the localhost webserver. diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index acc0ad225..c9e1e3913 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -212,14 +212,14 @@ class TestSocketName: def test_mac(self, basedir, expected): socketname = ipc._get_socketname(basedir) parts = socketname.split(os.sep) - assert parts[-2] == 'qute_test' + assert parts[-2] == 'qutebrowser' assert parts[-1] == expected @pytest.mark.linux @pytest.mark.parametrize('basedir, expected', POSIX_TESTS) def test_linux(self, basedir, fake_runtime_dir, expected): socketname = ipc._get_socketname(basedir) - expected_path = str(fake_runtime_dir / 'qute_test' / expected) + expected_path = str(fake_runtime_dir / 'qutebrowser' / expected) assert socketname == expected_path def test_other_unix(self): diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index aad524344..011e86b42 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -21,11 +21,13 @@ import os import sys +import json import os.path import types +import textwrap import collections import logging -import textwrap +import subprocess from PyQt5.QtCore import QStandardPaths import pytest @@ -33,13 +35,23 @@ import pytest from qutebrowser.utils import standarddir +# Use a different application name for tests to make sure we don't change real +# qutebrowser data if we accidentally access the real path in a test. +APPNAME = 'qute_test' + + pytestmark = pytest.mark.usefixtures('qapp') @pytest.fixture(autouse=True) -def clear_standarddir_cache(monkeypatch): - """Make sure the standarddir cache is cleared before/after each test.""" +def clear_standarddir_cache_and_patch(qapp, monkeypatch): + """Make sure the standarddir cache is cleared before/after each test. + + Also, patch APPNAME to qute_test. + """ + assert qapp.applicationName() == APPNAME monkeypatch.setattr(standarddir, '_locations', {}) + monkeypatch.setattr(standarddir, 'APPNAME', APPNAME) yield monkeypatch.setattr(standarddir, '_locations', {}) @@ -48,24 +60,27 @@ def test_fake_mac_config(tmpdir, monkeypatch): """Test standardir.config on a fake Mac.""" monkeypatch.setattr(sys, 'platform', 'darwin') monkeypatch.setenv('HOME', str(tmpdir)) - expected = str(tmpdir) + '/.qutebrowser' # always with / + expected = str(tmpdir) + '/.qute_test' # always with / standarddir._init_config(args=None) assert standarddir.config() == expected # FIXME:conf needs AppDataLocation @pytest.mark.qt55 -@pytest.mark.parametrize('what', ['data', 'config']) +@pytest.mark.parametrize('what', ['data', 'config', 'cache']) @pytest.mark.not_mac -def test_fake_windows_data_config(tmpdir, monkeypatch, what): - """Make sure the config is correct on a fake Windows.""" +def test_fake_windows(tmpdir, monkeypatch, what): + """Make sure the config/data/cache dirs are correct on a fake Windows.""" monkeypatch.setattr(os, 'name', 'nt') monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', - lambda typ: str(tmpdir)) + lambda typ: str(tmpdir / APPNAME)) + standarddir._init_config(args=None) standarddir._init_data(args=None) + standarddir._init_cache(args=None) + func = getattr(standarddir, what) - assert func() == str(tmpdir / what) + assert func() == str(tmpdir / APPNAME / what) class TestWritableLocation: @@ -83,6 +98,8 @@ class TestWritableLocation: def test_sep(self, monkeypatch): """Make sure the right kind of separator is used.""" monkeypatch.setattr(standarddir.os, 'sep', '\\') + monkeypatch.setattr(standarddir.os.path, 'join', + lambda *parts: '\\'.join(parts)) loc = standarddir._writable_location(QStandardPaths.DataLocation) assert '/' not in loc assert '\\' in loc @@ -109,13 +126,13 @@ class TestStandardDir: """ monkeypatch.setenv(varname, str(tmpdir)) standarddir._init_dirs() - assert func() == str(tmpdir / 'qute_test') + assert func() == str(tmpdir / APPNAME) @pytest.mark.parametrize('func, subdirs', [ - (standarddir.data, ['.local', 'share', 'qute_test']), - (standarddir.config, ['.config', 'qute_test']), - (lambda: standarddir.config(auto=True), ['.config', 'qute_test']), - (standarddir.cache, ['.cache', 'qute_test']), + (standarddir.data, ['.local', 'share', APPNAME]), + (standarddir.config, ['.config', APPNAME]), + (lambda: standarddir.config(auto=True), ['.config', APPNAME]), + (standarddir.cache, ['.cache', APPNAME]), (standarddir.download, ['Downloads']), ]) @pytest.mark.linux @@ -134,7 +151,7 @@ class TestStandardDir: monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmpdir / 'does-not-exist')) monkeypatch.setenv('TMPDIR', str(tmpdir / 'temp')) standarddir._init_dirs() - assert standarddir.runtime() == str(tmpdir / 'temp' / 'qute_test') + assert standarddir.runtime() == str(tmpdir / 'temp' / APPNAME) def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir): """With an empty tempdir on non-Linux, we should raise.""" @@ -145,10 +162,10 @@ class TestStandardDir: standarddir._init_runtime(args=None) @pytest.mark.parametrize('func, elems, expected', [ - (standarddir.data, 2, ['qute_test', 'data']), - (standarddir.config, 2, ['qute_test', 'config']), - (lambda: standarddir.config(auto=True), 2, ['qute_test', 'config']), - (standarddir.cache, 2, ['qute_test', 'cache']), + (standarddir.data, 2, [APPNAME, 'data']), + (standarddir.config, 2, [APPNAME, 'config']), + (lambda: standarddir.config(auto=True), 2, [APPNAME, 'config']), + (standarddir.cache, 2, [APPNAME, 'cache']), (standarddir.download, 1, ['Downloads']), ]) @pytest.mark.windows @@ -157,11 +174,11 @@ class TestStandardDir: assert func().split(os.sep)[-elems:] == expected @pytest.mark.parametrize('func, elems, expected', [ - (standarddir.data, 2, ['Application Support', 'qute_test']), - (lambda: standarddir.config(auto=True), 1, ['qute_test']), + (standarddir.data, 2, ['Application Support', APPNAME]), + (lambda: standarddir.config(auto=True), 1, [APPNAME]), (standarddir.config, 0, - os.path.expanduser('~').split(os.sep) + ['.qutebrowser']), - (standarddir.cache, 2, ['Caches', 'qute_test']), + os.path.expanduser('~').split(os.sep) + ['.qute_test']), + (standarddir.cache, 2, ['Caches', APPNAME]), (standarddir.download, 1, ['Downloads']), ]) @pytest.mark.mac @@ -291,11 +308,11 @@ class TestSystemData: """Test system data path.""" def test_system_datadir_exist_linux(self, monkeypatch): - """Test that /usr/share/qutebrowser is used if path exists.""" + """Test that /usr/share/qute_test is used if path exists.""" monkeypatch.setattr('sys.platform', "linux") monkeypatch.setattr(os.path, 'exists', lambda path: True) standarddir._init_dirs() - assert standarddir.data(system=True) == "/usr/share/qutebrowser" + assert standarddir.data(system=True) == "/usr/share/qute_test" @pytest.mark.linux def test_system_datadir_not_exist_linux(self, monkeypatch, tmpdir, @@ -317,24 +334,18 @@ class TestSystemData: # FIXME:conf needs AppDataLocation @pytest.mark.qt55 -class TestDataMigrations: +class TestMoveWindowsAndMacOS: - """Test moving various data from an old to a new location.""" + """Test other invocations of _move_data.""" @pytest.fixture(autouse=True) - def patch_standardpaths(self, files, tmpdir, monkeypatch): + def patch_standardpaths(self, files, monkeypatch): locations = { QStandardPaths.DataLocation: str(files.local_data_dir), - QStandardPaths.CacheLocation: str(tmpdir / 'cache'), QStandardPaths.AppDataLocation: str(files.roaming_data_dir), } monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', locations.get) - - monkeypatch.setattr(standarddir, 'data', - lambda: str(tmpdir / 'new_data')) - monkeypatch.setattr(standarddir, 'cache', - lambda: str(tmpdir / 'new_cache')) monkeypatch.setattr( standarddir, 'config', lambda auto=False: str(files.auto_config_dir if auto else files.config_dir)) @@ -342,112 +353,15 @@ class TestDataMigrations: @pytest.fixture def files(self, tmpdir): files = collections.namedtuple('Files', [ - 'old_webengine_data', 'new_webengine_data', - 'old_webengine_cache', 'new_webengine_cache', 'auto_config_dir', 'config_dir', 'local_data_dir', 'roaming_data_dir']) return files( - old_webengine_data=(tmpdir / 'data' / 'QtWebEngine' / 'Default' / - 'datafile'), - new_webengine_data=tmpdir / 'new_data' / 'webengine' / 'datafile', - old_webengine_cache=(tmpdir / 'cache' / 'QtWebEngine' / 'Default' / - 'cachefile'), - new_webengine_cache=(tmpdir / 'new_cache' / 'webengine' / - 'cachefile'), - auto_config_dir=tmpdir / 'auto_config', - config_dir=tmpdir / 'config', - local_data_dir=tmpdir / 'data', - roaming_data_dir=tmpdir / 'roaming-data', + auto_config_dir=tmpdir / 'auto_config' / APPNAME, + config_dir=tmpdir / 'config' / APPNAME, + local_data_dir=tmpdir / 'data' / APPNAME, + roaming_data_dir=tmpdir / 'roaming-data' / APPNAME, ) - def test_no_webengine_dir(self, caplog): - """Nothing should happen without any QtWebEngine directory.""" - standarddir._move_webengine_data() - assert not any(rec.message.startswith('Moving QtWebEngine') - for rec in caplog.records) - - def test_moving_data(self, files): - files.old_webengine_data.ensure() - files.old_webengine_cache.ensure() - - standarddir._move_webengine_data() - - assert not files.old_webengine_data.exists() - assert not files.old_webengine_cache.exists() - assert files.new_webengine_data.exists() - assert files.new_webengine_cache.exists() - - @pytest.mark.parametrize('what', ['data', 'cache']) - def test_already_existing(self, files, caplog, what): - files.old_webengine_data.ensure() - files.old_webengine_cache.ensure() - - if what == 'data': - files.new_webengine_data.ensure() - old_path = str(files.old_webengine_data.dirname) - new_path = str(files.new_webengine_data.dirname) - else: - files.new_webengine_cache.ensure() - old_path = str(files.old_webengine_cache.dirname) - new_path = str(files.new_webengine_cache.dirname) - - with caplog.at_level(logging.ERROR): - standarddir._move_webengine_data() - - record = caplog.records[-1] - expected = "Failed to move data from {} as {} is non-empty!".format( - old_path, new_path) - assert record.message == expected - - def test_deleting_empty_dirs(self, monkeypatch, tmpdir): - """When we have a qutebrowser/qutebrowser subfolder, clean it up.""" - old_data = tmpdir / 'data' / 'qutebrowser' / 'qutebrowser' - old_cache = tmpdir / 'cache' / 'qutebrowser' / 'qutebrowser' - locations = { - QStandardPaths.DataLocation: str(old_data), - QStandardPaths.CacheLocation: str(old_cache), - } - monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', - locations.get) - - old_data_file = old_data / 'QtWebEngine' / 'Default' / 'datafile' - old_cache_file = old_cache / 'QtWebEngine' / 'Default' / 'cachefile' - old_data_file.ensure() - old_cache_file.ensure() - - standarddir._move_webengine_data() - - assert not (tmpdir / 'data' / 'qutebrowser' / 'qutebrowser').exists() - assert not (tmpdir / 'cache' / 'qutebrowser' / 'qutebrowser').exists() - - def test_deleting_error(self, files, monkeypatch, mocker, caplog): - """When there was an error it should be logged.""" - mock = mocker.Mock(side_effect=OSError('error')) - monkeypatch.setattr(standarddir.shutil, 'move', mock) - files.old_webengine_data.ensure() - files.old_webengine_cache.ensure() - - with caplog.at_level(logging.ERROR): - standarddir._move_webengine_data() - - record = caplog.records[-1] - expected = "Failed to move data from {} to {}: error".format( - files.old_webengine_data.dirname, files.new_webengine_data.dirname) - assert record.message == expected - - def test_existing_but_empty(self, tmpdir): - """Make sure moving works with an empty destination dir.""" - old_dir = tmpdir / 'old' / 'foo' - new_dir = tmpdir / 'new' / 'foo' - old_file = old_dir / 'file' - new_file = new_dir / 'file' - old_file.ensure() - new_dir.ensure(dir=True) - - standarddir._move_data(str(old_dir), str(new_dir)) - assert not old_file.exists() - assert new_file.exists() - def test_move_macos(self, files): """Test moving configs on macOS.""" (files.auto_config_dir / 'autoconfig.yml').ensure() @@ -476,16 +390,68 @@ class TestDataMigrations: assert (files.local_data_dir / 'cache' / 'cachefile').exists() +class TestMove: + + @pytest.fixture + def dirs(self, tmpdir): + dirs = collections.namedtuple('Dirs', ['old', 'new', + 'old_file', 'new_file']) + old_dir = tmpdir / 'old' + new_dir = tmpdir / 'new' + return dirs(old=old_dir, new=new_dir, + old_file=old_dir / 'file', new_file=new_dir / 'file') + + def test_no_old_dir(self, dirs, caplog): + """Nothing should happen without any old directory.""" + standarddir._move_data(str(dirs.old), str(dirs.new)) + assert not any(rec.message.startswith('Migrating data from') + for rec in caplog.records) + + @pytest.mark.parametrize('empty_dest', [True, False]) + def test_moving_data(self, dirs, empty_dest): + dirs.old_file.ensure() + if empty_dest: + dirs.new.ensure(dir=True) + + standarddir._move_data(str(dirs.old), str(dirs.new)) + assert not dirs.old_file.exists() + assert dirs.new_file.exists() + + def test_already_existing(self, dirs, caplog): + dirs.old_file.ensure() + dirs.new_file.ensure() + + with caplog.at_level(logging.ERROR): + standarddir._move_data(str(dirs.old), str(dirs.new)) + + record = caplog.records[-1] + expected = "Failed to move data from {} as {} is non-empty!".format( + dirs.old, dirs.new) + assert record.message == expected + + def test_deleting_error(self, dirs, monkeypatch, mocker, caplog): + """When there was an error it should be logged.""" + mock = mocker.Mock(side_effect=OSError('error')) + monkeypatch.setattr(standarddir.shutil, 'move', mock) + dirs.old_file.ensure() + + with caplog.at_level(logging.ERROR): + standarddir._move_data(str(dirs.old), str(dirs.new)) + + record = caplog.records[-1] + expected = "Failed to move data from {} to {}: error".format( + dirs.old, dirs.new) + assert record.message == expected + + @pytest.mark.parametrize('args_kind', ['basedir', 'normal', 'none']) def test_init(mocker, tmpdir, args_kind): """Do some sanity checks for standarddir.init(). - Things like _init_cachedir_tag() and _move_webengine_data() are tested in - more detail in other tests. + Things like _init_cachedir_tag() are tested in more detail in other tests. """ assert standarddir._locations == {} - m = mocker.patch('qutebrowser.utils.standarddir._move_webengine_data') m_windows = mocker.patch('qutebrowser.utils.standarddir._move_windows') m_mac = mocker.patch('qutebrowser.utils.standarddir._move_macos') if args_kind == 'normal': @@ -500,7 +466,6 @@ def test_init(mocker, tmpdir, args_kind): assert standarddir._locations != {} if args_kind == 'normal': - assert m.called if sys.platform == 'darwin': assert not m_windows.called assert m_mac.called @@ -511,13 +476,12 @@ def test_init(mocker, tmpdir, args_kind): assert not m_windows.called assert not m_mac.called else: - assert not m.called assert not m_windows.called assert not m_mac.called @pytest.mark.linux -def test_downloads_dir_not_crated(monkeypatch, tmpdir): +def test_downloads_dir_not_created(monkeypatch, tmpdir): """Make sure ~/Downloads is not created.""" download_dir = tmpdir / 'Downloads' monkeypatch.setenv('HOME', str(tmpdir)) @@ -526,3 +490,33 @@ def test_downloads_dir_not_crated(monkeypatch, tmpdir): standarddir._init_dirs() assert standarddir.download() == str(download_dir) assert not download_dir.exists() + + +def test_no_qapplication(qapp, tmpdir): + """Make sure directories with/without QApplication are equal.""" + sub_code = """ + import sys + import json + sys.path = sys.argv[1:] # make sure we have the same python path + + from PyQt5.QtWidgets import QApplication + from qutebrowser.utils import standarddir + + assert QApplication.instance() is None + + standarddir.APPNAME = 'qute_test' + standarddir._init_dirs() + + locations = {k.name: v for k, v in standarddir._locations.items()} + print(json.dumps(locations)) + """ + pyfile = tmpdir / 'sub.py' + pyfile.write_text(textwrap.dedent(sub_code), encoding='ascii') + + output = subprocess.check_output([sys.executable, str(pyfile)] + sys.path, + universal_newlines=True) + sub_locations = json.loads(output) + + standarddir._init_dirs() + locations = {k.name: v for k, v in standarddir._locations.items()} + assert sub_locations == locations