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 - C:\Python27\python -u scripts\dev\ci_install.py
test_script: test_script:
- C:\Python34\Scripts\tox -e smoke - C:\Python34\Scripts\tox -e py34
- C:\Python34\Scripts\tox -e smoke-frozen
- C:\Python34\Scripts\tox -e unittests
- C:\Python34\Scripts\tox -e unittests-frozen - 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 pyflakes
- C:\Python34\Scripts\tox -e pylint - C:\Python34\Scripts\tox -e pylint

View File

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

2
.gitignore vendored
View File

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

View File

@ -17,7 +17,7 @@ install:
- python scripts/dev/ci_install.py - python scripts/dev/ci_install.py
script: 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 misc
- tox -e pep257 - tox -e pep257
- tox -e pyflakes - tox -e pyflakes

View File

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

View File

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

View File

@ -630,6 +630,8 @@ Syntax: +:tab-focus ['index']+
Select the tab given as argument/[count]. Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
==== positional arguments ==== positional arguments
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. * +'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. osx: Tests which only can run on OS X.
not_frozen: Tests which can't be run if sys.frozen is True. 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. 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 = flakes-ignore =
UnusedImport UnusedImport
UnusedVariable UnusedVariable

View File

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

View File

@ -19,7 +19,6 @@
"""Command dispatcher for TabbedBrowser.""" """Command dispatcher for TabbedBrowser."""
import re
import os import os
import shlex import shlex
import posixpath import posixpath
@ -304,7 +303,7 @@ class CommandDispatcher:
else: else:
try: try:
url = urlutils.fuzzy_url(url) url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e: except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
if tab or bg or window: if tab or bg or window:
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@ -472,29 +471,10 @@ class CommandDispatcher:
background: Open the link in a new background tab. background: Open the link in a new background tab.
window: Open the link in a new window. 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: try:
val = int(number) new_url = urlutils.incdec_number(url, incdec)
except ValueError: except urlutils.IncDecError as error:
raise cmdexc.CommandError("Could not parse number '{}'.".format( raise cmdexc.CommandError(error.msg)
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)
self._open(new_url, tab, background, window) self._open(new_url, tab, background, window)
def _navigate_up(self, 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)) log.misc.debug("{} contained: '{}'".format(target, text))
try: try:
url = urlutils.fuzzy_url(text) url = urlutils.fuzzy_url(text)
except urlutils.FuzzyUrlError as e: except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@ -898,6 +878,8 @@ class CommandDispatcher:
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None): def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
"""Select the tab given as argument/[count]. """Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
Args: Args:
index: The tab index to focus, starting with 1. The special value index: The tab index to focus, starting with 1. The special value
`last` focuses the last focused tab. `last` focuses the last focused tab.
@ -906,6 +888,9 @@ class CommandDispatcher:
if index == 'last': if index == 'last':
self._tab_focus_last() self._tab_focus_last()
return return
if index is None and count is None:
self.tab_next()
return
try: try:
idx = cmdutils.arg_or_count(index, count, default=1, idx = cmdutils.arg_or_count(index, count, default=1,
countzero=self._count()) countzero=self._count())
@ -1083,7 +1068,7 @@ class CommandDispatcher:
""" """
try: try:
url = urlutils.fuzzy_url(url) url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e: except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
self._open(url, tab, bg, window) self._open(url, tab, bg, window)

View File

@ -350,16 +350,20 @@ class NetworkManager(QNetworkAccessManager):
current_url = webview.url() current_url = webview.url()
referer_header_conf = config.get('network', 'referer-header') referer_header_conf = config.get('network', 'referer-header')
try:
if referer_header_conf == 'never': if referer_header_conf == 'never':
# Note: using ''.encode('ascii') sends a header with no value, # Note: using ''.encode('ascii') sends a header with no value,
# instead of no header at all # instead of no header at all
req.setRawHeader('Referer'.encode('ascii'), QByteArray()) req.setRawHeader('Referer'.encode('ascii'), QByteArray())
elif (referer_header_conf == 'same-domain' and elif (referer_header_conf == 'same-domain' and
current_url.isValid() and
not urlutils.same_domain(req.url(), current_url)): not urlutils.same_domain(req.url(), current_url)):
req.setRawHeader('Referer'.encode('ascii'), QByteArray()) req.setRawHeader('Referer'.encode('ascii'), QByteArray())
# If refer_header_conf is set to 'always', we leave the header alone as # If refer_header_conf is set to 'always', we leave the header
# QtWebKit did set it. # 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') accept_language = config.get('network', 'accept-language')
if accept_language is not None: if accept_language is not None:

View File

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

View File

@ -339,8 +339,6 @@ def get_child_frames(startframe):
def focus_elem(frame): def focus_elem(frame):
"""Get the focused element in a web frame. """Get the focused element in a web frame.
FIXME: Add tests.
Args: Args:
frame: The QWebFrame to search in. 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.config import config, style
from qutebrowser.completion import completiondelegate, completer from qutebrowser.completion import completiondelegate, completer
from qutebrowser.completion.models import base
from qutebrowser.utils import qtutils, objreg, utils 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 Based on QTreeView but heavily customized so root elements show as category
headers, and children show as flat list. headers, and children show as flat list.
Class attributes:
COLUMN_WIDTHS: A list of column widths, in percent.
Attributes: Attributes:
enabled: Whether showing the CompletionView is enabled. enabled: Whether showing the CompletionView is enabled.
_win_id: The ID of the window this CompletionView is associated with. _win_id: The ID of the window this CompletionView is associated with.
_height: The height to use for the CompletionView. _height: The height to use for the CompletionView.
_height_perc: Either None or a percentage if height should be relative. _height_perc: Either None or a percentage if height should be relative.
_delegate: The item delegate used. _delegate: The item delegate used.
_column_widths: A list of column widths, in percent.
Signals: Signals:
resize_completion: Emitted when the completion should be resized. resize_completion: Emitted when the completion should be resized.
@ -82,7 +81,6 @@ class CompletionView(QTreeView):
border: 0px; border: 0px;
} }
""" """
COLUMN_WIDTHS = (20, 70, 10)
# FIXME style scrollbar # FIXME style scrollbar
# https://github.com/The-Compiler/qutebrowser/issues/117 # https://github.com/The-Compiler/qutebrowser/issues/117
@ -103,6 +101,8 @@ class CompletionView(QTreeView):
# FIXME handle new aliases. # FIXME handle new aliases.
# objreg.get('config').changed.connect(self.init_command_completion) # objreg.get('config').changed.connect(self.init_command_completion)
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate) self.setItemDelegate(self._delegate)
style.set_register_stylesheet(self) style.set_register_stylesheet(self)
@ -128,9 +128,9 @@ class CompletionView(QTreeView):
return utils.get_repr(self) return utils.get_repr(self)
def _resize_columns(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() 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(): if self.verticalScrollBar().isVisible():
pixel_widths[-1] -= self.style().pixelMetric( pixel_widths[-1] -= self.style().pixelMetric(
QStyle.PM_ScrollBarExtent) + 5 QStyle.PM_ScrollBarExtent) + 5
@ -203,6 +203,8 @@ class CompletionView(QTreeView):
sel_model.deleteLater() sel_model.deleteLater()
for i in range(model.rowCount()): for i in range(model.rowCount()):
self.expand(model.index(i, 0)) self.expand(model.index(i, 0))
self._column_widths = model.srcmodel.COLUMN_WIDTHS
self._resize_columns() self._resize_columns()
self.maybe_resize_completion() self.maybe_resize_completion()

View File

@ -39,8 +39,14 @@ class BaseCompletionModel(QStandardItemModel):
Used for showing completions later in the CompletionView. Supports setting Used for showing completions later in the CompletionView. Supports setting
marks and adding new categories/items easily. 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setColumnCount(3) self.setColumnCount(3)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,22 @@ from qutebrowser.commands import cmdexc
# https://github.com/The-Compiler/qutebrowser/issues/108 # 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): def _parse_search_term(s):
"""Get a search engine name and search term from a string. """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) qtutils.ensure_valid(url)
else: else:
if not url.isValid(): if not url.isValid():
raise FuzzyUrlError("Invalid URL '{}'!".format(urlstr), url) raise InvalidUrlError(url)
return url return url
@ -355,7 +371,7 @@ def host_tuple(url):
This is suitable to identify a connection, e.g. for SSL errors. This is suitable to identify a connection, e.g. for SSL errors.
""" """
if not url.isValid(): if not url.isValid():
raise ValueError(get_errstring(url)) raise InvalidUrlError(url)
scheme, host, port = url.scheme(), url.host(), url.port() scheme, host, port = url.scheme(), url.host(), url.port()
assert scheme assert scheme
if not host: if not host:
@ -405,9 +421,9 @@ def same_domain(url1, url2):
True if the domains are the same, False otherwise. True if the domains are the same, False otherwise.
""" """
if not url1.isValid(): if not url1.isValid():
raise ValueError(get_errstring(url1)) raise InvalidUrlError(url1)
if not url2.isValid(): if not url2.isValid():
raise ValueError(get_errstring(url2)) raise InvalidUrlError(url2)
suffix1 = url1.topLevelDomain() suffix1 = url1.topLevelDomain()
suffix2 = url2.topLevelDomain() suffix2 = url2.topLevelDomain()
@ -422,24 +438,57 @@ def same_domain(url1, url2):
return domain1 == domain2 return domain1 == domain2
class FuzzyUrlError(Exception): class IncDecError(Exception):
"""Exception raised by fuzzy_url on problems. """Exception raised by incdec_number on problems.
Attributes: Attributes:
msg: The error message to use. msg: The error message.
url: The QUrl which caused the error. url: The QUrl which caused the error.
""" """
def __init__(self, msg, url=None): def __init__(self, msg, url):
super().__init__(msg) super().__init__(msg)
if url is not None and url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url self.url = url
self.msg = msg self.msg = msg
def __str__(self): def __str__(self):
if self.url is None or not self.url.errorString(): return '{}: {}'.format(self.msg, self.url.toString())
return self.msg
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: else:
return '{}: {}'.format(self.msg, self.url.errorString()) 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.""" """Enforce perfect coverage on some files."""
import os
import sys import sys
import os.path import os.path
@ -37,6 +38,8 @@ PERFECT_FILES = [
'qutebrowser/browser/tabhistory.py', 'qutebrowser/browser/tabhistory.py',
'qutebrowser/browser/http.py', 'qutebrowser/browser/http.py',
'qutebrowser/browser/rfc6266.py', 'qutebrowser/browser/rfc6266.py',
'qutebrowser/browser/webelem.py',
'qutebrowser/browser/network/schemehandler.py',
'qutebrowser/misc/readline.py', 'qutebrowser/misc/readline.py',
'qutebrowser/misc/split.py', 'qutebrowser/misc/split.py',
@ -45,10 +48,12 @@ PERFECT_FILES = [
'qutebrowser/mainwindow/statusbar/percentage.py', 'qutebrowser/mainwindow/statusbar/percentage.py',
'qutebrowser/mainwindow/statusbar/progress.py', 'qutebrowser/mainwindow/statusbar/progress.py',
'qutebrowser/mainwindow/statusbar/tabindex.py', 'qutebrowser/mainwindow/statusbar/tabindex.py',
'qutebrowser/mainwindow/statusbar/textbase.py',
'qutebrowser/config/configtypes.py', 'qutebrowser/config/configtypes.py',
'qutebrowser/config/configdata.py', 'qutebrowser/config/configdata.py',
'qutebrowser/config/configexc.py', 'qutebrowser/config/configexc.py',
'qutebrowser/config/textwrapper.py',
'qutebrowser/utils/qtutils.py', 'qutebrowser/utils/qtutils.py',
'qutebrowser/utils/standarddir.py', 'qutebrowser/utils/standarddir.py',
@ -56,6 +61,8 @@ PERFECT_FILES = [
'qutebrowser/utils/usertypes.py', 'qutebrowser/utils/usertypes.py',
'qutebrowser/utils/utils.py', 'qutebrowser/utils/utils.py',
'qutebrowser/utils/version.py', 'qutebrowser/utils/version.py',
'qutebrowser/utils/debug.py',
'qutebrowser/utils/jinja.py',
] ]
@ -67,10 +74,23 @@ def main():
""" """
utils.change_cwd() 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: for path in PERFECT_FILES:
assert os.path.exists(os.path.join(*path.split('/'))), path 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) tree = ElementTree.parse(f)
classes = tree.getroot().findall('./packages/package/classes/class') 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( print("{} has 100% coverage but is not in PERFECT_FILES!".format(
filename)) filename))
os.remove('.coverage.xml')
return status return status

View File

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

View File

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

View File

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

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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for the webelement utils.""" """Tests for the webelement utils."""
from unittest import mock from unittest import mock
@ -334,6 +332,14 @@ class TestIsVisible:
def frame(self, stubs): def frame(self, stubs):
return stubs.FakeWebFrame(QRect(0, 0, 100, 100)) 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): def test_invalid_invisible(self, frame):
"""Test elements with an invalid geometry which are invisible.""" """Test elements with an invalid geometry which are invisible."""
elem = get_webelem(QRect(0, 0, 0, 0), frame) 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 not objects.elems[2].is_visible(objects.frame)
assert objects.elems[3].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: 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.config.config.""" """Tests for qutebrowser.config.config."""
import os import os
@ -234,10 +232,12 @@ class TestKeyConfigParser:
assert new == new_expected assert new == new_expected
@pytest.mark.integration
class TestDefaultConfig: class TestDefaultConfig:
"""Test validating of the default config.""" """Test validating of the default config."""
@pytest.mark.usefixtures('qapp')
def test_default_config(self): def test_default_config(self):
"""Test validating of the default config.""" """Test validating of the default config."""
conf = config.ConfigManager(None, None) conf = config.ConfigManager(None, None)
@ -254,6 +254,7 @@ class TestDefaultConfig:
runner.parse(cmd, aliases=False) runner.parse(cmd, aliases=False)
@pytest.mark.integration
class TestConfigInit: class TestConfigInit:
"""Test initializing of the config.""" """Test initializing of the config."""
@ -272,8 +273,10 @@ class TestConfigInit:
objreg.register('save-manager', mock.MagicMock()) objreg.register('save-manager', mock.MagicMock())
args = argparse.Namespace(relaxed_config=False) args = argparse.Namespace(relaxed_config=False)
objreg.register('args', args) objreg.register('args', args)
old_standarddir_args = standarddir._args
yield yield
objreg.global_registry.clear() objreg.global_registry.clear()
standarddir._args = old_standarddir_args
def test_config_none(self, monkeypatch): def test_config_none(self, monkeypatch):
"""Test initializing with config path set to None.""" """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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.config.configtypes.""" """Tests for qutebrowser.config.configtypes."""
import re import re
import collections import collections
import itertools import itertools
import os.path
import base64 import base64
import pytest import pytest
@ -1076,6 +1075,7 @@ class TestRegexList:
assert klass().transform(val) == expected assert klass().transform(val) == expected
@pytest.mark.usefixtures('qapp')
class TestFileAndUserStyleSheet: class TestFileAndUserStyleSheet:
"""Test File/UserStyleSheet.""" """Test File/UserStyleSheet."""
@ -1146,6 +1146,26 @@ class TestFileAndUserStyleSheet:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
configtypes.File().validate('foobar') 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): def test_validate_expanduser(self, klass, os_mock):
"""Test if validate expands the user correctly.""" """Test if validate expands the user correctly."""
os_mock.path.isfile.side_effect = (lambda path: 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 sys
import collections import collections
import itertools import itertools
import logging
import pytest import pytest
import stubs as stubsmod import stubs as stubsmod
import logfail
from qutebrowser.config import configexc from qutebrowser.config import configexc
from qutebrowser.utils import objreg, usertypes 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. @pytest.yield_fixture(scope='session', autouse=True)
""" def fail_on_logging():
from log import init handler = logfail.LogFailHandler()
init() logging.getLogger().addHandler(handler)
yield
logging.getLogger().removeHandler(handler)
handler.close()
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@ -58,7 +60,7 @@ def unicode_encode_err():
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def qnam(): def qnam(qapp):
"""Session-wide QNetworkAccessManager.""" """Session-wide QNetworkAccessManager."""
from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtNetwork import QNetworkAccessManager
nam = QNetworkAccessManager() nam = QNetworkAccessManager()
@ -118,8 +120,14 @@ def pytest_collection_modifyitems(items):
http://pytest.org/latest/plugins.html http://pytest.org/latest/plugins.html
""" """
for item in items: for item in items:
if 'qtbot' in getattr(item, 'fixturenames', ()): if 'qapp' in getattr(item, 'fixturenames', ()):
item.add_marker('gui') 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(): def _generate_cmdline_tests():

View File

@ -67,12 +67,14 @@ def caret_tester(js_tester):
return CaretTester(js_tester) return CaretTester(js_tester)
@pytest.mark.integration
def test_simple(caret_tester): def test_simple(caret_tester):
"""Test with a simple (one-line) HTML text.""" """Test with a simple (one-line) HTML text."""
caret_tester.js.load('position_caret/simple.html') caret_tester.js.load('position_caret/simple.html')
caret_tester.check() caret_tester.check()
@pytest.mark.integration
def test_scrolled_down(caret_tester): def test_scrolled_down(caret_tester):
"""Test with multiple text blocks with the viewport scrolled down.""" """Test with multiple text blocks with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down.html') caret_tester.js.load('position_caret/scrolled_down.html')
@ -81,6 +83,7 @@ def test_scrolled_down(caret_tester):
caret_tester.check() caret_tester.check()
@pytest.mark.integration
@pytest.mark.parametrize('style', ['visibility: hidden', 'display: none']) @pytest.mark.parametrize('style', ['visibility: hidden', 'display: none'])
def test_invisible(caret_tester, style): def test_invisible(caret_tester, style):
"""Test with hidden text elements.""" """Test with hidden text elements."""
@ -88,6 +91,7 @@ def test_invisible(caret_tester, style):
caret_tester.check() caret_tester.check()
@pytest.mark.integration
def test_scrolled_down_img(caret_tester): def test_scrolled_down_img(caret_tester):
"""Test with an image at the top with the viewport scrolled down.""" """Test with an image at the top with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down_img.html') 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for BaseKeyParser.""" """Tests for BaseKeyParser."""
import sys import sys
@ -29,7 +27,6 @@ from PyQt5.QtCore import Qt
import pytest import pytest
from qutebrowser.keyinput import basekeyparser from qutebrowser.keyinput import basekeyparser
from qutebrowser.utils import log
CONFIG = {'input': {'timeout': 100}} CONFIG = {'input': {'timeout': 100}}
@ -107,7 +104,7 @@ class TestSpecialKeys:
def setup(self, caplog, fake_keyconfig): def setup(self, caplog, fake_keyconfig):
self.kp = basekeyparser.BaseKeyParser(0) self.kp = basekeyparser.BaseKeyParser(0)
self.kp.execute = mock.Mock() 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 # Ignoring keychain 'ccc' in mode 'test' because keychains are not
# supported there. # supported there.
self.kp.read_config('test') self.kp.read_config('test')
@ -186,7 +183,7 @@ class TestKeyChain:
self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None) self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None)
assert self.kp._keystring == '' 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): monkeypatch):
"""Test ambiguous keychain.""" """Test ambiguous keychain."""
config_stub.data = CONFIG config_stub.data = CONFIG

View File

@ -39,8 +39,6 @@ class TestsNormalKeyParser:
kp: The NormalKeyParser to be tested. kp: The NormalKeyParser to be tested.
""" """
# pylint: disable=protected-access
@pytest.yield_fixture(autouse=True) @pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig): def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig):
"""Set up mocks and read the test config.""" """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.setText(long_string)
label.resize(100, 50) label.resize(100, 50)
label.show() 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.""" """Tests for qutebrowser.misc.editor."""
# pylint: disable=protected-access
import os import os
import os.path import os.path
import logging import logging
@ -46,7 +44,7 @@ class TestArg:
stubs.fake_qprocess()) stubs.fake_qprocess())
self.editor = editor.ExternalEditor(0) self.editor = editor.ExternalEditor(0)
yield yield
self.editor._cleanup() # pylint: disable=protected-access self.editor._cleanup()
@pytest.fixture @pytest.fixture
def stubbed_config(self, config_stub, monkeypatch): def stubbed_config(self, config_stub, monkeypatch):
@ -229,18 +227,18 @@ class TestErrorMessage:
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
self.editor = editor.ExternalEditor(0) self.editor = editor.ExternalEditor(0)
yield yield
self.editor._cleanup() # pylint: disable=protected-access self.editor._cleanup()
def test_proc_error(self, caplog): def test_proc_error(self, caplog):
"""Test on_proc_error.""" """Test on_proc_error."""
self.editor.edit("") self.editor.edit("")
with caplog.atLevel(logging.ERROR, 'message'): with caplog.atLevel(logging.ERROR):
self.editor.on_proc_error(QProcess.Crashed) self.editor.on_proc_error(QProcess.Crashed)
assert len(caplog.records()) == 2 assert len(caplog.records()) == 2
def test_proc_return(self, caplog): def test_proc_return(self, caplog):
"""Test on_proc_finished with a bad exit status.""" """Test on_proc_finished with a bad exit status."""
self.editor.edit("") self.editor.edit("")
with caplog.atLevel(logging.ERROR, 'message'): with caplog.atLevel(logging.ERROR):
self.editor.on_proc_closed(1, QProcess.NormalExit) 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.misc.guiprocess.""" """Tests for qutebrowser.misc.guiprocess."""
import os
import sys import sys
import textwrap import textwrap
import logging
import pytest import pytest
from PyQt5.QtCore import QProcess from PyQt5.QtCore import QProcess
@ -97,6 +97,7 @@ def test_double_start(qtbot, proc):
@pytest.mark.not_frozen @pytest.mark.not_frozen
@pytest.mark.skipif(os.name == 'nt', reason="Test is flaky on Windows...")
def test_double_start_finished(qtbot, proc): def test_double_start_finished(qtbot, proc):
"""Test starting a GUIProcess twice (with the first call finished).""" """Test starting a GUIProcess twice (with the first call finished)."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], raising=True,
@ -117,8 +118,11 @@ def test_cmd_args(fake_proc):
assert (fake_proc.cmd, fake_proc.args) == (cmd, args) 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.""" """Test the process emitting an error."""
with caplog.atLevel(logging.ERROR, 'message'):
with qtbot.waitSignal(proc.error, raising=True): with qtbot.waitSignal(proc.error, raising=True):
proc.start('this_does_not_exist_either', []) proc.start('this_does_not_exist_either', [])

View File

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

View File

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

View File

@ -42,7 +42,11 @@ class FakeKeyEvent:
class FakeWebFrame: class FakeWebFrame:
"""A stub for QWebFrame.""" """A stub for QWebFrame.
Attributes:
focus_elem: The 'focused' element.
"""
def __init__(self, geometry, scroll=None, parent=None): def __init__(self, geometry, scroll=None, parent=None):
"""Constructor. """Constructor.
@ -57,6 +61,16 @@ class FakeWebFrame:
self.geometry = mock.Mock(return_value=geometry) self.geometry = mock.Mock(return_value=geometry)
self.scrollPosition = mock.Mock(return_value=scroll) self.scrollPosition = mock.Mock(return_value=scroll)
self.parentFrame = mock.Mock(return_value=parent) 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: class FakeChildrenFrame:
@ -73,11 +87,14 @@ class FakeQApplication:
"""Stub to insert as QApplication module.""" """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.instance = mock.Mock(return_value=self)
self.style = mock.Mock(spec=QCommonStyle) self.style = mock.Mock(spec=QCommonStyle)
self.style().metaObject().className.return_value = style self.style().metaObject().className.return_value = style
self.allWidgets = lambda: all_widgets
class FakeUrl: 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 os.path
import pytest import pytest
import jinja2
from qutebrowser.utils import jinja from qutebrowser.utils import jinja
@ -34,7 +35,7 @@ def patch_read_file(monkeypatch):
if path == os.path.join('html', 'test.html'): if path == os.path.join('html', 'test.html'):
return """Hello {{var}}""" return """Hello {{var}}"""
else: else:
raise ValueError("Invalid path {}!".format(path)) raise IOError("Invalid path {}!".format(path))
monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file) monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file)
@ -47,6 +48,13 @@ def test_simple_template():
assert data == "Hello World" 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(): def test_utf8():
"""Test rendering with an UTF8 template. """Test rendering with an UTF8 template.
@ -59,3 +67,16 @@ def test_utf8():
# https://bitbucket.org/logilab/pylint/issue/490/ # https://bitbucket.org/logilab/pylint/issue/490/
data = template.render(var='\u2603') # pylint: disable=no-member data = template.render(var='\u2603') # pylint: disable=no-member
assert data == "Hello \u2603" 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.log.""" """Tests for qutebrowser.utils.log."""
import logging import logging
@ -204,6 +202,7 @@ class TestRAMHandler:
assert handler.dump_log() == "Two\nThree" assert handler.dump_log() == "Two\nThree"
@pytest.mark.integration
class TestInitLog: class TestInitLog:
"""Tests for init_log.""" """Tests for init_log."""
@ -238,8 +237,8 @@ class TestHideQtWarning:
def test_unfiltered(self, logger, caplog): def test_unfiltered(self, logger, caplog):
"""Test a message which is not filtered.""" """Test a message which is not filtered."""
with log.hide_qt_warning("World", logger='qt-tests'): with log.hide_qt_warning("World", 'qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello World") logger.warning("Hello World")
assert len(caplog.records()) == 1 assert len(caplog.records()) == 1
record = caplog.records()[0] record = caplog.records()[0]
@ -248,21 +247,21 @@ class TestHideQtWarning:
def test_filtered_exact(self, logger, caplog): def test_filtered_exact(self, logger, caplog):
"""Test a message which is filtered (exact match).""" """Test a message which is filtered (exact match)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello") logger.warning("Hello")
assert not caplog.records() assert not caplog.records()
def test_filtered_start(self, logger, caplog): def test_filtered_start(self, logger, caplog):
"""Test a message which is filtered (match at line start).""" """Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning("Hello World") logger.warning("Hello World")
assert not caplog.records() assert not caplog.records()
def test_filtered_whitespace(self, logger, caplog): def test_filtered_whitespace(self, logger, caplog):
"""Test a message which is filtered (match with whitespace).""" """Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, 'qt-tests'):
logger.warning(" Hello World ") logger.warning(" Hello World ")
assert not caplog.records() assert not caplog.records()

View File

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

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.standarddir.""" """Tests for qutebrowser.utils.standarddir."""
import os import os
@ -29,23 +27,22 @@ import logging
import textwrap import textwrap
from PyQt5.QtCore import QStandardPaths from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
import pytest import pytest
from qutebrowser.utils import standarddir from qutebrowser.utils import standarddir
@pytest.yield_fixture(autouse=True) @pytest.yield_fixture(autouse=True)
def change_qapp_name(): def change_qapp_name(qapp):
"""Change the name of the QApplication instance. """Change the name of the QApplication instance.
This changes the applicationName for all tests in this module to This changes the applicationName for all tests in this module to
"qutebrowser_test". "qutebrowser_test".
""" """
old_name = QApplication.instance().applicationName() old_name = qapp.applicationName()
QApplication.instance().setApplicationName('qutebrowser_test') qapp.setApplicationName('qutebrowser_test')
yield yield
QApplication.instance().setApplicationName(old_name) qapp.setApplicationName(old_name)
@pytest.fixture @pytest.fixture
@ -271,7 +268,7 @@ class TestInitCacheDirTag:
monkeypatch.setattr('qutebrowser.utils.standarddir.cache', monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir)) lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError) mocker.patch('builtins.open', side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.atLevel(logging.ERROR, 'init'):
standarddir._init_cachedir_tag() standarddir._init_cachedir_tag()
assert len(caplog.records()) == 1 assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG' 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.urlutils.""" """Tests for qutebrowser.utils.urlutils."""
import os.path import os.path
@ -219,7 +217,7 @@ class TestFuzzyUrl:
@pytest.mark.parametrize('do_search, exception', [ @pytest.mark.parametrize('do_search, exception', [
(True, qtutils.QtValueError), (True, qtutils.QtValueError),
(False, urlutils.FuzzyUrlError), (False, urlutils.InvalidUrlError),
]) ])
def test_invalid_url(self, do_search, exception, is_url_mock, monkeypatch): def test_invalid_url(self, do_search, exception, is_url_mock, monkeypatch):
"""Test with an invalid URL.""" """Test with an invalid URL."""
@ -469,35 +467,41 @@ def test_host_tuple(qurl, tpl):
assert urlutils.host_tuple(qurl) == tpl assert urlutils.host_tuple(qurl) == tpl
@pytest.mark.parametrize('url, raising, has_err_string', [ class TestInvalidUrlError:
(None, False, False),
@pytest.mark.parametrize('url, raising, has_err_string', [
(QUrl(), False, False), (QUrl(), False, False),
(QUrl('http://www.example.com/'), True, False), (QUrl('http://www.example.com/'), True, False),
(QUrl('://'), False, True), (QUrl('://'), False, True),
]) ])
def test_fuzzy_url_error(url, raising, has_err_string): def test_invalid_url_error(self, url, raising, has_err_string):
"""Test FuzzyUrlError. """Test InvalidUrlError.
Args: Args:
url: The URL to pass to FuzzyUrlError. url: The URL to pass to InvalidUrlError.
raising; True if the FuzzyUrlError should raise itself. raising; True if the InvalidUrlError should raise itself.
has_err_string: Whether the QUrl is expected to have errorString set. has_err_string: Whether the QUrl is expected to have errorString
set.
""" """
if raising: if raising:
expected_exc = ValueError expected_exc = ValueError
else: else:
expected_exc = urlutils.FuzzyUrlError expected_exc = urlutils.InvalidUrlError
with pytest.raises(expected_exc) as excinfo: with pytest.raises(expected_exc) as excinfo:
raise urlutils.FuzzyUrlError("Error message", url) raise urlutils.InvalidUrlError(url)
if not raising: if not raising:
expected_text = "Invalid URL"
if has_err_string: if has_err_string:
expected_text = "Error message: " + url.errorString() expected_text += " - " + url.errorString()
else:
expected_text = "Error message"
assert str(excinfo.value) == expected_text 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', [ @pytest.mark.parametrize('are_same, url1, url2', [
(True, 'http://example.com', 'http://www.example.com'), (True, 'http://example.com', 'http://www.example.com'),
@ -522,5 +526,72 @@ def test_same_domain(are_same, url1, url2):
]) ])
def test_same_domain_invalid_url(url1, url2): def test_same_domain_invalid_url(url1, url2):
"""Test same_domain with invalid URLs.""" """Test same_domain with invalid URLs."""
with pytest.raises(ValueError): with pytest.raises(urlutils.InvalidUrlError):
urlutils.same_domain(QUrl(url1), QUrl(url2)) 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.version.""" """Tests for qutebrowser.utils.version."""
import io import io
@ -105,11 +103,12 @@ class TestGitStr:
commit_file_mock.return_value = 'deadbeef' commit_file_mock.return_value = 'deadbeef'
assert version._git_str() == '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.""" """Test with sys.frozen=True and OSError when reading git-commit-id."""
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
raising=False) raising=False)
commit_file_mock.side_effect = OSError commit_file_mock.side_effect = OSError
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._git_str() is None assert version._git_str() is None
@pytest.mark.not_frozen @pytest.mark.not_frozen
@ -130,12 +129,14 @@ class TestGitStr:
commit_file_mock.return_value = '1b4d1dea' commit_file_mock.return_value = '1b4d1dea'
assert version._git_str() == '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.""" """Test with things raising OSError."""
m = mocker.patch('qutebrowser.utils.version.os') m = mocker.patch('qutebrowser.utils.version.os')
m.path.join.side_effect = OSError m.path.join.side_effect = OSError
mocker.patch('qutebrowser.utils.version.utils.read_file', mocker.patch('qutebrowser.utils.version.utils.read_file',
side_effect=OSError) side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._git_str() is None assert version._git_str() is None
@pytest.mark.not_frozen @pytest.mark.not_frozen

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for the NeighborList class.""" """Tests for the NeighborList class."""
from qutebrowser.utils import usertypes 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.""" """Test Question.abort() with .emit() raising a TypeError."""
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted') signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
signal_mock.emit.side_effect = TypeError signal_mock.emit.side_effect = TypeError
with caplog.atLevel(logging.ERROR): with caplog.atLevel(logging.ERROR, 'misc'):
question.abort() question.abort()
assert caplog.records()[0].message == 'Error while aborting question' 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for Timer.""" """Tests for Timer."""
from qutebrowser.utils import usertypes from qutebrowser.utils import usertypes
@ -74,13 +72,13 @@ def test_start_overflow():
def test_timeout_start(qtbot): def test_timeout_start(qtbot):
"""Make sure the timer works with start().""" """Make sure the timer works with start()."""
t = usertypes.Timer() t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, raising=True): with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
t.start(200) t.start(200)
def test_timeout_set_interval(qtbot): def test_timeout_set_interval(qtbot):
"""Make sure the timer works with setInterval().""" """Make sure the timer works with setInterval()."""
t = usertypes.Timer() t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, raising=True): with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
t.setInterval(200) t.setInterval(200)
t.start() t.start()

91
tox.ini
View File

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