Merge branch 'download-page-tests' of https://github.com/The-Compiler/qutebrowser into download-page

This commit is contained in:
Daniel Schadt 2015-11-11 21:03:36 +01:00
commit 62b0c4d178
37 changed files with 849 additions and 57 deletions

View File

@ -48,6 +48,8 @@ Added
* `colors -> downloads.bg.system`
- New command `:download-retry` to retry a failed download.
- New command `:download-clear` which replaces `:download-remove --all`.
- `:set-cmd-text` has a new `--append` argument to append to the current
statusbar text.
Changed
~~~~~~~
@ -82,6 +84,8 @@ Fixed
- Fixed a crash when a website presents a very small favicon.
- Fixed prompting for download directory when
`storage -> prompt-download-directory` was unset.
- Fixed crash when using `:follow-hint` outside of hint mode.
- Fixed crash when using `:set foo bar?` with invalid section/option.
v0.4.1
------

View File

@ -140,9 +140,9 @@ Contributors, sorted by the number of commits in descending order:
* Florian Bruhin
* Antoni Boucher
* Bruno Oliveira
* Lamar Pavel
* Alexander Cogneau
* Martin Tournoij
* Lamar Pavel
* Raphael Pierzina
* Joel Torstensson
* Daniel

View File

@ -577,7 +577,7 @@ If the option name ends with '?', the value of the option is shown instead. If t
[[set-cmd-text]]
=== set-cmd-text
Syntax: +:set-cmd-text [*--space*] 'text'+
Syntax: +:set-cmd-text [*--space*] [*--append*] 'text'+
Preset the statusbar to some text.
@ -586,6 +586,7 @@ Preset the statusbar to some text.
==== optional arguments
* +*-s*+, +*--space*+: If given, a space is added to the end.
* +*-a*+, +*--append*+: If given, the text is appended to the current text.
==== note
* This command does not split arguments after the last argument and handles quotes literally.

View File

@ -35,3 +35,4 @@ qt_log_ignore =
^Type conversion already registered from type .*
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
^QWaitCondition: Destroyed while threads are still waiting
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom

View File

@ -65,7 +65,8 @@ class DiskCache(QNetworkDiskCache):
"""Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
elif (section, option) == ('general', 'private-browsing'):
elif (section, option) == ('general', # pragma: no branch
'private-browsing'):
self._maybe_activate()
def cacheSize(self):

View File

@ -810,10 +810,15 @@ class DownloadManager(QAbstractListModel):
download = DownloadItem(reply, self._win_id, self)
download.cancelled.connect(
functools.partial(self.remove_item, download))
delay = config.get('ui', 'remove-finished-downloads')
if delay > -1 or auto_remove:
if delay > -1:
download.finished.connect(
functools.partial(self.remove_item_delayed, download, delay))
elif auto_remove:
download.finished.connect(
functools.partial(self.remove_item, download))
download.data_changed.connect(
functools.partial(self.on_data_changed, download))
download.error.connect(self.on_error)

View File

@ -946,7 +946,8 @@ class HintManager(QObject):
elems.label.setInnerXml(string)
handler()
@cmdutils.register(instance='hintmanager', scope='tab', hide=True)
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
modes=[usertypes.KeyMode.hint])
def follow_hint(self, keystring=None):
"""Follow a hint.
@ -958,6 +959,8 @@ class HintManager(QObject):
raise cmdexc.CommandError("No hint to follow")
else:
keystring = self._context.to_follow
elif keystring not in self._context.elems:
raise cmdexc.CommandError("No hint {}!".format(keystring))
self.fire(keystring, force=True)
@pyqtSlot('QSize')

View File

@ -360,17 +360,21 @@ class NetworkManager(QNetworkAccessManager):
req.setRawHeader('DNT'.encode('ascii'), dnt)
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
if self._tab_id is None:
current_url = QUrl() # generic NetworkManager, e.g. for downloads
else:
# There are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed.
current_url = QUrl()
if self._tab_id is not None:
try:
webview = objreg.get('webview', scope='tab',
window=self._win_id, tab=self._tab_id)
except KeyError:
# https://github.com/The-Compiler/qutebrowser/issues/889
current_url = QUrl()
else:
current_url = webview.url()
except (KeyError, RuntimeError, TypeError):
# https://github.com/The-Compiler/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in
# the middle of the webpage shutdown here.
current_url = QUrl()
self.set_referer(req, current_url)

View File

@ -292,7 +292,7 @@ class WebView(QWebView):
try:
elem = webelem.focus_elem(self.page().currentFrame())
except (webelem.IsNullError, RuntimeError):
log.mouse.warning("Element/page vanished!")
log.mouse.debug("Element/page vanished!")
return
if elem.is_editable():
log.mouse.debug("Clicked editable element (delayed)!")

View File

@ -29,6 +29,7 @@ import sys
import os.path
import functools
import configparser
import contextlib
import collections
import collections.abc
@ -666,6 +667,18 @@ class ConfigManager(QObject):
newval = val.typ.transform(newval)
return newval
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except (configexc.NoOptionError, configexc.NoSectionError,
configexc.ValidationError) as e:
raise cmdexc.CommandError("set: {}".format(e))
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
@cmdutils.register(name='set', instance='config', win_id='win_id',
completion=[Completion.section, Completion.option,
Completion.value])
@ -699,12 +712,12 @@ class ConfigManager(QObject):
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
if option.endswith('?'):
if option.endswith('?') and option != '?':
option = option[:-1]
print_ = True
else:
try:
if option.endswith('!') and value is None:
with self._handle_config_error():
if option.endswith('!') and option != '!' and value is None:
option = option[:-1]
val = self.get(section_, option)
layer = 'temp' if temp else 'conf'
@ -719,12 +732,10 @@ class ConfigManager(QObject):
else:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
if print_:
val = self.get(section_, option, transformed=False)
with self._handle_config_error():
val = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format(
section_, option, val), immediately=True)

View File

@ -92,7 +92,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0)
def set_cmd_text_command(self, text, space=False):
def set_cmd_text_command(self, text, space=False, append=False):
"""Preset the statusbar to some text.
//
@ -103,6 +103,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
text: The commandline to set.
space: If given, a space is added to the end.
append: If given, the text is appended to the current text.
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
@ -122,8 +123,14 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
# I'm not sure what's the best thing to do here
# https://github.com/The-Compiler/qutebrowser/issues/123
text = text.replace('{url}', url)
if space:
text += ' '
if append:
if not self.text():
raise cmdexc.CommandError("No current text!")
text = self.text() + text
if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text))

View File

@ -82,7 +82,7 @@ class ExternalEditor(QObject):
encoding = config.get('general', 'editor-encoding')
try:
with open(self._filename, 'r', encoding=encoding) as f:
text = ''.join(f.readlines()) # pragma: no branch
text = f.read() # pragma: no branch
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.

View File

@ -213,9 +213,9 @@ class LineParser(BaseLineParser):
"""Read the data from self._configfile."""
with self._open('r') as f:
if self._binary:
self.data = [line.rstrip(b'\n') for line in f.readlines()]
self.data = [line.rstrip(b'\n') for line in f]
else:
self.data = [line.rstrip('\n') for line in f.readlines()]
self.data = [line.rstrip('\n') for line in f]
def save(self):
"""Save the config file."""

View File

@ -320,10 +320,14 @@ def qt_message_handler(msg_type, context, msg):
level = logging.DEBUG
else:
level = qt_to_logging[msg_type]
if context.function is None:
func = 'none'
elif ':' in context.function:
func = '"{}"'.format(context.function)
else:
func = context.function
if context.category is None or context.category == 'default':
name = 'qt'
else:

View File

@ -112,7 +112,7 @@ def _release_info():
for fn in glob.glob("/etc/*-release"):
try:
with open(fn, 'r', encoding='utf-8') as f:
data.append((fn, ''.join(f.readlines()))) # pragma: no branch
data.append((fn, f.read())) # pragma: no branch
except OSError:
log.misc.exception("Error while reading {}.".format(fn))
return data

View File

@ -48,6 +48,8 @@ PERFECT_FILES = [
('tests/unit/commands/test_argparser.py',
'qutebrowser/commands/argparser.py'),
('tests/unit/browser/test_cache.py',
'qutebrowser/browser/cache.py'),
('tests/unit/browser/test_cookies.py',
'qutebrowser/browser/cookies.py'),
('tests/unit/browser/test_tabhistory.py',
@ -248,9 +250,9 @@ def main():
"""
utils.change_cwd()
if '--check-all' in sys.argv:
main_check_all()
return main_check_all()
else:
main_check()
return main_check()
if __name__ == '__main__':

View File

@ -29,6 +29,7 @@ from helpers import utils # pylint: disable=import-error
({'a': [1, 2, 3]}, {'a': [1]}),
({'a': [1, 2, 3]}, {'a': [..., 2]}),
(1.0, 1.00000001),
("foobarbaz", "foo*baz"),
])
def test_partial_compare_equal(val1, val2):
assert utils.partial_compare(val1, val2)
@ -43,6 +44,7 @@ def test_partial_compare_equal(val1, val2):
([1], {1: 2}),
({1: 1}, {1: [1]}),
({'a': [1, 2, 3]}, {'a': [..., 3]}),
("foo*baz", "foobarbaz"),
])
def test_partial_compare_not_equal(val1, val2):
assert not utils.partial_compare(val1, val2)

View File

@ -20,6 +20,9 @@
"""Partial comparison of dicts/lists."""
import fnmatch
def _partial_compare_dict(val1, val2):
for key in val2:
if key not in val1:
@ -71,6 +74,9 @@ def partial_compare(val1, val2):
elif isinstance(val2, float):
print("Doing float comparison")
equal = abs(val1 - val2) < 0.00001
elif isinstance(val2, str):
print("Doing string comparison")
equal = fnmatch.fnmatchcase(val1, val2)
else:
print("Comparing via ==")
equal = val1 == val2

View File

@ -0,0 +1 @@
simple.html

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Simple MHTML test</title>
</head>
<body>
<a href="/">normal link to another page</a>
</body>
</html>

View File

@ -0,0 +1,20 @@
Content-Type: multipart/related; boundary="---=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67"
MIME-Version: 1.0
-----=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67
Content-Location: http://localhost:1234/data/downloads/mhtml/simple/simple.html
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE=20html><html><head>
=20=20=20=20=20=20=20=20<meta=20charset=3D"utf-8">
=20=20=20=20=20=20=20=20<title>Simple=20MHTML=20test</title>
=20=20=20=20</head>
=20=20=20=20<body>
=20=20=20=20=20=20=20=20<a=20href=3D"/">normal=20link=20to=20another=20page=
</a>
=20=20=20=20
</body></html>
-----=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67--

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A link to use hints on</title>
</head>
<body>
<a href="/data/hello.txt">Follow me!</a>
</body>
</html>

View File

@ -4,6 +4,7 @@ Feature: Going back and forward.
Scenario: Going back/forward
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I run :tab-only
And I run :back
And I wait until data/backforward/1.txt is loaded
And I reload
@ -15,13 +16,81 @@ Feature: Going back and forward.
data/backforward/2.txt
data/backforward/1.txt
data/backforward/2.txt
And the session should look like:
windows:
- tabs:
- history:
- url: http://localhost:*/data/backforward/1.txt
- active: true
url: http://localhost:*/data/backforward/2.txt
Scenario: Going back without history
Given I open data/backforward/1.txt
When I run :back
Then the error "At beginning of history." should be shown.
Scenario: Going back in a new tab
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I run :tab-only
And I run :back -t
And I wait until data/backforward/1.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- url: http://localhost:*/data/backforward/1.txt
- active: true
url: http://localhost:*/data/backforward/2.txt
- active: true
history:
- active: true
url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt
Scenario: Going forward without history
Given I open data/backforward/1.txt
When I run :forward
Then the error "At end of history." should be shown.
Scenario: Going back in a new background tab
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I run :tab-only
And I run :back -b
And I wait until data/backforward/1.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- url: http://localhost:*/data/backforward/1.txt
- active: true
url: http://localhost:*/data/backforward/2.txt
- history:
- active: true
url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt
Scenario: Going back in a new window
Given I have a fresh instance
When I open data/backforward/1.txt
And I open data/backforward/2.txt
And I run :back -w
And I wait until data/backforward/1.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- url: about:blank
- url: http://localhost:*/data/backforward/1.txt
- active: true
url: http://localhost:*/data/backforward/2.txt
- tabs:
- active: true
history:
- url: about:blank
- active: true
url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt
Scenario: Going back without history
Given I open data/backforward/1.txt
When I run :back
Then the error "At beginning of history." should be shown.
Scenario: Going forward without history
Given I open data/backforward/1.txt
When I run :forward
Then the error "At end of history." should be shown.

View File

@ -53,6 +53,13 @@ def run_command_given(quteproc, command):
quteproc.send_cmd(command)
@bdd.given("I have a fresh instance")
def fresh_instance(quteproc):
"""Restart qutebrowser instance for tests needing a fresh state."""
quteproc.terminate()
quteproc.start()
@bdd.when(bdd.parsers.parse("I run {command}"))
def run_command_when(quteproc, httpbin, command):
command = command.replace('(port)', str(httpbin.port))

View File

@ -0,0 +1,20 @@
Feature: Using hints
Scenario: Following a hint.
When I open data/hints/link.html
And I run :hint links normal
And I run :follow-hint a
And I wait until data/hello.txt is loaded
Then the requests should be:
data/hints/link.html
data/hello.txt
Scenario: Using :follow-hint outside of hint mode (issue 1105)
When I run :follow-hint
Then the error "follow-hint: This command is only allowed in hint mode." should be shown.
Scenario: Using :follow-hint with an invalid index.
When I open data/hints/link.html
And I run :hint links normal
And I run :follow-hint xyz
Then the error "No hint xyz!" should be shown.

View File

@ -0,0 +1,44 @@
Feature: Various utility commands.
Scenario: :set-cmd-text and :command-accept
When I run :set-cmd-text :message-info "Hello World"
And I run :command-accept
Then the message "Hello World" should be shown.
Scenario: :set-cmd-text with two commands
When I run :set-cmd-text :message-info test ;; message-error error
And I run :command-accept
Then the message "test" should be shown.
And the error "error" should be shown.
Scenario: :set-cmd-text with URL replacement
When I open data/hello.txt
When I run :set-cmd-text :message-info >{url}<
And I run :command-accept
Then the message ">http://localhost:*/hello.txt<" should be shown.
Scenario: :set-cmd-text with -s and -a
When I run :set-cmd-text -s :message-info "foo
And I run :set-cmd-text -a bar"
And I run :command-accept
Then the message "foo bar" should be shown.
Scenario: :set-cmd-text with -a but without text
When I run :set-cmd-text -a foo
Then the error "No current text!" should be shown.
Scenario: :set-cmd-text with invalid command
When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown.
Scenario: :message-error
When I run :message-error "Hello World"
Then the error "Hello World" should be shown.
Scenario: :message-info
When I run :message-info "Hello World"
Then the message "Hello World" should be shown.
Scenario: :message-warning
When I run :message-warning "Hello World"
Then the warning "Hello World" should be shown.

View File

@ -0,0 +1,101 @@
Feature: Setting settings.
Background:
Given I set ui -> message-timeout to 100
Scenario: Using :set
When I run :set colors statusbar.bg magenta
Then colors -> statusbar.bg should be magenta
Scenario: Only a section
When I run :set colors
Then the error "set: Either both section and option have to be given, or neither!" should be shown.
Scenario: Without value
When I run :set colors statusbar.bg
Then the error "set: The following arguments are required: value" should be shown.
Scenario: Invalid section
When I run :set blah blub foo
Then the error "set: Section 'blah' does not exist!" should be shown.
Scenario: Invalid option
When I run :set general blub foo
Then the error "set: No option 'blub' in section 'general'" should be shown.
Scenario: Toggling an option
When I run :set general auto-save-config false
And I run :set general auto-save-config!
Then general -> auto-save-config should be True
Scenario: Toggling a non-bool option
When I run :set colors statusbar.bg!
Then the error "set: Attempted inversion of non-boolean value." should be shown.
Scenario: Getting an option
When I run :set colors statusbar.bg magenta
And I run :set colors statusbar.bg?
Then the message "colors statusbar.bg = magenta" should be shown.
Scenario: Using -p
When I run :set -p colors statusbar.bg red
Then the message "colors statusbar.bg = red" should be shown.
Scenario: Using ! and -p
When I run :set general auto-save-config false
And I run :set -p general auto-save-config!
Then the message "general auto-save-config = True" should be shown.
Scenario: Setting an invalid value
When I run :set general auto-save-config blah
Then the error "set: Invalid value 'blah' - must be a boolean!" should be shown.
Scenario: Setting a temporary option
# We don't actually check if the option is temporary as this isn't easy
# to check.
When I run :set -t colors statusbar.bg green
Then colors -> statusbar.bg should be green
Scenario: Opening qute:settings
When I run :set
And I wait for "load status for <qutebrowser.browser.webview.WebView tab_id=0 url='qute:settings'>: LoadStatus.success" in the log
Then the session should look like:
windows:
- tabs:
- active: true
history:
- url: about:blank
- active: true
url: qute:settings
Scenario: Empty option with ? (issue 1109)
When I run :set general ?
Then the error "set: The following arguments are required: value" should be shown.
Scenario: Invalid section and empty option with ? (issue 1109)
When I run :set blah ?
Then the error "set: The following arguments are required: value" should be shown.
Scenario: Invalid option with ? (issue 1109)
When I run :set general foo?
Then the error "set: No option 'foo' in section 'general'" should be shown.
Scenario: Invalid section/option with ? (issue 1109)
When I run :set blah foo ?
Then the error "set: Section 'blah' does not exist!" should be shown.
Scenario: Empty option with !
When I run :set general !
Then the error "set: The following arguments are required: value" should be shown.
Scenario: Invalid section and empty option with !
When I run :set blah !
Then the error "set: The following arguments are required: value" should be shown.
Scenario: Invalid option with !
When I run :set general foo!
Then the error "set: No option 'foo' in section 'general'" should be shown.
Scenario: Invalid section/option with !
When I run :set blah foo !
Then the error "set: Section 'blah' does not exist!" should be shown.

View File

@ -0,0 +1,21 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest_bdd as bdd
bdd.scenarios('hints.feature')

View File

@ -0,0 +1,21 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest_bdd as bdd
bdd.scenarios('misc.feature')

View File

@ -0,0 +1,32 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import logging
import pytest_bdd as bdd
bdd.scenarios('set.feature')
@bdd.then(bdd.parsers.parse("{section} -> {option} should be {value}"))
def check_option(quteproc, section, option, value):
quteproc.send_cmd(':set {} {}?'.format(section, option))
msg = quteproc.wait_for(loglevel=logging.INFO, category='message',
message='{} {} = *'.format(section, option))
actual_value = msg.message.split(' = ')[1]
assert actual_value == value

View File

@ -68,7 +68,9 @@ class LogLine(testprocess.Line):
(?P<timestamp>\d\d:\d\d:\d\d)
\ (?P<loglevel>VDEBUG|DEBUG|INFO|WARNING|ERROR)
\ +(?P<category>\w+)
\ +(?P<module>(\w+|Unknown\ module)):(?P<function>\w+):(?P<line>\d+)
\ +(?P<module>(\w+|Unknown\ module)):
(?P<function>[^"][^:]*|"[^"]+"):
(?P<line>\d+)
\ (?P<message>.+)
""", re.VERBOSE)
@ -94,9 +96,22 @@ class LogLine(testprocess.Line):
else:
self.module = module
self.function = match.group('function')
self.line = int(match.group('line'))
self.message = match.group('message')
function = match.group('function')
if function == 'none':
self.function = None
else:
self.function = function.strip('"')
line = int(match.group('line'))
if self.function is None and line == 0:
self.line = None
else:
self.line = line
msg_match = re.match(r'^(\[(?P<prefix>\d+s ago)\] )?(?P<message>.*)',
match.group('message'))
self.prefix = msg_match.group('prefix')
self.message = msg_match.group('message')
self.expected = is_ignored_qt_message(self.message)
@ -177,7 +192,7 @@ class QuteProc(testprocess.Process):
ipc.send_to_running_instance(self._ipc_socket, [command],
target_arg='')
self.wait_for(category='commands', module='command', function='run',
message='Calling *')
message='command called: *')
def set_setting(self, sect, opt, value):
self.send_cmd(':set "{}" "{}" "{}"'.format(sect, opt, value))
@ -197,7 +212,7 @@ class QuteProc(testprocess.Process):
message=message)
line.expected = True
def wait_for(self, timeout=15000, **kwargs):
def wait_for(self, timeout=None, **kwargs):
"""Override testprocess.wait_for to check past messages.
self._data is cleared after every test to provide at least some

View File

@ -0,0 +1,103 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Test mhtml downloads based on sample files."""
import os
import re
import os.path
import pytest
def collect_tests():
basedir = os.path.dirname(__file__)
datadir = os.path.join(basedir, 'data', 'downloads', 'mhtml')
files = os.listdir(datadir)
return files
def normalize_line(line):
line = line.rstrip('\n')
line = re.sub('boundary="---=_qute-[0-9a-f-]+"',
'boundary="---=_qute-UUID"', line)
line = re.sub('^-----=_qute-[0-9a-f-]+$', '-----=_qute-UUID', line)
line = re.sub(r'localhost:\d{1,5}', 'localhost:(port)', line)
return line
class DownloadDir:
"""Abstraction over a download directory."""
def __init__(self, tmpdir):
self._tmpdir = tmpdir
self.location = str(tmpdir)
def read_file(self):
files = self._tmpdir.listdir()
assert len(files) == 1
with open(str(files[0]), 'r', encoding='utf-8') as f:
return f.readlines()
def compare_mhtml(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
expected_data = [normalize_line(line) for line in f]
actual_data = self.read_file()
actual_data = [normalize_line(line) for line in actual_data]
assert actual_data == expected_data
@pytest.fixture
def download_dir(tmpdir):
return DownloadDir(tmpdir)
@pytest.mark.parametrize('test_name', collect_tests())
def test_mhtml(test_name, download_dir, quteproc, httpbin):
quteproc.set_setting('storage', 'download-directory',
download_dir.location)
quteproc.set_setting('storage', 'prompt-download-directory', 'false')
test_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'data', 'downloads', 'mhtml', test_name)
test_path = 'data/downloads/mhtml/{}'.format(test_name)
quteproc.open_path('{}/{}.html'.format(test_path, test_name))
download_dest = os.path.join(download_dir.location,
'{}-downloaded.mht'.format(test_name))
quteproc.send_cmd(':download --mhtml --dest "{}"'.format(download_dest))
quteproc.wait_for(category='downloads', module='mhtml',
function='finish_file',
message='All assets downloaded, ready to finish off!')
expected_file = os.path.join(test_dir, '{}.mht'.format(test_name))
download_dir.compare_mhtml(expected_file)
with open(os.path.join(test_dir, 'requests'), encoding='utf-8') as f:
expected_requests = []
for line in f:
if line.startswith('#'):
continue
path = '/{}/{}'.format(test_path, line.strip())
expected_requests.append(httpbin.Request('GET', path))
actual_requests = httpbin.get_requests()
assert sorted(actual_requests) == sorted(expected_requests)

View File

@ -19,8 +19,14 @@
"""Test the quteproc fixture used for tests."""
import logging
import datetime
import pytest
import quteprocess
from qutebrowser.utils import log
def test_quteproc_error_message(qtbot, quteproc):
"""Make sure the test fails with an unexpected error message."""
@ -36,3 +42,76 @@ 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):
quteproc.send_cmd(':message-error "SpellCheck: test"')
@pytest.mark.parametrize('data, attrs', [
(
# Normal message
'01:02:03 DEBUG init earlyinit:init_log:280 Log initialized.',
{
'timestamp': datetime.datetime(year=1900, month=1, day=1,
hour=1, minute=2, second=3),
'loglevel': logging.DEBUG,
'category': 'init',
'module': 'earlyinit',
'function': 'init_log',
'line': 280,
'message': 'Log initialized.',
'expected': False,
}
),
(
# VDEBUG
'00:00:00 VDEBUG foo foo:foo:0 test',
{'loglevel': log.VDEBUG_LEVEL}
),
(
# Unknown module
'00:00:00 WARNING qt Unknown module:none:0 test',
{'module': None, 'function': None, 'line': None},
),
(
# Expected message
'00:00:00 VDEBUG foo foo:foo:0 SpellCheck: test',
{'expected': True},
),
(
# Weird Qt location
'00:00:00 DEBUG qt qnetworkreplyhttpimpl:"void '
'QNetworkReplyHttpImplPrivate::error(QNetworkReply::NetworkError, '
'const QString&)":1929 QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called once.',
{
'module': 'qnetworkreplyhttpimpl',
'function': 'void QNetworkReplyHttpImplPrivate::error('
'QNetworkReply::NetworkError, const QString&)',
'line': 1929
}
),
(
'00:00:00 WARNING qt qxcbxsettings:"QXcbXSettings::'
'QXcbXSettings(QXcbScreen*)":233 '
'QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection '
'owner for XSETTINGS_S atom ',
{
'module': 'qxcbxsettings',
'function': 'QXcbXSettings::QXcbXSettings(QXcbScreen*)',
'line': 233,
}
),
(
# With [2s ago] marker
'00:00:00 DEBUG foo foo:foo:0 [2s ago] test',
{'prefix': '2s ago', 'message': 'test'}
),
])
def test_log_line_parse(data, attrs):
line = quteprocess.LogLine(data)
for name, expected in attrs.items():
actual = getattr(line, name)
assert actual == expected, name
def test_log_line_no_match():
with pytest.raises(quteprocess.NoLineMatch):
quteprocess.LogLine("Hello World!")

View File

@ -20,6 +20,7 @@
"""Base class for a subprocess run for tests.."""
import re
import os
import time
import fnmatch
@ -136,7 +137,9 @@ class Process(QObject):
print("INVALID: {}".format(line))
continue
if parsed is not None:
if parsed is None:
print("IGNORED: {}".format(line))
else:
self._data.append(parsed)
self.new_data.emit(parsed)
@ -213,7 +216,7 @@ class Process(QObject):
else:
return value == expected
def wait_for(self, timeout=15000, **kwargs):
def wait_for(self, timeout=None, **kwargs):
"""Wait until a given value is found in the data.
Keyword arguments to this function get interpreted as attributes of the
@ -223,6 +226,11 @@ class Process(QObject):
Return:
The matched line.
"""
if timeout is None:
if 'CI' in os.environ:
timeout = 15000
else:
timeout = 5000
# Search existing messages
for line in self._data:
matches = []

View File

@ -35,6 +35,55 @@ def preload_cache(cache, url='http://www.example.com/', content=b'foobar'):
cache.insert(device)
def test_cache_config_change_cache_size(config_stub, tmpdir):
"""Change cache size and emit signal to trigger on_config_changed."""
max_cache_size = 1024
config_stub.data = {
'storage': {'cache-size': max_cache_size},
'general': {'private-browsing': False}
}
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.maximumCacheSize() == max_cache_size
config_stub.set('storage', 'cache-size', max_cache_size * 2)
assert disk_cache.maximumCacheSize() == max_cache_size * 2
def test_cache_config_enable_private_browsing(config_stub, tmpdir):
"""Change private-browsing config to True and emit signal."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.cacheSize() == 0
preload_cache(disk_cache)
assert disk_cache.cacheSize() > 0
config_stub.set('general', 'private-browsing', True)
assert disk_cache.cacheSize() == 0
def test_cache_config_disable_private_browsing(config_stub, tmpdir):
"""Change private-browsing config to False and emit signal."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
url = 'http://qutebrowser.org'
metadata = QNetworkCacheMetaData()
metadata.setUrl(QUrl(url))
assert metadata.isValid()
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.prepare(metadata) is None
config_stub.set('general', 'private-browsing', False)
content = b'cute'
preload_cache(disk_cache, url, content)
assert disk_cache.data(QUrl(url)).readAll() == content
def test_cache_size_leq_max_cache_size(config_stub, tmpdir):
"""Test cacheSize <= MaximumCacheSize when cache is activated."""
limit = 100
@ -54,6 +103,63 @@ def test_cache_size_leq_max_cache_size(config_stub, tmpdir):
assert disk_cache.cacheSize() < limit+100
def test_cache_size_deactivated(config_stub, tmpdir):
"""Confirm that the cache size returns 0 when deactivated."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.cacheSize() == 0
def test_cache_existing_metadata_file(config_stub, tmpdir):
"""Test querying existing meta data file from activated cache."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
url = 'http://qutebrowser.org'
content = b'foobar'
metadata = QNetworkCacheMetaData()
metadata.setUrl(QUrl(url))
assert metadata.isValid()
disk_cache = cache.DiskCache(str(tmpdir))
device = disk_cache.prepare(metadata)
assert device is not None
device.write(content)
disk_cache.insert(device)
disk_cache.updateMetaData(metadata)
files = list(tmpdir.visit(fil=lambda path: path.isfile()))
assert len(files) == 1
assert disk_cache.fileMetaData(str(files[0])) == metadata
def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
"""Test querying nonexistent meta data file from activated cache."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
disk_cache = cache.DiskCache(str(tmpdir))
cache_file = disk_cache.fileMetaData("nosuchfile")
assert cache_file.isValid() == False
def test_cache_deactivated_metadata_file(config_stub, tmpdir):
"""Test querying meta data file when cache is deactivated."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.fileMetaData("foo") == QNetworkCacheMetaData()
def test_cache_deactivated_private_browsing(config_stub, tmpdir):
"""Test if cache is deactivated in private-browsing mode."""
config_stub.data = {
@ -104,12 +210,15 @@ def test_cache_deactivated_remove_data(config_stub, tmpdir):
assert disk_cache.remove(url) == False
def test_cache_insert_data(tmpdir):
def test_cache_insert_data(config_stub, tmpdir):
"""Test if entries inserted into the cache are actually there."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
url = 'http://qutebrowser.org'
content = b'foobar'
disk_cache = QNetworkDiskCache()
disk_cache.setCacheDirectory(str(tmpdir))
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.cacheSize() == 0
preload_cache(disk_cache, url, content)
@ -118,11 +227,37 @@ def test_cache_insert_data(tmpdir):
assert disk_cache.data(QUrl(url)).readAll() == content
def test_cache_remove_data(tmpdir):
"""Test if a previously inserted entry can be removed from the cache."""
def test_cache_deactivated_insert_data(config_stub, tmpdir):
"""Insert data when cache is deactivated."""
# First create QNetworkDiskCache just to get a valid QIODevice from it
url = 'http://qutebrowser.org'
disk_cache = QNetworkDiskCache()
disk_cache.setCacheDirectory(str(tmpdir))
metadata = QNetworkCacheMetaData()
metadata.setUrl(QUrl(url))
device = disk_cache.prepare(metadata)
assert device is not None
# Now create a deactivated DiskCache and insert the valid device created
# above (there probably is a better way to get a valid QIODevice...)
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
deactivated_cache = cache.DiskCache(str(tmpdir))
assert deactivated_cache.insert(device) is None
def test_cache_remove_data(config_stub, tmpdir):
"""Test if a previously inserted entry can be removed from the cache."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
url = 'http://qutebrowser.org'
disk_cache = cache.DiskCache(str(tmpdir))
preload_cache(disk_cache, url)
assert disk_cache.cacheSize() > 0
@ -146,14 +281,27 @@ def test_cache_clear_activated(config_stub, tmpdir):
assert disk_cache.cacheSize() == 0
def test_cache_metadata(tmpdir):
def test_cache_clear_deactivated(config_stub, tmpdir):
"""Test method clear() on deactivated cache."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.clear() is None
def test_cache_metadata(config_stub, tmpdir):
"""Ensure that DiskCache.metaData() returns exactly what was inserted."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
url = 'http://qutebrowser.org'
metadata = QNetworkCacheMetaData()
metadata.setUrl(QUrl(url))
assert metadata.isValid()
disk_cache = QNetworkDiskCache()
disk_cache.setCacheDirectory(str(tmpdir))
disk_cache = cache.DiskCache(str(tmpdir))
device = disk_cache.prepare(metadata)
device.write(b'foobar')
disk_cache.insert(device)
@ -161,11 +309,26 @@ def test_cache_metadata(tmpdir):
assert disk_cache.metaData(QUrl(url)) == metadata
def test_cache_update_metadata(tmpdir):
"""Test updating the meta data for an existing cache entry."""
def test_cache_deactivated_metadata(config_stub, tmpdir):
"""Test querying metaData() on not activated cache."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
url = 'http://qutebrowser.org'
disk_cache = QNetworkDiskCache()
disk_cache.setCacheDirectory(str(tmpdir))
disk_cache = cache.DiskCache(str(tmpdir))
assert disk_cache.metaData(QUrl(url)) == QNetworkCacheMetaData()
def test_cache_update_metadata(config_stub, tmpdir):
"""Test updating the meta data for an existing cache entry."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': False}
}
url = 'http://qutebrowser.org'
disk_cache = cache.DiskCache(str(tmpdir))
preload_cache(disk_cache, url, b'foo')
assert disk_cache.cacheSize() > 0
@ -176,6 +339,21 @@ def test_cache_update_metadata(tmpdir):
assert disk_cache.metaData(QUrl(url)) == metadata
def test_cache_deactivated_update_metadata(config_stub, tmpdir):
"""Test updating the meta data when cache is not activated."""
config_stub.data = {
'storage': {'cache-size': 1024},
'general': {'private-browsing': True}
}
url = 'http://qutebrowser.org'
disk_cache = cache.DiskCache(str(tmpdir))
metadata = QNetworkCacheMetaData()
metadata.setUrl(QUrl(url))
assert metadata.isValid()
assert disk_cache.updateMetaData(metadata) is None
def test_cache_full(config_stub, tmpdir):
"""Do a sanity test involving everything."""
config_stub.data = {

View File

@ -520,11 +520,13 @@ def test_unset_organization(qapp, orgname, expected):
assert qapp.organizationName() == expected
if test_file is not None:
if test_file is not None and sys.platform != 'darwin':
# If we were able to import Python's test_file module, we run some code
# here which defines unittest TestCases to run the python tests over
# PyQIODevice.
# Those are not run on OS X because that seems to cause a hang sometimes.
@pytest.yield_fixture(scope='session', autouse=True)
def clean_up_python_testfile():
"""Clean up the python testfile after tests if tests didn't."""

View File

@ -39,7 +39,7 @@ deps =
six==1.10.0
termcolor==1.1.0
vulture==0.8.1
Werkzeug==0.11
Werkzeug==0.11.1
wheel==0.26.0
xvfbwrapper==0.2.5
commands =