diff --git a/.appveyor.yml b/.appveyor.yml index 584bbebf9..a7b22e9f8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -12,9 +12,8 @@ install: - C:\Python27\python -u scripts\dev\ci_install.py test_script: - - C:\Python34\Scripts\tox -e smoke - - C:\Python34\Scripts\tox -e smoke-frozen - - C:\Python34\Scripts\tox -e unittests + - C:\Python34\Scripts\tox -e py34 - C:\Python34\Scripts\tox -e unittests-frozen + - C:\Python34\Scripts\tox -e smoke-frozen - C:\Python34\Scripts\tox -e pyflakes - C:\Python34\Scripts\tox -e pylint diff --git a/.coveragerc b/.coveragerc index 16bebb0cc..07e1f32c2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,3 +12,6 @@ exclude_lines = raise AssertionError raise NotImplementedError if __name__ == ["']__main__["']: + +[xml] +output=.coverage.xml diff --git a/.gitignore b/.gitignore index cc23379f9..a237d558d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ __pycache__ /.venv /.coverage /htmlcov -/coverage.xml +/.coverage.xml /.tox /testresults.html /.cache diff --git a/.travis.yml b/.travis.yml index c5ff88926..d72073926 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - python scripts/dev/ci_install.py script: - - xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke + - xvfb-run -s "-screen 0 640x480x16" tox -e py34 - tox -e misc - tox -e pep257 - tox -e pyflakes diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 08cd555cf..b46791980 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -48,6 +48,8 @@ Changed mode and is not hidden anymore. - `minimal_webkit_testbrowser.py` now has a `--webengine` switch to test QtWebEngine if it's installed. +- The column width percentages for the completion view now depend on the + completion model. Fixed ~~~~~ @@ -60,6 +62,8 @@ Fixed - Fixed entering of insert mode when certain disabled text fields were clicked. - Fixed a crash when using `:set` with `-p` and `!` (invert value) - Downloads with unknown size are now handled correctly. +- `:navigate increment/decrement` (``/``) now handles some + corner-cases better. Removed ~~~~~~~ diff --git a/README.asciidoc b/README.asciidoc index e2fd39a13..c12f8133f 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -143,11 +143,12 @@ Contributors, sorted by the number of commits in descending order: * Lamar Pavel * Austin Anderson * Artur Shaik +* Alexander Cogneau * ZDarian * Peter Vilim * John ShaggyTwoDope Jenkins +* Daniel * Jimmy -* Alexander Cogneau * Zach-Button * rikn00 * Patric Schmitz @@ -157,6 +158,7 @@ Contributors, sorted by the number of commits in descending order: * sbinix * Tobias Patzl * Johannes Altmanninger +* Thorsten Wißmann * Samir Benmendil * Regina Hug * Mathias Fussenegger @@ -166,7 +168,6 @@ Contributors, sorted by the number of commits in descending order: * zwarag * error800 * Tim Harder -* Thorsten Wißmann * Thiago Barroso Perrotta * Matthias Lisin * Helen Sherwood-Taylor diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index a898b9698..012522909 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -630,6 +630,8 @@ Syntax: +:tab-focus ['index']+ Select the tab given as argument/[count]. +If neither count nor index are given, it behaves like tab-next. + ==== positional arguments * +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. diff --git a/pytest.ini b/pytest.ini index 7d374fb9a..c8a7c0aa2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,6 +8,7 @@ markers = osx: Tests which only can run on OS X. not_frozen: Tests which can't be run if sys.frozen is True. frozen: Tests which can only be run if sys.frozen is True. + integration: Tests which test a bigger portion of code, run without coverage. flakes-ignore = UnusedImport UnusedVariable diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 927d43b29..6ca23ba21 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -272,7 +272,7 @@ def process_pos_args(args, via_ipc=False, cwd=None): log.init.debug("Startup URL {}".format(cmd)) try: url = urlutils.fuzzy_url(cmd, cwd, relative=True) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: message.error('current', "Error in startup argument '{}': " "{}".format(cmd, e)) else: @@ -302,7 +302,7 @@ def _open_startpage(win_id=None): for urlstr in config.get('general', 'startpage'): try: url = urlutils.fuzzy_url(urlstr, do_search=False) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: message.error('current', "Error when opening startpage: " "{}".format(e)) tabbed_browser.tabopen(QUrl('about:blank')) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index ff117ca38..1fc2a5bf7 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -19,7 +19,6 @@ """Command dispatcher for TabbedBrowser.""" -import re import os import shlex import posixpath @@ -304,7 +303,7 @@ class CommandDispatcher: else: try: url = urlutils.fuzzy_url(url) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: raise cmdexc.CommandError(e) if tab or bg or window: self._open(url, tab, bg, window) @@ -472,29 +471,10 @@ class CommandDispatcher: background: Open the link in a new background tab. window: Open the link in a new window. """ - encoded = bytes(url.toEncoded()).decode('ascii') - # Get the last number in a string - match = re.match(r'(.*\D|^)(\d+)(.*)', encoded) - if not match: - raise cmdexc.CommandError("No number found in URL!") - pre, number, post = match.groups() - if not number: - raise cmdexc.CommandError("No number found in URL!") try: - val = int(number) - except ValueError: - raise cmdexc.CommandError("Could not parse number '{}'.".format( - number)) - if incdec == 'decrement': - if val <= 0: - raise cmdexc.CommandError("Can't decrement {}!".format(val)) - val -= 1 - elif incdec == 'increment': - val += 1 - else: - raise ValueError("Invalid value {} for indec!".format(incdec)) - urlstr = ''.join([pre, str(val), post]).encode('ascii') - new_url = QUrl.fromEncoded(urlstr) + new_url = urlutils.incdec_number(url, incdec) + except urlutils.IncDecError as error: + raise cmdexc.CommandError(error.msg) self._open(new_url, tab, background, window) def _navigate_up(self, url, tab, background, window): @@ -889,7 +869,7 @@ class CommandDispatcher: log.misc.debug("{} contained: '{}'".format(target, text)) try: url = urlutils.fuzzy_url(text) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: raise cmdexc.CommandError(e) self._open(url, tab, bg, window) @@ -898,6 +878,8 @@ class CommandDispatcher: def tab_focus(self, index: {'type': (int, 'last')}=None, count=None): """Select the tab given as argument/[count]. + If neither count nor index are given, it behaves like tab-next. + Args: index: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. @@ -906,6 +888,9 @@ class CommandDispatcher: if index == 'last': self._tab_focus_last() return + if index is None and count is None: + self.tab_next() + return try: idx = cmdutils.arg_or_count(index, count, default=1, countzero=self._count()) @@ -1083,7 +1068,7 @@ class CommandDispatcher: """ try: url = urlutils.fuzzy_url(url) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: raise cmdexc.CommandError(e) self._open(url, tab, bg, window) diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 7359a2d43..ff08b015a 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -350,16 +350,20 @@ class NetworkManager(QNetworkAccessManager): current_url = webview.url() referer_header_conf = config.get('network', 'referer-header') - if referer_header_conf == 'never': - # Note: using ''.encode('ascii') sends a header with no value, - # instead of no header at all - req.setRawHeader('Referer'.encode('ascii'), QByteArray()) - elif (referer_header_conf == 'same-domain' and - current_url.isValid() and - not urlutils.same_domain(req.url(), current_url)): - req.setRawHeader('Referer'.encode('ascii'), QByteArray()) - # If refer_header_conf is set to 'always', we leave the header alone as - # QtWebKit did set it. + try: + if referer_header_conf == 'never': + # Note: using ''.encode('ascii') sends a header with no value, + # instead of no header at all + req.setRawHeader('Referer'.encode('ascii'), QByteArray()) + elif (referer_header_conf == 'same-domain' and + not urlutils.same_domain(req.url(), current_url)): + req.setRawHeader('Referer'.encode('ascii'), QByteArray()) + # If refer_header_conf is set to 'always', we leave the header + # alone as QtWebKit did set it. + except urlutils.InvalidUrlError: + # req.url() or current_url can be invalid - this happens on + # https://www.playstation.com/ for example. + pass accept_language = config.get('network', 'accept-language') if accept_language is not None: diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 735f05e13..cacc8d9c0 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -225,7 +225,7 @@ class QuickmarkManager(UrlMarkManager): urlstr = self.marks[name] try: url = urlutils.fuzzy_url(urlstr, do_search=False) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: raise InvalidUrlError( "Invalid URL for quickmark {}: {}".format(name, str(e))) return url diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 292570c57..02e4812bb 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -339,8 +339,6 @@ def get_child_frames(startframe): def focus_elem(frame): """Get the focused element in a web frame. - FIXME: Add tests. - Args: frame: The QWebFrame to search in. """ diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index a3bea931a..36de445ac 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -28,6 +28,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate, completer +from qutebrowser.completion.models import base from qutebrowser.utils import qtutils, objreg, utils @@ -38,15 +39,13 @@ class CompletionView(QTreeView): Based on QTreeView but heavily customized so root elements show as category headers, and children show as flat list. - Class attributes: - COLUMN_WIDTHS: A list of column widths, in percent. - Attributes: enabled: Whether showing the CompletionView is enabled. _win_id: The ID of the window this CompletionView is associated with. _height: The height to use for the CompletionView. _height_perc: Either None or a percentage if height should be relative. _delegate: The item delegate used. + _column_widths: A list of column widths, in percent. Signals: resize_completion: Emitted when the completion should be resized. @@ -82,7 +81,6 @@ class CompletionView(QTreeView): border: 0px; } """ - COLUMN_WIDTHS = (20, 70, 10) # FIXME style scrollbar # https://github.com/The-Compiler/qutebrowser/issues/117 @@ -103,6 +101,8 @@ class CompletionView(QTreeView): # FIXME handle new aliases. # objreg.get('config').changed.connect(self.init_command_completion) + self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS + self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) style.set_register_stylesheet(self) @@ -128,9 +128,9 @@ class CompletionView(QTreeView): return utils.get_repr(self) def _resize_columns(self): - """Resize the completion columns based on COLUMN_WIDTHS.""" + """Resize the completion columns based on column_widths.""" width = self.size().width() - pixel_widths = [(width * perc // 100) for perc in self.COLUMN_WIDTHS] + pixel_widths = [(width * perc // 100) for perc in self._column_widths] if self.verticalScrollBar().isVisible(): pixel_widths[-1] -= self.style().pixelMetric( QStyle.PM_ScrollBarExtent) + 5 @@ -203,6 +203,8 @@ class CompletionView(QTreeView): sel_model.deleteLater() for i in range(model.rowCount()): self.expand(model.index(i, 0)) + + self._column_widths = model.srcmodel.COLUMN_WIDTHS self._resize_columns() self.maybe_resize_completion() diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py index 0d4bf7fca..a35c7f063 100644 --- a/qutebrowser/completion/models/base.py +++ b/qutebrowser/completion/models/base.py @@ -39,8 +39,14 @@ class BaseCompletionModel(QStandardItemModel): Used for showing completions later in the CompletionView. Supports setting marks and adding new categories/items easily. + + Class Attributes: + COLUMN_WIDTHS: The width percentages of the columns used in the + completion view. """ + COLUMN_WIDTHS = (30, 70, 0) + def __init__(self, parent=None): super().__init__(parent) self.setColumnCount(3) diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 6d39fed7b..0c45d817b 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -32,6 +32,8 @@ class SettingSectionCompletionModel(base.BaseCompletionModel): # pylint: disable=abstract-method + COLUMN_WIDTHS = (20, 70, 10) + def __init__(self, parent=None): super().__init__(parent) cat = self.new_category("Sections") @@ -51,6 +53,8 @@ class SettingOptionCompletionModel(base.BaseCompletionModel): # pylint: disable=abstract-method + COLUMN_WIDTHS = (20, 70, 10) + def __init__(self, section, parent=None): super().__init__(parent) cat = self.new_category(section) @@ -104,6 +108,8 @@ class SettingValueCompletionModel(base.BaseCompletionModel): # pylint: disable=abstract-method + COLUMN_WIDTHS = (20, 70, 10) + def __init__(self, section, option, parent=None): super().__init__(parent) self._section = section diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 2da1d0b71..ca1ef31fe 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -40,6 +40,8 @@ class UrlCompletionModel(base.BaseCompletionModel): TEXT_COLUMN = 1 TIME_COLUMN = 2 + COLUMN_WIDTHS = (40, 50, 10) + def __init__(self, parent=None): super().__init__(parent) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7a1f4f236..5e40bf7e1 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1236,7 +1236,7 @@ KEY_DATA = collections.OrderedDict([ ('tab-move', ['gm']), ('tab-move -', ['gl']), ('tab-move +', ['gr']), - ('tab-next', ['J', 'gt']), + ('tab-focus', ['J', 'gt']), ('tab-prev', ['K', 'gT']), ('tab-clone', ['gC']), ('reload', ['r']), diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index dd4f19d79..75e05360c 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -803,8 +803,8 @@ class File(BaseType): value = os.path.expandvars(value) if not os.path.isabs(value): cfgdir = standarddir.config() - if cfgdir is not None: - return os.path.join(cfgdir, value) + assert cfgdir is not None + return os.path.join(cfgdir, value) return value def validate(self, value): @@ -1113,7 +1113,7 @@ class FuzzyUrl(BaseType): from qutebrowser.utils import urlutils try: self.transform(value) - except urlutils.FuzzyUrlError as e: + except urlutils.InvalidUrlError as e: raise configexc.ValidationError(value, str(e)) def transform(self, value): diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py index 125cfb6b1..042d9e8b6 100644 --- a/qutebrowser/config/textwrapper.py +++ b/qutebrowser/config/textwrapper.py @@ -26,7 +26,7 @@ class TextWrapper(textwrap.TextWrapper): """Text wrapper customized to be used in configs.""" - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): kw = { 'width': 72, 'replace_whitespace': False, @@ -36,4 +36,4 @@ class TextWrapper(textwrap.TextWrapper): 'subsequent_indent': '# ', } kw.update(kwargs) - super().__init__(*args, **kw) + super().__init__(**kw) diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py index 5a5954f85..7db51649a 100644 --- a/qutebrowser/mainwindow/statusbar/textbase.py +++ b/qutebrowser/mainwindow/statusbar/textbase.py @@ -55,9 +55,11 @@ class TextBase(QLabel): Args: width: The maximal width the text should take. """ - if self.text is not None: + if self.text(): self._elided_text = self.fontMetrics().elidedText( self.text(), self._elidemode, width, Qt.TextShowMnemonic) + else: + self._elided_text = '' def setText(self, txt): """Extend QLabel::setText. diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 16aa53adf..0939389c4 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -25,7 +25,7 @@ import functools import datetime import contextlib -from PyQt5.QtCore import QEvent, QMetaMethod, QObject +from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject from PyQt5.QtWidgets import QApplication from qutebrowser.utils import log, utils, qtutils, objreg @@ -56,7 +56,7 @@ def log_signals(obj): dbg = dbg_signal(signal, args) try: r = repr(obj) - except RuntimeError: + except RuntimeError: # pragma: no cover r = '' log.signals.debug("Signal in {}: {}".format(r, dbg)) @@ -68,8 +68,9 @@ def log_signals(obj): qtutils.ensure_valid(meta_method) if meta_method.methodType() == QMetaMethod.Signal: name = bytes(meta_method.name()).decode('ascii') - signal = getattr(obj, name) - signal.connect(functools.partial(log_slot, obj, signal)) + if name != 'destroyed': + signal = getattr(obj, name) + signal.connect(functools.partial(log_slot, obj, signal)) if inspect.isclass(obj): old_init = obj.__init__ @@ -105,19 +106,21 @@ def qenum_key(base, value, add_base=False, klass=None): klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") + try: - idx = klass.staticMetaObject.indexOfEnumerator(klass.__name__) + idx = base.staticMetaObject.indexOfEnumerator(klass.__name__) + ret = base.staticMetaObject.enumerator(idx).valueToKey(value) except AttributeError: - idx = -1 - if idx != -1: - ret = klass.staticMetaObject.enumerator(idx).valueToKey(value) - else: + ret = None + + if ret is None: for name, obj in vars(base).items(): if isinstance(obj, klass) and obj == value: ret = name break else: ret = '0x{:04x}'.format(int(value)) + if add_base and hasattr(base, '__name__'): return '.'.join([base.__name__, ret]) else: @@ -177,7 +180,7 @@ def signal_name(sig): return m.group(1) -def _format_args(args=None, kwargs=None): +def format_args(args=None, kwargs=None): """Format a list of arguments/kwargs to a function-call like string.""" if args is not None: arglist = [utils.compact_text(repr(arg), 200) for arg in args] @@ -199,7 +202,7 @@ def dbg_signal(sig, args): Return: A human-readable string representation of signal/args. """ - return '{}({})'.format(signal_name(sig), _format_args(args)) + return '{}({})'.format(signal_name(sig), format_args(args)) def format_call(func, args=None, kwargs=None, full=True): @@ -218,7 +221,7 @@ def format_call(func, args=None, kwargs=None, full=True): name = utils.qualname(func) else: name = func.__name__ - return '{}({})'.format(name, _format_args(args, kwargs)) + return '{}({})'.format(name, format_args(args, kwargs)) @contextlib.contextmanager @@ -247,25 +250,30 @@ def _get_widgets(): def _get_pyqt_objects(lines, obj, depth=0): """Recursive method for get_all_objects to get Qt objects.""" - for kid in obj.findChildren(QObject): + for kid in obj.findChildren(QObject, '', Qt.FindDirectChildrenOnly): lines.append(' ' * depth + repr(kid)) _get_pyqt_objects(lines, kid, depth + 1) -def get_all_objects(): +def get_all_objects(start_obj=None): """Get all children of an object recursively as a string.""" output = [''] widget_lines = _get_widgets() widget_lines = [' ' + e for e in widget_lines] - widget_lines.insert(0, "Qt widgets - {} objects".format( + widget_lines.insert(0, "Qt widgets - {} objects:".format( len(widget_lines))) output += widget_lines + + if start_obj is None: + start_obj = QApplication.instance() + pyqt_lines = [] - _get_pyqt_objects(pyqt_lines, QApplication.instance()) + _get_pyqt_objects(pyqt_lines, start_obj) pyqt_lines = [' ' + e for e in pyqt_lines] pyqt_lines.insert(0, 'Qt objects - {} objects:'.format( len(pyqt_lines))) - output += pyqt_lines + output += [''] + output += pyqt_lines output += objreg.dump_objects() return '\n'.join(output) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 50f60d523..16acbb9aa 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -37,6 +37,22 @@ from qutebrowser.commands import cmdexc # https://github.com/The-Compiler/qutebrowser/issues/108 +class InvalidUrlError(ValueError): + + """Error raised if a function got an invalid URL. + + Inherits ValueError because that was the exception originally used for + that, so there still might be some code around which checks for that. + """ + + def __init__(self, url): + if url.isValid(): + raise ValueError("Got valid URL {}!".format(url.toDisplayString())) + self.url = url + self.msg = get_errstring(url) + super().__init__(self.msg) + + def _parse_search_term(s): """Get a search engine name and search term from a string. @@ -185,7 +201,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): qtutils.ensure_valid(url) else: if not url.isValid(): - raise FuzzyUrlError("Invalid URL '{}'!".format(urlstr), url) + raise InvalidUrlError(url) return url @@ -355,7 +371,7 @@ def host_tuple(url): This is suitable to identify a connection, e.g. for SSL errors. """ if not url.isValid(): - raise ValueError(get_errstring(url)) + raise InvalidUrlError(url) scheme, host, port = url.scheme(), url.host(), url.port() assert scheme if not host: @@ -405,9 +421,9 @@ def same_domain(url1, url2): True if the domains are the same, False otherwise. """ if not url1.isValid(): - raise ValueError(get_errstring(url1)) + raise InvalidUrlError(url1) if not url2.isValid(): - raise ValueError(get_errstring(url2)) + raise InvalidUrlError(url2) suffix1 = url1.topLevelDomain() suffix2 = url2.topLevelDomain() @@ -422,24 +438,57 @@ def same_domain(url1, url2): return domain1 == domain2 -class FuzzyUrlError(Exception): +class IncDecError(Exception): - """Exception raised by fuzzy_url on problems. + """Exception raised by incdec_number on problems. Attributes: - msg: The error message to use. + msg: The error message. url: The QUrl which caused the error. """ - def __init__(self, msg, url=None): + def __init__(self, msg, url): super().__init__(msg) - if url is not None and url.isValid(): - raise ValueError("Got valid URL {}!".format(url.toDisplayString())) self.url = url self.msg = msg def __str__(self): - if self.url is None or not self.url.errorString(): - return self.msg - else: - return '{}: {}'.format(self.msg, self.url.errorString()) + return '{}: {}'.format(self.msg, self.url.toString()) + + +def incdec_number(url, incdec): + """Find a number in the url and increment or decrement it. + + Args: + url: The current url + incdec: Either 'increment' or 'decrement' + + Return: + The new url with the number incremented/decremented. + + Raises IncDecError if the url contains no number. + """ + if not url.isValid(): + raise InvalidUrlError(url) + + path = url.path() + # Get the last number in a string + match = re.match(r'(.*\D|^)(\d+)(.*)', path) + if not match: + raise IncDecError("No number found in URL!", url) + pre, number, post = match.groups() + # This should always succeed because we match \d+ + val = int(number) + if incdec == 'decrement': + if val <= 0: + raise IncDecError("Can't decrement {}!".format(val), url) + val -= 1 + elif incdec == 'increment': + val += 1 + else: + raise ValueError("Invalid value {} for indec!".format(incdec)) + new_path = ''.join([pre, str(val), post]) + # Make a copy of the QUrl so we don't modify the original + new_url = QUrl(url) + new_url.setPath(new_path) + return new_url diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index d63a026ea..d2b158bd7 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -20,6 +20,7 @@ """Enforce perfect coverage on some files.""" +import os import sys import os.path @@ -37,6 +38,8 @@ PERFECT_FILES = [ 'qutebrowser/browser/tabhistory.py', 'qutebrowser/browser/http.py', 'qutebrowser/browser/rfc6266.py', + 'qutebrowser/browser/webelem.py', + 'qutebrowser/browser/network/schemehandler.py', 'qutebrowser/misc/readline.py', 'qutebrowser/misc/split.py', @@ -45,10 +48,12 @@ PERFECT_FILES = [ 'qutebrowser/mainwindow/statusbar/percentage.py', 'qutebrowser/mainwindow/statusbar/progress.py', 'qutebrowser/mainwindow/statusbar/tabindex.py', + 'qutebrowser/mainwindow/statusbar/textbase.py', 'qutebrowser/config/configtypes.py', 'qutebrowser/config/configdata.py', 'qutebrowser/config/configexc.py', + 'qutebrowser/config/textwrapper.py', 'qutebrowser/utils/qtutils.py', 'qutebrowser/utils/standarddir.py', @@ -56,6 +61,8 @@ PERFECT_FILES = [ 'qutebrowser/utils/usertypes.py', 'qutebrowser/utils/utils.py', 'qutebrowser/utils/version.py', + 'qutebrowser/utils/debug.py', + 'qutebrowser/utils/jinja.py', ] @@ -67,10 +74,23 @@ def main(): """ utils.change_cwd() + if sys.platform != 'linux': + print("Skipping coverage checks on non-Linux system.") + sys.exit() + elif '-k' in sys.argv[1:]: + print("Skipping coverage checks because -k is given.") + sys.exit() + elif '-m' in sys.argv[1:]: + print("Skipping coverage checks because -m is given.") + sys.exit() + elif any(arg.startswith('tests' + os.sep) for arg in sys.argv[1:]): + print("Skipping coverage checks because a filename is given.") + sys.exit() + for path in PERFECT_FILES: assert os.path.exists(os.path.join(*path.split('/'))), path - with open('coverage.xml', encoding='utf-8') as f: + with open('.coverage.xml', encoding='utf-8') as f: tree = ElementTree.parse(f) classes = tree.getroot().findall('./packages/package/classes/class') @@ -101,6 +121,8 @@ def main(): print("{} has 100% coverage but is not in PERFECT_FILES!".format( filename)) + os.remove('.coverage.xml') + return status diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index ec7ac0651..2ffa2974f 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -52,6 +52,7 @@ def main(): 'redefined-outer-name', 'unused-argument', 'missing-docstring', + 'protected-access', # https://bitbucket.org/logilab/pylint/issue/511/ 'undefined-variable', ] diff --git a/tests/browser/http/test_content_disposition.py b/tests/browser/http/test_content_disposition.py index 09dc1f828..dcb5a096a 100644 --- a/tests/browser/http/test_content_disposition.py +++ b/tests/browser/http/test_content_disposition.py @@ -24,7 +24,6 @@ import logging import pytest from qutebrowser.browser import http -from qutebrowser.utils import log DEFAULT_NAME = 'qutebrowser-download' @@ -56,7 +55,7 @@ class HeaderChecker: """Check if the passed header is ignored.""" reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) - with self.caplog.atLevel(logging.ERROR, logger=log.rfc6266.name): + with self.caplog.atLevel(logging.ERROR, 'rfc6266'): # with self.assertLogs(log.rfc6266, logging.ERROR): cd_inline, cd_filename = http.parse_content_disposition(reply) assert cd_filename == DEFAULT_NAME diff --git a/tests/browser/http/test_http_hypothesis.py b/tests/browser/http/test_http_hypothesis.py index 174509011..38178b392 100644 --- a/tests/browser/http/test_http_hypothesis.py +++ b/tests/browser/http/test_http_hypothesis.py @@ -19,6 +19,8 @@ """Hypothesis tests for qutebrowser.browser.http.""" +import logging + import pytest import hypothesis from hypothesis import strategies @@ -35,11 +37,12 @@ from qutebrowser.browser import http, rfc6266 'attachment; filename*={}', ]) @hypothesis.given(strategies.text(alphabet=[chr(x) for x in range(255)])) -def test_parse_content_disposition(template, stubs, s): +def test_parse_content_disposition(caplog, template, stubs, s): """Test parsing headers based on templates which hypothesis completes.""" header = template.format(s) reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header}) - http.parse_content_disposition(reply) + with caplog.atLevel(logging.ERROR, 'rfc6266'): + http.parse_content_disposition(reply) @hypothesis.given(strategies.binary()) diff --git a/tests/browser/network/test_schemehandler.py b/tests/browser/network/test_schemehandler.py new file mode 100644 index 000000000..bd07f4e54 --- /dev/null +++ b/tests/browser/network/test_schemehandler.py @@ -0,0 +1,35 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""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) diff --git a/tests/browser/test_webelem.py b/tests/browser/test_webelem.py index 3a1db9547..94fa566d7 100644 --- a/tests/browser/test_webelem.py +++ b/tests/browser/test_webelem.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for the webelement utils.""" from unittest import mock @@ -334,6 +332,14 @@ class TestIsVisible: def frame(self, stubs): return stubs.FakeWebFrame(QRect(0, 0, 100, 100)) + def test_invalid_frame_geometry(self, stubs): + """Test with an invalid frame geometry.""" + rect = QRect(0, 0, 0, 0) + assert not rect.isValid() + frame = stubs.FakeWebFrame(rect) + elem = get_webelem(QRect(0, 0, 10, 10), frame) + assert not elem.is_visible(frame) + def test_invalid_invisible(self, frame): """Test elements with an invalid geometry which are invisible.""" elem = get_webelem(QRect(0, 0, 0, 0), frame) @@ -460,6 +466,67 @@ class TestIsVisibleIframe: assert not objects.elems[2].is_visible(objects.frame) assert objects.elems[3].is_visible(objects.frame) + @pytest.fixture + def invalid_objects(self, stubs): + """Set up the following base situation: + + 0, 0 300, 0 + ############################## + # # + 0,10 # iframe 100,10 # + #********** # + #* e * elems[0]: 10, 10 in iframe (visible) + #* * # + #* * # + #********** # + 0,110 #. .100,110 # + #. . # + #. e . elems[2]: 20,150 in iframe (not visible) + #.......... # + ############################## + 300, 0 300, 300 + + Returns an Objects namedtuple with frame/iframe/elems attributes. + """ + frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300)) + iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame) + assert frame.geometry().contains(iframe.geometry()) + + elems = [ + get_webelem(QRect(10, 10, 0, 0), iframe), + get_webelem(QRect(20, 150, 0, 0), iframe), + ] + for e in elems: + assert not e.geometry().isValid() + + return self.Objects(frame=frame, iframe=iframe, elems=elems) + + def test_invalid_visible(self, invalid_objects): + """Test elements with an invalid geometry which are visible. + + This seems to happen sometimes in the real world, with real elements + which *are* visible, but don't have a valid geometry. + """ + elem = invalid_objects.elems[0] + assert elem.is_visible(invalid_objects.frame) + + def test_invalid_invisible(self, invalid_objects): + """Test elements with an invalid geometry which are invisible.""" + assert not invalid_objects.elems[1].is_visible(invalid_objects.frame) + + +def test_focus_element(stubs): + """Test getting focus element with a fake frame/element. + + Testing this with a real webpage is almost impossible because the window + and the element would have focus, which is hard to achieve consistently in + a test. + """ + frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100)) + elem = get_webelem() + frame.focus_elem = elem._elem + assert webelem.focus_elem(frame)._elem is elem._elem + class TestRectOnView: diff --git a/tests/completion/test_column_widths.py b/tests/completion/test_column_widths.py new file mode 100644 index 000000000..c9a219e78 --- /dev/null +++ b/tests/completion/test_column_widths.py @@ -0,0 +1,53 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Alexander Cogneau +# +# 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 . + +"""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 diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 3fff0ea66..0e70149dd 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.config.config.""" import os @@ -234,10 +232,12 @@ class TestKeyConfigParser: assert new == new_expected +@pytest.mark.integration class TestDefaultConfig: """Test validating of the default config.""" + @pytest.mark.usefixtures('qapp') def test_default_config(self): """Test validating of the default config.""" conf = config.ConfigManager(None, None) @@ -254,6 +254,7 @@ class TestDefaultConfig: runner.parse(cmd, aliases=False) +@pytest.mark.integration class TestConfigInit: """Test initializing of the config.""" @@ -272,8 +273,10 @@ class TestConfigInit: objreg.register('save-manager', mock.MagicMock()) args = argparse.Namespace(relaxed_config=False) objreg.register('args', args) + old_standarddir_args = standarddir._args yield objreg.global_registry.clear() + standarddir._args = old_standarddir_args def test_config_none(self, monkeypatch): """Test initializing with config path set to None.""" diff --git a/tests/config/test_configtypes.py b/tests/config/test_configtypes.py index 35ada44ab..636b5f9f6 100644 --- a/tests/config/test_configtypes.py +++ b/tests/config/test_configtypes.py @@ -16,13 +16,12 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.config.configtypes.""" import re import collections import itertools +import os.path import base64 import pytest @@ -1076,6 +1075,7 @@ class TestRegexList: assert klass().transform(val) == expected +@pytest.mark.usefixtures('qapp') class TestFileAndUserStyleSheet: """Test File/UserStyleSheet.""" @@ -1146,6 +1146,26 @@ class TestFileAndUserStyleSheet: with pytest.raises(configexc.ValidationError): configtypes.File().validate('foobar') + @pytest.mark.parametrize('configtype, value, raises', [ + (configtypes.File(), 'foobar', True), + (configtypes.UserStyleSheet(), 'foobar', False), + (configtypes.UserStyleSheet(), '\ud800', True), + ]) + def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype, + value, raises): + """Test with a relative path and standarddir.config returning None.""" + monkeypatch.setattr( + 'qutebrowser.config.configtypes.standarddir.config', + lambda: 'this/does/not/exist') + os_mock.path.isabs.return_value = False + os_mock.path.isfile.side_effect = os.path.isfile + + if raises: + with pytest.raises(configexc.ValidationError): + configtype.validate(value) + else: + configtype.validate(value) + def test_validate_expanduser(self, klass, os_mock): """Test if validate expands the user correctly.""" os_mock.path.isfile.side_effect = (lambda path: diff --git a/tests/config/test_textwrapper.py b/tests/config/test_textwrapper.py new file mode 100644 index 000000000..8fe42559e --- /dev/null +++ b/tests/config/test_textwrapper.py @@ -0,0 +1,38 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""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 diff --git a/tests/conftest.py b/tests/conftest.py index 1d6aa8b68..d88437225 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,22 +23,24 @@ import os import sys import collections import itertools +import logging import pytest import stubs as stubsmod +import logfail from qutebrowser.config import configexc from qutebrowser.utils import objreg, usertypes -@pytest.fixture(scope='session', autouse=True) -def app_and_logging(qapp): - """Initialize a QApplication and logging. - This ensures that a QApplication is created and used by all tests. - """ - from log import init - init() +@pytest.yield_fixture(scope='session', autouse=True) +def fail_on_logging(): + handler = logfail.LogFailHandler() + logging.getLogger().addHandler(handler) + yield + logging.getLogger().removeHandler(handler) + handler.close() @pytest.fixture(scope='session') @@ -58,7 +60,7 @@ def unicode_encode_err(): @pytest.fixture(scope='session') -def qnam(): +def qnam(qapp): """Session-wide QNetworkAccessManager.""" from PyQt5.QtNetwork import QNetworkAccessManager nam = QNetworkAccessManager() @@ -118,8 +120,14 @@ def pytest_collection_modifyitems(items): http://pytest.org/latest/plugins.html """ for item in items: - if 'qtbot' in getattr(item, 'fixturenames', ()): + if 'qapp' in getattr(item, 'fixturenames', ()): item.add_marker('gui') + if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''): + if 'CI' in os.environ: + raise Exception("No display available on CI!") + skip_marker = pytest.mark.skipif( + True, reason="No DISPLAY available") + item.add_marker(skip_marker) def _generate_cmdline_tests(): diff --git a/tests/javascript/position_caret/test_position_caret.py b/tests/javascript/position_caret/test_position_caret.py index 11f67090f..024f15f08 100644 --- a/tests/javascript/position_caret/test_position_caret.py +++ b/tests/javascript/position_caret/test_position_caret.py @@ -67,12 +67,14 @@ def caret_tester(js_tester): return CaretTester(js_tester) +@pytest.mark.integration def test_simple(caret_tester): """Test with a simple (one-line) HTML text.""" caret_tester.js.load('position_caret/simple.html') caret_tester.check() +@pytest.mark.integration def test_scrolled_down(caret_tester): """Test with multiple text blocks with the viewport scrolled down.""" caret_tester.js.load('position_caret/scrolled_down.html') @@ -81,6 +83,7 @@ def test_scrolled_down(caret_tester): caret_tester.check() +@pytest.mark.integration @pytest.mark.parametrize('style', ['visibility: hidden', 'display: none']) def test_invisible(caret_tester, style): """Test with hidden text elements.""" @@ -88,6 +91,7 @@ def test_invisible(caret_tester, style): caret_tester.check() +@pytest.mark.integration def test_scrolled_down_img(caret_tester): """Test with an image at the top with the viewport scrolled down.""" caret_tester.js.load('position_caret/scrolled_down_img.html') diff --git a/tests/keyinput/test_basekeyparser.py b/tests/keyinput/test_basekeyparser.py index e8f7f9325..d5eadbb5e 100644 --- a/tests/keyinput/test_basekeyparser.py +++ b/tests/keyinput/test_basekeyparser.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for BaseKeyParser.""" import sys @@ -29,7 +27,6 @@ from PyQt5.QtCore import Qt import pytest from qutebrowser.keyinput import basekeyparser -from qutebrowser.utils import log CONFIG = {'input': {'timeout': 100}} @@ -107,7 +104,7 @@ class TestSpecialKeys: def setup(self, caplog, fake_keyconfig): self.kp = basekeyparser.BaseKeyParser(0) self.kp.execute = mock.Mock() - with caplog.atLevel(logging.WARNING, log.keyboard.name): + with caplog.atLevel(logging.WARNING, 'keyboard'): # Ignoring keychain 'ccc' in mode 'test' because keychains are not # supported there. self.kp.read_config('test') @@ -186,7 +183,7 @@ class TestKeyChain: self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None) assert self.kp._keystring == '' - def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub, + def test_ambiguous_keychain(self, qapp, fake_keyevent_factory, config_stub, monkeypatch): """Test ambiguous keychain.""" config_stub.data = CONFIG diff --git a/tests/keyinput/test_modeparsers.py b/tests/keyinput/test_modeparsers.py index 47789b5b4..769b8a514 100644 --- a/tests/keyinput/test_modeparsers.py +++ b/tests/keyinput/test_modeparsers.py @@ -39,8 +39,6 @@ class TestsNormalKeyParser: kp: The NormalKeyParser to be tested. """ - # pylint: disable=protected-access - @pytest.yield_fixture(autouse=True) def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig): """Set up mocks and read the test config.""" diff --git a/tests/log.py b/tests/log.py deleted file mode 100644 index 37148be0e..000000000 --- a/tests/log.py +++ /dev/null @@ -1,68 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015 Florian Bruhin (The Compiler) -# -# 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 . - -"""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) diff --git a/tests/logfail.py b/tests/logfail.py new file mode 100644 index 000000000..b4a6afda5 --- /dev/null +++ b/tests/logfail.py @@ -0,0 +1,67 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""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())) diff --git a/tests/mainwindow/statusbar/test_textbase.py b/tests/mainwindow/statusbar/test_textbase.py index eadf9c46a..016d02aad 100644 --- a/tests/mainwindow/statusbar/test_textbase.py +++ b/tests/mainwindow/statusbar/test_textbase.py @@ -50,4 +50,54 @@ def test_elided_text(qtbot, elidemode, check): label.setText(long_string) label.resize(100, 50) label.show() - assert check(label._elided_text) # pylint: disable=protected-access + qtbot.waitForWindowShown(label) + assert check(label._elided_text) + + +def test_settext_empty(mocker, qtbot): + """Make sure using setText('') works and runs repaint.""" + label = TextBase() + qtbot.add_widget(label) + mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.repaint', + autospec=True) + + label.setText('') + label.repaint.assert_called_with() + + +def test_resize(qtbot): + """Make sure the elided text is updated when resizing.""" + label = TextBase() + qtbot.add_widget(label) + long_string = 'Hello world! ' * 20 + label.setText(long_string) + + label.show() + qtbot.waitForWindowShown(label) + + text_1 = label._elided_text + label.resize(20, 50) + text_2 = label._elided_text + + assert text_1 != text_2 + + +def test_text_elide_none(mocker, qtbot): + """Make sure the text doesn't get elided if it's empty.""" + label = TextBase() + qtbot.add_widget(label) + label.setText('') + mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.' + 'fontMetrics') + label._update_elided_text(20) + + assert not label.fontMetrics.called + + +def test_unset_text(qtbot): + """Make sure the text is cleared properly.""" + label = TextBase() + qtbot.add_widget(label) + label.setText('foo') + label.setText('') + assert not label._elided_text diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index 773769f80..274e720d2 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -19,8 +19,6 @@ """Tests for qutebrowser.misc.editor.""" -# pylint: disable=protected-access - import os import os.path import logging @@ -46,7 +44,7 @@ class TestArg: stubs.fake_qprocess()) self.editor = editor.ExternalEditor(0) yield - self.editor._cleanup() # pylint: disable=protected-access + self.editor._cleanup() @pytest.fixture def stubbed_config(self, config_stub, monkeypatch): @@ -229,18 +227,18 @@ class TestErrorMessage: monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) self.editor = editor.ExternalEditor(0) yield - self.editor._cleanup() # pylint: disable=protected-access + self.editor._cleanup() def test_proc_error(self, caplog): """Test on_proc_error.""" self.editor.edit("") - with caplog.atLevel(logging.ERROR, 'message'): + with caplog.atLevel(logging.ERROR): self.editor.on_proc_error(QProcess.Crashed) assert len(caplog.records()) == 2 def test_proc_return(self, caplog): """Test on_proc_finished with a bad exit status.""" self.editor.edit("") - with caplog.atLevel(logging.ERROR, 'message'): + with caplog.atLevel(logging.ERROR): self.editor.on_proc_closed(1, QProcess.NormalExit) - assert len(caplog.records()) == 3 + assert len(caplog.records()) == 2 diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py index 99788c39a..a3ec58c6f 100644 --- a/tests/misc/test_guiprocess.py +++ b/tests/misc/test_guiprocess.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.misc.guiprocess.""" +import os import sys import textwrap +import logging import pytest from PyQt5.QtCore import QProcess @@ -97,6 +97,7 @@ def test_double_start(qtbot, proc): @pytest.mark.not_frozen +@pytest.mark.skipif(os.name == 'nt', reason="Test is flaky on Windows...") def test_double_start_finished(qtbot, proc): """Test starting a GUIProcess twice (with the first call finished).""" with qtbot.waitSignals([proc.started, proc.finished], raising=True, @@ -117,10 +118,13 @@ def test_cmd_args(fake_proc): assert (fake_proc.cmd, fake_proc.args) == (cmd, args) -def test_error(qtbot, proc): +# WORKAROUND for https://github.com/pytest-dev/pytest-qt/issues/67 +@pytest.mark.skipif(os.name == 'nt', reason="Test is flaky on Windows...") +def test_error(qtbot, proc, caplog): """Test the process emitting an error.""" - with qtbot.waitSignal(proc.error, raising=True): - proc.start('this_does_not_exist_either', []) + with caplog.atLevel(logging.ERROR, 'message'): + with qtbot.waitSignal(proc.error, raising=True): + proc.start('this_does_not_exist_either', []) @pytest.mark.not_frozen diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index 62f562560..a54427ce3 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.misc.lineparser.""" import io diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index da2d05821..0aa883cc5 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -19,8 +19,6 @@ """Tests for qutebrowser.misc.readline.""" -# pylint: disable=protected-access - import re import inspect diff --git a/tests/stubs.py b/tests/stubs.py index 4345a42f6..a7d34133b 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -42,7 +42,11 @@ class FakeKeyEvent: class FakeWebFrame: - """A stub for QWebFrame.""" + """A stub for QWebFrame. + + Attributes: + focus_elem: The 'focused' element. + """ def __init__(self, geometry, scroll=None, parent=None): """Constructor. @@ -57,6 +61,16 @@ class FakeWebFrame: self.geometry = mock.Mock(return_value=geometry) self.scrollPosition = mock.Mock(return_value=scroll) self.parentFrame = mock.Mock(return_value=parent) + self.focus_elem = None + + def findFirstElement(self, selector): + if selector == '*:focus': + if self.focus_elem is not None: + return self.focus_elem + else: + raise Exception("Trying to get focus element but it's unset!") + else: + raise Exception("Unknown selector {!r}!".format(selector)) class FakeChildrenFrame: @@ -73,11 +87,14 @@ class FakeQApplication: """Stub to insert as QApplication module.""" - def __init__(self, style=None): + def __init__(self, style=None, all_widgets=None): self.instance = mock.Mock(return_value=self) + self.style = mock.Mock(spec=QCommonStyle) self.style().metaObject().className.return_value = style + self.allWidgets = lambda: all_widgets + class FakeUrl: diff --git a/tests/test_logfail.py b/tests/test_logfail.py new file mode 100644 index 000000000..b869c8cae --- /dev/null +++ b/tests/test_logfail.py @@ -0,0 +1,65 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""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') diff --git a/tests/utils/debug/test_log_time.py b/tests/utils/debug/test_log_time.py deleted file mode 100644 index 3769cb6a2..000000000 --- a/tests/utils/debug/test_log_time.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# 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 . - -"""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 diff --git a/tests/utils/debug/test_qenum_key.py b/tests/utils/debug/test_qenum_key.py deleted file mode 100644 index c43279a4a..000000000 --- a/tests/utils/debug/test_qenum_key.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# 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 . - -"""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' diff --git a/tests/utils/debug/test_qflags_key.py b/tests/utils/debug/test_qflags_key.py deleted file mode 100644 index 6a2a85069..000000000 --- a/tests/utils/debug/test_qflags_key.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# 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 . - -"""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' diff --git a/tests/utils/debug/test_signal.py b/tests/utils/debug/test_signal.py deleted file mode 100644 index 58b1c13d9..000000000 --- a/tests/utils/debug/test_signal.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# 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 . - -"""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')" diff --git a/tests/utils/test_debug.py b/tests/utils/test_debug.py new file mode 100644 index 000000000..e1e4a6615 --- /dev/null +++ b/tests/utils/test_debug.py @@ -0,0 +1,252 @@ +# Copyright 2014-2015 Florian Bruhin (The Compiler) +# 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 . + +"""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 '' + + +@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 : signal1()' + assert records[1].msg == "Signal in : 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: + + + + Qt objects - 3 objects: + + + + + 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 = '. -# pylint: disable=protected-access - """Tests for qutebrowser.utils.log.""" import logging @@ -204,6 +202,7 @@ class TestRAMHandler: assert handler.dump_log() == "Two\nThree" +@pytest.mark.integration class TestInitLog: """Tests for init_log.""" @@ -238,8 +237,8 @@ class TestHideQtWarning: def test_unfiltered(self, logger, caplog): """Test a message which is not filtered.""" - with log.hide_qt_warning("World", logger='qt-tests'): - with caplog.atLevel(logging.WARNING, logger='qt-tests'): + with log.hide_qt_warning("World", 'qt-tests'): + with caplog.atLevel(logging.WARNING, 'qt-tests'): logger.warning("Hello World") assert len(caplog.records()) == 1 record = caplog.records()[0] @@ -248,21 +247,21 @@ class TestHideQtWarning: def test_filtered_exact(self, logger, caplog): """Test a message which is filtered (exact match).""" - with log.hide_qt_warning("Hello", logger='qt-tests'): - with caplog.atLevel(logging.WARNING, logger='qt-tests'): + with log.hide_qt_warning("Hello", 'qt-tests'): + with caplog.atLevel(logging.WARNING, 'qt-tests'): logger.warning("Hello") assert not caplog.records() def test_filtered_start(self, logger, caplog): """Test a message which is filtered (match at line start).""" - with log.hide_qt_warning("Hello", logger='qt-tests'): - with caplog.atLevel(logging.WARNING, logger='qt-tests'): + with log.hide_qt_warning("Hello", 'qt-tests'): + with caplog.atLevel(logging.WARNING, 'qt-tests'): logger.warning("Hello World") assert not caplog.records() def test_filtered_whitespace(self, logger, caplog): """Test a message which is filtered (match with whitespace).""" - with log.hide_qt_warning("Hello", logger='qt-tests'): - with caplog.atLevel(logging.WARNING, logger='qt-tests'): + with log.hide_qt_warning("Hello", 'qt-tests'): + with caplog.atLevel(logging.WARNING, 'qt-tests'): logger.warning(" Hello World ") assert not caplog.records() diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 0f1b9b768..8466d2471 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -35,7 +35,6 @@ import unittest import unittest.mock from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, QTimer, QBuffer, QFile, QProcess, QFileDevice) -from PyQt5.QtWidgets import QApplication from qutebrowser import qutebrowser from qutebrowser.utils import qtutils @@ -505,19 +504,18 @@ class TestSavefileOpen: @pytest.mark.parametrize('orgname, expected', [(None, ''), ('test', 'test')]) -def test_unset_organization(orgname, expected): +def test_unset_organization(qapp, orgname, expected): """Test unset_organization. Args: orgname: The organizationName to set initially. expected: The organizationName which is expected when reading back. """ - app = QApplication.instance() - app.setOrganizationName(orgname) - assert app.organizationName() == expected # sanity check + qapp.setOrganizationName(orgname) + assert qapp.organizationName() == expected # sanity check with qtutils.unset_organization(): - assert app.organizationName() == '' - assert app.organizationName() == expected + assert qapp.organizationName() == '' + assert qapp.organizationName() == expected if test_file is not None: @@ -921,6 +919,7 @@ class TestPyQIODevice: assert str(excinfo.value) == 'Reading failed' +@pytest.mark.usefixtures('qapp') class TestEventLoop: """Tests for EventLoop. @@ -929,8 +928,6 @@ class TestEventLoop: loop: The EventLoop we're testing. """ - # pylint: disable=protected-access - def _assert_executing(self): """Slot which gets called from timers to be sure the loop runs.""" assert self.loop._executing diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index 3df3ebf88..fbe2c268a 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.utils.standarddir.""" import os @@ -29,23 +27,22 @@ import logging import textwrap from PyQt5.QtCore import QStandardPaths -from PyQt5.QtWidgets import QApplication import pytest from qutebrowser.utils import standarddir @pytest.yield_fixture(autouse=True) -def change_qapp_name(): +def change_qapp_name(qapp): """Change the name of the QApplication instance. This changes the applicationName for all tests in this module to "qutebrowser_test". """ - old_name = QApplication.instance().applicationName() - QApplication.instance().setApplicationName('qutebrowser_test') + old_name = qapp.applicationName() + qapp.setApplicationName('qutebrowser_test') yield - QApplication.instance().setApplicationName(old_name) + qapp.setApplicationName(old_name) @pytest.fixture @@ -271,7 +268,7 @@ class TestInitCacheDirTag: monkeypatch.setattr('qutebrowser.utils.standarddir.cache', lambda: str(tmpdir)) mocker.patch('builtins.open', side_effect=OSError) - with caplog.atLevel(logging.ERROR, 'misc'): + with caplog.atLevel(logging.ERROR, 'init'): standarddir._init_cachedir_tag() assert len(caplog.records()) == 1 assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG' diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py index fa1cfa665..cff406a6c 100644 --- a/tests/utils/test_urlutils.py +++ b/tests/utils/test_urlutils.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.utils.urlutils.""" import os.path @@ -219,7 +217,7 @@ class TestFuzzyUrl: @pytest.mark.parametrize('do_search, exception', [ (True, qtutils.QtValueError), - (False, urlutils.FuzzyUrlError), + (False, urlutils.InvalidUrlError), ]) def test_invalid_url(self, do_search, exception, is_url_mock, monkeypatch): """Test with an invalid URL.""" @@ -469,34 +467,40 @@ def test_host_tuple(qurl, tpl): assert urlutils.host_tuple(qurl) == tpl -@pytest.mark.parametrize('url, raising, has_err_string', [ - (None, False, False), - (QUrl(), False, False), - (QUrl('http://www.example.com/'), True, False), - (QUrl('://'), False, True), -]) -def test_fuzzy_url_error(url, raising, has_err_string): - """Test FuzzyUrlError. +class TestInvalidUrlError: - Args: - url: The URL to pass to FuzzyUrlError. - raising; True if the FuzzyUrlError should raise itself. - has_err_string: Whether the QUrl is expected to have errorString set. - """ - if raising: - expected_exc = ValueError - else: - expected_exc = urlutils.FuzzyUrlError + @pytest.mark.parametrize('url, raising, has_err_string', [ + (QUrl(), False, False), + (QUrl('http://www.example.com/'), True, False), + (QUrl('://'), False, True), + ]) + def test_invalid_url_error(self, url, raising, has_err_string): + """Test InvalidUrlError. - with pytest.raises(expected_exc) as excinfo: - raise urlutils.FuzzyUrlError("Error message", url) - - if not raising: - if has_err_string: - expected_text = "Error message: " + url.errorString() + Args: + url: The URL to pass to InvalidUrlError. + raising; True if the InvalidUrlError should raise itself. + has_err_string: Whether the QUrl is expected to have errorString + set. + """ + if raising: + expected_exc = ValueError else: - expected_text = "Error message" - assert str(excinfo.value) == expected_text + expected_exc = urlutils.InvalidUrlError + + with pytest.raises(expected_exc) as excinfo: + raise urlutils.InvalidUrlError(url) + + if not raising: + expected_text = "Invalid URL" + if has_err_string: + expected_text += " - " + url.errorString() + assert str(excinfo.value) == expected_text + + def test_value_error_subclass(self): + """Make sure InvalidUrlError is a ValueError subclass.""" + with pytest.raises(ValueError): + raise urlutils.InvalidUrlError(QUrl()) @pytest.mark.parametrize('are_same, url1, url2', [ @@ -522,5 +526,72 @@ def test_same_domain(are_same, url1, url2): ]) def test_same_domain_invalid_url(url1, url2): """Test same_domain with invalid URLs.""" - with pytest.raises(ValueError): + with pytest.raises(urlutils.InvalidUrlError): urlutils.same_domain(QUrl(url1), QUrl(url2)) + +class TestIncDecNumber: + + """Tests for urlutils.incdec_number().""" + + @pytest.mark.parametrize('url, incdec, output', [ + ("http://example.com/index1.html", "increment", "http://example.com/index2.html"), + ("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"), + ("http://bbc.c0.uk:80/story_1", "increment", "http://bbc.c0.uk:80/story_2"), + ("http://mydomain.tld/1_%C3%A4", "increment", "http://mydomain.tld/2_%C3%A4"), + ("http://example.com/site/5#5", "increment", "http://example.com/site/6#5"), + + ("http://example.com/index10.html", "decrement", "http://example.com/index9.html"), + ("http://foo.bar/folder_1/image_3", "decrement", "http://foo.bar/folder_1/image_2"), + ("http://bbc.c0.uk:80/story_1", "decrement", "http://bbc.c0.uk:80/story_0"), + ("http://mydomain.tld/2_%C3%A4", "decrement", "http://mydomain.tld/1_%C3%A4"), + ("http://example.com/site/5#5", "decrement", "http://example.com/site/4#5"), + ]) + def test_incdec_number(self, url, incdec, output): + """Test incdec_number with valid URLs.""" + new_url = urlutils.incdec_number(QUrl(url), incdec) + assert new_url == QUrl(output) + + @pytest.mark.parametrize('url', [ + "http://example.com/long/path/but/no/number", + "http://ex4mple.com/number/in/hostname", + "http://example.com:42/number/in/port", + "http://www2.example.com/number/in/subdomain", + "http://example.com/%C3%B6/urlencoded/data", + "http://example.com/number/in/anchor#5", + "http://www2.ex4mple.com:42/all/of/the/%C3%A4bove#5", + ]) + def test_no_number(self, url): + """Test incdec_number with URLs that don't contain a number.""" + with pytest.raises(urlutils.IncDecError): + urlutils.incdec_number(QUrl(url), "increment") + + def test_number_below_0(self): + """Test incdec_number with a number that would be below zero + after decrementing.""" + with pytest.raises(urlutils.IncDecError): + urlutils.incdec_number(QUrl('http://example.com/page_0.html'), + 'decrement') + + def test_invalid_url(self): + """Test if incdec_number rejects an invalid URL.""" + with pytest.raises(urlutils.InvalidUrlError): + urlutils.incdec_number(QUrl(""), "increment") + + def test_wrong_mode(self): + """Test if incdec_number rejects a wrong parameter for the incdec + argument.""" + valid_url = QUrl("http://example.com/0") + with pytest.raises(ValueError): + urlutils.incdec_number(valid_url, "foobar") + + @pytest.mark.parametrize("url, msg, expected_str", [ + ("http://example.com", "Invalid", "Invalid: http://example.com"), + ]) + def test_incdec_error(self, url, msg, expected_str): + """Test IncDecError.""" + url = QUrl(url) + with pytest.raises(urlutils.IncDecError) as excinfo: + raise urlutils.IncDecError(msg, url) + + assert excinfo.value.url == url + assert str(excinfo.value) == expected_str diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py index f59b6339a..80f8f2909 100644 --- a/tests/utils/test_version.py +++ b/tests/utils/test_version.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for qutebrowser.utils.version.""" import io @@ -105,12 +103,13 @@ class TestGitStr: commit_file_mock.return_value = 'deadbeef' assert version._git_str() == 'deadbeef' - def test_frozen_oserror(self, commit_file_mock, monkeypatch): + def test_frozen_oserror(self, caplog, commit_file_mock, monkeypatch): """Test with sys.frozen=True and OSError when reading git-commit-id.""" monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, raising=False) commit_file_mock.side_effect = OSError - assert version._git_str() is None + with caplog.atLevel(logging.ERROR, 'misc'): + assert version._git_str() is None @pytest.mark.not_frozen def test_normal_successful(self, git_str_subprocess_fake): @@ -130,13 +129,15 @@ class TestGitStr: commit_file_mock.return_value = '1b4d1dea' assert version._git_str() == '1b4d1dea' - def test_normal_path_oserror(self, mocker, git_str_subprocess_fake): + def test_normal_path_oserror(self, mocker, git_str_subprocess_fake, + caplog): """Test with things raising OSError.""" m = mocker.patch('qutebrowser.utils.version.os') m.path.join.side_effect = OSError mocker.patch('qutebrowser.utils.version.utils.read_file', side_effect=OSError) - assert version._git_str() is None + with caplog.atLevel(logging.ERROR, 'misc'): + assert version._git_str() is None @pytest.mark.not_frozen def test_normal_path_nofile(self, monkeypatch, caplog, diff --git a/tests/utils/usertypes/test_neighborlist.py b/tests/utils/usertypes/test_neighborlist.py index ffce4723e..1ec5a0733 100644 --- a/tests/utils/usertypes/test_neighborlist.py +++ b/tests/utils/usertypes/test_neighborlist.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for the NeighborList class.""" from qutebrowser.utils import usertypes diff --git a/tests/utils/usertypes/test_question.py b/tests/utils/usertypes/test_question.py index b63106513..b5977cc40 100644 --- a/tests/utils/usertypes/test_question.py +++ b/tests/utils/usertypes/test_question.py @@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog): """Test Question.abort() with .emit() raising a TypeError.""" signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted') signal_mock.emit.side_effect = TypeError - with caplog.atLevel(logging.ERROR): + with caplog.atLevel(logging.ERROR, 'misc'): question.abort() assert caplog.records()[0].message == 'Error while aborting question' diff --git a/tests/utils/usertypes/test_timer.py b/tests/utils/usertypes/test_timer.py index 04e163ed1..4976286d5 100644 --- a/tests/utils/usertypes/test_timer.py +++ b/tests/utils/usertypes/test_timer.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=protected-access - """Tests for Timer.""" from qutebrowser.utils import usertypes @@ -74,13 +72,13 @@ def test_start_overflow(): def test_timeout_start(qtbot): """Make sure the timer works with start().""" t = usertypes.Timer() - with qtbot.waitSignal(t.timeout, raising=True): + with qtbot.waitSignal(t.timeout, timeout=3000, raising=True): t.start(200) def test_timeout_set_interval(qtbot): """Make sure the timer works with setInterval().""" t = usertypes.Timer() - with qtbot.waitSignal(t.timeout, raising=True): + with qtbot.waitSignal(t.timeout, timeout=3000, raising=True): t.setInterval(200) t.start() diff --git a/tox.ini b/tox.ini index e88cb8a9e..679dba9e7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,23 +4,14 @@ # and then run "tox" from this directory. [tox] -envlist = smoke,unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest +envlist = py34,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] -passenv = PYTHON -basepython = python3 - -[testenv:mkvenv] -commands = {envpython} scripts/link_pyqt.py --tox {envdir} -envdir = {toxinidir}/.venv -usedevelop = true - -[testenv:unittests] # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms PYTEST_QT_API=pyqt5 -passenv = PYTHON DISPLAY XAUTHORITY HOME +passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI deps = -r{toxinidir}/requirements.txt py==1.4.30 @@ -28,18 +19,29 @@ deps = pytest-capturelog==0.7 pytest-qt==1.5.1 pytest-mock==0.7.0 - pytest-html==1.3.2 + pytest-html==1.4 hypothesis==1.10.1 - hypothesis-pytest==0.15.1 + hypothesis-pytest==0.17.0 + coverage==3.7.1 + pytest-cov==2.0.0 + cov-core==1.15.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test --strict -rfEsw {posargs:tests} + {envpython} -m py.test --strict -rfEsw --cov qutebrowser --cov-report xml --cov-report= {posargs:tests} + {envpython} scripts/dev/check_coverage.py {posargs} + {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" + +[testenv:mkvenv] +basepython = python3 +commands = {envpython} scripts/link_pyqt.py --tox {envdir} +envdir = {toxinidir}/.venv +usedevelop = true [testenv:unittests-watch] -setenv = {[testenv:unittests]setenv} -passenv = {[testenv:unittests]passenv} +basepython = python3 +passenv = {[testenv]passenv} deps = - {[testenv:unittests]deps} + {[testenv]deps} pytest-testmon==0.6 pytest-watch==3.2.0 commands = @@ -47,38 +49,32 @@ commands = {envdir}/bin/ptw -- --testmon --strict -rfEsw {posargs:tests} [testenv:unittests-frozen] -setenv = {[testenv:unittests]setenv} -passenv = {[testenv:unittests]passenv} +basepython = python3 +passenv = {[testenv]passenv} skip_install = true deps = - {[testenv:unittests]deps} + {[testenv]deps} cx_Freeze==4.3.4 commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/dev/freeze_tests.py build_exe -b {envdir}/build {envdir}/build/run-frozen-tests --strict -rfEsw {posargs} -[testenv:coverage] -passenv = PYTHON DISPLAY XAUTHORITY HOME -deps = - {[testenv:unittests]deps} - coverage==3.7.1 - pytest-cov==2.0.0 - cov-core==1.15.0 -commands = - {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html --cov-report xml {posargs:tests} - {envpython} scripts/dev/check_coverage.py - [testenv:misc] +basepython = python3 +# For global .gitignore files +passenv = HOME +deps = commands = {envpython} scripts/dev/misc_checks.py git {envpython} scripts/dev/misc_checks.py vcs {envpython} scripts/dev/misc_checks.py spelling [testenv:pylint] +basepython = python3 skip_install = true setenv = PYTHONPATH={toxinidir}/scripts/dev +passenv = deps = -r{toxinidir}/requirements.txt astroid==1.3.8 @@ -93,9 +89,10 @@ commands = {envpython} scripts/dev/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF [testenv:pep257] +basepython = python3 skip_install = true -deps = pep257==0.6.0 passenv = PYTHON LANG +deps = pep257==0.6.0 # Disabled checks: # D102: Missing docstring in public method (will be handled by others) # D103: Missing docstring in public function (will be handled by others) @@ -104,8 +101,10 @@ passenv = PYTHON LANG commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_*).*\.py' [testenv:pyflakes] +basepython = python3 # https://github.com/fschulze/pytest-flakes/issues/6 setenv = LANG=en_US.UTF-8 +passenv = deps = -r{toxinidir}/requirements.txt py==1.4.30 @@ -117,6 +116,8 @@ commands = {envpython} -m py.test -q --flakes --ignore=tests [testenv:pep8] +basepython = python3 +passenv = deps = -r{toxinidir}/requirements.txt py==1.4.30 @@ -128,6 +129,8 @@ commands = {envpython} -m py.test -q --pep8 --ignore=tests [testenv:mccabe] +basepython = python3 +passenv = deps = -r{toxinidir}/requirements.txt py==1.4.30 @@ -139,7 +142,9 @@ commands = {envpython} -m py.test -q --mccabe --ignore=tests [testenv:pyroma] +basepython = python3 skip_install = true +passenv = deps = pyroma==1.8.2 docutils==0.12 @@ -148,7 +153,9 @@ commands = {envdir}/bin/pyroma . [testenv:check-manifest] +basepython = python3 skip_install = true +passenv = deps = check-manifest==0.25 commands = @@ -156,8 +163,10 @@ commands = {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' [testenv:docs] +basepython = python3 skip_install = true whitelist_externals = git +passenv = deps = -r{toxinidir}/requirements.txt commands = @@ -166,22 +175,12 @@ commands = git --no-pager diff --exit-code --stat {envpython} scripts/asciidoc2html.py {posargs} -[testenv:smoke] -# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though -setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms -passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER -deps = - -r{toxinidir}/requirements.txt -commands = - {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" - [testenv:smoke-frozen] -setenv = {[testenv:smoke]setenv} -passenv = {[testenv:smoke]passenv} +basepython = python3 +passenv = {[testenv]passenv} skip_install = true deps = - {[testenv:smoke]deps} + {[testenv]deps} cx_Freeze==4.3.4 commands = {envpython} scripts/link_pyqt.py --tox {envdir}