Merge branch 'master' into feature/directory-browser

This commit is contained in:
Antoni Boucher 2015-08-12 16:57:45 -04:00
commit 77190554cc
61 changed files with 1104 additions and 560 deletions

View File

@ -12,9 +12,8 @@ install:
- C:\Python27\python -u scripts\dev\ci_install.py
test_script:
- C:\Python34\Scripts\tox -e smoke
- C:\Python34\Scripts\tox -e smoke-frozen
- C:\Python34\Scripts\tox -e unittests
- C:\Python34\Scripts\tox -e py34
- C:\Python34\Scripts\tox -e unittests-frozen
- C:\Python34\Scripts\tox -e smoke-frozen
- C:\Python34\Scripts\tox -e pyflakes
- C:\Python34\Scripts\tox -e pylint

View File

@ -12,3 +12,6 @@ exclude_lines =
raise AssertionError
raise NotImplementedError
if __name__ == ["']__main__["']:
[xml]
output=.coverage.xml

2
.gitignore vendored
View File

@ -21,7 +21,7 @@ __pycache__
/.venv
/.coverage
/htmlcov
/coverage.xml
/.coverage.xml
/.tox
/testresults.html
/.cache

View File

@ -17,7 +17,7 @@ install:
- python scripts/dev/ci_install.py
script:
- xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke
- xvfb-run -s "-screen 0 640x480x16" tox -e py34
- tox -e misc
- tox -e pep257
- tox -e pyflakes

View File

@ -48,6 +48,8 @@ Changed
mode and is not hidden anymore.
- `minimal_webkit_testbrowser.py` now has a `--webengine` switch to test
QtWebEngine if it's installed.
- The column width percentages for the completion view now depend on the
completion model.
Fixed
~~~~~
@ -60,6 +62,8 @@ Fixed
- Fixed entering of insert mode when certain disabled text fields were clicked.
- Fixed a crash when using `:set` with `-p` and `!` (invert value)
- Downloads with unknown size are now handled correctly.
- `:navigate increment/decrement` (`<Ctrl-A>`/`<Ctrl-X>`) now handles some
corner-cases better.
Removed
~~~~~~~

View File

@ -143,11 +143,12 @@ Contributors, sorted by the number of commits in descending order:
* Lamar Pavel
* Austin Anderson
* Artur Shaik
* Alexander Cogneau
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Daniel
* Jimmy
* Alexander Cogneau
* Zach-Button
* rikn00
* Patric Schmitz
@ -157,6 +158,7 @@ Contributors, sorted by the number of commits in descending order:
* sbinix
* Tobias Patzl
* Johannes Altmanninger
* Thorsten Wißmann
* Samir Benmendil
* Regina Hug
* Mathias Fussenegger
@ -166,7 +168,6 @@ Contributors, sorted by the number of commits in descending order:
* zwarag
* error800
* Tim Harder
* Thorsten Wißmann
* Thiago Barroso Perrotta
* Matthias Lisin
* Helen Sherwood-Taylor

View File

@ -630,6 +630,8 @@ Syntax: +:tab-focus ['index']+
Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
==== positional arguments
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab.

View File

@ -8,6 +8,7 @@ markers =
osx: Tests which only can run on OS X.
not_frozen: Tests which can't be run if sys.frozen is True.
frozen: Tests which can only be run if sys.frozen is True.
integration: Tests which test a bigger portion of code, run without coverage.
flakes-ignore =
UnusedImport
UnusedVariable

View File

@ -272,7 +272,7 @@ def process_pos_args(args, via_ipc=False, cwd=None):
log.init.debug("Startup URL {}".format(cmd))
try:
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
message.error('current', "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
@ -302,7 +302,7 @@ def _open_startpage(win_id=None):
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
message.error('current', "Error when opening startpage: "
"{}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))

View File

@ -19,7 +19,6 @@
"""Command dispatcher for TabbedBrowser."""
import re
import os
import shlex
import posixpath
@ -304,7 +303,7 @@ class CommandDispatcher:
else:
try:
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
if tab or bg or window:
self._open(url, tab, bg, window)
@ -472,29 +471,10 @@ class CommandDispatcher:
background: Open the link in a new background tab.
window: Open the link in a new window.
"""
encoded = bytes(url.toEncoded()).decode('ascii')
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', encoded)
if not match:
raise cmdexc.CommandError("No number found in URL!")
pre, number, post = match.groups()
if not number:
raise cmdexc.CommandError("No number found in URL!")
try:
val = int(number)
except ValueError:
raise cmdexc.CommandError("Could not parse number '{}'.".format(
number))
if incdec == 'decrement':
if val <= 0:
raise cmdexc.CommandError("Can't decrement {}!".format(val))
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
urlstr = ''.join([pre, str(val), post]).encode('ascii')
new_url = QUrl.fromEncoded(urlstr)
new_url = urlutils.incdec_number(url, incdec)
except urlutils.IncDecError as error:
raise cmdexc.CommandError(error.msg)
self._open(new_url, tab, background, window)
def _navigate_up(self, url, tab, background, window):
@ -889,7 +869,7 @@ class CommandDispatcher:
log.misc.debug("{} contained: '{}'".format(target, text))
try:
url = urlutils.fuzzy_url(text)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@ -898,6 +878,8 @@ class CommandDispatcher:
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
"""Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
Args:
index: The tab index to focus, starting with 1. The special value
`last` focuses the last focused tab.
@ -906,6 +888,9 @@ class CommandDispatcher:
if index == 'last':
self._tab_focus_last()
return
if index is None and count is None:
self.tab_next()
return
try:
idx = cmdutils.arg_or_count(index, count, default=1,
countzero=self._count())
@ -1083,7 +1068,7 @@ class CommandDispatcher:
"""
try:
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)

View File

@ -350,16 +350,20 @@ class NetworkManager(QNetworkAccessManager):
current_url = webview.url()
referer_header_conf = config.get('network', 'referer-header')
if referer_header_conf == 'never':
# Note: using ''.encode('ascii') sends a header with no value,
# instead of no header at all
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
elif (referer_header_conf == 'same-domain' and
current_url.isValid() and
not urlutils.same_domain(req.url(), current_url)):
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
# If refer_header_conf is set to 'always', we leave the header alone as
# QtWebKit did set it.
try:
if referer_header_conf == 'never':
# Note: using ''.encode('ascii') sends a header with no value,
# instead of no header at all
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
elif (referer_header_conf == 'same-domain' and
not urlutils.same_domain(req.url(), current_url)):
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
# If refer_header_conf is set to 'always', we leave the header
# alone as QtWebKit did set it.
except urlutils.InvalidUrlError:
# req.url() or current_url can be invalid - this happens on
# https://www.playstation.com/ for example.
pass
accept_language = config.get('network', 'accept-language')
if accept_language is not None:

View File

@ -225,7 +225,7 @@ class QuickmarkManager(UrlMarkManager):
urlstr = self.marks[name]
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
raise InvalidUrlError(
"Invalid URL for quickmark {}: {}".format(name, str(e)))
return url

View File

@ -339,8 +339,6 @@ def get_child_frames(startframe):
def focus_elem(frame):
"""Get the focused element in a web frame.
FIXME: Add tests.
Args:
frame: The QWebFrame to search in.
"""

View File

@ -28,6 +28,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate, completer
from qutebrowser.completion.models import base
from qutebrowser.utils import qtutils, objreg, utils
@ -38,15 +39,13 @@ class CompletionView(QTreeView):
Based on QTreeView but heavily customized so root elements show as category
headers, and children show as flat list.
Class attributes:
COLUMN_WIDTHS: A list of column widths, in percent.
Attributes:
enabled: Whether showing the CompletionView is enabled.
_win_id: The ID of the window this CompletionView is associated with.
_height: The height to use for the CompletionView.
_height_perc: Either None or a percentage if height should be relative.
_delegate: The item delegate used.
_column_widths: A list of column widths, in percent.
Signals:
resize_completion: Emitted when the completion should be resized.
@ -82,7 +81,6 @@ class CompletionView(QTreeView):
border: 0px;
}
"""
COLUMN_WIDTHS = (20, 70, 10)
# FIXME style scrollbar
# https://github.com/The-Compiler/qutebrowser/issues/117
@ -103,6 +101,8 @@ class CompletionView(QTreeView):
# FIXME handle new aliases.
# objreg.get('config').changed.connect(self.init_command_completion)
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
style.set_register_stylesheet(self)
@ -128,9 +128,9 @@ class CompletionView(QTreeView):
return utils.get_repr(self)
def _resize_columns(self):
"""Resize the completion columns based on COLUMN_WIDTHS."""
"""Resize the completion columns based on column_widths."""
width = self.size().width()
pixel_widths = [(width * perc // 100) for perc in self.COLUMN_WIDTHS]
pixel_widths = [(width * perc // 100) for perc in self._column_widths]
if self.verticalScrollBar().isVisible():
pixel_widths[-1] -= self.style().pixelMetric(
QStyle.PM_ScrollBarExtent) + 5
@ -203,6 +203,8 @@ class CompletionView(QTreeView):
sel_model.deleteLater()
for i in range(model.rowCount()):
self.expand(model.index(i, 0))
self._column_widths = model.srcmodel.COLUMN_WIDTHS
self._resize_columns()
self.maybe_resize_completion()

View File

@ -39,8 +39,14 @@ class BaseCompletionModel(QStandardItemModel):
Used for showing completions later in the CompletionView. Supports setting
marks and adding new categories/items easily.
Class Attributes:
COLUMN_WIDTHS: The width percentages of the columns used in the
completion view.
"""
COLUMN_WIDTHS = (30, 70, 0)
def __init__(self, parent=None):
super().__init__(parent)
self.setColumnCount(3)

View File

@ -32,6 +32,8 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10)
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sections")
@ -51,6 +53,8 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10)
def __init__(self, section, parent=None):
super().__init__(parent)
cat = self.new_category(section)
@ -104,6 +108,8 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10)
def __init__(self, section, option, parent=None):
super().__init__(parent)
self._section = section

View File

@ -40,6 +40,8 @@ class UrlCompletionModel(base.BaseCompletionModel):
TEXT_COLUMN = 1
TIME_COLUMN = 2
COLUMN_WIDTHS = (40, 50, 10)
def __init__(self, parent=None):
super().__init__(parent)

View File

@ -1236,7 +1236,7 @@ KEY_DATA = collections.OrderedDict([
('tab-move', ['gm']),
('tab-move -', ['gl']),
('tab-move +', ['gr']),
('tab-next', ['J', 'gt']),
('tab-focus', ['J', 'gt']),
('tab-prev', ['K', 'gT']),
('tab-clone', ['gC']),
('reload', ['r']),

View File

@ -803,8 +803,8 @@ class File(BaseType):
value = os.path.expandvars(value)
if not os.path.isabs(value):
cfgdir = standarddir.config()
if cfgdir is not None:
return os.path.join(cfgdir, value)
assert cfgdir is not None
return os.path.join(cfgdir, value)
return value
def validate(self, value):
@ -1113,7 +1113,7 @@ class FuzzyUrl(BaseType):
from qutebrowser.utils import urlutils
try:
self.transform(value)
except urlutils.FuzzyUrlError as e:
except urlutils.InvalidUrlError as e:
raise configexc.ValidationError(value, str(e))
def transform(self, value):

View File

@ -26,7 +26,7 @@ class TextWrapper(textwrap.TextWrapper):
"""Text wrapper customized to be used in configs."""
def __init__(self, *args, **kwargs):
def __init__(self, **kwargs):
kw = {
'width': 72,
'replace_whitespace': False,
@ -36,4 +36,4 @@ class TextWrapper(textwrap.TextWrapper):
'subsequent_indent': '# ',
}
kw.update(kwargs)
super().__init__(*args, **kw)
super().__init__(**kw)

View File

@ -55,9 +55,11 @@ class TextBase(QLabel):
Args:
width: The maximal width the text should take.
"""
if self.text is not None:
if self.text():
self._elided_text = self.fontMetrics().elidedText(
self.text(), self._elidemode, width, Qt.TextShowMnemonic)
else:
self._elided_text = ''
def setText(self, txt):
"""Extend QLabel::setText.

View File

@ -25,7 +25,7 @@ import functools
import datetime
import contextlib
from PyQt5.QtCore import QEvent, QMetaMethod, QObject
from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, utils, qtutils, objreg
@ -56,7 +56,7 @@ def log_signals(obj):
dbg = dbg_signal(signal, args)
try:
r = repr(obj)
except RuntimeError:
except RuntimeError: # pragma: no cover
r = '<deleted>'
log.signals.debug("Signal in {}: {}".format(r, dbg))
@ -68,8 +68,9 @@ def log_signals(obj):
qtutils.ensure_valid(meta_method)
if meta_method.methodType() == QMetaMethod.Signal:
name = bytes(meta_method.name()).decode('ascii')
signal = getattr(obj, name)
signal.connect(functools.partial(log_slot, obj, signal))
if name != 'destroyed':
signal = getattr(obj, name)
signal.connect(functools.partial(log_slot, obj, signal))
if inspect.isclass(obj):
old_init = obj.__init__
@ -105,19 +106,21 @@ def qenum_key(base, value, add_base=False, klass=None):
klass = value.__class__
if klass == int:
raise TypeError("Can't guess enum class of an int!")
try:
idx = klass.staticMetaObject.indexOfEnumerator(klass.__name__)
idx = base.staticMetaObject.indexOfEnumerator(klass.__name__)
ret = base.staticMetaObject.enumerator(idx).valueToKey(value)
except AttributeError:
idx = -1
if idx != -1:
ret = klass.staticMetaObject.enumerator(idx).valueToKey(value)
else:
ret = None
if ret is None:
for name, obj in vars(base).items():
if isinstance(obj, klass) and obj == value:
ret = name
break
else:
ret = '0x{:04x}'.format(int(value))
if add_base and hasattr(base, '__name__'):
return '.'.join([base.__name__, ret])
else:
@ -177,7 +180,7 @@ def signal_name(sig):
return m.group(1)
def _format_args(args=None, kwargs=None):
def format_args(args=None, kwargs=None):
"""Format a list of arguments/kwargs to a function-call like string."""
if args is not None:
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
@ -199,7 +202,7 @@ def dbg_signal(sig, args):
Return:
A human-readable string representation of signal/args.
"""
return '{}({})'.format(signal_name(sig), _format_args(args))
return '{}({})'.format(signal_name(sig), format_args(args))
def format_call(func, args=None, kwargs=None, full=True):
@ -218,7 +221,7 @@ def format_call(func, args=None, kwargs=None, full=True):
name = utils.qualname(func)
else:
name = func.__name__
return '{}({})'.format(name, _format_args(args, kwargs))
return '{}({})'.format(name, format_args(args, kwargs))
@contextlib.contextmanager
@ -247,25 +250,30 @@ def _get_widgets():
def _get_pyqt_objects(lines, obj, depth=0):
"""Recursive method for get_all_objects to get Qt objects."""
for kid in obj.findChildren(QObject):
for kid in obj.findChildren(QObject, '', Qt.FindDirectChildrenOnly):
lines.append(' ' * depth + repr(kid))
_get_pyqt_objects(lines, kid, depth + 1)
def get_all_objects():
def get_all_objects(start_obj=None):
"""Get all children of an object recursively as a string."""
output = ['']
widget_lines = _get_widgets()
widget_lines = [' ' + e for e in widget_lines]
widget_lines.insert(0, "Qt widgets - {} objects".format(
widget_lines.insert(0, "Qt widgets - {} objects:".format(
len(widget_lines)))
output += widget_lines
if start_obj is None:
start_obj = QApplication.instance()
pyqt_lines = []
_get_pyqt_objects(pyqt_lines, QApplication.instance())
_get_pyqt_objects(pyqt_lines, start_obj)
pyqt_lines = [' ' + e for e in pyqt_lines]
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(
len(pyqt_lines)))
output += pyqt_lines
output += ['']
output += pyqt_lines
output += objreg.dump_objects()
return '\n'.join(output)

View File

@ -37,6 +37,22 @@ from qutebrowser.commands import cmdexc
# https://github.com/The-Compiler/qutebrowser/issues/108
class InvalidUrlError(ValueError):
"""Error raised if a function got an invalid URL.
Inherits ValueError because that was the exception originally used for
that, so there still might be some code around which checks for that.
"""
def __init__(self, url):
if url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url
self.msg = get_errstring(url)
super().__init__(self.msg)
def _parse_search_term(s):
"""Get a search engine name and search term from a string.
@ -185,7 +201,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
qtutils.ensure_valid(url)
else:
if not url.isValid():
raise FuzzyUrlError("Invalid URL '{}'!".format(urlstr), url)
raise InvalidUrlError(url)
return url
@ -355,7 +371,7 @@ def host_tuple(url):
This is suitable to identify a connection, e.g. for SSL errors.
"""
if not url.isValid():
raise ValueError(get_errstring(url))
raise InvalidUrlError(url)
scheme, host, port = url.scheme(), url.host(), url.port()
assert scheme
if not host:
@ -405,9 +421,9 @@ def same_domain(url1, url2):
True if the domains are the same, False otherwise.
"""
if not url1.isValid():
raise ValueError(get_errstring(url1))
raise InvalidUrlError(url1)
if not url2.isValid():
raise ValueError(get_errstring(url2))
raise InvalidUrlError(url2)
suffix1 = url1.topLevelDomain()
suffix2 = url2.topLevelDomain()
@ -422,24 +438,57 @@ def same_domain(url1, url2):
return domain1 == domain2
class FuzzyUrlError(Exception):
class IncDecError(Exception):
"""Exception raised by fuzzy_url on problems.
"""Exception raised by incdec_number on problems.
Attributes:
msg: The error message to use.
msg: The error message.
url: The QUrl which caused the error.
"""
def __init__(self, msg, url=None):
def __init__(self, msg, url):
super().__init__(msg)
if url is not None and url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url
self.msg = msg
def __str__(self):
if self.url is None or not self.url.errorString():
return self.msg
else:
return '{}: {}'.format(self.msg, self.url.errorString())
return '{}: {}'.format(self.msg, self.url.toString())
def incdec_number(url, incdec):
"""Find a number in the url and increment or decrement it.
Args:
url: The current url
incdec: Either 'increment' or 'decrement'
Return:
The new url with the number incremented/decremented.
Raises IncDecError if the url contains no number.
"""
if not url.isValid():
raise InvalidUrlError(url)
path = url.path()
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', path)
if not match:
raise IncDecError("No number found in URL!", url)
pre, number, post = match.groups()
# This should always succeed because we match \d+
val = int(number)
if incdec == 'decrement':
if val <= 0:
raise IncDecError("Can't decrement {}!".format(val), url)
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
new_path = ''.join([pre, str(val), post])
# Make a copy of the QUrl so we don't modify the original
new_url = QUrl(url)
new_url.setPath(new_path)
return new_url

View File

@ -20,6 +20,7 @@
"""Enforce perfect coverage on some files."""
import os
import sys
import os.path
@ -37,6 +38,8 @@ PERFECT_FILES = [
'qutebrowser/browser/tabhistory.py',
'qutebrowser/browser/http.py',
'qutebrowser/browser/rfc6266.py',
'qutebrowser/browser/webelem.py',
'qutebrowser/browser/network/schemehandler.py',
'qutebrowser/misc/readline.py',
'qutebrowser/misc/split.py',
@ -45,10 +48,12 @@ PERFECT_FILES = [
'qutebrowser/mainwindow/statusbar/percentage.py',
'qutebrowser/mainwindow/statusbar/progress.py',
'qutebrowser/mainwindow/statusbar/tabindex.py',
'qutebrowser/mainwindow/statusbar/textbase.py',
'qutebrowser/config/configtypes.py',
'qutebrowser/config/configdata.py',
'qutebrowser/config/configexc.py',
'qutebrowser/config/textwrapper.py',
'qutebrowser/utils/qtutils.py',
'qutebrowser/utils/standarddir.py',
@ -56,6 +61,8 @@ PERFECT_FILES = [
'qutebrowser/utils/usertypes.py',
'qutebrowser/utils/utils.py',
'qutebrowser/utils/version.py',
'qutebrowser/utils/debug.py',
'qutebrowser/utils/jinja.py',
]
@ -67,10 +74,23 @@ def main():
"""
utils.change_cwd()
if sys.platform != 'linux':
print("Skipping coverage checks on non-Linux system.")
sys.exit()
elif '-k' in sys.argv[1:]:
print("Skipping coverage checks because -k is given.")
sys.exit()
elif '-m' in sys.argv[1:]:
print("Skipping coverage checks because -m is given.")
sys.exit()
elif any(arg.startswith('tests' + os.sep) for arg in sys.argv[1:]):
print("Skipping coverage checks because a filename is given.")
sys.exit()
for path in PERFECT_FILES:
assert os.path.exists(os.path.join(*path.split('/'))), path
with open('coverage.xml', encoding='utf-8') as f:
with open('.coverage.xml', encoding='utf-8') as f:
tree = ElementTree.parse(f)
classes = tree.getroot().findall('./packages/package/classes/class')
@ -101,6 +121,8 @@ def main():
print("{} has 100% coverage but is not in PERFECT_FILES!".format(
filename))
os.remove('.coverage.xml')
return status

View File

@ -52,6 +52,7 @@ def main():
'redefined-outer-name',
'unused-argument',
'missing-docstring',
'protected-access',
# https://bitbucket.org/logilab/pylint/issue/511/
'undefined-variable',
]

View File

@ -24,7 +24,6 @@ import logging
import pytest
from qutebrowser.browser import http
from qutebrowser.utils import log
DEFAULT_NAME = 'qutebrowser-download'
@ -56,7 +55,7 @@ class HeaderChecker:
"""Check if the passed header is ignored."""
reply = self.stubs.FakeNetworkReply(
headers={'Content-Disposition': header})
with self.caplog.atLevel(logging.ERROR, logger=log.rfc6266.name):
with self.caplog.atLevel(logging.ERROR, 'rfc6266'):
# with self.assertLogs(log.rfc6266, logging.ERROR):
cd_inline, cd_filename = http.parse_content_disposition(reply)
assert cd_filename == DEFAULT_NAME

View File

@ -19,6 +19,8 @@
"""Hypothesis tests for qutebrowser.browser.http."""
import logging
import pytest
import hypothesis
from hypothesis import strategies
@ -35,11 +37,12 @@ from qutebrowser.browser import http, rfc6266
'attachment; filename*={}',
])
@hypothesis.given(strategies.text(alphabet=[chr(x) for x in range(255)]))
def test_parse_content_disposition(template, stubs, s):
def test_parse_content_disposition(caplog, template, stubs, s):
"""Test parsing headers based on templates which hypothesis completes."""
header = template.format(s)
reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header})
http.parse_content_disposition(reply)
with caplog.atLevel(logging.ERROR, 'rfc6266'):
http.parse_content_disposition(reply)
@hypothesis.given(strategies.binary())

View File

@ -0,0 +1,35 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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 browser.network.schemehandler."""
import pytest
from qutebrowser.browser.network import schemehandler
def test_init():
handler = schemehandler.SchemeHandler(0)
assert handler._win_id == 0
def test_create_request():
handler = schemehandler.SchemeHandler(0)
with pytest.raises(NotImplementedError):
handler.createRequest(None, None, None)

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for the webelement utils."""
from unittest import mock
@ -334,6 +332,14 @@ class TestIsVisible:
def frame(self, stubs):
return stubs.FakeWebFrame(QRect(0, 0, 100, 100))
def test_invalid_frame_geometry(self, stubs):
"""Test with an invalid frame geometry."""
rect = QRect(0, 0, 0, 0)
assert not rect.isValid()
frame = stubs.FakeWebFrame(rect)
elem = get_webelem(QRect(0, 0, 10, 10), frame)
assert not elem.is_visible(frame)
def test_invalid_invisible(self, frame):
"""Test elements with an invalid geometry which are invisible."""
elem = get_webelem(QRect(0, 0, 0, 0), frame)
@ -460,6 +466,67 @@ class TestIsVisibleIframe:
assert not objects.elems[2].is_visible(objects.frame)
assert objects.elems[3].is_visible(objects.frame)
@pytest.fixture
def invalid_objects(self, stubs):
"""Set up the following base situation:
0, 0 300, 0
##############################
# #
0,10 # iframe 100,10 #
#********** #
#* e * elems[0]: 10, 10 in iframe (visible)
#* * #
#* * #
#********** #
0,110 #. .100,110 #
#. . #
#. e . elems[2]: 20,150 in iframe (not visible)
#.......... #
##############################
300, 0 300, 300
Returns an Objects namedtuple with frame/iframe/elems attributes.
"""
frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)
assert frame.geometry().contains(iframe.geometry())
elems = [
get_webelem(QRect(10, 10, 0, 0), iframe),
get_webelem(QRect(20, 150, 0, 0), iframe),
]
for e in elems:
assert not e.geometry().isValid()
return self.Objects(frame=frame, iframe=iframe, elems=elems)
def test_invalid_visible(self, invalid_objects):
"""Test elements with an invalid geometry which are visible.
This seems to happen sometimes in the real world, with real elements
which *are* visible, but don't have a valid geometry.
"""
elem = invalid_objects.elems[0]
assert elem.is_visible(invalid_objects.frame)
def test_invalid_invisible(self, invalid_objects):
"""Test elements with an invalid geometry which are invisible."""
assert not invalid_objects.elems[1].is_visible(invalid_objects.frame)
def test_focus_element(stubs):
"""Test getting focus element with a fake frame/element.
Testing this with a real webpage is almost impossible because the window
and the element would have focus, which is hard to achieve consistently in
a test.
"""
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100))
elem = get_webelem()
frame.focus_elem = elem._elem
assert webelem.focus_elem(frame)._elem is elem._elem
class TestRectOnView:

View File

@ -0,0 +1,53 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Alexander Cogneau <alexander.cogneau@gmail.com>
#
# 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.completion.models column widths"""
import pytest
from qutebrowser.completion.models.base import BaseCompletionModel
from qutebrowser.completion.models.configmodel import (
SettingOptionCompletionModel, SettingSectionCompletionModel,
SettingValueCompletionModel)
from qutebrowser.completion.models.miscmodels import (
CommandCompletionModel, HelpCompletionModel, QuickmarkCompletionModel,
BookmarkCompletionModel, SessionCompletionModel)
from qutebrowser.completion.models.urlmodel import UrlCompletionModel
class TestColumnWidths:
"""Tests for the column widths of the completion models"""
CLASSES = [BaseCompletionModel, SettingOptionCompletionModel,
SettingOptionCompletionModel, SettingSectionCompletionModel,
SettingValueCompletionModel, CommandCompletionModel,
HelpCompletionModel, QuickmarkCompletionModel,
BookmarkCompletionModel, SessionCompletionModel,
UrlCompletionModel]
@pytest.mark.parametrize("model", CLASSES)
def test_list_size(self, model):
"""Test if there are 3 items in the COLUMN_WIDTHS property"""
assert len(model.COLUMN_WIDTHS) == 3
@pytest.mark.parametrize("model", CLASSES)
def test_column_width_sum(self, model):
"""Test if the sum of the widths asserts to 100"""
assert sum(model.COLUMN_WIDTHS) == 100

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.config.config."""
import os
@ -234,10 +232,12 @@ class TestKeyConfigParser:
assert new == new_expected
@pytest.mark.integration
class TestDefaultConfig:
"""Test validating of the default config."""
@pytest.mark.usefixtures('qapp')
def test_default_config(self):
"""Test validating of the default config."""
conf = config.ConfigManager(None, None)
@ -254,6 +254,7 @@ class TestDefaultConfig:
runner.parse(cmd, aliases=False)
@pytest.mark.integration
class TestConfigInit:
"""Test initializing of the config."""
@ -272,8 +273,10 @@ class TestConfigInit:
objreg.register('save-manager', mock.MagicMock())
args = argparse.Namespace(relaxed_config=False)
objreg.register('args', args)
old_standarddir_args = standarddir._args
yield
objreg.global_registry.clear()
standarddir._args = old_standarddir_args
def test_config_none(self, monkeypatch):
"""Test initializing with config path set to None."""

View File

@ -16,13 +16,12 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.config.configtypes."""
import re
import collections
import itertools
import os.path
import base64
import pytest
@ -1076,6 +1075,7 @@ class TestRegexList:
assert klass().transform(val) == expected
@pytest.mark.usefixtures('qapp')
class TestFileAndUserStyleSheet:
"""Test File/UserStyleSheet."""
@ -1146,6 +1146,26 @@ class TestFileAndUserStyleSheet:
with pytest.raises(configexc.ValidationError):
configtypes.File().validate('foobar')
@pytest.mark.parametrize('configtype, value, raises', [
(configtypes.File(), 'foobar', True),
(configtypes.UserStyleSheet(), 'foobar', False),
(configtypes.UserStyleSheet(), '\ud800', True),
])
def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype,
value, raises):
"""Test with a relative path and standarddir.config returning None."""
monkeypatch.setattr(
'qutebrowser.config.configtypes.standarddir.config',
lambda: 'this/does/not/exist')
os_mock.path.isabs.return_value = False
os_mock.path.isfile.side_effect = os.path.isfile
if raises:
with pytest.raises(configexc.ValidationError):
configtype.validate(value)
else:
configtype.validate(value)
def test_validate_expanduser(self, klass, os_mock):
"""Test if validate expands the user correctly."""
os_mock.path.isfile.side_effect = (lambda path:

View File

@ -0,0 +1,38 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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 config.textwrapper."""
from qutebrowser.config import textwrapper
def test_default_args():
wrapper = textwrapper.TextWrapper()
assert wrapper.width == 72
assert not wrapper.replace_whitespace
assert not wrapper.break_long_words
assert not wrapper.break_on_hyphens
assert wrapper.initial_indent == '# '
assert wrapper.subsequent_indent == '# '
def test_custom_args():
wrapper = textwrapper.TextWrapper(drop_whitespace=False)
assert wrapper.width == 72
assert not wrapper.drop_whitespace

View File

@ -23,22 +23,24 @@ import os
import sys
import collections
import itertools
import logging
import pytest
import stubs as stubsmod
import logfail
from qutebrowser.config import configexc
from qutebrowser.utils import objreg, usertypes
@pytest.fixture(scope='session', autouse=True)
def app_and_logging(qapp):
"""Initialize a QApplication and logging.
This ensures that a QApplication is created and used by all tests.
"""
from log import init
init()
@pytest.yield_fixture(scope='session', autouse=True)
def fail_on_logging():
handler = logfail.LogFailHandler()
logging.getLogger().addHandler(handler)
yield
logging.getLogger().removeHandler(handler)
handler.close()
@pytest.fixture(scope='session')
@ -58,7 +60,7 @@ def unicode_encode_err():
@pytest.fixture(scope='session')
def qnam():
def qnam(qapp):
"""Session-wide QNetworkAccessManager."""
from PyQt5.QtNetwork import QNetworkAccessManager
nam = QNetworkAccessManager()
@ -118,8 +120,14 @@ def pytest_collection_modifyitems(items):
http://pytest.org/latest/plugins.html
"""
for item in items:
if 'qtbot' in getattr(item, 'fixturenames', ()):
if 'qapp' in getattr(item, 'fixturenames', ()):
item.add_marker('gui')
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
if 'CI' in os.environ:
raise Exception("No display available on CI!")
skip_marker = pytest.mark.skipif(
True, reason="No DISPLAY available")
item.add_marker(skip_marker)
def _generate_cmdline_tests():

View File

@ -67,12 +67,14 @@ def caret_tester(js_tester):
return CaretTester(js_tester)
@pytest.mark.integration
def test_simple(caret_tester):
"""Test with a simple (one-line) HTML text."""
caret_tester.js.load('position_caret/simple.html')
caret_tester.check()
@pytest.mark.integration
def test_scrolled_down(caret_tester):
"""Test with multiple text blocks with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down.html')
@ -81,6 +83,7 @@ def test_scrolled_down(caret_tester):
caret_tester.check()
@pytest.mark.integration
@pytest.mark.parametrize('style', ['visibility: hidden', 'display: none'])
def test_invisible(caret_tester, style):
"""Test with hidden text elements."""
@ -88,6 +91,7 @@ def test_invisible(caret_tester, style):
caret_tester.check()
@pytest.mark.integration
def test_scrolled_down_img(caret_tester):
"""Test with an image at the top with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down_img.html')

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for BaseKeyParser."""
import sys
@ -29,7 +27,6 @@ from PyQt5.QtCore import Qt
import pytest
from qutebrowser.keyinput import basekeyparser
from qutebrowser.utils import log
CONFIG = {'input': {'timeout': 100}}
@ -107,7 +104,7 @@ class TestSpecialKeys:
def setup(self, caplog, fake_keyconfig):
self.kp = basekeyparser.BaseKeyParser(0)
self.kp.execute = mock.Mock()
with caplog.atLevel(logging.WARNING, log.keyboard.name):
with caplog.atLevel(logging.WARNING, 'keyboard'):
# Ignoring keychain 'ccc' in mode 'test' because keychains are not
# supported there.
self.kp.read_config('test')
@ -186,7 +183,7 @@ class TestKeyChain:
self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None)
assert self.kp._keystring == ''
def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub,
def test_ambiguous_keychain(self, qapp, fake_keyevent_factory, config_stub,
monkeypatch):
"""Test ambiguous keychain."""
config_stub.data = CONFIG

View File

@ -39,8 +39,6 @@ class TestsNormalKeyParser:
kp: The NormalKeyParser to be tested.
"""
# pylint: disable=protected-access
@pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig):
"""Set up mocks and read the test config."""

View File

@ -1,68 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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/>.
"""Logging setup for the tests."""
import logging
from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg,
qInstallMessageHandler)
def init():
"""Initialize logging for the tests."""
logging.basicConfig(format='\nLOG %(levelname)s %(name)s '
'%(module)s:%(funcName)s:%(lineno)d %(message)s',
level=logging.WARNING)
logging.captureWarnings(True)
qInstallMessageHandler(qt_message_handler)
def qt_message_handler(msg_type, context, msg):
"""Qt message handler to redirect qWarning etc. to the logging system.
Args:
QtMsgType msg_type: The level of the message.
QMessageLogContext context: The source code location of the message.
msg: The message text.
"""
# Mapping from Qt logging levels to the matching logging module levels.
# Note we map critical to ERROR as it's actually "just" an error, and fatal
# to critical.
qt_to_logging = {
QtDebugMsg: logging.DEBUG,
QtWarningMsg: logging.WARNING,
QtCriticalMsg: logging.ERROR,
QtFatalMsg: logging.CRITICAL,
}
level = qt_to_logging[msg_type]
# There's very similar code in utils.log, but we want it duplicated here
# for the tests.
if context.function is None:
func = 'none'
else:
func = context.function
if context.category is None or context.category == 'default':
name = 'qt'
else:
name = 'qt-' + context.category
logger = logging.getLogger('qt-tests')
record = logger.makeRecord(name, level, context.file, context.line, msg,
None, None, func)
logger.handle(record)

67
tests/logfail.py Normal file
View File

@ -0,0 +1,67 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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/>.
"""Logging handling for the tests."""
import logging
import pytest
try:
import pytest_capturelog
except ImportError:
# When using pytest for pyflakes/pep8/..., the plugin won't be available
# but conftest.py will still be loaded.
#
# However, LogFailHandler.emit will never be used in that case, so we just
# ignore the ImportError.
pass
class LogFailHandler(logging.Handler):
"""A logging handler which makes tests fail on unexpected messages."""
def __init__(self, level=logging.NOTSET, min_level=logging.WARNING):
self._min_level = min_level
super().__init__(level)
def emit(self, record):
logger = logging.getLogger(record.name)
root_logger = logging.getLogger()
for h in root_logger.handlers:
if isinstance(h, pytest_capturelog.CaptureLogHandler):
caplog_handler = h
break
else:
# The CaptureLogHandler is not available anymore during fixture
# teardown, so we ignore logging messages emitted there..
return
if (logger.level == record.levelno or
caplog_handler.level == record.levelno):
# caplog.atLevel(...) was used with the level of this message, i.e.
# it was expected.
return
if record.levelno < self._min_level:
return
pytest.fail("Got logging message on logger {} with level {}: "
"{}!".format(record.name, record.levelname,
record.getMessage()))

View File

@ -50,4 +50,54 @@ def test_elided_text(qtbot, elidemode, check):
label.setText(long_string)
label.resize(100, 50)
label.show()
assert check(label._elided_text) # pylint: disable=protected-access
qtbot.waitForWindowShown(label)
assert check(label._elided_text)
def test_settext_empty(mocker, qtbot):
"""Make sure using setText('') works and runs repaint."""
label = TextBase()
qtbot.add_widget(label)
mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.repaint',
autospec=True)
label.setText('')
label.repaint.assert_called_with()
def test_resize(qtbot):
"""Make sure the elided text is updated when resizing."""
label = TextBase()
qtbot.add_widget(label)
long_string = 'Hello world! ' * 20
label.setText(long_string)
label.show()
qtbot.waitForWindowShown(label)
text_1 = label._elided_text
label.resize(20, 50)
text_2 = label._elided_text
assert text_1 != text_2
def test_text_elide_none(mocker, qtbot):
"""Make sure the text doesn't get elided if it's empty."""
label = TextBase()
qtbot.add_widget(label)
label.setText('')
mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.'
'fontMetrics')
label._update_elided_text(20)
assert not label.fontMetrics.called
def test_unset_text(qtbot):
"""Make sure the text is cleared properly."""
label = TextBase()
qtbot.add_widget(label)
label.setText('foo')
label.setText('')
assert not label._elided_text

View File

@ -19,8 +19,6 @@
"""Tests for qutebrowser.misc.editor."""
# pylint: disable=protected-access
import os
import os.path
import logging
@ -46,7 +44,7 @@ class TestArg:
stubs.fake_qprocess())
self.editor = editor.ExternalEditor(0)
yield
self.editor._cleanup() # pylint: disable=protected-access
self.editor._cleanup()
@pytest.fixture
def stubbed_config(self, config_stub, monkeypatch):
@ -229,18 +227,18 @@ class TestErrorMessage:
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
self.editor = editor.ExternalEditor(0)
yield
self.editor._cleanup() # pylint: disable=protected-access
self.editor._cleanup()
def test_proc_error(self, caplog):
"""Test on_proc_error."""
self.editor.edit("")
with caplog.atLevel(logging.ERROR, 'message'):
with caplog.atLevel(logging.ERROR):
self.editor.on_proc_error(QProcess.Crashed)
assert len(caplog.records()) == 2
def test_proc_return(self, caplog):
"""Test on_proc_finished with a bad exit status."""
self.editor.edit("")
with caplog.atLevel(logging.ERROR, 'message'):
with caplog.atLevel(logging.ERROR):
self.editor.on_proc_closed(1, QProcess.NormalExit)
assert len(caplog.records()) == 3
assert len(caplog.records()) == 2

View File

@ -17,12 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.misc.guiprocess."""
import os
import sys
import textwrap
import logging
import pytest
from PyQt5.QtCore import QProcess
@ -97,6 +97,7 @@ def test_double_start(qtbot, proc):
@pytest.mark.not_frozen
@pytest.mark.skipif(os.name == 'nt', reason="Test is flaky on Windows...")
def test_double_start_finished(qtbot, proc):
"""Test starting a GUIProcess twice (with the first call finished)."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
@ -117,10 +118,13 @@ def test_cmd_args(fake_proc):
assert (fake_proc.cmd, fake_proc.args) == (cmd, args)
def test_error(qtbot, proc):
# WORKAROUND for https://github.com/pytest-dev/pytest-qt/issues/67
@pytest.mark.skipif(os.name == 'nt', reason="Test is flaky on Windows...")
def test_error(qtbot, proc, caplog):
"""Test the process emitting an error."""
with qtbot.waitSignal(proc.error, raising=True):
proc.start('this_does_not_exist_either', [])
with caplog.atLevel(logging.ERROR, 'message'):
with qtbot.waitSignal(proc.error, raising=True):
proc.start('this_does_not_exist_either', [])
@pytest.mark.not_frozen

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.misc.lineparser."""
import io

View File

@ -19,8 +19,6 @@
"""Tests for qutebrowser.misc.readline."""
# pylint: disable=protected-access
import re
import inspect

View File

@ -42,7 +42,11 @@ class FakeKeyEvent:
class FakeWebFrame:
"""A stub for QWebFrame."""
"""A stub for QWebFrame.
Attributes:
focus_elem: The 'focused' element.
"""
def __init__(self, geometry, scroll=None, parent=None):
"""Constructor.
@ -57,6 +61,16 @@ class FakeWebFrame:
self.geometry = mock.Mock(return_value=geometry)
self.scrollPosition = mock.Mock(return_value=scroll)
self.parentFrame = mock.Mock(return_value=parent)
self.focus_elem = None
def findFirstElement(self, selector):
if selector == '*:focus':
if self.focus_elem is not None:
return self.focus_elem
else:
raise Exception("Trying to get focus element but it's unset!")
else:
raise Exception("Unknown selector {!r}!".format(selector))
class FakeChildrenFrame:
@ -73,11 +87,14 @@ class FakeQApplication:
"""Stub to insert as QApplication module."""
def __init__(self, style=None):
def __init__(self, style=None, all_widgets=None):
self.instance = mock.Mock(return_value=self)
self.style = mock.Mock(spec=QCommonStyle)
self.style().metaObject().className.return_value = style
self.allWidgets = lambda: all_widgets
class FakeUrl:

65
tests/test_logfail.py Normal file
View File

@ -0,0 +1,65 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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 the LogFailHandler test helper."""
import logging
import pytest
def test_log_debug():
logging.debug('foo')
def test_log_warning():
with pytest.raises(pytest.fail.Exception):
logging.warning('foo')
def test_log_expected(caplog):
with caplog.atLevel(logging.ERROR):
logging.error('foo')
def test_log_expected_logger(caplog):
logger = 'logfail_test_logger'
with caplog.atLevel(logging.ERROR, logger):
logging.getLogger(logger).error('foo')
def test_log_expected_wrong_level(caplog):
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR):
logging.critical('foo')
def test_log_expected_logger_wrong_level(caplog):
logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger):
logging.getLogger(logger).critical('foo')
def test_log_expected_wrong_logger(caplog):
logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger):
logging.error('foo')

View File

@ -1,45 +0,0 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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.debug.log_time."""
import logging
import re
import time
from qutebrowser.utils import debug
def test_log_time(caplog):
"""Test if log_time logs properly."""
logger_name = 'qt-tests'
with caplog.atLevel(logging.DEBUG, logger=logger_name):
with debug.log_time(logging.getLogger(logger_name), action='foobar'):
time.sleep(0.1)
records = caplog.records()
assert len(records) == 1
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(records[0].msg)
assert match
duration = float(match.group(1))
assert 0 < duration < 1

View File

@ -1,64 +0,0 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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.debug.qenum_key."""
import pytest
from PyQt5.QtWidgets import QStyle, QFrame
from qutebrowser.utils import debug
def test_no_metaobj():
"""Test with an enum with no meta-object."""
assert not hasattr(QStyle.PrimitiveElement, 'staticMetaObject')
key = debug.qenum_key(QStyle, QStyle.PE_PanelButtonCommand)
assert key == 'PE_PanelButtonCommand'
def test_metaobj():
"""Test with an enum with meta-object."""
assert hasattr(QFrame, 'staticMetaObject')
key = debug.qenum_key(QFrame, QFrame.Sunken)
assert key == 'Sunken'
def test_add_base():
"""Test with add_base=True."""
key = debug.qenum_key(QFrame, QFrame.Sunken, add_base=True)
assert key == 'QFrame.Sunken'
def test_int_noklass():
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qenum_key(QFrame, 42)
def test_int():
"""Test passing an int with explicit klass given."""
key = debug.qenum_key(QFrame, 0x0030, klass=QFrame.Shadow)
assert key == 'Sunken'
def test_unknown():
"""Test passing an unknown value."""
key = debug.qenum_key(QFrame, 0x1337, klass=QFrame.Shadow)
assert key == '0x1337'

View File

@ -1,77 +0,0 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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.debug.qflags_key.
https://github.com/The-Compiler/qutebrowser/issues/42
"""
import pytest
from PyQt5.QtCore import Qt
from qutebrowser.utils import debug
fixme = pytest.mark.xfail(reason="See issue #42", raises=AssertionError)
@fixme
def test_single():
"""Test with single value."""
flags = debug.qflags_key(Qt, Qt.AlignTop)
assert flags == 'AlignTop'
@fixme
def test_multiple():
"""Test with multiple values."""
flags = debug.qflags_key(Qt, Qt.AlignLeft | Qt.AlignTop)
assert flags == 'AlignLeft|AlignTop'
def test_combined():
"""Test with a combined value."""
flags = debug.qflags_key(Qt, Qt.AlignCenter)
assert flags == 'AlignHCenter|AlignVCenter'
@fixme
def test_add_base():
"""Test with add_base=True."""
flags = debug.qflags_key(Qt, Qt.AlignTop, add_base=True)
assert flags == 'Qt.AlignTop'
def test_int_noklass():
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qflags_key(Qt, 42)
@fixme
def test_int():
"""Test passing an int with explicit klass given."""
flags = debug.qflags_key(Qt, 0x0021, klass=Qt.Alignment)
assert flags == 'AlignLeft|AlignTop'
def test_unknown():
"""Test passing an unknown value."""
flags = debug.qflags_key(Qt, 0x1100, klass=Qt.Alignment)
assert flags == '0x0100|0x1000'

View File

@ -1,52 +0,0 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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/>.
"""Test signal debug output functions."""
import pytest
from qutebrowser.utils import debug
@pytest.fixture
def signal(stubs):
"""Fixture to provide a faked pyqtSignal."""
return stubs.FakeSignal()
def test_signal_name(signal):
"""Test signal_name()."""
assert debug.signal_name(signal) == 'fake'
def test_dbg_signal(signal):
"""Test dbg_signal()."""
assert debug.dbg_signal(signal, [23, 42]) == 'fake(23, 42)'
def test_dbg_signal_eliding(signal):
"""Test eliding in dbg_signal()."""
dbg_signal = debug.dbg_signal(signal, ['x' * 201])
assert dbg_signal == "fake('{}\u2026)".format('x' * 198)
def test_dbg_signal_newline(signal):
"""Test dbg_signal() with a newline."""
dbg_signal = debug.dbg_signal(signal, ['foo\nbar'])
assert dbg_signal == r"fake('foo\nbar')"

252
tests/utils/test_debug.py Normal file
View File

@ -0,0 +1,252 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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.debug."""
import logging
import re
import time
import textwrap
import pytest
from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QObject
from PyQt5.QtWidgets import QStyle, QFrame
from qutebrowser.utils import debug
@debug.log_events
class EventObject(QObject):
pass
def test_log_events(qapp, caplog):
obj = EventObject()
qapp.postEvent(obj, QEvent(QEvent.User))
qapp.processEvents()
records = caplog.records()
assert len(records) == 1
assert records[0].msg == 'Event in test_debug.EventObject: User'
class SignalObject(QObject):
signal1 = pyqtSignal()
signal2 = pyqtSignal(str, str)
def __repr__(self):
"""This is not a nice thing to do, but it makes our tests easier."""
return '<repr>'
@debug.log_signals
class DecoratedSignalObject(SignalObject):
pass
@pytest.fixture(params=[(SignalObject, True), (DecoratedSignalObject, False)])
def signal_obj(request):
klass, wrap = request.param
obj = klass()
if wrap:
debug.log_signals(obj)
return obj
def test_log_signals(caplog, signal_obj):
signal_obj.signal1.emit()
signal_obj.signal2.emit('foo', 'bar')
records = caplog.records()
assert len(records) == 2
assert records[0].msg == 'Signal in <repr>: signal1()'
assert records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
def test_log_time(caplog):
logger_name = 'qt-tests'
with caplog.atLevel(logging.DEBUG, logger_name):
with debug.log_time(logging.getLogger(logger_name), action='foobar'):
time.sleep(0.1)
records = caplog.records()
assert len(records) == 1
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(records[0].msg)
assert match
duration = float(match.group(1))
assert 0 < duration < 1
class TestQEnumKey:
def test_metaobj(self):
"""Make sure the classes we use in the tests have a metaobj or not.
If Qt/PyQt even changes and our tests wouldn't test the full
functionality of qenum_key because of that, this test will tell us.
"""
assert not hasattr(QStyle.PrimitiveElement, 'staticMetaObject')
assert hasattr(QFrame, 'staticMetaObject')
@pytest.mark.parametrize('base, value, klass, expected', [
(QStyle, QStyle.PE_PanelButtonCommand, None, 'PE_PanelButtonCommand'),
(QFrame, QFrame.Sunken, None, 'Sunken'),
(QFrame, 0x0030, QFrame.Shadow, 'Sunken'),
(QFrame, 0x1337, QFrame.Shadow, '0x1337'),
])
def test_qenum_key(self, base, value, klass, expected):
key = debug.qenum_key(base, value, klass=klass)
assert key == expected
def test_add_base(self):
key = debug.qenum_key(QFrame, QFrame.Sunken, add_base=True)
assert key == 'QFrame.Sunken'
def test_int_noklass(self):
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qenum_key(QFrame, 42)
class TestQFlagsKey:
"""Tests for qutebrowser.utils.debug.qflags_key.
https://github.com/The-Compiler/qutebrowser/issues/42
"""
fixme = pytest.mark.xfail(reason="See issue #42", raises=AssertionError)
@pytest.mark.parametrize('base, value, klass, expected', [
fixme((Qt, Qt.AlignTop, None, 'AlignTop')),
fixme((Qt, Qt.AlignLeft | Qt.AlignTop, None, 'AlignLeft|AlignTop')),
(Qt, Qt.AlignCenter, None, 'AlignHCenter|AlignVCenter'),
fixme((Qt, 0x0021, Qt.Alignment, 'AlignLeft|AlignTop')),
(Qt, 0x1100, Qt.Alignment, '0x0100|0x1000'),
])
def test_qflags_key(self, base, value, klass, expected):
flags = debug.qflags_key(base, value, klass=klass)
assert flags == expected
@fixme
def test_add_base(self):
"""Test with add_base=True."""
flags = debug.qflags_key(Qt, Qt.AlignTop, add_base=True)
assert flags == 'Qt.AlignTop'
def test_int_noklass(self):
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qflags_key(Qt, 42)
@pytest.mark.parametrize('signal, expected', [
(SignalObject().signal1, 'signal1'),
(SignalObject().signal2, 'signal2'),
])
def test_signal_name(signal, expected):
assert debug.signal_name(signal) == expected
@pytest.mark.parametrize('args, kwargs, expected', [
([], {}, ''),
(None, None, ''),
(['foo'], None, "'foo'"),
(['foo', 'bar'], None, "'foo', 'bar'"),
(None, {'foo': 'bar'}, "foo='bar'"),
(['foo', 'bar'], {'baz': 'fish'}, "'foo', 'bar', baz='fish'"),
(['x' * 300], None, "'{}".format('x' * 198 + '')),
])
def test_format_args(args, kwargs, expected):
assert debug.format_args(args, kwargs) == expected
def func():
pass
@pytest.mark.parametrize('func, args, kwargs, full, expected', [
(func, None, None, False, 'func()'),
(func, [1, 2], None, False, 'func(1, 2)'),
(func, [1, 2], None, True, 'test_debug.func(1, 2)'),
(func, [1, 2], {'foo': 3}, False, 'func(1, 2, foo=3)'),
])
def test_format_call(func, args, kwargs, full, expected):
assert debug.format_call(func, args, kwargs, full) == expected
@pytest.mark.parametrize('args, expected', [
([23, 42], 'fake(23, 42)'),
(['x' * 201], "fake('{}\u2026)".format('x' * 198)),
(['foo\nbar'], r"fake('foo\nbar')"),
])
def test_dbg_signal(stubs, args, expected):
assert debug.dbg_signal(stubs.FakeSignal(), args) == expected
class TestGetAllObjects:
class Object(QObject):
def __init__(self, name, parent=None):
self._name = name
super().__init__(parent)
def __repr__(self):
return '<{}>'.format(self._name)
def test_get_all_objects(self, stubs, monkeypatch):
# pylint: disable=unused-variable
widgets = [self.Object('Widget 1'), self.Object('Widget 2')]
app = stubs.FakeQApplication(all_widgets=widgets)
monkeypatch.setattr(debug, 'QApplication', app)
root = QObject()
o1 = self.Object('Object 1', root)
o2 = self.Object('Object 2', o1)
o3 = self.Object('Object 3', root)
expected = textwrap.dedent("""
Qt widgets - 2 objects:
<Widget 1>
<Widget 2>
Qt objects - 3 objects:
<Object 1>
<Object 2>
<Object 3>
global object registry - 0 objects:
""").rstrip('\n')
assert debug.get_all_objects(start_obj=root) == expected
@pytest.mark.usefixtures('qapp')
def test_get_all_objects_qapp(self):
objects = debug.get_all_objects()
event_dispatcher = '<PyQt5.QtCore.QAbstractEventDispatcher object at'
session_manager = '<PyQt5.QtGui.QSessionManager object at'
assert event_dispatcher in objects or session_manager in objects

View File

@ -22,6 +22,7 @@
import os.path
import pytest
import jinja2
from qutebrowser.utils import jinja
@ -34,7 +35,7 @@ def patch_read_file(monkeypatch):
if path == os.path.join('html', 'test.html'):
return """Hello {{var}}"""
else:
raise ValueError("Invalid path {}!".format(path))
raise IOError("Invalid path {}!".format(path))
monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file)
@ -47,6 +48,13 @@ def test_simple_template():
assert data == "Hello World"
def test_not_found():
"""Test with a template which does not exist."""
with pytest.raises(jinja2.TemplateNotFound) as excinfo:
jinja.env.get_template('does_not_exist.html')
assert str(excinfo.value) == 'does_not_exist.html'
def test_utf8():
"""Test rendering with an UTF8 template.
@ -59,3 +67,16 @@ def test_utf8():
# https://bitbucket.org/logilab/pylint/issue/490/
data = template.render(var='\u2603') # pylint: disable=no-member
assert data == "Hello \u2603"
@pytest.mark.parametrize('name, expected', [
(None, False),
('foo', False),
('foo.html', True),
('foo.htm', True),
('foo.xml', True),
('blah/bar/foo.html', True),
('foo.bar.html', True),
])
def test_autoescape(name, expected):
assert jinja._guess_autoescape(name) == expected

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.log."""
import logging
@ -204,6 +202,7 @@ class TestRAMHandler:
assert handler.dump_log() == "Two\nThree"
@pytest.mark.integration
class TestInitLog:
"""Tests for init_log."""
@ -238,8 +237,8 @@ class TestHideQtWarning:
def test_unfiltered(self, logger, caplog):
"""Test a message which is not filtered."""
with log.hide_qt_warning("World", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
with log.hide_qt_warning("World", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello World")
assert len(caplog.records()) == 1
record = caplog.records()[0]
@ -248,21 +247,21 @@ class TestHideQtWarning:
def test_filtered_exact(self, logger, caplog):
"""Test a message which is filtered (exact match)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello")
assert not caplog.records()
def test_filtered_start(self, logger, caplog):
"""Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello World")
assert not caplog.records()
def test_filtered_whitespace(self, logger, caplog):
"""Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning(" Hello World ")
assert not caplog.records()

View File

@ -35,7 +35,6 @@ import unittest
import unittest.mock
from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice,
QTimer, QBuffer, QFile, QProcess, QFileDevice)
from PyQt5.QtWidgets import QApplication
from qutebrowser import qutebrowser
from qutebrowser.utils import qtutils
@ -505,19 +504,18 @@ class TestSavefileOpen:
@pytest.mark.parametrize('orgname, expected', [(None, ''), ('test', 'test')])
def test_unset_organization(orgname, expected):
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.
"""
app = QApplication.instance()
app.setOrganizationName(orgname)
assert app.organizationName() == expected # sanity check
qapp.setOrganizationName(orgname)
assert qapp.organizationName() == expected # sanity check
with qtutils.unset_organization():
assert app.organizationName() == ''
assert app.organizationName() == expected
assert qapp.organizationName() == ''
assert qapp.organizationName() == expected
if test_file is not None:
@ -921,6 +919,7 @@ class TestPyQIODevice:
assert str(excinfo.value) == 'Reading failed'
@pytest.mark.usefixtures('qapp')
class TestEventLoop:
"""Tests for EventLoop.
@ -929,8 +928,6 @@ class TestEventLoop:
loop: The EventLoop we're testing.
"""
# pylint: disable=protected-access
def _assert_executing(self):
"""Slot which gets called from timers to be sure the loop runs."""
assert self.loop._executing

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.standarddir."""
import os
@ -29,23 +27,22 @@ import logging
import textwrap
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
import pytest
from qutebrowser.utils import standarddir
@pytest.yield_fixture(autouse=True)
def change_qapp_name():
def change_qapp_name(qapp):
"""Change the name of the QApplication instance.
This changes the applicationName for all tests in this module to
"qutebrowser_test".
"""
old_name = QApplication.instance().applicationName()
QApplication.instance().setApplicationName('qutebrowser_test')
old_name = qapp.applicationName()
qapp.setApplicationName('qutebrowser_test')
yield
QApplication.instance().setApplicationName(old_name)
qapp.setApplicationName(old_name)
@pytest.fixture
@ -271,7 +268,7 @@ class TestInitCacheDirTag:
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'misc'):
with caplog.atLevel(logging.ERROR, 'init'):
standarddir._init_cachedir_tag()
assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG'

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.urlutils."""
import os.path
@ -219,7 +217,7 @@ class TestFuzzyUrl:
@pytest.mark.parametrize('do_search, exception', [
(True, qtutils.QtValueError),
(False, urlutils.FuzzyUrlError),
(False, urlutils.InvalidUrlError),
])
def test_invalid_url(self, do_search, exception, is_url_mock, monkeypatch):
"""Test with an invalid URL."""
@ -469,34 +467,40 @@ def test_host_tuple(qurl, tpl):
assert urlutils.host_tuple(qurl) == tpl
@pytest.mark.parametrize('url, raising, has_err_string', [
(None, False, False),
(QUrl(), False, False),
(QUrl('http://www.example.com/'), True, False),
(QUrl('://'), False, True),
])
def test_fuzzy_url_error(url, raising, has_err_string):
"""Test FuzzyUrlError.
class TestInvalidUrlError:
Args:
url: The URL to pass to FuzzyUrlError.
raising; True if the FuzzyUrlError should raise itself.
has_err_string: Whether the QUrl is expected to have errorString set.
"""
if raising:
expected_exc = ValueError
else:
expected_exc = urlutils.FuzzyUrlError
@pytest.mark.parametrize('url, raising, has_err_string', [
(QUrl(), False, False),
(QUrl('http://www.example.com/'), True, False),
(QUrl('://'), False, True),
])
def test_invalid_url_error(self, url, raising, has_err_string):
"""Test InvalidUrlError.
with pytest.raises(expected_exc) as excinfo:
raise urlutils.FuzzyUrlError("Error message", url)
if not raising:
if has_err_string:
expected_text = "Error message: " + url.errorString()
Args:
url: The URL to pass to InvalidUrlError.
raising; True if the InvalidUrlError should raise itself.
has_err_string: Whether the QUrl is expected to have errorString
set.
"""
if raising:
expected_exc = ValueError
else:
expected_text = "Error message"
assert str(excinfo.value) == expected_text
expected_exc = urlutils.InvalidUrlError
with pytest.raises(expected_exc) as excinfo:
raise urlutils.InvalidUrlError(url)
if not raising:
expected_text = "Invalid URL"
if has_err_string:
expected_text += " - " + url.errorString()
assert str(excinfo.value) == expected_text
def test_value_error_subclass(self):
"""Make sure InvalidUrlError is a ValueError subclass."""
with pytest.raises(ValueError):
raise urlutils.InvalidUrlError(QUrl())
@pytest.mark.parametrize('are_same, url1, url2', [
@ -522,5 +526,72 @@ def test_same_domain(are_same, url1, url2):
])
def test_same_domain_invalid_url(url1, url2):
"""Test same_domain with invalid URLs."""
with pytest.raises(ValueError):
with pytest.raises(urlutils.InvalidUrlError):
urlutils.same_domain(QUrl(url1), QUrl(url2))
class TestIncDecNumber:
"""Tests for urlutils.incdec_number()."""
@pytest.mark.parametrize('url, incdec, output', [
("http://example.com/index1.html", "increment", "http://example.com/index2.html"),
("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"),
("http://bbc.c0.uk:80/story_1", "increment", "http://bbc.c0.uk:80/story_2"),
("http://mydomain.tld/1_%C3%A4", "increment", "http://mydomain.tld/2_%C3%A4"),
("http://example.com/site/5#5", "increment", "http://example.com/site/6#5"),
("http://example.com/index10.html", "decrement", "http://example.com/index9.html"),
("http://foo.bar/folder_1/image_3", "decrement", "http://foo.bar/folder_1/image_2"),
("http://bbc.c0.uk:80/story_1", "decrement", "http://bbc.c0.uk:80/story_0"),
("http://mydomain.tld/2_%C3%A4", "decrement", "http://mydomain.tld/1_%C3%A4"),
("http://example.com/site/5#5", "decrement", "http://example.com/site/4#5"),
])
def test_incdec_number(self, url, incdec, output):
"""Test incdec_number with valid URLs."""
new_url = urlutils.incdec_number(QUrl(url), incdec)
assert new_url == QUrl(output)
@pytest.mark.parametrize('url', [
"http://example.com/long/path/but/no/number",
"http://ex4mple.com/number/in/hostname",
"http://example.com:42/number/in/port",
"http://www2.example.com/number/in/subdomain",
"http://example.com/%C3%B6/urlencoded/data",
"http://example.com/number/in/anchor#5",
"http://www2.ex4mple.com:42/all/of/the/%C3%A4bove#5",
])
def test_no_number(self, url):
"""Test incdec_number with URLs that don't contain a number."""
with pytest.raises(urlutils.IncDecError):
urlutils.incdec_number(QUrl(url), "increment")
def test_number_below_0(self):
"""Test incdec_number with a number that would be below zero
after decrementing."""
with pytest.raises(urlutils.IncDecError):
urlutils.incdec_number(QUrl('http://example.com/page_0.html'),
'decrement')
def test_invalid_url(self):
"""Test if incdec_number rejects an invalid URL."""
with pytest.raises(urlutils.InvalidUrlError):
urlutils.incdec_number(QUrl(""), "increment")
def test_wrong_mode(self):
"""Test if incdec_number rejects a wrong parameter for the incdec
argument."""
valid_url = QUrl("http://example.com/0")
with pytest.raises(ValueError):
urlutils.incdec_number(valid_url, "foobar")
@pytest.mark.parametrize("url, msg, expected_str", [
("http://example.com", "Invalid", "Invalid: http://example.com"),
])
def test_incdec_error(self, url, msg, expected_str):
"""Test IncDecError."""
url = QUrl(url)
with pytest.raises(urlutils.IncDecError) as excinfo:
raise urlutils.IncDecError(msg, url)
assert excinfo.value.url == url
assert str(excinfo.value) == expected_str

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.version."""
import io
@ -105,12 +103,13 @@ class TestGitStr:
commit_file_mock.return_value = 'deadbeef'
assert version._git_str() == 'deadbeef'
def test_frozen_oserror(self, commit_file_mock, monkeypatch):
def test_frozen_oserror(self, caplog, commit_file_mock, monkeypatch):
"""Test with sys.frozen=True and OSError when reading git-commit-id."""
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
raising=False)
commit_file_mock.side_effect = OSError
assert version._git_str() is None
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._git_str() is None
@pytest.mark.not_frozen
def test_normal_successful(self, git_str_subprocess_fake):
@ -130,13 +129,15 @@ class TestGitStr:
commit_file_mock.return_value = '1b4d1dea'
assert version._git_str() == '1b4d1dea'
def test_normal_path_oserror(self, mocker, git_str_subprocess_fake):
def test_normal_path_oserror(self, mocker, git_str_subprocess_fake,
caplog):
"""Test with things raising OSError."""
m = mocker.patch('qutebrowser.utils.version.os')
m.path.join.side_effect = OSError
mocker.patch('qutebrowser.utils.version.utils.read_file',
side_effect=OSError)
assert version._git_str() is None
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._git_str() is None
@pytest.mark.not_frozen
def test_normal_path_nofile(self, monkeypatch, caplog,

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for the NeighborList class."""
from qutebrowser.utils import usertypes

View File

@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog):
"""Test Question.abort() with .emit() raising a TypeError."""
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
signal_mock.emit.side_effect = TypeError
with caplog.atLevel(logging.ERROR):
with caplog.atLevel(logging.ERROR, 'misc'):
question.abort()
assert caplog.records()[0].message == 'Error while aborting question'

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for Timer."""
from qutebrowser.utils import usertypes
@ -74,13 +72,13 @@ def test_start_overflow():
def test_timeout_start(qtbot):
"""Make sure the timer works with start()."""
t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, raising=True):
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
t.start(200)
def test_timeout_set_interval(qtbot):
"""Make sure the timer works with setInterval()."""
t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, raising=True):
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
t.setInterval(200)
t.start()

91
tox.ini
View File

@ -4,23 +4,14 @@
# and then run "tox" from this directory.
[tox]
envlist = smoke,unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
envlist = py34,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
[testenv]
passenv = PYTHON
basepython = python3
[testenv:mkvenv]
commands = {envpython} scripts/link_pyqt.py --tox {envdir}
envdir = {toxinidir}/.venv
usedevelop = true
[testenv:unittests]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
setenv =
QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
PYTEST_QT_API=pyqt5
passenv = PYTHON DISPLAY XAUTHORITY HOME
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI
deps =
-r{toxinidir}/requirements.txt
py==1.4.30
@ -28,18 +19,29 @@ deps =
pytest-capturelog==0.7
pytest-qt==1.5.1
pytest-mock==0.7.0
pytest-html==1.3.2
pytest-html==1.4
hypothesis==1.10.1
hypothesis-pytest==0.15.1
hypothesis-pytest==0.17.0
coverage==3.7.1
pytest-cov==2.0.0
cov-core==1.15.0
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEsw {posargs:tests}
{envpython} -m py.test --strict -rfEsw --cov qutebrowser --cov-report xml --cov-report= {posargs:tests}
{envpython} scripts/dev/check_coverage.py {posargs}
{envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit"
[testenv:mkvenv]
basepython = python3
commands = {envpython} scripts/link_pyqt.py --tox {envdir}
envdir = {toxinidir}/.venv
usedevelop = true
[testenv:unittests-watch]
setenv = {[testenv:unittests]setenv}
passenv = {[testenv:unittests]passenv}
basepython = python3
passenv = {[testenv]passenv}
deps =
{[testenv:unittests]deps}
{[testenv]deps}
pytest-testmon==0.6
pytest-watch==3.2.0
commands =
@ -47,38 +49,32 @@ commands =
{envdir}/bin/ptw -- --testmon --strict -rfEsw {posargs:tests}
[testenv:unittests-frozen]
setenv = {[testenv:unittests]setenv}
passenv = {[testenv:unittests]passenv}
basepython = python3
passenv = {[testenv]passenv}
skip_install = true
deps =
{[testenv:unittests]deps}
{[testenv]deps}
cx_Freeze==4.3.4
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/dev/freeze_tests.py build_exe -b {envdir}/build
{envdir}/build/run-frozen-tests --strict -rfEsw {posargs}
[testenv:coverage]
passenv = PYTHON DISPLAY XAUTHORITY HOME
deps =
{[testenv:unittests]deps}
coverage==3.7.1
pytest-cov==2.0.0
cov-core==1.15.0
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html --cov-report xml {posargs:tests}
{envpython} scripts/dev/check_coverage.py
[testenv:misc]
basepython = python3
# For global .gitignore files
passenv = HOME
deps =
commands =
{envpython} scripts/dev/misc_checks.py git
{envpython} scripts/dev/misc_checks.py vcs
{envpython} scripts/dev/misc_checks.py spelling
[testenv:pylint]
basepython = python3
skip_install = true
setenv = PYTHONPATH={toxinidir}/scripts/dev
passenv =
deps =
-r{toxinidir}/requirements.txt
astroid==1.3.8
@ -93,9 +89,10 @@ commands =
{envpython} scripts/dev/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
[testenv:pep257]
basepython = python3
skip_install = true
deps = pep257==0.6.0
passenv = PYTHON LANG
deps = pep257==0.6.0
# Disabled checks:
# D102: Missing docstring in public method (will be handled by others)
# D103: Missing docstring in public function (will be handled by others)
@ -104,8 +101,10 @@ passenv = PYTHON LANG
commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_*).*\.py'
[testenv:pyflakes]
basepython = python3
# https://github.com/fschulze/pytest-flakes/issues/6
setenv = LANG=en_US.UTF-8
passenv =
deps =
-r{toxinidir}/requirements.txt
py==1.4.30
@ -117,6 +116,8 @@ commands =
{envpython} -m py.test -q --flakes --ignore=tests
[testenv:pep8]
basepython = python3
passenv =
deps =
-r{toxinidir}/requirements.txt
py==1.4.30
@ -128,6 +129,8 @@ commands =
{envpython} -m py.test -q --pep8 --ignore=tests
[testenv:mccabe]
basepython = python3
passenv =
deps =
-r{toxinidir}/requirements.txt
py==1.4.30
@ -139,7 +142,9 @@ commands =
{envpython} -m py.test -q --mccabe --ignore=tests
[testenv:pyroma]
basepython = python3
skip_install = true
passenv =
deps =
pyroma==1.8.2
docutils==0.12
@ -148,7 +153,9 @@ commands =
{envdir}/bin/pyroma .
[testenv:check-manifest]
basepython = python3
skip_install = true
passenv =
deps =
check-manifest==0.25
commands =
@ -156,8 +163,10 @@ commands =
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
[testenv:docs]
basepython = python3
skip_install = true
whitelist_externals = git
passenv =
deps =
-r{toxinidir}/requirements.txt
commands =
@ -166,22 +175,12 @@ commands =
git --no-pager diff --exit-code --stat
{envpython} scripts/asciidoc2html.py {posargs}
[testenv:smoke]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER
deps =
-r{toxinidir}/requirements.txt
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit"
[testenv:smoke-frozen]
setenv = {[testenv:smoke]setenv}
passenv = {[testenv:smoke]passenv}
basepython = python3
passenv = {[testenv]passenv}
skip_install = true
deps =
{[testenv:smoke]deps}
{[testenv]deps}
cx_Freeze==4.3.4
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}