qutebrowser/tests/unit/utils/test_standarddir.py

566 lines
20 KiB
Python
Raw Normal View History

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2017-05-09 21:37:03 +02:00
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.utils.standarddir."""
import os
import sys
import json
import os.path
import types
import textwrap
import logging
import subprocess
2017-09-19 22:18:02 +02:00
import attr
from PyQt5.QtCore import QStandardPaths
import pytest
from qutebrowser.utils import standarddir, utils
# 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_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', {})
@pytest.mark.parametrize('orgname, expected', [(None, ''), ('test', 'test')])
def test_unset_organization(qapp, orgname, expected):
"""Test unset_organization.
Args:
orgname: The organizationName to set initially.
expected: The organizationName which is expected when reading back.
"""
qapp.setOrganizationName(orgname)
assert qapp.organizationName() == expected # sanity check
with standarddir._unset_organization():
assert qapp.organizationName() == ''
assert qapp.organizationName() == expected
def test_unset_organization_no_qapp(monkeypatch):
"""Without a QApplication, _unset_organization should do nothing."""
monkeypatch.setattr(standarddir.QApplication, 'instance', lambda: None)
with standarddir._unset_organization():
pass
2017-09-20 11:28:19 +02:00
@pytest.mark.fake_os('mac')
def test_fake_mac_config(tmpdir, monkeypatch):
"""Test standardir.config on a fake Mac."""
monkeypatch.setenv('HOME', str(tmpdir))
expected = str(tmpdir) + '/.qute_test' # always with /
standarddir._init_config(args=None)
assert standarddir.config() == expected
2017-09-17 09:44:34 +02:00
@pytest.mark.parametrize('what', ['data', 'config', 'cache'])
2017-09-13 23:28:02 +02:00
@pytest.mark.not_mac
2017-09-20 11:28:19 +02:00
@pytest.mark.fake_os('windows')
2017-09-17 09:44:34 +02:00
def test_fake_windows(tmpdir, monkeypatch, what):
"""Make sure the config/data/cache dirs are correct on a fake Windows."""
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
lambda typ: str(tmpdir / APPNAME))
2017-09-17 09:44:34 +02:00
standarddir._init_config(args=None)
standarddir._init_data(args=None)
2017-09-17 09:44:34 +02:00
standarddir._init_cache(args=None)
func = getattr(standarddir, what)
assert func() == str(tmpdir / APPNAME / what)
2017-10-21 19:01:22 +02:00
@pytest.mark.posix
def test_fake_haiku(tmpdir, monkeypatch):
"""Test getting data dir on HaikuOS."""
locations = {
QStandardPaths.DataLocation: '',
QStandardPaths.ConfigLocation: str(tmpdir / 'config' / APPNAME),
}
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
locations.get)
monkeypatch.setattr(standarddir.sys, 'platform', 'haiku1')
standarddir._init_data(args=None)
assert standarddir.data() == str(tmpdir / 'config' / APPNAME / 'data')
class TestWritableLocation:
"""Tests for _writable_location."""
def test_empty(self, monkeypatch):
"""Test QStandardPaths returning an empty value."""
monkeypatch.setattr(
'qutebrowser.utils.standarddir.QStandardPaths.writableLocation',
lambda typ: '')
with pytest.raises(standarddir.EmptyValueError):
standarddir._writable_location(QStandardPaths.DataLocation)
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
2015-09-11 21:46:33 +02:00
class TestStandardDir:
"""Tests for standarddir."""
@pytest.mark.parametrize('func, varname', [
(standarddir.data, 'XDG_DATA_HOME'),
(standarddir.config, 'XDG_CONFIG_HOME'),
(lambda: standarddir.config(auto=True), 'XDG_CONFIG_HOME'),
2015-09-11 21:46:33 +02:00
(standarddir.cache, 'XDG_CACHE_HOME'),
2016-11-15 09:12:52 +01:00
(standarddir.runtime, 'XDG_RUNTIME_DIR'),
2015-09-11 21:46:33 +02:00
])
@pytest.mark.linux
def test_linux_explicit(self, monkeypatch, tmpdir, func, varname):
2015-09-11 21:59:39 +02:00
"""Test dirs with XDG environment variables explicitly set.
Args:
func: The function to test.
varname: The environment variable which should be set.
"""
2015-09-11 21:46:33 +02:00
monkeypatch.setenv(varname, str(tmpdir))
standarddir._init_dirs()
assert func() == str(tmpdir / APPNAME)
2015-09-11 21:46:33 +02:00
@pytest.mark.parametrize('func, subdirs', [
(standarddir.data, ['.local', 'share', APPNAME]),
(standarddir.config, ['.config', APPNAME]),
(lambda: standarddir.config(auto=True), ['.config', APPNAME]),
(standarddir.cache, ['.cache', APPNAME]),
2015-09-11 21:59:39 +02:00
(standarddir.download, ['Downloads']),
2015-09-11 21:46:33 +02:00
])
@pytest.mark.linux
def test_linux_normal(self, monkeypatch, tmpdir, func, subdirs):
"""Test dirs with XDG_*_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir))
for var in ['DATA', 'CONFIG', 'CACHE']:
2015-09-11 21:46:33 +02:00
monkeypatch.delenv('XDG_{}_HOME'.format(var), raising=False)
standarddir._init_dirs()
2015-09-11 21:59:39 +02:00
assert func() == str(tmpdir.join(*subdirs))
2015-09-11 21:46:33 +02:00
@pytest.mark.linux
@pytest.mark.qt_log_ignore(r'^QStandardPaths: ')
def test_linux_invalid_runtimedir(self, monkeypatch, tmpdir):
"""With invalid XDG_RUNTIME_DIR, fall back to TempLocation."""
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' / APPNAME)
2017-09-20 11:28:19 +02:00
@pytest.mark.fake_os('windows')
def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir):
"""With an empty tempdir on non-Linux, we should raise."""
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
lambda typ: '')
with pytest.raises(standarddir.EmptyValueError):
standarddir._init_runtime(args=None)
2015-09-11 21:46:33 +02:00
@pytest.mark.parametrize('func, elems, expected', [
(standarddir.data, 2, [APPNAME, 'data']),
(standarddir.config, 2, [APPNAME, 'config']),
(lambda: standarddir.config(auto=True), 2, [APPNAME, 'config']),
(standarddir.cache, 2, [APPNAME, 'cache']),
2015-09-11 21:59:39 +02:00
(standarddir.download, 1, ['Downloads']),
2015-09-11 21:46:33 +02:00
])
@pytest.mark.windows
def test_windows(self, func, elems, expected):
standarddir._init_dirs()
2015-09-11 21:46:33 +02:00
assert func().split(os.sep)[-elems:] == expected
2015-09-15 20:39:01 +02:00
@pytest.mark.parametrize('func, elems, expected', [
(standarddir.data, 2, ['Application Support', APPNAME]),
(lambda: standarddir.config(auto=True), 1, [APPNAME]),
2017-09-13 23:28:02 +02:00
(standarddir.config, 0,
2017-09-17 09:47:34 +02:00
os.path.expanduser('~').split(os.sep) + ['.qute_test']),
(standarddir.cache, 2, ['Caches', APPNAME]),
2015-09-15 20:39:01 +02:00
(standarddir.download, 1, ['Downloads']),
])
2017-07-08 11:12:43 +02:00
@pytest.mark.mac
def test_mac(self, func, elems, expected):
standarddir._init_dirs()
2015-09-15 20:39:01 +02:00
assert func().split(os.sep)[-elems:] == expected
class TestArguments:
"""Tests the --basedir argument."""
@pytest.mark.parametrize('typ, args', [
('config', []),
('config', [True]), # user config
('data', []),
('cache', []),
('download', []),
pytest.param('runtime', [], marks=pytest.mark.linux)])
def test_basedir(self, tmpdir, typ, args):
"""Test --basedir."""
expected = str(tmpdir / typ)
init_args = types.SimpleNamespace(basedir=str(tmpdir))
standarddir._init_dirs(init_args)
func = getattr(standarddir, typ)
assert func(*args) == expected
def test_basedir_relative(self, tmpdir):
"""Test --basedir with a relative path."""
basedir = (tmpdir / 'basedir')
basedir.ensure(dir=True)
with tmpdir.as_cwd():
args = types.SimpleNamespace(basedir='basedir')
standarddir._init_dirs(args)
assert standarddir.config() == str(basedir / 'config')
class TestInitCacheDirTag:
"""Tests for _init_cachedir_tag."""
2015-10-13 23:52:13 +02:00
def test_existent_cache_dir_tag(self, tmpdir, mocker, monkeypatch):
"""Test with an existent CACHEDIR.TAG."""
monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=AssertionError)
m = mocker.patch('qutebrowser.utils.standarddir.os')
m.path.join.side_effect = os.path.join
m.path.exists.return_value = True
standarddir._init_cachedir_tag()
assert not tmpdir.listdir()
m.path.exists.assert_called_with(str(tmpdir / 'CACHEDIR.TAG'))
def test_new_cache_dir_tag(self, tmpdir, mocker, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir))
standarddir._init_cachedir_tag()
assert tmpdir.listdir() == [(tmpdir / 'CACHEDIR.TAG')]
data = (tmpdir / 'CACHEDIR.TAG').read_text('utf-8')
assert data == textwrap.dedent("""
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by qutebrowser.
# For information about cache directory tags, see:
# http://www.brynosaurus.com/cachedir/
""").lstrip()
def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError)
with caplog.at_level(logging.ERROR, 'init'):
standarddir._init_cachedir_tag()
assert len(caplog.records) == 1
assert caplog.records[0].message == 'Failed to create CACHEDIR.TAG'
assert not tmpdir.listdir()
class TestCreatingDir:
2015-10-04 15:41:42 +02:00
"""Make sure inexistent directories are created properly."""
2015-09-11 21:16:06 +02:00
DIR_TYPES = ['config', 'data', 'cache', 'download', 'runtime']
@pytest.mark.parametrize('typ', DIR_TYPES)
def test_basedir(self, tmpdir, typ):
"""Test --basedir."""
basedir = tmpdir / 'basedir'
assert not basedir.exists()
args = types.SimpleNamespace(basedir=str(basedir))
standarddir._init_dirs(args)
func = getattr(standarddir, typ)
func()
assert basedir.exists()
if utils.is_posix:
assert basedir.stat().mode & 0o777 == 0o700
@pytest.mark.parametrize('typ', DIR_TYPES)
def test_exists_race_condition(self, mocker, tmpdir, typ):
"""Make sure there can't be a TOCTOU issue when creating the file.
2017-02-05 00:13:11 +01:00
See https://github.com/qutebrowser/qutebrowser/issues/942.
"""
(tmpdir / typ).ensure(dir=True)
m = mocker.patch('qutebrowser.utils.standarddir.os')
m.makedirs = os.makedirs
m.sep = os.sep
m.path.join = os.path.join
2017-09-13 18:30:03 +02:00
m.expanduser = os.path.expanduser
m.path.exists.return_value = False
m.path.abspath = lambda x: x
args = types.SimpleNamespace(basedir=str(tmpdir))
standarddir._init_dirs(args)
func = getattr(standarddir, typ)
func()
class TestSystemData:
"""Test system data path."""
2017-09-20 12:20:46 +02:00
@pytest.mark.linux
def test_system_datadir_exist_linux(self, monkeypatch):
"""Test that /usr/share/qute_test is used if path exists."""
monkeypatch.setattr(os.path, 'exists', lambda path: True)
standarddir._init_dirs()
assert standarddir.data(system=True) == "/usr/share/qute_test"
@pytest.mark.linux
2016-07-12 03:14:33 +02:00
def test_system_datadir_not_exist_linux(self, monkeypatch, tmpdir,
fake_args):
"""Test that system-wide path isn't used on linux if path not exist."""
2016-07-12 03:14:33 +02:00
fake_args.basedir = str(tmpdir)
monkeypatch.setattr(os.path, 'exists', lambda path: False)
standarddir._init_dirs(fake_args)
assert standarddir.data(system=True) == standarddir.data()
2016-07-12 03:14:33 +02:00
def test_system_datadir_unsupportedos(self, monkeypatch, tmpdir,
fake_args):
"""Test that system-wide path is not used on non-Linux OS."""
2016-07-12 03:14:33 +02:00
fake_args.basedir = str(tmpdir)
monkeypatch.setattr('sys.platform', "potato")
standarddir._init_dirs(fake_args)
assert standarddir.data(system=True) == standarddir.data()
2016-11-15 10:33:49 +01:00
class TestMoveWindowsAndMacOS:
"""Test other invocations of _move_data."""
@pytest.fixture(autouse=True)
def patch_standardpaths(self, files, monkeypatch):
locations = {
QStandardPaths.DataLocation: str(files.local_data_dir),
QStandardPaths.AppDataLocation: str(files.roaming_data_dir),
}
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
locations.get)
monkeypatch.setattr(
standarddir, 'config', lambda auto=False:
str(files.auto_config_dir if auto else files.config_dir))
@pytest.fixture
def files(self, tmpdir):
2017-09-19 22:18:02 +02:00
@attr.s
class Files:
auto_config_dir = attr.ib()
config_dir = attr.ib()
local_data_dir = attr.ib()
roaming_data_dir = attr.ib()
return Files(
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_move_macos(self, files):
"""Test moving configs on macOS."""
(files.auto_config_dir / 'autoconfig.yml').ensure()
(files.auto_config_dir / 'quickmarks').ensure()
files.config_dir.ensure(dir=True)
standarddir._move_macos()
assert (files.auto_config_dir / 'autoconfig.yml').exists()
assert not (files.config_dir / 'autoconfig.yml').exists()
assert not (files.auto_config_dir / 'quickmarks').exists()
assert (files.config_dir / 'quickmarks').exists()
def test_move_windows(self, files):
"""Test moving configs on Windows."""
(files.local_data_dir / 'data' / 'blocked-hosts').ensure()
(files.local_data_dir / 'qutebrowser.conf').ensure()
2017-09-14 09:50:18 +02:00
(files.local_data_dir / 'cache' / 'cachefile').ensure()
standarddir._move_windows()
assert (files.roaming_data_dir / 'data' / 'blocked-hosts').exists()
assert (files.roaming_data_dir / 'config' /
'qutebrowser.conf').exists()
2017-09-14 09:50:18 +02:00
assert not (files.roaming_data_dir / 'cache').exists()
assert (files.local_data_dir / 'cache' / 'cachefile').exists()
class TestMove:
@pytest.fixture
def dirs(self, tmpdir):
2017-09-19 22:18:02 +02:00
@attr.s
class Dirs:
old = attr.ib()
new = attr.ib()
old_file = attr.ib()
new_file = attr.ib()
old_dir = tmpdir / 'old'
new_dir = tmpdir / 'new'
2017-09-19 22:18:02 +02:00
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() are tested in more detail in other tests.
"""
assert standarddir._locations == {}
m_windows = mocker.patch('qutebrowser.utils.standarddir._move_windows')
m_mac = mocker.patch('qutebrowser.utils.standarddir._move_macos')
if args_kind == 'normal':
args = types.SimpleNamespace(basedir=None)
elif args_kind == 'basedir':
args = types.SimpleNamespace(basedir=str(tmpdir))
else:
assert args_kind == 'none'
args = None
standarddir.init(args)
assert standarddir._locations != {}
if args_kind == 'normal':
if utils.is_mac:
2017-09-22 19:58:38 +02:00
m_windows.assert_not_called()
assert m_mac.called
elif utils.is_windows:
assert m_windows.called
2017-09-22 19:58:38 +02:00
m_mac.assert_not_called()
else:
2017-09-22 19:58:38 +02:00
m_windows.assert_not_called()
m_mac.assert_not_called()
else:
2017-09-22 19:58:38 +02:00
m_windows.assert_not_called()
m_mac.assert_not_called()
@pytest.mark.linux
2017-09-16 23:28:06 +02:00
def test_downloads_dir_not_created(monkeypatch, tmpdir):
"""Make sure ~/Downloads is not created."""
download_dir = tmpdir / 'Downloads'
monkeypatch.setenv('HOME', str(tmpdir))
# Make sure xdg-user-dirs.dirs is not picked up
monkeypatch.delenv('XDG_CONFIG_HOME', raising=False)
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.run([sys.executable, str(pyfile)] + sys.path,
universal_newlines=True,
check=True, stdout=subprocess.PIPE).stdout
sub_locations = json.loads(output)
standarddir._init_dirs()
locations = {k.name: v for k, v in standarddir._locations.items()}
assert sub_locations == locations