Merge branch 'master' into download-page

This commit is contained in:
Daniel Schadt 2015-11-18 19:27:26 +01:00
commit 3438a45b19
80 changed files with 2111 additions and 487 deletions

View File

@ -1,3 +1,4 @@
shallow_clone: true
version: '{branch}-{build}' version: '{branch}-{build}'
cache: cache:
- C:\projects\qutebrowser\.cache - C:\projects\qutebrowser\.cache
@ -13,7 +14,7 @@ install:
- C:\Python27\python -u scripts\dev\ci_install.py - C:\Python27\python -u scripts\dev\ci_install.py
test_script: test_script:
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" --junitxml=junit.xml - C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml
after_test: after_test:
- ps: | - ps: |
@ -22,6 +23,3 @@ after_test:
$file = '.\junit.xml' $file = '.\junit.xml'
(New-Object 'System.Net.WebClient').UploadFile($url, (Resolve-Path $file)) (New-Object 'System.Net.WebClient').UploadFile($url, (Resolve-Path $file))
} }
if ($env:TESTENV -eq 'py34') {
C:\Python34\Scripts\codecov -e TESTENV -X gcov
}

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ __pycache__
/.coverage /.coverage
/htmlcov /htmlcov
/.coverage.xml /.coverage.xml
/.coverage.*
/.tox /.tox
/testresults.html /testresults.html
/.cache /.cache

View File

@ -37,10 +37,10 @@ install:
- python scripts/dev/ci_install.py - python scripts/dev/ci_install.py
script: script:
- tox -e $TESTENV -- -p no:sugar --faulthandler-timeout=70 -v --cov-report term tests - tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
after_success: after_success:
- '[[ $TESTENV == py34 || $TESTENV == py35 ]] && codecov -e TESTENV -X gcov' - '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
matrix: matrix:
exclude: exclude:

View File

@ -68,6 +68,7 @@ Changed
finished. When set to `-1`, downloads are never removed. finished. When set to `-1`, downloads are never removed.
- The `:follow-hint` command now optionally takes the keystring of a hint to - The `:follow-hint` command now optionally takes the keystring of a hint to
follow. follow.
- `:scroll-px` now doesn't take floats anymore, which made little sense.
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
@ -86,6 +87,7 @@ Fixed
`storage -> prompt-download-directory` was unset. `storage -> prompt-download-directory` was unset.
- Fixed crash when using `:follow-hint` outside of hint mode. - Fixed crash when using `:follow-hint` outside of hint mode.
- Fixed crash when using `:set foo bar?` with invalid section/option. - Fixed crash when using `:set foo bar?` with invalid section/option.
- Fixed scrolling to the very left/right with `:scroll-perc`.
v0.4.1 v0.4.1
------ ------

View File

@ -548,6 +548,13 @@ workaround.
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
bugs] and check if they're fixed. bugs] and check if they're fixed.
New PyQt release
~~~~~~~~~~~~~~~~
* See above
* Install new PyQt in Windows VM (32- and 64-bit)
* Download new installer and update PyQt installer path in `ci_install.py`.
qutebrowser release qutebrowser release
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~

View File

@ -1,5 +1,6 @@
Frequently asked questions Frequently asked questions
========================== ==========================
:title: Frequently asked questions
The Compiler <mail@qutebrowser.org> The Compiler <mail@qutebrowser.org>
[qanda] [qanda]
@ -87,6 +88,16 @@ Note that you might need an additional package (e.g.
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
Archlinux) to play web videos with mpv. Archlinux) to play web videos with mpv.
How do I use qutebrowser with mutt?::
Due to a Qt limitation, local files without `.html` extensions are
"downloaded" instead of displayed, see
https://github.com/The-Compiler/qutebrowser/issues/566[#566]. You can work
around this by using this in your `mailcap`:
+
----
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
----
== Troubleshooting == Troubleshooting
Configuration not saved after modifying config.:: Configuration not saved after modifying config.::

View File

@ -17,6 +17,7 @@ include requirements.txt
include tox.ini include tox.ini
include qutebrowser.py include qutebrowser.py
prune www
prune scripts/dev prune scripts/dev
exclude scripts/asciidoc2html.py exclude scripts/asciidoc2html.py
exclude doc/notes exclude doc/notes

View File

@ -6,6 +6,7 @@
qutebrowser qutebrowser
=========== ===========
// QUTE_WEB_HIDE
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.* image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"] image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
@ -14,6 +15,7 @@ image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=fl
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"] image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"] image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"] image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
// QUTE_WEB_HIDE_END
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL. on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
@ -24,7 +26,7 @@ Screenshots
----------- -----------
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"] image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
image:doc/img/downloads.png["screenshot 2",width=300j,link="doc/img/downloads.png"] image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"] image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"] image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
@ -89,11 +91,11 @@ Requirements
The following software and libraries are required to run qutebrowser: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 * http://www.python.org/[Python] 3.4 or newer
* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended) * http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended)
* QtWebKit * QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.5.0 recommended) for Python 3 (5.5.1 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2] * http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
@ -139,32 +141,33 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START // QUTE_AUTHORS_START
* Florian Bruhin * Florian Bruhin
* Antoni Boucher * Antoni Boucher
* Bruno Oliveira
* Lamar Pavel * Lamar Pavel
* Bruno Oliveira
* Alexander Cogneau * Alexander Cogneau
* Martin Tournoij * Martin Tournoij
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Daniel * Daniel
* Claude * Claude
* meles5
* Nathan Isom * Nathan Isom
* Austin Anderson * Austin Anderson
* Artur Shaik * Artur Shaik
* Thorsten Wißmann * Thorsten Wißmann
* Alexey "Averrin" Nabrodov * Alexey "Averrin" Nabrodov
* meles5
* ZDarian * ZDarian
* John ShaggyTwoDope Jenkins * John ShaggyTwoDope Jenkins
* Peter Vilim * Peter Vilim
* Jonas Schürmann * Jonas Schürmann
* Jimmy * Jimmy
* skinnay * skinnay
* error800
* Zach-Button * Zach-Button
* Halfwit
* Felix Van der Jeugt * Felix Van der Jeugt
* rikn00 * rikn00
* Patric Schmitz * Patric Schmitz
* Martin Zimmermann * Martin Zimmermann
* Error 800
* Brian Jackson * Brian Jackson
* sbinix * sbinix
* neeasade * neeasade
@ -180,7 +183,6 @@ Contributors, sorted by the number of commits in descending order:
* Fritz V155 Reichwald * Fritz V155 Reichwald
* Franz Fellner * Franz Fellner
* zwarag * zwarag
* error800
* Tim Harder * Tim Harder
* Thiago Barroso Perrotta * Thiago Barroso Perrotta
* Matthias Lisin * Matthias Lisin

60
misc/userscripts/qutedmenu Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Handle open -s && open -t with bemenu
#:bind o spawn --userscript /path/to/userscripts/qutedmenu open
#:bind O spawn --userscript /path/to/userscripts/qutedmenu tab
# If you would like to set a custom colorscheme/font use these dirs.
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
readonly datadir=${XDG_DATA_HOME:-$HOME/.local/share}
readonly optsfile=$confdir/dmenu/bemenucolors
create_menu() {
# Check quickmarks
while read -r url; do
printf -- '%s\n' "$url"
done < "$confdir"/qutebrowser/quickmarks
# Next bookmarks
while read -r url _; do
printf -- '%s\n' "$url"
done < "$confdir"/qutebrowser/bookmarks/urls
# Finally history
while read -r _ url; do
printf -- '%s\n' "$url"
done < "$datadir"/qutebrowser/history
}
get_selection() {
opts+=(-p qutebrowser)
#create_menu | dmenu -l 10 "${opts[@]}"
create_menu | bemenu -l 10 "${opts[@]}"
}
# Main
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
if [[ -s $confdir/dmenu/font ]]; then
read -r font < "$confdir"/dmenu/font
fi
if [[ $font ]]; then
opts+=(-fn "$font")
fi
if [[ -s $optsfile ]]; then
source "$optsfile"
fi
url=$(get_selection)
url=${url/*http/http}
# If no selection is made, exit (escape pressed, e.g.)
[[ ! $url ]] && exit 0
case $1 in
open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
tab) printf '%s' "open -t $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
esac

View File

@ -36,3 +36,4 @@ qt_log_ignore =
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\. ^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
^QWaitCondition: Destroyed while threads are still waiting ^QWaitCondition: Destroyed while threads are still waiting
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom ^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)

View File

@ -814,12 +814,9 @@ class EventFilter(QObject):
Return: Return:
True if the event should be filtered, False if it's passed through. True if the event should be filtered, False if it's passed through.
""" """
if qApp.overrideCursor() is None: # Mouse cursor shown (overrideCursor None) -> don't filter event
# Mouse cursor shown -> don't filter event # Mouse cursor hidden (overrideCursor not None) -> filter event
return False return qApp.overrideCursor() is not None
else:
# Mouse cursor hidden -> filter event
return True
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
"""Handle an event. """Handle an event.

View File

@ -152,30 +152,6 @@ class CommandDispatcher:
else: else:
return None return None
def _scroll_percent(self, perc=None, count=None, orientation=None):
"""Inner logic for scroll_percent_(x|y).
Args:
perc: How many percent to scroll, or None
count: How many percent to scroll, or None
orientation: Qt.Horizontal or Qt.Vertical
"""
if perc is None and count is None:
perc = 100
elif perc is None:
perc = count
if perc == 0:
self.scroll('top')
elif perc == 100:
self.scroll('bottom')
else:
perc = qtutils.check_overflow(perc, 'int', fatal=False)
frame = self._current_widget().page().currentFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
return
frame.setScrollBarValue(orientation, int(m * perc / 100))
def _tab_move_absolute(self, idx): def _tab_move_absolute(self, idx):
"""Get an index for moving a tab absolutely. """Get an index for moving a tab absolutely.
@ -412,20 +388,27 @@ class CommandDispatcher:
def _back_forward(self, tab, bg, window, count, forward): def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward.""" """Helper function for :back/:forward."""
if (not forward and not # Catch common cases before e.g. cloning tab
self._current_widget().page().history().canGoBack()): history = self._current_widget().page().history()
if not forward and not history.canGoBack():
raise cmdexc.CommandError("At beginning of history.") raise cmdexc.CommandError("At beginning of history.")
if (forward and not elif forward and not history.canGoForward():
self._current_widget().page().history().canGoForward()):
raise cmdexc.CommandError("At end of history.") raise cmdexc.CommandError("At end of history.")
if tab or bg or window: if tab or bg or window:
widget = self.tab_clone(bg, window) widget = self.tab_clone(bg, window)
else: else:
widget = self._current_widget() widget = self._current_widget()
history = widget.page().history()
for _ in range(count): for _ in range(count):
if forward: if forward:
if not history.canGoForward():
raise cmdexc.CommandError("At end of history.")
widget.forward() widget.forward()
else: else:
if not history.canGoBack():
raise cmdexc.CommandError("At beginning of history.")
widget.back() widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
@ -538,7 +521,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count') scope='window', count='count')
def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1): def scroll_px(self, dx: {'type': int}, dy: {'type': int}, count=1):
"""Scroll the current tab by 'count * dx/dy' pixels. """Scroll the current tab by 'count * dx/dy' pixels.
Args: Args:
@ -555,8 +538,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count') scope='window', count='count')
def scroll(self, def scroll(self,
direction: {'type': (str, float)}, direction: {'type': (str, int)},
dy: {'type': float, 'hide': True}=None, dy: {'type': int, 'hide': True}=None,
count=1): count=1):
"""Scroll the current tab in the given direction. """Scroll the current tab in the given direction.
@ -569,8 +552,8 @@ class CommandDispatcher:
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
try: try:
# Check for deprecated dx/dy form (like with scroll-px). # Check for deprecated dx/dy form (like with scroll-px).
dx = float(direction) dx = int(direction)
dy = float(dy) dy = int(dy)
except (ValueError, TypeError): except (ValueError, TypeError):
# Invalid values will get handled later. # Invalid values will get handled later.
pass pass
@ -643,8 +626,24 @@ class CommandDispatcher:
horizontal: Scroll horizontally instead of vertically. horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll. count: Percentage to scroll.
""" """
self._scroll_percent(perc, count, if perc is None and count is None:
Qt.Horizontal if horizontal else Qt.Vertical) perc = 100
elif perc is None:
perc = count
orientation = Qt.Horizontal if horizontal else Qt.Vertical
if perc == 0 and orientation == Qt.Vertical:
self.scroll('top')
elif perc == 100 and orientation == Qt.Vertical:
self.scroll('bottom')
else:
perc = qtutils.check_overflow(perc, 'int', fatal=False)
frame = self._current_widget().page().currentFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
return
frame.setScrollBarValue(orientation, int(m * perc / 100))
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count') scope='window', count='count')
@ -686,7 +685,7 @@ class CommandDispatcher:
pass pass
elif mult_y < 0: elif mult_y < 0:
self.scroll('page-up', count=-int(mult_y)) self.scroll('page-up', count=-int(mult_y))
elif mult_y > 0: elif mult_y > 0: # pragma: no branch
self.scroll('page-down', count=int(mult_y)) self.scroll('page-down', count=int(mult_y))
mult_y = 0 mult_y = 0
if mult_x == 0 and mult_y == 0: if mult_x == 0 and mult_y == 0:

View File

@ -29,7 +29,8 @@ from qutebrowser.utils import message, log, objreg, qtutils
from qutebrowser.misc import split from qutebrowser.misc import split
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline') ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
'count'])
def replace_variables(win_id, arglist): def replace_variables(win_id, arglist):
@ -117,6 +118,26 @@ class CommandRunner(QObject):
for sub in sub_texts: for sub in sub_texts:
yield self.parse(sub, *args, **kwargs) yield self.parse(sub, *args, **kwargs)
def _parse_count(self, cmdstr):
"""Split a count prefix off from a command for parse().
Args:
cmdstr: The command/args including the count.
Return:
A (count, cmdstr) tuple, with count being None or int.
"""
if ':' not in cmdstr:
return (None, cmdstr)
count, cmdstr = cmdstr.split(':', maxsplit=1)
try:
count = int(count)
except ValueError:
# We just ignore invalid prefixes
count = None
return (count, cmdstr)
def parse(self, text, *, aliases=True, fallback=False, keep=False): def parse(self, text, *, aliases=True, fallback=False, keep=False):
"""Split the commandline text into command and arguments. """Split the commandline text into command and arguments.
@ -128,9 +149,11 @@ class CommandRunner(QObject):
keep: Whether to keep special chars and whitespace keep: Whether to keep special chars and whitespace
Return: Return:
A (cmd, args, cmdline) ParseResult tuple. A ParseResult tuple.
""" """
cmdstr, sep, argstr = text.partition(' ') cmdstr, sep, argstr = text.partition(' ')
count, cmdstr = self._parse_count(cmdstr)
if not cmdstr and not fallback: if not cmdstr and not fallback:
raise cmdexc.NoSuchCommandError("No command given") raise cmdexc.NoSuchCommandError("No command given")
if aliases: if aliases:
@ -161,7 +184,7 @@ class CommandRunner(QObject):
cmdline = [cmdstr, sep] cmdline = [cmdstr, sep]
else: else:
cmdline = [cmdstr] + args[:] cmdline = [cmdstr] + args[:]
return ParseResult(cmd=cmd, args=args, cmdline=cmdline) return ParseResult(cmd=cmd, args=args, cmdline=cmdline, count=count)
def _split_args(self, cmd, argstr, keep): def _split_args(self, cmd, argstr, keep):
"""Split the arguments from an arg string. """Split the arguments from an arg string.
@ -216,7 +239,12 @@ class CommandRunner(QObject):
for result in self.parse_all(text): for result in self.parse_all(text):
args = replace_variables(self._win_id, result.args) args = replace_variables(self._win_id, result.args)
if count is not None: if count is not None:
if result.count is not None:
raise cmdexc.CommandMetaError("Got count via command and "
"prefix!")
result.cmd.run(self._win_id, args, count=count) result.cmd.run(self._win_id, args, count=count)
elif result.count is not None:
result.cmd.run(self._win_id, args, count=result.count)
else: else:
result.cmd.run(self._win_id, args) result.cmd.run(self._win_id, args)

View File

@ -37,7 +37,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
cat = self.new_category("Sections") cat = self.new_category("Sections")
for name in configdata.DATA.keys(): for name in configdata.DATA:
desc = configdata.SECTION_DESC[name].splitlines()[0].strip() desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
self.new_item(cat, name, desc) self.new_item(cat, name, desc)
@ -62,7 +62,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
self._misc_items = {} self._misc_items = {}
self._section = section self._section = section
objreg.get('config').changed.connect(self.update_misc_column) objreg.get('config').changed.connect(self.update_misc_column)
for name in sectdata.keys(): for name in sectdata:
try: try:
desc = sectdata.descriptions[name] desc = sectdata.descriptions[name]
except (KeyError, AttributeError): except (KeyError, AttributeError):

View File

@ -70,7 +70,7 @@ def _init_setting_completions():
model = configmodel.SettingOptionCompletionModel(sectname) model = configmodel.SettingOptionCompletionModel(sectname)
_instances[usertypes.Completion.option][sectname] = model _instances[usertypes.Completion.option][sectname] = model
_instances[usertypes.Completion.value][sectname] = {} _instances[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys(): for opt in configdata.DATA[sectname]:
model = configmodel.SettingValueCompletionModel(sectname, opt) model = configmodel.SettingValueCompletionModel(sectname, opt)
_instances[usertypes.Completion.value][sectname][opt] = model _instances[usertypes.Completion.value][sectname][opt] = model

View File

@ -77,7 +77,7 @@ class HelpCompletionModel(base.BaseCompletionModel):
"""Fill completion with section->option entries.""" """Fill completion with section->option entries."""
cat = self.new_category("Settings") cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items(): for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys(): for optname in sectdata:
try: try:
desc = sectdata.descriptions[optname] desc = sectdata.descriptions[optname]
except (KeyError, AttributeError): except (KeyError, AttributeError):

View File

@ -368,7 +368,7 @@ class ConfigManager(QObject):
self.sections = configdata.data() self.sections = configdata.data()
self._interpolation = configparser.ExtendedInterpolation() self._interpolation = configparser.ExtendedInterpolation()
self._proxies = {} self._proxies = {}
for sectname in self.sections.keys(): for sectname in self.sections:
self._proxies[sectname] = SectionProxy(self, sectname) self._proxies[sectname] = SectionProxy(self, sectname)
self._fname = fname self._fname = fname
if configdir is None: if configdir is None:

View File

@ -1569,49 +1569,41 @@ class UserAgent(BaseType):
def validate(self, value): def validate(self, value):
self._basic_validation(value) self._basic_validation(value)
# To update the following list of user agents, run the script 'ua_fetch.py'
# Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py
def complete(self): def complete(self):
"""Complete a list of common user agents.""" """Complete a list of common user agents."""
out = [ out = [
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 ' ('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 '
'Firefox/35.0', 'Firefox/41.0',
"Firefox 35.0 Win7 64-bit"), "Firefox 41.0 Win7 64-bit"),
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 ' ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) '
'Firefox/35.0', 'Gecko/20100101 Firefox/41.0',
"Firefox 35.0 Ubuntu"), "Firefox 41.0 MacOSX"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) ' ('Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 '
'Gecko/20100101 Firefox/35.0', 'Firefox/41.0',
"Firefox 35.0 MacOSX"), "Firefox 41.0 Linux"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) '
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 ' 'AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 '
'Safari/600.3.18', 'Safari/601.2.7',
"Safari 8.0 MacOSX"), "Safari Generic MacOSX"),
('Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) '
'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 '
'Mobile/13B143 Safari/601.1',
"Mobile Safari Generic iOS"),
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, ' ('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/40.0.2214.111 Safari/537.36', 'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
"Chrome 40.0 Win7 64-bit"), "Chrome 46.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 ' 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 '
'Safari/537.36', 'Safari/537.36',
"Chrome 40.0 MacOSX"), "Chrome 46.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, '
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36', 'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
"Chrome 40.0 Linux"), "Chrome 46.0 Linux"),
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
'Gecko',
"IE 11.0 Win7 64-bit"),
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
'Mobile/12B440 Safari/600.1.4',
"Mobile Safari 8.0 iOS"),
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
"Firefox 35, Android"),
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
'Chrome/37.0.0.0 Mobile Safari/537.36',
"Android Browser"),
('Mozilla/5.0 (compatible; Googlebot/2.1; ' ('Mozilla/5.0 (compatible; Googlebot/2.1; '
'+http://www.google.com/bot.html', '+http://www.google.com/bot.html',
@ -1619,7 +1611,11 @@ class UserAgent(BaseType):
('Wget/1.16.1 (linux-gnu)', ('Wget/1.16.1 (linux-gnu)',
"wget 1.16.1"), "wget 1.16.1"),
('curl/7.40.0', ('curl/7.40.0',
"curl 7.40.0") "curl 7.40.0"),
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
'Gecko',
"IE 11.0 for Desktop Win7 64-bit")
] ]
return out return out

View File

@ -273,10 +273,8 @@ class KeyConfigParser(QObject):
return True return True
if keychain in bindings: if keychain in bindings:
return False return False
elif command in bindings.values():
return False
else: else:
return True return command not in bindings.values()
def _read(self, relaxed=False): def _read(self, relaxed=False):
"""Read the config file from disk and parse it. """Read the config file from disk and parse it.

View File

@ -371,10 +371,8 @@ class QtWarningFilter(logging.Filter):
def filter(self, record): def filter(self, record):
"""Determine if the specified record is to be logged.""" """Determine if the specified record is to be logged."""
if record.msg.strip().startswith(self._pattern): do_log = not record.msg.strip().startswith(self._pattern)
return False # filter return do_log
else:
return True # log
class LogFilter(logging.Filter): class LogFilter(logging.Filter):

View File

@ -122,10 +122,7 @@ def _is_url_naive(urlstr):
if not QHostAddress(urlstr).isNull(): if not QHostAddress(urlstr).isNull():
return False return False
if '.' in url.host(): return '.' in url.host()
return True
else:
return False
def _is_url_dns(urlstr): def _is_url_dns(urlstr):
@ -254,10 +251,7 @@ def is_url(urlstr):
# no autosearch, so everything is a URL unless it has an explicit # no autosearch, so everything is a URL unless it has an explicit
# search engine. # search engine.
engine, _term = _parse_search_term(urlstr) engine, _term = _parse_search_term(urlstr)
if engine is None: return engine is None
return True
else:
return False
if not qurl_userinput.isValid(): if not qurl_userinput.isValid():
# This will also catch URLs containing spaces. # This will also catch URLs containing spaces.

View File

@ -20,22 +20,172 @@
"""Generate the html documentation based on the asciidoc files.""" """Generate the html documentation based on the asciidoc files."""
import re
import os import os
import os.path import os.path
import sys import sys
import subprocess import subprocess
import glob import glob
import shutil
import tempfile
import argparse import argparse
import io
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils from scripts import utils
def _get_asciidoc_cmd(args): class AsciiDoc:
"""Abstraction of an asciidoc subprocess."""
FILES = [
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
]
def __init__(self, args):
self._cmd = None
self._args = args
self._homedir = None
self._themedir = None
self._tempdir = None
self._failed = False
def prepare(self):
"""Get the asciidoc command and create the homedir to use."""
self._cmd = self._get_asciidoc_cmd()
self._homedir = tempfile.mkdtemp()
self._themedir = os.path.join(
self._homedir, '.asciidoc', 'themes', 'qute')
self._tempdir = os.path.join(self._homedir, 'tmp')
os.makedirs(self._tempdir)
os.makedirs(self._themedir)
def cleanup(self):
"""Clean up the temporary home directory for asciidoc."""
if self._homedir is not None and not self._failed:
shutil.rmtree(self._homedir)
def build(self):
if self._args.website:
self._build_website()
else:
self._build_docs()
def _build_docs(self):
"""Render .asciidoc files to .html sites."""
files = self.FILES[:]
for src in glob.glob('doc/help/*.asciidoc'):
name, _ext = os.path.splitext(os.path.basename(src))
dst = 'qutebrowser/html/doc/{}.html'.format(name)
files.append((src, dst))
for src, dst in files:
self.call(src, dst)
def _build_website_file(self, root, filename):
"""Build a single website file."""
# pylint: disable=too-many-locals
src = os.path.join(root, filename)
src_basename = os.path.basename(src)
parts = [self._args.website[0]]
dirname = os.path.dirname(src)
if dirname:
parts.append(os.path.relpath(os.path.dirname(src)))
parts.append(
os.extsep.join((os.path.splitext(src_basename)[0],
'html')))
dst = os.path.join(*parts)
os.makedirs(os.path.dirname(dst), exist_ok=True)
modified_src = os.path.join(self._tempdir, src_basename)
shutil.copy('www/header.asciidoc', modified_src)
outfp = io.StringIO()
with open(modified_src, 'r', encoding='utf-8') as header_file:
header = header_file.read()
header += "\n\n"
with open(src, 'r', encoding='utf-8') as infp:
outfp.write("\n\n")
hidden = False
found_title = False
title = ""
last_line = ""
for line in infp:
if line.strip() == '// QUTE_WEB_HIDE':
assert not hidden
hidden = True
elif line.strip() == '// QUTE_WEB_HIDE_END':
assert hidden
hidden = False
elif line == "The Compiler <mail@qutebrowser.org>\n":
continue
elif re.match(r'^:\w+:.*', line):
# asciidoc field
continue
if not found_title:
if re.match(r'^=+$', line):
line = line.replace('=', '-')
found_title = True
title = last_line + "=" * (len(last_line) - 1)
elif re.match(r'^= .+', line):
line = '==' + line[1:]
found_title = True
title = last_line + "=" * (len(last_line) - 1)
if not hidden:
outfp.write(line.replace(".asciidoc[", ".html["))
last_line = line
current_lines = outfp.getvalue()
outfp.close()
with open(modified_src, 'w+', encoding='utf-8') as final_version:
final_version.write(title + "\n\n" + header + current_lines)
self.call(modified_src, dst, '--theme=qute')
def _build_website(self):
"""Prepare and build the website."""
theme_file = os.path.abspath(os.path.join('www', 'qute.css'))
shutil.copy(theme_file, self._themedir)
outdir = self._args.website[0]
for root, _dirs, files in os.walk(os.getcwd()):
for filename in files:
basename, ext = os.path.splitext(filename)
if (ext != '.asciidoc' or
basename in ('header', 'OpenSans-License')):
continue
self._build_website_file(root, filename)
copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'}
for src, dest in copy.items():
full_dest = os.path.join(outdir, dest)
try:
shutil.rmtree(full_dest)
except FileNotFoundError:
pass
shutil.copytree(src, full_dest)
try:
os.symlink('README.html', os.path.join(outdir, 'index.html'))
except FileExistsError:
pass
def _get_asciidoc_cmd(self):
"""Try to find out what commandline to use to invoke asciidoc.""" """Try to find out what commandline to use to invoke asciidoc."""
if args.asciidoc is not None: if self._args.asciidoc is not None:
return args.asciidoc return self._args.asciidoc
try: try:
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL, subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
@ -55,24 +205,27 @@ def _get_asciidoc_cmd(args):
raise FileNotFoundError raise FileNotFoundError
def call(self, src, dst, *args):
def call_asciidoc(args, src, dst):
"""Call asciidoc for the given files. """Call asciidoc for the given files.
Args: Args:
args: The asciidoc binary to use, as a list.
src: The source .asciidoc file. src: The source .asciidoc file.
dst: The destination .html file, or None to auto-guess. dst: The destination .html file, or None to auto-guess.
*args: Additional arguments passed to asciidoc.
""" """
print("Calling asciidoc for {}...".format(os.path.basename(src))) print("Calling asciidoc for {}...".format(os.path.basename(src)))
args = args[:] cmdline = self._cmd[:]
if dst is not None: if dst is not None:
args += ['--out-file', dst] cmdline += ['--out-file', dst]
args.append(src) cmdline += args
cmdline.append(src)
try: try:
subprocess.check_call(args) subprocess.check_call(cmdline, env={'HOME': self._homedir})
self._failed = True
except (subprocess.CalledProcessError, OSError) as e: except (subprocess.CalledProcessError, OSError) as e:
self._failed = True
utils.print_col(str(e), 'red') utils.print_col(str(e), 'red')
print("Keeping modified sources in {}.".format(self._homedir))
sys.exit(1) sys.exit(1)
@ -81,53 +234,31 @@ def main(colors=False):
utils.change_cwd() utils.change_cwd()
utils.use_color = colors utils.use_color = colors
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--all', help="Build all documentation into a given " parser.add_argument('--website', help="Build website into a given "
"directory.", nargs=1) "directory.", nargs=1)
parser.add_argument('--asciidoc', help="Full path to python and " parser.add_argument('--asciidoc', help="Full path to python and "
"asciidoc.py. If not given, it's searched in PATH.", "asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False, nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC')) metavar=('PYTHON', 'ASCIIDOC'))
args = parser.parse_args() args = parser.parse_args()
asciidoc_files = [
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
]
try: try:
os.mkdir('qutebrowser/html/doc') os.mkdir('qutebrowser/html/doc')
except FileExistsError: except FileExistsError:
pass pass
asciidoc = AsciiDoc(args)
try: try:
asciidoc = _get_asciidoc_cmd(args) asciidoc.prepare()
except FileNotFoundError: except FileNotFoundError:
utils.print_col("Could not find asciidoc! Please install it, or use " utils.print_col("Could not find asciidoc! Please install it, or use "
"the --asciidoc argument to point this script to the " "the --asciidoc argument to point this script to the "
"correct python/asciidoc.py location!", 'red') "correct python/asciidoc.py location!", 'red')
sys.exit(1) sys.exit(1)
if args.all:
for root, _dirs, files in os.walk(os.getcwd()): try:
for filename in files: asciidoc.build()
if os.path.splitext(filename)[1] != '.asciidoc': finally:
continue asciidoc.cleanup()
src = os.path.join(root, filename)
parts = [args.all[0]]
dirname = os.path.dirname(src)
if dirname:
parts.append(os.path.relpath(os.path.dirname(src)))
parts.append(
os.extsep.join((os.path.splitext(os.path.basename(src))[0],
'html')))
dst = os.path.join(*parts)
os.makedirs(os.path.dirname(dst), exist_ok=True)
call_asciidoc(asciidoc, src, dst)
else:
for src in glob.glob('doc/help/*.asciidoc'):
name, _ext = os.path.splitext(os.path.basename(src))
dst = 'qutebrowser/html/doc/{}.html'.format(name)
asciidoc_files.append((src, dst))
for src, dst in asciidoc_files:
call_asciidoc(asciidoc, src, dst)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -34,7 +34,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
from scripts import utils from scripts import utils
Message = collections.namedtuple('Message', 'typ, text') Message = collections.namedtuple('Message', 'typ, text')
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file') MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
@ -143,6 +142,18 @@ class Skipped(Exception):
super().__init__("Skipping coverage checks " + reason) super().__init__("Skipping coverage checks " + reason)
def _get_filename(filename):
"""Transform the absolute test filenames to relative ones."""
if os.path.isabs(filename):
basedir = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..'))
common_path = os.path.commonprefix([basedir, filename])
if common_path:
filename = filename[len(common_path):].lstrip('/')
return filename
def check(fileobj, perfect_files): def check(fileobj, perfect_files):
"""Main entry point which parses/checks coverage.xml if applicable.""" """Main entry point which parses/checks coverage.xml if applicable."""
if sys.platform != 'linux': if sys.platform != 'linux':
@ -151,6 +162,8 @@ def check(fileobj, perfect_files):
raise Skipped("because -k is given.") raise Skipped("because -k is given.")
elif '-m' in sys.argv[1:]: elif '-m' in sys.argv[1:]:
raise Skipped("because -m is given.") raise Skipped("because -m is given.")
elif '--lf' in sys.argv[1:]:
raise Skipped("because --lf is given.")
perfect_src_files = [e[1] for e in perfect_files] perfect_src_files = [e[1] for e in perfect_files]
@ -168,7 +181,8 @@ def check(fileobj, perfect_files):
messages = [] messages = []
for klass in classes: for klass in classes:
filename = klass.attrib['filename'] filename = _get_filename(klass.attrib['filename'])
line_cov = float(klass.attrib['line-rate']) * 100 line_cov = float(klass.attrib['line-rate']) * 100
branch_cov = float(klass.attrib['branch-rate']) * 100 branch_cov = float(klass.attrib['branch-rate']) * 100

View File

@ -44,7 +44,7 @@ INSTALL_PYQT = TESTENV in ('py34', 'py35', 'unittests-nodisp', 'vulture',
'pylint') 'pylint')
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34' XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
pip_packages = ['tox'] pip_packages = ['tox']
if TESTENV in ['py34', 'py35']: if TESTENV in ['py34', 'py35'] and TRAVIS_OS == 'linux':
pip_packages.append('codecov') pip_packages.append('codecov')
@ -69,9 +69,12 @@ def check_setup(executable):
if 'APPVEYOR' in os.environ: if 'APPVEYOR' in os.environ:
print("Getting PyQt5...") print("Getting PyQt5...")
urllib.urlretrieve( qt_version = '5.5.1'
'http://www.qutebrowser.org/pyqt/PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe', pyqt_version = '5.5.1'
r'C:\install-PyQt5.exe') pyqt_url = ('http://www.qutebrowser.org/pyqt/'
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
pyqt_version, qt_version))
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
print("Fixing registry...") print("Fixing registry...")
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, with winreg.OpenKey(winreg.HKEY_CURRENT_USER,

View File

@ -93,10 +93,8 @@ def is_qutebrowser_dump(parsed):
return True return True
else: else:
return '-m qutebrowser' in cmdline return '-m qutebrowser' in cmdline
elif basename == 'qutebrowser':
return True
else: else:
return False return basename == 'qutebrowser'
def dump_infos_gdb(parsed): def dump_infos_gdb(parsed):

View File

@ -27,7 +27,7 @@ import sys
import pytest import pytest
import pytestqt.plugin import pytestqt.plugin
import pytest_mock import pytest_mock
import pytest_capturelog import pytest_catchlog
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock, sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
pytest_capturelog])) pytest_catchlog]))

View File

@ -111,11 +111,7 @@ def filter_func(item):
True if the missing function should be filtered/ignored, False True if the missing function should be filtered/ignored, False
otherwise. otherwise.
""" """
if re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)): return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
# probably a virtual Qt method
return True
else:
return False
def report(items): def report(items):

View File

@ -389,6 +389,8 @@ def _get_authors():
'binix': 'sbinix', 'binix': 'sbinix',
'Averrin': 'Alexey "Averrin" Nabrodov', 'Averrin': 'Alexey "Averrin" Nabrodov',
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov', 'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
'Michael': 'Halfwit',
'Error 800': 'error800',
} }
commits = subprocess.check_output(['git', 'log', '--format=%aN']) commits = subprocess.check_output(['git', 'log', '--format=%aN'])
authors = [corrections.get(author, author) authors = [corrections.get(author, author)

124
scripts/dev/ua_fetch.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 lamarpavel
# Copyright 2015 Alexey Nabrodov (Averrin)
#
# 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/>.
"""Fetch list of popular user-agents.
The script is based on a gist posted by github.com/averrin, the output of this
script is formatted to be pasted into configtypes.py.
"""
import requests
from lxml import html # pylint: disable=import-error
def fetch():
"""Fetch list of popular user-agents.
Return:
List of relevant strings.
"""
url = 'https://techblog.willshouse.com/2012/01/03/most-common-user-agents/'
page = requests.get(url)
page = html.fromstring(page.text)
path = '//*[@id="post-2229"]/div[2]/table/tbody'
return page.xpath(path)[0]
def filter_list(complete_list, browsers):
"""Filter the received list based on a look up table.
The LUT should be a dictionary of the format {browser: versions}, where
'browser' is the name of the browser (eg. "Firefox") as string and
'versions' is a set of different versions of this browser that should be
included when found (eg. {"Linux", "MacOSX"}). This function returns a
dictionary with the same keys as the LUT, but storing lists of tuples
(user_agent, browser_description) as values.
"""
table = {}
for entry in complete_list:
# Tuple of (user_agent, browser_description)
candidate = (entry[1].text_content(), entry[2].text_content())
for name in browsers:
found = False
if name.lower() in candidate[1].lower():
for version in browsers[name]:
if version.lower() in candidate[1].lower():
if table.get(name) is None:
table[name] = []
table[name].append(candidate)
browsers[name].remove(version)
found = True
break
if found:
break
return table
def add_diversity(table):
"""Insert a few additional entries for diversity into the dict.
(as returned by filter_list())
"""
table["Obscure"] = [
('Mozilla/5.0 (compatible; Googlebot/2.1; '
'+http://www.google.com/bot.html',
"Google Bot"),
('Wget/1.16.1 (linux-gnu)',
"wget 1.16.1"),
('curl/7.40.0',
"curl 7.40.0")
]
return table
def main():
"""Generate user agent code."""
fetched = fetch()
lut = {
"Firefox": {"Win", "MacOSX", "Linux", "Android"},
"Chrome": {"Win", "MacOSX", "Linux"},
"Safari": {"MacOSX", "iOS"}
}
filtered = filter_list(fetched, lut)
filtered = add_diversity(filtered)
tab = " "
print(tab + "def complete(self):")
print((2 * tab) + "\"\"\"Complete a list of common user agents.\"\"\"")
print((2 * tab) + "%sout = [")
for browser in ["Firefox", "Safari", "Chrome", "Obscure"]:
for it in filtered[browser]:
print("{}(\'{}\',\n{} \"{}\"),".format(3 * tab, it[0],
3 * tab, it[1]))
print("")
print("""\
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
'Gecko',
"IE 11.0 for Desktop Win7 64-bit")""")
print("{}]\n{}return out\n".format(2 * tab, 2 * tab))
if __name__ == '__main__':
main()

View File

@ -33,7 +33,7 @@ import pytest
import helpers.stubs as stubsmod import helpers.stubs as stubsmod
from helpers import logfail from helpers import logfail
from helpers.logfail import fail_on_logging, caplog_bug_workaround from helpers.logfail import fail_on_logging
from helpers.messagemock import message_mock from helpers.messagemock import message_mock
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import objreg from qutebrowser.utils import objreg
@ -121,11 +121,8 @@ def pytest_collection_modifyitems(items):
def pytest_ignore_collect(path): def pytest_ignore_collect(path):
"""Ignore BDD tests during collection if frozen.""" """Ignore BDD tests during collection if frozen."""
rel_path = path.relto(os.path.dirname(__file__)) rel_path = path.relto(os.path.dirname(__file__))
if (rel_path == os.path.join('integration', 'features') and return (rel_path == os.path.join('integration', 'features') and
hasattr(sys, 'frozen')): hasattr(sys, 'frozen'))
return True
else:
return False
@pytest.fixture(scope='session') @pytest.fixture(scope='session')

View File

@ -24,7 +24,7 @@ import logging
import pytest import pytest
try: try:
import pytest_capturelog as caplog_mod import pytest_catchlog as catchlog_mod
except ImportError: except ImportError:
# When using pytest for pyflakes/pep8/..., the plugin won't be available # When using pytest for pyflakes/pep8/..., the plugin won't be available
# but conftest.py will still be loaded. # but conftest.py will still be loaded.
@ -47,18 +47,18 @@ class LogFailHandler(logging.Handler):
root_logger = logging.getLogger() root_logger = logging.getLogger()
for h in root_logger.handlers: for h in root_logger.handlers:
if isinstance(h, caplog_mod.CaptureLogHandler): if isinstance(h, catchlog_mod.LogCaptureHandler):
caplog_handler = h catchlog_handler = h
break break
else: else:
# The CaptureLogHandler is not available anymore during fixture # The LogCaptureHandler is not available anymore during fixture
# teardown, so we ignore logging messages emitted there.. # teardown, so we ignore logging messages emitted there..
return return
if (logger.level == record.levelno or if (logger.level == record.levelno or
caplog_handler.level == record.levelno): catchlog_handler.level == record.levelno):
# caplog.atLevel(...) was used with the level of this message, i.e. # caplog.at_level(...) was used with the level of this message,
# it was expected. # i.e. it was expected.
return return
if record.levelno < self._min_level: if record.levelno < self._min_level:
return return
@ -74,25 +74,3 @@ def fail_on_logging():
yield yield
logging.getLogger().removeHandler(handler) logging.getLogger().removeHandler(handler)
handler.close() handler.close()
@pytest.yield_fixture(autouse=True)
def caplog_bug_workaround(request):
"""WORKAROUND for pytest-capturelog bug.
https://bitbucket.org/memedough/pytest-capturelog/issues/7/
This would lead to LogFailHandler failing after skipped tests as there are
multiple CaptureLogHandlers.
"""
yield
if caplog_mod is None:
return
root_logger = logging.getLogger()
caplog_handlers = [h for h in root_logger.handlers
if isinstance(h, caplog_mod.CaptureLogHandler)]
for h in caplog_handlers:
root_logger.removeHandler(h)
h.close()

View File

@ -61,7 +61,7 @@ class MessageMock:
} }
log_level = log_levels[level] log_level = log_levels[level]
with self._caplog.atLevel(log_level): # needed so we don't fail with self._caplog.at_level(log_level): # needed so we don't fail
logging.getLogger('message').log(log_level, text) logging.getLogger('message').log(log_level, text)
self.messages.append(Message(level, win_id, text, immediately)) self.messages.append(Message(level, win_id, text, immediately))

View File

@ -48,3 +48,24 @@ def test_partial_compare_equal(val1, val2):
]) ])
def test_partial_compare_not_equal(val1, val2): def test_partial_compare_not_equal(val1, val2):
assert not utils.partial_compare(val1, val2) assert not utils.partial_compare(val1, val2)
@pytest.mark.parametrize('pattern, value, expected', [
('foo', 'foo', True),
('foo', 'bar', False),
('foo', 'Foo', False),
('foo', 'foobar', False),
('foo', 'barfoo', False),
('foo*', 'foobarbaz', True),
('*bar', 'foobar', True),
('foo*baz', 'foobarbaz', True),
('foo[b]ar', 'foobar', False),
('foo[b]ar', 'foo[b]ar', True),
('foo?ar', 'foobar', False),
('foo?ar', 'foo?ar', True),
])
def test_pattern_match(pattern, value, expected):
assert utils.pattern_match(pattern=pattern, value=value) == expected

View File

@ -23,7 +23,7 @@
import logging import logging
import pytest import pytest
import pytest_capturelog # pylint: disable=import-error import pytest_catchlog # pylint: disable=import-error
def test_log_debug(): def test_log_debug():
@ -36,33 +36,33 @@ def test_log_warning():
def test_log_expected(caplog): def test_log_expected(caplog):
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
logging.error('foo') logging.error('foo')
def test_log_expected_logger(caplog): def test_log_expected_logger(caplog):
logger = 'logfail_test_logger' logger = 'logfail_test_logger'
with caplog.atLevel(logging.ERROR, logger): with caplog.at_level(logging.ERROR, logger):
logging.getLogger(logger).error('foo') logging.getLogger(logger).error('foo')
def test_log_expected_wrong_level(caplog): def test_log_expected_wrong_level(caplog):
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
logging.critical('foo') logging.critical('foo')
def test_log_expected_logger_wrong_level(caplog): def test_log_expected_logger_wrong_level(caplog):
logger = 'logfail_test_logger' logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger): with caplog.at_level(logging.ERROR, logger):
logging.getLogger(logger).critical('foo') logging.getLogger(logger).critical('foo')
def test_log_expected_wrong_logger(caplog): def test_log_expected_wrong_logger(caplog):
logger = 'logfail_test_logger' logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger): with caplog.at_level(logging.ERROR, logger):
logging.error('foo') logging.error('foo')
@ -82,6 +82,6 @@ def test_caplog_bug_workaround_2():
""" """
caplog_handler = None caplog_handler = None
for h in logging.getLogger().handlers: for h in logging.getLogger().handlers:
if isinstance(h, pytest_capturelog.CaptureLogHandler): if isinstance(h, pytest_catchlog.LogCaptureHandler):
assert caplog_handler is None assert caplog_handler is None
caplog_handler = h caplog_handler = h

View File

@ -20,32 +20,39 @@
"""Partial comparison of dicts/lists.""" """Partial comparison of dicts/lists."""
import fnmatch import re
import pprint
def _partial_compare_dict(val1, val2): def print_i(text, indent, error=False):
if error:
text = '| ****** {} ******'.format(text)
for line in text.splitlines():
print('| ' * indent + line)
def _partial_compare_dict(val1, val2, *, indent=0):
for key in val2: for key in val2:
if key not in val1: if key not in val1:
print("Key {!r} is in second dict but not in first!".format(key)) print_i("Key {!r} is in second dict but not in first!".format(key),
indent, error=True)
return False return False
if not partial_compare(val1[key], val2[key]): if not partial_compare(val1[key], val2[key], indent=indent+1):
print("Comparison failed for {!r} and {!r}!".format(
val1[key], val2[key]))
return False return False
return True return True
def _partial_compare_list(val1, val2): def _partial_compare_list(val1, val2, *, indent=0):
if len(val1) < len(val2): if len(val1) < len(val2):
print("Second list is longer than first list -> False!") print_i("Second list is longer than first list", indent, error=True)
return False return False
for item1, item2 in zip(val1, val2): for item1, item2 in zip(val1, val2):
if not partial_compare(item1, item2): if not partial_compare(item1, item2, indent=indent+1):
return False return False
return True return True
def partial_compare(val1, val2): def partial_compare(val1, val2, *, indent=0):
"""Do a partial comparison between the given values. """Do a partial comparison between the given values.
For dicts, keys in val2 are checked, others are ignored. For dicts, keys in val2 are checked, others are ignored.
@ -54,31 +61,44 @@ def partial_compare(val1, val2):
This happens recursively. This happens recursively.
""" """
print() print_i("Comparing", indent)
print("Comparing\n {!r}\nto\n {!r}".format(val1, val2)) print_i(pprint.pformat(val1), indent + 1)
print_i("|---- to ----", indent)
print_i(pprint.pformat(val2), indent + 1)
if val2 is Ellipsis: if val2 is Ellipsis:
print("Ignoring ellipsis comparison") print_i("Ignoring ellipsis comparison", indent, error=True)
return True return True
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
print("Different types ({}, {}) -> False".format( print_i("Different types ({}, {}) -> False".format(
type(val1), type(val2))) type(val1), type(val2)), indent, error=True)
return False return False
if isinstance(val2, dict): if isinstance(val2, dict):
print("Comparing as dicts") print_i("|======= Comparing as dicts", indent)
equal = _partial_compare_dict(val1, val2) equal = _partial_compare_dict(val1, val2, indent=indent)
elif isinstance(val2, list): elif isinstance(val2, list):
print("Comparing as lists") print_i("|======= Comparing as lists", indent)
equal = _partial_compare_list(val1, val2) equal = _partial_compare_list(val1, val2, indent=indent)
elif isinstance(val2, float): elif isinstance(val2, float):
print("Doing float comparison") print_i("|======= Doing float comparison", indent)
equal = abs(val1 - val2) < 0.00001 equal = abs(val1 - val2) < 0.00001
elif isinstance(val2, str): elif isinstance(val2, str):
print("Doing string comparison") print_i("|======= Doing string comparison", indent)
equal = fnmatch.fnmatchcase(val1, val2) equal = pattern_match(pattern=val2, value=val1)
else: else:
print("Comparing via ==") print_i("|======= Comparing via ==", indent)
equal = val1 == val2 equal = val1 == val2
print("---> {}".format(equal)) print_i("---> {}".format(equal), indent)
return equal return equal
def pattern_match(*, pattern, value):
"""Do fnmatch.fnmatchcase like matching, but only with * active.
Return:
True on a match, False otherwise.
"""
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*'))
return re.fullmatch(re_pattern, value) is not None

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1 @@
Hello World 2!

View File

@ -0,0 +1 @@
Hello World 3!

View File

@ -207,5 +207,8 @@
198 198
199 199
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet.
</pre>
<a href="/data/hello2.txt">next</a> link to test the --top-navigate argument for :scroll-page.
<a href="/data/hello3.txt">prev</a> link to test the --bottom-navigate argument for :scroll-page.
</body> </body>
</html> </html>

View File

@ -43,6 +43,19 @@ Feature: Going back and forward.
url: http://localhost:*/data/backforward/1.txt url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt - url: http://localhost:*/data/backforward/2.txt
Scenario: Going back in a new tab without history
Given I open data/backforward/1.txt
When I run :tab-only
And I run :back -t
Then the error "At beginning of history." should be shown.
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/backforward/1.txt
Scenario: Going back in a new background tab Scenario: Going back in a new background tab
Given I open data/backforward/1.txt Given I open data/backforward/1.txt
When I open data/backforward/2.txt When I open data/backforward/2.txt
@ -62,6 +75,31 @@ Feature: Going back and forward.
url: http://localhost:*/data/backforward/1.txt url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt - url: http://localhost:*/data/backforward/2.txt
Scenario: Going back with count.
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I open data/backforward/3.txt
And I run :tab-only
And I run :back with count 2
And I wait until data/backforward/1.txt is loaded
And I reload
Then the session should look like:
windows:
- tabs:
- history:
- active: true
url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt
- url: http://localhost:*/data/backforward/3.txt
Scenario: Going back with very big count.
Given I open data/backforward/1.txt
When I run :back with count 99999999999
# Make sure it doesn't hang
And I run :message-info "Still alive!"
Then the error "At beginning of history." should be shown.
And the message "Still alive!" should be shown.
Scenario: Going back in a new window Scenario: Going back in a new window
Given I have a fresh instance Given I have a fresh instance
When I open data/backforward/1.txt When I open data/backforward/1.txt

View File

@ -62,8 +62,13 @@ def fresh_instance(quteproc):
@bdd.when(bdd.parsers.parse("I run {command}")) @bdd.when(bdd.parsers.parse("I run {command}"))
def run_command_when(quteproc, httpbin, command): def run_command_when(quteproc, httpbin, command):
if 'with count' in command:
command, count = command.split(' with count ')
count = int(count)
else:
count = None
command = command.replace('(port)', str(httpbin.port)) command = command.replace('(port)', str(httpbin.port))
quteproc.send_cmd(command) quteproc.send_cmd(command, count=count)
@bdd.when(bdd.parsers.parse("I reload")) @bdd.when(bdd.parsers.parse("I reload"))
@ -93,13 +98,12 @@ def wait_for_message(quteproc, httpbin, category, message):
@bdd.then(bdd.parsers.parse("{path} should be loaded")) @bdd.then(bdd.parsers.parse("{path} should be loaded"))
def path_should_be_loaded(httpbin, path): def path_should_be_loaded(httpbin, path):
requests = httpbin.get_requests() httpbin.wait_for(verb='GET', path='/' + path)
assert requests[-1] == httpbin.Request('GET', '/' + path)
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}")) @bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
def list_of_loaded_pages(httpbin, pages): def list_of_loaded_pages(httpbin, pages):
expected_requests = [httpbin.Request('GET', '/' + path.strip()) expected_requests = [httpbin.ExpectedRequest('GET', '/' + path.strip())
for path in pages.split('\n')] for path in pages.split('\n')]
actual_requests = httpbin.get_requests() actual_requests = httpbin.get_requests()
assert actual_requests == expected_requests assert actual_requests == expected_requests
@ -131,6 +135,11 @@ def compare_session(quteproc, expected):
assert utils.partial_compare(data, expected) assert utils.partial_compare(data, expected)
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
def ensure_not_logged(quteproc, pattern):
quteproc.ensure_not_logged(message=pattern)
@bdd.then("no crash should happen") @bdd.then("no crash should happen")
def no_crash(): def no_crash():
"""Don't do anything. """Don't do anything.

View File

@ -1,5 +1,7 @@
Feature: Various utility commands. Feature: Various utility commands.
## :set-cmd-text
Scenario: :set-cmd-text and :command-accept Scenario: :set-cmd-text and :command-accept
When I run :set-cmd-text :message-info "Hello World" When I run :set-cmd-text :message-info "Hello World"
And I run :command-accept And I run :command-accept
@ -31,6 +33,8 @@ Feature: Various utility commands.
When I run :set-cmd-text foo When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown. Then the error "Invalid command text 'foo'." should be shown.
## :message-*
Scenario: :message-error Scenario: :message-error
When I run :message-error "Hello World" When I run :message-error "Hello World"
Then the error "Hello World" should be shown. Then the error "Hello World" should be shown.
@ -42,3 +46,57 @@ Feature: Various utility commands.
Scenario: :message-warning Scenario: :message-warning
When I run :message-warning "Hello World" When I run :message-warning "Hello World"
Then the warning "Hello World" should be shown. Then the warning "Hello World" should be shown.
## :jseval
Scenario: :jseval
When I set general -> log-javascript-console to true
And I run :jseval console.log("Hello from JS!");
And I wait for "[:0] Hello from JS!" in the log
Then the message "No output or error" should be shown.
Scenario: :jseval without logging
When I set general -> log-javascript-console to false
And I run :jseval console.log("Hello from JS!");
Then the message "No output or error" should be shown.
And "[:0] Hello from JS!" should not be logged
Scenario: :jseval with --quiet
When I set general -> log-javascript-console to true
And I run :jseval --quiet console.log("Hello from JS!");
And I wait for "[:0] Hello from JS!" in the log
Then "No output or error" should not be logged
Scenario: :jseval with a value
When I run :jseval "foo"
Then the message "foo" should be shown.
Scenario: :jseval with a long, truncated value
When I run :jseval Array(5002).join("x")
Then the message "x* [...trimmed...]" should be shown.
# :debug-webaction
Scenario: :debug-webaction with valid value
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I run :tab-only
And I run :debug-webaction Back
And I wait until data/backforward/1.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- active: true
url: http://localhost:*/data/backforward/1.txt
- url: http://localhost:*/data/backforward/2.txt
Scenario: :debug-webaction with invalid value
When I open data/hello.txt
And I run :debug-webaction blah
Then the error "blah is not a valid web action!" should be shown.
Scenario: :debug-webaction with non-webaction member
When I open data/hello.txt
And I run :debug-webaction PermissionUnknown
Then the error "PermissionUnknown is not a valid web action!" should be shown.

View File

@ -5,6 +5,8 @@ Feature: Scrolling
Given I open data/scroll.html Given I open data/scroll.html
And I run :tab-only And I run :tab-only
## :scroll-px
Scenario: Scrolling pixel-wise vertically Scenario: Scrolling pixel-wise vertically
When I run :scroll-px 0 10 When I run :scroll-px 0 10
Then the page should be scrolled vertically. Then the page should be scrolled vertically.
@ -13,6 +15,45 @@ Feature: Scrolling
When I run :scroll-px 10 0 When I run :scroll-px 10 0
Then the page should be scrolled horizontally. Then the page should be scrolled horizontally.
Scenario: Scrolling down and up
When I run :scroll-px 10 0
And I run :scroll-px -10 0
Then the page should not be scrolled.
Scenario: Scrolling right and left
When I run :scroll-px 0 10
And I run :scroll-px 0 -10
Then the page should not be scrolled.
Scenario: Scrolling down and up with count
When I run :scroll-px 0 10 with count 2
When I run :scroll-px 0 -10
When I run :scroll-px 0 -10
Then the page should not be scrolled.
Scenario: Scrolling left and right with count
When I run :scroll-px 10 0 with count 2
When I run :scroll-px -10 0
When I run :scroll-px -10 0
Then the page should not be scrolled.
Scenario: :scroll-px with a very big value
When I run :scroll-px 99999999999 0
Then the error "Numeric argument is too large for internal int representation." should be shown.
Scenario: :scroll-px on a page without scrolling
When I open data/hello.txt
And I run :scroll-px 10 10
Then no crash should happen
Scenario: :scroll-px with floats
# This used to be allowed, but doesn't make much sense.
When I run :scroll-px 2.5 2.5
Then the error "scroll-px: Argument dx: invalid int value: '2.5'" should be shown.
And the page should not be scrolled.
## :scroll
Scenario: Scrolling down Scenario: Scrolling down
When I run :scroll down When I run :scroll down
Then the page should be scrolled vertically. Then the page should be scrolled vertically.
@ -58,3 +99,155 @@ Feature: Scrolling
When I run :scroll 0 10 When I run :scroll 0 10
Then the warning ":scroll with dx/dy arguments is deprecated - use :scroll-px instead!" should be shown. Then the warning ":scroll with dx/dy arguments is deprecated - use :scroll-px instead!" should be shown.
Then the page should be scrolled vertically. Then the page should be scrolled vertically.
Scenario: :scroll with deprecated pixel argument (float)
When I run :scroll 2.5 2.5
Then the error "scroll: Argument dy: invalid int value: '2.5'" should be shown.
And the page should not be scrolled.
Scenario: Scrolling down and up with count
When I run :scroll down with count 2
And I run :scroll up
And I run :scroll up
Then the page should not be scrolled.
Scenario: Scrolling right
When I run :scroll right
Then the page should be scrolled horizontally.
Scenario: Scrolling right and left
When I run :scroll right
And I run :scroll left
Then the page should not be scrolled.
Scenario: Scrolling right and left with count
When I run :scroll right with count 2
And I run :scroll left
And I run :scroll left
Then the page should not be scrolled.
Scenario: Scrolling down with a very big count
When I run :scroll down with count 99999999999
# Make sure it doesn't hang
And I run :message-info "Still alive!"
Then the message "Still alive!" should be shown.
Scenario: :scroll on a page without scrolling
When I open data/hello.txt
And I run :scroll down
Then no crash should happen
## :scroll-perc
Scenario: Scrolling to bottom with :scroll-perc
When I run :scroll-perc 100
Then the page should be scrolled vertically.
Scenario: Scrolling to bottom and to top with :scroll-perc
When I run :scroll-perc 100
And I run :scroll-perc 0
Then the page should not be scrolled.
Scenario: Scrolling to middle with :scroll-perc
When I run :scroll-perc 50
Then the page should be scrolled vertically.
Scenario: Scrolling to middle with :scroll-perc (float)
When I run :scroll-perc 50.5
Then the page should be scrolled vertically.
Scenario: Scrolling to middle and to top with :scroll-perc
When I run :scroll-perc 50
And I run :scroll-perc 0
Then the page should not be scrolled.
Scenario: Scrolling to right with :scroll-perc
When I run :scroll-perc --horizontal 100
Then the page should be scrolled horizontally.
Scenario: Scrolling to right and to left with :scroll-perc
When I run :scroll-perc --horizontal 100
And I run :scroll-perc --horizontal 0
Then the page should not be scrolled.
Scenario: Scrolling to middle (horizontally) with :scroll-perc
When I run :scroll-perc --horizontal 50
Then the page should be scrolled horizontally.
Scenario: Scrolling to middle and to left with :scroll-perc
When I run :scroll-perc --horizontal 50
And I run :scroll-perc --horizontal 0
Then the page should not be scrolled.
Scenario: :scroll-perc without argument
When I run :scroll-perc
Then the page should be scrolled vertically.
Scenario: :scroll-perc without argument and --horizontal
When I run :scroll-perc --horizontal
Then the page should be scrolled horizontally.
Scenario: :scroll-perc with count
When I run :scroll-perc with count 50
Then the page should be scrolled vertically.
Scenario: :scroll-perc with a very big value
When I run :scroll-perc 99999999999
Then no crash should happen
Scenario: :scroll-perc on a page without scrolling
When I open data/hello.txt
And I run :scroll-perc 20
Then no crash should happen
## :scroll-page
Scenario: Scrolling down with :scroll-page
When I run :scroll-page 0 1
Then the page should be scrolled vertically.
Scenario: Scrolling down with :scroll-page (float)
When I run :scroll-page 0 1.5
Then the page should be scrolled vertically.
Scenario: Scrolling down and up with :scroll-page
When I run :scroll-page 0 1
And I run :scroll-page 0 -1
Then the page should not be scrolled.
Scenario: Scrolling right with :scroll-page
When I run :scroll-page 1 0
Then the page should be scrolled horizontally.
Scenario: Scrolling right with :scroll-page (float)
When I run :scroll-page 1.5 0
Then the page should be scrolled horizontally.
Scenario: Scrolling right and left with :scroll-page
When I run :scroll-page 1 0
And I run :scroll-page -1 0
Then the page should not be scrolled.
Scenario: Scrolling right and left with :scroll-page and count
When I run :scroll-page 1 0 with count 2
And I run :scroll-page -1 0
And I run :scroll-page -1 0
Then the page should not be scrolled.
Scenario: :scroll-page with --bottom-navigate
When I run :scroll-perc 100
And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded
Scenario: :scroll-page with --top-navigate
When I run :scroll-page --top-navigate prev 0 -1
Then data/hello3.txt should be loaded
Scenario: :scroll-page with a very big value
When I run :scroll-page 99999999999 99999999999
Then the error "Numeric argument is too large for internal int representation." should be shown.
Scenario: :scroll-page on a page without scrolling
When I open data/hello.txt
And I run :scroll-page 1 1
Then no crash should happen

View File

@ -27,22 +27,46 @@ import pytest_bdd as bdd
bdd.scenarios('yankpaste.feature') bdd.scenarios('yankpaste.feature')
@pytest.fixture(autouse=True)
def skip_with_broken_clipboard(qapp):
"""The clipboard seems to be broken on some platforms (OS X Yosemite?).
This skips the tests if this is the case.
"""
clipboard = qapp.clipboard()
clipboard.setText("Does this work?")
if clipboard.text() != "Does this work?":
pytest.skip("Clipboard seems to be broken on this platform.")
def _get_mode(qapp, what):
"""Get the QClipboard::Mode to use based on a string."""
if what == 'clipboard':
return QClipboard.Clipboard
elif what == 'primary selection':
assert qapp.clipboard().supportsSelection()
return QClipboard.Selection
else:
raise AssertionError
@bdd.when("selection is supported") @bdd.when("selection is supported")
def selection_supported(qapp): def selection_supported(qapp):
if not qapp.clipboard().supportsSelection(): if not qapp.clipboard().supportsSelection():
pytest.skip("OS doesn't support primary selection!") pytest.skip("OS doesn't support primary selection!")
@bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the '
r'(?P<what>primary selection|clipboard)'))
def fill_clipboard(qapp, httpbin, what, content):
mode = _get_mode(qapp, what)
content = content.replace('(port)', str(httpbin.port))
qapp.clipboard().setText(content, mode)
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should ' @bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
r'contain "(?P<content>.*)"')) r'contain "(?P<content>.*)"'))
def clipboard_contains(qapp, httpbin, what, content): def clipboard_contains(qapp, httpbin, what, content):
if what == 'clipboard': mode = _get_mode(qapp, what)
mode = QClipboard.Clipboard
elif what == 'primary selection':
mode = QClipboard.Selection
else:
raise AssertionError
expected = content.replace('(port)', str(httpbin.port)) expected = content.replace('(port)', str(httpbin.port))
data = qapp.clipboard().text(mode=mode) data = qapp.clipboard().text(mode=mode)

View File

@ -19,3 +19,10 @@
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('zoom.feature') bdd.scenarios('zoom.feature')
@bdd.then(bdd.parsers.parse("the zoom should be {zoom}%"))
def check_zoom(quteproc, zoom):
data = quteproc.get_session()
value = data['windows'][0]['tabs'][0]['history'][0]['zoom'] * 100
assert abs(value - float(zoom)) < 0.0001

View File

@ -3,26 +3,112 @@ Feature: Yanking and pasting.
clipboard and primary selection. clipboard and primary selection.
Background: Background:
Given I open data/yankpaste.html Given I run :tab-only
#### :yank
Scenario: Yanking URLs to clipboard Scenario: Yanking URLs to clipboard
When I run :yank When I open data/yankpaste.html
And I run :yank
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/yankpaste.html" should be shown. Then the message "Yanked URL to clipboard: http://localhost:(port)/data/yankpaste.html" should be shown.
And the clipboard should contain "http://localhost:(port)/data/yankpaste.html" And the clipboard should contain "http://localhost:(port)/data/yankpaste.html"
Scenario: Yanking URLs to primary selection Scenario: Yanking URLs to primary selection
When selection is supported When selection is supported
And I open data/yankpaste.html
And I run :yank --sel And I run :yank --sel
Then the message "Yanked URL to primary selection: http://localhost:(port)/data/yankpaste.html" should be shown. Then the message "Yanked URL to primary selection: http://localhost:(port)/data/yankpaste.html" should be shown.
And the primary selection should contain "http://localhost:(port)/data/yankpaste.html" And the primary selection should contain "http://localhost:(port)/data/yankpaste.html"
Scenario: Yanking title to clipboard Scenario: Yanking title to clipboard
When I wait for regex "Changing title for idx \d to 'Test title'" in the log When I open data/yankpaste.html
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
And I run :yank --title And I run :yank --title
Then the message "Yanked title to clipboard: Test title" should be shown. Then the message "Yanked title to clipboard: Test title" should be shown.
And the clipboard should contain "Test title" And the clipboard should contain "Test title"
Scenario: Yanking domain to clipboard Scenario: Yanking domain to clipboard
When I run :yank --domain When I open data/yankpaste.html
And I run :yank --domain
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown. Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown.
And the clipboard should contain "http://localhost:(port)" And the clipboard should contain "http://localhost:(port)"
#### :paste
Scenario: Pasting an URL
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste
And I wait until data/hello.txt is loaded
Then the requests should be:
data/hello.txt
Scenario: Pasting an URL from primary selection
When selection is supported
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
And I run :paste --sel
And I wait until data/hello2.txt is loaded
Then the requests should be:
data/hello2.txt
Scenario: Pasting with empty clipboard
When I put "" into the clipboard
And I run :paste
Then the error "Clipboard is empty." should be shown.
Scenario: Pasting with empty selection
When selection is supported
And I put "" into the primary selection
And I run :paste --sel
Then the error "Primary selection is empty." should be shown.
Scenario: Pasting in a new tab
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -t
And I wait until data/hello.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- active: true
url: about:blank
- active: true
history:
- active: true
url: http://localhost:*/data/hello.txt
Scenario: Pasting in a background tab
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -b
And I wait until data/hello.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: about:blank
- history:
- active: true
url: http://localhost:*/data/hello.txt
Scenario: Pasting in a new window
Given I have a fresh instance
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -w
And I wait until data/hello.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: about:blank
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/hello.txt

View File

@ -2,41 +2,56 @@ Feature: Zooming in and out
Background: Background:
Given I open data/hello.txt Given I open data/hello.txt
And I set ui -> zoom-levels to 50%,90%,100%,110%,120%
And I run :tab-only And I run :tab-only
Scenario: Zooming in Scenario: Zooming in
When I run :zoom-in When I run :zoom-in
Then the message "Zoom level: 110%" should be shown. Then the message "Zoom level: 110%" should be shown.
And the session should look like: And the zoom should be 110%
windows:
- tabs:
- history:
- zoom: 1.1
Scenario: Zooming out Scenario: Zooming out
When I run :zoom-out When I run :zoom-out
Then the message "Zoom level: 90%" should be shown. Then the message "Zoom level: 90%" should be shown.
And the session should look like: And the zoom should be 90%
windows:
- tabs: Scenario: Zooming in with count
- history: When I run :zoom-in with count 2
- zoom: 0.9 Then the message "Zoom level: 120%" should be shown.
And the zoom should be 120%
# https://github.com/The-Compiler/qutebrowser/issues/1118
Scenario: Zooming in with very big count
When I run :zoom-in with count 99999999999
Then the message "Zoom level: 100%" should be shown.
And the zoom should be 100%
Scenario: Zooming out with count
When I run :zoom-out with count 2
Then the message "Zoom level: 50%" should be shown.
And the zoom should be 50%
Scenario: Setting zoom Scenario: Setting zoom
When I run :zoom 50 When I run :zoom 50
Then the message "Zoom level: 50%" should be shown. Then the message "Zoom level: 50%" should be shown.
And the session should look like: And the zoom should be 50%
windows:
- tabs: Scenario: Setting zoom with count
- history: When I run :zoom with count 40
- zoom: 0.5 Then the message "Zoom level: 40%" should be shown.
And the zoom should be 40%
Scenario: Resetting zoom Scenario: Resetting zoom
When I run :zoom 50 When I set ui -> default-zoom to 42%
And I run :zoom 50
And I run :zoom And I run :zoom
Then the message "Zoom level: 100%" should be shown. Then the message "Zoom level: 42%" should be shown.
And the session should look like: And the zoom should be 42%
windows:
- tabs: Scenario: Setting zoom to invalid value
- history: When I run :zoom -1
- zoom: 1.0 Then the error "Can't zoom -1%!" should be shown.
Scenario: Setting zoom with very big count
When I run :zoom with count 99999999999
Then the message "Zoom level: 99999999999%" should be shown.

View File

@ -47,13 +47,6 @@ def is_ignored_qt_message(message):
return False return False
class NoLineMatch(Exception):
"""Raised by LogLine on unmatched lines."""
pass
class LogLine(testprocess.Line): class LogLine(testprocess.Line):
"""A parsed line from the qutebrowser log output. """A parsed line from the qutebrowser log output.
@ -78,7 +71,7 @@ class LogLine(testprocess.Line):
super().__init__(data) super().__init__(data)
match = self.LOG_RE.match(data) match = self.LOG_RE.match(data)
if match is None: if match is None:
raise NoLineMatch(data) raise testprocess.InvalidLine(data)
self.timestamp = datetime.datetime.strptime(match.group('timestamp'), self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
'%H:%M:%S') '%H:%M:%S')
@ -127,6 +120,9 @@ class QuteProc(testprocess.Process):
got_error = pyqtSignal() got_error = pyqtSignal()
KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line',
'message']
def __init__(self, httpbin, parent=None): def __init__(self, httpbin, parent=None):
super().__init__(parent) super().__init__(parent)
self._httpbin = httpbin self._httpbin = httpbin
@ -135,7 +131,7 @@ class QuteProc(testprocess.Process):
def _parse_line(self, line): def _parse_line(self, line):
try: try:
log_line = LogLine(line) log_line = LogLine(line)
except NoLineMatch: except testprocess.InvalidLine:
if line.startswith(' '): if line.startswith(' '):
# Multiple lines in some log output... # Multiple lines in some log output...
return None return None
@ -144,7 +140,7 @@ class QuteProc(testprocess.Process):
elif is_ignored_qt_message(line): elif is_ignored_qt_message(line):
return None return None
else: else:
raise testprocess.InvalidLine raise
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
pytest.config.getoption('--verbose')): pytest.config.getoption('--verbose')):
@ -177,6 +173,12 @@ class QuteProc(testprocess.Process):
'about:blank'] 'about:blank']
return executable, args return executable, args
def _path_to_url(self, path):
if path.startswith('about:') or path.startswith('qute:'):
return path
else:
return 'http://localhost:{}/{}'.format(self._httpbin.port, path)
def after_test(self): def after_test(self):
bad_msgs = [msg for msg in self._data bad_msgs = [msg for msg in self._data
if msg.loglevel > logging.INFO and not msg.expected] if msg.loglevel > logging.INFO and not msg.expected]
@ -186,9 +188,12 @@ class QuteProc(testprocess.Process):
str(e) for e in bad_msgs) str(e) for e in bad_msgs)
pytest.fail(text, pytrace=False) pytest.fail(text, pytrace=False)
def send_cmd(self, command): def send_cmd(self, command, count=None):
assert self._ipc_socket is not None assert self._ipc_socket is not None
if count is not None:
command = ':{}:{}'.format(count, command.lstrip(':'))
ipc.send_to_running_instance(self._ipc_socket, [command], ipc.send_to_running_instance(self._ipc_socket, [command],
target_arg='') target_arg='')
self.wait_for(category='commands', module='command', function='run', self.wait_for(category='commands', module='command', function='run',
@ -199,7 +204,7 @@ class QuteProc(testprocess.Process):
self.wait_for(category='config', message='Config option changed: *') self.wait_for(category='config', message='Config option changed: *')
def open_path(self, path, new_tab=False): def open_path(self, path, new_tab=False):
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path) url = self._path_to_url(path)
if new_tab: if new_tab:
self.send_cmd(':open -t ' + url) self.send_cmd(':open -t ' + url)
else: else:
@ -218,11 +223,12 @@ class QuteProc(testprocess.Process):
self._data is cleared after every test to provide at least some self._data is cleared after every test to provide at least some
isolation. isolation.
""" """
__tracebackhide__ = True
return super().wait_for(timeout, **kwargs) return super().wait_for(timeout, **kwargs)
def wait_for_load_finished(self, path, timeout=15000): def wait_for_load_finished(self, path, timeout=15000):
"""Wait until any tab has finished loading.""" """Wait until any tab has finished loading."""
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path) url = self._path_to_url(path)
pattern = re.compile( pattern = re.compile(
r"(load status for <qutebrowser.browser.webview.WebView " r"(load status for <qutebrowser.browser.webview.WebView "
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: " r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "

View File

@ -25,6 +25,7 @@ import datetime
import pytest import pytest
import quteprocess import quteprocess
import testprocess
from qutebrowser.utils import log from qutebrowser.utils import log
@ -113,5 +114,5 @@ def test_log_line_parse(data, attrs):
def test_log_line_no_match(): def test_log_line_no_match():
with pytest.raises(quteprocess.NoLineMatch): with pytest.raises(testprocess.InvalidLine):
quteprocess.LogLine("Hello World!") quteprocess.LogLine("Hello World!")

View File

@ -74,19 +74,20 @@ class PythonProcess(testprocess.Process):
return (sys.executable, ['-c', ';'.join(code)]) return (sys.executable, ['-c', ';'.join(code)])
class TestWaitFor:
@pytest.yield_fixture @pytest.yield_fixture
def pyproc(self): def pyproc():
proc = PythonProcess() proc = PythonProcess()
yield proc yield proc
proc.terminate() proc.terminate()
class TestWaitFor:
def test_successful(self, pyproc): def test_successful(self, pyproc):
"""Using wait_for with the expected text.""" """Using wait_for with the expected text."""
pyproc.code = "time.sleep(0.5); print('foobar')" pyproc.code = "time.sleep(0.5); print('foobar')"
with stopwatch(min_ms=500):
pyproc.start() pyproc.start()
with stopwatch(min_ms=300): # on Windows, this can be done faster...
pyproc.wait_for(data="foobar") pyproc.wait_for(data="foobar")
def test_other_text(self, pyproc): def test_other_text(self, pyproc):
@ -103,12 +104,13 @@ class TestWaitFor:
with pytest.raises(testprocess.WaitForTimeout): with pytest.raises(testprocess.WaitForTimeout):
pyproc.wait_for(data="foobar", timeout=100) pyproc.wait_for(data="foobar", timeout=100)
def test_existing_message(self, pyproc): @pytest.mark.parametrize('message', ['foobar', 'literal [x]'])
def test_existing_message(self, message, pyproc):
"""Test with a message which already passed when waiting.""" """Test with a message which already passed when waiting."""
pyproc.code = "print('foobar')" pyproc.code = "print('{}')".format(message)
pyproc.start() pyproc.start()
time.sleep(0.5) # to make sure the message is printed time.sleep(0.5) # to make sure the message is printed
pyproc.wait_for(data="foobar") pyproc.wait_for(data=message)
def test_existing_message_previous_test(self, pyproc): def test_existing_message_previous_test(self, pyproc):
"""Make sure the message of a previous test gets ignored.""" """Make sure the message of a previous test gets ignored."""
@ -133,3 +135,36 @@ class TestWaitFor:
pyproc.wait_for(data="foobar") pyproc.wait_for(data="foobar")
with pytest.raises(testprocess.WaitForTimeout): with pytest.raises(testprocess.WaitForTimeout):
pyproc.wait_for(data="foobar", timeout=100) pyproc.wait_for(data="foobar", timeout=100)
class TestEnsureNotLogged:
@pytest.mark.parametrize('message, pattern', [
('blacklisted', 'blacklisted'),
('bl[a]cklisted', 'bl[a]cklisted'),
('blacklisted', 'black*'),
])
def test_existing_message(self, pyproc, message, pattern):
pyproc.code = "print('{}')".format(message)
pyproc.start()
with stopwatch(max_ms=1000):
with pytest.raises(testprocess.BlacklistedMessageError):
pyproc.ensure_not_logged(data=pattern, delay=2000)
def test_late_message(self, pyproc):
pyproc.code = "time.sleep(0.5); print('blacklisted')"
pyproc.start()
with pytest.raises(testprocess.BlacklistedMessageError):
pyproc.ensure_not_logged(data='blacklisted', delay=1000)
def test_no_matching_message(self, pyproc):
pyproc.code = "print('blacklisted... nope!')"
pyproc.start()
pyproc.ensure_not_logged(data='blacklisted', delay=100)
def test_wait_for_and_blacklist(self, pyproc):
pyproc.code = "print('blacklisted')"
pyproc.start()
pyproc.wait_for(data='blacklisted')
with pytest.raises(testprocess.BlacklistedMessageError):
pyproc.ensure_not_logged(data='blacklisted', delay=0)

View File

@ -46,5 +46,19 @@ def test_httpbin(httpbin, qtbot, path, content, expected):
data = response.read().decode('utf-8') data = response.read().decode('utf-8')
assert httpbin.get_requests() == [httpbin.Request('GET', path)] assert httpbin.get_requests() == [httpbin.ExpectedRequest('GET', path)]
assert (content in data) == expected assert (content in data) == expected
@pytest.mark.parametrize('line, verb, path, equal', [
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
'GET', '/', True),
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
'GET', '/foo', False),
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
'POST', '/foo', False),
])
def test_expected_request(httpbin, line, verb, path, equal):
expected = httpbin.ExpectedRequest(verb, path)
request = httpbin.Request(line)
assert (expected == request) == equal

View File

@ -22,12 +22,13 @@
import re import re
import os import os
import time import time
import fnmatch
import pytestqt.plugin # pylint: disable=import-error import pytestqt.plugin # pylint: disable=import-error
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
from PyQt5.QtTest import QSignalSpy from PyQt5.QtTest import QSignalSpy
from helpers import utils # pylint: disable=import-error
class InvalidLine(Exception): class InvalidLine(Exception):
@ -48,6 +49,11 @@ class WaitForTimeout(Exception):
"""Raised when wait_for didn't get the expected message.""" """Raised when wait_for didn't get the expected message."""
class BlacklistedMessageError(Exception):
"""Raised when ensure_not_logged found a message."""
class Line: class Line:
"""Container for a line of data the process emits. """Container for a line of data the process emits.
@ -78,6 +84,7 @@ class Process(QObject):
ready = pyqtSignal() ready = pyqtSignal()
new_data = pyqtSignal(object) new_data = pyqtSignal(object)
KEYS = ['data']
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -138,6 +145,7 @@ class Process(QObject):
continue continue
if parsed is None: if parsed is None:
if self._invalid:
print("IGNORED: {}".format(line)) print("IGNORED: {}".format(line))
else: else:
self._data.append(parsed) self._data.append(parsed)
@ -198,7 +206,7 @@ class Process(QObject):
- If expected is None, the filter always matches. - If expected is None, the filter always matches.
- If the value is a string or bytes object and the expected value is - If the value is a string or bytes object and the expected value is
too, the pattern is treated as a fnmatch glob pattern. too, the pattern is treated as a glob pattern (with only * active).
- If the value is a string or bytes object and the expected value is a - If the value is a string or bytes object and the expected value is a
compiled regex, it is used for matching. compiled regex, it is used for matching.
- If the value is any other type, == is used. - If the value is any other type, == is used.
@ -212,25 +220,34 @@ class Process(QObject):
elif isinstance(expected, regex_type): elif isinstance(expected, regex_type):
return expected.match(value) return expected.match(value)
elif isinstance(value, (bytes, str)): elif isinstance(value, (bytes, str)):
return fnmatch.fnmatchcase(value, expected) return utils.pattern_match(pattern=expected, value=value)
else: else:
return value == expected return value == expected
def wait_for(self, timeout=None, **kwargs): def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs):
"""Wait until a given value is found in the data. """Wait until a given value is found in the data.
Keyword arguments to this function get interpreted as attributes of the Keyword arguments to this function get interpreted as attributes of the
searched data. Every given argument is treated as a pattern which searched data. Every given argument is treated as a pattern which
the attribute has to match against. the attribute has to match against.
Args:
timeout: How long to wait for the message.
override_waited_for: If set, gets triggered by previous messages
again.
Return: Return:
The matched line. The matched line.
""" """
__tracebackhide__ = True
if timeout is None: if timeout is None:
if 'CI' in os.environ: if 'CI' in os.environ:
timeout = 15000 timeout = 15000
else: else:
timeout = 5000 timeout = 5000
for key in kwargs:
assert key in self.KEYS
# Search existing messages # Search existing messages
for line in self._data: for line in self._data:
matches = [] matches = []
@ -239,7 +256,7 @@ class Process(QObject):
value = getattr(line, key) value = getattr(line, key)
matches.append(self._match_data(value, expected)) matches.append(self._match_data(value, expected))
if all(matches) and not line.waited_for: if all(matches) and (not line.waited_for or override_waited_for):
# If we waited for this line, chances are we don't mean the # If we waited for this line, chances are we don't mean the
# same thing the next time we use wait_for and it matches # same thing the next time we use wait_for and it matches
# this line again. # this line again.
@ -273,3 +290,18 @@ class Process(QObject):
# this line again. # this line again.
line.waited_for = True line.waited_for = True
return line return line
def ensure_not_logged(self, delay=500, **kwargs):
"""Make sure the data matching the given arguments is not logged.
If nothing is found in the log, we wait for delay ms to make sure
nothing arrives.
"""
__tracebackhide__ = True
try:
line = self.wait_for(timeout=delay, override_waited_for=True,
**kwargs)
except WaitForTimeout:
return
else:
raise BlacklistedMessageError(line)

View File

@ -26,7 +26,6 @@ import re
import sys import sys
import socket import socket
import os.path import os.path
import collections
import pytest import pytest
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal
@ -34,25 +33,17 @@ from PyQt5.QtCore import pyqtSignal
import testprocess # pylint: disable=import-error import testprocess # pylint: disable=import-error
Request = collections.namedtuple('Request', 'verb, path') class Request(testprocess.Line):
"""A parsed line from the httpbin/flask log output.
class HTTPBin(testprocess.Process): Attributes:
timestamp/verb/path/status: Parsed from the log output.
"""Abstraction over a running HTTPbin server process.
Reads the log from its stdout and parses it.
Class attributes: Class attributes:
LOG_RE: Used to parse the CLF log which httpbin outputs. LOG_RE: Used to parse the CLF log which httpbin outputs.
Signals:
new_request: Emitted when there's a new request received.
""" """
new_request = pyqtSignal(Request)
Request = Request # So it can be used from the fixture easily.
LOG_RE = re.compile(r""" LOG_RE = re.compile(r"""
(?P<host>[^ ]*) (?P<host>[^ ]*)
\ ([^ ]*) # ignored \ ([^ ]*) # ignored
@ -67,6 +58,69 @@ class HTTPBin(testprocess.Process):
\ (?P<size>[^ ]*) \ (?P<size>[^ ]*)
""", re.VERBOSE) """, re.VERBOSE)
def __init__(self, data):
super().__init__(data)
match = self.LOG_RE.match(data)
if match is None:
raise testprocess.InvalidLine(data)
assert match.group('host') == '127.0.0.1'
assert match.group('user') == '-'
self.timestamp = match.group('date')
self.verb = match.group('verb')
# FIXME do we need to allow other options?
assert match.group('protocol') == 'HTTP/1.1'
assert self.verb == 'GET'
self.path = match.group('path')
self.status = int(match.group('status'))
missing_paths = ['/favicon.ico', '/does-not-exist']
if self.path in missing_paths:
assert self.status == 404
else:
assert self.status < 400
assert match.group('size') == '-'
def __eq__(self, other):
return NotImplemented
class ExpectedRequest:
"""Class to compare expected requests easily."""
def __init__(self, verb, path):
self.verb = verb
self.path = path
def __eq__(self, other):
if isinstance(other, (Request, ExpectedRequest)):
return (self.verb == other.verb and
self.path == other.path)
else:
return NotImplemented
class HTTPBin(testprocess.Process):
"""Abstraction over a running HTTPbin server process.
Reads the log from its stdout and parses it.
Signals:
new_request: Emitted when there's a new request received.
"""
new_request = pyqtSignal(Request)
Request = Request # So it can be used from the fixture easily.
ExpectedRequest = ExpectedRequest
KEYS = ['verb', 'path']
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.port = self._get_port() self.port = self._get_port()
@ -91,14 +145,7 @@ class HTTPBin(testprocess.Process):
'quit)'.format(self.port)): 'quit)'.format(self.port)):
self.ready.emit() self.ready.emit()
return None return None
return Request(line)
match = self.LOG_RE.match(line)
if match is None:
raise testprocess.InvalidLine
# FIXME do we need to allow other options?
assert match.group('protocol') == 'HTTP/1.1'
return Request(verb=match.group('verb'), path=match.group('path'))
def _executable_args(self): def _executable_args(self):
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):

View File

@ -55,7 +55,7 @@ class HeaderChecker:
"""Check if the passed header is ignored.""" """Check if the passed header is ignored."""
reply = self.stubs.FakeNetworkReply( reply = self.stubs.FakeNetworkReply(
headers={'Content-Disposition': header}) headers={'Content-Disposition': header})
with self.caplog.atLevel(logging.ERROR, 'rfc6266'): with self.caplog.at_level(logging.ERROR, 'rfc6266'):
# with self.assertLogs(log.rfc6266, logging.ERROR): # with self.assertLogs(log.rfc6266, logging.ERROR):
cd_inline, cd_filename = http.parse_content_disposition(reply) cd_inline, cd_filename = http.parse_content_disposition(reply)
assert cd_filename == DEFAULT_NAME assert cd_filename == DEFAULT_NAME

View File

@ -41,7 +41,7 @@ def test_parse_content_disposition(caplog, template, stubs, s):
"""Test parsing headers based on templates which hypothesis completes.""" """Test parsing headers based on templates which hypothesis completes."""
header = template.format(s) header = template.format(s)
reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header}) reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header})
with caplog.atLevel(logging.ERROR, 'rfc6266'): with caplog.at_level(logging.ERROR, 'rfc6266'):
http.parse_content_disposition(reply) http.parse_content_disposition(reply)

View File

@ -147,7 +147,7 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
disk_cache = cache.DiskCache(str(tmpdir)) disk_cache = cache.DiskCache(str(tmpdir))
cache_file = disk_cache.fileMetaData("nosuchfile") cache_file = disk_cache.fileMetaData("nosuchfile")
assert cache_file.isValid() == False assert not cache_file.isValid()
def test_cache_deactivated_metadata_file(config_stub, tmpdir): def test_cache_deactivated_metadata_file(config_stub, tmpdir):
@ -207,7 +207,7 @@ def test_cache_deactivated_remove_data(config_stub, tmpdir):
disk_cache = cache.DiskCache(str(tmpdir)) disk_cache = cache.DiskCache(str(tmpdir))
url = QUrl('http://www.example.com/') url = QUrl('http://www.example.com/')
assert disk_cache.remove(url) == False assert not disk_cache.remove(url)
def test_cache_insert_data(config_stub, tmpdir): def test_cache_insert_data(config_stub, tmpdir):

View File

@ -106,13 +106,12 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
tabbed_browser.current_index = 0 tabbed_browser.current_index = 0
tabbed_browser.index_of = index_of tabbed_browser.index_of = index_of
with caplog.atLevel(logging.DEBUG, logger='signals'): with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.signal.emit('foo') objects.signaller.signal.emit('foo')
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1
expected_msg = "{}: filtered_signal('foo') (tab {})".format(verb, index_of) expected_msg = "{}: filtered_signal('foo') (tab {})".format(verb, index_of)
assert records[0].msg == expected_msg assert caplog.records[0].msg == expected_msg
@pytest.mark.parametrize('index_of', [0, 1]) @pytest.mark.parametrize('index_of', [0, 1])
@ -120,10 +119,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
tabbed_browser.current_index = 0 tabbed_browser.current_index = 0
tabbed_browser.index_of = index_of tabbed_browser.index_of = index_of
with caplog.atLevel(logging.DEBUG, logger='signals'): with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.statusbar_message.emit('foo') objects.signaller.statusbar_message.emit('foo')
assert not caplog.records() assert not caplog.records
def test_runtime_error(objects, tabbed_browser): def test_runtime_error(objects, tabbed_browser):

View File

@ -42,3 +42,12 @@ class TestCommandRunner:
else: else:
with pytest.raises(cmdexc.NoSuchCommandError): with pytest.raises(cmdexc.NoSuchCommandError):
list(cr.parse_all(cmdline_test.cmd, aliases=False)) list(cr.parse_all(cmdline_test.cmd, aliases=False))
def test_parse_with_count(self):
"""Test parsing of commands with a count."""
cr = runners.CommandRunner(0)
result = cr.parse('20:scroll down', aliases=False)
assert result.cmd.name == 'scroll'
assert result.count == 20
assert result.args == ['down']
assert result.cmdline == ['scroll', 'down']

View File

@ -64,12 +64,11 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}} config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
obj = Obj("{{ font['foo'] }}") obj = Obj("{{ font['foo'] }}")
with caplog.atLevel(9): # VDEBUG with caplog.at_level(9): # VDEBUG
style.set_register_stylesheet(obj) style.set_register_stylesheet(obj)
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1 assert caplog.records[0].message == 'stylesheet for Obj: bar'
assert records[0].message == 'stylesheet for Obj: bar'
assert obj.rendered_stylesheet == 'bar' assert obj.rendered_stylesheet == 'bar'
@ -104,11 +103,10 @@ class TestColorDict:
def test_key_error(self, caplog): def test_key_error(self, caplog):
d = style.ColorDict() d = style.ColorDict()
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
d['foo'] # pylint: disable=pointless-statement d['foo'] # pylint: disable=pointless-statement
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1 assert caplog.records[0].message == 'No color defined for foo!'
assert records[0].message == 'No color defined for foo!'
def test_qcolor(self): def test_qcolor(self):
d = style.ColorDict() d = style.ColorDict()

View File

@ -74,14 +74,14 @@ class TestDebugLog:
def test_log(self, keyparser, caplog): def test_log(self, keyparser, caplog):
keyparser._debug_log('foo') keyparser._debug_log('foo')
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
record = caplog.records()[0] record = caplog.records[0]
assert record.message == 'foo' assert record.message == 'foo'
def test_no_log(self, keyparser, caplog): def test_no_log(self, keyparser, caplog):
keyparser.do_log = False keyparser.do_log = False
keyparser._debug_log('foo') keyparser._debug_log('foo')
assert not caplog.records() assert not caplog.records
@pytest.mark.parametrize('input_key, supports_count, expected', [ @pytest.mark.parametrize('input_key, supports_count, expected', [
@ -161,10 +161,10 @@ class TestReadConfig:
0, supports_count=False, supports_chains=False) 0, supports_count=False, supports_chains=False)
kp._warn_on_keychains = warn_on_keychains kp._warn_on_keychains = warn_on_keychains
with caplog.atLevel(logging.WARNING): with caplog.at_level(logging.WARNING):
kp.read_config('normal') kp.read_config('normal')
assert bool(caplog.records()) == warn_on_keychains assert bool(caplog.records) == warn_on_keychains
class TestSpecialKeys: class TestSpecialKeys:

View File

@ -174,16 +174,16 @@ def test_start_logging(fake_proc, caplog):
"""Make sure that starting logs the executed commandline.""" """Make sure that starting logs the executed commandline."""
cmd = 'does_not_exist' cmd = 'does_not_exist'
args = ['arg', 'arg with spaces'] args = ['arg', 'arg with spaces']
with caplog.atLevel(logging.DEBUG): with caplog.at_level(logging.DEBUG):
fake_proc.start(cmd, args) fake_proc.start(cmd, args)
msgs = [e.msg for e in caplog.records()] msgs = [e.msg for e in caplog.records]
assert msgs == ["Starting process.", assert msgs == ["Starting process.",
"Executing: does_not_exist arg 'arg with spaces'"] "Executing: does_not_exist arg 'arg with spaces'"]
def test_error(qtbot, proc, caplog, guiprocess_message_mock): def test_error(qtbot, proc, caplog, guiprocess_message_mock):
"""Test the process emitting an error.""" """Test the process emitting an error."""
with caplog.atLevel(logging.ERROR, 'message'): with caplog.at_level(logging.ERROR, 'message'):
with qtbot.waitSignal(proc.error, raising=True, timeout=5000): with qtbot.waitSignal(proc.error, raising=True, timeout=5000):
proc.start('this_does_not_exist_either', []) proc.start('this_does_not_exist_either', [])

View File

@ -358,11 +358,10 @@ class TestListen:
@pytest.mark.posix @pytest.mark.posix
def test_atime_update_no_name(self, qtbot, caplog, ipc_server): def test_atime_update_no_name(self, qtbot, caplog, ipc_server):
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
ipc_server.update_atime() ipc_server.update_atime()
records = caplog.records() assert caplog.records[-1].msg == "In update_atime with no server path!"
assert records[-1].msg == "In update_atime with no server path!"
@pytest.mark.posix @pytest.mark.posix
def test_atime_shutdown_typeerror(self, qtbot, ipc_server): def test_atime_shutdown_typeerror(self, qtbot, ipc_server):
@ -408,22 +407,21 @@ class TestHandleConnection:
def test_no_connection(self, ipc_server, caplog): def test_no_connection(self, ipc_server, caplog):
ipc_server.handle_connection() ipc_server.handle_connection()
record = caplog.records()[-1] assert caplog.records[-1].message == "No new connection to handle."
assert record.message == "No new connection to handle."
def test_double_connection(self, qlocalsocket, ipc_server, caplog): def test_double_connection(self, qlocalsocket, ipc_server, caplog):
ipc_server._socket = qlocalsocket ipc_server._socket = qlocalsocket
ipc_server.handle_connection() ipc_server.handle_connection()
message = ("Got new connection but ignoring it because we're still " message = ("Got new connection but ignoring it because we're still "
"handling another one.") "handling another one.")
assert message in [rec.message for rec in caplog.records()] assert message in [rec.message for rec in caplog.records]
def test_disconnected_immediately(self, ipc_server, caplog): def test_disconnected_immediately(self, ipc_server, caplog):
socket = FakeSocket(state=QLocalSocket.UnconnectedState) socket = FakeSocket(state=QLocalSocket.UnconnectedState)
ipc_server._server = FakeServer(socket) ipc_server._server = FakeServer(socket)
ipc_server.handle_connection() ipc_server.handle_connection()
msg = "Socket was disconnected immediately." msg = "Socket was disconnected immediately."
all_msgs = [r.message for r in caplog.records()] all_msgs = [r.message for r in caplog.records]
assert msg in all_msgs assert msg in all_msgs
def test_error_immediately(self, ipc_server, caplog): def test_error_immediately(self, ipc_server, caplog):
@ -436,7 +434,7 @@ class TestHandleConnection:
exc_msg = 'Error while handling IPC connection: Error string (error 7)' exc_msg = 'Error while handling IPC connection: Error string (error 7)'
assert str(excinfo.value) == exc_msg assert str(excinfo.value) == exc_msg
msg = "We got an error immediately." msg = "We got an error immediately."
all_msgs = [r.message for r in caplog.records()] all_msgs = [r.message for r in caplog.records]
assert msg in all_msgs assert msg in all_msgs
def test_read_line_immediately(self, qtbot, ipc_server, caplog): def test_read_line_immediately(self, qtbot, ipc_server, caplog):
@ -454,7 +452,7 @@ class TestHandleConnection:
assert spy[0][0] == ['foo'] assert spy[0][0] == ['foo']
assert spy[0][1] == 'tab' assert spy[0][1] == 'tab'
all_msgs = [r.message for r in caplog.records()] all_msgs = [r.message for r in caplog.records]
assert "We can read a line immediately." in all_msgs assert "We can read a line immediately." in all_msgs
@ -505,11 +503,11 @@ def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
got_args_spy = QSignalSpy(ipc_server.got_args) got_args_spy = QSignalSpy(ipc_server.got_args)
signals = [ipc_server.got_invalid_data, connected_socket.disconnected] signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignals(signals, raising=True): with qtbot.waitSignals(signals, raising=True):
connected_socket.write(data) connected_socket.write(data)
messages = [r.message for r in caplog.records()] messages = [r.message for r in caplog.records]
assert messages[-1] == 'Ignoring invalid IPC data.' assert messages[-1] == 'Ignoring invalid IPC data.'
assert messages[-2].startswith(msg) assert messages[-2].startswith(msg)
assert not got_args_spy assert not got_args_spy
@ -542,11 +540,11 @@ class TestSendToRunningInstance:
def test_no_server(self, caplog): def test_no_server(self, caplog):
sent = ipc.send_to_running_instance('qute-test', [], None) sent = ipc.send_to_running_instance('qute-test', [], None)
assert not sent assert not sent
msg = caplog.records()[-1].message msg = caplog.records[-1].message
assert msg == "No existing instance present (error 2)" assert msg == "No existing instance present (error 2)"
@pytest.mark.parametrize('has_cwd', [True, False]) @pytest.mark.parametrize('has_cwd', [True, False])
@pytest.mark.posix(reason="Causes random trouble on Windows") @pytest.mark.linux(reason="Causes random trouble on Windows and OS X")
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd): def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
ipc_server.listen() ipc_server.listen()
spy = QSignalSpy(ipc_server.got_args) spy = QSignalSpy(ipc_server.got_args)
@ -610,12 +608,12 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True): with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
qlocalsocket.connectToServer('qute-test') qlocalsocket.connectToServer('qute-test')
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True, with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
timeout=5000): timeout=5000):
pass pass
assert caplog.records()[-1].message == "IPC connection timed out." assert caplog.records[-1].message == "IPC connection timed out."
@pytest.mark.parametrize('method, args, is_warning', [ @pytest.mark.parametrize('method, args, is_warning', [
@ -628,13 +626,13 @@ def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
assert ipc_server._socket is None assert ipc_server._socket is None
if is_warning: if is_warning:
with caplog.atLevel(logging.WARNING): with caplog.at_level(logging.WARNING):
func(*args) func(*args)
else: else:
func(*args) func(*args)
msg = "In {} with None socket!".format(method) msg = "In {} with None socket!".format(method)
assert msg in [r.message for r in caplog.records()] assert msg in [r.message for r in caplog.records]
class TestSendOrListen: class TestSendOrListen:
@ -683,7 +681,7 @@ class TestSendOrListen:
def test_normal_connection(self, caplog, qtbot, args): def test_normal_connection(self, caplog, qtbot, args):
ret_server = ipc.send_or_listen(args) ret_server = ipc.send_or_listen(args)
assert isinstance(ret_server, ipc.IPCServer) assert isinstance(ret_server, ipc.IPCServer)
msgs = [e.message for e in caplog.records()] msgs = [e.message for e in caplog.records]
assert "Starting IPC server..." in msgs assert "Starting IPC server..." in msgs
objreg_server = objreg.get('ipc-server') objreg_server = objreg.get('ipc-server')
assert objreg_server is ret_server assert objreg_server is ret_server
@ -698,7 +696,7 @@ class TestSendOrListen:
with qtbot.waitSignal(legacy_server.got_args, raising=True): with qtbot.waitSignal(legacy_server.got_args, raising=True):
ret = ipc.send_or_listen(args) ret = ipc.send_or_listen(args)
assert ret is None assert ret is None
msgs = [e.message for e in caplog.records()] msgs = [e.message for e in caplog.records]
assert "Connecting to {}".format(legacy_server._socketname) in msgs assert "Connecting to {}".format(legacy_server._socketname) in msgs
@pytest.mark.posix(reason="Unneeded on Windows") @pytest.mark.posix(reason="Unneeded on Windows")
@ -775,7 +773,7 @@ class TestSendOrListen:
ret = ipc.send_or_listen(args) ret = ipc.send_or_listen(args)
assert ret is None assert ret is None
msgs = [e.message for e in caplog.records()] msgs = [e.message for e in caplog.records]
assert "Got AddressInUseError, trying again." in msgs assert "Got AddressInUseError, trying again." in msgs
@pytest.mark.parametrize('has_error, exc_name, exc_msg', [ @pytest.mark.parametrize('has_error, exc_name, exc_msg', [
@ -812,12 +810,11 @@ class TestSendOrListen:
QLocalSocket.ConnectionRefusedError, # error() gets called twice QLocalSocket.ConnectionRefusedError, # error() gets called twice
] ]
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
with pytest.raises(ipc.Error): with pytest.raises(ipc.Error):
ipc.send_or_listen(args) ipc.send_or_listen(args)
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1
error_msgs = [ error_msgs = [
'Handling fatal misc.ipc.{} with --no-err-windows!'.format( 'Handling fatal misc.ipc.{} with --no-err-windows!'.format(
@ -828,7 +825,7 @@ class TestSendOrListen:
'post_text: Maybe another instance is running but frozen?', 'post_text: Maybe another instance is running but frozen?',
'exception text: {}'.format(exc_msg), 'exception text: {}'.format(exc_msg),
] ]
assert records[0].msg == '\n'.join(error_msgs) assert caplog.records[0].msg == '\n'.join(error_msgs)
@pytest.mark.posix(reason="Flaky on Windows") @pytest.mark.posix(reason="Flaky on Windows")
def test_error_while_listening(self, qlocalserver_mock, caplog, args): def test_error_while_listening(self, qlocalserver_mock, caplog, args):
@ -837,12 +834,11 @@ class TestSendOrListen:
err = QAbstractSocket.SocketResourceError err = QAbstractSocket.SocketResourceError
qlocalserver_mock().serverError.return_value = err qlocalserver_mock().serverError.return_value = err
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
with pytest.raises(ipc.Error): with pytest.raises(ipc.Error):
ipc.send_or_listen(args) ipc.send_or_listen(args)
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1
error_msgs = [ error_msgs = [
'Handling fatal misc.ipc.ListenError with --no-err-windows!', 'Handling fatal misc.ipc.ListenError with --no-err-windows!',
@ -853,7 +849,7 @@ class TestSendOrListen:
'exception text: Error while listening to IPC server: Error ' 'exception text: Error while listening to IPC server: Error '
'string (error 4)', 'string (error 4)',
] ]
assert records[0].msg == '\n'.join(error_msgs) assert caplog.records[0].msg == '\n'.join(error_msgs)
@pytest.mark.windows @pytest.mark.windows

View File

@ -821,10 +821,9 @@ class TestSessionDelete:
tmpdir.chmod(0o555) # unwritable tmpdir.chmod(0o555) # unwritable
with pytest.raises(cmdexc.CommandError) as excinfo: with pytest.raises(cmdexc.CommandError) as excinfo:
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
sess_man.session_delete('foo') sess_man.session_delete('foo')
assert str(excinfo.value).startswith('Error while deleting session: ') assert str(excinfo.value).startswith('Error while deleting session: ')
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1 assert caplog.records[0].message == 'Error while deleting session!'
assert records[0].message == 'Error while deleting session!'

View File

@ -51,7 +51,8 @@ class CovtestHelper:
coveragerc = str(self._testdir.tmpdir / 'coveragerc') coveragerc = str(self._testdir.tmpdir / 'coveragerc')
return self._testdir.runpytest('--cov=module', return self._testdir.runpytest('--cov=module',
'--cov-config={}'.format(coveragerc), '--cov-config={}'.format(coveragerc),
'--cov-report=xml') '--cov-report=xml',
plugins=['no:faulthandler'])
def check(self, perfect_files=None): def check(self, perfect_files=None):
"""Run check_coverage.py and run its return value.""" """Run check_coverage.py and run its return value."""
@ -165,6 +166,7 @@ def test_tested_unlisted(covtest):
@pytest.mark.parametrize('args, reason', [ @pytest.mark.parametrize('args, reason', [
(['-k', 'foo'], "because -k is given."), (['-k', 'foo'], "because -k is given."),
(['-m', 'foo'], "because -m is given."), (['-m', 'foo'], "because -m is given."),
(['--lf'], "because --lf is given."),
(['blah', '-m', 'foo'], "because -m is given."), (['blah', '-m', 'foo'], "because -m is given."),
(['tests/foo'], "because there is nothing to check."), (['tests/foo'], "because there is nothing to check."),
]) ])

View File

@ -41,9 +41,8 @@ def test_log_events(qapp, caplog):
obj = EventObject() obj = EventObject()
qapp.postEvent(obj, QEvent(QEvent.User)) qapp.postEvent(obj, QEvent(QEvent.User))
qapp.processEvents() qapp.processEvents()
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1 assert caplog.records[0].msg == 'Event in test_debug.EventObject: User'
assert records[0].msg == 'Event in test_debug.EventObject: User'
class SignalObject(QObject): class SignalObject(QObject):
@ -75,10 +74,9 @@ def test_log_signals(caplog, signal_obj):
signal_obj.signal1.emit() signal_obj.signal1.emit()
signal_obj.signal2.emit('foo', 'bar') signal_obj.signal2.emit('foo', 'bar')
records = caplog.records() assert len(caplog.records) == 2
assert len(records) == 2 assert caplog.records[0].msg == 'Signal in <repr>: signal1()'
assert records[0].msg == 'Signal in <repr>: signal1()' assert caplog.records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
assert records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
class TestLogTime: class TestLogTime:
@ -86,15 +84,14 @@ class TestLogTime:
def test_duration(self, caplog): def test_duration(self, caplog):
logger_name = 'qt-tests' logger_name = 'qt-tests'
with caplog.atLevel(logging.DEBUG, logger_name): with caplog.at_level(logging.DEBUG, logger_name):
with debug.log_time(logger_name, action='foobar'): with debug.log_time(logger_name, action='foobar'):
time.sleep(0.1) time.sleep(0.1)
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$') pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(records[0].msg) match = pattern.match(caplog.records[0].msg)
assert match assert match
duration = float(match.group(1)) duration = float(match.group(1))
@ -104,11 +101,11 @@ class TestLogTime:
"""Test with an explicit logger instead of a name.""" """Test with an explicit logger instead of a name."""
logger_name = 'qt-tests' logger_name = 'qt-tests'
with caplog.atLevel(logging.DEBUG, logger_name): with caplog.at_level(logging.DEBUG, logger_name):
with debug.log_time(logging.getLogger(logger_name)): with debug.log_time(logging.getLogger(logger_name)):
pass pass
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
def test_decorator(self, caplog): def test_decorator(self, caplog):
logger_name = 'qt-tests' logger_name = 'qt-tests'
@ -118,12 +115,11 @@ class TestLogTime:
assert arg == 1 assert arg == 1
assert kwarg == 2 assert kwarg == 2
with caplog.atLevel(logging.DEBUG, logger_name): with caplog.at_level(logging.DEBUG, logger_name):
func(1, kwarg=2) func(1, kwarg=2)
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1 assert caplog.records[0].msg.startswith('Foo took')
assert records[0].msg.startswith('Foo took')
class TestQEnumKey: class TestQEnumKey:

View File

@ -52,12 +52,11 @@ def test_no_err_windows(caplog, exc, name, exc_text):
try: try:
raise exc raise exc
except Exception as e: except Exception as e:
with caplog.atLevel(logging.ERROR): with caplog.at_level(logging.ERROR):
error.handle_fatal_exc(e, Args(no_err_windows=True), 'title', error.handle_fatal_exc(e, Args(no_err_windows=True), 'title',
pre_text='pre', post_text='post') pre_text='pre', post_text='post')
records = caplog.records() assert len(caplog.records) == 1
assert len(records) == 1
expected = [ expected = [
'Handling fatal {} with --no-err-windows!'.format(name), 'Handling fatal {} with --no-err-windows!'.format(name),
@ -67,7 +66,7 @@ def test_no_err_windows(caplog, exc, name, exc_text):
'post_text: post', 'post_text: post',
'exception text: {}'.format(exc_text), 'exception text: {}'.format(exc_text),
] ]
assert records[0].msg == '\n'.join(expected) assert caplog.records[0].msg == '\n'.join(expected)
# This happens on Xvfb for some reason # This happens on Xvfb for some reason

View File

@ -25,7 +25,7 @@ import itertools
import sys import sys
import pytest import pytest
import pytest_capturelog # pylint: disable=import-error import pytest_catchlog # pylint: disable=import-error
from qutebrowser.utils import log from qutebrowser.utils import log
@ -60,10 +60,11 @@ def restore_loggers():
while root_logger.handlers: while root_logger.handlers:
h = root_logger.handlers[0] h = root_logger.handlers[0]
root_logger.removeHandler(h) root_logger.removeHandler(h)
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
h.close() h.close()
root_logger.setLevel(original_logging_level) root_logger.setLevel(original_logging_level)
for h in root_handlers: for h in root_handlers:
if not isinstance(h, pytest_capturelog.CaptureLogHandler): if not isinstance(h, pytest_catchlog.LogCaptureHandler):
# https://github.com/The-Compiler/qutebrowser/issues/856 # https://github.com/The-Compiler/qutebrowser/issues/856
root_logger.addHandler(h) root_logger.addHandler(h)
logging._acquireLock() logging._acquireLock()
@ -238,30 +239,30 @@ class TestHideQtWarning:
def test_unfiltered(self, logger, caplog): def test_unfiltered(self, logger, caplog):
"""Test a message which is not filtered.""" """Test a message which is not filtered."""
with log.hide_qt_warning("World", 'qt-tests'): with log.hide_qt_warning("World", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'): with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning("Hello World") logger.warning("Hello World")
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
record = caplog.records()[0] record = caplog.records[0]
assert record.levelname == 'WARNING' assert record.levelname == 'WARNING'
assert record.message == "Hello World" assert record.message == "Hello World"
def test_filtered_exact(self, logger, caplog): def test_filtered_exact(self, logger, caplog):
"""Test a message which is filtered (exact match).""" """Test a message which is filtered (exact match)."""
with log.hide_qt_warning("Hello", 'qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'): with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning("Hello") logger.warning("Hello")
assert not caplog.records() assert not caplog.records
def test_filtered_start(self, logger, caplog): def test_filtered_start(self, logger, caplog):
"""Test a message which is filtered (match at line start).""" """Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", 'qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'): with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning("Hello World") logger.warning("Hello World")
assert not caplog.records() assert not caplog.records
def test_filtered_whitespace(self, logger, caplog): def test_filtered_whitespace(self, logger, caplog):
"""Test a message which is filtered (match with whitespace).""" """Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", 'qt-tests'): with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'): with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning(" Hello World ") logger.warning(" Hello World ")
assert not caplog.records() assert not caplog.records

View File

@ -263,10 +263,10 @@ class TestInitCacheDirTag:
monkeypatch.setattr('qutebrowser.utils.standarddir.cache', monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir)) lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError) mocker.patch('builtins.open', side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'init'): with caplog.at_level(logging.ERROR, 'init'):
standarddir._init_cachedir_tag() standarddir._init_cachedir_tag()
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG' assert caplog.records[0].message == 'Failed to create CACHEDIR.TAG'
assert not tmpdir.listdir() assert not tmpdir.listdir()

View File

@ -241,11 +241,11 @@ class TestActuteWarning:
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError, mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
create=True) create=True)
with caplog.atLevel(logging.ERROR, 'init'): with caplog.at_level(logging.ERROR, 'init'):
utils.actute_warning() utils.actute_warning()
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
assert caplog.records()[0].message == 'Failed to read Compose file' assert caplog.records[0].message == 'Failed to read Compose file'
out, _err = capsys.readouterr() out, _err = capsys.readouterr()
assert not out assert not out
@ -427,8 +427,7 @@ class TestFormatSize:
class TestKeyToString: class TestKeyToString:
KEYS = [(k, v) for k, v in sorted(vars(Qt).items()) """Test key_to_string."""
if isinstance(v, Qt.Key)]
@pytest.mark.parametrize('key, expected', [ @pytest.mark.parametrize('key, expected', [
(Qt.Key_Blue, 'Blue'), (Qt.Key_Blue, 'Blue'),
@ -449,11 +448,13 @@ class TestKeyToString:
# want to know if the mapping still behaves properly. # want to know if the mapping still behaves properly.
assert utils.key_to_string(Qt.Key_A) == 'A' assert utils.key_to_string(Qt.Key_A) == 'A'
@pytest.mark.parametrize('key', [e[1] for e in KEYS], def test_all(self):
ids=[e[0] for e in KEYS])
def test_all(self, key):
"""Make sure there's some sensible output for all keys.""" """Make sure there's some sensible output for all keys."""
string = utils.key_to_string(key) for name, value in sorted(vars(Qt).items()):
if not isinstance(value, Qt.Key):
continue
print(name)
string = utils.key_to_string(value)
assert string assert string
string.encode('utf-8') # make sure it's encodable string.encode('utf-8') # make sure it's encodable
@ -669,12 +670,12 @@ class TestPreventExceptions:
def test_raising(self, caplog): def test_raising(self, caplog):
"""Test with a raising function.""" """Test with a raising function."""
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
ret = self.func_raising() ret = self.func_raising()
assert ret == 42 assert ret == 42
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
expected = 'Error in test_utils.TestPreventExceptions.func_raising' expected = 'Error in test_utils.TestPreventExceptions.func_raising'
actual = caplog.records()[0].message actual = caplog.records[0].message
assert actual == expected assert actual == expected
@utils.prevent_exceptions(42) @utils.prevent_exceptions(42)
@ -683,10 +684,10 @@ class TestPreventExceptions:
def test_not_raising(self, caplog): def test_not_raising(self, caplog):
"""Test with a non-raising function.""" """Test with a non-raising function."""
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
ret = self.func_not_raising() ret = self.func_not_raising()
assert ret == 23 assert ret == 23
assert not caplog.records() assert not caplog.records
@utils.prevent_exceptions(42, True) @utils.prevent_exceptions(42, True)
def func_predicate_true(self): def func_predicate_true(self):
@ -694,10 +695,10 @@ class TestPreventExceptions:
def test_predicate_true(self, caplog): def test_predicate_true(self, caplog):
"""Test with a True predicate.""" """Test with a True predicate."""
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
ret = self.func_predicate_true() ret = self.func_predicate_true()
assert ret == 42 assert ret == 42
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
@utils.prevent_exceptions(42, False) @utils.prevent_exceptions(42, False)
def func_predicate_false(self): def func_predicate_false(self):
@ -705,10 +706,10 @@ class TestPreventExceptions:
def test_predicate_false(self, caplog): def test_predicate_false(self, caplog):
"""Test with a False predicate.""" """Test with a False predicate."""
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
with pytest.raises(Exception): with pytest.raises(Exception):
self.func_predicate_false() self.func_predicate_false()
assert not caplog.records() assert not caplog.records
class Obj: class Obj:

View File

@ -108,7 +108,7 @@ class TestGitStr:
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
raising=False) raising=False)
commit_file_mock.side_effect = OSError commit_file_mock.side_effect = OSError
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
assert version._git_str() is None assert version._git_str() is None
@pytest.mark.not_frozen @pytest.mark.not_frozen
@ -136,7 +136,7 @@ class TestGitStr:
m.path.join.side_effect = OSError m.path.join.side_effect = OSError
mocker.patch('qutebrowser.utils.version.utils.read_file', mocker.patch('qutebrowser.utils.version.utils.read_file',
side_effect=OSError) side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
assert version._git_str() is None assert version._git_str() is None
@pytest.mark.not_frozen @pytest.mark.not_frozen
@ -145,10 +145,10 @@ class TestGitStr:
"""Test with undefined __file__ but available git-commit-id.""" """Test with undefined __file__ but available git-commit-id."""
monkeypatch.delattr('qutebrowser.utils.version.__file__') monkeypatch.delattr('qutebrowser.utils.version.__file__')
commit_file_mock.return_value = '0deadcode' commit_file_mock.return_value = '0deadcode'
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
assert version._git_str() == '0deadcode' assert version._git_str() == '0deadcode'
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
assert caplog.records()[0].message == "Error while getting git path" assert caplog.records[0].message == "Error while getting git path"
def _has_git(): def _has_git():
@ -294,11 +294,11 @@ def test_release_info(files, expected, caplog, monkeypatch):
fake = ReleaseInfoFake(files) fake = ReleaseInfoFake(files)
monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake) monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake)
monkeypatch.setattr(version, 'open', fake.open_fake, raising=False) monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
assert version._release_info() == expected assert version._release_info() == expected
if files is None: if files is None:
assert len(caplog.records()) == 1 assert len(caplog.records) == 1
assert caplog.records()[0].message == "Error while reading fake-file." assert caplog.records[0].message == "Error while reading fake-file."
class ImportFake: class ImportFake:

View File

@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog):
"""Test Question.abort() with .emit() raising a TypeError.""" """Test Question.abort() with .emit() raising a TypeError."""
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted') signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
signal_mock.emit.side_effect = TypeError signal_mock.emit.side_effect = TypeError
with caplog.atLevel(logging.ERROR, 'misc'): with caplog.at_level(logging.ERROR, 'misc'):
question.abort() question.abort()
assert caplog.records()[0].message == 'Error while aborting question' assert caplog.records[0].message == 'Error while aborting question'

13
tox.ini
View File

@ -26,11 +26,11 @@ deps =
parse==1.6.6 parse==1.6.6
parse-type==0.3.4 parse-type==0.3.4
py==1.4.30 py==1.4.30
pytest==2.7.3 # rq.filter: <2.8.0 pytest==2.8.2
pytest-bdd==2.15.0 pytest-bdd==2.15.0
pytest-capturelog==0.7 pytest-catchlog==1.2.0
pytest-cov==2.2.0 pytest-cov==2.2.0
pytest-faulthandler==1.0.1 pytest-faulthandler==1.1.0
pytest-html==1.7 pytest-html==1.7
pytest-mock==0.8.1 pytest-mock==0.8.1
pytest-qt==1.9.0 pytest-qt==1.9.0
@ -39,12 +39,12 @@ deps =
six==1.10.0 six==1.10.0
termcolor==1.1.0 termcolor==1.1.0
vulture==0.8.1 vulture==0.8.1
Werkzeug==0.11.1 Werkzeug==0.11.2
wheel==0.26.0 wheel==0.26.0
xvfbwrapper==0.2.5 xvfbwrapper==0.2.5
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEsw --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} {envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
{envpython} scripts/dev/check_coverage.py {posargs} {envpython} scripts/dev/check_coverage.py {posargs}
[testenv:mkvenv] [testenv:mkvenv]
@ -105,6 +105,7 @@ deps =
astroid==1.3.8 astroid==1.3.8
pylint==1.4.4 pylint==1.4.4
logilab-common==1.1.0 logilab-common==1.1.0
requests==2.8.1
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF {envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
@ -204,7 +205,7 @@ basepython = python3
skip_install = true skip_install = true
passenv = passenv =
deps = deps =
check-manifest==0.27 check-manifest==0.28
commands = commands =
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'

17
www/header.asciidoc Normal file
View File

@ -0,0 +1,17 @@
+++
<div id="headline">
<img class="qutebrowser-logo" src="icons/qutebrowser.svg" />
<div class="text">
<h1>qutebrowser</h1>
A keyboard-driven browser.
</div>
</div>
<div id="menu">
<a href="index.html">Home</a>
<a href="FAQ.html">FAQ</a>
<a href="INSTALL.html">Install</a>
<a href="CHANGELOG.html">Changelog</a>
<a href="CONTRIBUTING.html">Contributing</a>
<a href="https://www.github.com/The-Compiler/qutebrowser">GitHub</a>
</div>
+++

Binary file not shown.

View File

@ -0,0 +1,255 @@
License notice for both OpenSans font files
===========================================
Open Sans font by https://www.google.com/fonts/specimen/Open+Sans[Google], licensed under the http://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0] license.
[options="header"]
|=================================================================================
|File |Copyright
|`OpenSans-Regular.woff2` |Digitized data copyright 2010-2011, Google Corporation.
|`OpenSans-Bold.woff2` |Digitized data copyright 2010-2011, Google Corporation.
|=================================================================================
MANIFEST.json
-------------
Here's an excerpt of the https://github.com/google/fonts/blob/master/apache/opensans/METADATA.json[MANIFEST.json] file
from https://github.com/google/fonts/blob/master/apache/opensans/[the offical repository]:
----
{
"name": "Open Sans",
"designer": "Steve Matteson",
"license": "Apache2",
"visibility": "External",
"category": "Sans Serif",
"size": 113987,
"fonts": [
{
"name": "Open Sans",
"style": "normal",
"weight": 400,
"filename": "OpenSans-Regular.ttf",
"postScriptName": "OpenSans",
"fullName": "Open Sans",
"copyright": "Digitized data copyright 2010-2011, Google Corporation."
},
"name": "Open Sans",
"style": "normal",
"weight": 700,
"filename": "OpenSans-Bold.ttf",
"postScriptName": "OpenSans-Bold",
"fullName": "Open Sans Bold",
"copyright": "Digitized data copyright 2010-2011, Google Corporation."
}
]
}
----
The Apache 2.0 License text
---------------------------
----
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
----

Binary file not shown.

BIN
www/media/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

11
www/media/font.css Normal file
View File

@ -0,0 +1,11 @@
@font-face {
font-family: "Open Sans";
font-weight: normal;
src: url(OpenSans-Regular.woff2) format("woff2");
}
@font-face {
font-family: 'Open Sans';
font-weight: bold;
src: url(OpenSans-Bold.woff2) format("woff2");
}

211
www/media/qutebrowser.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

181
www/qute.css Normal file
View File

@ -0,0 +1,181 @@
* {
margin: 0px 0px;
padding: 0px 0px;
}
body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-text-size-adjust: none;
color: #333333;
}
#header {
display: none;
}
#headline {
background-color: #333333;
padding: 20px 20px;
overflow: auto;
color: #888;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
#headline .qutebrowser-logo {
display: block;
height: 70px;
float: left;
}
#headline .text {
float: right;
text-align: right;
}
#headline .text h1 {
color: #1e89c6;
border: none;
}
#headline .text {
color: #666666;
}
#menu {
padding: 0px 20px;
background-color: #555555;
color: #CCC;
overflow: auto;
margin-bottom: 10px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
#menu a {
color: #CCC;
text-decoration: none;
background-color: #555555;
padding: 10px 20px;
float: left;
}
#menu a:hover {
background-color: #1e89c6;
}
.sect1 {
padding: 10px 40px;
}
.sect2 {
padding: 10px 0px;
}
div.footnote {
padding: 10px 40px;
}
hr {
margin: 0px 40px;
color: #CCCCCC;
}
h1, h2, h3, h4, h5, h6 {
color: #0A396E;
margin-bottom: 10px;
border-bottom: 1px solid #CCCCCC;
}
.ulist {
padding-left: 20px;
margin-top: 10px;
}
#footer {
padding: 20px 40px;
border-top: 1px solid #CCCCCC;
color: #888888;
}
a {
color: #1e89c6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ol {
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
li {
margin-bottom: 10px;
}
.hdlist1 {
color: #0A396E;
margin-bottom: 10px;
margin-top: 10px;
border-bottom: 1px solid #CCCCCC;
}
code {
background-color: #DDDDDD;
border-radius: 2px;
}
.listingblock {
padding: 10px 10px;
background-color: #DDDDDD;
border-radius: 4px;
}
table td {
padding: 10px 10px;
}
@media screen and (max-width: 480px) {
#headline .qutebrowser-logo {
margin-left: auto;
margin-right: auto;
display: block;
width: 30%;
height: auto;
float: none;
}
#headline .text {
display: none;
}
#menu {
padding: 0px 0px;
background-color: #555555;
color: #CCC;
overflow: hidden;
width: 100%;
}
#menu a {
color: #CCC;
text-decoration: none;
background-color: #555555;
width: 100%;
padding: 10px 40px;
}
}
</style>
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;">
<link href='media/font.css' rel='stylesheet' type='text/css'>
<link rel="icon" href="media/favicon.png" type="image/png">
<style>