diff --git a/.appveyor.yml b/.appveyor.yml index b6c5cf66a..f1fd299ae 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,4 +14,4 @@ install: - C:\Python27\python -u scripts\dev\ci_install.py test_script: - - C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml + - C:\Python34\Scripts\tox -e %TESTENV% -- -v --junitxml=junit.xml diff --git a/.gitignore b/.gitignore index 25cca2601..1916d4505 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__ /setuptools-*.egg /setuptools-*.zip /qutebrowser/git-commit-id +/qutebrowser/3rdparty /doc/*.html /README.html /CHANGELOG.html diff --git a/.travis.yml b/.travis.yml index a20f1ba9d..3761c38e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ install: - python scripts/dev/ci_install.py script: - - tox -e $TESTENV -- -p no:sugar -v --cov-report term tests + - tox -e $TESTENV -- -v --cov-report term tests after_success: - '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov' diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 50721f9dc..090c812fc 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -14,6 +14,31 @@ This project adheres to http://semver.org/[Semantic Versioning]. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +v0.6.0 (unreleased) +------------------- + +Added +~~~~~ + +- New `--quiet` argument for the `:debug-pyeval` command to not open a tab with + the results. Note `:debug-pyeval` is still only intended for debugging. + +Changed +~~~~~~~ + +- Pasting multiple lines via `:paste` now opens each line in a new tab. + +v0.5.1 +------ + +Fixed +~~~~~ + +- Fixed completion for various config values when using `:set`. +- Fixed config validation for various config values. +- Prevented an error being logged when a website with HTTP authentication was + opened on Windows. + v0.5.0 ------ diff --git a/INSTALL.asciidoc b/INSTALL.asciidoc index 3aeb09cad..aa46ab310 100644 --- a/INSTALL.asciidoc +++ b/INSTALL.asciidoc @@ -152,9 +152,29 @@ $ nix-env -i qutebrowser On Windows ---------- -You can either use one of the -https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone -packages or MSI installers], or install manually: +There are different ways to install qutebrowser on Windows: + +Prebuilt binaries +~~~~~~~~~~~~~~~~~ + +Prebuilt standalone packages and MSI installers +https://github.com/The-Compiler/qutebrowser/releases[are built] for every +release. + +https://chocolatey.org/packages/qutebrowser[Chocolatey package] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* PackageManagement PowerShell module +---- +PS C:\> Install-Package qutebrowser +---- +* Chocolatey's client +---- +C:\> choco install qutebrowser +---- + +Manual install +~~~~~~~~~~~~~~ * Use the installer from http://www.python.org/downloads[python.org] to get Python 3 (be sure to install pip). diff --git a/README.asciidoc b/README.asciidoc index 51c0f5803..768097314 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -160,6 +160,7 @@ Contributors, sorted by the number of commits in descending order: * ZDarian * John ShaggyTwoDope Jenkins * Peter Vilim +* Tarcisio Fedrizzi * Jonas Schürmann * Panagiotis Ktistakis * Jimmy @@ -176,6 +177,7 @@ Contributors, sorted by the number of commits in descending order: * jnphilipp * Tobias Patzl * Peter Michely +* Link * Larry Hynes * Johannes Altmanninger * Samir Benmendil @@ -186,6 +188,7 @@ Contributors, sorted by the number of commits in descending order: * Corentin Jule * zwarag * xd1le +* evan * dylan araps * Tim Harder * Thiago Barroso Perrotta diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 68f6a98a9..ddbd2c8ac 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -402,6 +402,8 @@ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+ Open a page from the clipboard. +If the pasted text contains newlines, each line gets opened in its own tab. + ==== optional arguments * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-t*+, +*--tab*+: Open in a new tab. @@ -1224,6 +1226,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser |Command|Description |<>|Print a list of all objects to the debug log. |<>|Print LRU cache stats. +|<>|Clear remembered SSL error answers. |<>|Show the debugging console. |<>|Crash for debugging purposes. |<>|Dump the current page's content to a file. @@ -1239,6 +1242,10 @@ Print a list of all objects to the debug log. === debug-cache-stats Print LRU cache stats. +[[debug-clear-ssl-errors]] +=== debug-clear-ssl-errors +Clear remembered SSL error answers. + [[debug-console]] === debug-console Show the debugging console. @@ -1266,13 +1273,16 @@ Dump the current page's content to a file. [[debug-pyeval]] === debug-pyeval -Syntax: +:debug-pyeval 's'+ +Syntax: +:debug-pyeval [*--quiet*] 's'+ Evaluate a python string and display the results as a web page. ==== positional arguments * +'s'+: The string to evaluate. +==== optional arguments +* +*-q*+, +*--quiet*+: Don't show the output in a new tab. + ==== note * This command does not split arguments after the last argument and handles quotes literally. * With this command, +;;+ is interpreted literally instead of splitting off a second command. diff --git a/pytest.ini b/pytest.ini index 606c46cf2..962fdeafe 100644 --- a/pytest.ini +++ b/pytest.ini @@ -11,6 +11,9 @@ markers = not_xvfb: Tests which can't be run with Xvfb. frozen: Tests which can only be run if sys.frozen is True. integration: Tests which test a bigger portion of code, run without coverage. + skip: Always skipped test. + pyqt531_or_newer: Needs PyQt 5.3.1 or newer. + xfail_norun: xfail the test with out running it flakes-ignore = UnusedImport UnusedVariable @@ -39,3 +42,6 @@ qt_log_ignore = ^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .* ^QXcbClipboard: SelectionRequest too old + ^QGeoclueMaster error creating GeoclueMasterClient\. + ^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127 +qt_wait_signal_raising = true diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1cc82a994..e1d293781 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -809,6 +809,9 @@ class CommandDispatcher: def paste(self, sel=False, tab=False, bg=False, window=False): """Open a page from the clipboard. + If the pasted text contains newlines, each line gets opened in its own + tab. + Args: sel: Use the primary selection instead of the clipboard. tab: Open in a new tab. @@ -825,12 +828,18 @@ class CommandDispatcher: text = clipboard.text(mode) if not text: raise cmdexc.CommandError("{} is empty.".format(target)) - log.misc.debug("{} contained: '{}'".format(target, text)) - try: - url = urlutils.fuzzy_url(text) - except urlutils.InvalidUrlError as e: - raise cmdexc.CommandError(e) - self._open(url, tab, bg, window) + log.misc.debug("{} contained: '{}'".format(target, + text.replace('\n', '\\n'))) + text_urls = enumerate(u for u in text.split('\n') if u) + for i, text_url in text_urls: + if not window and i > 0: + tab = False + bg = True + try: + url = urlutils.fuzzy_url(text_url) + except urlutils.InvalidUrlError as e: + raise cmdexc.CommandError(e) + self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window', count='count') @@ -1462,11 +1471,11 @@ class CommandDispatcher: webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToNextWord] - if sys.platform == 'win32': + if sys.platform == 'win32': # pragma: no cover act.append(QWebPage.MoveToPreviousChar) else: act = [QWebPage.SelectNextWord] - if sys.platform == 'win32': + if sys.platform == 'win32': # pragma: no cover act.append(QWebPage.SelectPreviousChar) for _ in range(count): for a in act: @@ -1483,11 +1492,11 @@ class CommandDispatcher: webview = self._current_widget() if not webview.selection_enabled: act = [QWebPage.MoveToNextWord] - if sys.platform != 'win32': + if sys.platform != 'win32': # pragma: no branch act.append(QWebPage.MoveToNextChar) else: act = [QWebPage.SelectNextWord] - if sys.platform != 'win32': + if sys.platform != 'win32': # pragma: no branch act.append(QWebPage.SelectNextChar) for _ in range(count): for a in act: @@ -1755,3 +1764,10 @@ class CommandDispatcher: QApplication.postEvent(receiver, press_event) QApplication.postEvent(receiver, release_event) + + @cmdutils.register(instance='command-dispatcher', scope='window', + debug=True) + def debug_clear_ssl_errors(self): + """Clear remembered SSL error answers.""" + nam = self._current_widget().page().networkAccessManager() + nam.clear_all_ssl_errors() diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 42adf2be6..20b94edb0 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -484,8 +484,10 @@ class HintManager(QObject): mode = QClipboard.Selection if sel else QClipboard.Clipboard urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) QApplication.clipboard().setText(urlstr, mode) - message.info(self._win_id, "URL yanked to {}".format( - "primary selection" if sel else "clipboard")) + msg = "Yanked URL to {}: {}".format( + "primary selection" if sel else "clipboard", + urlstr) + message.info(self._win_id, msg) def _run_cmd(self, url, context): """Run the command based on a hint URL. diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 595765871..a222f2713 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -19,6 +19,7 @@ """Our own QNetworkAccessManager.""" +import os import collections import netrc @@ -29,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError, from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, - urlutils) + urlutils, debug) from qutebrowser.browser import cookies from qutebrowser.browser.network import qutescheme, networkreply from qutebrowser.browser.network import filescheme @@ -62,6 +63,11 @@ class SslError(QSslError): except TypeError: return hash((self.certificate().toDer(), self.error())) + def __repr__(self): + return utils.get_repr( + self, error=debug.qenum_key(QSslError, self.error()), + string=self.errorString()) + class NetworkManager(QNetworkAccessManager): @@ -189,44 +195,51 @@ class NetworkManager(QNetworkAccessManager): """ errors = [SslError(e) for e in errors] ssl_strict = config.get('network', 'ssl-strict') + log.webview.debug("SSL errors {!r}, strict {}".format( + errors, ssl_strict)) + + try: + host_tpl = urlutils.host_tuple(reply.url()) + except ValueError: + host_tpl = None + is_accepted = False + is_rejected = False + else: + is_accepted = set(errors).issubset( + self._accepted_ssl_errors[host_tpl]) + is_rejected = set(errors).issubset( + self._rejected_ssl_errors[host_tpl]) + + if (ssl_strict and ssl_strict != 'ask') or is_rejected: + return + elif is_accepted: + reply.ignoreSslErrors() + return + if ssl_strict == 'ask': - try: - host_tpl = urlutils.host_tuple(reply.url()) - except ValueError: - host_tpl = None - is_accepted = False - is_rejected = False - else: - is_accepted = set(errors).issubset( - self._accepted_ssl_errors[host_tpl]) - is_rejected = set(errors).issubset( - self._rejected_ssl_errors[host_tpl]) - if is_accepted: + err_string = '\n'.join('- ' + err.errorString() for err in errors) + answer = self._ask('SSL errors - continue?\n{}'.format(err_string), + mode=usertypes.PromptMode.yesno, owner=reply) + if answer: reply.ignoreSslErrors() - elif is_rejected: - pass + err_dict = self._accepted_ssl_errors else: - err_string = '\n'.join('- ' + err.errorString() for err in - errors) - answer = self._ask('SSL errors - continue?\n{}'.format( - err_string), mode=usertypes.PromptMode.yesno, - owner=reply) - if answer: - reply.ignoreSslErrors() - d = self._accepted_ssl_errors - else: - d = self._rejected_ssl_errors - if host_tpl is not None: - d[host_tpl] += errors - elif ssl_strict: - pass + err_dict = self._rejected_ssl_errors + if host_tpl is not None: + err_dict[host_tpl] += errors else: for err in errors: # FIXME we might want to use warn here (non-fatal error) # https://github.com/The-Compiler/qutebrowser/issues/114 - message.error(self._win_id, - 'SSL error: {}'.format(err.errorString())) + message.error(self._win_id, 'SSL error: {}'.format( + err.errorString())) reply.ignoreSslErrors() + self._accepted_ssl_errors[host_tpl] += errors + + def clear_all_ssl_errors(self): + """Clear all remembered SSL errors.""" + self._accepted_ssl_errors.clear() + self._rejected_ssl_errors.clear() @pyqtSlot(QUrl) def clear_rejected_ssl_errors(self, url): @@ -244,7 +257,10 @@ class NetworkManager(QNetworkAccessManager): def on_authentication_required(self, reply, authenticator): """Called when a website needs authentication.""" user, password = None, None - if not hasattr(reply, "netrc_used"): + if not hasattr(reply, "netrc_used") and 'HOME' in os.environ: + # We'll get an OSError by netrc if 'HOME' isn't available in + # os.environ. We don't want to log that, so we prevent it + # altogether. reply.netrc_used = True try: net = netrc.netrc() diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 0ebaf38f9..cef34ea6b 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -258,6 +258,13 @@ class String(BaseType): self._basic_validation(value) if not value: return + + if self.valid_values is not None: + if value not in self.valid_values: + raise configexc.ValidationError( + value, "valid values: {}".format(', '.join( + self.valid_values))) + if self.forbidden is not None and any(c in value for c in self.forbidden): raise configexc.ValidationError(value, "may not contain the chars " @@ -270,7 +277,10 @@ class String(BaseType): "long!".format(self.maxlen)) def complete(self): - return self._completions + if self._completions is not None: + return self._completions + else: + return super().complete() class List(BaseType): diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index fb27a57c3..11d77f4c4 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -417,9 +417,6 @@ class MainWindow(QWidget): window=self.win_id) download_count = download_manager.rowCount() quit_texts = [] - # Close if set to never ask for confirmation - if 'never' in confirm_quit: - pass # Ask if multiple-tabs are open if 'multiple-tabs' in confirm_quit and tab_count > 1: quit_texts.append("{} {} open.".format( diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 047fa1396..2651fe4a8 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -229,8 +229,9 @@ class IPCServer(QObject): log.ipc.debug("In on_error with None socket!") return self._timer.stop() - log.ipc.debug("Socket error {}: {}".format( - self._socket.error(), self._socket.errorString())) + log.ipc.debug("Socket 0x{:x}: error {}: {}".format( + id(self._socket), self._socket.error(), + self._socket.errorString())) if err != QLocalSocket.PeerClosedError: raise SocketError("handling IPC connection", self._socket) @@ -241,13 +242,14 @@ class IPCServer(QObject): return if self._socket is not None: log.ipc.debug("Got new connection but ignoring it because we're " - "still handling another one.") + "still handling another one (0x{:x}).".format( + id(self._socket))) return socket = self._server.nextPendingConnection() if socket is None: log.ipc.debug("No new connection to handle.") return - log.ipc.debug("Client connected.") + log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket))) self._timer.start() self._socket = socket socket.readyRead.connect(self.on_ready_read) @@ -267,7 +269,8 @@ class IPCServer(QObject): @pyqtSlot() def on_disconnected(self): """Clean up socket when the client disconnected.""" - log.ipc.debug("Client disconnected.") + log.ipc.debug("Client disconnected from socket 0x{:x}.".format( + id(self._socket))) self._timer.stop() if self._socket is None: log.ipc.debug("In on_disconnected with None socket!") @@ -279,7 +282,8 @@ class IPCServer(QObject): def _handle_invalid_data(self): """Handle invalid data we got from a QLocalSocket.""" - log.ipc.error("Ignoring invalid IPC data.") + log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format( + id(self._socket))) self.got_invalid_data.emit() self._socket.error.connect(self.on_error) self._socket.disconnectFromServer() @@ -292,11 +296,12 @@ class IPCServer(QObject): # active for some reason. log.ipc.warning("In on_ready_read with None socket!") return - self._timer.start() + self._timer.stop() while self._socket is not None and self._socket.canReadLine(): data = bytes(self._socket.readLine()) self.got_raw.emit(data) - log.ipc.debug("Read from socket: {}".format(data)) + log.ipc.debug("Read from socket 0x{:x}: {}".format( + id(self._socket), data)) try: decoded = data.decode('utf-8') @@ -337,11 +342,13 @@ class IPCServer(QObject): cwd = json_data.get('cwd', None) self.got_args.emit(json_data['args'], json_data['target_arg'], cwd) + self._timer.start() @pyqtSlot() def on_timeout(self): """Cancel the current connection if it was idle for too long.""" - log.ipc.error("IPC connection timed out.") + log.ipc.error("IPC connection timed out " + "(socket 0x{:x}).".format(id(self._socket))) self._socket.disconnectFromServer() if self._socket is not None: # pragma: no cover # on_socket_disconnected sets it to None @@ -369,7 +376,8 @@ class IPCServer(QObject): def shutdown(self): """Shut down the IPC server cleanly.""" - log.ipc.debug("Shutting down IPC") + log.ipc.debug("Shutting down IPC (socket 0x{:x})".format( + id(self._socket))) if self._socket is not None: self._socket.deleteLater() self._socket = None diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index e89240f4b..d8d664c9f 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -35,6 +35,8 @@ from qutebrowser.config import style from qutebrowser.misc import consolewidget from PyQt5.QtCore import QUrl +# so it's available for :debug-pyeval +from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import @cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id') @@ -176,18 +178,23 @@ def debug_trace(expr=""): @cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) -def debug_pyeval(s): +def debug_pyeval(s, quiet=False): """Evaluate a python string and display the results as a web page. Args: s: The string to evaluate. + quiet: Don't show the output in a new tab. """ try: r = eval(s) out = repr(r) except Exception: out = traceback.format_exc() + qutescheme.pyeval_output = out - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='last-focused') - tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) + if quiet: + log.misc.debug("pyeval output: {}".format(out)) + else: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 307ade709..53637a466 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -168,6 +168,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): Return: A target QUrl to a search page or the original URL. """ + urlstr = urlstr.strip() expanded = os.path.expanduser(urlstr) if os.path.isabs(expanded): path = expanded @@ -181,11 +182,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): else: path = None - stripped = urlstr.strip() if path is not None and os.path.exists(path): log.url.debug("URL is a local file") url = QUrl.fromLocalFile(path) - elif (not do_search) or is_url(stripped): + elif (not do_search) or is_url(urlstr): # probably an address log.url.debug("URL is a fuzzy address") url = qurl_from_user_input(urlstr) @@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True): try: url = _get_search_url(urlstr) except ValueError: # invalid search engine - url = qurl_from_user_input(stripped) + url = qurl_from_user_input(urlstr) log.url.debug("Converting fuzzy term {} to URL -> {}".format( urlstr, url.toDisplayString())) if do_search and config.get('general', 'auto-search'): diff --git a/requirements.txt b/requirements.txt index 5ef20f84b..9c060aec4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,8 @@ Jinja2==2.8.0 MarkupSafe==0.23 -Pygments==2.0.2 +Pygments==2.1 pyPEG2==2.15.2 PyYAML==3.11 -# "ValueError: I/O operation on closed file" with pytest since 0.3.5 -# WORKAROUND for https://github.com/tartley/colorama/issues/81 -colorama==0.3.3 # rq.filter: <=0.3.3 +colorama==0.3.6 colorlog==2.6.0 cssutils==1.0.1 diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index e2fb79f43..e45e4e21d 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -100,6 +100,8 @@ PERFECT_FILES = [ 'qutebrowser/mainwindow/statusbar/tabindex.py'), ('tests/unit/mainwindow/statusbar/test_textbase.py', 'qutebrowser/mainwindow/statusbar/textbase.py'), + ('tests/unit/mainwindow/statusbar/test_prompt.py', + 'qutebrowser/mainwindow/statusbar/prompt.py'), ('tests/unit/config/test_configtypes.py', 'qutebrowser/config/configtypes.py'), @@ -133,6 +135,10 @@ PERFECT_FILES = [ ] +# 100% coverage because of integration tests, but no perfect unit tests yet. +WHITELISTED_FILES = [] + + class Skipped(Exception): """Exception raised when skipping coverage checks.""" @@ -199,7 +205,8 @@ def check(fileobj, perfect_files): text = "{} has {}% line and {}% branch coverage!".format( filename, line_cov, branch_cov) messages.append(Message(MsgType.insufficent_coverage, text)) - elif filename not in perfect_src_files and not is_bad: + elif (filename not in perfect_src_files and not is_bad and + filename not in WHITELISTED_FILES): text = ("{} has 100% coverage but is not in " "perfect_files!".format(filename)) messages.append(Message(MsgType.perfect_file, text)) diff --git a/tests/conftest.py b/tests/conftest.py index f36edc93c..5fe4cda91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,13 +39,16 @@ from helpers.messagemock import message_mock from qutebrowser.config import config from qutebrowser.utils import objreg -from PyQt5.QtCore import QEvent +from PyQt5.QtCore import QEvent, QSize, Qt, PYQT_VERSION +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from PyQt5.QtNetwork import QNetworkCookieJar import xvfbwrapper # Set hypothesis settings -hypothesis.Settings.default.strict = True # pylint: disable=no-member +hypothesis.settings.register_profile('default', + hypothesis.settings(strict=True)) +hypothesis.settings.load_profile('default') def _apply_platform_markers(item): @@ -62,6 +65,9 @@ def _apply_platform_markers(item): "Can only run when frozen"), ('not_xvfb', item.config.xvfb_display is not None, "Can't be run with Xvfb."), + ('skip', True, "Always skipped."), + ('pyqt531_or_newer', PYQT_VERSION < 0x050301, + "Needs PyQt 5.3.1 or newer"), ] for searched_marker, condition, default_reason in markers: @@ -108,7 +114,7 @@ def pytest_collection_modifyitems(items): item.add_marker('gui') if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''): if ('CI' in os.environ and - not os.environ.get('QUTE_NO_DISPLAY_OK', '')): + not os.environ.get('QUTE_NO_DISPLAY', '')): raise Exception("No display available on CI!") skip_marker = pytest.mark.skipif( True, reason="No DISPLAY available") @@ -124,6 +130,8 @@ def pytest_collection_modifyitems(items): item.add_marker(pytest.mark.integration) _apply_platform_markers(item) + if item.get_marker('xfail_norun'): + item.add_marker(pytest.mark.xfail(run=False)) def pytest_ignore_collect(path): @@ -161,6 +169,41 @@ class WinRegistryHelper: del objreg.window_registry[win_id] +class FakeStatusBar(QWidget): + + """Fake statusbar to test progressbar sizing.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.hbox = QHBoxLayout(self) + self.hbox.addStretch() + self.hbox.setContentsMargins(0, 0, 0, 0) + self.setAttribute(Qt.WA_StyledBackground, True) + self.setStyleSheet('background-color: red;') + + def minimumSizeHint(self): + return QSize(1, self.fontMetrics().height()) + + +@pytest.fixture +def fake_statusbar(qtbot): + """Fixture providing a statusbar in a container window.""" + container = QWidget() + qtbot.add_widget(container) + vbox = QVBoxLayout(container) + vbox.addStretch() + + statusbar = FakeStatusBar(container) + # to make sure container isn't GCed + # pylint: disable=attribute-defined-outside-init + statusbar.container = container + vbox.addWidget(statusbar) + + container.show() + qtbot.waitForWindowShown(container) + return statusbar + + @pytest.yield_fixture def win_registry(): """Fixture providing a window registry for win_id 0 and 1.""" @@ -373,7 +416,10 @@ def pytest_configure(config): if os.environ.get('DISPLAY', None) == '': # xvfbwrapper doesn't handle DISPLAY="" correctly del os.environ['DISPLAY'] - if sys.platform.startswith('linux') and not config.getoption('--no-xvfb'): + + if (sys.platform.startswith('linux') and + not config.getoption('--no-xvfb') and + 'QUTE_NO_DISPLAY' not in os.environ): assert 'QUTE_BUILDBOT' not in os.environ try: disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index fe79f992f..2d01a4aeb 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -21,6 +21,6 @@ """Things needed for integration testing.""" -from webserver import httpbin, httpbin_after_test +from webserver import httpbin, httpbin_after_test, ssl_server from quteprocess import quteproc_process, quteproc from testprocess import pytest_runtest_makereport diff --git a/tests/integration/data/caret.html b/tests/integration/data/caret.html index b704d2a83..985fce0e7 100644 --- a/tests/integration/data/caret.html +++ b/tests/integration/data/caret.html @@ -5,7 +5,7 @@ Caret mode -

one two three
eins zwei drei

+

one two three
eins zwei drei

four five six
vier fünf sechs

diff --git a/tests/integration/data/misc/hello.txt.html b/tests/integration/data/misc/hello.txt.html index 0a7b41c7a..5a59b8934 100644 --- a/tests/integration/data/misc/hello.txt.html +++ b/tests/integration/data/misc/hello.txt.html @@ -11,8 +11,10 @@ body .c { color: #408080; font-style: italic } /* Comment */ body .err { border: 1px solid #FF0000 } /* Error */ body .k { color: #008000; font-weight: bold } /* Keyword */ body .o { color: #666666 } /* Operator */ +body .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ body .c1 { color: #408080; font-style: italic } /* Comment.Single */ body .cs { color: #408080; font-style: italic } /* Comment.Special */ body .gd { color: #A00000 } /* Generic.Deleted */ @@ -75,8 +77,8 @@ body .il { color: #666666 } /* Literal.Number.Integer.Long */

1
-2
<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">Hello World!
-</pre></body></html>
+2
<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">Hello World!
+</pre></body></html>
 
diff --git a/tests/integration/data/numbers/10.txt b/tests/integration/data/numbers/10.txt new file mode 100644 index 000000000..e48b2f48c --- /dev/null +++ b/tests/integration/data/numbers/10.txt @@ -0,0 +1 @@ +ten diff --git a/tests/integration/data/numbers/11.txt b/tests/integration/data/numbers/11.txt new file mode 100644 index 000000000..5bace8b1c --- /dev/null +++ b/tests/integration/data/numbers/11.txt @@ -0,0 +1 @@ +eleven diff --git a/tests/integration/data/numbers/12.txt b/tests/integration/data/numbers/12.txt new file mode 100644 index 000000000..79e17a397 --- /dev/null +++ b/tests/integration/data/numbers/12.txt @@ -0,0 +1 @@ +twelve diff --git a/tests/integration/data/numbers/13.txt b/tests/integration/data/numbers/13.txt new file mode 100644 index 000000000..f6b12997d --- /dev/null +++ b/tests/integration/data/numbers/13.txt @@ -0,0 +1 @@ +thirteen diff --git a/tests/integration/data/numbers/14.txt b/tests/integration/data/numbers/14.txt new file mode 100644 index 000000000..ba0f38023 --- /dev/null +++ b/tests/integration/data/numbers/14.txt @@ -0,0 +1 @@ +fourteen diff --git a/tests/integration/data/numbers/8.txt b/tests/integration/data/numbers/8.txt new file mode 100644 index 000000000..6bb5f0b07 --- /dev/null +++ b/tests/integration/data/numbers/8.txt @@ -0,0 +1 @@ +eight diff --git a/tests/integration/data/numbers/9.txt b/tests/integration/data/numbers/9.txt new file mode 100644 index 000000000..01bf69b15 --- /dev/null +++ b/tests/integration/data/numbers/9.txt @@ -0,0 +1 @@ +nine diff --git a/tests/integration/data/prompt/geolocation.html b/tests/integration/data/prompt/geolocation.html new file mode 100644 index 000000000..89e7edd4c --- /dev/null +++ b/tests/integration/data/prompt/geolocation.html @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/tests/integration/data/prompt/jsalert.html b/tests/integration/data/prompt/jsalert.html new file mode 100644 index 000000000..d4af29850 --- /dev/null +++ b/tests/integration/data/prompt/jsalert.html @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tests/integration/data/prompt/jsconfirm.html b/tests/integration/data/prompt/jsconfirm.html new file mode 100644 index 000000000..292b5e2c8 --- /dev/null +++ b/tests/integration/data/prompt/jsconfirm.html @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tests/integration/data/prompt/jsprompt.html b/tests/integration/data/prompt/jsprompt.html new file mode 100644 index 000000000..bc4178a6f --- /dev/null +++ b/tests/integration/data/prompt/jsprompt.html @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tests/integration/data/prompt/notifications.html b/tests/integration/data/prompt/notifications.html new file mode 100644 index 000000000..a4e08e50d --- /dev/null +++ b/tests/integration/data/prompt/notifications.html @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/tests/integration/data/reload.txt b/tests/integration/data/reload.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/data/search.html b/tests/integration/data/search.html new file mode 100644 index 000000000..2eec560d6 --- /dev/null +++ b/tests/integration/data/search.html @@ -0,0 +1,21 @@ + + + + + Searching text on the page + + +

+ foo
+ Foo
+ Bar
+ bar
+ blüb
+ baz
+ Baz
+ BAZ
+ space travel
+ /slash
+

+ + diff --git a/tests/integration/data/ssl/cert.csr b/tests/integration/data/ssl/cert.csr new file mode 100644 index 000000000..7a95d69f4 --- /dev/null +++ b/tests/integration/data/ssl/cert.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICpTCCAY0CAQAwYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVzdCBjZXJ0aWZp +Y2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkBFhRtYWlsQHF1 +dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO77 +e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsLin4SO3iAd5ti +XOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM/dI1vS/LvBKH +OY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcTSDERr0DT0DY4 +oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0UyHzumuSkjIFV +G5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7TqsvC6R9E0HWhF +b4JJkPB3EDVEzWqQFgcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBC7JrJuHyF +YFiujBlXFZIQrPNW7FF28zqBuXLfviwVBF/sKmNMKwC0nUgmCb/wFPxv3yrj+7az +r29FWSGVhs6k15GVsqSwnbSJDznh/W1elWwpTo2GODMmRY3VeYSY9WiQUhe5KA5x +56p5Kgtl53wZzdl+Pi93xVYAZFWl2O3GFs4f+GCrORjHC7ejZoq6xfRzNLZbLF0a +QyptcnYaZSppDB/nZx4p75GKcj9qWXaJbT8mjqJdgRCFPyUkQjSY6WEEAP3LXrXx +ThZUekv81Jh+kPTZjSd1d24Bd0nFkQdFf8SRn21jnP+PrzipBOdvm+bT8dI/71xg +8ZJ631jogV4L +-----END CERTIFICATE REQUEST----- diff --git a/tests/integration/data/ssl/cert.pem b/tests/integration/data/ssl/cert.pem new file mode 100644 index 000000000..f9c0c55d1 --- /dev/null +++ b/tests/integration/data/ssl/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPDCCAiQCCQCHskwLQC4vHDANBgkqhkiG9w0BAQsFADBgMSUwIwYDVQQKDBxx +dXRlYnJvd3NlciB0ZXN0IGNlcnRpZmljYXRlMRIwEAYDVQQDDAlsb2NhbGhvc3Qx +IzAhBgkqhkiG9w0BCQEWFG1haWxAcXV0ZWJyb3dzZXIub3JnMB4XDTE2MDExMjE4 +NDYyM1oXDTI2MDEwOTE4NDYyM1owYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVz +dCBjZXJ0aWZpY2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkB +FhRtYWlsQHF1dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAO77e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsL +in4SO3iAd5tiXOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM +/dI1vS/LvBKHOY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcT +SDERr0DT0DY4oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0U +yHzumuSkjIFVG5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7Tq +svC6R9E0HWhFb4JJkPB3EDVEzWqQFgcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA +lTuJK8wseifpepUaWIev+59ulxxMzeippi+xqoYnjrNjINNdk5Wh+Dj7Crb5R8dn +afkC+XE9PMKEvKBmQZj/KVEL/G7bjZBA73oibKpBMWIdxaIwSFN2Xq4zKWLHESrb +2Wy8MiehZiSdgUtnmTPM0BlDmc6u9/0nLdCjsBoKYVOLw2FDcD1P8NOJT0dUjSUu +aYmUakcn+lQEjuBplrsGvL0vCGR/kzG2vwoTuGnx66HURuHU6E7yBTQ2diyhzOQc +sMwwDfrsY19K3IH6AuVcCgGit1LE/zCqMFQuFrIhYB5Mt5bLSeWVBDzKClxZB0Di +OxK2sWZvLdGLsFltKB+IJA== +-----END CERTIFICATE----- diff --git a/tests/integration/data/ssl/key.pem b/tests/integration/data/ssl/key.pem new file mode 100644 index 000000000..dbe3f87db --- /dev/null +++ b/tests/integration/data/ssl/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA7vt7pCqN4YOOry0MIZISL7ub9wMvo9tfzM00qhWmVygmOg8L +/oqZ2wuKfhI7eIB3m2Jc6k9CrIIGAFR3trE/lhrozquFYlphRzNQx3HxtGviZ1pN +t39SFUz90jW9L8u8Eoc5j7yTcTo15vmN4N7Z19eCiRREvhz+MhLUwO6a9gIC16gd +Fi+MZxNIMRGvQNPQNjig/KsiZptIdEZcvPR65Cwxjh31lNqSiZgfnZxEw0Q4oCtT +UBE1nRTIfO6a5KSMgVUbk8WAytrS2AA90IDWzHnVxBQE5jGeox2A4FOGietu6xhh +HXsPtOqy8LpH0TQdaEVvgkmQ8HcQNUTNapAWBwIDAQABAoIBADysrryEbVdHLm+9 +USooyuNBj5yMO4kvhkgaBXf1XTEdqW7uKQ5sJBnf+T5+5Ih4nWVe+NYoX3Yq4Nku +mOJSaCF1HYxzMb9B0RbhqW2puUMkbOvumnKvKajszjiTmj/LSymtGWkr6IdDzzGg +RGxGSCqrtaGV+soF1GfkLg35xnAUnwk3pfVqGyXl66+bCCWcqXZTUlOB55KEa+5F +9rkMlS6/X3DGZLvON7ZtZqZe7E8Foo9qU1VSHHfxIkS5P4UNxjf7woQogmhNTRT6 +tX0SmDQdP59sdFJ09Expr2AfSFxfkGuQf+JSG/JMprg0ub0ksw7UZvaW1uJNKL9I +XQSVPgECgYEA94DlPsGd8wWllMjOIEDkERUP2s4uJjPb6jodqewf9tuyxuwRnpOs +fb5uq7mMJXG3sszqom0q3DBoapNdCX1vTywWHKc1Nik5PT7jbEXFaRLfvA/F8WfF +6Rugm/S+nezTc7XhtDnOpfl+7wFSJy0we0C3RvxJqAaLaQRDobeNiQcCgYEA9y+z +wdXaOcJnC5bPO3ollFewX00WJaAAFpDnfqC3ALJx94/xJVJW6A7TZnKKJmWQ/bFz +0iuyhMe3Nd2yzAhl0qs0lmVe2V2tgJO/CVVP8OQmwlHKSZssDCjaBrHIkNwdL00j +qtSYg/FafLPL24AFSr25+sBn/FfxHTzlWVlWywECgYAUyjX3dIoQ/NtwyQFPgkPm +D2/agFEuElMZtLIDMPtqX///Z5r/SAZINbPUJuzXxFqa4U2gQS1Fe6d5tFEvV+L+ +soRU+dKlbwcI1vyBfsbbUaOLh4OoCIB+WTy/fOp6F4eXg6Km4egy1udLqj+9XLVi +1QfQJacGPy58rsgDkIiKBwKBgHtVtd91kNlZAolpyiTnIXEO/9XNZMuJNgIMczVf +g3A5mVvo2m3A09Qd8aUgaYYXD21F6YBohT5zWBrsb5YWapffDPItylGyyCtrjNpf +Uu/jJuO2Y7SuVCANEhxdALIm4fkECFPol+DdwESQgZsYGYvddrqC3l+ukYQBKn6W +cRQBAoGBAMA8tN67zOtZWalkokLHPDDK/TRUI/+Idc7xX7Rx/KZLuhfT26pLbe5Q +onbhe+TSq+4aYfUdcWJE2oM8DQn6CrNZFXKhz/0DLE+leASwwJLNCBbDdLjij2sy +7x2VeGKVG7V2KEhqcDUH/TO0e9PeGnz0vnebzN2+EZue6J9OTfLr +-----END RSA PRIVATE KEY----- diff --git a/tests/integration/data/ssl/privkey.pem b/tests/integration/data/ssl/privkey.pem new file mode 100644 index 000000000..5426cb849 --- /dev/null +++ b/tests/integration/data/ssl/privkey.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISAypJ52ykvkCAggA +MBQGCCqGSIb3DQMHBAgcPyy/O0hXkgSCBMgA4rZIvVKE73SsGCpJou1LgGAuPX1m +qyOPwRGC9T1p1HPaMcucIKpZPp5JSx3B9xwN/V+gpi3XXU1oTLaJhXwOpp8v106l +lR9Us91o4nUWVmo2C6nG1z/GSP573RBqjxChiHQchjT5UufKOi+/0elg6tgpu2cv +k+CLcgKp80dUr+UOPLAqIC2B+ex4BQHPrki+wbsTeMoZaXnPcTbl0OjABXbG6X4l +Gf2xftM7I+Wr/E7dnOEHGwUUH4hAzqflgUTHTZZtUDU7v99ggBBRux5vp9Zi2gWp +ksuAmxfPDMcE1Mpu+ZTZ3+cp4TWuWwKRpCX9USjmwnkhdEhqc/arHID/Db+SNP6z +lrdHY7BeAWcwDTo+4KZAEK7LKpAukRvpLcyvufo/smGaXsYytFz6Un8scSoySuqo +TEKyAioxNsOGJ2Xz5Jt+tdNLO/5W4jCuvwPx1GDlumwPcMHjDrXlZUa0qfoJCcun +lptbxZfqd7ouXLy1OF5FAsLs/iCmBwsyOS/qysFwq442WEwT/qn3ZoGBNkkJahu4 +OQ5sA14+nZHsBp1+iXZZxKmAERvQfFIRY0oe+Hmdwvyzb4mbIgFyPzU0CRFb+L1/ +x+eyrJymBhUL6FVQtoARcYD9g0ya1q3taJQ+JhGW1Ib+DtZzrV4CfDU6q5hWrOOX +d9/CAPM4NsjxuAfsy8nH+IOmcLyOXgfTgNFYVv5REnLVYOEoE630uBxnrOKchtpk +1iBSSGCPVcNioLQdUS3rPxtgkZkthar22xme7RDuUj1cg9p6Gu+6hyJIB7y41NdM +rLdZeHcRlgy56yb6YBXTnilPDCFhtOx6L8cXnL4CVYtg7ityq5khDSMVrtgiF8wQ +n6hDJbSLdFMQMdm9gIQ6lobZkHi4R3yk9S/rHtl7Gc3Set/2rqnxpyt5WsNHcBoy +uNkvGZuP9Pb6n4k7eR0/qX2cg3xycNI/uuxqDTpieHr+/lvOflqcj6+6Fq3Uvg65 +8rl5vzsrWArX/3/5sfGG6pqPaCjEHb0FeP8zzxzUTw6J46mzCuG90ERCJ/75wTmT +QD3oCtLtu/nI4MsR8I4VVn26u8FO63xDSk8xPvS6o8wU7EoZXH3+74EFf5beGgt8 +cMTS1Zil/MrtFOSC+MypihKCaYYjVr66F3h3I1RBef+bwuwOuQacaQCXkLHOWC3S +pH1iuKGt7lbpGPz103pkc4ssMYAc66nEYXf9I8MATP1aYOyP5o78yegWqgiUs+jd +frdgEsW3fsmeA655+5XZmXLHlmkpbb31KeVfCQXoTbHvExTqK91k73xn7/YRHLKq +vFKsz6cuWFnHmhb9gInH8iNzEM8DEJq+lEEhEi9XjeNmgnzd2vVl+3a2GPoy2h7u +VoGAwr7phI1PiD2aRoB7ZWiR4xxbwl8n+hHh63hSGNYHOeQ7JosPnqcwvHUZo4JZ +CXAI6T9snlZRg2G/BT627LYRGqu8piWl3FJXVaVd8lo6g4ZUrhyuV+48tJy1OvHT +gM1IATYnml6FPLXAqouxDrMKToAw45KOLrevGDDaQ91kxPrgEpK3fcnvH0FgJ16x +/N7uqBmo2XYZM6QxTrq1iShpGFoZ+DC3FOtDT3TKnsrlEUBLzgP3yqJje9Dn+BRs +td8= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/integration/features/backforward.feature b/tests/integration/features/backforward.feature index 70a448e2f..4b7cf247b 100644 --- a/tests/integration/features/backforward.feature +++ b/tests/integration/features/backforward.feature @@ -92,6 +92,13 @@ Feature: Going back and forward. - url: http://localhost:*/data/backforward/2.txt - url: http://localhost:*/data/backforward/3.txt + Scenario: Going back too much with count. + Given I open data/backforward/1.txt + When I open data/backforward/2.txt + And I open data/backforward/3.txt + And I run :back with count 3 + Then the error "At beginning of history." should be shown + Scenario: Going back with very big count. Given I open data/backforward/1.txt When I run :back with count 99999999999 @@ -132,3 +139,11 @@ Feature: Going back and forward. Given I open data/backforward/1.txt When I run :forward Then the error "At end of history." should be shown + + Scenario: Going forward too much with count. + Given I open data/backforward/1.txt + When I open data/backforward/2.txt + And I open data/backforward/3.txt + And I run :back with count 2 + And I run :forward with count 3 + Then the error "At end of history." should be shown diff --git a/tests/integration/features/caret.feature b/tests/integration/features/caret.feature index 79faae189..45c6cef9f 100644 --- a/tests/integration/features/caret.feature +++ b/tests/integration/features/caret.feature @@ -3,7 +3,7 @@ Feature: Caret mode Background: Given I open data/caret.html - And I run :enter-mode caret + And I run :tab-only ;; :enter-mode caret # document @@ -258,3 +258,63 @@ Feature: Caret mode Then the message "3 chars yanked to clipboard" should be shown. And the message "7 chars yanked to clipboard" should be shown. And the clipboard should contain "one two" + + # :drop-selection + + Scenario: :drop-selection + When I run :toggle-selection + And I run :move-to-end-of-word + And I run :drop-selection + And I run :yank-selected + Then the message "Nothing to yank" should be shown. + + # :follow-selected + + Scenario: :follow-selected without a selection + When I run :follow-selected + Then no crash should happen + + Scenario: :follow-selected with text + When I run :move-to-next-word + And I run :toggle-selection + And I run :move-to-end-of-word + And I run :follow-selected + Then no crash should happen + + Scenario: :follow-selected with link (with JS) + When I set content -> allow-javascript to true + And I run :toggle-selection + And I run :move-to-end-of-word + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: :follow-selected with link (without JS) + When I set content -> allow-javascript to false + And I run :toggle-selection + And I run :move-to-end-of-word + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: :follow-selected with --tab (with JS) + When I set content -> allow-javascript to true + And I run :tab-only + And I run :enter-mode caret + And I run :toggle-selection + And I run :move-to-end-of-word + And I run :follow-selected --tab + Then data/hello.txt should be loaded + And the following tabs should be open: + - data/caret.html + - data/hello.txt (active) + + Scenario: :follow-selected with --tab (without JS) + When I set content -> allow-javascript to false + And I run :tab-only + And I run :enter-mode caret + And I run :toggle-selection + And I run :move-to-end-of-word + And I run :follow-selected --tab + Then data/hello.txt should be loaded + And the following tabs should be open: + - data/caret.html + - data/hello.txt (active) diff --git a/tests/integration/features/conftest.py b/tests/integration/features/conftest.py index 89ed992d3..8929fc404 100644 --- a/tests/integration/features/conftest.py +++ b/tests/integration/features/conftest.py @@ -25,6 +25,7 @@ import json import os.path import logging import collections +import textwrap import pytest import yaml @@ -35,6 +36,11 @@ from PyQt5.QtGui import QClipboard from helpers import utils +class WaitForClipboardTimeout(Exception): + + """Raised when _wait_for_clipboard didn't get the expected message.""" + + def _clipboard_mode(qapp, what): """Get the QClipboard::Mode to use based on a string.""" if what == 'clipboard': @@ -68,6 +74,7 @@ def open_path_given(quteproc, path): It always opens a new tab, unlike "When I open ..." """ quteproc.open_path(path, new_tab=True) + quteproc.wait_for_load_finished(path) @bdd.given(bdd.parsers.parse("I run {command}")) @@ -93,17 +100,32 @@ def fresh_instance(quteproc): def open_path(quteproc, path): """Open a URL. - If used like "When I open ... in a new tab", the URL is opened ina new - tab. + If used like "When I open ... in a new tab", the URL is opened in a new + tab. With "... in a new window", it's opened in a new window. """ + new_tab = False + new_window = False + wait_for_load_finished = True + new_tab_suffix = ' in a new tab' + new_window_suffix = ' in a new window' + do_not_wait_suffix = ' without waiting' + if path.endswith(new_tab_suffix): path = path[:-len(new_tab_suffix)] new_tab = True - else: - new_tab = False + elif path.endswith(new_window_suffix): + path = path[:-len(new_window_suffix)] + new_window = True - quteproc.open_path(path, new_tab=new_tab) + if path.endswith(do_not_wait_suffix): + path = path[:-len(do_not_wait_suffix)] + wait_for_load_finished = False + + quteproc.open_path(path, new_tab=new_tab, new_window=new_window) + + if wait_for_load_finished: + quteproc.wait_for_load_finished(path) @bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}")) @@ -131,7 +153,7 @@ def run_command(quteproc, httpbin, command): @bdd.when(bdd.parsers.parse("I reload")) def reload(qtbot, httpbin, quteproc, command): """Reload and wait until a new request is received.""" - with qtbot.waitSignal(httpbin.new_request, raising=True): + with qtbot.waitSignal(httpbin.new_request): quteproc.send_cmd(':reload') @@ -142,8 +164,9 @@ def wait_until_loaded(quteproc, path): @bdd.when(bdd.parsers.re(r'I wait for (?Pregex )?"' - r'(?P[^"]+)" in the log')) -def wait_in_log(quteproc, is_regex, pattern): + r'(?P[^"]+)" in the log(?P or skip ' + r'the test)?')) +def wait_in_log(quteproc, is_regex, pattern, do_skip): """Wait for a given pattern in the qutebrowser log. If used like "When I wait for regex ... in the log" the argument is treated @@ -151,7 +174,9 @@ def wait_in_log(quteproc, is_regex, pattern): """ if is_regex: pattern = re.compile(pattern) - quteproc.wait_for(message=pattern) + + line = quteproc.wait_for(message=pattern, do_skip=bool(do_skip)) + line.expected = True @bdd.when(bdd.parsers.re(r'I wait for the (?Perror|message|warning) ' @@ -191,11 +216,33 @@ def fill_clipboard(qtbot, qapp, httpbin, what, content): clipboard.setText(content, mode) +@bdd.when(bdd.parsers.re(r'I put the following lines into the ' + r'(?Pprimary selection|clipboard):\n' + r'(?P.+)$', flags=re.DOTALL)) +def fill_clipboard_multiline(qtbot, qapp, httpbin, what, content): + fill_clipboard(qtbot, qapp, httpbin, what, textwrap.dedent(content)) + + ## Then @bdd.then(bdd.parsers.parse("{path} should be loaded")) -def path_should_be_loaded(httpbin, path): +def path_should_be_loaded(quteproc, path): + """Make sure the given path was loaded according to the log. + + This is usally the better check compared to "should be requested" as the + page could be loaded from local cache. + """ + url = quteproc.path_to_url(path) + pattern = re.compile( + r"load status for : LoadStatus\.success".format( + url=re.escape(url))) + quteproc.wait_for(message=pattern) + + +@bdd.then(bdd.parsers.parse("{path} should be requested")) +def path_should_be_requested(httpbin, path): """Make sure the given path was loaded from the webserver.""" httpbin.wait_for(verb='GET', path='/' + path) @@ -243,7 +290,8 @@ def should_be_logged(quteproc, is_regex, pattern): """Expect the given pattern on regex in the log.""" if is_regex: pattern = re.compile(pattern) - quteproc.wait_for(message=pattern) + line = quteproc.wait_for(message=pattern) + line.expected = True @bdd.then(bdd.parsers.parse('"{pattern}" should not be logged')) @@ -256,8 +304,7 @@ def ensure_not_logged(quteproc, pattern): 'logged')) def javascript_message_logged(quteproc, message): """Make sure the given message was logged via javascript.""" - quteproc.wait_for(category='js', function='javaScriptConsoleMessage', - message='[*] {}'.format(message)) + quteproc.wait_for_js(message) @bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be ' @@ -316,7 +363,24 @@ def check_contents(quteproc, filename): path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'data', os.path.join(*filename.split('/'))) with open(path, 'r', encoding='utf-8') as f: - assert content == f.read() + file_content = f.read() + assert content == file_content + + +@bdd.then(bdd.parsers.parse('the page should contain the plaintext "{text}"')) +def check_contents_plain(quteproc, text): + """Check the current page's content based on a substring.""" + content = quteproc.get_content().strip() + assert text in content + + +@bdd.then(bdd.parsers.parse('the json on the page should be:\n{text}')) +def check_contents_json(quteproc, text): + """Check the current page's content as json.""" + content = quteproc.get_content().strip() + expected = json.loads(text) + actual = json.loads(content) + assert actual == expected @bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}")) @@ -335,6 +399,7 @@ def check_open_tabs(quteproc, tabs): for i, line in enumerate(tabs): line = line.strip() + assert line.startswith('- ') line = line[2:] # remove "- " prefix if line.endswith(active_suffix): path = line[:-len(active_suffix)] @@ -359,16 +424,27 @@ def _wait_for_clipboard(qtbot, clipboard, mode, expected): while True: if clipboard.text(mode=mode) == expected: return - with qtbot.waitSignal(clipboard.changed, timeout=timeout) as blocker: + + # We need to poll the clipboard, as for some reason it can change with + # emitting changed (?). + with qtbot.waitSignal(clipboard.changed, timeout=100, raising=False): pass - if not blocker.signal_triggered or timer.hasExpired(timeout): + + if timer.hasExpired(timeout): mode_names = { QClipboard.Clipboard: 'clipboard', QClipboard.Selection: 'primary selection', } - raise WaitForTimeout( - "Timed out after {}ms waiting for {} in {}.".format( - timeout, expected, mode_names[mode])) + raise WaitForClipboardTimeout( + "Timed out after {timeout}ms waiting for {what}:\n" + " expected: {expected!r}\n" + " clipboard: {clipboard!r}\n" + " primary: {primary!r}.".format( + timeout=timeout, what=mode_names[mode], + expected=expected, + clipboard=clipboard.text(mode=QClipboard.Clipboard), + primary=clipboard.text(mode=QClipboard.Selection)) + ) @bdd.then(bdd.parsers.re(r'the (?Pprimary selection|clipboard) should ' @@ -382,6 +458,13 @@ def clipboard_contains(qtbot, qapp, httpbin, what, content): @bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}')) def clipboard_contains_multiline(qtbot, qapp, content): - expected = '\n'.join(line.strip() for line in content.splitlines()) + expected = textwrap.dedent(content) _wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard, expected) + + +@bdd.then("qutebrowser should quit") +def should_quit(qtbot, quteproc): + quteproc.exit_expected = True + with qtbot.waitSignal(quteproc.proc.finished, timeout=5000): + pass diff --git a/tests/integration/features/misc.feature b/tests/integration/features/misc.feature index 030731a80..38a77d6e6 100644 --- a/tests/integration/features/misc.feature +++ b/tests/integration/features/misc.feature @@ -117,8 +117,25 @@ Feature: Various utility commands. And I wait for "Focus object changed: *" in the log Then no crash should happen + # Different code path as an inspector got created now + Scenario: Inspector without developer extras (after smoke) + When I set general -> developer-extras to false + And I run :inspector + Then the error "Please enable developer-extras before using the webinspector!" should be shown + + # Different code path as an inspector got created now + @not_xvfb @posix + Scenario: Inspector smoke test 2 + When I set general -> developer-extras to true + And I run :inspector + And I wait for "Focus object changed: " in the log + And I run :inspector + And I wait for "Focus object changed: *" in the log + Then no crash should happen + # :stop/:reload + # WORKAROUND for https://bitbucket.org/cherrypy/cherrypy/pull-requests/117/ @not_osx Scenario: :stop Given I have a fresh instance @@ -135,13 +152,19 @@ Feature: Various utility commands. custom/redirect-later?delay=-1 # no request on / because we stopped the redirect - Scenario: :reload + Scenario: :stop with wrong count When I open data/hello.txt + And I run :tab-only + And I run :stop with count 2 + Then no crash should happen + + Scenario: :reload + When I open data/reload.txt And I run :reload - And I wait until data/hello.txt is loaded + And I wait until data/reload.txt is loaded Then the requests should be: - data/hello.txt - data/hello.txt + data/reload.txt + data/reload.txt Scenario: :reload with force When I open headers @@ -149,6 +172,12 @@ Feature: Various utility commands. And I wait until headers is loaded Then the header Cache-Control should be set to no-cache + Scenario: :reload with wrong count + When I open data/hello.txt + And I run :tab-only + And I run :reload with count 2 + Then no crash should happen + # :view-source Scenario: :view-source @@ -260,3 +289,42 @@ Feature: Various utility commands. And I set storage -> prompt-download-directory to false And I open data/misc/test.pdf Then "Download finished" should be logged + + # :print + + # Disabled because it causes weird segfaults and QPainter warnings in Qt... + @xfail_norun + Scenario: print preview + When I open data/hello.txt + And I run :print --preview + And I wait for "Focus object changed: *" in the log + And I run :debug-pyeval QApplication.instance().activeModalWidget().close() + Then no crash should happen + + # On Windows/OS X, we get a "QPrintDialog: Cannot be used on non-native + # printers" qWarning. + # + # Disabled because it causes weird segfaults and QPainter warnings in Qt... + @xfail_norun + Scenario: print + When I open data/hello.txt + And I run :print + And I wait for "Focus object changed: *" in the log or skip the test + And I run :debug-pyeval QApplication.instance().activeModalWidget().close() + Then no crash should happen + + # :pyeval + + Scenario: Running :pyeval + When I run :debug-pyeval 1+1 + And I wait until qute:pyeval is loaded + Then the page should contain the plaintext "2" + + Scenario: Causing exception in :pyeval + When I run :debug-pyeval 1/0 + And I wait until qute:pyeval is loaded + Then the page should contain the plaintext "ZeroDivisionError" + + Scenario: Running :pyeval with --quiet + When I run :debug-pyeval --quiet 1+1 + Then "pyeval output: 2" should be logged diff --git a/tests/integration/features/prompts.feature b/tests/integration/features/prompts.feature new file mode 100644 index 000000000..d3f77f6da --- /dev/null +++ b/tests/integration/features/prompts.feature @@ -0,0 +1,195 @@ +Feature: Prompts + Various prompts (javascript, SSL errors, authentification, etc.) + + Background: + Given I set general -> log-javascript-console to debug + + # Javascript + + Scenario: Javascript alert + When I open data/prompt/jsalert.html + And I click the button + And I wait for a prompt + And I run :prompt-accept + Then the javascript message "Alert done" should be logged + + Scenario: Using content -> ignore-javascript-alert + When I set content -> ignore-javascript-alert to true + And I open data/prompt/jsalert.html + And I click the button + Then the javascript message "Alert done" should be logged + + Scenario: Javascript confirm - yes + When I open data/prompt/jsconfirm.html + And I click the button + And I wait for a prompt + And I run :prompt-yes + Then the javascript message "confirm reply: true" should be logged + + Scenario: Javascript confirm - no + When I open data/prompt/jsconfirm.html + And I click the button + And I wait for a prompt + And I run :prompt-no + Then the javascript message "confirm reply: false" should be logged + + Scenario: Javascript confirm - aborted + When I open data/prompt/jsconfirm.html + And I click the button + And I wait for a prompt + And I run :leave-mode + Then the javascript message "confirm reply: false" should be logged + + @pyqt531_or_newer + Scenario: Javascript prompt + When I open data/prompt/jsprompt.html + And I click the button + And I wait for a prompt + And I press the keys "prompt test" + And I run :prompt-accept + Then the javascript message "Prompt reply: prompt test" should be logged + + @pyqt531_or_newer + Scenario: Rejected javascript prompt + When I open data/prompt/jsprompt.html + And I click the button + And I wait for a prompt + And I press the keys "prompt test" + And I run :leave-mode + Then the javascript message "Prompt reply: null" should be logged + + @pyqt531_or_newer + Scenario: Using content -> ignore-javascript-prompt + When I set content -> ignore-javascript-prompt to true + And I open data/prompt/jsprompt.html + And I click the button + Then the javascript message "Prompt reply: null" should be logged + + # SSL + + Scenario: SSL error with ssl-strict = false + When I run :debug-clear-ssl-errors + And I set network -> ssl-strict to false + And I load a SSL page + And I wait until the SSL page finished loading + Then the error "SSL error: *" should be shown + And the page should contain the plaintext "Hello World via SSL!" + + Scenario: SSL error with ssl-strict = true + When I run :debug-clear-ssl-errors + And I set network -> ssl-strict to true + And I load a SSL page + Then "Error while loading *: SSL handshake failed" should be logged + And the page should contain the plaintext "Unable to load page" + + Scenario: SSL error with ssl-strict = ask -> yes + When I run :debug-clear-ssl-errors + And I set network -> ssl-strict to ask + And I load a SSL page + And I wait for a prompt + And I run :prompt-yes + And I wait until the SSL page finished loading + Then the page should contain the plaintext "Hello World via SSL!" + + Scenario: SSL error with ssl-strict = ask -> no + When I run :debug-clear-ssl-errors + And I set network -> ssl-strict to ask + And I load a SSL page + And I wait for a prompt + And I run :prompt-no + Then "Error while loading *: SSL handshake failed" should be logged + And the page should contain the plaintext "Unable to load page" + + # Geolocation + + Scenario: Always rejecting geolocation + When I set content -> geolocation to false + And I open data/prompt/geolocation.html in a new tab + And I click the button + Then the javascript message "geolocation permission denied" should be logged + + Scenario: Always accepting geolocation + When I set content -> geolocation to true + And I open data/prompt/geolocation.html in a new tab + And I click the button + Then the javascript message "geolocation permission denied" should not be logged + + Scenario: geolocation with ask -> true + When I set content -> geolocation to ask + And I open data/prompt/geolocation.html in a new tab + And I click the button + And I wait for a prompt + And I run :prompt-yes + Then the javascript message "geolocation permission denied" should not be logged + + Scenario: geolocation with ask -> false + When I set content -> geolocation to ask + And I open data/prompt/geolocation.html in a new tab + And I click the button + And I wait for a prompt + And I run :prompt-no + Then the javascript message "geolocation permission denied" should be logged + + Scenario: geolocation with ask -> abort + When I set content -> geolocation to ask + And I open data/prompt/geolocation.html in a new tab + And I click the button + And I wait for a prompt + And I run :leave-mode + Then the javascript message "geolocation permission denied" should be logged + + # Notifications + + Scenario: Always rejecting notifications + When I set content -> notifications to false + And I open data/prompt/notifications.html in a new tab + And I click the button + Then the javascript message "notification permission denied" should be logged + + Scenario: Always accepting notifications + When I set content -> notifications to true + And I open data/prompt/notifications.html in a new tab + And I click the button + Then the javascript message "notification permission granted" should be logged + + Scenario: notifications with ask -> false + When I set content -> notifications to ask + And I open data/prompt/notifications.html in a new tab + And I click the button + And I wait for a prompt + And I run :prompt-no + Then the javascript message "notification permission denied" should be logged + + Scenario: notifications with ask -> true + When I set content -> notifications to ask + And I open data/prompt/notifications.html in a new tab + And I click the button + And I wait for a prompt + And I run :prompt-yes + Then the javascript message "notification permission granted" should be logged + + # This actually gives us a denied rather than an aborted + @xfail_norun + Scenario: notifications with ask -> abort + When I set content -> notifications to ask + And I open data/prompt/notifications.html in a new tab + And I click the button + And I wait for a prompt + And I run :leave-mode + Then the javascript message "notification permission aborted" should be logged + + # Page authentication + + Scenario: Successful webpage authentification + When I open basic-auth/user/password without waiting + And I wait for a prompt + And I press the keys "user" + And I run :prompt-accept + And I press the keys "password" + And I run :prompt-accept + And I wait until basic-auth/user/password is loaded + Then the json on the page should be: + { + "authenticated": true, + "user": "user" + } diff --git a/tests/integration/features/search.feature b/tests/integration/features/search.feature new file mode 100644 index 000000000..ebd2f065d --- /dev/null +++ b/tests/integration/features/search.feature @@ -0,0 +1,187 @@ +Feature: Searching on a page + Searching text on the page (like /foo) with different options. + + Background: + Given I open data/search.html + And I run :tab-only + + ## searching + + Scenario: Searching text + When I run :search foo + And I run :yank-selected + Then the clipboard should contain "foo" + + Scenario: Searching twice + When I run :search foo + And I run :search bar + And I run :yank-selected + Then the clipboard should contain "Bar" + + Scenario: Searching with --reverse + When I set general -> ignore-case to true + And I run :search -r foo + And I run :yank-selected + Then the clipboard should contain "Foo" + + Scenario: Searching without matches + When I run :search doesnotmatch + Then the warning "Text 'doesnotmatch' not found on page!" should be shown + + @xfail_norun + Scenario: Searching with / and spaces at the end (issue 874) + When I run :set-cmd-text -s /space + And I run :command-accept + And I run :yank-selected + Then the clipboard should contain "space " + + Scenario: Searching with / and slash in search term (issue 507) + When I run :set-cmd-text -s //slash + And I run :command-accept + And I run :yank-selected + Then the clipboard should contain "/slash" + + # This doesn't work because this is QtWebKit behaviour. + @xfail_norun + Scenario: Searching text with umlauts + When I run :search blub + Then the warning "Text 'blub' not found on page!" should be shown + + ## ignore-case + + Scenario: Searching text with ignore-case = true + When I set general -> ignore-case to true + And I run :search bar + And I run :yank-selected + Then the clipboard should contain "Bar" + + Scenario: Searching text with ignore-case = false + When I set general -> ignore-case to false + And I run :search bar + And I run :yank-selected + Then the clipboard should contain "bar" + + Scenario: Searching text with ignore-case = smart (lower-case) + When I set general -> ignore-case to smart + And I run :search bar + And I run :yank-selected + Then the clipboard should contain "Bar" + + Scenario: Searching text with ignore-case = smart (upper-case) + When I set general -> ignore-case to smart + And I run :search Foo + And I run :yank-selected + Then the clipboard should contain "Foo" # even though foo was first + + ## :search-next + + Scenario: Jumping to next match + When I set general -> ignore-case to true + And I run :search foo + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "Foo" + + Scenario: Jumping to next match with count + When I set general -> ignore-case to true + And I run :search baz + And I run :search-next with count 2 + And I run :yank-selected + Then the clipboard should contain "BAZ" + + Scenario: Jumping to next match with --reverse + When I set general -> ignore-case to true + And I run :search --reverse foo + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "foo" + + Scenario: Jumping to next match without search + # Make sure there was no search in the same window before + When I open data/search.html in a new window + And I run :search-next + Then no crash should happen + + Scenario: Repeating search in a second tab (issue #940) + When I open data/search.html in a new tab + And I run :search foo + And I run :tab-prev + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "foo" + + ## :search-prev + + Scenario: Jumping to previous match + When I set general -> ignore-case to true + And I run :search foo + And I run :search-next + And I run :search-prev + And I run :yank-selected + Then the clipboard should contain "foo" + + Scenario: Jumping to previous match with count + When I set general -> ignore-case to true + And I run :search baz + And I run :search-next + And I run :search-next + And I run :search-prev with count 2 + And I run :yank-selected + Then the clipboard should contain "baz" + + Scenario: Jumping to previous match with --reverse + When I set general -> ignore-case to true + And I run :search --reverse foo + And I run :search-next + And I run :search-prev + And I run :yank-selected + Then the clipboard should contain "Foo" + + Scenario: Jumping to previous match without search + # Make sure there was no search in the same window before + When I open data/search.html in a new window + And I run :search-prev + Then no crash should happen + + ## wrapping + + Scenario: Wrapping around page + When I set general -> wrap-search to true + And I run :search foo + And I run :search-next + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "foo" + + Scenario: Wrapping around page with wrap-search = false + When I set general -> wrap-search to false + And I run :search foo + And I run :search-next + And I run :search-next + Then the warning "Search hit BOTTOM without match for: foo" should be shown + + Scenario: Wrapping around page with --reverse + When I set general -> wrap-search to true + And I run :search --reverse foo + And I run :search-next + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "Foo" + + Scenario: Wrapping around page with wrap-search = false and --reverse + When I set general -> wrap-search to false + And I run :search --reverse foo + And I run :search-next + And I run :search-next + Then the warning "Search hit TOP without match for: foo" should be shown + + Scenario: Wrapping around page + When I set general -> wrap-search to true + And I run :search foo + And I run :search-next + And I run :search-next + And I run :yank-selected + Then the clipboard should contain "foo" + + # TODO: wrapping message with scrolling + # TODO: wrapping message without scrolling diff --git a/tests/integration/features/tabs.feature b/tests/integration/features/tabs.feature index 97ea18704..6e1753428 100644 --- a/tests/integration/features/tabs.feature +++ b/tests/integration/features/tabs.feature @@ -533,3 +533,105 @@ Feature: Tab management - tabs: - history: - url: http://localhost:*/data/numbers/2.txt + + # :undo + + Scenario: Undo without any closed tabs + Given I have a fresh instance + When I run :undo + Then the error "Nothing to undo!" should be shown + + Scenario: Undo closing a tab + When I open data/numbers/1.txt + And I run :tab-only + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt + And I run :tab-close + And I run :undo + Then the session should look like: + windows: + - tabs: + - history: + - url: about:blank + - url: http://localhost:*/data/numbers/1.txt + - active: true + history: + - url: http://localhost:*/data/numbers/2.txt + - url: http://localhost:*/data/numbers/3.txt + + Scenario: Undo with auto-created last tab + When I open data/hello.txt + And I run :tab-only + And I set tabs -> last-close to blank + And I run :tab-close + And I run :undo + Then the following tabs should be open: + - data/hello.txt (active) + + Scenario: Undo with auto-created last tab, with history + When I open data/hello.txt + And I open data/hello2.txt + And I run :tab-only + And I set tabs -> last-close to blank + And I run :tab-close + And I run :undo + Then the following tabs should be open: + - data/hello2.txt (active) + + Scenario: Undo with auto-created last tab (startpage) + When I open data/hello.txt + And I run :tab-only + And I set tabs -> last-close to startpage + And I set general -> startpage to http://localhost:(port)/data/numbers/4.txt,http://localhost:(port)/data/numbers/5.txt + And I run :tab-close + And I run :undo + Then the following tabs should be open: + - data/hello.txt (active) + + Scenario: Undo with auto-created last tab (default-page) + When I open data/hello.txt + And I run :tab-only + And I set tabs -> last-close to default-page + And I set general -> default-page to http://localhost:(port)/data/numbers/6.txt + And I run :tab-close + And I run :undo + Then the following tabs should be open: + - data/hello.txt (active) + + # last-close + + Scenario: last-close = blank + When I open data/hello.txt + And I set tabs -> last-close to blank + And I run :tab-only + And I run :tab-close + And I wait until about:blank is loaded + Then the following tabs should be open: + - about:blank (active) + + Scenario: last-close = startpage + When I set general -> startpage to http://localhost:(port)/data/numbers/7.txt,http://localhost:(port)/data/numbers/8.txt + And I set tabs -> last-close to startpage + And I open data/hello.txt + And I run :tab-only + And I run :tab-close + And I wait until data/numbers/7.txt is loaded + Then the following tabs should be open: + - data/numbers/7.txt (active) + + Scenario: last-close = default-page + When I set general -> default-page to http://localhost:(port)/data/numbers/9.txt + And I set tabs -> last-close to default-page + And I open data/hello.txt + And I run :tab-only + And I run :tab-close + And I wait until data/numbers/9.txt is loaded + Then the following tabs should be open: + - data/numbers/9.txt (active) + + Scenario: last-close = close + When I open data/hello.txt + And I set tabs -> last-close to close + And I run :tab-only + And I run :tab-close + Then qutebrowser should quit diff --git a/tests/integration/features/test_prompts.py b/tests/integration/features/test_prompts.py new file mode 100644 index 000000000..9126597fb --- /dev/null +++ b/tests/integration/features/test_prompts.py @@ -0,0 +1,50 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 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 . + +import pytest_bdd as bdd +bdd.scenarios('prompts.feature') + + +@bdd.when("I load a SSL page") +def load_ssl_page(quteproc, ssl_server): + quteproc.open_path('/', port=ssl_server.port, https=True) + # We don't call wait_for_load_finished here as we can get an SSL question. + + +@bdd.when("I wait until the SSL page finished loading") +def wait_ssl_page_finished_loading(quteproc, ssl_server): + quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True, + load_status='warn') + + +@bdd.when("I click the button") +def click_button(quteproc): + quteproc.send_cmd(':hint') + quteproc.send_cmd(':follow-hint a') + + +@bdd.when("I wait for a prompt") +def wait_for_prompt(quteproc): + quteproc.wait_for(message='Entering mode KeyMode.* (reason: question ' + 'asked)') + +@bdd.then("no prompt should be shown") +def no_prompt_shown(quteproc): + quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: ' + 'question asked)') diff --git a/tests/integration/features/test_search.py b/tests/integration/features/test_search.py new file mode 100644 index 000000000..b53f3120c --- /dev/null +++ b/tests/integration/features/test_search.py @@ -0,0 +1,26 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 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 . + +import pytest_bdd as bdd + +# pylint: disable=unused-import +from test_yankpaste import skip_with_broken_clipboard + + +bdd.scenarios('search.feature') diff --git a/tests/integration/features/test_urlmarks.py b/tests/integration/features/test_urlmarks.py index e46f72a36..170fdd30b 100644 --- a/tests/integration/features/test_urlmarks.py +++ b/tests/integration/features/test_urlmarks.py @@ -26,20 +26,49 @@ from helpers import utils bdd.scenarios('urlmarks.feature') -@bdd.then(bdd.parsers.parse('the bookmark file should contain "{expected}"')) -def bookmark_file_contains(quteproc, expected): - bookmark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks', +def _check_marks(quteproc, quickmarks, expected, contains): + """Make sure the given line does (not) exist in the bookmarks. + + Args: + quickmarks: True to check the quickmarks file instead of bookmarks. + expected: The line to search for. + contains: True if the line should be there, False otherwise. + """ + if quickmarks: + mark_file = os.path.join(quteproc.basedir, 'config', 'quickmarks') + else: + mark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks', 'urls') quteproc.clear_data() # So we don't match old messages quteproc.send_cmd(':save') - quteproc.wait_for(message='Saved to {}'.format(bookmark_file)) + quteproc.wait_for(message='Saved to {}'.format(mark_file)) - with open(bookmark_file, 'r', encoding='utf-8') as f: + with open(mark_file, 'r', encoding='utf-8') as f: lines = f.readlines() matched_line = any( utils.pattern_match(pattern=expected, value=line.rstrip('\n')) for line in lines) - assert matched_line, lines + assert matched_line == contains, lines + + +@bdd.then(bdd.parsers.parse('the bookmark file should contain "{line}"')) +def bookmark_file_contains(quteproc, line): + _check_marks(quteproc, quickmarks=False, expected=line, contains=True) + + +@bdd.then(bdd.parsers.parse('the bookmark file should not contain "{line}"')) +def bookmark_file_does_not_contain(quteproc, line): + _check_marks(quteproc, quickmarks=False, expected=line, contains=False) + + +@bdd.then(bdd.parsers.parse('the quickmark file should contain "{line}"')) +def quickmark_file_contains(quteproc, line): + _check_marks(quteproc, quickmarks=True, expected=line, contains=True) + + +@bdd.then(bdd.parsers.parse('the quickmark file should not contain "{line}"')) +def quickmark_file_does_not_contain(quteproc, line): + _check_marks(quteproc, quickmarks=True, expected=line, contains=False) diff --git a/tests/integration/features/test_yankpaste.py b/tests/integration/features/test_yankpaste.py index 756e59e0e..67f3df8db 100644 --- a/tests/integration/features/test_yankpaste.py +++ b/tests/integration/features/test_yankpaste.py @@ -33,7 +33,7 @@ def skip_with_broken_clipboard(qtbot, qapp): """ clipboard = qapp.clipboard() - with qtbot.waitSignal(clipboard.changed): + with qtbot.waitSignal(clipboard.changed, raising=False): clipboard.setText("Does this work?") if clipboard.text() != "Does this work?": diff --git a/tests/integration/features/urlmarks.feature b/tests/integration/features/urlmarks.feature index e4000443b..d3b7daaf5 100644 --- a/tests/integration/features/urlmarks.feature +++ b/tests/integration/features/urlmarks.feature @@ -1,7 +1,174 @@ Feature: quickmarks and bookmarks + ## bookmarks + Scenario: Saving a bookmark When I open data/title.html And I run :bookmark-add Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown And the bookmark file should contain "http://localhost:*/data/title.html Test title" + + Scenario: Saving a duplicate bookmark + Given I have a fresh instance + When I open data/title.html + And I run :bookmark-add + And I run :bookmark-add + Then the error "Bookmark already exists!" should be shown + + Scenario: Loading a bookmark + When I run :tab-only + And I run :bookmark-load http://localhost:(port)/data/numbers/1.txt + Then data/numbers/1.txt should be loaded + And the following tabs should be open: + - data/numbers/1.txt (active) + + Scenario: Loading a bookmark in a new tab + Given I open about:blank + When I run :tab-only + And I run :bookmark-load -t http://localhost:(port)/data/numbers/2.txt + Then data/numbers/2.txt should be loaded + And the following tabs should be open: + - about:blank + - data/numbers/2.txt (active) + + Scenario: Loading a bookmark in a background tab + Given I open about:blank + When I run :tab-only + And I run :bookmark-load -b http://localhost:(port)/data/numbers/3.txt + Then data/numbers/3.txt should be loaded + And the following tabs should be open: + - about:blank (active) + - data/numbers/3.txt + + Scenario: Loading a bookmark in a new window + Given I open about:blank + When I run :tab-only + And I run :bookmark-load -w http://localhost:(port)/data/numbers/4.txt + And I wait until data/numbers/4.txt is loaded + Then the session should look like: + windows: + - tabs: + - active: true + history: + - active: true + url: about:blank + - tabs: + - active: true + history: + - active: true + url: http://localhost:*/data/numbers/4.txt + + Scenario: Loading a bookmark with -t and -b + When I run :bookmark-load -t -b about:blank + Then the error "Only one of -t/-b/-w can be given!" should be shown + + Scenario: Deleting a bookmark which does not exist + When I run :bookmark-del doesnotexist + Then the error "Bookmark 'doesnotexist' not found!" should be shown + + Scenario: Deleting a bookmark + When I open data/numbers/5.txt + And I run :bookmark-add + And I run :bookmark-del http://localhost:(port)/data/numbers/5.txt + Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt " + + ## quickmarks + + Scenario: Saving a quickmark (:quickmark-add) + When I run :quickmark-add http://localhost:(port)/data/numbers/6.txt six + Then the quickmark file should contain "six http://localhost:*/data/numbers/6.txt" + + Scenario: Saving a quickmark (:quickmark-save) + When I open http://localhost:(port)/data/numbers/7.txt + And I run :quickmark-save + And I wait for "Entering mode KeyMode.prompt (reason: question asked)" in the log + And I press the keys "seven" + And I press the keys "" + Then the quickmark file should contain "seven http://localhost:*/data/numbers/7.txt" + + Scenario: Saving a duplicate quickmark (without override) + When I run :quickmark-add http://localhost:(port)/data/numbers/8.txt eight + And I run :quickmark-add http://localhost:(port)/data/numbers/8_2.txt eight + And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log + And I run :prompt-no + Then the quickmark file should contain "eight http://localhost:*/data/numbers/8.txt" + + Scenario: Saving a duplicate quickmark (with override) + When I run :quickmark-add http://localhost:(port)/data/numbers/9.txt nine + And I run :quickmark-add http://localhost:(port)/data/numbers/9_2.txt nine + And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log + And I run :prompt-yes + Then the quickmark file should contain "nine http://localhost:*/data/numbers/9_2.txt" + + Scenario: Adding a quickmark with an empty name + When I run :quickmark-add about:blank "" + Then the error "Can't set mark with empty name!" should be shown + + Scenario: Adding a quickmark with an empty URL + When I run :quickmark-add "" foo + Then the error "Can't set mark with empty URL!" should be shown + + Scenario: Loading a quickmark + Given I have a fresh instance + When I run :quickmark-add http://localhost:(port)/data/numbers/10.txt ten + And I run :quickmark-load ten + Then data/numbers/10.txt should be loaded + And the following tabs should be open: + - data/numbers/10.txt (active) + + Scenario: Loading a quickmark in a new tab + Given I open about:blank + When I run :tab-only + And I run :quickmark-add http://localhost:(port)/data/numbers/11.txt eleven + And I run :quickmark-load -t eleven + Then data/numbers/11.txt should be loaded + And the following tabs should be open: + - about:blank + - data/numbers/11.txt (active) + + Scenario: Loading a quickmark in a background tab + Given I open about:blank + When I run :tab-only + And I run :quickmark-add http://localhost:(port)/data/numbers/12.txt twelve + And I run :quickmark-load -b twelve + Then data/numbers/12.txt should be loaded + And the following tabs should be open: + - about:blank (active) + - data/numbers/12.txt + + Scenario: Loading a quickmark in a new window + Given I open about:blank + When I run :tab-only + And I run :quickmark-add http://localhost:(port)/data/numbers/13.txt thirteen + And I run :quickmark-load -w thirteen + And I wait until data/numbers/13.txt is loaded + Then the session should look like: + windows: + - tabs: + - active: true + history: + - active: true + url: about:blank + - tabs: + - active: true + history: + - active: true + url: http://localhost:*/data/numbers/13.txt + + Scenario: Loading a quickmark which does not exist + When I run :quickmark-load -b doesnotexist + Then the error "Quickmark 'doesnotexist' does not exist!" should be shown + + Scenario: Loading a quickmark with -t and -b + When I run :quickmark-add http://localhost:(port)/data/numbers/14.txt fourteen + When I run :quickmark-load -t -b fourteen + Then the error "Only one of -t/-b/-w can be given!" should be shown + + Scenario: Deleting a quickmark which does not exist + When I run :quickmark-del doesnotexist + Then the error "Quickmark 'doesnotexist' not found!" should be shown + + Scenario: Deleting a quickmark + When I run :quickmark-add http://localhost:(port)/data/numbers/15.txt fifteen + And I run :quickmark-del fifteen + Then the quickmark file should not contain "fourteen http://localhost:*/data/numbers/15.txt " diff --git a/tests/integration/features/yankpaste.feature b/tests/integration/features/yankpaste.feature index a9bdb713a..b09bf7ce5 100644 --- a/tests/integration/features/yankpaste.feature +++ b/tests/integration/features/yankpaste.feature @@ -98,3 +98,75 @@ Feature: Yanking and pasting. history: - active: true url: http://localhost:*/data/hello.txt + + Scenario: Pasting an invalid URL + When I set general -> auto-search to false + And I put "foo bar" into the clipboard + And I run :paste + Then the error "Invalid URL" should be shown + + Scenario: Pasting multiple urls in a new tab + Given I have a fresh instance + When I put the following lines into the clipboard: + http://localhost:(port)/data/hello.txt + http://localhost:(port)/data/hello2.txt + http://localhost:(port)/data/hello3.txt + And I run :paste -t + And I wait until data/hello.txt is loaded + And I wait until data/hello2.txt is loaded + And I wait until data/hello3.txt is loaded + Then the following tabs should be open: + - about:blank + - data/hello.txt (active) + - data/hello2.txt + - data/hello3.txt + + Scenario: Pasting multiple urls in a background tab + Given I open about:blank + When I run :tab-only + And I put the following lines into the clipboard: + http://localhost:(port)/data/hello.txt + http://localhost:(port)/data/hello2.txt + http://localhost:(port)/data/hello3.txt + And I run :paste -b + And I wait until data/hello.txt is loaded + And I wait until data/hello2.txt is loaded + And I wait until data/hello3.txt is loaded + Then the following tabs should be open: + - about:blank (active) + - data/hello.txt + - data/hello2.txt + - data/hello3.txt + + Scenario: Pasting multiple urls in new windows + Given I have a fresh instance + When I put the following lines into the clipboard: + http://localhost:(port)/data/hello.txt + http://localhost:(port)/data/hello2.txt + http://localhost:(port)/data/hello3.txt + And I run :paste -w + And I wait until data/hello.txt is loaded + And I wait until data/hello2.txt is loaded + And I wait until data/hello3.txt is loaded + Then the session should look like: + windows: + - tabs: + - active: true + history: + - active: true + url: about:blank + - tabs: + - active: true + history: + - active: true + url: http://localhost:*/data/hello.txt + - tabs: + - active: true + history: + - active: true + url: http://localhost:*/data/hello2.txt + - tabs: + - active: true + history: + - active: true + url: http://localhost:*/data/hello3.txt diff --git a/tests/integration/features/zoom.feature b/tests/integration/features/zoom.feature index d0dd4494e..4b0b1800b 100644 --- a/tests/integration/features/zoom.feature +++ b/tests/integration/features/zoom.feature @@ -55,3 +55,7 @@ Feature: Zooming in and out Scenario: Setting zoom with very big count When I run :zoom with count 99999999999 Then the message "Zoom level: 99999999999%" should be shown + + Scenario: Setting zoom with argument and count + When I run :zoom 50 with count 60 + Then the error "Both count and argument given!" should be shown diff --git a/tests/integration/quteprocess.py b/tests/integration/quteprocess.py index 58f9f59a1..74d3487b0 100644 --- a/tests/integration/quteprocess.py +++ b/tests/integration/quteprocess.py @@ -19,6 +19,7 @@ """Fixtures to run qutebrowser in a QProcess and communicate.""" +import os import re import sys import time @@ -35,6 +36,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl import testprocess from qutebrowser.misc import ipc from qutebrowser.utils import log, utils +from helpers import utils as testutils def is_ignored_qt_message(message): @@ -169,12 +171,7 @@ class QuteProc(testprocess.Process): else: raise - # WORKAROUND for https://bitbucket.org/logilab/pylint/issues/717/ - # we should switch to generated-members after that - # pylint: disable=no-member - if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or - pytest.config.getoption('--verbose')): - self._log(line) + self._log(line) start_okay_message_load = ( "load status for logging.INFO: + elif self._is_error_logline(log_line): self.got_error.emit() return log_line @@ -214,7 +211,7 @@ class QuteProc(testprocess.Process): 'about:blank'] return executable, args - def path_to_url(self, path): + def path_to_url(self, path, *, port=None, https=False): """Get a URL based on a filename for the localhost webserver. URLs like about:... and qute:... are handled specially and returned @@ -223,18 +220,53 @@ class QuteProc(testprocess.Process): if path.startswith('about:') or path.startswith('qute:'): return path else: - return 'http://localhost:{}/{}'.format( - self._httpbin.port, + return '{}://localhost:{}/{}'.format( + 'https' if https else 'http', + self._httpbin.port if port is None else port, path if path != '/' else '') + def wait_for_js(self, message): + """Wait for the given javascript console message.""" + self.wait_for(category='js', function='javaScriptConsoleMessage', + message='[*] {}'.format(message)) + + def _is_error_logline(self, msg): + """Check if the given LogLine is some kind of error message.""" + is_js_error = (msg.category == 'js' and + msg.function == 'javaScriptConsoleMessage' and + testutils.pattern_match(pattern='[*] [FAIL] *', + value=msg.message)) + return msg.loglevel > logging.INFO or is_js_error + + def _maybe_skip(self): + """Skip the test if [SKIP] lines were logged.""" + skip_texts = [] + + for msg in self._data: + if (msg.category == 'js' and + msg.function == 'javaScriptConsoleMessage' and + testutils.pattern_match(pattern='[*] [SKIP] *', + value=msg.message)): + skip_texts.append(msg.message.partition(' [SKIP] ')[2]) + + if skip_texts: + pytest.skip(', '.join(skip_texts)) + def after_test(self): bad_msgs = [msg for msg in self._data - if msg.loglevel > logging.INFO and not msg.expected] - super().after_test() - if bad_msgs: - text = 'Logged unexpected errors:\n\n' + '\n'.join( - str(e) for e in bad_msgs) - pytest.fail(text, pytrace=False) + if self._is_error_logline(msg) and not msg.expected] + + try: + if bad_msgs: + text = 'Logged unexpected errors:\n\n' + '\n'.join( + str(e) for e in bad_msgs) + # We'd like to use pytrace=False here but don't as a WORKAROUND + # for https://github.com/pytest-dev/pytest/issues/1316 + pytest.fail(text) + else: + self._maybe_skip() + finally: + super().after_test() def send_cmd(self, command, count=None): """Send a command to the running qutebrowser instance.""" @@ -269,14 +301,19 @@ class QuteProc(testprocess.Process): yield self.set_setting(sect, opt, old_value) - def open_path(self, path, new_tab=False): + def open_path(self, path, *, new_tab=False, new_window=False, port=None, + https=False): """Open the given path on the local webserver in qutebrowser.""" - url = self.path_to_url(path) + if new_tab and new_window: + raise ValueError("new_tab and new_window given!") + + url = self.path_to_url(path, port=port, https=https) if new_tab: self.send_cmd(':open -t ' + url) + elif new_window: + self.send_cmd(':open -w ' + url) else: self.send_cmd(':open ' + url) - self.wait_for_load_finished(path) def mark_expected(self, category=None, loglevel=None, message=None): """Mark a given logging message as expected.""" @@ -284,16 +321,24 @@ class QuteProc(testprocess.Process): message=message) line.expected = True - def wait_for_load_finished(self, path, timeout=15000): + def wait_for_load_finished(self, path, *, port=None, https=False, + timeout=None, load_status='success'): """Wait until any tab has finished loading.""" - url = self.path_to_url(path) + if timeout is None: + if 'CI' in os.environ: + timeout = 15000 + else: + timeout = 5000 + + url = self.path_to_url(path, port=port, https=https) # We really need the same representation that the webview uses in its # __repr__ url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100) pattern = re.compile( - r"(load status for : LoadStatus.success|fetch: " - r"PyQt5.QtCore.QUrl\('{url}'\) -> .*)".format(url=re.escape(url))) + r"(load status for : LoadStatus\.{load_status}|fetch: " + r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format( + load_status=re.escape(load_status), url=re.escape(url))) self.wait_for(message=pattern, timeout=timeout) def get_session(self): diff --git a/tests/integration/test_mhtml_e2e.py b/tests/integration/test_mhtml_e2e.py index 8f4a18bf5..14dac2a31 100644 --- a/tests/integration/test_mhtml_e2e.py +++ b/tests/integration/test_mhtml_e2e.py @@ -87,7 +87,10 @@ def test_mhtml(test_name, download_dir, quteproc, httpbin): 'data', 'downloads', 'mhtml', test_name) test_path = 'data/downloads/mhtml/{}'.format(test_name) - quteproc.open_path('{}/{}.html'.format(test_path, test_name)) + url_path = '{}/{}.html'.format(test_path, test_name) + quteproc.open_path(url_path) + quteproc.wait_for_load_finished(url_path) + download_dest = os.path.join(download_dir.location, '{}-downloaded.mht'.format(test_name)) diff --git a/tests/integration/test_quteprocess.py b/tests/integration/test_quteprocess.py index 307907c86..acb99aa65 100644 --- a/tests/integration/test_quteprocess.py +++ b/tests/integration/test_quteprocess.py @@ -29,22 +29,54 @@ import testprocess from qutebrowser.utils import log -def test_quteproc_error_message(qtbot, quteproc): +@pytest.mark.parametrize('cmd', [ + ':message-error test', + ':jseval console.log("[FAIL] test");' +]) +def test_quteproc_error_message(qtbot, quteproc, cmd): """Make sure the test fails with an unexpected error message.""" - with qtbot.waitSignal(quteproc.got_error, raising=True): - quteproc.send_cmd(':message-error test') + with qtbot.waitSignal(quteproc.got_error): + quteproc.send_cmd(cmd) # Usually we wouldn't call this from inside a test, but here we force the # error to occur during the test rather than at teardown time. with pytest.raises(pytest.fail.Exception): quteproc.after_test() +def test_quteproc_skip_via_js(qtbot, quteproc): + with pytest.raises(pytest.skip.Exception) as excinfo: + quteproc.send_cmd(':jseval console.log("[SKIP] test");') + quteproc.wait_for_js('[SKIP] test') + + # Usually we wouldn't call this from inside a test, but here we force + # the error to occur during the test rather than at teardown time. + quteproc.after_test() + + assert str(excinfo.value) == 'test' + + +def test_quteproc_skip_and_wait_for(qtbot, quteproc): + """This test will skip *again* during teardown, but we don't care.""" + with pytest.raises(pytest.skip.Exception): + quteproc.send_cmd(':jseval console.log("[SKIP] foo");') + quteproc.wait_for_js("[SKIP] foo") + quteproc.wait_for(message='This will not match') + + def test_qt_log_ignore(qtbot, quteproc): """Make sure the test passes when logging a qt_log_ignore message.""" - with qtbot.waitSignal(quteproc.got_error, raising=True): + with qtbot.waitSignal(quteproc.got_error): quteproc.send_cmd(':message-error "SpellCheck: test"') +def test_quteprocess_quitting(qtbot, quteproc_process): + """When qutebrowser quits, after_test should fail.""" + with qtbot.waitSignal(quteproc_process.proc.finished, timeout=5000): + quteproc_process.send_cmd(':quit') + with pytest.raises(testprocess.ProcessExited): + quteproc_process.after_test() + + @pytest.mark.parametrize('data, attrs', [ ( # Normal message diff --git a/tests/integration/test_testprocess.py b/tests/integration/test_testprocess.py index ada11c707..4c9a259d3 100644 --- a/tests/integration/test_testprocess.py +++ b/tests/integration/test_testprocess.py @@ -74,6 +74,29 @@ class PythonProcess(testprocess.Process): return (sys.executable, ['-c', ';'.join(code)]) +class QuitPythonProcess(testprocess.Process): + + """A testprocess which quits immediately.""" + + def __init__(self): + super().__init__() + self.proc.setReadChannel(QProcess.StandardOutput) + + def _parse_line(self, line): + print("LINE: {}".format(line)) + if line.strip() == 'ready': + self.ready.emit() + return testprocess.Line(line) + + def _executable_args(self): + code = [ + 'import sys', + 'print("ready")', + 'sys.exit(0)', + ] + return (sys.executable, ['-c', ';'.join(code)]) + + @pytest.yield_fixture def pyproc(): proc = PythonProcess() @@ -81,6 +104,35 @@ def pyproc(): proc.terminate() +@pytest.yield_fixture +def quit_pyproc(): + proc = QuitPythonProcess() + yield proc + proc.terminate() + + +def test_quitting_process(qtbot, quit_pyproc): + with qtbot.waitSignal(quit_pyproc.proc.finished): + quit_pyproc.start() + with pytest.raises(testprocess.ProcessExited): + quit_pyproc.after_test() + + +def test_quitting_process_expected(qtbot, quit_pyproc): + quit_pyproc.exit_expected = True + with qtbot.waitSignal(quit_pyproc.proc.finished): + quit_pyproc.start() + quit_pyproc.after_test() + + +def test_wait_signal_raising(qtbot): + """testprocess._wait_signal should raise by default.""" + proc = testprocess.Process() + with pytest.raises(qtbot.SignalTimeoutError): + with proc._wait_signal(proc.proc.started, timeout=0): + pass + + class TestWaitFor: def test_successful(self, pyproc): @@ -144,6 +196,13 @@ class TestWaitFor: with pytest.raises(TypeError): pyproc.wait_for() + def test_do_skip(self, pyproc): + """Test wait_for when getting no text at all, with do_skip.""" + pyproc.code = "pass" + pyproc.start() + with pytest.raises(pytest.skip.Exception): + pyproc.wait_for(data="foobar", timeout=100, do_skip=True) + class TestEnsureNotLogged: diff --git a/tests/integration/test_webserver.py b/tests/integration/test_webserver.py index 5794d3573..8588a1a62 100644 --- a/tests/integration/test_webserver.py +++ b/tests/integration/test_webserver.py @@ -33,7 +33,7 @@ import pytest ('/data/hello.txt', 'Hello World!', True), ]) def test_httpbin(httpbin, qtbot, path, content, expected): - with qtbot.waitSignal(httpbin.new_request, raising=True, timeout=100): + with qtbot.waitSignal(httpbin.new_request, timeout=100): url = 'http://localhost:{}{}'.format(httpbin.port, path) try: response = urllib.request.urlopen(url) diff --git a/tests/integration/testprocess.py b/tests/integration/testprocess.py index b349fd4a5..75972ab3f 100644 --- a/tests/integration/testprocess.py +++ b/tests/integration/testprocess.py @@ -72,11 +72,21 @@ class Line: return '{}({!r})'.format(self.__class__.__name__, self.data) +def _render_log(data, threshold=50): + """Shorten the given log without -v and convert to a string.""" + # pylint: disable=no-member + if len(data) > threshold and not pytest.config.getoption('--verbose'): + msg = '[{} lines suppressed, use -v to show]'.format( + len(data) - threshold) + data = [msg] + data[-threshold:] + return '\n'.join(data) + + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): """Add qutebrowser/httpbin sections to captured output if a test failed.""" outcome = yield - if call.when != 'call': + if call.when not in ['call', 'teardown']: return report = outcome.get_result() @@ -91,12 +101,16 @@ def pytest_runtest_makereport(item, call): # actually a tuple. This is handled similarily in pytest-qt too. return + # pylint: disable=no-member + if pytest.config.getoption('--capture') == 'no': + # Already printed live + return + if quteproc_log is not None: report.longrepr.addsection("qutebrowser output", - '\n'.join(quteproc_log)) + _render_log(quteproc_log)) if httpbin_log is not None: - report.longrepr.addsection("httpbin output", - '\n'.join(httpbin_log)) + report.longrepr.addsection("httpbin output", _render_log(httpbin_log)) class Process(QObject): @@ -109,6 +123,7 @@ class Process(QObject): _invalid: A list of lines which could not be parsed. _data: A list of parsed lines. proc: The QProcess for the underlying process. + exit_expected: Whether the process is expected to quit. Signals: ready: Emitted when the server finished starting up. @@ -126,9 +141,13 @@ class Process(QObject): self._data = [] self.proc = QProcess() self.proc.setReadChannel(QProcess.StandardError) + self.exit_expected = False def _log(self, line): """Add the given line to the captured log output.""" + # pylint: disable=no-member + if pytest.config.getoption('--capture') == 'no': + print(line) self.captured_log.append(line) def _parse_line(self, line): @@ -225,8 +244,9 @@ class Process(QObject): raise InvalidLine(self._invalid) self.clear_data() - if not self.is_running(): + if not self.is_running() and not self.exit_expected: raise ProcessExited + self.exit_expected = False def clear_data(self): """Clear the collected data.""" @@ -287,7 +307,41 @@ class Process(QObject): return line return None - def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs): + def _wait_for_match(self, spy, kwargs): + """Try matching the kwargs with the given QSignalSpy.""" + for args in spy: + assert len(args) == 1 + line = args[0] + + matches = [] + + for key, expected in kwargs.items(): + value = getattr(line, key) + matches.append(self._match_data(value, expected)) + + if all(matches): + # If we waited for this line, chances are we don't mean the + # same thing the next time we use wait_for and it matches + # this line again. + line.waited_for = True + return line + return None + + def _maybe_skip(self): + """Can be overridden by subclasses to skip on certain log lines. + + We can't run pytest.skip directly while parsing the log, as that would + lead to a pytest.skip.Exception error in a virtual Qt method, which + means pytest-qt fails the test. + + Instead, we check for skip messages periodically in + QuteProc._maybe_skip, and call _maybe_skip after every parsed message + in wait_for (where it's most likely that new messages arrive). + """ + pass + + def wait_for(self, timeout=None, *, override_waited_for=False, + do_skip=False, **kwargs): """Wait until a given value is found in the data. Keyword arguments to this function get interpreted as attributes of the @@ -298,13 +352,17 @@ class Process(QObject): timeout: How long to wait for the message. override_waited_for: If set, gets triggered by previous messages again. + do_skip: If set, call pytest.skip on a timeout. Return: The matched line. """ __tracebackhide__ = True + if timeout is None: - if 'CI' in os.environ: + if do_skip: + timeout = 2000 + elif 'CI' in os.environ: timeout = 15000 else: timeout = 5000 @@ -324,27 +382,20 @@ class Process(QObject): elapsed_timer.start() while True: + # Skip if there are pending messages causing a skip + self._maybe_skip() got_signal = spy.wait(timeout) if not got_signal or elapsed_timer.hasExpired(timeout): - raise WaitForTimeout("Timed out after {}ms waiting for " - "{!r}.".format(timeout, kwargs)) + msg = "Timed out after {}ms waiting for {!r}.".format( + timeout, kwargs) + if do_skip: + pytest.skip(msg) + else: + raise WaitForTimeout(msg) - for args in spy: - assert len(args) == 1 - line = args[0] - - matches = [] - - for key, expected in kwargs.items(): - value = getattr(line, key) - matches.append(self._match_data(value, expected)) - - if all(matches): - # If we waited for this line, chances are we don't mean the - # same thing the next time we use wait_for and it matches - # this line again. - line.waited_for = True - return line + match = self._wait_for_match(spy, kwargs) + if match is not None: + return match def ensure_not_logged(self, delay=500, **kwargs): """Make sure the data matching the given arguments is not logged. diff --git a/tests/integration/webserver.py b/tests/integration/webserver.py index f3ea17478..d9dd40be3 100644 --- a/tests/integration/webserver.py +++ b/tests/integration/webserver.py @@ -19,13 +19,15 @@ """Fixtures for the httpbin webserver.""" +import re import sys import json import socket import os.path +import http.client import pytest -from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSignal, QUrl import testprocess @@ -54,13 +56,23 @@ class Request(testprocess.Line): self.path = '/' if path == '/' else path.rstrip('/') self.status = parsed['status'] + self._check_status() - missing_paths = ['/favicon.ico', '/does-not-exist'] + def _check_status(self): + """Check if the http status is what we expected.""" + # WORKAROUND for https://github.com/PyCQA/pylint/issues/399 (?) + # pylint: disable=no-member, useless-suppression + path_to_statuses = { + '/favicon.ico': [http.client.NOT_FOUND], + '/does-not-exist': [http.client.NOT_FOUND], + '/custom/redirect-later': [http.client.FOUND], + '/basic-auth/user/password': + [http.client.UNAUTHORIZED, http.client.OK], + } - if self.path in missing_paths: - assert self.status == 404 - else: - assert self.status < 400 + sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo + expected_statuses = path_to_statuses.get(sanitized, [http.client.OK]) + assert self.status in expected_statuses def __eq__(self, other): return NotImplemented @@ -94,7 +106,7 @@ class ExpectedRequest: .format(self.verb, self.path)) -class HTTPBin(testprocess.Process): +class WebserverProcess(testprocess.Process): """Abstraction over a running HTTPbin server process. @@ -110,8 +122,9 @@ class HTTPBin(testprocess.Process): KEYS = ['verb', 'path'] - def __init__(self, parent=None): + def __init__(self, script, parent=None): super().__init__(parent) + self._script = script self.port = self._get_port() self.new_data.connect(self.new_request) @@ -130,8 +143,9 @@ class HTTPBin(testprocess.Process): def _parse_line(self, line): self._log(line) - if line == (' * Running on http://127.0.0.1:{}/ (Press CTRL+C to ' - 'quit)'.format(self.port)): + started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ ' + r'\(Press CTRL\+C to quit\)'.format(self.port)) + if started_re.fullmatch(line): self.ready.emit() return None return Request(line) @@ -139,12 +153,12 @@ class HTTPBin(testprocess.Process): def _executable_args(self): if hasattr(sys, 'frozen'): executable = os.path.join(os.path.dirname(sys.executable), - 'webserver_sub') + self._script) args = [str(self.port)] else: executable = sys.executable py_file = os.path.join(os.path.dirname(__file__), - 'webserver_sub.py') + self._script + '.py') args = [py_file, str(self.port)] return executable, args @@ -157,7 +171,7 @@ class HTTPBin(testprocess.Process): @pytest.yield_fixture(scope='session', autouse=True) def httpbin(qapp): """Fixture for a httpbin object which ensures clean setup/teardown.""" - httpbin = HTTPBin() + httpbin = WebserverProcess('webserver_sub') httpbin.start() yield httpbin httpbin.cleanup() @@ -169,3 +183,18 @@ def httpbin_after_test(httpbin, request): request.node._httpbin_log = httpbin.captured_log yield httpbin.after_test() + + +@pytest.yield_fixture +def ssl_server(request, qapp): + """Fixture for a webserver with a self-signed SSL certificate. + + This needs to be explicitly used in a test, and overwrites the httpbin log + used in that test. + """ + server = WebserverProcess('webserver_sub_ssl') + request.node._httpbin_log = server.captured_log + server.start() + yield server + server.after_test() + server.cleanup() diff --git a/tests/integration/webserver_sub_ssl.py b/tests/integration/webserver_sub_ssl.py new file mode 100644 index 000000000..98869b264 --- /dev/null +++ b/tests/integration/webserver_sub_ssl.py @@ -0,0 +1,66 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 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 . + +"""Minimal flask webserver serving a Hello World via SSL. + +This script gets called as a QProcess from integration/conftest.py. +""" + +import ssl +import sys +import logging +import os.path + +import flask + +import webserver_sub + + +app = flask.Flask(__name__) + + +@app.route('/') +def hello_world(): + return "Hello World via SSL!" + + +@app.after_request +def log_request(response): + return webserver_sub.log_request(response) + + +@app.before_first_request +def turn_off_logging(): + # Turn off werkzeug logging after the startup message has been printed. + logging.getLogger('werkzeug').setLevel(logging.ERROR) + + +def main(): + ssl_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'data', 'ssl') + # WORKAROUND for https://github.com/PyCQA/pylint/issues/399 + # pylint: disable=no-member, useless-suppression + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + context.load_cert_chain(os.path.join(ssl_dir, 'cert.pem'), + os.path.join(ssl_dir, 'key.pem')) + app.run(port=int(sys.argv[1]), debug=False, ssl_context=context) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/browser/network/test_networkreply.py b/tests/unit/browser/network/test_networkreply.py index 409a19f01..bca374d2f 100644 --- a/tests/unit/browser/network/test_networkreply.py +++ b/tests/unit/browser/network/test_networkreply.py @@ -53,7 +53,7 @@ class TestFixedDataNetworkReply: def test_data(self, qtbot, req, data): reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo') with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead, - reply.finished], raising=True): + reply.finished]): pass assert reply.bytesAvailable() == len(data) @@ -78,7 +78,7 @@ def test_error_network_reply(qtbot, req): reply = networkreply.ErrorNetworkReply( req, "This is an error", QNetworkReply.UnknownNetworkError) - with qtbot.waitSignals([reply.error, reply.finished], raising=True): + with qtbot.waitSignals([reply.error, reply.finished]): pass reply.abort() # shouldn't do anything diff --git a/tests/unit/browser/test_cookies.py b/tests/unit/browser/test_cookies.py index caeb7f55c..eb60a55dc 100644 --- a/tests/unit/browser/test_cookies.py +++ b/tests/unit/browser/test_cookies.py @@ -22,7 +22,6 @@ from unittest import mock from PyQt5.QtNetwork import QNetworkCookie -from PyQt5.QtTest import QSignalSpy from PyQt5.QtCore import QUrl import pytest @@ -79,7 +78,7 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch): ram_jar = cookies.RAMCookieJar() cookie = QNetworkCookie(b'foo', b'bar') url = QUrl('http://example.com/') - with qtbot.waitSignal(ram_jar.changed, raising=True): + with qtbot.waitSignal(ram_jar.changed): assert ram_jar.setCookiesFromUrl([cookie], url) # assert the cookies are added correctly @@ -90,15 +89,15 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch): assert saved_cookie.name(), saved_cookie.value() == expected -def test_set_cookies_never_accept(config_stub): +def test_set_cookies_never_accept(qtbot, config_stub): """Test setCookiesFromUrl when cookies are not accepted.""" config_stub.data = CONFIG_NEVER_COOKIES ram_jar = cookies.RAMCookieJar() - changed_signal_spy = QSignalSpy(ram_jar.changed) url = QUrl('http://example.com/') - assert not ram_jar.setCookiesFromUrl(url, 'test') - assert not changed_signal_spy + + with qtbot.assertNotEmitted(ram_jar.changed): + assert not ram_jar.setCookiesFromUrl(url, 'test') assert not ram_jar.cookiesForUrl(url) @@ -151,21 +150,10 @@ def test_cookies_changed_emit(config_stub, fake_save_manager, 'LineParser', LineparserSaveStub) jar = cookies.CookieJar() - with qtbot.waitSignal(jar.changed, raising=True): + with qtbot.waitSignal(jar.changed): config_stub.set('content', 'cookies-store', False) -def test_cookies_changed_not_emitted(config_stub, fake_save_manager, - monkeypatch, qapp): - """Test that changed is not emitted when nothing changes.""" - config_stub.data = CONFIG_COOKIES_ENABLED - monkeypatch.setattr(lineparser, - 'LineParser', LineparserSaveStub) - jar = cookies.CookieJar() - changed_spy = QSignalSpy(jar.changed) - assert not changed_spy - - @pytest.mark.parametrize('store_cookies,empty', [ (True, False), (False, True) diff --git a/tests/unit/browser/test_webelem.py b/tests/unit/browser/test_webelem.py index b01e1cd80..0b008b626 100644 --- a/tests/unit/browser/test_webelem.py +++ b/tests/unit/browser/test_webelem.py @@ -628,7 +628,7 @@ class TestJavascriptEscape: with open(path, encoding='utf-8') as f: html_source = f.read().replace('%INPUT%', escaped) - with qtbot.waitSignal(webframe.loadFinished, raising=True) as blocker: + with qtbot.waitSignal(webframe.loadFinished) as blocker: webframe.setHtml(html_source) assert blocker.args == [True] diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py index 5f62cf5b5..09e74d170 100644 --- a/tests/unit/commands/test_userscripts.py +++ b/tests/unit/commands/test_userscripts.py @@ -79,8 +79,7 @@ def test_command(qtbot, py_proc, runner): with open(os.environ['QUTE_FIFO'], 'w') as f: f.write('foo\n') """) - with qtbot.waitSignal(runner.got_cmd, raising=True, - timeout=10000) as blocker: + with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: runner.run(cmd, *args) assert blocker.args == ['foo'] @@ -100,8 +99,7 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner): f.write('\n') """) - with qtbot.waitSignal(runner.got_cmd, raising=True, - timeout=10000) as blocker: + with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: runner.run(cmd, *args, env=env) data = blocker.args[0] @@ -136,9 +134,8 @@ def test_temporary_files(qtbot, tmpdir, py_proc, runner): f.write('\n') """) - with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): - with qtbot.waitSignal(runner.got_cmd, raising=True, - timeout=10000) as blocker: + with qtbot.waitSignal(runner.finished, timeout=10000): + with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: runner.run(cmd, *args, env=env) data = blocker.args[0] @@ -160,7 +157,7 @@ def test_command_with_error(qtbot, tmpdir, py_proc, runner): sys.exit(1) """) - with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): + with qtbot.waitSignal(runner.finished, timeout=10000): runner.run(cmd, *args, env=env) assert not text_file.exists() @@ -191,14 +188,13 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner): """) args.append(str(pidfile)) - with qtbot.waitSignal(watcher.directoryChanged, raising=True, - timeout=10000): + with qtbot.waitSignal(watcher.directoryChanged, timeout=10000): runner.run(cmd, *args, env=env) # Make sure the PID was written to the file, not just the file created time.sleep(0.5) - with qtbot.waitSignal(runner.finished, raising=True): + with qtbot.waitSignal(runner.finished): os.kill(int(pidfile.read()), signal.SIGTERM) assert not text_file.exists() @@ -216,7 +212,7 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, tmpdir, py_proc, """) with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): + with qtbot.waitSignal(runner.finished, timeout=10000): runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)}) assert len(caplog.records) == 1 diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index e8e4d45ba..26502a332 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -305,6 +305,8 @@ class TestString: ({'minlen': 2}, 'fo'), ({'minlen': 2, 'maxlen': 3}, 'fo'), ({'minlen': 2, 'maxlen': 3}, 'foo'), + # valid_values + ({'valid_values': configtypes.ValidValues('fooo')}, 'fooo'), ]) def test_validate_valid(self, klass, kwargs, val): klass(**kwargs).validate(val) @@ -319,6 +321,8 @@ class TestString: ({'maxlen': 2}, 'fob'), ({'minlen': 2, 'maxlen': 3}, 'f'), ({'minlen': 2, 'maxlen': 3}, 'fooo'), + # valid_values + ({'valid_values': configtypes.ValidValues('blah')}, 'fooo'), ]) def test_validate_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): @@ -335,6 +339,15 @@ class TestString: def test_complete(self, klass, value): assert klass(completions=value).complete() == value + @pytest.mark.parametrize('valid_values, expected', [ + (configtypes.ValidValues('one', 'two'), + [('one', ''), ('two', '')]), + (configtypes.ValidValues(('1', 'one'), ('2', 'two')), + [('1', 'one'), ('2', 'two')]), + ]) + def test_complete_valid_values(self, klass, valid_values, expected): + assert klass(valid_values=valid_values).complete() == expected + class TestList: diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 3b661c1ac..6ab9deb6b 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -94,8 +94,7 @@ class JSTester: **kwargs: Passed to jinja's template.render(). """ template = self._jinja_env.get_template(path) - with self._qtbot.waitSignal(self.webview.loadFinished, - raising=True) as blocker: + with self._qtbot.waitSignal(self.webview.loadFinished) as blocker: self.webview.setHtml(template.render(**kwargs)) assert blocker.args == [True] diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 0638726b3..de7acaab3 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -282,7 +282,7 @@ class TestKeyChain: assert not keyparser.execute.called assert keyparser._ambiguous_timer.isActive() # We wait for the timeout to occur. - with qtbot.waitSignal(keyparser.keystring_updated, raising=True): + with qtbot.waitSignal(keyparser.keystring_updated): pass assert keyparser.execute.called diff --git a/tests/unit/mainwindow/statusbar/test_progress.py b/tests/unit/mainwindow/statusbar/test_progress.py index 0bb241a41..0b1a64967 100644 --- a/tests/unit/mainwindow/statusbar/test_progress.py +++ b/tests/unit/mainwindow/statusbar/test_progress.py @@ -23,29 +23,11 @@ from collections import namedtuple import pytest -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout -from PyQt5.QtCore import QSize, Qt from qutebrowser.browser import webview from qutebrowser.mainwindow.statusbar.progress import Progress -class FakeStatusBar(QWidget): - - """Fake statusbar to test progressbar sizing.""" - - def __init__(self, parent=None): - super().__init__(parent) - self.hbox = QHBoxLayout(self) - self.hbox.addStretch() - self.hbox.setContentsMargins(0, 0, 0, 0) - self.setAttribute(Qt.WA_StyledBackground, True) - self.setStyleSheet('background-color: red;') - - def minimumSizeHint(self): - return QSize(1, self.fontMetrics().height()) - - @pytest.fixture def progress_widget(qtbot, monkeypatch, config_stub): """Create a Progress widget and checks its initial state.""" @@ -62,25 +44,6 @@ def progress_widget(qtbot, monkeypatch, config_stub): return widget -@pytest.fixture -def fake_statusbar(qtbot): - """Fixture providing a statusbar in a container window.""" - container = QWidget() - qtbot.add_widget(container) - vbox = QVBoxLayout(container) - vbox.addStretch() - - statusbar = FakeStatusBar(container) - # to make sure container isn't GCed - # pylint: disable=attribute-defined-outside-init - statusbar.container = container - vbox.addWidget(statusbar) - - container.show() - qtbot.waitForWindowShown(container) - return statusbar - - def test_load_started(progress_widget): """Ensure the Progress widget reacts properly when the page starts loading. diff --git a/tests/unit/mainwindow/statusbar/test_prompt.py b/tests/unit/mainwindow/statusbar/test_prompt.py new file mode 100644 index 000000000..cc3e6761a --- /dev/null +++ b/tests/unit/mainwindow/statusbar/test_prompt.py @@ -0,0 +1,57 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 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 . + +"""Test Prompt widget.""" + +import sip + +import pytest + +from qutebrowser.mainwindow.statusbar.prompt import Prompt +from qutebrowser.utils import objreg + + +@pytest.yield_fixture +def prompt(qtbot, win_registry): + prompt = Prompt(0) + qtbot.addWidget(prompt) + + yield prompt + + # If we don't clean up here, this test will remove 'prompter' from the + # objreg at some point in the future, which will cause some other test to + # fail. + sip.delete(prompt) + + +def test_prompt(prompt): + prompt.show() + objreg.get('prompt', scope='window', window=0) + objreg.get('prompter', scope='window', window=0) + + +@pytest.mark.xfail(reason="This test is broken and I don't get why") +def test_resizing(fake_statusbar, prompt): + fake_statusbar.hbox.addWidget(prompt) + + prompt.txt.setText("Blah?") + old_width = prompt.lineedit.width() + + prompt.lineedit.setText("Hello World" * 100) + assert prompt.lineedit.width() > old_width diff --git a/tests/unit/misc/test_autoupdate.py b/tests/unit/misc/test_autoupdate.py index 58c44e271..4b8750624 100644 --- a/tests/unit/misc/test_autoupdate.py +++ b/tests/unit/misc/test_autoupdate.py @@ -20,7 +20,6 @@ """Tests for qutebrowser.misc.autoupdate""" import pytest -from PyQt5.QtTest import QSignalSpy from PyQt5.QtCore import QUrl from qutebrowser.misc import autoupdate, httpclient @@ -63,13 +62,9 @@ def test_get_version_success(qtbot): http_stub = HTTPGetStub(success=True) client = autoupdate.PyPIVersionClient(client=http_stub) - # Use a spy to inspect the signal - error_spy = QSignalSpy(client.error) - - with qtbot.waitSignal(client.success, raising=True): - client.get_version('test') - - assert len(error_spy) == 0 + with qtbot.assertNotEmitted(client.error): + with qtbot.waitSignal(client.success): + client.get_version('test') assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json') @@ -79,13 +74,9 @@ def test_get_version_error(qtbot): http_stub = HTTPGetStub(success=False) client = autoupdate.PyPIVersionClient(client=http_stub) - # Use a spy to inspect the signal - success_spy = QSignalSpy(client.success) - - with qtbot.waitSignal(client.error, raising=True): - client.get_version('test') - - assert len(success_spy) == 0 + with qtbot.assertNotEmitted(client.success): + with qtbot.waitSignal(client.error): + client.get_version('test') @pytest.mark.parametrize('json', INVALID_JSON) @@ -95,10 +86,6 @@ def test_invalid_json(qtbot, json): client = autoupdate.PyPIVersionClient(client=http_stub) client.get_version('test') - # Use a spy to inspect the signal - success_spy = QSignalSpy(client.success) - - with qtbot.waitSignal(client.error, raising=True): - client.get_version('test') - - assert len(success_spy) == 0 + with qtbot.assertNotEmitted(client.success): + with qtbot.waitSignal(client.error): + client.get_version('test') diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 86498d2d1..9aa7f99d5 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -40,7 +40,8 @@ def proc(qtbot): p = guiprocess.GUIProcess(0, 'testprocess') yield p if p._proc.state() == QProcess.Running: - with qtbot.waitSignal(p.finished, timeout=10000) as blocker: + with qtbot.waitSignal(p.finished, timeout=10000, + raising=False) as blocker: p._proc.terminate() if not blocker.signal_triggered: p._proc.kill() @@ -56,8 +57,7 @@ def fake_proc(monkeypatch, stubs): def test_start(proc, qtbot, guiprocess_message_mock, py_proc): """Test simply starting a process.""" - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -69,8 +69,7 @@ def test_start_verbose(proc, qtbot, guiprocess_message_mock, py_proc): """Test starting a process verbosely.""" proc.verbose = True - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -97,8 +96,7 @@ def test_start_env(monkeypatch, qtbot, py_proc): sys.exit(0) """) - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): proc.start(*argv) data = bytes(proc._proc.readAll()).decode('utf-8') @@ -110,8 +108,7 @@ def test_start_env(monkeypatch, qtbot, py_proc): @pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device', extend=True) def test_start_mode(proc, qtbot, py_proc): """Test simply starting a process with mode parameter.""" - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv, mode=QIODevice.NotOpen) @@ -139,7 +136,7 @@ def test_start_detached_error(fake_proc, guiprocess_message_mock): def test_double_start(qtbot, proc, py_proc): """Test starting a GUIProcess twice.""" - with qtbot.waitSignal(proc.started, raising=True, timeout=10000): + with qtbot.waitSignal(proc.started, timeout=10000): argv = py_proc("import time; time.sleep(10)") proc.start(*argv) with pytest.raises(ValueError): @@ -148,12 +145,10 @@ def test_double_start(qtbot, proc, py_proc): def test_double_start_finished(qtbot, proc, py_proc): """Test starting a GUIProcess twice (with the first call finished).""" - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) - with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) @@ -180,7 +175,7 @@ def test_start_logging(fake_proc, caplog): def test_error(qtbot, proc, caplog, guiprocess_message_mock): """Test the process emitting an error.""" with caplog.at_level(logging.ERROR, 'message'): - with qtbot.waitSignal(proc.error, raising=True, timeout=5000): + with qtbot.waitSignal(proc.error, timeout=5000): proc.start('this_does_not_exist_either', []) msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error, @@ -191,7 +186,7 @@ def test_error(qtbot, proc, caplog, guiprocess_message_mock): def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc): - with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): + with qtbot.waitSignal(proc.finished, timeout=10000): proc.start(*py_proc('import sys; sys.exit(1)')) msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error) @@ -202,7 +197,7 @@ def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc): def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream): """When a process fails, its output should be logged.""" with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): + with qtbot.waitSignal(proc.finished, timeout=10000): proc.start(*py_proc(""" import sys print("test", file=sys.{}) @@ -219,7 +214,7 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream): The test doesn't actually check the log as it'd fail because of the error logging. """ - with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): + with qtbot.waitSignal(proc.finished, timeout=10000): proc.start(*py_proc(""" import sys print("test", file=sys.{}) diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 760874689..d66f9198f 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -343,13 +343,11 @@ class TestListen: ipc_server.listen() old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns - with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000, - raising=True): + with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): pass # Make sure the timer is not singleShot - with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000, - raising=True): + with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): pass new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns @@ -412,9 +410,9 @@ class TestHandleConnection: def test_double_connection(self, qlocalsocket, ipc_server, caplog): ipc_server._socket = qlocalsocket ipc_server.handle_connection() - message = ("Got new connection but ignoring it because we're still " - "handling another one.") - assert message in [rec.message for rec in caplog.records] + msg = ("Got new connection but ignoring it because we're still " + "handling another one") + assert any(rec.message.startswith(msg) for rec in caplog.records) def test_disconnected_immediately(self, ipc_server, caplog): socket = FakeSocket(state=QLocalSocket.UnconnectedState) @@ -444,7 +442,7 @@ class TestHandleConnection: ipc_server._server = FakeServer(socket) - with qtbot.waitSignal(ipc_server.got_args, raising=True) as blocker: + with qtbot.waitSignal(ipc_server.got_args) as blocker: ipc_server.handle_connection() assert blocker.args == [['foo'], 'tab', ''] @@ -458,7 +456,7 @@ def connected_socket(qtbot, qlocalsocket, ipc_server): pytest.skip("Skipping connected_socket test - " "https://github.com/The-Compiler/qutebrowser/issues/1045") ipc_server.listen() - with qtbot.waitSignal(ipc_server._server.newConnection, raising=True): + with qtbot.waitSignal(ipc_server._server.newConnection): qlocalsocket.connectToServer('qute-test') yield qlocalsocket qlocalsocket.disconnectFromServer() @@ -496,22 +494,19 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8') (b'{"args": [], "target_arg": null}\n', 'invalid version'), ]) def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): - got_args_spy = QSignalSpy(ipc_server.got_args) - signals = [ipc_server.got_invalid_data, connected_socket.disconnected] with caplog.at_level(logging.ERROR): - with qtbot.waitSignals(signals, raising=True): - connected_socket.write(data) + with qtbot.assertNotEmitted(ipc_server.got_args): + with qtbot.waitSignals(signals): + connected_socket.write(data) messages = [r.message for r in caplog.records] - assert messages[-1] == 'Ignoring invalid IPC data.' + assert messages[-1].startswith('Ignoring invalid IPC data from socket ') assert messages[-2].startswith(msg) - assert not got_args_spy def test_multiline(qtbot, ipc_server, connected_socket): spy = QSignalSpy(ipc_server.got_args) - error_spy = QSignalSpy(ipc_server.got_invalid_data) data = ('{{"args": ["one"], "target_arg": "tab",' ' "protocol_version": {version}}}\n' @@ -519,11 +514,10 @@ def test_multiline(qtbot, ipc_server, connected_socket): ' "protocol_version": {version}}}\n'.format( version=ipc.PROTOCOL_VERSION)) - with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args], - raising=True): - connected_socket.write(data.encode('utf-8')) + with qtbot.assertNotEmitted(ipc_server.got_invalid_data): + with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args]): + connected_socket.write(data.encode('utf-8')) - assert not error_spy assert len(spy) == 2 assert spy[0] == [['one'], 'tab', ''] assert spy[1] == [['two'], '', ''] @@ -542,19 +536,19 @@ class TestSendToRunningInstance: def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd): ipc_server.listen() raw_spy = QSignalSpy(ipc_server.got_raw) - error_spy = QSignalSpy(ipc_server.got_invalid_data) - with qtbot.waitSignal(ipc_server.got_args, raising=True, - timeout=5000) as blocker: - with tmpdir.as_cwd(): - if not has_cwd: - m = mocker.patch('qutebrowser.misc.ipc.os') - m.getcwd.side_effect = OSError - sent = ipc.send_to_running_instance('qute-test', ['foo'], None) + with qtbot.assertNotEmitted(ipc_server.got_invalid_data): + with qtbot.waitSignal(ipc_server.got_args, + timeout=5000) as blocker: + with tmpdir.as_cwd(): + if not has_cwd: + m = mocker.patch('qutebrowser.misc.ipc.os') + m.getcwd.side_effect = OSError + sent = ipc.send_to_running_instance('qute-test', ['foo'], + None) - assert sent + assert sent - assert not error_spy expected_cwd = str(tmpdir) if has_cwd else '' assert blocker.args == [['foo'], '', expected_cwd] @@ -598,15 +592,14 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server): ipc_server._timer.setInterval(100) ipc_server.listen() - with qtbot.waitSignal(ipc_server._server.newConnection, raising=True): + with qtbot.waitSignal(ipc_server._server.newConnection): qlocalsocket.connectToServer('qute-test') with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(qlocalsocket.disconnected, raising=True, - timeout=5000): + with qtbot.waitSignal(qlocalsocket.disconnected, timeout=5000): pass - assert caplog.records[-1].message == "IPC connection timed out." + assert caplog.records[-1].message.startswith("IPC connection timed out") @pytest.mark.parametrize('method, args, is_warning', [ @@ -679,14 +672,14 @@ class TestSendOrListen: objreg_server = objreg.get('ipc-server') assert objreg_server is ret_server - with qtbot.waitSignal(ret_server.got_args, raising=True): + with qtbot.waitSignal(ret_server.got_args): ret_client = ipc.send_or_listen(args) assert ret_client is None @pytest.mark.posix(reason="Unneeded on Windows") def test_legacy_name(self, caplog, qtbot, args, legacy_server): - with qtbot.waitSignal(legacy_server.got_args, raising=True): + with qtbot.waitSignal(legacy_server.got_args): ret = ipc.send_or_listen(args) assert ret is None msgs = [e.message for e in caplog.records] @@ -727,7 +720,7 @@ class TestSendOrListen: assert isinstance(ret_server, ipc.IPCServer) logging.debug('== Connecting ==') - with qtbot.waitSignal(ret_server.got_args, raising=True): + with qtbot.waitSignal(ret_server.got_args): ret_client = ipc.send_or_listen(args) assert ret_client is None diff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py index f9b436d8a..b84956d75 100644 --- a/tests/unit/misc/test_msgbox.py +++ b/tests/unit/misc/test_msgbox.py @@ -74,7 +74,7 @@ def test_finished_signal(qtbot): qtbot.add_widget(box) - with qtbot.waitSignal(box.finished, raising=True): + with qtbot.waitSignal(box.finished): box.accept() assert signal_triggered diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py index 32f697e19..3fdd6af0d 100644 --- a/tests/unit/misc/test_sessions.py +++ b/tests/unit/misc/test_sessions.py @@ -439,9 +439,8 @@ class TestSave: def test_update_completion_signal(self, sess_man, tmpdir, qtbot): session_path = tmpdir / 'foo.yml' - blocker = qtbot.waitSignal(sess_man.update_completion) - sess_man.save(str(session_path)) - assert blocker.signal_triggered + with qtbot.waitSignal(sess_man.update_completion): + sess_man.save(str(session_path)) def test_no_state_config(self, sess_man, tmpdir, state_config): session_path = tmpdir / 'foo.yml' @@ -691,9 +690,8 @@ class TestDelete: sess = tmpdir / 'foo.yml' sess.ensure() - blocker = qtbot.waitSignal(sess_man.update_completion) - sess_man.delete(str(sess)) - assert blocker.signal_triggered + with qtbot.waitSignal(sess_man.update_completion): + sess_man.delete(str(sess)) def test_not_existing(self, sess_man, qtbot, tmpdir): sess = tmpdir / 'foo.yml' diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 6ee82a586..99e61a832 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -166,13 +166,17 @@ class TestFuzzyUrl: assert not os_mock.path.exists.called assert url == QUrl('http://foo') - def test_file_absolute(self, os_mock): + @pytest.mark.parametrize('path, expected', [ + ('/foo', QUrl('file:///foo')), + ('/bar\n', QUrl('file:///bar')), + ]) + def test_file_absolute(self, path, expected, os_mock): """Test with an absolute path.""" os_mock.path.exists.return_value = True os_mock.path.isabs.return_value = True - url = urlutils.fuzzy_url('/foo') - assert url == QUrl('file:///foo') + url = urlutils.fuzzy_url(path) + assert url == expected @pytest.mark.posix def test_file_absolute_expanded(self, os_mock): diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py index 8bc3dcc85..edd4c1081 100644 --- a/tests/unit/utils/usertypes/test_question.py +++ b/tests/unit/utils/usertypes/test_question.py @@ -61,23 +61,21 @@ def test_done(mode, answer, signal_names, question, qtbot): question.mode = mode question.answer = answer signals = [getattr(question, name) for name in signal_names] - with qtbot.waitSignals(signals, raising=True): + with qtbot.waitSignals(signals): question.done() assert not question.is_aborted def test_cancel(question, qtbot): """Test Question.cancel().""" - with qtbot.waitSignals([question.cancelled, question.completed], - raising=True): + with qtbot.waitSignals([question.cancelled, question.completed]): question.cancel() assert not question.is_aborted def test_abort(question, qtbot): """Test Question.abort().""" - with qtbot.waitSignals([question.aborted, question.completed], - raising=True): + with qtbot.waitSignals([question.aborted, question.completed]): question.abort() assert question.is_aborted diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index 5f2575ea9..e48bd332b 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -72,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, timeout=3000, raising=True): + with qtbot.waitSignal(t.timeout, timeout=3000): t.start(200) def test_timeout_set_interval(qtbot): """Make sure the timer works with setInterval().""" t = usertypes.Timer() - with qtbot.waitSignal(t.timeout, timeout=3000, raising=True): + with qtbot.waitSignal(t.timeout, timeout=3000): t.setInterval(200) t.start() diff --git a/tox.ini b/tox.ini index f0fa72491..9624da5b4 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = Flask==0.10.1 glob2==0.4.1 httpbin==0.4.0 - hypothesis==1.18.1 + hypothesis==2.0.0 itsdangerous==0.24 Mako==1.0.3 parse==1.6.6 @@ -33,9 +33,10 @@ deps = pytest-faulthandler==1.3.0 pytest-html==1.7 pytest-mock==0.9.0 - pytest-qt==1.10.0 - pytest-sugar==0.5.1 + pytest-qt==1.11.0 + pytest-instafail==0.3.0 pytest-travis-fold==1.2.0 + pytest-repeat==0.2 six==1.10.0 termcolor==1.1.0 vulture==0.8.1 @@ -45,7 +46,7 @@ deps = cherrypy==4.0.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} + {envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --instafail --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} {envpython} scripts/dev/check_coverage.py {posargs} [testenv:mkvenv] @@ -73,7 +74,7 @@ passenv = {[testenv]passenv} deps = {[testenv]deps} setenv = DISPLAY= - QUTE_NO_DISPLAY_OK=1 + QUTE_NO_DISPLAY=1 commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test --strict -rfEw {posargs:tests} @@ -105,8 +106,8 @@ passenv = deps = {[testenv]deps} {[testenv:misc]deps} - astroid==1.4.3 - pylint==1.5.2 + astroid==1.4.4 + pylint==1.5.4 requests==2.9.1 commands = {envpython} scripts/link_pyqt.py --tox {envdir} @@ -156,7 +157,6 @@ deps = py==1.4.31 pyflakes==1.0.0 pytest==2.8.5 - pytest-cache==1.0 pytest-flakes==1.0.1 commands = {envpython} -m py.test -q --flakes --ignore=tests --noconftest @@ -168,10 +168,9 @@ deps = -r{toxinidir}/requirements.txt apipkg==1.4 execnet==1.4.1 - pep8==1.6.2 + pep8==1.7 py==1.4.31 pytest==2.8.5 - pytest-cache==1.0 pytest-pep8==1.0.6 commands = {envpython} -m py.test -q --pep8 --ignore=tests --noconftest @@ -187,7 +186,6 @@ deps = mccabe==0.3.1 py==1.4.31 pytest==2.8.5 - pytest-cache==1.0 pytest-mccabe==0.1 commands = {envpython} -m py.test -q --mccabe --ignore=tests --noconftest