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}'
cache:
- C:\projects\qutebrowser\.cache
@ -13,7 +14,7 @@ install:
- C:\Python27\python -u scripts\dev\ci_install.py
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:
- ps: |
@ -22,6 +23,3 @@ after_test:
$file = '.\junit.xml'
(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
/htmlcov
/.coverage.xml
/.coverage.*
/.tox
/testresults.html
/.cache

View File

@ -37,10 +37,10 @@ install:
- python scripts/dev/ci_install.py
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:
- '[[ $TESTENV == py34 || $TESTENV == py35 ]] && codecov -e TESTENV -X gcov'
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
matrix:
exclude:

View File

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

View File

@ -1,5 +1,6 @@
Frequently asked questions
==========================
:title: Frequently asked questions
The Compiler <mail@qutebrowser.org>
[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
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
Configuration not saved after modifying config.::

View File

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

View File

@ -6,6 +6,7 @@
qutebrowser
===========
// QUTE_WEB_HIDE
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"]
@ -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://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"]
// QUTE_WEB_HIDE_END
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.
@ -24,7 +26,7 @@ Screenshots
-----------
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/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:
* http://www.python.org/[Python] 3.4
* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended)
* http://www.python.org/[Python] 3.4 or newer
* http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended)
* QtWebKit
* 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]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
@ -139,32 +141,33 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Antoni Boucher
* Bruno Oliveira
* Lamar Pavel
* Bruno Oliveira
* Alexander Cogneau
* Martin Tournoij
* Raphael Pierzina
* Joel Torstensson
* Daniel
* Claude
* meles5
* Nathan Isom
* Austin Anderson
* Artur Shaik
* Thorsten Wißmann
* Alexey "Averrin" Nabrodov
* meles5
* ZDarian
* John ShaggyTwoDope Jenkins
* Peter Vilim
* Jonas Schürmann
* Jimmy
* skinnay
* error800
* Zach-Button
* Halfwit
* Felix Van der Jeugt
* rikn00
* Patric Schmitz
* Martin Zimmermann
* Error 800
* Brian Jackson
* sbinix
* neeasade
@ -180,7 +183,6 @@ Contributors, sorted by the number of commits in descending order:
* Fritz V155 Reichwald
* Franz Fellner
* zwarag
* error800
* Tim Harder
* Thiago Barroso Perrotta
* 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\.
^QWaitCondition: Destroyed while threads are still waiting
^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:
True if the event should be filtered, False if it's passed through.
"""
if qApp.overrideCursor() is None:
# Mouse cursor shown -> don't filter event
return False
else:
# Mouse cursor hidden -> filter event
return True
# Mouse cursor shown (overrideCursor None) -> don't filter event
# Mouse cursor hidden (overrideCursor not None) -> filter event
return qApp.overrideCursor() is not None
def eventFilter(self, obj, event):
"""Handle an event.

View File

@ -152,30 +152,6 @@ class CommandDispatcher:
else:
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):
"""Get an index for moving a tab absolutely.
@ -412,20 +388,27 @@ class CommandDispatcher:
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
if (not forward and not
self._current_widget().page().history().canGoBack()):
# Catch common cases before e.g. cloning tab
history = self._current_widget().page().history()
if not forward and not history.canGoBack():
raise cmdexc.CommandError("At beginning of history.")
if (forward and not
self._current_widget().page().history().canGoForward()):
elif forward and not history.canGoForward():
raise cmdexc.CommandError("At end of history.")
if tab or bg or window:
widget = self.tab_clone(bg, window)
else:
widget = self._current_widget()
history = widget.page().history()
for _ in range(count):
if forward:
if not history.canGoForward():
raise cmdexc.CommandError("At end of history.")
widget.forward()
else:
if not history.canGoBack():
raise cmdexc.CommandError("At beginning of history.")
widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window',
@ -538,7 +521,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True,
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.
Args:
@ -555,8 +538,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count')
def scroll(self,
direction: {'type': (str, float)},
dy: {'type': float, 'hide': True}=None,
direction: {'type': (str, int)},
dy: {'type': int, 'hide': True}=None,
count=1):
"""Scroll the current tab in the given direction.
@ -569,8 +552,8 @@ class CommandDispatcher:
# pylint: disable=too-many-locals
try:
# Check for deprecated dx/dy form (like with scroll-px).
dx = float(direction)
dy = float(dy)
dx = int(direction)
dy = int(dy)
except (ValueError, TypeError):
# Invalid values will get handled later.
pass
@ -643,8 +626,24 @@ class CommandDispatcher:
horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll.
"""
self._scroll_percent(perc, count,
Qt.Horizontal if horizontal else Qt.Vertical)
if perc is None and count is None:
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,
scope='window', count='count')
@ -686,7 +685,7 @@ class CommandDispatcher:
pass
elif mult_y < 0:
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))
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
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
'count'])
def replace_variables(win_id, arglist):
@ -117,6 +118,26 @@ class CommandRunner(QObject):
for sub in sub_texts:
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):
"""Split the commandline text into command and arguments.
@ -128,9 +149,11 @@ class CommandRunner(QObject):
keep: Whether to keep special chars and whitespace
Return:
A (cmd, args, cmdline) ParseResult tuple.
A ParseResult tuple.
"""
cmdstr, sep, argstr = text.partition(' ')
count, cmdstr = self._parse_count(cmdstr)
if not cmdstr and not fallback:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
@ -161,7 +184,7 @@ class CommandRunner(QObject):
cmdline = [cmdstr, sep]
else:
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):
"""Split the arguments from an arg string.
@ -216,7 +239,12 @@ class CommandRunner(QObject):
for result in self.parse_all(text):
args = replace_variables(self._win_id, result.args)
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)
elif result.count is not None:
result.cmd.run(self._win_id, args, count=result.count)
else:
result.cmd.run(self._win_id, args)

View File

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

View File

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

View File

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

View File

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

View File

@ -1569,49 +1569,41 @@ class UserAgent(BaseType):
def validate(self, 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):
"""Complete a list of common user agents."""
out = [
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Win7 64-bit"),
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Ubuntu"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
'Gecko/20100101 Firefox/35.0',
"Firefox 35.0 MacOSX"),
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 '
'Firefox/41.0',
"Firefox 41.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) '
'Gecko/20100101 Firefox/41.0',
"Firefox 41.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 '
'Firefox/41.0',
"Firefox 41.0 Linux"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
'Safari/600.3.18',
"Safari 8.0 MacOSX"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) '
'AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 '
'Safari/601.2.7',
"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, '
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
"Chrome 46.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 '
'Safari/537.36',
"Chrome 40.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.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"),
"Chrome 46.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
"Chrome 46.0 Linux"),
('Mozilla/5.0 (compatible; Googlebot/2.1; '
'+http://www.google.com/bot.html',
@ -1619,7 +1611,11 @@ class UserAgent(BaseType):
('Wget/1.16.1 (linux-gnu)',
"wget 1.16.1"),
('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

View File

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

View File

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

View File

@ -122,10 +122,7 @@ def _is_url_naive(urlstr):
if not QHostAddress(urlstr).isNull():
return False
if '.' in url.host():
return True
else:
return False
return '.' in url.host()
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
# search engine.
engine, _term = _parse_search_term(urlstr)
if engine is None:
return True
else:
return False
return engine is None
if not qurl_userinput.isValid():
# This will also catch URLs containing spaces.

View File

@ -20,60 +20,213 @@
"""Generate the html documentation based on the asciidoc files."""
import re
import os
import os.path
import sys
import subprocess
import glob
import shutil
import tempfile
import argparse
import io
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils
def _get_asciidoc_cmd(args):
"""Try to find out what commandline to use to invoke asciidoc."""
if args.asciidoc is not None:
return args.asciidoc
class AsciiDoc:
try:
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except OSError:
pass
else:
return ['asciidoc']
"""Abstraction of an asciidoc subprocess."""
try:
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except OSError:
pass
else:
return ['asciidoc.py']
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'),
]
raise FileNotFoundError
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 call_asciidoc(args, src, dst):
"""Call asciidoc for the given files.
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)
Args:
args: The asciidoc binary to use, as a list.
src: The source .asciidoc file.
dst: The destination .html file, or None to auto-guess.
"""
print("Calling asciidoc for {}...".format(os.path.basename(src)))
args = args[:]
if dst is not None:
args += ['--out-file', dst]
args.append(src)
try:
subprocess.check_call(args)
except (subprocess.CalledProcessError, OSError) as e:
utils.print_col(str(e), 'red')
sys.exit(1)
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."""
if self._args.asciidoc is not None:
return self._args.asciidoc
try:
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except OSError:
pass
else:
return ['asciidoc']
try:
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except OSError:
pass
else:
return ['asciidoc.py']
raise FileNotFoundError
def call(self, src, dst, *args):
"""Call asciidoc for the given files.
Args:
src: The source .asciidoc file.
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)))
cmdline = self._cmd[:]
if dst is not None:
cmdline += ['--out-file', dst]
cmdline += args
cmdline.append(src)
try:
subprocess.check_call(cmdline, env={'HOME': self._homedir})
self._failed = True
except (subprocess.CalledProcessError, OSError) as e:
self._failed = True
utils.print_col(str(e), 'red')
print("Keeping modified sources in {}.".format(self._homedir))
sys.exit(1)
def main(colors=False):
@ -81,53 +234,31 @@ def main(colors=False):
utils.change_cwd()
utils.use_color = colors
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)
parser.add_argument('--asciidoc', help="Full path to python and "
"asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC'))
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:
os.mkdir('qutebrowser/html/doc')
except FileExistsError:
pass
asciidoc = AsciiDoc(args)
try:
asciidoc = _get_asciidoc_cmd(args)
asciidoc.prepare()
except FileNotFoundError:
utils.print_col("Could not find asciidoc! Please install it, or use "
"the --asciidoc argument to point this script to the "
"correct python/asciidoc.py location!", 'red')
sys.exit(1)
if args.all:
for root, _dirs, files in os.walk(os.getcwd()):
for filename in files:
if os.path.splitext(filename)[1] != '.asciidoc':
continue
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)
try:
asciidoc.build()
finally:
asciidoc.cleanup()
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
Message = collections.namedtuple('Message', 'typ, text')
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
@ -143,6 +142,18 @@ class Skipped(Exception):
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):
"""Main entry point which parses/checks coverage.xml if applicable."""
if sys.platform != 'linux':
@ -151,6 +162,8 @@ def check(fileobj, perfect_files):
raise Skipped("because -k is given.")
elif '-m' in sys.argv[1:]:
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]
@ -168,7 +181,8 @@ def check(fileobj, perfect_files):
messages = []
for klass in classes:
filename = klass.attrib['filename']
filename = _get_filename(klass.attrib['filename'])
line_cov = float(klass.attrib['line-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')
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
pip_packages = ['tox']
if TESTENV in ['py34', 'py35']:
if TESTENV in ['py34', 'py35'] and TRAVIS_OS == 'linux':
pip_packages.append('codecov')
@ -69,9 +69,12 @@ def check_setup(executable):
if 'APPVEYOR' in os.environ:
print("Getting PyQt5...")
urllib.urlretrieve(
'http://www.qutebrowser.org/pyqt/PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe',
r'C:\install-PyQt5.exe')
qt_version = '5.5.1'
pyqt_version = '5.5.1'
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...")
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,

View File

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

View File

@ -27,7 +27,7 @@ import sys
import pytest
import pytestqt.plugin
import pytest_mock
import pytest_capturelog
import pytest_catchlog
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
otherwise.
"""
if re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)):
# probably a virtual Qt method
return True
else:
return False
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
def report(items):

View File

@ -389,6 +389,8 @@ def _get_authors():
'binix': 'sbinix',
'Averrin': 'Alexey "Averrin" Nabrodov',
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
'Michael': 'Halfwit',
'Error 800': 'error800',
}
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
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
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 qutebrowser.config import config
from qutebrowser.utils import objreg
@ -121,11 +121,8 @@ def pytest_collection_modifyitems(items):
def pytest_ignore_collect(path):
"""Ignore BDD tests during collection if frozen."""
rel_path = path.relto(os.path.dirname(__file__))
if (rel_path == os.path.join('integration', 'features') and
hasattr(sys, 'frozen')):
return True
else:
return False
return (rel_path == os.path.join('integration', 'features') and
hasattr(sys, 'frozen'))
@pytest.fixture(scope='session')

View File

@ -24,7 +24,7 @@ import logging
import pytest
try:
import pytest_capturelog as caplog_mod
import pytest_catchlog as catchlog_mod
except ImportError:
# When using pytest for pyflakes/pep8/..., the plugin won't be available
# but conftest.py will still be loaded.
@ -47,18 +47,18 @@ class LogFailHandler(logging.Handler):
root_logger = logging.getLogger()
for h in root_logger.handlers:
if isinstance(h, caplog_mod.CaptureLogHandler):
caplog_handler = h
if isinstance(h, catchlog_mod.LogCaptureHandler):
catchlog_handler = h
break
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..
return
if (logger.level == record.levelno or
caplog_handler.level == record.levelno):
# caplog.atLevel(...) was used with the level of this message, i.e.
# it was expected.
catchlog_handler.level == record.levelno):
# caplog.at_level(...) was used with the level of this message,
# i.e. it was expected.
return
if record.levelno < self._min_level:
return
@ -74,25 +74,3 @@ def fail_on_logging():
yield
logging.getLogger().removeHandler(handler)
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]
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)
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):
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 pytest
import pytest_capturelog # pylint: disable=import-error
import pytest_catchlog # pylint: disable=import-error
def test_log_debug():
@ -36,33 +36,33 @@ def test_log_warning():
def test_log_expected(caplog):
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
logging.error('foo')
def test_log_expected_logger(caplog):
logger = 'logfail_test_logger'
with caplog.atLevel(logging.ERROR, logger):
with caplog.at_level(logging.ERROR, logger):
logging.getLogger(logger).error('foo')
def test_log_expected_wrong_level(caplog):
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
logging.critical('foo')
def test_log_expected_logger_wrong_level(caplog):
logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger):
with caplog.at_level(logging.ERROR, logger):
logging.getLogger(logger).critical('foo')
def test_log_expected_wrong_logger(caplog):
logger = 'logfail_test_logger'
with pytest.raises(pytest.fail.Exception):
with caplog.atLevel(logging.ERROR, logger):
with caplog.at_level(logging.ERROR, logger):
logging.error('foo')
@ -82,6 +82,6 @@ def test_caplog_bug_workaround_2():
"""
caplog_handler = None
for h in logging.getLogger().handlers:
if isinstance(h, pytest_capturelog.CaptureLogHandler):
if isinstance(h, pytest_catchlog.LogCaptureHandler):
assert caplog_handler is None
caplog_handler = h

View File

@ -20,32 +20,39 @@
"""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:
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
if not partial_compare(val1[key], val2[key]):
print("Comparison failed for {!r} and {!r}!".format(
val1[key], val2[key]))
if not partial_compare(val1[key], val2[key], indent=indent+1):
return False
return True
def _partial_compare_list(val1, val2):
def _partial_compare_list(val1, val2, *, indent=0):
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
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 True
def partial_compare(val1, val2):
def partial_compare(val1, val2, *, indent=0):
"""Do a partial comparison between the given values.
For dicts, keys in val2 are checked, others are ignored.
@ -54,31 +61,44 @@ def partial_compare(val1, val2):
This happens recursively.
"""
print()
print("Comparing\n {!r}\nto\n {!r}".format(val1, val2))
print_i("Comparing", indent)
print_i(pprint.pformat(val1), indent + 1)
print_i("|---- to ----", indent)
print_i(pprint.pformat(val2), indent + 1)
if val2 is Ellipsis:
print("Ignoring ellipsis comparison")
print_i("Ignoring ellipsis comparison", indent, error=True)
return True
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
print("Different types ({}, {}) -> False".format(
type(val1), type(val2)))
print_i("Different types ({}, {}) -> False".format(
type(val1), type(val2)), indent, error=True)
return False
if isinstance(val2, dict):
print("Comparing as dicts")
equal = _partial_compare_dict(val1, val2)
print_i("|======= Comparing as dicts", indent)
equal = _partial_compare_dict(val1, val2, indent=indent)
elif isinstance(val2, list):
print("Comparing as lists")
equal = _partial_compare_list(val1, val2)
print_i("|======= Comparing as lists", indent)
equal = _partial_compare_list(val1, val2, indent=indent)
elif isinstance(val2, float):
print("Doing float comparison")
print_i("|======= Doing float comparison", indent)
equal = abs(val1 - val2) < 0.00001
elif isinstance(val2, str):
print("Doing string comparison")
equal = fnmatch.fnmatchcase(val1, val2)
print_i("|======= Doing string comparison", indent)
equal = pattern_match(pattern=val2, value=val1)
else:
print("Comparing via ==")
print_i("|======= Comparing via ==", indent)
equal = val1 == val2
print("---> {}".format(equal))
print_i("---> {}".format(equal), indent)
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
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.
</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>
</html>

View File

@ -43,6 +43,19 @@ Feature: Going back and forward.
url: http://localhost:*/data/backforward/1.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
Given I open data/backforward/1.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/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
Given I have a fresh instance
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}"))
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))
quteproc.send_cmd(command)
quteproc.send_cmd(command, count=count)
@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"))
def path_should_be_loaded(httpbin, path):
requests = httpbin.get_requests()
assert requests[-1] == httpbin.Request('GET', '/' + path)
httpbin.wait_for(verb='GET', path='/' + path)
@bdd.then(bdd.parsers.parse("The requests should be:\n{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')]
actual_requests = httpbin.get_requests()
assert actual_requests == expected_requests
@ -131,6 +135,11 @@ def compare_session(quteproc, 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")
def no_crash():
"""Don't do anything.

View File

@ -1,5 +1,7 @@
Feature: Various utility commands.
## :set-cmd-text
Scenario: :set-cmd-text and :command-accept
When I run :set-cmd-text :message-info "Hello World"
And I run :command-accept
@ -31,6 +33,8 @@ Feature: Various utility commands.
When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown.
## :message-*
Scenario: :message-error
When I run :message-error "Hello World"
Then the error "Hello World" should be shown.
@ -42,3 +46,57 @@ Feature: Various utility commands.
Scenario: :message-warning
When I run :message-warning "Hello World"
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
And I run :tab-only
## :scroll-px
Scenario: Scrolling pixel-wise vertically
When I run :scroll-px 0 10
Then the page should be scrolled vertically.
@ -13,6 +15,45 @@ Feature: Scrolling
When I run :scroll-px 10 0
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
When I run :scroll down
Then the page should be scrolled vertically.
@ -58,3 +99,155 @@ Feature: Scrolling
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 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')
@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")
def selection_supported(qapp):
if not qapp.clipboard().supportsSelection():
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 '
r'contain "(?P<content>.*)"'))
def clipboard_contains(qapp, httpbin, what, content):
if what == 'clipboard':
mode = QClipboard.Clipboard
elif what == 'primary selection':
mode = QClipboard.Selection
else:
raise AssertionError
mode = _get_mode(qapp, what)
expected = content.replace('(port)', str(httpbin.port))
data = qapp.clipboard().text(mode=mode)

View File

@ -19,3 +19,10 @@
import pytest_bdd as bdd
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.
Background:
Given I open data/yankpaste.html
Given I run :tab-only
#### :yank
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.
And the clipboard should contain "http://localhost:(port)/data/yankpaste.html"
Scenario: Yanking URLs to primary selection
When selection is supported
And I open data/yankpaste.html
And I run :yank --sel
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"
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
Then the message "Yanked title to clipboard: Test title" should be shown.
And the clipboard should contain "Test title"
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.
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:
Given I open data/hello.txt
And I set ui -> zoom-levels to 50%,90%,100%,110%,120%
And I run :tab-only
Scenario: Zooming in
When I run :zoom-in
Then the message "Zoom level: 110%" should be shown.
And the session should look like:
windows:
- tabs:
- history:
- zoom: 1.1
And the zoom should be 110%
Scenario: Zooming out
When I run :zoom-out
Then the message "Zoom level: 90%" should be shown.
And the session should look like:
windows:
- tabs:
- history:
- zoom: 0.9
And the zoom should be 90%
Scenario: Zooming in with count
When I run :zoom-in with count 2
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
When I run :zoom 50
Then the message "Zoom level: 50%" should be shown.
And the session should look like:
windows:
- tabs:
- history:
- zoom: 0.5
And the zoom should be 50%
Scenario: Setting zoom with count
When I run :zoom with count 40
Then the message "Zoom level: 40%" should be shown.
And the zoom should be 40%
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
Then the message "Zoom level: 100%" should be shown.
And the session should look like:
windows:
- tabs:
- history:
- zoom: 1.0
Then the message "Zoom level: 42%" should be shown.
And the zoom should be 42%
Scenario: Setting zoom to invalid value
When I run :zoom -1
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
class NoLineMatch(Exception):
"""Raised by LogLine on unmatched lines."""
pass
class LogLine(testprocess.Line):
"""A parsed line from the qutebrowser log output.
@ -78,7 +71,7 @@ class LogLine(testprocess.Line):
super().__init__(data)
match = self.LOG_RE.match(data)
if match is None:
raise NoLineMatch(data)
raise testprocess.InvalidLine(data)
self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
'%H:%M:%S')
@ -127,6 +120,9 @@ class QuteProc(testprocess.Process):
got_error = pyqtSignal()
KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line',
'message']
def __init__(self, httpbin, parent=None):
super().__init__(parent)
self._httpbin = httpbin
@ -135,7 +131,7 @@ class QuteProc(testprocess.Process):
def _parse_line(self, line):
try:
log_line = LogLine(line)
except NoLineMatch:
except testprocess.InvalidLine:
if line.startswith(' '):
# Multiple lines in some log output...
return None
@ -144,7 +140,7 @@ class QuteProc(testprocess.Process):
elif is_ignored_qt_message(line):
return None
else:
raise testprocess.InvalidLine
raise
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
pytest.config.getoption('--verbose')):
@ -177,6 +173,12 @@ class QuteProc(testprocess.Process):
'about:blank']
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):
bad_msgs = [msg for msg in self._data
if msg.loglevel > logging.INFO and not msg.expected]
@ -186,9 +188,12 @@ class QuteProc(testprocess.Process):
str(e) for e in bad_msgs)
pytest.fail(text, pytrace=False)
def send_cmd(self, command):
def send_cmd(self, command, count=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],
target_arg='')
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: *')
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:
self.send_cmd(':open -t ' + url)
else:
@ -218,11 +223,12 @@ class QuteProc(testprocess.Process):
self._data is cleared after every test to provide at least some
isolation.
"""
__tracebackhide__ = True
return super().wait_for(timeout, **kwargs)
def wait_for_load_finished(self, path, timeout=15000):
"""Wait until any tab has finished loading."""
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path)
url = self._path_to_url(path)
pattern = re.compile(
r"(load status for <qutebrowser.browser.webview.WebView "
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "

View File

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

View File

@ -74,19 +74,20 @@ class PythonProcess(testprocess.Process):
return (sys.executable, ['-c', ';'.join(code)])
class TestWaitFor:
@pytest.yield_fixture
def pyproc():
proc = PythonProcess()
yield proc
proc.terminate()
@pytest.yield_fixture
def pyproc(self):
proc = PythonProcess()
yield proc
proc.terminate()
class TestWaitFor:
def test_successful(self, pyproc):
"""Using wait_for with the expected text."""
pyproc.code = "time.sleep(0.5); print('foobar')"
pyproc.start()
with stopwatch(min_ms=300): # on Windows, this can be done faster...
with stopwatch(min_ms=500):
pyproc.start()
pyproc.wait_for(data="foobar")
def test_other_text(self, pyproc):
@ -103,12 +104,13 @@ class TestWaitFor:
with pytest.raises(testprocess.WaitForTimeout):
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."""
pyproc.code = "print('foobar')"
pyproc.code = "print('{}')".format(message)
pyproc.start()
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):
"""Make sure the message of a previous test gets ignored."""
@ -133,3 +135,36 @@ class TestWaitFor:
pyproc.wait_for(data="foobar")
with pytest.raises(testprocess.WaitForTimeout):
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')
assert httpbin.get_requests() == [httpbin.Request('GET', path)]
assert httpbin.get_requests() == [httpbin.ExpectedRequest('GET', path)]
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 os
import time
import fnmatch
import pytestqt.plugin # pylint: disable=import-error
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
from PyQt5.QtTest import QSignalSpy
from helpers import utils # pylint: disable=import-error
class InvalidLine(Exception):
@ -48,6 +49,11 @@ class WaitForTimeout(Exception):
"""Raised when wait_for didn't get the expected message."""
class BlacklistedMessageError(Exception):
"""Raised when ensure_not_logged found a message."""
class Line:
"""Container for a line of data the process emits.
@ -78,6 +84,7 @@ class Process(QObject):
ready = pyqtSignal()
new_data = pyqtSignal(object)
KEYS = ['data']
def __init__(self, parent=None):
super().__init__(parent)
@ -138,7 +145,8 @@ class Process(QObject):
continue
if parsed is None:
print("IGNORED: {}".format(line))
if self._invalid:
print("IGNORED: {}".format(line))
else:
self._data.append(parsed)
self.new_data.emit(parsed)
@ -198,7 +206,7 @@ class Process(QObject):
- If expected is None, the filter always matches.
- 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
compiled regex, it is used for matching.
- If the value is any other type, == is used.
@ -212,25 +220,34 @@ class Process(QObject):
elif isinstance(expected, regex_type):
return expected.match(value)
elif isinstance(value, (bytes, str)):
return fnmatch.fnmatchcase(value, expected)
return utils.pattern_match(pattern=expected, value=value)
else:
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.
Keyword arguments to this function get interpreted as attributes of the
searched data. Every given argument is treated as a pattern which
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:
The matched line.
"""
__tracebackhide__ = True
if timeout is None:
if 'CI' in os.environ:
timeout = 15000
else:
timeout = 5000
for key in kwargs:
assert key in self.KEYS
# Search existing messages
for line in self._data:
matches = []
@ -239,7 +256,7 @@ class Process(QObject):
value = getattr(line, key)
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
# same thing the next time we use wait_for and it matches
# this line again.
@ -273,3 +290,18 @@ class Process(QObject):
# this line again.
line.waited_for = True
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 socket
import os.path
import collections
import pytest
from PyQt5.QtCore import pyqtSignal
@ -34,25 +33,17 @@ from PyQt5.QtCore import pyqtSignal
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):
"""Abstraction over a running HTTPbin server process.
Reads the log from its stdout and parses it.
Attributes:
timestamp/verb/path/status: Parsed from the log output.
Class attributes:
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"""
(?P<host>[^ ]*)
\ ([^ ]*) # ignored
@ -67,6 +58,69 @@ class HTTPBin(testprocess.Process):
\ (?P<size>[^ ]*)
""", 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):
super().__init__(parent)
self.port = self._get_port()
@ -91,14 +145,7 @@ class HTTPBin(testprocess.Process):
'quit)'.format(self.port)):
self.ready.emit()
return None
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'))
return Request(line)
def _executable_args(self):
if hasattr(sys, 'frozen'):

View File

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

View File

@ -147,7 +147,7 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
disk_cache = cache.DiskCache(str(tmpdir))
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):
@ -207,7 +207,7 @@ def test_cache_deactivated_remove_data(config_stub, tmpdir):
disk_cache = cache.DiskCache(str(tmpdir))
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):

View File

@ -106,13 +106,12 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
tabbed_browser.current_index = 0
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')
records = caplog.records()
assert len(records) == 1
assert len(caplog.records) == 1
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])
@ -120,10 +119,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
tabbed_browser.current_index = 0
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')
assert not caplog.records()
assert not caplog.records
def test_runtime_error(objects, tabbed_browser):

View File

@ -42,3 +42,12 @@ class TestCommandRunner:
else:
with pytest.raises(cmdexc.NoSuchCommandError):
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': {}}
obj = Obj("{{ font['foo'] }}")
with caplog.atLevel(9): # VDEBUG
with caplog.at_level(9): # VDEBUG
style.set_register_stylesheet(obj)
records = caplog.records()
assert len(records) == 1
assert records[0].message == 'stylesheet for Obj: bar'
assert len(caplog.records) == 1
assert caplog.records[0].message == 'stylesheet for Obj: bar'
assert obj.rendered_stylesheet == 'bar'
@ -104,11 +103,10 @@ class TestColorDict:
def test_key_error(self, caplog):
d = style.ColorDict()
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
d['foo'] # pylint: disable=pointless-statement
records = caplog.records()
assert len(records) == 1
assert records[0].message == 'No color defined for foo!'
assert len(caplog.records) == 1
assert caplog.records[0].message == 'No color defined for foo!'
def test_qcolor(self):
d = style.ColorDict()

View File

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

View File

@ -174,16 +174,16 @@ def test_start_logging(fake_proc, caplog):
"""Make sure that starting logs the executed commandline."""
cmd = 'does_not_exist'
args = ['arg', 'arg with spaces']
with caplog.atLevel(logging.DEBUG):
with caplog.at_level(logging.DEBUG):
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.",
"Executing: does_not_exist arg 'arg with spaces'"]
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
"""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):
proc.start('this_does_not_exist_either', [])

View File

@ -358,11 +358,10 @@ class TestListen:
@pytest.mark.posix
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()
records = caplog.records()
assert records[-1].msg == "In update_atime with no server path!"
assert caplog.records[-1].msg == "In update_atime with no server path!"
@pytest.mark.posix
def test_atime_shutdown_typeerror(self, qtbot, ipc_server):
@ -408,22 +407,21 @@ class TestHandleConnection:
def test_no_connection(self, ipc_server, caplog):
ipc_server.handle_connection()
record = caplog.records()[-1]
assert record.message == "No new connection to handle."
assert caplog.records[-1].message == "No new connection to handle."
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
ipc_server._socket = qlocalsocket
ipc_server.handle_connection()
message = ("Got new connection but ignoring it because we're still "
"handling another one.")
assert message in [rec.message for rec in caplog.records()]
assert message in [rec.message for rec in caplog.records]
def test_disconnected_immediately(self, ipc_server, caplog):
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
ipc_server._server = FakeServer(socket)
ipc_server.handle_connection()
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
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)'
assert str(excinfo.value) == exc_msg
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
def test_read_line_immediately(self, qtbot, ipc_server, caplog):
@ -454,7 +452,7 @@ class TestHandleConnection:
assert spy[0][0] == ['foo']
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
@ -505,11 +503,11 @@ def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
got_args_spy = QSignalSpy(ipc_server.got_args)
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
with qtbot.waitSignals(signals, raising=True):
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[-2].startswith(msg)
assert not got_args_spy
@ -542,11 +540,11 @@ class TestSendToRunningInstance:
def test_no_server(self, caplog):
sent = ipc.send_to_running_instance('qute-test', [], None)
assert not sent
msg = caplog.records()[-1].message
msg = caplog.records[-1].message
assert msg == "No existing instance present (error 2)"
@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):
ipc_server.listen()
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):
qlocalsocket.connectToServer('qute-test')
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
timeout=5000):
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', [
@ -628,13 +626,13 @@ def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
assert ipc_server._socket is None
if is_warning:
with caplog.atLevel(logging.WARNING):
with caplog.at_level(logging.WARNING):
func(*args)
else:
func(*args)
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:
@ -683,7 +681,7 @@ class TestSendOrListen:
def test_normal_connection(self, caplog, qtbot, args):
ret_server = ipc.send_or_listen(args)
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
objreg_server = objreg.get('ipc-server')
assert objreg_server is ret_server
@ -698,7 +696,7 @@ class TestSendOrListen:
with qtbot.waitSignal(legacy_server.got_args, raising=True):
ret = ipc.send_or_listen(args)
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
@pytest.mark.posix(reason="Unneeded on Windows")
@ -775,7 +773,7 @@ class TestSendOrListen:
ret = ipc.send_or_listen(args)
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
@pytest.mark.parametrize('has_error, exc_name, exc_msg', [
@ -812,12 +810,11 @@ class TestSendOrListen:
QLocalSocket.ConnectionRefusedError, # error() gets called twice
]
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
with pytest.raises(ipc.Error):
ipc.send_or_listen(args)
records = caplog.records()
assert len(records) == 1
assert len(caplog.records) == 1
error_msgs = [
'Handling fatal misc.ipc.{} with --no-err-windows!'.format(
@ -828,7 +825,7 @@ class TestSendOrListen:
'post_text: Maybe another instance is running but frozen?',
'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")
def test_error_while_listening(self, qlocalserver_mock, caplog, args):
@ -837,12 +834,11 @@ class TestSendOrListen:
err = QAbstractSocket.SocketResourceError
qlocalserver_mock().serverError.return_value = err
with caplog.atLevel(logging.ERROR):
with caplog.at_level(logging.ERROR):
with pytest.raises(ipc.Error):
ipc.send_or_listen(args)
records = caplog.records()
assert len(records) == 1
assert len(caplog.records) == 1
error_msgs = [
'Handling fatal misc.ipc.ListenError with --no-err-windows!',
@ -853,7 +849,7 @@ class TestSendOrListen:
'exception text: Error while listening to IPC server: Error '
'string (error 4)',
]
assert records[0].msg == '\n'.join(error_msgs)
assert caplog.records[0].msg == '\n'.join(error_msgs)
@pytest.mark.windows

View File

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

View File

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

View File

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

View File

@ -52,12 +52,11 @@ def test_no_err_windows(caplog, exc, name, exc_text):
try:
raise exc
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',
pre_text='pre', post_text='post')
records = caplog.records()
assert len(records) == 1
assert len(caplog.records) == 1
expected = [
'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',
'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

View File

@ -25,7 +25,7 @@ import itertools
import sys
import pytest
import pytest_capturelog # pylint: disable=import-error
import pytest_catchlog # pylint: disable=import-error
from qutebrowser.utils import log
@ -60,10 +60,11 @@ def restore_loggers():
while root_logger.handlers:
h = root_logger.handlers[0]
root_logger.removeHandler(h)
h.close()
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
h.close()
root_logger.setLevel(original_logging_level)
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
root_logger.addHandler(h)
logging._acquireLock()
@ -238,30 +239,30 @@ class TestHideQtWarning:
def test_unfiltered(self, logger, caplog):
"""Test a message which is not filtered."""
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")
assert len(caplog.records()) == 1
record = caplog.records()[0]
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == 'WARNING'
assert record.message == "Hello World"
def test_filtered_exact(self, logger, caplog):
"""Test a message which is filtered (exact match)."""
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")
assert not caplog.records()
assert not caplog.records
def test_filtered_start(self, logger, caplog):
"""Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
with caplog.at_level(logging.WARNING, 'qt-tests'):
logger.warning("Hello World")
assert not caplog.records()
assert not caplog.records
def test_filtered_whitespace(self, logger, caplog):
"""Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", 'qt-tests'):
with caplog.atLevel(logging.WARNING, 'qt-tests'):
with caplog.at_level(logging.WARNING, 'qt-tests'):
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',
lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'init'):
with caplog.at_level(logging.ERROR, 'init'):
standarddir._init_cachedir_tag()
assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG'
assert len(caplog.records) == 1
assert caplog.records[0].message == 'Failed to create CACHEDIR.TAG'
assert not tmpdir.listdir()

View File

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

View File

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

View File

@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog):
"""Test Question.abort() with .emit() raising a TypeError."""
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
signal_mock.emit.side_effect = TypeError
with caplog.atLevel(logging.ERROR, 'misc'):
with caplog.at_level(logging.ERROR, 'misc'):
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-type==0.3.4
py==1.4.30
pytest==2.7.3 # rq.filter: <2.8.0
pytest==2.8.2
pytest-bdd==2.15.0
pytest-capturelog==0.7
pytest-catchlog==1.2.0
pytest-cov==2.2.0
pytest-faulthandler==1.0.1
pytest-faulthandler==1.1.0
pytest-html==1.7
pytest-mock==0.8.1
pytest-qt==1.9.0
@ -39,12 +39,12 @@ deps =
six==1.10.0
termcolor==1.1.0
vulture==0.8.1
Werkzeug==0.11.1
Werkzeug==0.11.2
wheel==0.26.0
xvfbwrapper==0.2.5
commands =
{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}
[testenv:mkvenv]
@ -105,6 +105,7 @@ deps =
astroid==1.3.8
pylint==1.4.4
logilab-common==1.1.0
requests==2.8.1
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{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
passenv =
deps =
check-manifest==0.27
check-manifest==0.28
commands =
{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>