diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..9556c6763 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +doc/changelog.asciidoc merge=union diff --git a/.github/CONTRIBUTING.asciidoc b/.github/CONTRIBUTING.asciidoc index 4421f071a..6449c6323 100644 --- a/.github/CONTRIBUTING.asciidoc +++ b/.github/CONTRIBUTING.asciidoc @@ -1,8 +1,3 @@ -IMPORTANT: I'm currently (July 2018) more busy than usual until September, -because of exams coming up. Review of non-trivial pull requests will thus be -delayed until then. If you're reading this note after mid-September, please -open an issue. - - Before you start to work on something, please leave a comment on the relevant issue (or open one). This makes sure there is no duplicate work done. diff --git a/.gitignore b/.gitignore index b41285fd3..9efceef63 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ __pycache__ /doc/*.html /README.html /qutebrowser/html/doc/ -/qutebrowser/html/*.html /.venv* /.coverage /htmlcov diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 49125de2e..6eb427a94 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -27,10 +27,24 @@ Added - New `content.mouse_lock` setting to handle HTML5 pointer locking. - New `completion.web_history.exclude` setting which hides a list of URL patterns from the completion. +- Rewritten PDF.js support: + * PDF.js support and the `content.pdfjs` setting are now available with + QtWebEngine. + * Opening a PDF file now doesn't start a second request anymore. + * Opening PDFs on https:// sites now works properly. +- New `qt.process_model` setting which can be used to change Chromium's process + model. +- New `qt.low_end_device_mode` setting which turns on Chromium's low-end device + mode. This mode uses less RAM, but the expense of performance. +- New `content.webrtc_ip_handling_policy` setting, which allows more + fine-grained/restrictive control about which IPs are exposed via WebRTC. +- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only + once), as support for those is going to be removed in a future release. Changed ~~~~~~~ +- The `content.headers.referer` setting now works on QtWebEngine. - The `:repeat` command now takes a count which is multiplied with the given "times" argument. - The default keybinding to leave passthrough mode was changed from `` @@ -43,6 +57,29 @@ Changed already did). - The `completion.web_history_max_items` setting got renamed to `completion.web_history.max_items`. +- The Makefile shipped with qutebrowser now supports overriding variables + DATADIR and MANDIR. +- Various performance improvements when many tabs are opened. +- Regenerating completion history now shows a progress dialog. +- Make qute:// pages work properly on Qt 5.11.2 +- The `content.autoplay` setting now supports URL patterns on Qt >= 5.11. +- The `content.host_blocking.whitelist` setting now takes a list of URL + patterns instead of globs. + +Fixed +~~~~~ + +- Error when passing a substring with spaces to `:tab-take`. +- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly. +- When no documentation has been generated, the plaintext documentation now can + be shown for more files such as `qute://help/userscripts.html`. + +Removed +~~~~~~~ + +- Support for importing pre-v1.0.0 history files has been removed. +- The `content.webrtc_public_interfaces_only` setting has been removed and + replaced by `content.webrtc_ip_handling_policy`. v1.4.2 ------ @@ -1615,7 +1652,7 @@ Changed `tabs.bg/fg.selected.odd/even`. - `:spawn --userscript` and `:hint` with the `userscript` target now look up relative paths in `~/.local/share/qutebrowser/userscripts` or - `$XDG_DATA_DIR`. Using a binary in `$PATH` won't work anymore with + `$XDG_DATA_HOME`. Using a binary in `$PATH` won't work anymore with `--userscript`. - New design for error pages - Link filtering for hints now checks if the text is contained anywhere in diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 031d63a22..69aac9a0d 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -5,11 +5,6 @@ The Compiler :data-uri: :toc: -IMPORTANT: I'm currently (July 2018) more busy than usual until September, -because of exams coming up. Review of non-trivial pull requests will thus be -delayed until then. If you're reading this note after mid-September, please -open an issue. - I `<3` footnote:[Of course, that says `<3` in HTML.] contributors! This document contains guidelines for contributing to qutebrowser, as well as diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 4b4ad54bc..51d3b30a9 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -576,7 +576,7 @@ Start hinting. - With `userscript`: The userscript to execute. Either store the userscript in `~/.local/share/qutebrowser/userscripts` - (or `$XDG_DATA_DIR`), or use an absolute + (or `$XDG_DATA_HOME`), or use an absolute path. - With `fill`: The command to fill the statusbar with. `{hint-url}` will get replaced by the selected @@ -1193,7 +1193,7 @@ Spawn a command in a shell. * +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those locations: - `~/.local/share/qutebrowser/userscripts` - (or `$XDG_DATA_DIR`) + (or `$XDG_DATA_HOME`) - `/usr/share/qutebrowser/userscripts` * +*-v*+, +*--verbose*+: Show notifications when the command started/exited. @@ -1342,6 +1342,9 @@ Take a tab from another window. * +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken. +==== note +* This command does not split arguments after the last argument and handles quotes literally. + [[unbind]] === unbind Syntax: +:unbind [*--mode* 'mode'] 'key'+ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 5e302adfa..dc418a99a 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -129,7 +129,7 @@ |<>|User agent to send. Unset to send the default. |<>|Enable host blocking. |<>|List of URLs of lists which contain hosts to block. -|<>|List of domains that should always be loaded, despite being ad-blocked. +|<>|A list of patterns that should always be loaded, despite being ad-blocked. |<>|Enable hyperlink auditing (``). |<>|Load images automatically in web pages. |<>|Show javascript alerts. @@ -158,7 +158,7 @@ |<>|Validate SSL handshakes. |<>|List of user stylesheet filenames to use. |<>|Enable WebGL. -|<>|Only expose public interfaces via WebRTC. +|<>|Which interfaces to expose via WebRTC. |<>|Limit fullscreen to the browser window (does not expand to fill the screen). |<>|Monitor load requests for cross-site scripting attempts. |<>|Directory to save downloads to. @@ -229,6 +229,8 @@ |<>|Force a Qt platform to use. |<>|Force software rendering for QtWebEngine. |<>|Turn on Qt HighDPI scaling. +|<>|When to use Chromium's low-end device mode. +|<>|Which Chromium process model to use. |<>|Show a scrollbar. |<>|Enable smooth scrolling for web pages. |<>|When to find text on a page case-insensitively. @@ -1488,7 +1490,9 @@ Default: [[content.autoplay]] === content.autoplay Automatically start playing `

Qt 5.7 was released in June 2016, with the 5.7.1 patch release in December +2016. It is based on Chromium 49 (March 2016) with (some) security fixes up to +Chromium 54 (October 2016). It is also +not covered +by Debian security updates.

+ +

Qt 5.8 has had various bugs, and has been unsupported (but working to some +degree) in qutebrowser for a while.

+ +

Because of those security issues and the maintaince burden coming with +supporting old versions, support for Qt < 5.9 will be dropped in a future +qutebrowser release. You might want to check +alternate installation methods +which allow you to get a newer Qt.

+{% endblock %} diff --git a/qutebrowser/html/warning-webkit.html b/qutebrowser/html/warning-webkit.html new file mode 100644 index 000000000..2797ea228 --- /dev/null +++ b/qutebrowser/html/warning-webkit.html @@ -0,0 +1,82 @@ +{% extends "styled.html" %} + +{% block content %} +

{{ title }}

+Note this warning will only appear once. Use :open +qute://warning/webkit to show it again at a later time. + +

You're using qutebrowser with the QtWebKit backend.

+ +

Unfortunately, QtWebKit hasn't seen a release (including security updates) +since June 2017, and it also lacks various security features (process +isolation/sandboxing) present in QtWebEngine.

+ +

Because of those security issues and the maintaince burden coming with +supporting QtWebKit, support for it will be dropped in a future qutebrowser +release. It's recommended that you use QtWebEngine instead.

+ +

(Outdated) reasons to use QtWebKit

+

Most reasons why people preferred the QtWebKit backend aren't relevant anymore:

+ +

PDF.js support: This qutebrowser release comes with PDF.js support +for QtWebEngine.

+ +

Missing control over Referer header: This qutebrowser release +supports content.headers.referer for QtWebEngine.

+ +

Missing control over cookies: With Qt 5.11 or newer, the content.cookies.accept setting works on QtWebEngine.

+ +

Graphical glitches: The new values for the qt.force_software_rendering setting added in v1.4.0 should +hopefully help.

+ +

Missing support for notifications: Those aren't supported yet in +Qt, but support is planned to be added in Qt 5.13, released around May 2019.

+ +

Resource usage: This release adds the qt.process_model and qt.low_end_device_mode settings which can be used to +decrease the resource usage of QtWebEngine (but come with other drawbacks).

+ +

Not trusting Google: Various people have checked the connections made +by QtWebEngine/qutebrowser, and it doesn't make any connections to Google (or +any other unsolicited connections at all). Arguably, having to trust Google +also is a smaller issue than having to trust every website you visit because of +heaps of security issues...

+ +

Nouveau graphic driver: You can use QtWebEngine with software +rendering. With Qt 5.13 (~May 2019) it might be possible to run with Nouveau +without software rendering.

+ +

Wayland: It's possible to use QtWebEngine with XWayland. Some users +also seem to be able to run it natively with Qt 5.11, but currently, QUTE_SKIP_WAYLAND_CHECK=1 needs to be set in the +environment to do so.

+ +

Instability on FreeBSD: Those seem to be FreeBSD-specific crashes, +and unfortunately nobody has looked into them yet so far...

+ +

QtWebEngine being unavailable in ArchlinuxARM's PyQt package: +QtWebEngine itself is available on the armv7h/aarch64 architectures, but their +PyQt package is broken and doesn't come with QtWebEngine support. This +has +been reported in their forums, but without any change so far. It should +however be possible to rebuild the PyQt package from source with QtWebEngine +installed.

+ +

QtWebEngine being unavailable on Parabola: Claims of Parabola +developers about QtWebEngine being "non-free" have repeatedly been disputed, +and so far nobody came up with solid evidence about that being the case. Also, +note that their qutebrowser package is orphaned and was often outdated in the +past (even qutebrowser security fixes took months to arrive there). You +might be better off chosing an alternative install +method.

+ +

White flashing between loads with a custom stylesheet: This doesn't +seem to happen with qt.process_model = single-process +set. However, note that that setting comes with decreased security and +stability, but QtWebKit doesn't have any process isolation at all.

+{% endblock %} diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 9c4473874..d6164fd49 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -719,9 +719,9 @@ class TabbedBrowser(QWidget): except TabDeletedError: # We can get signals for tabs we already deleted... return - start = config.val.colors.tabs.indicator.start - stop = config.val.colors.tabs.indicator.stop - system = config.val.colors.tabs.indicator.system + start = config.cache['colors.tabs.indicator.start'] + stop = config.cache['colors.tabs.indicator.stop'] + system = config.cache['colors.tabs.indicator.system'] color = utils.interpolate_color(start, stop, perc, system) self.widget.set_tab_indicator_color(idx, color) self.widget.update_tab_title(idx) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 02934a532..94e88ea71 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -139,9 +139,9 @@ class TabWidget(QTabWidget): """ tab = self.widget(idx) if tab.data.pinned: - fmt = config.val.tabs.title.format_pinned + fmt = config.cache['tabs.title.format_pinned'] else: - fmt = config.val.tabs.title.format + fmt = config.cache['tabs.title.format'] if (field is not None and (fmt is None or ('{' + field + '}') not in fmt)): @@ -604,7 +604,7 @@ class TabBar(QTabBar): minimum_size = self.minimumTabSizeHint(index) height = minimum_size.height() if self.vertical: - confwidth = str(config.val.tabs.width) + confwidth = str(config.cache['tabs.width']) if confwidth.endswith('%'): main_window = objreg.get('main-window', scope='window', window=self._win_id) @@ -614,7 +614,7 @@ class TabBar(QTabBar): width = int(confwidth) size = QSize(max(minimum_size.width(), width), height) else: - if config.val.tabs.pinned.shrink: + if config.cache['tabs.pinned.shrink']: pinned = self._tab_pinned(index) pinned_count, pinned_width = self._pinned_statistics() else: @@ -652,15 +652,15 @@ class TabBar(QTabBar): tab = QStyleOptionTab() self.initStyleOption(tab, idx) - # pylint: disable=bad-config-option - setting = config.val.colors.tabs - # pylint: enable=bad-config-option + setting = 'colors.tabs' if idx == selected: - setting = setting.selected - setting = setting.odd if (idx + 1) % 2 else setting.even + setting += '.selected' + setting += '.odd' if (idx + 1) % 2 else '.even' - tab.palette.setColor(QPalette.Window, setting.bg) - tab.palette.setColor(QPalette.WindowText, setting.fg) + tab.palette.setColor(QPalette.Window, + config.cache[setting + '.bg']) + tab.palette.setColor(QPalette.WindowText, + config.cache[setting + '.fg']) indicator_color = self.tab_indicator_color(idx) tab.palette.setColor(QPalette.Base, indicator_color) @@ -805,7 +805,7 @@ class TabBarStyle(QCommonStyle): elif element == QStyle.CE_TabBarTabLabel: if not opt.icon.isNull() and layouts.icon.isValid(): self._draw_icon(layouts, opt, p) - alignment = (config.val.tabs.title.alignment | + alignment = (config.cache['tabs.title.alignment'] | Qt.AlignVCenter | Qt.TextHideMnemonic) self._style.drawItemText(p, layouts.text, alignment, opt.palette, opt.state & QStyle.State_Enabled, @@ -878,8 +878,8 @@ class TabBarStyle(QCommonStyle): Return: A Layout object with two QRects. """ - padding = config.val.tabs.padding - indicator_padding = config.val.tabs.indicator.padding + padding = config.cache['tabs.padding'] + indicator_padding = config.cache['tabs.indicator.padding'] text_rect = QRect(opt.rect) if not text_rect.isValid(): @@ -890,7 +890,7 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) - indicator_width = config.val.tabs.indicator.width + indicator_width = config.cache['tabs.indicator.width'] if indicator_width == 0: indicator_rect = QRect() else: @@ -933,9 +933,9 @@ class TabBarStyle(QCommonStyle): icon_state = (QIcon.On if opt.state & QStyle.State_Selected else QIcon.Off) # reserve space for favicon when tab bar is vertical (issue #1968) - position = config.val.tabs.position + position = config.cache['tabs.position'] if (position in [QTabWidget.East, QTabWidget.West] and - config.val.tabs.favicons.show != 'never'): + config.cache['tabs.favicons.show'] != 'never'): tab_icon_size = icon_size else: actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 363f7f23c..3bd52eb44 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -68,6 +68,11 @@ def _other_backend(backend): def _error_text(because, text, backend): """Get an error text for the given information.""" other_backend, other_setting = _other_backend(backend) + if other_backend == usertypes.Backend.QtWebKit: + warning = ("Note that QtWebKit hasn't been updated since " + "July 2017 (including security updates).") + else: + warning = "" return ("Failed to start with the {backend} backend!" "

qutebrowser tried to start with the {backend} backend but " "failed because {because}.

{text}" @@ -75,9 +80,10 @@ def _error_text(because, text, backend): "

This forces usage of the {other_backend.name} backend by " "setting the backend = '{other_setting}' option " "(if you have a config.py file, you'll need to set " - "this manually).

".format( + "this manually). {warning}

".format( backend=backend.name, because=because, text=text, - other_backend=other_backend, other_setting=other_setting)) + other_backend=other_backend, other_setting=other_setting, + warning=warning)) class _Dialog(QDialog): @@ -102,8 +108,10 @@ class _Dialog(QDialog): quit_button.clicked.connect(lambda: self.done(_Result.quit)) hbox.addWidget(quit_button) - backend_button = QPushButton("Force {} backend".format( - other_backend.name)) + backend_text = "Force {} backend".format(other_backend.name) + if other_backend == usertypes.Backend.QtWebKit: + backend_text += ' (not recommended)' + backend_button = QPushButton(backend_text) backend_button.clicked.connect(functools.partial( self._change_setting, 'backend', other_setting)) hbox.addWidget(backend_button) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 8053e5151..4c300a3da 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -35,6 +35,7 @@ class SqliteErrorCode: in qutebrowser here. """ + UNKNOWN = '-1' BUSY = '5' # database is locked READONLY = '8' # attempt to write a readonly database IOERR = '10' # disk I/O error @@ -86,12 +87,17 @@ class SqlBugError(SqlError): def raise_sqlite_error(msg, error): """Raise either a SqlBugError or SqlEnvironmentError.""" + error_code = error.nativeErrorCode() + database_text = error.databaseText() + driver_text = error.driverText() + log.sql.debug("SQL error:") log.sql.debug("type: {}".format( debug.qenum_key(QSqlError, error.type()))) - log.sql.debug("database text: {}".format(error.databaseText())) - log.sql.debug("driver text: {}".format(error.driverText())) - log.sql.debug("error code: {}".format(error.nativeErrorCode())) + log.sql.debug("database text: {}".format(database_text)) + log.sql.debug("driver text: {}".format(driver_text)) + log.sql.debug("error code: {}".format(error_code)) + environmental_errors = [ SqliteErrorCode.BUSY, SqliteErrorCode.READONLY, @@ -100,17 +106,15 @@ def raise_sqlite_error(msg, error): SqliteErrorCode.FULL, SqliteErrorCode.CANTOPEN, ] - # At least in init(), we can get errors like this: - # > type: ConnectionError - # > database text: out of memory - # > driver text: Error opening database - # > error code: -1 - environmental_strings = [ - "out of memory", - ] - errcode = error.nativeErrorCode() - if (errcode in environmental_errors or - (errcode == -1 and error.databaseText() in environmental_strings)): + + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70506 + # We don't know what the actual error was, but let's assume it's not us to + # blame... Usually this is something like an unreadable database file. + qtbug_70506 = (error_code == SqliteErrorCode.UNKNOWN and + driver_text == "Error opening database" and + database_text == "out of memory") + + if error_code in environmental_errors or qtbug_70506: raise SqlEnvironmentError(msg, error) else: raise SqlBugError(msg, error) diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index b06444f93..d2ddfaeb7 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -22,7 +22,6 @@ import os import os.path import contextlib -import mimetypes import html import jinja2 @@ -108,9 +107,8 @@ class Environment(jinja2.Environment): """Get a data: url for the broken qutebrowser logo.""" data = utils.read_file(path, binary=True) filename = utils.resource_filename(path) - mimetype = mimetypes.guess_type(filename) - assert mimetype is not None, path - return urlutils.data_url(mimetype[0], data).toString() + mimetype = utils.guess_mimetype(filename) + return urlutils.data_url(mimetype, data).toString() def getattr(self, obj, attribute): """Override jinja's getattr() to be less clever. diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index fe255331d..e9d4c3659 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -33,6 +33,7 @@ import contextlib import socket import shlex import glob +import mimetypes from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -683,3 +684,19 @@ def chunk(elems, n): raise ValueError("n needs to be at least 1!") for i in range(0, len(elems), n): yield elems[i:i + n] + + +def guess_mimetype(filename, fallback=False): + """Guess a mimetype based on a filename. + + Args: + filename: The filename to check. + fallback: Fall back to application/octet-stream if unknown. + """ + mimetype, _encoding = mimetypes.guess_type(filename) + if mimetype is None: + if fallback: + return 'application/octet-stream' + else: + raise ValueError("Got None mimetype for {}".format(filename)) + return mimetype diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 254132b3c..83227a0c2 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -209,15 +209,6 @@ def build_mac(): return [(dmg_name, 'application/x-apple-diskimage', 'macOS .dmg')] -def patch_windows(out_dir): - """Copy missing DLLs for windows into the given output.""" - dll_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'site-packages', - 'PyQt5', 'Qt', 'bin') - dlls = ['libEGL.dll', 'libGLESv2.dll', 'libeay32.dll', 'ssleay32.dll'] - for dll in dlls: - shutil.copy(os.path.join(dll_dir, dll), out_dir) - - def build_windows(): """Build windows executables/setups.""" utils.print_title("Updating 3rdparty content") @@ -252,7 +243,9 @@ def build_windows(): _maybe_remove(out_64) call_tox('pyinstaller', '-r', python=python_x64) shutil.move(out_pyinstaller, out_64) - patch_windows(out_64) + + utils.print_title("Running 64bit smoke test") + smoke_test(os.path.join(out_64, 'qutebrowser.exe')) utils.print_title("Building installers") subprocess.run(['makensis.exe', @@ -268,9 +261,6 @@ def build_windows(): 'Windows 64bit installer'), ] - utils.print_title("Running 64bit smoke test") - smoke_test(os.path.join(out_64, 'qutebrowser.exe')) - utils.print_title("Zipping 64bit standalone...") name = 'qutebrowser-{}-windows-standalone-amd64'.format( qutebrowser.__version__) @@ -375,6 +365,8 @@ def pypi_upload(artifacts): def main(): parser = argparse.ArgumentParser() + parser.add_argument('--no-asciidoc', action='store_true', + help="Don't generate docs") parser.add_argument('--asciidoc', help="Full path to python and " "asciidoc.py. If not given, it's searched in PATH.", nargs=2, required=False, @@ -392,7 +384,11 @@ def main(): import github3 # pylint: disable=unused-variable read_github_token() - run_asciidoc2html(args) + if args.no_asciidoc: + os.makedirs(os.path.join('qutebrowser', 'html', 'doc'), exist_ok=True) + else: + run_asciidoc2html(args) + if os.name == 'nt': artifacts = build_windows() elif sys.platform == 'darwin': diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 32c5afc49..5c678ac96 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -64,6 +64,8 @@ PERFECT_FILES = [ 'browser/webkit/cookies.py'), ('tests/unit/browser/test_history.py', 'browser/history.py'), + ('tests/unit/browser/test_pdfjs.py', + 'browser/pdfjs.py'), ('tests/unit/browser/webkit/http/test_http.py', 'browser/webkit/http.py'), ('tests/unit/browser/webkit/http/test_content_disposition.py', @@ -147,6 +149,8 @@ PERFECT_FILES = [ 'config/configcommands.py'), ('tests/unit/config/test_configutils.py', 'config/configutils.py'), + ('tests/unit/config/test_configcache.py', + 'config/configcache.py'), ('tests/unit/utils/test_qtutils.py', 'utils/qtutils.py'), diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 341088db8..0375554b8 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -65,9 +65,16 @@ def temp_basedir_env(tmpdir, short_tmpdir): runtime_dir.ensure(dir=True) runtime_dir.chmod(0o700) - (data_dir / 'qutebrowser' / 'state').write_text( - '[general]\nquickstart-done = 1\nbackend-warning-shown=1', - encoding='utf-8', ensure=True) + lines = [ + '[general]', + 'quickstart-done = 1', + 'backend-warning-shown = 1', + 'old-qt-warning-shown = 1', + 'webkit-warning-shown = 1', + ] + + state_file = data_dir / 'qutebrowser' / 'state' + state_file.write_text('\n'.join(lines), encoding='utf-8', ensure=True) env = { 'XDG_DATA_HOME': str(data_dir), diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index ec562c3b4..5ce081e3b 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -42,9 +42,9 @@ from PyQt5.QtNetwork import QNetworkCookieJar import helpers.stubs as stubsmod import helpers.utils from qutebrowser.config import (config, configdata, configtypes, configexc, - configfiles) + configfiles, configcache) from qutebrowser.utils import objreg, standarddir, utils, usertypes -from qutebrowser.browser import greasemonkey +from qutebrowser.browser import greasemonkey, history from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql, objects from qutebrowser.keyinput import modeman @@ -253,6 +253,9 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub): container = config.ConfigContainer(conf) monkeypatch.setattr(config, 'val', container) + cache = configcache.ConfigCache() + monkeypatch.setattr(config, 'cache', cache) + try: configtypes.Font.monospace_fonts = container.fonts.monospace except configexc.NoOptionError: @@ -569,3 +572,14 @@ def download_stub(win_registry, tmpdir, stubs): objreg.register('qtnetwork-download-manager', stub) yield stub objreg.delete('qtnetwork-download-manager') + + +@pytest.fixture +def web_history(fake_save_manager, tmpdir, init_sql, config_stub, stubs): + """Create a web history and register it into objreg.""" + config_stub.val.completion.timestamp_format = '%Y-%m-%d' + config_stub.val.completion.web_history.max_items = -1 + web_history = history.WebHistory(stubs.FakeHistoryProgress()) + objreg.register('web-history', web_history) + yield web_history + objreg.delete('web-history') diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 84e5b0125..f10522a02 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -632,3 +632,22 @@ class FakeDownloadManager: shutil.copyfileobj(fake_url_file, download_item.fileobj) self.downloads.append(download_item) return download_item + + +class FakeHistoryProgress: + + """Fake for a WebHistoryProgress object.""" + + def __init__(self): + self._started = False + self._finished = False + self._value = 0 + + def start(self, _text, _maximum): + self._started = True + + def tick(self): + self._value += 1 + + def finish(self): + self._finished = True diff --git a/tests/unit/browser/test_adblock.py b/tests/unit/browser/test_adblock.py index afb556559..f9a695f5b 100644 --- a/tests/unit/browser/test_adblock.py +++ b/tests/unit/browser/test_adblock.py @@ -33,7 +33,7 @@ pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir') # TODO See ../utils/test_standarddirutils for OSError and caplog assertion -WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io') +WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io', 'http://*.edu') BLOCKLIST_HOSTS = ('localhost', 'mediumhost.io', @@ -50,7 +50,8 @@ URLS_TO_CHECK = ('http://localhost', 'http://ads.worsthostever.net', 'http://goodhost.gov', 'ftp://verygoodhost.com', - 'http://qutebrowser.org') + 'http://qutebrowser.org', + 'http://veryverygoodhost.edu') class BaseDirStub: diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 2f4827b0a..5b84eac4c 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -37,110 +37,106 @@ def prerequisites(config_stub, fake_save_manager, init_sql, fake_args): config_stub.data = {'general': {'private-browsing': False}} -@pytest.fixture() -def hist(tmpdir): - return history.WebHistory() - - class TestSpecialMethods: - def test_iter(self, hist): + def test_iter(self, web_history): urlstr = 'http://www.example.com/' url = QUrl(urlstr) - hist.add_url(url, atime=12345) + web_history.add_url(url, atime=12345) - assert list(hist) == [(urlstr, '', 12345, False)] + assert list(web_history) == [(urlstr, '', 12345, False)] - def test_len(self, hist): - assert len(hist) == 0 + def test_len(self, web_history): + assert len(web_history) == 0 url = QUrl('http://www.example.com/') - hist.add_url(url) + web_history.add_url(url) - assert len(hist) == 1 + assert len(web_history) == 1 - def test_contains(self, hist): - hist.add_url(QUrl('http://www.example.com/'), title='Title', - atime=12345) - assert 'http://www.example.com/' in hist - assert 'www.example.com' not in hist - assert 'Title' not in hist - assert 12345 not in hist + def test_contains(self, web_history): + web_history.add_url(QUrl('http://www.example.com/'), + title='Title', atime=12345) + assert 'http://www.example.com/' in web_history + assert 'www.example.com' not in web_history + assert 'Title' not in web_history + assert 12345 not in web_history class TestGetting: - def test_get_recent(self, hist): - hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) - hist.add_url(QUrl('http://example.com/'), atime=12345) - assert list(hist.get_recent()) == [ + def test_get_recent(self, web_history): + web_history.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) + web_history.add_url(QUrl('http://example.com/'), atime=12345) + assert list(web_history.get_recent()) == [ ('http://www.qutebrowser.org/', '', 67890, False), ('http://example.com/', '', 12345, False), ] - def test_entries_between(self, hist): - hist.add_url(QUrl('http://www.example.com/1'), atime=12345) - hist.add_url(QUrl('http://www.example.com/2'), atime=12346) - hist.add_url(QUrl('http://www.example.com/3'), atime=12347) - hist.add_url(QUrl('http://www.example.com/4'), atime=12348) - hist.add_url(QUrl('http://www.example.com/5'), atime=12348) - hist.add_url(QUrl('http://www.example.com/6'), atime=12349) - hist.add_url(QUrl('http://www.example.com/7'), atime=12350) + def test_entries_between(self, web_history): + web_history.add_url(QUrl('http://www.example.com/1'), atime=12345) + web_history.add_url(QUrl('http://www.example.com/2'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/3'), atime=12347) + web_history.add_url(QUrl('http://www.example.com/4'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/5'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/6'), atime=12349) + web_history.add_url(QUrl('http://www.example.com/7'), atime=12350) - times = [x.atime for x in hist.entries_between(12346, 12349)] + times = [x.atime for x in web_history.entries_between(12346, 12349)] assert times == [12349, 12348, 12348, 12347] - def test_entries_before(self, hist): - hist.add_url(QUrl('http://www.example.com/1'), atime=12346) - hist.add_url(QUrl('http://www.example.com/2'), atime=12346) - hist.add_url(QUrl('http://www.example.com/3'), atime=12347) - hist.add_url(QUrl('http://www.example.com/4'), atime=12348) - hist.add_url(QUrl('http://www.example.com/5'), atime=12348) - hist.add_url(QUrl('http://www.example.com/6'), atime=12348) - hist.add_url(QUrl('http://www.example.com/7'), atime=12349) - hist.add_url(QUrl('http://www.example.com/8'), atime=12349) + def test_entries_before(self, web_history): + web_history.add_url(QUrl('http://www.example.com/1'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/2'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/3'), atime=12347) + web_history.add_url(QUrl('http://www.example.com/4'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/5'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/6'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/7'), atime=12349) + web_history.add_url(QUrl('http://www.example.com/8'), atime=12349) times = [x.atime for x in - hist.entries_before(12348, limit=3, offset=2)] + web_history.entries_before(12348, limit=3, offset=2)] assert times == [12348, 12347, 12346] class TestDelete: - def test_clear(self, qtbot, tmpdir, hist, mocker): - hist.add_url(QUrl('http://example.com/')) - hist.add_url(QUrl('http://www.qutebrowser.org/')) + def test_clear(self, qtbot, tmpdir, web_history, mocker): + web_history.add_url(QUrl('http://example.com/')) + web_history.add_url(QUrl('http://www.qutebrowser.org/')) m = mocker.patch('qutebrowser.browser.history.message.confirm_async', new=mocker.Mock, spec=[]) - hist.clear() + web_history.clear() assert m.called - def test_clear_force(self, qtbot, tmpdir, hist): - hist.add_url(QUrl('http://example.com/')) - hist.add_url(QUrl('http://www.qutebrowser.org/')) - hist.clear(force=True) - assert not len(hist) - assert not len(hist.completion) + def test_clear_force(self, qtbot, tmpdir, web_history): + web_history.add_url(QUrl('http://example.com/')) + web_history.add_url(QUrl('http://www.qutebrowser.org/')) + web_history.clear(force=True) + assert not len(web_history) + assert not len(web_history.completion) @pytest.mark.parametrize('raw, escaped', [ ('http://example.com/1', 'http://example.com/1'), ('http://example.com/1 2', 'http://example.com/1%202'), ]) - def test_delete_url(self, hist, raw, escaped): - hist.add_url(QUrl('http://example.com/'), atime=0) - hist.add_url(QUrl(escaped), atime=0) - hist.add_url(QUrl('http://example.com/2'), atime=0) + def test_delete_url(self, web_history, raw, escaped): + web_history.add_url(QUrl('http://example.com/'), atime=0) + web_history.add_url(QUrl(escaped), atime=0) + web_history.add_url(QUrl('http://example.com/2'), atime=0) - before = set(hist) - completion_before = set(hist.completion) + before = set(web_history) + completion_before = set(web_history.completion) - hist.delete_url(QUrl(raw)) + web_history.delete_url(QUrl(raw)) - diff = before.difference(set(hist)) + diff = before.difference(set(web_history)) assert diff == {(escaped, '', 0, False)} - completion_diff = completion_before.difference(set(hist.completion)) + completion_diff = completion_before.difference( + set(web_history.completion)) assert completion_diff == {(raw, '', 0)} @@ -164,30 +160,32 @@ class TestAdd: 'https://user@example.com', 'https://user@example.com'), ] ) - def test_add_url(self, qtbot, hist, + def test_add_url(self, qtbot, web_history, url, atime, title, redirect, history_url, completion_url): - hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect) - assert list(hist) == [(history_url, title, atime, redirect)] + web_history.add_url(QUrl(url), atime=atime, title=title, + redirect=redirect) + assert list(web_history) == [(history_url, title, atime, redirect)] if completion_url is None: - assert not len(hist.completion) + assert not len(web_history.completion) else: - assert list(hist.completion) == [(completion_url, title, atime)] + expected = [(completion_url, title, atime)] + assert list(web_history.completion) == expected - def test_no_sql_history(self, hist, fake_args): + def test_no_sql_web_history(self, web_history, fake_args): fake_args.debug_flags = 'no-sql-history' - hist.add_url(QUrl('https://www.example.com/'), atime=12346, - title='Hello World', redirect=False) - assert not list(hist) + web_history.add_url(QUrl('https://www.example.com/'), atime=12346, + title='Hello World', redirect=False) + assert not list(web_history) - def test_invalid(self, qtbot, hist, caplog): + def test_invalid(self, qtbot, web_history, caplog): with caplog.at_level(logging.WARNING): - hist.add_url(QUrl()) - assert not list(hist) - assert not list(hist.completion) + web_history.add_url(QUrl()) + assert not list(web_history) + assert not list(web_history.completion) @pytest.mark.parametrize('environmental', [True, False]) @pytest.mark.parametrize('completion', [True, False]) - def test_error(self, monkeypatch, hist, message_mock, caplog, + def test_error(self, monkeypatch, web_history, message_mock, caplog, environmental, completion): def raise_error(url, replace=False): if environmental: @@ -196,18 +194,18 @@ class TestAdd: raise sql.SqlBugError("Error message") if completion: - monkeypatch.setattr(hist.completion, 'insert', raise_error) + monkeypatch.setattr(web_history.completion, 'insert', raise_error) else: - monkeypatch.setattr(hist, 'insert', raise_error) + monkeypatch.setattr(web_history, 'insert', raise_error) if environmental: with caplog.at_level(logging.ERROR): - hist.add_url(QUrl('https://www.example.org/')) + web_history.add_url(QUrl('https://www.example.org/')) msg = message_mock.getmsg(usertypes.MessageLevel.error) assert msg.text == "Failed to write history: Error message" else: with pytest.raises(sql.SqlBugError): - hist.add_url(QUrl('https://www.example.org/')) + web_history.add_url(QUrl('https://www.example.org/')) @pytest.mark.parametrize('level, url, req_url, expected', [ (logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]), @@ -218,32 +216,33 @@ class TestAdd: (logging.WARNING, 'data:foo', '', []), (logging.WARNING, 'a.com', 'data:foo', []), ]) - def test_from_tab(self, hist, caplog, mock_time, + def test_from_tab(self, web_history, caplog, mock_time, level, url, req_url, expected): with caplog.at_level(level): - hist.add_from_tab(QUrl(url), QUrl(req_url), 'title') - assert set(hist) == set(expected) + web_history.add_from_tab(QUrl(url), QUrl(req_url), 'title') + assert set(web_history) == set(expected) - def test_exclude(self, hist, config_stub): + def test_exclude(self, web_history, config_stub): """Excluded URLs should be in the history but not completion.""" config_stub.val.completion.web_history.exclude = ['*.example.org'] url = QUrl('http://www.example.org/') - hist.add_from_tab(url, url, 'title') - assert list(hist) - assert not list(hist.completion) + web_history.add_from_tab(url, url, 'title') + assert list(web_history) + assert not list(web_history.completion) class TestHistoryInterface: @pytest.fixture - def hist_interface(self, hist): + def hist_interface(self, web_history): # pylint: disable=invalid-name QtWebKit = pytest.importorskip('PyQt5.QtWebKit') from qutebrowser.browser.webkit import webkithistory QWebHistoryInterface = QtWebKit.QWebHistoryInterface # pylint: enable=invalid-name - hist.add_url(url=QUrl('http://www.example.com/'), title='example') - interface = webkithistory.WebHistoryInterface(hist) + web_history.add_url(url=QUrl('http://www.example.com/'), + title='example') + interface = webkithistory.WebHistoryInterface(web_history) QWebHistoryInterface.setDefaultInterface(interface) yield QWebHistoryInterface.setDefaultInterface(None) @@ -261,9 +260,9 @@ class TestInit: def cleanup_init(self): # prevent test_init from leaking state yield - hist = objreg.get('web-history', None) - if hist is not None: - hist.setParent(None) + web_history = objreg.get('web-history', None) + if web_history is not None: + web_history.setParent(None) objreg.delete('web-history') try: from PyQt5.QtWebKit import QWebHistoryInterface @@ -304,202 +303,130 @@ class TestInit: assert default_interface is None -class TestImport: - - def test_import_txt(self, hist, data_tmpdir, monkeypatch, stubs): - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - # empty line is deliberate, to test skipping empty lines - histfile.write('''12345 http://example.com/ title - 12346 http://qutebrowser.org/ - 67890 http://example.com/path - - 68891-r http://example.com/path/other ''') - - hist.import_txt() - - assert list(hist) == [ - ('http://example.com/', 'title', 12345, False), - ('http://qutebrowser.org/', '', 12346, False), - ('http://example.com/path', '', 67890, False), - ('http://example.com/path/other', '', 68891, True) - ] - - assert not histfile.exists() - assert (data_tmpdir / 'history.bak').exists() - - def test_existing_backup(self, hist, data_tmpdir, monkeypatch, stubs): - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - bakfile = data_tmpdir / 'history.bak' - histfile.write('12345 http://example.com/ title') - bakfile.write('12346 http://qutebrowser.org/') - - hist.import_txt() - - assert list(hist) == [('http://example.com/', 'title', 12345, False)] - - assert not histfile.exists() - assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/', - '12345 http://example.com/ title'] - - @pytest.mark.parametrize('line', [ - '', - '#12345 http://example.com/commented', - - # https://bugreports.qt.io/browse/QTBUG-60364 - '12345 http://.com/', - '12345 https://.com/', - '12345 http://www..com/', - '12345 https://www..com/', - - # issue #2646 - ('12345 data:text/html;' - 'charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-'), - ]) - def test_skip(self, hist, data_tmpdir, monkeypatch, stubs, line): - """import_txt should skip certain lines silently.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - histfile.write(line) - - hist.import_txt() - - assert not histfile.exists() - assert not len(hist) - - @pytest.mark.parametrize('line', [ - 'xyz http://example.com/bad-timestamp', - '12345', - 'http://example.com/no-timestamp', - '68891-r-r http://example.com/double-flag', - '68891-x http://example.com/bad-flag', - '68891 http://.com', - ]) - def test_invalid(self, hist, data_tmpdir, monkeypatch, stubs, caplog, - line): - """import_txt should fail on certain lines.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - histfile.write(line) - - with caplog.at_level(logging.ERROR): - hist.import_txt() - - assert any(rec.msg.startswith("Failed to import history:") - for rec in caplog.records) - - assert histfile.exists() - - def test_nonexistent(self, hist, data_tmpdir, monkeypatch, stubs): - """import_txt should do nothing if the history file doesn't exist.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - hist.import_txt() - - class TestDump: - def test_debug_dump_history(self, hist, tmpdir): - hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345) - hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346) - hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347) - hist.add_url(QUrl('http://example.com/4'), title="Title4", atime=12348, - redirect=True) + def test_debug_dump_history(self, web_history, tmpdir): + web_history.add_url(QUrl('http://example.com/1'), + title="Title1", atime=12345) + web_history.add_url(QUrl('http://example.com/2'), + title="Title2", atime=12346) + web_history.add_url(QUrl('http://example.com/3'), + title="Title3", atime=12347) + web_history.add_url(QUrl('http://example.com/4'), + title="Title4", atime=12348, redirect=True) histfile = tmpdir / 'history' - hist.debug_dump_history(str(histfile)) + web_history.debug_dump_history(str(histfile)) expected = ['12345 http://example.com/1 Title1', '12346 http://example.com/2 Title2', '12347 http://example.com/3 Title3', '12348-r http://example.com/4 Title4'] assert histfile.read() == '\n'.join(expected) - def test_nonexistent(self, hist, tmpdir): + def test_nonexistent(self, web_history, tmpdir): histfile = tmpdir / 'nonexistent' / 'history' with pytest.raises(cmdexc.CommandError): - hist.debug_dump_history(str(histfile)) + web_history.debug_dump_history(str(histfile)) class TestRebuild: - def test_delete(self, hist): - hist.insert({'url': 'example.com/1', 'title': 'example1', - 'redirect': False, 'atime': 1}) - hist.insert({'url': 'example.com/1', 'title': 'example1', - 'redirect': False, 'atime': 2}) - hist.insert({'url': 'example.com/2%203', 'title': 'example2', - 'redirect': False, 'atime': 3}) - hist.insert({'url': 'example.com/3', 'title': 'example3', - 'redirect': True, 'atime': 4}) - hist.insert({'url': 'example.com/2 3', 'title': 'example2', - 'redirect': False, 'atime': 5}) - hist.completion.delete_all() + def test_delete(self, web_history, stubs): + web_history.insert({'url': 'example.com/1', 'title': 'example1', + 'redirect': False, 'atime': 1}) + web_history.insert({'url': 'example.com/1', 'title': 'example1', + 'redirect': False, 'atime': 2}) + web_history.insert({'url': 'example.com/2%203', 'title': 'example2', + 'redirect': False, 'atime': 3}) + web_history.insert({'url': 'example.com/3', 'title': 'example3', + 'redirect': True, 'atime': 4}) + web_history.insert({'url': 'example.com/2 3', 'title': 'example2', + 'redirect': False, 'atime': 5}) + web_history.completion.delete_all() - hist2 = history.WebHistory() + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist2.completion) == [ ('example.com/1', 'example1', 2), ('example.com/2 3', 'example2', 5), ] - def test_no_rebuild(self, hist): + def test_no_rebuild(self, web_history, stubs): """Ensure that completion is not regenerated unless empty.""" - hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) - hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) - hist.completion.delete('url', 'example.com/2') + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') - hist2 = history.WebHistory() + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist2.completion) == [('example.com/1', '', 1)] - def test_user_version(self, hist, monkeypatch): + def test_user_version(self, web_history, stubs, monkeypatch): """Ensure that completion is regenerated if user_version changes.""" - hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) - hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) - hist.completion.delete('url', 'example.com/2') + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') - hist2 = history.WebHistory() + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist2.completion) == [('example.com/1', '', 1)] monkeypatch.setattr(history, '_USER_VERSION', history._USER_VERSION + 1) - hist3 = history.WebHistory() + hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist3.completion) == [ ('example.com/1', '', 1), ('example.com/2', '', 2), ] - def test_force_rebuild(self, hist): + def test_force_rebuild(self, web_history, stubs): """Ensure that completion is regenerated if we force a rebuild.""" - hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) - hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) - hist.completion.delete('url', 'example.com/2') + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') - hist2 = history.WebHistory() + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist2.completion) == [('example.com/1', '', 1)] hist2.metainfo['force_rebuild'] = True - hist3 = history.WebHistory() + hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist3.completion) == [ ('example.com/1', '', 1), ('example.com/2', '', 2), ] assert not hist3.metainfo['force_rebuild'] - def test_exclude(self, config_stub, hist): + def test_exclude(self, config_stub, web_history, stubs): """Ensure that patterns in completion.web_history.exclude are ignored. This setting should only be used for the completion. """ config_stub.val.completion.web_history.exclude = ['*.example.org'] - assert hist.metainfo['force_rebuild'] + assert web_history.metainfo['force_rebuild'] - hist.add_url(QUrl('http://example.com'), redirect=False, atime=1) - hist.add_url(QUrl('http://example.org'), redirect=False, atime=2) + web_history.add_url(QUrl('http://example.com'), + redirect=False, atime=1) + web_history.add_url(QUrl('http://example.org'), + redirect=False, atime=2) - hist2 = history.WebHistory() + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) assert list(hist2.completion) == [('http://example.com', '', 1)] - def test_unrelated_config_change(self, config_stub, hist): + def test_unrelated_config_change(self, config_stub, web_history): config_stub.val.history_gap_interval = 1234 - assert not hist.metainfo['force_rebuild'] + assert not web_history.metainfo['force_rebuild'] + + @pytest.mark.parametrize('patch_threshold', [True, False]) + def test_progress(self, web_history, config_stub, monkeypatch, stubs, + patch_threshold): + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.metainfo['force_rebuild'] = True + + if patch_threshold: + monkeypatch.setattr(history.WebHistory, '_PROGRESS_THRESHOLD', 1) + + progress = stubs.FakeHistoryProgress() + history.WebHistory(progress=progress) + assert progress._value == 2 + assert progress._finished + assert progress._started == patch_threshold class TestCompletionMetaInfo: @@ -527,3 +454,33 @@ class TestCompletionMetaInfo: assert not metainfo['force_rebuild'] metainfo['force_rebuild'] = True assert metainfo['force_rebuild'] + + +class TestHistoryProgress: + + @pytest.fixture + def progress(self): + return history.HistoryProgress() + + def test_no_start(self, progress): + """Test calling tick/finish without start.""" + progress.tick() + progress.finish() + assert progress._progress is None + assert progress._value == 1 + + def test_gui(self, qtbot, progress): + progress.start("Hello World", 42) + dialog = progress._progress + qtbot.add_widget(dialog) + progress.tick() + + assert dialog.isVisible() + assert dialog.labelText() == "Hello World" + assert dialog.minimum() == 0 + assert dialog.maximum() == 42 + assert dialog.value() == 1 + assert dialog.minimumDuration() == 500 + + progress.finish() + assert not dialog.isVisible() diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index a33dae5bf..91536e416 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -17,51 +17,117 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import textwrap - import pytest from PyQt5.QtCore import QUrl from qutebrowser.browser import pdfjs +from qutebrowser.utils import usertypes, utils + + +@pytest.mark.parametrize('available, snippet', [ + pytest.param(True, 'PDF.js viewer', + marks=pytest.mark.skipif(not pdfjs.is_available(), + reason='PDF.js unavailable')), + (False, '

No pdf.js installation found

'), + ('force', 'fake PDF.js'), +]) +def test_generate_pdfjs_page(available, snippet, monkeypatch): + if available == 'force': + monkeypatch.setattr(pdfjs, 'is_available', lambda: True) + monkeypatch.setattr(pdfjs, 'get_pdfjs_res', + lambda filename: b'fake PDF.js') + else: + monkeypatch.setattr(pdfjs, 'is_available', lambda: available) + + content = pdfjs.generate_pdfjs_page('example.pdf', QUrl()) + print(content) + assert snippet in content # Note that we got double protection, once because we use QUrl.FullyEncoded and # because we use qutebrowser.utils.javascript.string_escape. Characters # like " are already replaced by QUrl. -@pytest.mark.parametrize('url, expected', [ - ('http://foo.bar', "http://foo.bar"), - ('http://"', ''), - ('\0', '%00'), - ('http://foobar/");alert("attack!");', - 'http://foobar/%22);alert(%22attack!%22);'), +@pytest.mark.parametrize('filename, expected', [ + ('foo.bar', "foo.bar"), + ('foo"bar', "foo%22bar"), + ('foo\0bar', 'foo%00bar'), + ('foobar");alert("attack!");', + 'foobar%22);alert(%22attack!%22);'), ]) -def test_generate_pdfjs_script(url, expected): - expected_open = 'open("{}");'.format(expected) - url = QUrl(url) - actual = pdfjs._generate_pdfjs_script(url) +def test_generate_pdfjs_script(filename, expected): + expected_open = 'open("qute://pdfjs/file?filename={}");'.format(expected) + actual = pdfjs._generate_pdfjs_script(filename) assert expected_open in actual assert 'PDFView' in actual -def test_fix_urls(): - page = textwrap.dedent(""" - - - - - - """).strip() +@pytest.mark.parametrize('qt, backend, expected', [ + ('new', usertypes.Backend.QtWebEngine, False), + ('new', usertypes.Backend.QtWebKit, False), + ('old', usertypes.Backend.QtWebEngine, True), + ('old', usertypes.Backend.QtWebKit, False), + ('5.7', usertypes.Backend.QtWebEngine, False), + ('5.7', usertypes.Backend.QtWebKit, False), +]) +def test_generate_pdfjs_script_disable_object_url(monkeypatch, + qt, backend, expected): + if qt == 'new': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + False if version == '5.7.1' else True) + elif qt == 'old': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: False) + elif qt == '5.7': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + True if version == '5.7.1' else False) + else: + raise utils.Unreachable - expected = textwrap.dedent(""" - - - - - - """).strip() + monkeypatch.setattr(pdfjs.objects, 'backend', backend) - actual = pdfjs.fix_urls(page) - assert actual == expected + script = pdfjs._generate_pdfjs_script('testfile') + assert ('PDFJS.disableCreateObjectURL' in script) == expected + + +class TestResources: + + @pytest.fixture + def read_system_mock(self, mocker): + return mocker.patch.object(pdfjs, '_read_from_system', autospec=True) + + @pytest.fixture + def read_file_mock(self, mocker): + return mocker.patch.object(pdfjs.utils, 'read_file', autospec=True) + + def test_get_pdfjs_res_system(self, read_system_mock): + read_system_mock.return_value = (b'content', 'path') + + assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', 'path') + assert pdfjs.get_pdfjs_res('web/test') == b'content' + + read_system_mock.assert_called_with('/usr/share/pdf.js/', + ['web/test', 'test']) + + def test_get_pdfjs_res_bundled(self, read_system_mock, read_file_mock): + read_system_mock.return_value = (None, None) + + read_file_mock.return_value = b'content' + + assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', None) + assert pdfjs.get_pdfjs_res('web/test') == b'content' + + for path in pdfjs.SYSTEM_PDFJS_PATHS: + read_system_mock.assert_any_call(path, ['web/test', 'test']) + + def test_get_pdfjs_res_not_found(self, read_system_mock, read_file_mock): + read_system_mock.return_value = (None, None) + read_file_mock.side_effect = FileNotFoundError + + with pytest.raises(pdfjs.PDFJSNotFound, + match="Path 'web/test' not found"): + pdfjs.get_pdfjs_res_and_path('web/test') @pytest.mark.parametrize('path, expected', [ @@ -72,3 +138,58 @@ def test_fix_urls(): ]) def test_remove_prefix(path, expected): assert pdfjs._remove_prefix(path) == expected + + +@pytest.mark.parametrize('names, expected_name', [ + (['one'], 'one'), + (['doesnotexist', 'two'], 'two'), + (['one', 'two'], 'one'), + (['does', 'not', 'onexist'], None), +]) +def test_read_from_system(names, expected_name, tmpdir): + file1 = tmpdir / 'one' + file1.write_text('text1', encoding='ascii') + file2 = tmpdir / 'two' + file2.write_text('text2', encoding='ascii') + + if expected_name == 'one': + expected = (b'text1', str(file1)) + elif expected_name == 'two': + expected = (b'text2', str(file2)) + elif expected_name is None: + expected = (None, None) + + assert pdfjs._read_from_system(str(tmpdir), names) == expected + + +@pytest.mark.parametrize('available', [True, False]) +def test_is_available(available, mocker): + mock = mocker.patch.object(pdfjs, 'get_pdfjs_res', autospec=True) + if available: + mock.return_value = b'foo' + else: + mock.side_effect = pdfjs.PDFJSNotFound('build/pdf.js') + + assert pdfjs.is_available() == available + + +@pytest.mark.parametrize('mimetype, url, enabled, expected', [ + # PDF files + ('application/pdf', 'http://www.example.com', True, True), + ('application/x-pdf', 'http://www.example.com', True, True), + # Not a PDF + ('application/octet-stream', 'http://www.example.com', True, False), + # PDF.js disabled + ('application/pdf', 'http://www.example.com', False, False), + # Download button in PDF.js + ('application/pdf', 'blob:qute%3A///b45250b3', True, False), +]) +def test_should_use_pdfjs(mimetype, url, enabled, expected, config_stub): + config_stub.val.content.pdfjs = enabled + assert pdfjs.should_use_pdfjs(mimetype, QUrl(url)) == expected + + +def test_get_main_url(): + expected = ('qute://pdfjs/web/viewer.html?filename=' + 'hello?world.pdf&file=') + assert pdfjs.get_main_url('hello?world.pdf') == QUrl(expected) diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index 46113561b..db8f9b29b 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -20,12 +20,13 @@ import json import os import time +import logging -from PyQt5.QtCore import QUrl +import py.path # pylint: disable=no-name-in-module +from PyQt5.QtCore import QUrl, QUrlQuery import pytest -from qutebrowser.browser import history, qutescheme -from qutebrowser.utils import objreg +from qutebrowser.browser import qutescheme, pdfjs, downloads class TestJavascriptHandler: @@ -96,21 +97,12 @@ class TestHistoryHandler: return items - @pytest.fixture - def fake_web_history(self, fake_save_manager, tmpdir, init_sql, - config_stub): - """Create a fake web-history and register it into objreg.""" - web_history = history.WebHistory() - objreg.register('web-history', web_history) - yield web_history - objreg.delete('web-history') - @pytest.fixture(autouse=True) - def fake_history(self, fake_web_history, fake_args, entries): + def fake_history(self, web_history, fake_args, entries): """Create fake history.""" fake_args.debug_flags = [] for item in entries: - fake_web_history.add_url(**item) + web_history.add_url(**item) @pytest.mark.parametrize("start_time_offset, expected_item_count", [ (0, 4), @@ -134,7 +126,7 @@ class TestHistoryHandler: assert item['time'] <= start_time assert item['time'] > end_time - def test_exclude(self, fake_web_history, now, config_stub): + def test_exclude(self, web_history, now, config_stub): """Make sure the completion.web_history.exclude setting is not used.""" config_stub.val.completion.web_history.exclude = ['www.x.com'] @@ -143,7 +135,7 @@ class TestHistoryHandler: items = json.loads(data) assert items - def test_qute_history_benchmark(self, fake_web_history, benchmark, now): + def test_qute_history_benchmark(self, web_history, benchmark, now): r = range(100000) entries = { 'atime': [int(now - t) for t in r], @@ -152,7 +144,7 @@ class TestHistoryHandler: 'redirect': [False for _ in r], } - fake_web_history.insert_batch(entries) + web_history.insert_batch(entries) url = QUrl("qute://history/data?start_time={}".format(now)) _mimetype, data = benchmark(qutescheme.qute_history, url) assert len(json.loads(data)) > 1 @@ -179,3 +171,68 @@ class TestHelpHandler: mimetype, data = qutescheme.qute_help(QUrl('qute://help/foo.bin')) assert mimetype == 'application/octet-stream' assert data == b'\xff' + + +class TestPDFJSHandler: + + """Test the qute://pdfjs endpoint.""" + + @pytest.fixture(autouse=True) + def fake_pdfjs(self, monkeypatch): + def get_pdfjs_res(path): + if path == '/existing/file.html': + return b'foobar' + raise pdfjs.PDFJSNotFound(path) + + monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res) + + @pytest.fixture + def download_tmpdir(self): + tdir = downloads.temp_download_manager.get_tmpdir() + yield py.path.local(tdir.name) # pylint: disable=no-member + tdir.cleanup() + + def test_existing_resource(self): + """Test with a resource that exists.""" + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/existing/file.html')) + assert data == b'foobar' + + def test_nonexisting_resource(self, caplog): + """Test with a resource that does not exist.""" + with caplog.at_level(logging.WARNING, 'misc'): + with pytest.raises(qutescheme.NotFoundError): + qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html')) + assert len(caplog.records) == 1 + assert (caplog.records[0].message == + 'pdfjs resource requested but not found: /no/file.html') + + def test_viewer_page(self): + """Load the /web/viewer.html page.""" + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/web/viewer.html?filename=foobar')) + assert b'PDF.js' in data + + def test_viewer_no_filename(self): + with pytest.raises(qutescheme.UrlInvalidError): + qutescheme.data_for_url(QUrl('qute://pdfjs/web/viewer.html')) + + def test_file(self, download_tmpdir): + """Load a file via qute://pdfjs/file.""" + (download_tmpdir / 'testfile').write_binary(b'foo') + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/file?filename=testfile')) + assert data == b'foo' + + def test_file_no_filename(self): + with pytest.raises(qutescheme.UrlInvalidError): + qutescheme.data_for_url(QUrl('qute://pdfjs/file')) + + @pytest.mark.parametrize('sep', ['/', os.sep]) + def test_file_pathsep(self, sep): + url = QUrl('qute://pdfjs/file') + query = QUrlQuery() + query.addQueryItem('filename', 'foo{}bar'.format(sep)) + url.setQuery(query) + with pytest.raises(qutescheme.RequestDeniedError): + qutescheme.data_for_url(url) diff --git a/tests/unit/browser/webkit/network/test_webkitqutescheme.py b/tests/unit/browser/webkit/network/test_webkitqutescheme.py deleted file mode 100644 index 05190d230..000000000 --- a/tests/unit/browser/webkit/network/test_webkitqutescheme.py +++ /dev/null @@ -1,63 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016-2018 Daniel Schadt -# Copyright 2016-2018 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 logging - -import pytest -from PyQt5.QtCore import QUrl - -from qutebrowser.utils import usertypes -from qutebrowser.browser import pdfjs, qutescheme -# pylint: disable=unused-import -from qutebrowser.browser.webkit.network import webkitqutescheme -# pylint: enable=unused-import - - -class TestPDFJSHandler: - """Test the qute://pdfjs endpoint.""" - - @pytest.fixture(autouse=True) - def fake_pdfjs(self, monkeypatch): - def get_pdfjs_res(path): - if path == '/existing/file.html': - return b'foobar' - raise pdfjs.PDFJSNotFound(path) - - monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res) - - @pytest.fixture(autouse=True) - def patch_backend(self, monkeypatch): - monkeypatch.setattr(qutescheme.objects, 'backend', - usertypes.Backend.QtWebKit) - - def test_existing_resource(self): - """Test with a resource that exists.""" - _mimetype, data = qutescheme.data_for_url( - QUrl('qute://pdfjs/existing/file.html')) - assert data == b'foobar' - - def test_nonexisting_resource(self, caplog): - """Test with a resource that does not exist.""" - with caplog.at_level(logging.WARNING, 'misc'): - with pytest.raises(qutescheme.NotFoundError): - qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html')) - assert len(caplog.records) == 1 - assert (caplog.records[0].message == - 'pdfjs resource requested but not found: /no/file.html') diff --git a/tests/unit/browser/webkit/test_downloads.py b/tests/unit/browser/webkit/test_downloads.py index 571e21704..b42caed00 100644 --- a/tests/unit/browser/webkit/test_downloads.py +++ b/tests/unit/browser/webkit/test_downloads.py @@ -68,10 +68,6 @@ def test_page_titles(url, title, out): class TestDownloadTarget: - def test_base(self): - with pytest.raises(NotImplementedError): - downloads._DownloadTarget() - def test_filename(self): target = downloads.FileDownloadTarget("/foo/bar") assert target.filename == "/foo/bar" diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 5181cab6f..81cdfc19e 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -30,8 +30,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.completion import completer from qutebrowser.completion.models import miscmodels, urlmodel, configmodel from qutebrowser.config import configdata, configtypes -from qutebrowser.utils import objreg, usertypes -from qutebrowser.browser import history +from qutebrowser.utils import usertypes from qutebrowser.commands import cmdutils @@ -168,17 +167,6 @@ def bookmarks(bookmark_manager_stub): return bookmark_manager_stub -@pytest.fixture -def web_history(init_sql, stubs, config_stub): - """Fixture which provides a web-history object.""" - config_stub.val.completion.timestamp_format = '%Y-%m-%d' - config_stub.val.completion.web_history.max_items = -1 - stub = history.WebHistory() - objreg.register('web-history', stub) - yield stub - objreg.delete('web-history') - - @pytest.fixture def web_history_populated(web_history): """Pre-populate the web-history database.""" diff --git a/tests/unit/config/test_configcache.py b/tests/unit/config/test_configcache.py new file mode 100644 index 000000000..91e2f22fa --- /dev/null +++ b/tests/unit/config/test_configcache.py @@ -0,0 +1,52 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Jay Kamat : +# +# 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 . + +# False-positives +# FIXME: Report this to pylint? +# pylint: disable=unsubscriptable-object + +"""Tests for qutebrowser.config.configcache.""" + +import pytest + +from qutebrowser.config import config + + +def test_configcache_except_pattern(config_stub): + with pytest.raises(AssertionError): + assert config.cache['content.javascript.enabled'] + + +def test_configcache_error_set(config_stub): + # pylint: disable=unsupported-assignment-operation + with pytest.raises(TypeError): + config.cache['content.javascript.enabled'] = True + + +def test_configcache_get(config_stub): + assert len(config.cache._cache) == 0 + assert not config.cache['auto_save.session'] + assert len(config.cache._cache) == 1 + assert not config.cache['auto_save.session'] + + +def test_configcache_get_after_set(config_stub): + assert not config.cache['auto_save.session'] + config_stub.val.auto_save.session = True + assert config.cache['auto_save.session'] diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 8f2c83a85..2b793b58b 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -235,6 +235,21 @@ class TestYaml: data = autoconfig.read() assert 'bindings.default' not in data + @pytest.mark.parametrize('public_only, expected', [ + (True, 'default-public-interface-only'), + (False, 'all-interfaces'), + ]) + def test_webrtc(self, yaml, autoconfig, public_only, expected): + """Tests for migration of content.webrtc_public_interfaces_only.""" + autoconfig.write({'content.webrtc_public_interfaces_only': + {'global': public_only}}) + + yaml.load() + yaml._save() + + data = autoconfig.read() + assert data['content.webrtc_ip_handling_policy']['global'] == expected + @pytest.mark.parametrize('show, expected', [ (True, 'always'), (False, 'never'), diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 56f2a3c90..40f143086 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -359,10 +359,11 @@ class TestQtArgs: return parser @pytest.fixture(autouse=True) - def patch_version_check(self, monkeypatch): - """Make sure no --disable-shared-workers argument gets added.""" + def reduce_args(self, monkeypatch, config_stub): + """Make sure no --disable-shared-workers/referer argument get added.""" monkeypatch.setattr(configinit.qtutils, 'version_check', lambda version, compiled=False: True) + config_stub.val.content.headers.referer = 'always' @pytest.mark.parametrize('args, expected', [ # No Qt arguments @@ -438,23 +439,35 @@ class TestQtArgs: assert ('--autoplay-policy=user-gesture-required' in args) == added @utils.qt59 - @pytest.mark.parametrize('new_version, public_only, added', [ - (True, True, False), # new enough to not need it - (False, False, False), # option disabled - (False, True, True), + @pytest.mark.parametrize('policy, arg', [ + ('all-interfaces', None), + + ('default-public-and-private-interfaces', + '--force-webrtc-ip-handling-policy=' + 'default_public_and_private_interfaces'), + + ('default-public-interface-only', + '--force-webrtc-ip-handling-policy=' + 'default_public_interface_only'), + + ('disable-non-proxied-udp', + '--force-webrtc-ip-handling-policy=' + 'disable_non_proxied_udp'), ]) def test_webrtc(self, config_stub, monkeypatch, parser, - new_version, public_only, added): + policy, arg): monkeypatch.setattr(configinit.objects, 'backend', usertypes.Backend.QtWebEngine) - config_stub.val.content.webrtc_public_interfaces_only = public_only - monkeypatch.setattr(configinit.qtutils, 'version_check', - lambda version, compiled=False: new_version) + config_stub.val.content.webrtc_ip_handling_policy = policy parsed = parser.parse_args([]) args = configinit.qt_args(parsed) - arg = '--force-webrtc-ip-handling-policy=default_public_interface_only' - assert (arg in args) == added + + if arg is None: + assert not any(a.startswith('--force-webrtc-ip-handling-policy=') + for a in args) + else: + assert arg in args @pytest.mark.parametrize('canvas_reading, added', [ (True, False), # canvas reading enabled @@ -470,6 +483,67 @@ class TestQtArgs: args = configinit.qt_args(parsed) assert ('--disable-reading-from-canvas' in args) == added + @pytest.mark.parametrize('process_model, added', [ + ('process-per-site-instance', False), + ('process-per-site', True), + ('single-process', True), + ]) + def test_process_model(self, config_stub, monkeypatch, parser, + process_model, added): + monkeypatch.setattr(configinit.objects, 'backend', + usertypes.Backend.QtWebEngine) + + config_stub.val.qt.process_model = process_model + parsed = parser.parse_args([]) + args = configinit.qt_args(parsed) + + if added: + assert '--' + process_model in args + else: + assert '--process-per-site' not in args + assert '--single-process' not in args + assert '--process-per-site-instance' not in args + assert '--process-per-tab' not in args + + @pytest.mark.parametrize('low_end_device_mode, arg', [ + ('auto', None), + ('always', '--enable-low-end-device-mode'), + ('never', '--disable-low-end-device-mode'), + ]) + def test_low_end_device_mode(self, config_stub, monkeypatch, parser, + low_end_device_mode, arg): + monkeypatch.setattr(configinit.objects, 'backend', + usertypes.Backend.QtWebEngine) + + config_stub.val.qt.low_end_device_mode = low_end_device_mode + parsed = parser.parse_args([]) + args = configinit.qt_args(parsed) + + if arg is None: + assert '--enable-low-end-device-mode' not in args + assert '--disable-low-end-device-mode' not in args + else: + assert arg in args + + @pytest.mark.parametrize('referer, arg', [ + ('always', None), + ('never', '--no-referrers'), + ('same-domain', '--reduced-referrer-granularity'), + ]) + def test_referer(self, config_stub, monkeypatch, parser, referer, arg): + monkeypatch.setattr(configinit.objects, 'backend', + usertypes.Backend.QtWebEngine) + + config_stub.val.content.headers.referer = referer + parsed = parser.parse_args([]) + args = configinit.qt_args(parsed) + + if arg is None: + assert '--no-referrers' not in args + assert '--reduced-referrer-granularity' not in args + else: + assert arg in args + @pytest.mark.parametrize('arg, confval, used', [ # overridden by commandline arg diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py index 15fa4321e..79983a5c9 100644 --- a/tests/unit/javascript/test_greasemonkey.py +++ b/tests/unit/javascript/test_greasemonkey.py @@ -130,6 +130,26 @@ def test_load_emits_signal(qtbot): gm_manager.load_scripts() +def test_utf8_bom(): + """Make sure UTF-8 BOMs are stripped from scripts. + + If we don't strip them, we'll have a BOM in the middle of the file, causing + QtWebEngine to not catch the "// ==UserScript==" line. + """ + script = textwrap.dedent(""" + \N{BYTE ORDER MARK}// ==UserScript== + // @name qutebrowser test userscript + // ==/UserScript== + """.lstrip('\n')) + _save_script(script, 'bom.user.js') + gm_manager = greasemonkey.GreasemonkeyManager() + + scripts = gm_manager.all_scripts() + assert len(scripts) == 1 + script = scripts[0] + assert '// ==UserScript==' in script.code().splitlines() + + def test_required_scripts_are_included(download_stub, tmpdir): test_require_script = textwrap.dedent(""" // ==UserScript== diff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py index 82c33841f..54c785286 100644 --- a/tests/unit/misc/test_sql.py +++ b/tests/unit/misc/test_sql.py @@ -49,6 +49,19 @@ class TestSqlError: with pytest.raises(exception): sql.raise_sqlite_error("Message", sql_err) + def test_qtbug_70506(self): + """Test Qt's wrong handling of errors while opening the database. + + Due to https://bugreports.qt.io/browse/QTBUG-70506 we get an error with + "out of memory" as string and -1 as error code. + """ + sql_err = QSqlError("Error opening database", + "out of memory", + QSqlError.UnknownError, + sql.SqliteErrorCode.UNKNOWN) + with pytest.raises(sql.SqlEnvironmentError): + sql.raise_sqlite_error("Message", sql_err) + def test_logging(self, caplog): sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError, '23') diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 550213386..e8a96d448 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -816,3 +816,16 @@ def test_chunk(elems, n, expected): def test_chunk_invalid(n): with pytest.raises(ValueError): list(utils.chunk([], n)) + + +@pytest.mark.parametrize('filename, expected', [ + ('test.jpg', 'image/jpeg'), + ('test.blabla', 'application/octet-stream'), +]) +def test_guess_mimetype(filename, expected): + assert utils.guess_mimetype(filename, fallback=True) == expected + + +def test_guess_mimetype_no_fallback(): + with pytest.raises(ValueError): + utils.guess_mimetype('test.blabla')