Add/improve tests for qutebrowser.utils.utils.

This commit is contained in:
Florian Bruhin 2015-05-19 16:24:43 +02:00
parent 4a4856c176
commit 4204579c06
2 changed files with 513 additions and 163 deletions

View File

@ -50,8 +50,6 @@ def elide(text, length):
def compact_text(text, elidelength=None): def compact_text(text, elidelength=None):
"""Remove leading whitespace and newlines from a text and maybe elide it. """Remove leading whitespace and newlines from a text and maybe elide it.
FIXME: Add tests.
Args: Args:
text: The text to compact. text: The text to compact.
elidelength: To how many chars to elide. elidelength: To how many chars to elide.
@ -105,12 +103,12 @@ def actute_warning():
try: try:
if qtutils.version_check('5.3.0'): if qtutils.version_check('5.3.0'):
return return
except ValueError: except ValueError: # pragma: no cover
pass pass
try: try:
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r', with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
encoding='utf-8') as f: encoding='utf-8') as f:
for line in f: for line in f: # pragma: no branch
if '<dead_actute>' in line: if '<dead_actute>' in line:
if sys.stdout is not None: if sys.stdout is not None:
sys.stdout.flush() sys.stdout.flush()
@ -118,7 +116,7 @@ def actute_warning():
"that is not a bug in qutebrowser! See " "that is not a bug in qutebrowser! See "
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 " "https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
"for details.") "for details.")
break break # pragma: no branch
except OSError: except OSError:
log.init.exception("Failed to read Compose file") log.init.exception("Failed to read Compose file")

View File

@ -23,14 +23,22 @@ import sys
import enum import enum
import datetime import datetime
import os.path import os.path
import io
import logging
import functools
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
import pytest import pytest
import qutebrowser
import qutebrowser.utils # for test_qualname
from qutebrowser.utils import utils, qtutils from qutebrowser.utils import utils, qtutils
ELLIPSIS = '\u2026'
class Color(QColor): class Color(QColor):
"""A QColor with a nicer repr().""" """A QColor with a nicer repr()."""
@ -41,39 +49,193 @@ class Color(QColor):
alpha=self.alpha()) alpha=self.alpha())
class TestCompactText:
"""Test compact_text."""
@pytest.mark.parametrize('text, expected', [
('foo\nbar', 'foobar'),
(' foo \n bar ', 'foobar'),
('\nfoo\n', 'foo'),
])
def test_compact_text(self, text, expected):
"""Test folding of newlines."""
assert utils.compact_text(text) == expected
@pytest.mark.parametrize('elidelength, text, expected', [
(None, 'x' * 100, 'x' * 100),
(6, 'foobar', 'foobar'),
(5, 'foobar', 'foob' + ELLIPSIS),
(5, 'foo\nbar', 'foob' + ELLIPSIS),
(7, 'foo\nbar', 'foobar'),
])
def test_eliding(self, elidelength, text, expected):
"""Test eliding."""
assert utils.compact_text(text, elidelength) == expected
class TestEliding: class TestEliding:
"""Test elide.""" """Test elide."""
ELLIPSIS = '\u2026'
def test_too_small(self): def test_too_small(self):
"""Test eliding to 0 chars which should fail.""" """Test eliding to 0 chars which should fail."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
utils.elide('foo', 0) utils.elide('foo', 0)
def test_length_one(self): @pytest.mark.parametrize('text, length, expected', [
"""Test eliding to 1 char which should yield ...""" ('foo', 1, ELLIPSIS),
assert utils.elide('foo', 1) == self.ELLIPSIS ('foo', 3, 'foo'),
('foobar', 3, 'fo' + ELLIPSIS),
def test_fits(self): ])
"""Test eliding with a string which fits exactly.""" def test_elided(self, text, length, expected):
assert utils.elide('foo', 3) == 'foo' assert utils.elide(text, length) == expected
def test_elided(self):
"""Test eliding with a string which should get elided."""
assert utils.elide('foobar', 3) == 'fo' + self.ELLIPSIS
class TestReadFile: class TestReadFile:
"""Test read_file.""" """Test read_file."""
@pytest.fixture(autouse=True, params=[True, False])
def freezer(self, request, monkeypatch):
if request.param:
monkeypatch.setattr(sys, 'frozen', True, raising=False)
monkeypatch.setattr('sys.executable', qutebrowser.__file__)
def test_readfile(self): def test_readfile(self):
"""Read a test file.""" """Read a test file."""
content = utils.read_file(os.path.join('utils', 'testfile')) content = utils.read_file(os.path.join('utils', 'testfile'))
assert content.splitlines()[0] == "Hello World!" assert content.splitlines()[0] == "Hello World!"
def test_readfile_binary(self):
"""Read a test file in binary mode."""
content = utils.read_file(os.path.join('utils', 'testfile'),
binary=True)
assert content.splitlines()[0] == b"Hello World!"
class Patcher:
"""Helper for TestActuteWarning.
Attributes:
monkeypatch: The pytest monkeypatch fixture.
"""
def __init__(self, monkeypatch):
self.monkeypatch = monkeypatch
def patch_platform(self, platform='linux'):
"""Patch sys.platform."""
self.monkeypatch.setattr('sys.platform', platform)
def patch_exists(self, exists=True):
"""Patch os.path.exists."""
self.monkeypatch.setattr('qutebrowser.utils.utils.os.path.exists',
lambda path: exists)
def patch_version(self, version='5.2.0'):
"""Patch Qt version."""
self.monkeypatch.setattr(
'qutebrowser.utils.utils.qtutils.qVersion', lambda: version)
def patch_file(self, data):
"""Patch open() to return the given data."""
fake_file = io.StringIO(data)
self.monkeypatch.setattr(utils, 'open',
lambda filename, mode, encoding: fake_file,
raising=False)
def patch_all(self, data):
"""Patch everything so the issue would exist."""
self.patch_platform()
self.patch_exists()
self.patch_version()
self.patch_file(data)
class TestActuteWarning:
"""Test actute_warning."""
@pytest.fixture
def patcher(self, monkeypatch):
"""Fixture providing a Patcher helper."""
return Patcher(monkeypatch)
def test_non_linux(self, patcher, capsys):
"""Test with a non-Linux OS."""
patcher.patch_platform('toaster')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_no_compose(self, patcher, capsys):
"""Test with no compose file."""
patcher.patch_platform()
patcher.patch_exists(False)
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_newer_qt(self, patcher, capsys):
"""Test with compose file but newer Qt version."""
patcher.patch_platform()
patcher.patch_exists()
patcher.patch_version('5.4')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_no_match(self, patcher, capsys):
"""Test with compose file and affected Qt but no match."""
patcher.patch_all('foobar')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_empty(self, patcher, capsys):
"""Test with empty compose file."""
patcher.patch_all(None)
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_match(self, patcher, capsys):
"""Test with compose file and affected Qt and a match."""
patcher.patch_all('foobar\n<dead_actute>\nbaz')
utils.actute_warning()
out, err = capsys.readouterr()
assert out.startswith('Note: If you got a')
assert not err
def test_match_stdout_none(self, monkeypatch, patcher, capsys):
"""Test with a match and stdout being None."""
patcher.patch_all('foobar\n<dead_actute>\nbaz')
monkeypatch.setattr('sys.stdout', None)
utils.actute_warning()
def test_unreadable(self, mocker, patcher, capsys, caplog):
"""Test with an unreadable compose file."""
patcher.patch_platform()
patcher.patch_exists()
patcher.patch_version()
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
create=True)
with caplog.atLevel(logging.ERROR, 'init'):
utils.actute_warning()
assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to read Compose file'
out, _err = capsys.readouterr()
assert not out
class TestInterpolateColor: class TestInterpolateColor:
@ -164,15 +326,7 @@ class TestInterpolateColor:
assert Color(color) == expected assert Color(color) == expected
class TestFormatSeconds: @pytest.mark.parametrize('seconds, out', [
"""Tests for format_seconds.
Class attributes:
TESTS: A list of (input, output) tuples.
"""
TESTS = [
(-1, '-0:01'), (-1, '-0:01'),
(0, '0:00'), (0, '0:00'),
(59, '0:59'), (59, '0:59'),
@ -184,23 +338,12 @@ class TestFormatSeconds:
(3600, '1:00:00'), (3600, '1:00:00'),
(3601, '1:00:01'), (3601, '1:00:01'),
(36000, '10:00:00'), (36000, '10:00:00'),
] ])
def test_format_seconds(seconds, out):
@pytest.mark.parametrize('seconds, out', TESTS)
def test_format_seconds(self, seconds, out):
"""Test format_seconds with several tests."""
assert utils.format_seconds(seconds) == out assert utils.format_seconds(seconds) == out
class TestFormatTimedelta: @pytest.mark.parametrize('td, out', [
"""Tests for format_timedelta.
Class attributes:
TESTS: A list of (input, output) tuples.
"""
TESTS = [
(datetime.timedelta(seconds=-1), '-1s'), (datetime.timedelta(seconds=-1), '-1s'),
(datetime.timedelta(seconds=0), '0s'), (datetime.timedelta(seconds=0), '0s'),
(datetime.timedelta(seconds=59), '59s'), (datetime.timedelta(seconds=59), '59s'),
@ -214,11 +357,8 @@ class TestFormatTimedelta:
(datetime.timedelta(seconds=3723), '1h 2m 3s'), (datetime.timedelta(seconds=3723), '1h 2m 3s'),
(datetime.timedelta(seconds=3780), '1h 3m'), (datetime.timedelta(seconds=3780), '1h 3m'),
(datetime.timedelta(seconds=36000), '10h'), (datetime.timedelta(seconds=36000), '10h'),
] ])
def test_format_timedelta(td, out):
@pytest.mark.parametrize('td, out', TESTS)
def test_format_seconds(self, td, out):
"""Test format_seconds with several tests."""
assert utils.format_timedelta(td) == out assert utils.format_timedelta(td) == out
@ -264,30 +404,24 @@ class TestKeyToString:
"""Test key_to_string.""" """Test key_to_string."""
def test_unicode_garbage_keys(self): @pytest.mark.parametrize('key, expected', [
(Qt.Key_Blue, 'Blue'),
(Qt.Key_Backtab, 'Tab'),
(Qt.Key_Escape, 'Escape'),
(Qt.Key_A, 'A'),
(Qt.Key_degree, '°'),
])
def test_normal(self, key, expected):
"""Test a special key where QKeyEvent::toString works incorrectly.""" """Test a special key where QKeyEvent::toString works incorrectly."""
assert utils.key_to_string(Qt.Key_Blue) == 'Blue' assert utils.key_to_string(key) == expected
def test_backtab(self): def test_missing(self, monkeypatch):
"""Test if backtab is normalized to tab correctly.""" """Test with a missing key."""
assert utils.key_to_string(Qt.Key_Backtab) == 'Tab' monkeypatch.delattr('qutebrowser.utils.utils.Qt.Key_Blue')
# We don't want to test the key which is actually missing - we only
def test_escape(self): # want to know if the mapping still behaves properly.
"""Test if escape is normalized to escape correctly."""
assert utils.key_to_string(Qt.Key_Escape) == 'Escape'
def test_letter(self):
"""Test a simple letter key."""
assert utils.key_to_string(Qt.Key_A) == 'A' assert utils.key_to_string(Qt.Key_A) == 'A'
def test_unicode(self):
"""Test a printable unicode key."""
assert utils.key_to_string(Qt.Key_degree) == '°'
def test_special(self):
"""Test a non-printable key handled by QKeyEvent::toString."""
assert utils.key_to_string(Qt.Key_F1) == 'F1'
class TestKeyEventToString: class TestKeyEventToString:
@ -323,12 +457,14 @@ class TestKeyEventToString:
Qt.MetaModifier | Qt.ShiftModifier)) Qt.MetaModifier | Qt.ShiftModifier))
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A'
def test_mac(self, monkeypatch, fake_keyevent_factory):
"""Test with a simulated mac."""
monkeypatch.setattr('sys.platform', 'darwin')
evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier)
assert utils.keyevent_to_string(evt) == 'Meta+A'
class TestNormalize:
"""Test normalize_keystr.""" @pytest.mark.parametrize('orig, repl', [
STRINGS = (
('Control+x', 'ctrl+x'), ('Control+x', 'ctrl+x'),
('Windows+x', 'meta+x'), ('Windows+x', 'meta+x'),
('Mod1+x', 'alt+x'), ('Mod1+x', 'alt+x'),
@ -337,14 +473,258 @@ class TestNormalize:
('Windows++', 'meta++'), ('Windows++', 'meta++'),
('ctrl-x', 'ctrl+x'), ('ctrl-x', 'ctrl+x'),
('control+x', 'ctrl+x') ('control+x', 'ctrl+x')
) ])
def test_normalize_keystr(orig, repl):
@pytest.mark.parametrize('orig, repl', STRINGS)
def test_normalize(self, orig, repl):
"""Test normalize with some strings."""
assert utils.normalize_keystr(orig) == repl assert utils.normalize_keystr(orig) == repl
class TestFakeIOStream:
"""Test FakeIOStream."""
def _write_func(self, text):
return text
def test_flush(self):
"""Smoke-test to see if flushing works."""
s = utils.FakeIOStream(self._write_func)
s.flush()
def test_isatty(self):
"""Make sure isatty() is always false."""
s = utils.FakeIOStream(self._write_func)
assert not s.isatty()
def test_write(self):
"""Make sure writing works."""
s = utils.FakeIOStream(self._write_func)
assert s.write('echo') == 'echo'
class TestFakeIO:
"""Test FakeIO."""
@pytest.yield_fixture(autouse=True)
def restore_streams(self):
"""Restore sys.stderr/sys.stdout after tests."""
old_stdout = sys.stdout
old_stderr = sys.stderr
yield
sys.stdout = old_stdout
sys.stderr = old_stderr
def test_normal(self, capsys):
"""Test without changing sys.stderr/sys.stdout."""
data = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('back to\n')
sys.stderr.write('normal\n')
out, err = capsys.readouterr()
assert out == 'back to\n'
assert err == 'normal\n'
def test_stdout_replaced(self, capsys):
"""Test with replaced stdout."""
data = io.StringIO()
new_stdout = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
sys.stdout = new_stdout
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('still new\n')
sys.stderr.write('normal\n')
out, err = capsys.readouterr()
assert not out
assert err == 'normal\n'
assert new_stdout.getvalue() == 'still new\n'
def test_stderr_replaced(self, capsys):
"""Test with replaced stderr."""
data = io.StringIO()
new_stderr = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
sys.stderr = new_stderr
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('normal\n')
sys.stderr.write('still new\n')
out, err = capsys.readouterr()
assert out == 'normal\n'
assert not err
assert new_stderr.getvalue() == 'still new\n'
class GotException(Exception):
"""Exception used for TestDisabledExcepthook."""
pass
def excepthook(_exc, _val, _tb):
return
def excepthook_2(_exc, _val, _tb):
return
class TestDisabledExcepthook:
"""Test disabled_excepthook.
This doesn't test much as some things are untestable without triggering
the excepthook (which is hard to test).
"""
@pytest.yield_fixture(autouse=True)
def restore_excepthook(self):
"""Restore sys.excepthook and sys.__excepthook__ after tests."""
old_excepthook = sys.excepthook
old_dunder_excepthook = sys.__excepthook__
yield
sys.excepthook = old_excepthook
sys.__excepthook__ = old_dunder_excepthook
def test_normal(self):
"""Test without changing sys.excepthook."""
sys.excepthook = excepthook
assert sys.excepthook is excepthook
with utils.disabled_excepthook():
assert sys.excepthook is not excepthook
assert sys.excepthook is excepthook
def test_changed(self):
"""Test with changed sys.excepthook."""
sys.excepthook = excepthook
with utils.disabled_excepthook():
assert sys.excepthook is not excepthook
sys.excepthook = excepthook_2
assert sys.excepthook is excepthook_2
class TestPreventExceptions:
"""Test prevent_exceptions."""
@utils.prevent_exceptions(42)
def func_raising(self):
raise Exception
def test_raising(self, caplog):
"""Test with a raising function."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_raising()
assert ret == 42
assert len(caplog.records()) == 1
expected = 'Error in test_utils.TestPreventExceptions.func_raising'
actual = caplog.records()[0].message
assert actual == expected
@utils.prevent_exceptions(42)
def func_not_raising(self):
return 23
def test_not_raising(self, caplog):
"""Test with a non-raising function."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_not_raising()
assert ret == 23
assert not caplog.records()
@utils.prevent_exceptions(42, True)
def func_predicate_true(self):
raise Exception
def test_predicate_true(self, caplog):
"""Test with a True predicate."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_predicate_true()
assert ret == 42
assert len(caplog.records()) == 1
@utils.prevent_exceptions(42, False)
def func_predicate_false(self):
raise Exception
def test_predicate_false(self, caplog):
"""Test with a False predicate."""
with caplog.atLevel(logging.ERROR, 'misc'):
with pytest.raises(Exception):
self.func_predicate_false()
assert not caplog.records()
class Obj:
"""Test object for test_get_repr()."""
pass
@pytest.mark.parametrize('constructor, attrs, expected', [
(False, {}, '<test_utils.Obj>'),
(False, {'foo': None}, '<test_utils.Obj foo=None>'),
(False, {'foo': "b'ar", 'baz': 2}, '<test_utils.Obj baz=2 foo="b\'ar">'),
(True, {}, 'test_utils.Obj()'),
(True, {'foo': None}, 'test_utils.Obj(foo=None)'),
(True, {'foo': "te'st", 'bar': 2}, 'test_utils.Obj(bar=2, foo="te\'st")'),
])
def test_get_repr(constructor, attrs, expected):
"""Test get_repr()."""
assert utils.get_repr(Obj(), constructor, **attrs) == expected
class QualnameObj():
"""Test object for test_qualname."""
def func(self):
"""Test method for test_qualname."""
pass
def qualname_func(_blah):
"""Test function for test_qualname."""
pass
@pytest.mark.parametrize('obj, expected', [
(QualnameObj(), '<unknown>'), # instance - unknown
(QualnameObj, 'test_utils.QualnameObj'), # class
(QualnameObj.func, 'test_utils.QualnameObj.func'), # unbound method
(QualnameObj().func, 'test_utils.QualnameObj.func'), # bound method
(qualname_func, 'test_utils.qualname_func'), # function
(functools.partial(qualname_func, True), 'test_utils.qualname_func'),
(qutebrowser, 'qutebrowser'), # module
(qutebrowser.utils, 'qutebrowser.utils'), # submodule
(utils, 'qutebrowser.utils.utils'), # submodule (from-import)
])
def test_qualname(obj, expected):
assert utils.qualname(obj) == expected
class TestIsEnum: class TestIsEnum:
"""Test is_enum.""" """Test is_enum."""
@ -412,19 +792,12 @@ class TestRaises:
utils.raises(ValueError, self.do_raise) utils.raises(ValueError, self.do_raise)
class TestForceEncoding: @pytest.mark.parametrize('inp, enc, expected', [
"""Test force_encoding."""
TESTS = [
('hello world', 'ascii', 'hello world'), ('hello world', 'ascii', 'hello world'),
('hellö wörld', 'utf-8', 'hellö wörld'), ('hellö wörld', 'utf-8', 'hellö wörld'),
('hellö wörld', 'ascii', 'hell? w?rld'), ('hellö wörld', 'ascii', 'hell? w?rld'),
] ])
def test_force_encoding(inp, enc, expected):
@pytest.mark.parametrize('inp, enc, expected', TESTS)
def test_fitting_ascii(self, inp, enc, expected):
"""Test force_encoding will yield expected text."""
assert utils.force_encoding(inp, enc) == expected assert utils.force_encoding(inp, enc) == expected
@ -437,44 +810,23 @@ class TestNewestSlice:
with pytest.raises(ValueError): with pytest.raises(ValueError):
utils.newest_slice([], -2) utils.newest_slice([], -2)
def test_count_minus_one(self): @pytest.mark.parametrize('items, count, expected', [
"""Test with a count of -1 (all elements).""" # Count of -1 (all elements).
items = range(20) (range(20), -1, range(20)),
sliced = utils.newest_slice(items, -1) # Count of 0 (no elements).
assert list(sliced) == list(items) (range(20), 0, []),
# Count which is much smaller than the iterable.
def test_count_zero(self): (range(20), 5, [15, 16, 17, 18, 19]),
"""Test with a count of 0 (no elements).""" # Count which is exactly one smaller."""
items = range(20) (range(5), 4, [1, 2, 3, 4]),
sliced = utils.newest_slice(items, 0) # Count which is just as large as the iterable."""
assert list(sliced) == [] (range(5), 5, range(5)),
# Count which is one bigger than the iterable.
def test_count_much_smaller(self): (range(5), 6, range(5)),
"""Test with a count which is much smaller than the iterable.""" # Count which is much bigger than the iterable.
items = range(20) (range(5), 50, range(5)),
sliced = utils.newest_slice(items, 5) ])
assert list(sliced) == [15, 16, 17, 18, 19] def test_good(self, items, count, expected):
"""Test slices which shouldn't raise an exception."""
def test_count_smaller(self): sliced = utils.newest_slice(items, count)
"""Test with a count which is exactly one smaller.""" assert list(sliced) == list(expected)
items = range(5)
sliced = utils.newest_slice(items, 4)
assert list(sliced) == [1, 2, 3, 4]
def test_count_equal(self):
"""Test with a count which is just as large as the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 5)
assert list(sliced) == list(items)
def test_count_bigger(self):
"""Test with a count which is one bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 6)
assert list(sliced) == list(items)
def test_count_much_bigger(self):
"""Test with a count which is much bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 50)
assert list(sliced) == list(items)