Merge branch 'master' into download-page
This commit is contained in:
commit
3438a45b19
@ -1,3 +1,4 @@
|
|||||||
|
shallow_clone: true
|
||||||
version: '{branch}-{build}'
|
version: '{branch}-{build}'
|
||||||
cache:
|
cache:
|
||||||
- C:\projects\qutebrowser\.cache
|
- C:\projects\qutebrowser\.cache
|
||||||
@ -13,7 +14,7 @@ install:
|
|||||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" --junitxml=junit.xml
|
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml
|
||||||
|
|
||||||
after_test:
|
after_test:
|
||||||
- ps: |
|
- ps: |
|
||||||
@ -22,6 +23,3 @@ after_test:
|
|||||||
$file = '.\junit.xml'
|
$file = '.\junit.xml'
|
||||||
(New-Object 'System.Net.WebClient').UploadFile($url, (Resolve-Path $file))
|
(New-Object 'System.Net.WebClient').UploadFile($url, (Resolve-Path $file))
|
||||||
}
|
}
|
||||||
if ($env:TESTENV -eq 'py34') {
|
|
||||||
C:\Python34\Scripts\codecov -e TESTENV -X gcov
|
|
||||||
}
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ __pycache__
|
|||||||
/.coverage
|
/.coverage
|
||||||
/htmlcov
|
/htmlcov
|
||||||
/.coverage.xml
|
/.coverage.xml
|
||||||
|
/.coverage.*
|
||||||
/.tox
|
/.tox
|
||||||
/testresults.html
|
/testresults.html
|
||||||
/.cache
|
/.cache
|
||||||
|
@ -37,10 +37,10 @@ install:
|
|||||||
- python scripts/dev/ci_install.py
|
- python scripts/dev/ci_install.py
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- tox -e $TESTENV -- -p no:sugar --faulthandler-timeout=70 -v --cov-report term tests
|
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- '[[ $TESTENV == py34 || $TESTENV == py35 ]] && codecov -e TESTENV -X gcov'
|
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
|
@ -68,6 +68,7 @@ Changed
|
|||||||
finished. When set to `-1`, downloads are never removed.
|
finished. When set to `-1`, downloads are never removed.
|
||||||
- The `:follow-hint` command now optionally takes the keystring of a hint to
|
- The `:follow-hint` command now optionally takes the keystring of a hint to
|
||||||
follow.
|
follow.
|
||||||
|
- `:scroll-px` now doesn't take floats anymore, which made little sense.
|
||||||
|
|
||||||
Deprecated
|
Deprecated
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@ -86,6 +87,7 @@ Fixed
|
|||||||
`storage -> prompt-download-directory` was unset.
|
`storage -> prompt-download-directory` was unset.
|
||||||
- Fixed crash when using `:follow-hint` outside of hint mode.
|
- Fixed crash when using `:follow-hint` outside of hint mode.
|
||||||
- Fixed crash when using `:set foo bar?` with invalid section/option.
|
- Fixed crash when using `:set foo bar?` with invalid section/option.
|
||||||
|
- Fixed scrolling to the very left/right with `:scroll-perc`.
|
||||||
|
|
||||||
v0.4.1
|
v0.4.1
|
||||||
------
|
------
|
||||||
|
@ -548,6 +548,13 @@ workaround.
|
|||||||
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
|
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
|
||||||
bugs] and check if they're fixed.
|
bugs] and check if they're fixed.
|
||||||
|
|
||||||
|
New PyQt release
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* See above
|
||||||
|
* Install new PyQt in Windows VM (32- and 64-bit)
|
||||||
|
* Download new installer and update PyQt installer path in `ci_install.py`.
|
||||||
|
|
||||||
qutebrowser release
|
qutebrowser release
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
11
FAQ.asciidoc
11
FAQ.asciidoc
@ -1,5 +1,6 @@
|
|||||||
Frequently asked questions
|
Frequently asked questions
|
||||||
==========================
|
==========================
|
||||||
|
:title: Frequently asked questions
|
||||||
The Compiler <mail@qutebrowser.org>
|
The Compiler <mail@qutebrowser.org>
|
||||||
|
|
||||||
[qanda]
|
[qanda]
|
||||||
@ -87,6 +88,16 @@ Note that you might need an additional package (e.g.
|
|||||||
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
|
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
|
||||||
Archlinux) to play web videos with mpv.
|
Archlinux) to play web videos with mpv.
|
||||||
|
|
||||||
|
How do I use qutebrowser with mutt?::
|
||||||
|
Due to a Qt limitation, local files without `.html` extensions are
|
||||||
|
"downloaded" instead of displayed, see
|
||||||
|
https://github.com/The-Compiler/qutebrowser/issues/566[#566]. You can work
|
||||||
|
around this by using this in your `mailcap`:
|
||||||
|
+
|
||||||
|
----
|
||||||
|
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
|
||||||
|
----
|
||||||
|
|
||||||
== Troubleshooting
|
== Troubleshooting
|
||||||
|
|
||||||
Configuration not saved after modifying config.::
|
Configuration not saved after modifying config.::
|
||||||
|
@ -17,6 +17,7 @@ include requirements.txt
|
|||||||
include tox.ini
|
include tox.ini
|
||||||
include qutebrowser.py
|
include qutebrowser.py
|
||||||
|
|
||||||
|
prune www
|
||||||
prune scripts/dev
|
prune scripts/dev
|
||||||
exclude scripts/asciidoc2html.py
|
exclude scripts/asciidoc2html.py
|
||||||
exclude doc/notes
|
exclude doc/notes
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
qutebrowser
|
qutebrowser
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
// QUTE_WEB_HIDE
|
||||||
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
|
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
|
||||||
|
|
||||||
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
|
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
|
||||||
@ -14,6 +15,7 @@ image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=fl
|
|||||||
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
||||||
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
|
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
|
||||||
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
|
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
|
||||||
|
// QUTE_WEB_HIDE_END
|
||||||
|
|
||||||
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
||||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||||
@ -24,7 +26,7 @@ Screenshots
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
||||||
image:doc/img/downloads.png["screenshot 2",width=300j,link="doc/img/downloads.png"]
|
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
|
||||||
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
||||||
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
||||||
|
|
||||||
@ -89,11 +91,11 @@ Requirements
|
|||||||
|
|
||||||
The following software and libraries are required to run qutebrowser:
|
The following software and libraries are required to run qutebrowser:
|
||||||
|
|
||||||
* http://www.python.org/[Python] 3.4
|
* http://www.python.org/[Python] 3.4 or newer
|
||||||
* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended)
|
* http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended)
|
||||||
* QtWebKit
|
* QtWebKit
|
||||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||||
(5.5.0 recommended) for Python 3
|
(5.5.1 recommended) for Python 3
|
||||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||||
* http://fdik.org/pyPEG/[pyPEG2]
|
* http://fdik.org/pyPEG/[pyPEG2]
|
||||||
* http://jinja.pocoo.org/[jinja2]
|
* http://jinja.pocoo.org/[jinja2]
|
||||||
@ -139,32 +141,33 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
// QUTE_AUTHORS_START
|
// QUTE_AUTHORS_START
|
||||||
* Florian Bruhin
|
* Florian Bruhin
|
||||||
* Antoni Boucher
|
* Antoni Boucher
|
||||||
* Bruno Oliveira
|
|
||||||
* Lamar Pavel
|
* Lamar Pavel
|
||||||
|
* Bruno Oliveira
|
||||||
* Alexander Cogneau
|
* Alexander Cogneau
|
||||||
* Martin Tournoij
|
* Martin Tournoij
|
||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
* Daniel
|
* Daniel
|
||||||
* Claude
|
* Claude
|
||||||
|
* meles5
|
||||||
* Nathan Isom
|
* Nathan Isom
|
||||||
* Austin Anderson
|
* Austin Anderson
|
||||||
* Artur Shaik
|
* Artur Shaik
|
||||||
* Thorsten Wißmann
|
* Thorsten Wißmann
|
||||||
* Alexey "Averrin" Nabrodov
|
* Alexey "Averrin" Nabrodov
|
||||||
* meles5
|
|
||||||
* ZDarian
|
* ZDarian
|
||||||
* John ShaggyTwoDope Jenkins
|
* John ShaggyTwoDope Jenkins
|
||||||
* Peter Vilim
|
* Peter Vilim
|
||||||
* Jonas Schürmann
|
* Jonas Schürmann
|
||||||
* Jimmy
|
* Jimmy
|
||||||
* skinnay
|
* skinnay
|
||||||
|
* error800
|
||||||
* Zach-Button
|
* Zach-Button
|
||||||
|
* Halfwit
|
||||||
* Felix Van der Jeugt
|
* Felix Van der Jeugt
|
||||||
* rikn00
|
* rikn00
|
||||||
* Patric Schmitz
|
* Patric Schmitz
|
||||||
* Martin Zimmermann
|
* Martin Zimmermann
|
||||||
* Error 800
|
|
||||||
* Brian Jackson
|
* Brian Jackson
|
||||||
* sbinix
|
* sbinix
|
||||||
* neeasade
|
* neeasade
|
||||||
@ -180,7 +183,6 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Fritz V155 Reichwald
|
* Fritz V155 Reichwald
|
||||||
* Franz Fellner
|
* Franz Fellner
|
||||||
* zwarag
|
* zwarag
|
||||||
* error800
|
|
||||||
* Tim Harder
|
* Tim Harder
|
||||||
* Thiago Barroso Perrotta
|
* Thiago Barroso Perrotta
|
||||||
* Matthias Lisin
|
* Matthias Lisin
|
||||||
|
60
misc/userscripts/qutedmenu
Executable file
60
misc/userscripts/qutedmenu
Executable 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
|
@ -36,3 +36,4 @@ qt_log_ignore =
|
|||||||
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
|
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
|
||||||
^QWaitCondition: Destroyed while threads are still waiting
|
^QWaitCondition: Destroyed while threads are still waiting
|
||||||
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
||||||
|
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||||
|
@ -814,12 +814,9 @@ class EventFilter(QObject):
|
|||||||
Return:
|
Return:
|
||||||
True if the event should be filtered, False if it's passed through.
|
True if the event should be filtered, False if it's passed through.
|
||||||
"""
|
"""
|
||||||
if qApp.overrideCursor() is None:
|
# Mouse cursor shown (overrideCursor None) -> don't filter event
|
||||||
# Mouse cursor shown -> don't filter event
|
# Mouse cursor hidden (overrideCursor not None) -> filter event
|
||||||
return False
|
return qApp.overrideCursor() is not None
|
||||||
else:
|
|
||||||
# Mouse cursor hidden -> filter event
|
|
||||||
return True
|
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
"""Handle an event.
|
"""Handle an event.
|
||||||
|
@ -152,30 +152,6 @@ class CommandDispatcher:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _scroll_percent(self, perc=None, count=None, orientation=None):
|
|
||||||
"""Inner logic for scroll_percent_(x|y).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
perc: How many percent to scroll, or None
|
|
||||||
count: How many percent to scroll, or None
|
|
||||||
orientation: Qt.Horizontal or Qt.Vertical
|
|
||||||
"""
|
|
||||||
if perc is None and count is None:
|
|
||||||
perc = 100
|
|
||||||
elif perc is None:
|
|
||||||
perc = count
|
|
||||||
if perc == 0:
|
|
||||||
self.scroll('top')
|
|
||||||
elif perc == 100:
|
|
||||||
self.scroll('bottom')
|
|
||||||
else:
|
|
||||||
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
|
||||||
frame = self._current_widget().page().currentFrame()
|
|
||||||
m = frame.scrollBarMaximum(orientation)
|
|
||||||
if m == 0:
|
|
||||||
return
|
|
||||||
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
|
||||||
|
|
||||||
def _tab_move_absolute(self, idx):
|
def _tab_move_absolute(self, idx):
|
||||||
"""Get an index for moving a tab absolutely.
|
"""Get an index for moving a tab absolutely.
|
||||||
|
|
||||||
@ -412,20 +388,27 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
def _back_forward(self, tab, bg, window, count, forward):
|
def _back_forward(self, tab, bg, window, count, forward):
|
||||||
"""Helper function for :back/:forward."""
|
"""Helper function for :back/:forward."""
|
||||||
if (not forward and not
|
# Catch common cases before e.g. cloning tab
|
||||||
self._current_widget().page().history().canGoBack()):
|
history = self._current_widget().page().history()
|
||||||
|
if not forward and not history.canGoBack():
|
||||||
raise cmdexc.CommandError("At beginning of history.")
|
raise cmdexc.CommandError("At beginning of history.")
|
||||||
if (forward and not
|
elif forward and not history.canGoForward():
|
||||||
self._current_widget().page().history().canGoForward()):
|
|
||||||
raise cmdexc.CommandError("At end of history.")
|
raise cmdexc.CommandError("At end of history.")
|
||||||
|
|
||||||
if tab or bg or window:
|
if tab or bg or window:
|
||||||
widget = self.tab_clone(bg, window)
|
widget = self.tab_clone(bg, window)
|
||||||
else:
|
else:
|
||||||
widget = self._current_widget()
|
widget = self._current_widget()
|
||||||
|
|
||||||
|
history = widget.page().history()
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
if forward:
|
if forward:
|
||||||
|
if not history.canGoForward():
|
||||||
|
raise cmdexc.CommandError("At end of history.")
|
||||||
widget.forward()
|
widget.forward()
|
||||||
else:
|
else:
|
||||||
|
if not history.canGoBack():
|
||||||
|
raise cmdexc.CommandError("At beginning of history.")
|
||||||
widget.back()
|
widget.back()
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
@ -538,7 +521,7 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1):
|
def scroll_px(self, dx: {'type': int}, dy: {'type': int}, count=1):
|
||||||
"""Scroll the current tab by 'count * dx/dy' pixels.
|
"""Scroll the current tab by 'count * dx/dy' pixels.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -555,8 +538,8 @@ class CommandDispatcher:
|
|||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
def scroll(self,
|
def scroll(self,
|
||||||
direction: {'type': (str, float)},
|
direction: {'type': (str, int)},
|
||||||
dy: {'type': float, 'hide': True}=None,
|
dy: {'type': int, 'hide': True}=None,
|
||||||
count=1):
|
count=1):
|
||||||
"""Scroll the current tab in the given direction.
|
"""Scroll the current tab in the given direction.
|
||||||
|
|
||||||
@ -569,8 +552,8 @@ class CommandDispatcher:
|
|||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
try:
|
try:
|
||||||
# Check for deprecated dx/dy form (like with scroll-px).
|
# Check for deprecated dx/dy form (like with scroll-px).
|
||||||
dx = float(direction)
|
dx = int(direction)
|
||||||
dy = float(dy)
|
dy = int(dy)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
# Invalid values will get handled later.
|
# Invalid values will get handled later.
|
||||||
pass
|
pass
|
||||||
@ -643,8 +626,24 @@ class CommandDispatcher:
|
|||||||
horizontal: Scroll horizontally instead of vertically.
|
horizontal: Scroll horizontally instead of vertically.
|
||||||
count: Percentage to scroll.
|
count: Percentage to scroll.
|
||||||
"""
|
"""
|
||||||
self._scroll_percent(perc, count,
|
if perc is None and count is None:
|
||||||
Qt.Horizontal if horizontal else Qt.Vertical)
|
perc = 100
|
||||||
|
elif perc is None:
|
||||||
|
perc = count
|
||||||
|
|
||||||
|
orientation = Qt.Horizontal if horizontal else Qt.Vertical
|
||||||
|
|
||||||
|
if perc == 0 and orientation == Qt.Vertical:
|
||||||
|
self.scroll('top')
|
||||||
|
elif perc == 100 and orientation == Qt.Vertical:
|
||||||
|
self.scroll('bottom')
|
||||||
|
else:
|
||||||
|
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
||||||
|
frame = self._current_widget().page().currentFrame()
|
||||||
|
m = frame.scrollBarMaximum(orientation)
|
||||||
|
if m == 0:
|
||||||
|
return
|
||||||
|
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
@ -686,7 +685,7 @@ class CommandDispatcher:
|
|||||||
pass
|
pass
|
||||||
elif mult_y < 0:
|
elif mult_y < 0:
|
||||||
self.scroll('page-up', count=-int(mult_y))
|
self.scroll('page-up', count=-int(mult_y))
|
||||||
elif mult_y > 0:
|
elif mult_y > 0: # pragma: no branch
|
||||||
self.scroll('page-down', count=int(mult_y))
|
self.scroll('page-down', count=int(mult_y))
|
||||||
mult_y = 0
|
mult_y = 0
|
||||||
if mult_x == 0 and mult_y == 0:
|
if mult_x == 0 and mult_y == 0:
|
||||||
|
@ -29,7 +29,8 @@ from qutebrowser.utils import message, log, objreg, qtutils
|
|||||||
from qutebrowser.misc import split
|
from qutebrowser.misc import split
|
||||||
|
|
||||||
|
|
||||||
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
|
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
|
||||||
|
'count'])
|
||||||
|
|
||||||
|
|
||||||
def replace_variables(win_id, arglist):
|
def replace_variables(win_id, arglist):
|
||||||
@ -117,6 +118,26 @@ class CommandRunner(QObject):
|
|||||||
for sub in sub_texts:
|
for sub in sub_texts:
|
||||||
yield self.parse(sub, *args, **kwargs)
|
yield self.parse(sub, *args, **kwargs)
|
||||||
|
|
||||||
|
def _parse_count(self, cmdstr):
|
||||||
|
"""Split a count prefix off from a command for parse().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmdstr: The command/args including the count.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A (count, cmdstr) tuple, with count being None or int.
|
||||||
|
"""
|
||||||
|
if ':' not in cmdstr:
|
||||||
|
return (None, cmdstr)
|
||||||
|
|
||||||
|
count, cmdstr = cmdstr.split(':', maxsplit=1)
|
||||||
|
try:
|
||||||
|
count = int(count)
|
||||||
|
except ValueError:
|
||||||
|
# We just ignore invalid prefixes
|
||||||
|
count = None
|
||||||
|
return (count, cmdstr)
|
||||||
|
|
||||||
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
||||||
"""Split the commandline text into command and arguments.
|
"""Split the commandline text into command and arguments.
|
||||||
|
|
||||||
@ -128,9 +149,11 @@ class CommandRunner(QObject):
|
|||||||
keep: Whether to keep special chars and whitespace
|
keep: Whether to keep special chars and whitespace
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A (cmd, args, cmdline) ParseResult tuple.
|
A ParseResult tuple.
|
||||||
"""
|
"""
|
||||||
cmdstr, sep, argstr = text.partition(' ')
|
cmdstr, sep, argstr = text.partition(' ')
|
||||||
|
count, cmdstr = self._parse_count(cmdstr)
|
||||||
|
|
||||||
if not cmdstr and not fallback:
|
if not cmdstr and not fallback:
|
||||||
raise cmdexc.NoSuchCommandError("No command given")
|
raise cmdexc.NoSuchCommandError("No command given")
|
||||||
if aliases:
|
if aliases:
|
||||||
@ -161,7 +184,7 @@ class CommandRunner(QObject):
|
|||||||
cmdline = [cmdstr, sep]
|
cmdline = [cmdstr, sep]
|
||||||
else:
|
else:
|
||||||
cmdline = [cmdstr] + args[:]
|
cmdline = [cmdstr] + args[:]
|
||||||
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
|
return ParseResult(cmd=cmd, args=args, cmdline=cmdline, count=count)
|
||||||
|
|
||||||
def _split_args(self, cmd, argstr, keep):
|
def _split_args(self, cmd, argstr, keep):
|
||||||
"""Split the arguments from an arg string.
|
"""Split the arguments from an arg string.
|
||||||
@ -216,7 +239,12 @@ class CommandRunner(QObject):
|
|||||||
for result in self.parse_all(text):
|
for result in self.parse_all(text):
|
||||||
args = replace_variables(self._win_id, result.args)
|
args = replace_variables(self._win_id, result.args)
|
||||||
if count is not None:
|
if count is not None:
|
||||||
|
if result.count is not None:
|
||||||
|
raise cmdexc.CommandMetaError("Got count via command and "
|
||||||
|
"prefix!")
|
||||||
result.cmd.run(self._win_id, args, count=count)
|
result.cmd.run(self._win_id, args, count=count)
|
||||||
|
elif result.count is not None:
|
||||||
|
result.cmd.run(self._win_id, args, count=result.count)
|
||||||
else:
|
else:
|
||||||
result.cmd.run(self._win_id, args)
|
result.cmd.run(self._win_id, args)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
cat = self.new_category("Sections")
|
cat = self.new_category("Sections")
|
||||||
for name in configdata.DATA.keys():
|
for name in configdata.DATA:
|
||||||
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
|
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
|
||||||
self.new_item(cat, name, desc)
|
self.new_item(cat, name, desc)
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
|
|||||||
self._misc_items = {}
|
self._misc_items = {}
|
||||||
self._section = section
|
self._section = section
|
||||||
objreg.get('config').changed.connect(self.update_misc_column)
|
objreg.get('config').changed.connect(self.update_misc_column)
|
||||||
for name in sectdata.keys():
|
for name in sectdata:
|
||||||
try:
|
try:
|
||||||
desc = sectdata.descriptions[name]
|
desc = sectdata.descriptions[name]
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
|
@ -70,7 +70,7 @@ def _init_setting_completions():
|
|||||||
model = configmodel.SettingOptionCompletionModel(sectname)
|
model = configmodel.SettingOptionCompletionModel(sectname)
|
||||||
_instances[usertypes.Completion.option][sectname] = model
|
_instances[usertypes.Completion.option][sectname] = model
|
||||||
_instances[usertypes.Completion.value][sectname] = {}
|
_instances[usertypes.Completion.value][sectname] = {}
|
||||||
for opt in configdata.DATA[sectname].keys():
|
for opt in configdata.DATA[sectname]:
|
||||||
model = configmodel.SettingValueCompletionModel(sectname, opt)
|
model = configmodel.SettingValueCompletionModel(sectname, opt)
|
||||||
_instances[usertypes.Completion.value][sectname][opt] = model
|
_instances[usertypes.Completion.value][sectname][opt] = model
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class HelpCompletionModel(base.BaseCompletionModel):
|
|||||||
"""Fill completion with section->option entries."""
|
"""Fill completion with section->option entries."""
|
||||||
cat = self.new_category("Settings")
|
cat = self.new_category("Settings")
|
||||||
for sectname, sectdata in configdata.DATA.items():
|
for sectname, sectdata in configdata.DATA.items():
|
||||||
for optname in sectdata.keys():
|
for optname in sectdata:
|
||||||
try:
|
try:
|
||||||
desc = sectdata.descriptions[optname]
|
desc = sectdata.descriptions[optname]
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
|
@ -368,7 +368,7 @@ class ConfigManager(QObject):
|
|||||||
self.sections = configdata.data()
|
self.sections = configdata.data()
|
||||||
self._interpolation = configparser.ExtendedInterpolation()
|
self._interpolation = configparser.ExtendedInterpolation()
|
||||||
self._proxies = {}
|
self._proxies = {}
|
||||||
for sectname in self.sections.keys():
|
for sectname in self.sections:
|
||||||
self._proxies[sectname] = SectionProxy(self, sectname)
|
self._proxies[sectname] = SectionProxy(self, sectname)
|
||||||
self._fname = fname
|
self._fname = fname
|
||||||
if configdir is None:
|
if configdir is None:
|
||||||
|
@ -1569,49 +1569,41 @@ class UserAgent(BaseType):
|
|||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
self._basic_validation(value)
|
self._basic_validation(value)
|
||||||
|
|
||||||
|
# To update the following list of user agents, run the script 'ua_fetch.py'
|
||||||
|
# Vim-protip: Place your cursor below this comment and run
|
||||||
|
# :r!python scripts/dev/ua_fetch.py
|
||||||
def complete(self):
|
def complete(self):
|
||||||
"""Complete a list of common user agents."""
|
"""Complete a list of common user agents."""
|
||||||
out = [
|
out = [
|
||||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
|
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 '
|
||||||
'Firefox/35.0',
|
'Firefox/41.0',
|
||||||
"Firefox 35.0 Win7 64-bit"),
|
"Firefox 41.0 Win7 64-bit"),
|
||||||
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
|
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) '
|
||||||
'Firefox/35.0',
|
'Gecko/20100101 Firefox/41.0',
|
||||||
"Firefox 35.0 Ubuntu"),
|
"Firefox 41.0 MacOSX"),
|
||||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
|
('Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 '
|
||||||
'Gecko/20100101 Firefox/35.0',
|
'Firefox/41.0',
|
||||||
"Firefox 35.0 MacOSX"),
|
"Firefox 41.0 Linux"),
|
||||||
|
|
||||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) '
|
||||||
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
|
'AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 '
|
||||||
'Safari/600.3.18',
|
'Safari/601.2.7',
|
||||||
"Safari 8.0 MacOSX"),
|
"Safari Generic MacOSX"),
|
||||||
|
('Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) '
|
||||||
|
'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 '
|
||||||
|
'Mobile/13B143 Safari/601.1',
|
||||||
|
"Mobile Safari Generic iOS"),
|
||||||
|
|
||||||
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
||||||
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||||
"Chrome 40.0 Win7 64-bit"),
|
"Chrome 46.0 Win7 64-bit"),
|
||||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) '
|
||||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
|
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 '
|
||||||
'Safari/537.36',
|
'Safari/537.36',
|
||||||
"Chrome 40.0 MacOSX"),
|
"Chrome 46.0 MacOSX"),
|
||||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, '
|
||||||
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||||
"Chrome 40.0 Linux"),
|
"Chrome 46.0 Linux"),
|
||||||
|
|
||||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
|
||||||
'Gecko',
|
|
||||||
"IE 11.0 Win7 64-bit"),
|
|
||||||
|
|
||||||
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
|
|
||||||
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
|
|
||||||
'Mobile/12B440 Safari/600.1.4',
|
|
||||||
"Mobile Safari 8.0 iOS"),
|
|
||||||
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
|
|
||||||
"Firefox 35, Android"),
|
|
||||||
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
|
|
||||||
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
|
|
||||||
'Chrome/37.0.0.0 Mobile Safari/537.36',
|
|
||||||
"Android Browser"),
|
|
||||||
|
|
||||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||||
'+http://www.google.com/bot.html',
|
'+http://www.google.com/bot.html',
|
||||||
@ -1619,7 +1611,11 @@ class UserAgent(BaseType):
|
|||||||
('Wget/1.16.1 (linux-gnu)',
|
('Wget/1.16.1 (linux-gnu)',
|
||||||
"wget 1.16.1"),
|
"wget 1.16.1"),
|
||||||
('curl/7.40.0',
|
('curl/7.40.0',
|
||||||
"curl 7.40.0")
|
"curl 7.40.0"),
|
||||||
|
|
||||||
|
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||||
|
'Gecko',
|
||||||
|
"IE 11.0 for Desktop Win7 64-bit")
|
||||||
]
|
]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@ -273,10 +273,8 @@ class KeyConfigParser(QObject):
|
|||||||
return True
|
return True
|
||||||
if keychain in bindings:
|
if keychain in bindings:
|
||||||
return False
|
return False
|
||||||
elif command in bindings.values():
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
return True
|
return command not in bindings.values()
|
||||||
|
|
||||||
def _read(self, relaxed=False):
|
def _read(self, relaxed=False):
|
||||||
"""Read the config file from disk and parse it.
|
"""Read the config file from disk and parse it.
|
||||||
|
@ -371,10 +371,8 @@ class QtWarningFilter(logging.Filter):
|
|||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
"""Determine if the specified record is to be logged."""
|
"""Determine if the specified record is to be logged."""
|
||||||
if record.msg.strip().startswith(self._pattern):
|
do_log = not record.msg.strip().startswith(self._pattern)
|
||||||
return False # filter
|
return do_log
|
||||||
else:
|
|
||||||
return True # log
|
|
||||||
|
|
||||||
|
|
||||||
class LogFilter(logging.Filter):
|
class LogFilter(logging.Filter):
|
||||||
|
@ -122,10 +122,7 @@ def _is_url_naive(urlstr):
|
|||||||
if not QHostAddress(urlstr).isNull():
|
if not QHostAddress(urlstr).isNull():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if '.' in url.host():
|
return '.' in url.host()
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _is_url_dns(urlstr):
|
def _is_url_dns(urlstr):
|
||||||
@ -254,10 +251,7 @@ def is_url(urlstr):
|
|||||||
# no autosearch, so everything is a URL unless it has an explicit
|
# no autosearch, so everything is a URL unless it has an explicit
|
||||||
# search engine.
|
# search engine.
|
||||||
engine, _term = _parse_search_term(urlstr)
|
engine, _term = _parse_search_term(urlstr)
|
||||||
if engine is None:
|
return engine is None
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not qurl_userinput.isValid():
|
if not qurl_userinput.isValid():
|
||||||
# This will also catch URLs containing spaces.
|
# This will also catch URLs containing spaces.
|
||||||
|
@ -20,60 +20,213 @@
|
|||||||
|
|
||||||
"""Generate the html documentation based on the asciidoc files."""
|
"""Generate the html documentation based on the asciidoc files."""
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import glob
|
import glob
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
import argparse
|
import argparse
|
||||||
|
import io
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from scripts import utils
|
from scripts import utils
|
||||||
|
|
||||||
|
|
||||||
def _get_asciidoc_cmd(args):
|
class AsciiDoc:
|
||||||
"""Try to find out what commandline to use to invoke asciidoc."""
|
|
||||||
if args.asciidoc is not None:
|
|
||||||
return args.asciidoc
|
|
||||||
|
|
||||||
try:
|
"""Abstraction of an asciidoc subprocess."""
|
||||||
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return ['asciidoc']
|
|
||||||
|
|
||||||
try:
|
FILES = [
|
||||||
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL,
|
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
||||||
stderr=subprocess.DEVNULL)
|
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
|
||||||
except OSError:
|
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
|
||||||
pass
|
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
|
||||||
else:
|
]
|
||||||
return ['asciidoc.py']
|
|
||||||
|
|
||||||
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):
|
def cleanup(self):
|
||||||
"""Call asciidoc for the given files.
|
"""Clean up the temporary home directory for asciidoc."""
|
||||||
|
if self._homedir is not None and not self._failed:
|
||||||
|
shutil.rmtree(self._homedir)
|
||||||
|
|
||||||
Args:
|
def build(self):
|
||||||
args: The asciidoc binary to use, as a list.
|
if self._args.website:
|
||||||
src: The source .asciidoc file.
|
self._build_website()
|
||||||
dst: The destination .html file, or None to auto-guess.
|
else:
|
||||||
"""
|
self._build_docs()
|
||||||
print("Calling asciidoc for {}...".format(os.path.basename(src)))
|
|
||||||
args = args[:]
|
def _build_docs(self):
|
||||||
if dst is not None:
|
"""Render .asciidoc files to .html sites."""
|
||||||
args += ['--out-file', dst]
|
files = self.FILES[:]
|
||||||
args.append(src)
|
for src in glob.glob('doc/help/*.asciidoc'):
|
||||||
try:
|
name, _ext = os.path.splitext(os.path.basename(src))
|
||||||
subprocess.check_call(args)
|
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||||
except (subprocess.CalledProcessError, OSError) as e:
|
files.append((src, dst))
|
||||||
utils.print_col(str(e), 'red')
|
for src, dst in files:
|
||||||
sys.exit(1)
|
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):
|
def main(colors=False):
|
||||||
@ -81,53 +234,31 @@ def main(colors=False):
|
|||||||
utils.change_cwd()
|
utils.change_cwd()
|
||||||
utils.use_color = colors
|
utils.use_color = colors
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--all', help="Build all documentation into a given "
|
parser.add_argument('--website', help="Build website into a given "
|
||||||
"directory.", nargs=1)
|
"directory.", nargs=1)
|
||||||
parser.add_argument('--asciidoc', help="Full path to python and "
|
parser.add_argument('--asciidoc', help="Full path to python and "
|
||||||
"asciidoc.py. If not given, it's searched in PATH.",
|
"asciidoc.py. If not given, it's searched in PATH.",
|
||||||
nargs=2, required=False,
|
nargs=2, required=False,
|
||||||
metavar=('PYTHON', 'ASCIIDOC'))
|
metavar=('PYTHON', 'ASCIIDOC'))
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
asciidoc_files = [
|
|
||||||
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
|
||||||
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
|
|
||||||
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
|
|
||||||
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
os.mkdir('qutebrowser/html/doc')
|
os.mkdir('qutebrowser/html/doc')
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
asciidoc = AsciiDoc(args)
|
||||||
try:
|
try:
|
||||||
asciidoc = _get_asciidoc_cmd(args)
|
asciidoc.prepare()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
utils.print_col("Could not find asciidoc! Please install it, or use "
|
utils.print_col("Could not find asciidoc! Please install it, or use "
|
||||||
"the --asciidoc argument to point this script to the "
|
"the --asciidoc argument to point this script to the "
|
||||||
"correct python/asciidoc.py location!", 'red')
|
"correct python/asciidoc.py location!", 'red')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.all:
|
|
||||||
for root, _dirs, files in os.walk(os.getcwd()):
|
try:
|
||||||
for filename in files:
|
asciidoc.build()
|
||||||
if os.path.splitext(filename)[1] != '.asciidoc':
|
finally:
|
||||||
continue
|
asciidoc.cleanup()
|
||||||
src = os.path.join(root, filename)
|
|
||||||
parts = [args.all[0]]
|
|
||||||
dirname = os.path.dirname(src)
|
|
||||||
if dirname:
|
|
||||||
parts.append(os.path.relpath(os.path.dirname(src)))
|
|
||||||
parts.append(
|
|
||||||
os.extsep.join((os.path.splitext(os.path.basename(src))[0],
|
|
||||||
'html')))
|
|
||||||
dst = os.path.join(*parts)
|
|
||||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
||||||
call_asciidoc(asciidoc, src, dst)
|
|
||||||
else:
|
|
||||||
for src in glob.glob('doc/help/*.asciidoc'):
|
|
||||||
name, _ext = os.path.splitext(os.path.basename(src))
|
|
||||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
|
||||||
asciidoc_files.append((src, dst))
|
|
||||||
for src, dst in asciidoc_files:
|
|
||||||
call_asciidoc(asciidoc, src, dst)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -34,7 +34,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
|||||||
|
|
||||||
from scripts import utils
|
from scripts import utils
|
||||||
|
|
||||||
|
|
||||||
Message = collections.namedtuple('Message', 'typ, text')
|
Message = collections.namedtuple('Message', 'typ, text')
|
||||||
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
|
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
|
||||||
|
|
||||||
@ -143,6 +142,18 @@ class Skipped(Exception):
|
|||||||
super().__init__("Skipping coverage checks " + reason)
|
super().__init__("Skipping coverage checks " + reason)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_filename(filename):
|
||||||
|
"""Transform the absolute test filenames to relative ones."""
|
||||||
|
if os.path.isabs(filename):
|
||||||
|
basedir = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
common_path = os.path.commonprefix([basedir, filename])
|
||||||
|
if common_path:
|
||||||
|
filename = filename[len(common_path):].lstrip('/')
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def check(fileobj, perfect_files):
|
def check(fileobj, perfect_files):
|
||||||
"""Main entry point which parses/checks coverage.xml if applicable."""
|
"""Main entry point which parses/checks coverage.xml if applicable."""
|
||||||
if sys.platform != 'linux':
|
if sys.platform != 'linux':
|
||||||
@ -151,6 +162,8 @@ def check(fileobj, perfect_files):
|
|||||||
raise Skipped("because -k is given.")
|
raise Skipped("because -k is given.")
|
||||||
elif '-m' in sys.argv[1:]:
|
elif '-m' in sys.argv[1:]:
|
||||||
raise Skipped("because -m is given.")
|
raise Skipped("because -m is given.")
|
||||||
|
elif '--lf' in sys.argv[1:]:
|
||||||
|
raise Skipped("because --lf is given.")
|
||||||
|
|
||||||
perfect_src_files = [e[1] for e in perfect_files]
|
perfect_src_files = [e[1] for e in perfect_files]
|
||||||
|
|
||||||
@ -168,7 +181,8 @@ def check(fileobj, perfect_files):
|
|||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
for klass in classes:
|
for klass in classes:
|
||||||
filename = klass.attrib['filename']
|
filename = _get_filename(klass.attrib['filename'])
|
||||||
|
|
||||||
line_cov = float(klass.attrib['line-rate']) * 100
|
line_cov = float(klass.attrib['line-rate']) * 100
|
||||||
branch_cov = float(klass.attrib['branch-rate']) * 100
|
branch_cov = float(klass.attrib['branch-rate']) * 100
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ INSTALL_PYQT = TESTENV in ('py34', 'py35', 'unittests-nodisp', 'vulture',
|
|||||||
'pylint')
|
'pylint')
|
||||||
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
|
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
|
||||||
pip_packages = ['tox']
|
pip_packages = ['tox']
|
||||||
if TESTENV in ['py34', 'py35']:
|
if TESTENV in ['py34', 'py35'] and TRAVIS_OS == 'linux':
|
||||||
pip_packages.append('codecov')
|
pip_packages.append('codecov')
|
||||||
|
|
||||||
|
|
||||||
@ -69,9 +69,12 @@ def check_setup(executable):
|
|||||||
|
|
||||||
if 'APPVEYOR' in os.environ:
|
if 'APPVEYOR' in os.environ:
|
||||||
print("Getting PyQt5...")
|
print("Getting PyQt5...")
|
||||||
urllib.urlretrieve(
|
qt_version = '5.5.1'
|
||||||
'http://www.qutebrowser.org/pyqt/PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe',
|
pyqt_version = '5.5.1'
|
||||||
r'C:\install-PyQt5.exe')
|
pyqt_url = ('http://www.qutebrowser.org/pyqt/'
|
||||||
|
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
|
||||||
|
pyqt_version, qt_version))
|
||||||
|
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||||
|
|
||||||
print("Fixing registry...")
|
print("Fixing registry...")
|
||||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||||
|
@ -93,10 +93,8 @@ def is_qutebrowser_dump(parsed):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return '-m qutebrowser' in cmdline
|
return '-m qutebrowser' in cmdline
|
||||||
elif basename == 'qutebrowser':
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
return False
|
return basename == 'qutebrowser'
|
||||||
|
|
||||||
|
|
||||||
def dump_infos_gdb(parsed):
|
def dump_infos_gdb(parsed):
|
||||||
|
@ -27,7 +27,7 @@ import sys
|
|||||||
import pytest
|
import pytest
|
||||||
import pytestqt.plugin
|
import pytestqt.plugin
|
||||||
import pytest_mock
|
import pytest_mock
|
||||||
import pytest_capturelog
|
import pytest_catchlog
|
||||||
|
|
||||||
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
|
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
|
||||||
pytest_capturelog]))
|
pytest_catchlog]))
|
||||||
|
@ -111,11 +111,7 @@ def filter_func(item):
|
|||||||
True if the missing function should be filtered/ignored, False
|
True if the missing function should be filtered/ignored, False
|
||||||
otherwise.
|
otherwise.
|
||||||
"""
|
"""
|
||||||
if re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)):
|
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
|
||||||
# probably a virtual Qt method
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def report(items):
|
def report(items):
|
||||||
|
@ -389,6 +389,8 @@ def _get_authors():
|
|||||||
'binix': 'sbinix',
|
'binix': 'sbinix',
|
||||||
'Averrin': 'Alexey "Averrin" Nabrodov',
|
'Averrin': 'Alexey "Averrin" Nabrodov',
|
||||||
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
|
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
|
||||||
|
'Michael': 'Halfwit',
|
||||||
|
'Error 800': 'error800',
|
||||||
}
|
}
|
||||||
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
||||||
authors = [corrections.get(author, author)
|
authors = [corrections.get(author, author)
|
||||||
|
124
scripts/dev/ua_fetch.py
Normal file
124
scripts/dev/ua_fetch.py
Normal 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()
|
@ -33,7 +33,7 @@ import pytest
|
|||||||
|
|
||||||
import helpers.stubs as stubsmod
|
import helpers.stubs as stubsmod
|
||||||
from helpers import logfail
|
from helpers import logfail
|
||||||
from helpers.logfail import fail_on_logging, caplog_bug_workaround
|
from helpers.logfail import fail_on_logging
|
||||||
from helpers.messagemock import message_mock
|
from helpers.messagemock import message_mock
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import objreg
|
from qutebrowser.utils import objreg
|
||||||
@ -121,11 +121,8 @@ def pytest_collection_modifyitems(items):
|
|||||||
def pytest_ignore_collect(path):
|
def pytest_ignore_collect(path):
|
||||||
"""Ignore BDD tests during collection if frozen."""
|
"""Ignore BDD tests during collection if frozen."""
|
||||||
rel_path = path.relto(os.path.dirname(__file__))
|
rel_path = path.relto(os.path.dirname(__file__))
|
||||||
if (rel_path == os.path.join('integration', 'features') and
|
return (rel_path == os.path.join('integration', 'features') and
|
||||||
hasattr(sys, 'frozen')):
|
hasattr(sys, 'frozen'))
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
|
@ -24,7 +24,7 @@ import logging
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pytest_capturelog as caplog_mod
|
import pytest_catchlog as catchlog_mod
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# When using pytest for pyflakes/pep8/..., the plugin won't be available
|
# When using pytest for pyflakes/pep8/..., the plugin won't be available
|
||||||
# but conftest.py will still be loaded.
|
# but conftest.py will still be loaded.
|
||||||
@ -47,18 +47,18 @@ class LogFailHandler(logging.Handler):
|
|||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
for h in root_logger.handlers:
|
for h in root_logger.handlers:
|
||||||
if isinstance(h, caplog_mod.CaptureLogHandler):
|
if isinstance(h, catchlog_mod.LogCaptureHandler):
|
||||||
caplog_handler = h
|
catchlog_handler = h
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# The CaptureLogHandler is not available anymore during fixture
|
# The LogCaptureHandler is not available anymore during fixture
|
||||||
# teardown, so we ignore logging messages emitted there..
|
# teardown, so we ignore logging messages emitted there..
|
||||||
return
|
return
|
||||||
|
|
||||||
if (logger.level == record.levelno or
|
if (logger.level == record.levelno or
|
||||||
caplog_handler.level == record.levelno):
|
catchlog_handler.level == record.levelno):
|
||||||
# caplog.atLevel(...) was used with the level of this message, i.e.
|
# caplog.at_level(...) was used with the level of this message,
|
||||||
# it was expected.
|
# i.e. it was expected.
|
||||||
return
|
return
|
||||||
if record.levelno < self._min_level:
|
if record.levelno < self._min_level:
|
||||||
return
|
return
|
||||||
@ -74,25 +74,3 @@ def fail_on_logging():
|
|||||||
yield
|
yield
|
||||||
logging.getLogger().removeHandler(handler)
|
logging.getLogger().removeHandler(handler)
|
||||||
handler.close()
|
handler.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture(autouse=True)
|
|
||||||
def caplog_bug_workaround(request):
|
|
||||||
"""WORKAROUND for pytest-capturelog bug.
|
|
||||||
|
|
||||||
https://bitbucket.org/memedough/pytest-capturelog/issues/7/
|
|
||||||
|
|
||||||
This would lead to LogFailHandler failing after skipped tests as there are
|
|
||||||
multiple CaptureLogHandlers.
|
|
||||||
"""
|
|
||||||
yield
|
|
||||||
if caplog_mod is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
caplog_handlers = [h for h in root_logger.handlers
|
|
||||||
if isinstance(h, caplog_mod.CaptureLogHandler)]
|
|
||||||
|
|
||||||
for h in caplog_handlers:
|
|
||||||
root_logger.removeHandler(h)
|
|
||||||
h.close()
|
|
||||||
|
@ -61,7 +61,7 @@ class MessageMock:
|
|||||||
}
|
}
|
||||||
log_level = log_levels[level]
|
log_level = log_levels[level]
|
||||||
|
|
||||||
with self._caplog.atLevel(log_level): # needed so we don't fail
|
with self._caplog.at_level(log_level): # needed so we don't fail
|
||||||
logging.getLogger('message').log(log_level, text)
|
logging.getLogger('message').log(log_level, text)
|
||||||
|
|
||||||
self.messages.append(Message(level, win_id, text, immediately))
|
self.messages.append(Message(level, win_id, text, immediately))
|
||||||
|
@ -48,3 +48,24 @@ def test_partial_compare_equal(val1, val2):
|
|||||||
])
|
])
|
||||||
def test_partial_compare_not_equal(val1, val2):
|
def test_partial_compare_not_equal(val1, val2):
|
||||||
assert not utils.partial_compare(val1, val2)
|
assert not utils.partial_compare(val1, val2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pattern, value, expected', [
|
||||||
|
('foo', 'foo', True),
|
||||||
|
('foo', 'bar', False),
|
||||||
|
('foo', 'Foo', False),
|
||||||
|
('foo', 'foobar', False),
|
||||||
|
('foo', 'barfoo', False),
|
||||||
|
|
||||||
|
('foo*', 'foobarbaz', True),
|
||||||
|
('*bar', 'foobar', True),
|
||||||
|
('foo*baz', 'foobarbaz', True),
|
||||||
|
|
||||||
|
('foo[b]ar', 'foobar', False),
|
||||||
|
('foo[b]ar', 'foo[b]ar', True),
|
||||||
|
|
||||||
|
('foo?ar', 'foobar', False),
|
||||||
|
('foo?ar', 'foo?ar', True),
|
||||||
|
])
|
||||||
|
def test_pattern_match(pattern, value, expected):
|
||||||
|
assert utils.pattern_match(pattern=pattern, value=value) == expected
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_capturelog # pylint: disable=import-error
|
import pytest_catchlog # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
def test_log_debug():
|
def test_log_debug():
|
||||||
@ -36,33 +36,33 @@ def test_log_warning():
|
|||||||
|
|
||||||
|
|
||||||
def test_log_expected(caplog):
|
def test_log_expected(caplog):
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
logging.error('foo')
|
logging.error('foo')
|
||||||
|
|
||||||
|
|
||||||
def test_log_expected_logger(caplog):
|
def test_log_expected_logger(caplog):
|
||||||
logger = 'logfail_test_logger'
|
logger = 'logfail_test_logger'
|
||||||
with caplog.atLevel(logging.ERROR, logger):
|
with caplog.at_level(logging.ERROR, logger):
|
||||||
logging.getLogger(logger).error('foo')
|
logging.getLogger(logger).error('foo')
|
||||||
|
|
||||||
|
|
||||||
def test_log_expected_wrong_level(caplog):
|
def test_log_expected_wrong_level(caplog):
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
logging.critical('foo')
|
logging.critical('foo')
|
||||||
|
|
||||||
|
|
||||||
def test_log_expected_logger_wrong_level(caplog):
|
def test_log_expected_logger_wrong_level(caplog):
|
||||||
logger = 'logfail_test_logger'
|
logger = 'logfail_test_logger'
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
with caplog.atLevel(logging.ERROR, logger):
|
with caplog.at_level(logging.ERROR, logger):
|
||||||
logging.getLogger(logger).critical('foo')
|
logging.getLogger(logger).critical('foo')
|
||||||
|
|
||||||
|
|
||||||
def test_log_expected_wrong_logger(caplog):
|
def test_log_expected_wrong_logger(caplog):
|
||||||
logger = 'logfail_test_logger'
|
logger = 'logfail_test_logger'
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
with caplog.atLevel(logging.ERROR, logger):
|
with caplog.at_level(logging.ERROR, logger):
|
||||||
logging.error('foo')
|
logging.error('foo')
|
||||||
|
|
||||||
|
|
||||||
@ -82,6 +82,6 @@ def test_caplog_bug_workaround_2():
|
|||||||
"""
|
"""
|
||||||
caplog_handler = None
|
caplog_handler = None
|
||||||
for h in logging.getLogger().handlers:
|
for h in logging.getLogger().handlers:
|
||||||
if isinstance(h, pytest_capturelog.CaptureLogHandler):
|
if isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||||
assert caplog_handler is None
|
assert caplog_handler is None
|
||||||
caplog_handler = h
|
caplog_handler = h
|
||||||
|
@ -20,32 +20,39 @@
|
|||||||
"""Partial comparison of dicts/lists."""
|
"""Partial comparison of dicts/lists."""
|
||||||
|
|
||||||
|
|
||||||
import fnmatch
|
import re
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
|
||||||
def _partial_compare_dict(val1, val2):
|
def print_i(text, indent, error=False):
|
||||||
|
if error:
|
||||||
|
text = '| ****** {} ******'.format(text)
|
||||||
|
for line in text.splitlines():
|
||||||
|
print('| ' * indent + line)
|
||||||
|
|
||||||
|
|
||||||
|
def _partial_compare_dict(val1, val2, *, indent=0):
|
||||||
for key in val2:
|
for key in val2:
|
||||||
if key not in val1:
|
if key not in val1:
|
||||||
print("Key {!r} is in second dict but not in first!".format(key))
|
print_i("Key {!r} is in second dict but not in first!".format(key),
|
||||||
|
indent, error=True)
|
||||||
return False
|
return False
|
||||||
if not partial_compare(val1[key], val2[key]):
|
if not partial_compare(val1[key], val2[key], indent=indent+1):
|
||||||
print("Comparison failed for {!r} and {!r}!".format(
|
|
||||||
val1[key], val2[key]))
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _partial_compare_list(val1, val2):
|
def _partial_compare_list(val1, val2, *, indent=0):
|
||||||
if len(val1) < len(val2):
|
if len(val1) < len(val2):
|
||||||
print("Second list is longer than first list -> False!")
|
print_i("Second list is longer than first list", indent, error=True)
|
||||||
return False
|
return False
|
||||||
for item1, item2 in zip(val1, val2):
|
for item1, item2 in zip(val1, val2):
|
||||||
if not partial_compare(item1, item2):
|
if not partial_compare(item1, item2, indent=indent+1):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def partial_compare(val1, val2):
|
def partial_compare(val1, val2, *, indent=0):
|
||||||
"""Do a partial comparison between the given values.
|
"""Do a partial comparison between the given values.
|
||||||
|
|
||||||
For dicts, keys in val2 are checked, others are ignored.
|
For dicts, keys in val2 are checked, others are ignored.
|
||||||
@ -54,31 +61,44 @@ def partial_compare(val1, val2):
|
|||||||
|
|
||||||
This happens recursively.
|
This happens recursively.
|
||||||
"""
|
"""
|
||||||
print()
|
print_i("Comparing", indent)
|
||||||
print("Comparing\n {!r}\nto\n {!r}".format(val1, val2))
|
print_i(pprint.pformat(val1), indent + 1)
|
||||||
|
print_i("|---- to ----", indent)
|
||||||
|
print_i(pprint.pformat(val2), indent + 1)
|
||||||
|
|
||||||
|
|
||||||
if val2 is Ellipsis:
|
if val2 is Ellipsis:
|
||||||
print("Ignoring ellipsis comparison")
|
print_i("Ignoring ellipsis comparison", indent, error=True)
|
||||||
return True
|
return True
|
||||||
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
|
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
|
||||||
print("Different types ({}, {}) -> False".format(
|
print_i("Different types ({}, {}) -> False".format(
|
||||||
type(val1), type(val2)))
|
type(val1), type(val2)), indent, error=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if isinstance(val2, dict):
|
if isinstance(val2, dict):
|
||||||
print("Comparing as dicts")
|
print_i("|======= Comparing as dicts", indent)
|
||||||
equal = _partial_compare_dict(val1, val2)
|
equal = _partial_compare_dict(val1, val2, indent=indent)
|
||||||
elif isinstance(val2, list):
|
elif isinstance(val2, list):
|
||||||
print("Comparing as lists")
|
print_i("|======= Comparing as lists", indent)
|
||||||
equal = _partial_compare_list(val1, val2)
|
equal = _partial_compare_list(val1, val2, indent=indent)
|
||||||
elif isinstance(val2, float):
|
elif isinstance(val2, float):
|
||||||
print("Doing float comparison")
|
print_i("|======= Doing float comparison", indent)
|
||||||
equal = abs(val1 - val2) < 0.00001
|
equal = abs(val1 - val2) < 0.00001
|
||||||
elif isinstance(val2, str):
|
elif isinstance(val2, str):
|
||||||
print("Doing string comparison")
|
print_i("|======= Doing string comparison", indent)
|
||||||
equal = fnmatch.fnmatchcase(val1, val2)
|
equal = pattern_match(pattern=val2, value=val1)
|
||||||
else:
|
else:
|
||||||
print("Comparing via ==")
|
print_i("|======= Comparing via ==", indent)
|
||||||
equal = val1 == val2
|
equal = val1 == val2
|
||||||
print("---> {}".format(equal))
|
print_i("---> {}".format(equal), indent)
|
||||||
return equal
|
return equal
|
||||||
|
|
||||||
|
|
||||||
|
def pattern_match(*, pattern, value):
|
||||||
|
"""Do fnmatch.fnmatchcase like matching, but only with * active.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True on a match, False otherwise.
|
||||||
|
"""
|
||||||
|
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*'))
|
||||||
|
return re.fullmatch(re_pattern, value) is not None
|
||||||
|
1
tests/integration/data/backforward/3.txt
Normal file
1
tests/integration/data/backforward/3.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello World!
|
1
tests/integration/data/hello2.txt
Normal file
1
tests/integration/data/hello2.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello World 2!
|
1
tests/integration/data/hello3.txt
Normal file
1
tests/integration/data/hello3.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello World 3!
|
@ -207,5 +207,8 @@
|
|||||||
198
|
198
|
||||||
199
|
199
|
||||||
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet.
|
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet.
|
||||||
|
</pre>
|
||||||
|
<a href="/data/hello2.txt">next</a> link to test the --top-navigate argument for :scroll-page.
|
||||||
|
<a href="/data/hello3.txt">prev</a> link to test the --bottom-navigate argument for :scroll-page.
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -43,6 +43,19 @@ Feature: Going back and forward.
|
|||||||
url: http://localhost:*/data/backforward/1.txt
|
url: http://localhost:*/data/backforward/1.txt
|
||||||
- url: http://localhost:*/data/backforward/2.txt
|
- url: http://localhost:*/data/backforward/2.txt
|
||||||
|
|
||||||
|
Scenario: Going back in a new tab without history
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :back -t
|
||||||
|
Then the error "At beginning of history." should be shown.
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/backforward/1.txt
|
||||||
|
|
||||||
Scenario: Going back in a new background tab
|
Scenario: Going back in a new background tab
|
||||||
Given I open data/backforward/1.txt
|
Given I open data/backforward/1.txt
|
||||||
When I open data/backforward/2.txt
|
When I open data/backforward/2.txt
|
||||||
@ -62,6 +75,31 @@ Feature: Going back and forward.
|
|||||||
url: http://localhost:*/data/backforward/1.txt
|
url: http://localhost:*/data/backforward/1.txt
|
||||||
- url: http://localhost:*/data/backforward/2.txt
|
- url: http://localhost:*/data/backforward/2.txt
|
||||||
|
|
||||||
|
Scenario: Going back with count.
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I open data/backforward/2.txt
|
||||||
|
And I open data/backforward/3.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :back with count 2
|
||||||
|
And I wait until data/backforward/1.txt is loaded
|
||||||
|
And I reload
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/backforward/1.txt
|
||||||
|
- url: http://localhost:*/data/backforward/2.txt
|
||||||
|
- url: http://localhost:*/data/backforward/3.txt
|
||||||
|
|
||||||
|
Scenario: Going back with very big count.
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I run :back with count 99999999999
|
||||||
|
# Make sure it doesn't hang
|
||||||
|
And I run :message-info "Still alive!"
|
||||||
|
Then the error "At beginning of history." should be shown.
|
||||||
|
And the message "Still alive!" should be shown.
|
||||||
|
|
||||||
Scenario: Going back in a new window
|
Scenario: Going back in a new window
|
||||||
Given I have a fresh instance
|
Given I have a fresh instance
|
||||||
When I open data/backforward/1.txt
|
When I open data/backforward/1.txt
|
||||||
|
@ -62,8 +62,13 @@ def fresh_instance(quteproc):
|
|||||||
|
|
||||||
@bdd.when(bdd.parsers.parse("I run {command}"))
|
@bdd.when(bdd.parsers.parse("I run {command}"))
|
||||||
def run_command_when(quteproc, httpbin, command):
|
def run_command_when(quteproc, httpbin, command):
|
||||||
|
if 'with count' in command:
|
||||||
|
command, count = command.split(' with count ')
|
||||||
|
count = int(count)
|
||||||
|
else:
|
||||||
|
count = None
|
||||||
command = command.replace('(port)', str(httpbin.port))
|
command = command.replace('(port)', str(httpbin.port))
|
||||||
quteproc.send_cmd(command)
|
quteproc.send_cmd(command, count=count)
|
||||||
|
|
||||||
|
|
||||||
@bdd.when(bdd.parsers.parse("I reload"))
|
@bdd.when(bdd.parsers.parse("I reload"))
|
||||||
@ -93,13 +98,12 @@ def wait_for_message(quteproc, httpbin, category, message):
|
|||||||
|
|
||||||
@bdd.then(bdd.parsers.parse("{path} should be loaded"))
|
@bdd.then(bdd.parsers.parse("{path} should be loaded"))
|
||||||
def path_should_be_loaded(httpbin, path):
|
def path_should_be_loaded(httpbin, path):
|
||||||
requests = httpbin.get_requests()
|
httpbin.wait_for(verb='GET', path='/' + path)
|
||||||
assert requests[-1] == httpbin.Request('GET', '/' + path)
|
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
|
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
|
||||||
def list_of_loaded_pages(httpbin, pages):
|
def list_of_loaded_pages(httpbin, pages):
|
||||||
expected_requests = [httpbin.Request('GET', '/' + path.strip())
|
expected_requests = [httpbin.ExpectedRequest('GET', '/' + path.strip())
|
||||||
for path in pages.split('\n')]
|
for path in pages.split('\n')]
|
||||||
actual_requests = httpbin.get_requests()
|
actual_requests = httpbin.get_requests()
|
||||||
assert actual_requests == expected_requests
|
assert actual_requests == expected_requests
|
||||||
@ -131,6 +135,11 @@ def compare_session(quteproc, expected):
|
|||||||
assert utils.partial_compare(data, expected)
|
assert utils.partial_compare(data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
|
||||||
|
def ensure_not_logged(quteproc, pattern):
|
||||||
|
quteproc.ensure_not_logged(message=pattern)
|
||||||
|
|
||||||
|
|
||||||
@bdd.then("no crash should happen")
|
@bdd.then("no crash should happen")
|
||||||
def no_crash():
|
def no_crash():
|
||||||
"""Don't do anything.
|
"""Don't do anything.
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
Feature: Various utility commands.
|
Feature: Various utility commands.
|
||||||
|
|
||||||
|
## :set-cmd-text
|
||||||
|
|
||||||
Scenario: :set-cmd-text and :command-accept
|
Scenario: :set-cmd-text and :command-accept
|
||||||
When I run :set-cmd-text :message-info "Hello World"
|
When I run :set-cmd-text :message-info "Hello World"
|
||||||
And I run :command-accept
|
And I run :command-accept
|
||||||
@ -31,6 +33,8 @@ Feature: Various utility commands.
|
|||||||
When I run :set-cmd-text foo
|
When I run :set-cmd-text foo
|
||||||
Then the error "Invalid command text 'foo'." should be shown.
|
Then the error "Invalid command text 'foo'." should be shown.
|
||||||
|
|
||||||
|
## :message-*
|
||||||
|
|
||||||
Scenario: :message-error
|
Scenario: :message-error
|
||||||
When I run :message-error "Hello World"
|
When I run :message-error "Hello World"
|
||||||
Then the error "Hello World" should be shown.
|
Then the error "Hello World" should be shown.
|
||||||
@ -42,3 +46,57 @@ Feature: Various utility commands.
|
|||||||
Scenario: :message-warning
|
Scenario: :message-warning
|
||||||
When I run :message-warning "Hello World"
|
When I run :message-warning "Hello World"
|
||||||
Then the warning "Hello World" should be shown.
|
Then the warning "Hello World" should be shown.
|
||||||
|
|
||||||
|
## :jseval
|
||||||
|
|
||||||
|
Scenario: :jseval
|
||||||
|
When I set general -> log-javascript-console to true
|
||||||
|
And I run :jseval console.log("Hello from JS!");
|
||||||
|
And I wait for "[:0] Hello from JS!" in the log
|
||||||
|
Then the message "No output or error" should be shown.
|
||||||
|
|
||||||
|
Scenario: :jseval without logging
|
||||||
|
When I set general -> log-javascript-console to false
|
||||||
|
And I run :jseval console.log("Hello from JS!");
|
||||||
|
Then the message "No output or error" should be shown.
|
||||||
|
And "[:0] Hello from JS!" should not be logged
|
||||||
|
|
||||||
|
Scenario: :jseval with --quiet
|
||||||
|
When I set general -> log-javascript-console to true
|
||||||
|
And I run :jseval --quiet console.log("Hello from JS!");
|
||||||
|
And I wait for "[:0] Hello from JS!" in the log
|
||||||
|
Then "No output or error" should not be logged
|
||||||
|
|
||||||
|
Scenario: :jseval with a value
|
||||||
|
When I run :jseval "foo"
|
||||||
|
Then the message "foo" should be shown.
|
||||||
|
|
||||||
|
Scenario: :jseval with a long, truncated value
|
||||||
|
When I run :jseval Array(5002).join("x")
|
||||||
|
Then the message "x* [...trimmed...]" should be shown.
|
||||||
|
|
||||||
|
# :debug-webaction
|
||||||
|
|
||||||
|
Scenario: :debug-webaction with valid value
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I open data/backforward/2.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :debug-webaction Back
|
||||||
|
And I wait until data/backforward/1.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/backforward/1.txt
|
||||||
|
- url: http://localhost:*/data/backforward/2.txt
|
||||||
|
|
||||||
|
Scenario: :debug-webaction with invalid value
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :debug-webaction blah
|
||||||
|
Then the error "blah is not a valid web action!" should be shown.
|
||||||
|
|
||||||
|
Scenario: :debug-webaction with non-webaction member
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :debug-webaction PermissionUnknown
|
||||||
|
Then the error "PermissionUnknown is not a valid web action!" should be shown.
|
||||||
|
@ -5,6 +5,8 @@ Feature: Scrolling
|
|||||||
Given I open data/scroll.html
|
Given I open data/scroll.html
|
||||||
And I run :tab-only
|
And I run :tab-only
|
||||||
|
|
||||||
|
## :scroll-px
|
||||||
|
|
||||||
Scenario: Scrolling pixel-wise vertically
|
Scenario: Scrolling pixel-wise vertically
|
||||||
When I run :scroll-px 0 10
|
When I run :scroll-px 0 10
|
||||||
Then the page should be scrolled vertically.
|
Then the page should be scrolled vertically.
|
||||||
@ -13,6 +15,45 @@ Feature: Scrolling
|
|||||||
When I run :scroll-px 10 0
|
When I run :scroll-px 10 0
|
||||||
Then the page should be scrolled horizontally.
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling down and up
|
||||||
|
When I run :scroll-px 10 0
|
||||||
|
And I run :scroll-px -10 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling right and left
|
||||||
|
When I run :scroll-px 0 10
|
||||||
|
And I run :scroll-px 0 -10
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling down and up with count
|
||||||
|
When I run :scroll-px 0 10 with count 2
|
||||||
|
When I run :scroll-px 0 -10
|
||||||
|
When I run :scroll-px 0 -10
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling left and right with count
|
||||||
|
When I run :scroll-px 10 0 with count 2
|
||||||
|
When I run :scroll-px -10 0
|
||||||
|
When I run :scroll-px -10 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: :scroll-px with a very big value
|
||||||
|
When I run :scroll-px 99999999999 0
|
||||||
|
Then the error "Numeric argument is too large for internal int representation." should be shown.
|
||||||
|
|
||||||
|
Scenario: :scroll-px on a page without scrolling
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :scroll-px 10 10
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
Scenario: :scroll-px with floats
|
||||||
|
# This used to be allowed, but doesn't make much sense.
|
||||||
|
When I run :scroll-px 2.5 2.5
|
||||||
|
Then the error "scroll-px: Argument dx: invalid int value: '2.5'" should be shown.
|
||||||
|
And the page should not be scrolled.
|
||||||
|
|
||||||
|
## :scroll
|
||||||
|
|
||||||
Scenario: Scrolling down
|
Scenario: Scrolling down
|
||||||
When I run :scroll down
|
When I run :scroll down
|
||||||
Then the page should be scrolled vertically.
|
Then the page should be scrolled vertically.
|
||||||
@ -58,3 +99,155 @@ Feature: Scrolling
|
|||||||
When I run :scroll 0 10
|
When I run :scroll 0 10
|
||||||
Then the warning ":scroll with dx/dy arguments is deprecated - use :scroll-px instead!" should be shown.
|
Then the warning ":scroll with dx/dy arguments is deprecated - use :scroll-px instead!" should be shown.
|
||||||
Then the page should be scrolled vertically.
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: :scroll with deprecated pixel argument (float)
|
||||||
|
When I run :scroll 2.5 2.5
|
||||||
|
Then the error "scroll: Argument dy: invalid int value: '2.5'" should be shown.
|
||||||
|
And the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling down and up with count
|
||||||
|
When I run :scroll down with count 2
|
||||||
|
And I run :scroll up
|
||||||
|
And I run :scroll up
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling right
|
||||||
|
When I run :scroll right
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling right and left
|
||||||
|
When I run :scroll right
|
||||||
|
And I run :scroll left
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling right and left with count
|
||||||
|
When I run :scroll right with count 2
|
||||||
|
And I run :scroll left
|
||||||
|
And I run :scroll left
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling down with a very big count
|
||||||
|
When I run :scroll down with count 99999999999
|
||||||
|
# Make sure it doesn't hang
|
||||||
|
And I run :message-info "Still alive!"
|
||||||
|
Then the message "Still alive!" should be shown.
|
||||||
|
|
||||||
|
Scenario: :scroll on a page without scrolling
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :scroll down
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
## :scroll-perc
|
||||||
|
|
||||||
|
Scenario: Scrolling to bottom with :scroll-perc
|
||||||
|
When I run :scroll-perc 100
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: Scrolling to bottom and to top with :scroll-perc
|
||||||
|
When I run :scroll-perc 100
|
||||||
|
And I run :scroll-perc 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling to middle with :scroll-perc
|
||||||
|
When I run :scroll-perc 50
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: Scrolling to middle with :scroll-perc (float)
|
||||||
|
When I run :scroll-perc 50.5
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: Scrolling to middle and to top with :scroll-perc
|
||||||
|
When I run :scroll-perc 50
|
||||||
|
And I run :scroll-perc 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling to right with :scroll-perc
|
||||||
|
When I run :scroll-perc --horizontal 100
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling to right and to left with :scroll-perc
|
||||||
|
When I run :scroll-perc --horizontal 100
|
||||||
|
And I run :scroll-perc --horizontal 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling to middle (horizontally) with :scroll-perc
|
||||||
|
When I run :scroll-perc --horizontal 50
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling to middle and to left with :scroll-perc
|
||||||
|
When I run :scroll-perc --horizontal 50
|
||||||
|
And I run :scroll-perc --horizontal 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: :scroll-perc without argument
|
||||||
|
When I run :scroll-perc
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: :scroll-perc without argument and --horizontal
|
||||||
|
When I run :scroll-perc --horizontal
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: :scroll-perc with count
|
||||||
|
When I run :scroll-perc with count 50
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: :scroll-perc with a very big value
|
||||||
|
When I run :scroll-perc 99999999999
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
Scenario: :scroll-perc on a page without scrolling
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :scroll-perc 20
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
## :scroll-page
|
||||||
|
|
||||||
|
Scenario: Scrolling down with :scroll-page
|
||||||
|
When I run :scroll-page 0 1
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: Scrolling down with :scroll-page (float)
|
||||||
|
When I run :scroll-page 0 1.5
|
||||||
|
Then the page should be scrolled vertically.
|
||||||
|
|
||||||
|
Scenario: Scrolling down and up with :scroll-page
|
||||||
|
When I run :scroll-page 0 1
|
||||||
|
And I run :scroll-page 0 -1
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling right with :scroll-page
|
||||||
|
When I run :scroll-page 1 0
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling right with :scroll-page (float)
|
||||||
|
When I run :scroll-page 1.5 0
|
||||||
|
Then the page should be scrolled horizontally.
|
||||||
|
|
||||||
|
Scenario: Scrolling right and left with :scroll-page
|
||||||
|
When I run :scroll-page 1 0
|
||||||
|
And I run :scroll-page -1 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: Scrolling right and left with :scroll-page and count
|
||||||
|
When I run :scroll-page 1 0 with count 2
|
||||||
|
And I run :scroll-page -1 0
|
||||||
|
And I run :scroll-page -1 0
|
||||||
|
Then the page should not be scrolled.
|
||||||
|
|
||||||
|
Scenario: :scroll-page with --bottom-navigate
|
||||||
|
When I run :scroll-perc 100
|
||||||
|
And I run :scroll-page --bottom-navigate next 0 1
|
||||||
|
Then data/hello2.txt should be loaded
|
||||||
|
|
||||||
|
Scenario: :scroll-page with --top-navigate
|
||||||
|
When I run :scroll-page --top-navigate prev 0 -1
|
||||||
|
Then data/hello3.txt should be loaded
|
||||||
|
|
||||||
|
Scenario: :scroll-page with a very big value
|
||||||
|
When I run :scroll-page 99999999999 99999999999
|
||||||
|
Then the error "Numeric argument is too large for internal int representation." should be shown.
|
||||||
|
|
||||||
|
Scenario: :scroll-page on a page without scrolling
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :scroll-page 1 1
|
||||||
|
Then no crash should happen
|
||||||
|
@ -27,22 +27,46 @@ import pytest_bdd as bdd
|
|||||||
bdd.scenarios('yankpaste.feature')
|
bdd.scenarios('yankpaste.feature')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def skip_with_broken_clipboard(qapp):
|
||||||
|
"""The clipboard seems to be broken on some platforms (OS X Yosemite?).
|
||||||
|
|
||||||
|
This skips the tests if this is the case.
|
||||||
|
"""
|
||||||
|
clipboard = qapp.clipboard()
|
||||||
|
clipboard.setText("Does this work?")
|
||||||
|
if clipboard.text() != "Does this work?":
|
||||||
|
pytest.skip("Clipboard seems to be broken on this platform.")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mode(qapp, what):
|
||||||
|
"""Get the QClipboard::Mode to use based on a string."""
|
||||||
|
if what == 'clipboard':
|
||||||
|
return QClipboard.Clipboard
|
||||||
|
elif what == 'primary selection':
|
||||||
|
assert qapp.clipboard().supportsSelection()
|
||||||
|
return QClipboard.Selection
|
||||||
|
else:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
|
||||||
@bdd.when("selection is supported")
|
@bdd.when("selection is supported")
|
||||||
def selection_supported(qapp):
|
def selection_supported(qapp):
|
||||||
if not qapp.clipboard().supportsSelection():
|
if not qapp.clipboard().supportsSelection():
|
||||||
pytest.skip("OS doesn't support primary selection!")
|
pytest.skip("OS doesn't support primary selection!")
|
||||||
|
|
||||||
|
@bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the '
|
||||||
|
r'(?P<what>primary selection|clipboard)'))
|
||||||
|
def fill_clipboard(qapp, httpbin, what, content):
|
||||||
|
mode = _get_mode(qapp, what)
|
||||||
|
content = content.replace('(port)', str(httpbin.port))
|
||||||
|
qapp.clipboard().setText(content, mode)
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
|
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
|
||||||
r'contain "(?P<content>.*)"'))
|
r'contain "(?P<content>.*)"'))
|
||||||
def clipboard_contains(qapp, httpbin, what, content):
|
def clipboard_contains(qapp, httpbin, what, content):
|
||||||
if what == 'clipboard':
|
mode = _get_mode(qapp, what)
|
||||||
mode = QClipboard.Clipboard
|
|
||||||
elif what == 'primary selection':
|
|
||||||
mode = QClipboard.Selection
|
|
||||||
else:
|
|
||||||
raise AssertionError
|
|
||||||
|
|
||||||
expected = content.replace('(port)', str(httpbin.port))
|
expected = content.replace('(port)', str(httpbin.port))
|
||||||
|
|
||||||
data = qapp.clipboard().text(mode=mode)
|
data = qapp.clipboard().text(mode=mode)
|
||||||
|
@ -19,3 +19,10 @@
|
|||||||
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('zoom.feature')
|
bdd.scenarios('zoom.feature')
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse("the zoom should be {zoom}%"))
|
||||||
|
def check_zoom(quteproc, zoom):
|
||||||
|
data = quteproc.get_session()
|
||||||
|
value = data['windows'][0]['tabs'][0]['history'][0]['zoom'] * 100
|
||||||
|
assert abs(value - float(zoom)) < 0.0001
|
||||||
|
@ -3,26 +3,112 @@ Feature: Yanking and pasting.
|
|||||||
clipboard and primary selection.
|
clipboard and primary selection.
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given I open data/yankpaste.html
|
Given I run :tab-only
|
||||||
|
|
||||||
|
#### :yank
|
||||||
|
|
||||||
Scenario: Yanking URLs to clipboard
|
Scenario: Yanking URLs to clipboard
|
||||||
When I run :yank
|
When I open data/yankpaste.html
|
||||||
|
And I run :yank
|
||||||
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/yankpaste.html" should be shown.
|
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/yankpaste.html" should be shown.
|
||||||
And the clipboard should contain "http://localhost:(port)/data/yankpaste.html"
|
And the clipboard should contain "http://localhost:(port)/data/yankpaste.html"
|
||||||
|
|
||||||
Scenario: Yanking URLs to primary selection
|
Scenario: Yanking URLs to primary selection
|
||||||
When selection is supported
|
When selection is supported
|
||||||
|
And I open data/yankpaste.html
|
||||||
And I run :yank --sel
|
And I run :yank --sel
|
||||||
Then the message "Yanked URL to primary selection: http://localhost:(port)/data/yankpaste.html" should be shown.
|
Then the message "Yanked URL to primary selection: http://localhost:(port)/data/yankpaste.html" should be shown.
|
||||||
And the primary selection should contain "http://localhost:(port)/data/yankpaste.html"
|
And the primary selection should contain "http://localhost:(port)/data/yankpaste.html"
|
||||||
|
|
||||||
Scenario: Yanking title to clipboard
|
Scenario: Yanking title to clipboard
|
||||||
When I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
When I open data/yankpaste.html
|
||||||
|
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
||||||
And I run :yank --title
|
And I run :yank --title
|
||||||
Then the message "Yanked title to clipboard: Test title" should be shown.
|
Then the message "Yanked title to clipboard: Test title" should be shown.
|
||||||
And the clipboard should contain "Test title"
|
And the clipboard should contain "Test title"
|
||||||
|
|
||||||
Scenario: Yanking domain to clipboard
|
Scenario: Yanking domain to clipboard
|
||||||
When I run :yank --domain
|
When I open data/yankpaste.html
|
||||||
|
And I run :yank --domain
|
||||||
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown.
|
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown.
|
||||||
And the clipboard should contain "http://localhost:(port)"
|
And the clipboard should contain "http://localhost:(port)"
|
||||||
|
|
||||||
|
#### :paste
|
||||||
|
|
||||||
|
Scenario: Pasting an URL
|
||||||
|
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||||
|
And I run :paste
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
Then the requests should be:
|
||||||
|
data/hello.txt
|
||||||
|
|
||||||
|
Scenario: Pasting an URL from primary selection
|
||||||
|
When selection is supported
|
||||||
|
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
|
||||||
|
And I run :paste --sel
|
||||||
|
And I wait until data/hello2.txt is loaded
|
||||||
|
Then the requests should be:
|
||||||
|
data/hello2.txt
|
||||||
|
|
||||||
|
Scenario: Pasting with empty clipboard
|
||||||
|
When I put "" into the clipboard
|
||||||
|
And I run :paste
|
||||||
|
Then the error "Clipboard is empty." should be shown.
|
||||||
|
|
||||||
|
Scenario: Pasting with empty selection
|
||||||
|
When selection is supported
|
||||||
|
And I put "" into the primary selection
|
||||||
|
And I run :paste --sel
|
||||||
|
Then the error "Primary selection is empty." should be shown.
|
||||||
|
|
||||||
|
Scenario: Pasting in a new tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||||
|
And I run :paste -t
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello.txt
|
||||||
|
|
||||||
|
Scenario: Pasting in a background tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||||
|
And I run :paste -b
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello.txt
|
||||||
|
|
||||||
|
Scenario: Pasting in a new window
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||||
|
And I run :paste -w
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello.txt
|
||||||
|
@ -2,41 +2,56 @@ Feature: Zooming in and out
|
|||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given I open data/hello.txt
|
Given I open data/hello.txt
|
||||||
|
And I set ui -> zoom-levels to 50%,90%,100%,110%,120%
|
||||||
And I run :tab-only
|
And I run :tab-only
|
||||||
|
|
||||||
Scenario: Zooming in
|
Scenario: Zooming in
|
||||||
When I run :zoom-in
|
When I run :zoom-in
|
||||||
Then the message "Zoom level: 110%" should be shown.
|
Then the message "Zoom level: 110%" should be shown.
|
||||||
And the session should look like:
|
And the zoom should be 110%
|
||||||
windows:
|
|
||||||
- tabs:
|
|
||||||
- history:
|
|
||||||
- zoom: 1.1
|
|
||||||
|
|
||||||
Scenario: Zooming out
|
Scenario: Zooming out
|
||||||
When I run :zoom-out
|
When I run :zoom-out
|
||||||
Then the message "Zoom level: 90%" should be shown.
|
Then the message "Zoom level: 90%" should be shown.
|
||||||
And the session should look like:
|
And the zoom should be 90%
|
||||||
windows:
|
|
||||||
- tabs:
|
Scenario: Zooming in with count
|
||||||
- history:
|
When I run :zoom-in with count 2
|
||||||
- zoom: 0.9
|
Then the message "Zoom level: 120%" should be shown.
|
||||||
|
And the zoom should be 120%
|
||||||
|
|
||||||
|
# https://github.com/The-Compiler/qutebrowser/issues/1118
|
||||||
|
Scenario: Zooming in with very big count
|
||||||
|
When I run :zoom-in with count 99999999999
|
||||||
|
Then the message "Zoom level: 100%" should be shown.
|
||||||
|
And the zoom should be 100%
|
||||||
|
|
||||||
|
Scenario: Zooming out with count
|
||||||
|
When I run :zoom-out with count 2
|
||||||
|
Then the message "Zoom level: 50%" should be shown.
|
||||||
|
And the zoom should be 50%
|
||||||
|
|
||||||
Scenario: Setting zoom
|
Scenario: Setting zoom
|
||||||
When I run :zoom 50
|
When I run :zoom 50
|
||||||
Then the message "Zoom level: 50%" should be shown.
|
Then the message "Zoom level: 50%" should be shown.
|
||||||
And the session should look like:
|
And the zoom should be 50%
|
||||||
windows:
|
|
||||||
- tabs:
|
Scenario: Setting zoom with count
|
||||||
- history:
|
When I run :zoom with count 40
|
||||||
- zoom: 0.5
|
Then the message "Zoom level: 40%" should be shown.
|
||||||
|
And the zoom should be 40%
|
||||||
|
|
||||||
Scenario: Resetting zoom
|
Scenario: Resetting zoom
|
||||||
When I run :zoom 50
|
When I set ui -> default-zoom to 42%
|
||||||
|
And I run :zoom 50
|
||||||
And I run :zoom
|
And I run :zoom
|
||||||
Then the message "Zoom level: 100%" should be shown.
|
Then the message "Zoom level: 42%" should be shown.
|
||||||
And the session should look like:
|
And the zoom should be 42%
|
||||||
windows:
|
|
||||||
- tabs:
|
Scenario: Setting zoom to invalid value
|
||||||
- history:
|
When I run :zoom -1
|
||||||
- zoom: 1.0
|
Then the error "Can't zoom -1%!" should be shown.
|
||||||
|
|
||||||
|
Scenario: Setting zoom with very big count
|
||||||
|
When I run :zoom with count 99999999999
|
||||||
|
Then the message "Zoom level: 99999999999%" should be shown.
|
||||||
|
@ -47,13 +47,6 @@ def is_ignored_qt_message(message):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class NoLineMatch(Exception):
|
|
||||||
|
|
||||||
"""Raised by LogLine on unmatched lines."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LogLine(testprocess.Line):
|
class LogLine(testprocess.Line):
|
||||||
|
|
||||||
"""A parsed line from the qutebrowser log output.
|
"""A parsed line from the qutebrowser log output.
|
||||||
@ -78,7 +71,7 @@ class LogLine(testprocess.Line):
|
|||||||
super().__init__(data)
|
super().__init__(data)
|
||||||
match = self.LOG_RE.match(data)
|
match = self.LOG_RE.match(data)
|
||||||
if match is None:
|
if match is None:
|
||||||
raise NoLineMatch(data)
|
raise testprocess.InvalidLine(data)
|
||||||
|
|
||||||
self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
|
self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
|
||||||
'%H:%M:%S')
|
'%H:%M:%S')
|
||||||
@ -127,6 +120,9 @@ class QuteProc(testprocess.Process):
|
|||||||
|
|
||||||
got_error = pyqtSignal()
|
got_error = pyqtSignal()
|
||||||
|
|
||||||
|
KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line',
|
||||||
|
'message']
|
||||||
|
|
||||||
def __init__(self, httpbin, parent=None):
|
def __init__(self, httpbin, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._httpbin = httpbin
|
self._httpbin = httpbin
|
||||||
@ -135,7 +131,7 @@ class QuteProc(testprocess.Process):
|
|||||||
def _parse_line(self, line):
|
def _parse_line(self, line):
|
||||||
try:
|
try:
|
||||||
log_line = LogLine(line)
|
log_line = LogLine(line)
|
||||||
except NoLineMatch:
|
except testprocess.InvalidLine:
|
||||||
if line.startswith(' '):
|
if line.startswith(' '):
|
||||||
# Multiple lines in some log output...
|
# Multiple lines in some log output...
|
||||||
return None
|
return None
|
||||||
@ -144,7 +140,7 @@ class QuteProc(testprocess.Process):
|
|||||||
elif is_ignored_qt_message(line):
|
elif is_ignored_qt_message(line):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise testprocess.InvalidLine
|
raise
|
||||||
|
|
||||||
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
|
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
|
||||||
pytest.config.getoption('--verbose')):
|
pytest.config.getoption('--verbose')):
|
||||||
@ -177,6 +173,12 @@ class QuteProc(testprocess.Process):
|
|||||||
'about:blank']
|
'about:blank']
|
||||||
return executable, args
|
return executable, args
|
||||||
|
|
||||||
|
def _path_to_url(self, path):
|
||||||
|
if path.startswith('about:') or path.startswith('qute:'):
|
||||||
|
return path
|
||||||
|
else:
|
||||||
|
return 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
||||||
|
|
||||||
def after_test(self):
|
def after_test(self):
|
||||||
bad_msgs = [msg for msg in self._data
|
bad_msgs = [msg for msg in self._data
|
||||||
if msg.loglevel > logging.INFO and not msg.expected]
|
if msg.loglevel > logging.INFO and not msg.expected]
|
||||||
@ -186,9 +188,12 @@ class QuteProc(testprocess.Process):
|
|||||||
str(e) for e in bad_msgs)
|
str(e) for e in bad_msgs)
|
||||||
pytest.fail(text, pytrace=False)
|
pytest.fail(text, pytrace=False)
|
||||||
|
|
||||||
def send_cmd(self, command):
|
def send_cmd(self, command, count=None):
|
||||||
assert self._ipc_socket is not None
|
assert self._ipc_socket is not None
|
||||||
|
|
||||||
|
if count is not None:
|
||||||
|
command = ':{}:{}'.format(count, command.lstrip(':'))
|
||||||
|
|
||||||
ipc.send_to_running_instance(self._ipc_socket, [command],
|
ipc.send_to_running_instance(self._ipc_socket, [command],
|
||||||
target_arg='')
|
target_arg='')
|
||||||
self.wait_for(category='commands', module='command', function='run',
|
self.wait_for(category='commands', module='command', function='run',
|
||||||
@ -199,7 +204,7 @@ class QuteProc(testprocess.Process):
|
|||||||
self.wait_for(category='config', message='Config option changed: *')
|
self.wait_for(category='config', message='Config option changed: *')
|
||||||
|
|
||||||
def open_path(self, path, new_tab=False):
|
def open_path(self, path, new_tab=False):
|
||||||
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
url = self._path_to_url(path)
|
||||||
if new_tab:
|
if new_tab:
|
||||||
self.send_cmd(':open -t ' + url)
|
self.send_cmd(':open -t ' + url)
|
||||||
else:
|
else:
|
||||||
@ -218,11 +223,12 @@ class QuteProc(testprocess.Process):
|
|||||||
self._data is cleared after every test to provide at least some
|
self._data is cleared after every test to provide at least some
|
||||||
isolation.
|
isolation.
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
return super().wait_for(timeout, **kwargs)
|
return super().wait_for(timeout, **kwargs)
|
||||||
|
|
||||||
def wait_for_load_finished(self, path, timeout=15000):
|
def wait_for_load_finished(self, path, timeout=15000):
|
||||||
"""Wait until any tab has finished loading."""
|
"""Wait until any tab has finished loading."""
|
||||||
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
url = self._path_to_url(path)
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r"(load status for <qutebrowser.browser.webview.WebView "
|
r"(load status for <qutebrowser.browser.webview.WebView "
|
||||||
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
||||||
|
@ -25,6 +25,7 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import quteprocess
|
import quteprocess
|
||||||
|
import testprocess
|
||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
|
|
||||||
@ -113,5 +114,5 @@ def test_log_line_parse(data, attrs):
|
|||||||
|
|
||||||
|
|
||||||
def test_log_line_no_match():
|
def test_log_line_no_match():
|
||||||
with pytest.raises(quteprocess.NoLineMatch):
|
with pytest.raises(testprocess.InvalidLine):
|
||||||
quteprocess.LogLine("Hello World!")
|
quteprocess.LogLine("Hello World!")
|
||||||
|
@ -74,19 +74,20 @@ class PythonProcess(testprocess.Process):
|
|||||||
return (sys.executable, ['-c', ';'.join(code)])
|
return (sys.executable, ['-c', ';'.join(code)])
|
||||||
|
|
||||||
|
|
||||||
class TestWaitFor:
|
@pytest.yield_fixture
|
||||||
|
def pyproc():
|
||||||
|
proc = PythonProcess()
|
||||||
|
yield proc
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def pyproc(self):
|
class TestWaitFor:
|
||||||
proc = PythonProcess()
|
|
||||||
yield proc
|
|
||||||
proc.terminate()
|
|
||||||
|
|
||||||
def test_successful(self, pyproc):
|
def test_successful(self, pyproc):
|
||||||
"""Using wait_for with the expected text."""
|
"""Using wait_for with the expected text."""
|
||||||
pyproc.code = "time.sleep(0.5); print('foobar')"
|
pyproc.code = "time.sleep(0.5); print('foobar')"
|
||||||
pyproc.start()
|
with stopwatch(min_ms=500):
|
||||||
with stopwatch(min_ms=300): # on Windows, this can be done faster...
|
pyproc.start()
|
||||||
pyproc.wait_for(data="foobar")
|
pyproc.wait_for(data="foobar")
|
||||||
|
|
||||||
def test_other_text(self, pyproc):
|
def test_other_text(self, pyproc):
|
||||||
@ -103,12 +104,13 @@ class TestWaitFor:
|
|||||||
with pytest.raises(testprocess.WaitForTimeout):
|
with pytest.raises(testprocess.WaitForTimeout):
|
||||||
pyproc.wait_for(data="foobar", timeout=100)
|
pyproc.wait_for(data="foobar", timeout=100)
|
||||||
|
|
||||||
def test_existing_message(self, pyproc):
|
@pytest.mark.parametrize('message', ['foobar', 'literal [x]'])
|
||||||
|
def test_existing_message(self, message, pyproc):
|
||||||
"""Test with a message which already passed when waiting."""
|
"""Test with a message which already passed when waiting."""
|
||||||
pyproc.code = "print('foobar')"
|
pyproc.code = "print('{}')".format(message)
|
||||||
pyproc.start()
|
pyproc.start()
|
||||||
time.sleep(0.5) # to make sure the message is printed
|
time.sleep(0.5) # to make sure the message is printed
|
||||||
pyproc.wait_for(data="foobar")
|
pyproc.wait_for(data=message)
|
||||||
|
|
||||||
def test_existing_message_previous_test(self, pyproc):
|
def test_existing_message_previous_test(self, pyproc):
|
||||||
"""Make sure the message of a previous test gets ignored."""
|
"""Make sure the message of a previous test gets ignored."""
|
||||||
@ -133,3 +135,36 @@ class TestWaitFor:
|
|||||||
pyproc.wait_for(data="foobar")
|
pyproc.wait_for(data="foobar")
|
||||||
with pytest.raises(testprocess.WaitForTimeout):
|
with pytest.raises(testprocess.WaitForTimeout):
|
||||||
pyproc.wait_for(data="foobar", timeout=100)
|
pyproc.wait_for(data="foobar", timeout=100)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnsureNotLogged:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('message, pattern', [
|
||||||
|
('blacklisted', 'blacklisted'),
|
||||||
|
('bl[a]cklisted', 'bl[a]cklisted'),
|
||||||
|
('blacklisted', 'black*'),
|
||||||
|
])
|
||||||
|
def test_existing_message(self, pyproc, message, pattern):
|
||||||
|
pyproc.code = "print('{}')".format(message)
|
||||||
|
pyproc.start()
|
||||||
|
with stopwatch(max_ms=1000):
|
||||||
|
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||||
|
pyproc.ensure_not_logged(data=pattern, delay=2000)
|
||||||
|
|
||||||
|
def test_late_message(self, pyproc):
|
||||||
|
pyproc.code = "time.sleep(0.5); print('blacklisted')"
|
||||||
|
pyproc.start()
|
||||||
|
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||||
|
pyproc.ensure_not_logged(data='blacklisted', delay=1000)
|
||||||
|
|
||||||
|
def test_no_matching_message(self, pyproc):
|
||||||
|
pyproc.code = "print('blacklisted... nope!')"
|
||||||
|
pyproc.start()
|
||||||
|
pyproc.ensure_not_logged(data='blacklisted', delay=100)
|
||||||
|
|
||||||
|
def test_wait_for_and_blacklist(self, pyproc):
|
||||||
|
pyproc.code = "print('blacklisted')"
|
||||||
|
pyproc.start()
|
||||||
|
pyproc.wait_for(data='blacklisted')
|
||||||
|
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||||
|
pyproc.ensure_not_logged(data='blacklisted', delay=0)
|
||||||
|
@ -46,5 +46,19 @@ def test_httpbin(httpbin, qtbot, path, content, expected):
|
|||||||
|
|
||||||
data = response.read().decode('utf-8')
|
data = response.read().decode('utf-8')
|
||||||
|
|
||||||
assert httpbin.get_requests() == [httpbin.Request('GET', path)]
|
assert httpbin.get_requests() == [httpbin.ExpectedRequest('GET', path)]
|
||||||
assert (content in data) == expected
|
assert (content in data) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('line, verb, path, equal', [
|
||||||
|
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||||
|
'GET', '/', True),
|
||||||
|
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||||
|
'GET', '/foo', False),
|
||||||
|
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||||
|
'POST', '/foo', False),
|
||||||
|
])
|
||||||
|
def test_expected_request(httpbin, line, verb, path, equal):
|
||||||
|
expected = httpbin.ExpectedRequest(verb, path)
|
||||||
|
request = httpbin.Request(line)
|
||||||
|
assert (expected == request) == equal
|
||||||
|
@ -22,12 +22,13 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import fnmatch
|
|
||||||
|
|
||||||
import pytestqt.plugin # pylint: disable=import-error
|
import pytestqt.plugin # pylint: disable=import-error
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
|
||||||
from PyQt5.QtTest import QSignalSpy
|
from PyQt5.QtTest import QSignalSpy
|
||||||
|
|
||||||
|
from helpers import utils # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
class InvalidLine(Exception):
|
class InvalidLine(Exception):
|
||||||
|
|
||||||
@ -48,6 +49,11 @@ class WaitForTimeout(Exception):
|
|||||||
"""Raised when wait_for didn't get the expected message."""
|
"""Raised when wait_for didn't get the expected message."""
|
||||||
|
|
||||||
|
|
||||||
|
class BlacklistedMessageError(Exception):
|
||||||
|
|
||||||
|
"""Raised when ensure_not_logged found a message."""
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
class Line:
|
||||||
|
|
||||||
"""Container for a line of data the process emits.
|
"""Container for a line of data the process emits.
|
||||||
@ -78,6 +84,7 @@ class Process(QObject):
|
|||||||
|
|
||||||
ready = pyqtSignal()
|
ready = pyqtSignal()
|
||||||
new_data = pyqtSignal(object)
|
new_data = pyqtSignal(object)
|
||||||
|
KEYS = ['data']
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -138,7 +145,8 @@ class Process(QObject):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
print("IGNORED: {}".format(line))
|
if self._invalid:
|
||||||
|
print("IGNORED: {}".format(line))
|
||||||
else:
|
else:
|
||||||
self._data.append(parsed)
|
self._data.append(parsed)
|
||||||
self.new_data.emit(parsed)
|
self.new_data.emit(parsed)
|
||||||
@ -198,7 +206,7 @@ class Process(QObject):
|
|||||||
|
|
||||||
- If expected is None, the filter always matches.
|
- If expected is None, the filter always matches.
|
||||||
- If the value is a string or bytes object and the expected value is
|
- If the value is a string or bytes object and the expected value is
|
||||||
too, the pattern is treated as a fnmatch glob pattern.
|
too, the pattern is treated as a glob pattern (with only * active).
|
||||||
- If the value is a string or bytes object and the expected value is a
|
- If the value is a string or bytes object and the expected value is a
|
||||||
compiled regex, it is used for matching.
|
compiled regex, it is used for matching.
|
||||||
- If the value is any other type, == is used.
|
- If the value is any other type, == is used.
|
||||||
@ -212,25 +220,34 @@ class Process(QObject):
|
|||||||
elif isinstance(expected, regex_type):
|
elif isinstance(expected, regex_type):
|
||||||
return expected.match(value)
|
return expected.match(value)
|
||||||
elif isinstance(value, (bytes, str)):
|
elif isinstance(value, (bytes, str)):
|
||||||
return fnmatch.fnmatchcase(value, expected)
|
return utils.pattern_match(pattern=expected, value=value)
|
||||||
else:
|
else:
|
||||||
return value == expected
|
return value == expected
|
||||||
|
|
||||||
def wait_for(self, timeout=None, **kwargs):
|
def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs):
|
||||||
"""Wait until a given value is found in the data.
|
"""Wait until a given value is found in the data.
|
||||||
|
|
||||||
Keyword arguments to this function get interpreted as attributes of the
|
Keyword arguments to this function get interpreted as attributes of the
|
||||||
searched data. Every given argument is treated as a pattern which
|
searched data. Every given argument is treated as a pattern which
|
||||||
the attribute has to match against.
|
the attribute has to match against.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: How long to wait for the message.
|
||||||
|
override_waited_for: If set, gets triggered by previous messages
|
||||||
|
again.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
The matched line.
|
The matched line.
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
if 'CI' in os.environ:
|
if 'CI' in os.environ:
|
||||||
timeout = 15000
|
timeout = 15000
|
||||||
else:
|
else:
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
|
for key in kwargs:
|
||||||
|
assert key in self.KEYS
|
||||||
|
|
||||||
# Search existing messages
|
# Search existing messages
|
||||||
for line in self._data:
|
for line in self._data:
|
||||||
matches = []
|
matches = []
|
||||||
@ -239,7 +256,7 @@ class Process(QObject):
|
|||||||
value = getattr(line, key)
|
value = getattr(line, key)
|
||||||
matches.append(self._match_data(value, expected))
|
matches.append(self._match_data(value, expected))
|
||||||
|
|
||||||
if all(matches) and not line.waited_for:
|
if all(matches) and (not line.waited_for or override_waited_for):
|
||||||
# If we waited for this line, chances are we don't mean the
|
# If we waited for this line, chances are we don't mean the
|
||||||
# same thing the next time we use wait_for and it matches
|
# same thing the next time we use wait_for and it matches
|
||||||
# this line again.
|
# this line again.
|
||||||
@ -273,3 +290,18 @@ class Process(QObject):
|
|||||||
# this line again.
|
# this line again.
|
||||||
line.waited_for = True
|
line.waited_for = True
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
def ensure_not_logged(self, delay=500, **kwargs):
|
||||||
|
"""Make sure the data matching the given arguments is not logged.
|
||||||
|
|
||||||
|
If nothing is found in the log, we wait for delay ms to make sure
|
||||||
|
nothing arrives.
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
try:
|
||||||
|
line = self.wait_for(timeout=delay, override_waited_for=True,
|
||||||
|
**kwargs)
|
||||||
|
except WaitForTimeout:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise BlacklistedMessageError(line)
|
||||||
|
@ -26,7 +26,6 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
import os.path
|
import os.path
|
||||||
import collections
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5.QtCore import pyqtSignal
|
from PyQt5.QtCore import pyqtSignal
|
||||||
@ -34,25 +33,17 @@ from PyQt5.QtCore import pyqtSignal
|
|||||||
import testprocess # pylint: disable=import-error
|
import testprocess # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
Request = collections.namedtuple('Request', 'verb, path')
|
class Request(testprocess.Line):
|
||||||
|
|
||||||
|
"""A parsed line from the httpbin/flask log output.
|
||||||
|
|
||||||
class HTTPBin(testprocess.Process):
|
Attributes:
|
||||||
|
timestamp/verb/path/status: Parsed from the log output.
|
||||||
"""Abstraction over a running HTTPbin server process.
|
|
||||||
|
|
||||||
Reads the log from its stdout and parses it.
|
|
||||||
|
|
||||||
Class attributes:
|
Class attributes:
|
||||||
LOG_RE: Used to parse the CLF log which httpbin outputs.
|
LOG_RE: Used to parse the CLF log which httpbin outputs.
|
||||||
|
|
||||||
Signals:
|
|
||||||
new_request: Emitted when there's a new request received.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
new_request = pyqtSignal(Request)
|
|
||||||
Request = Request # So it can be used from the fixture easily.
|
|
||||||
|
|
||||||
LOG_RE = re.compile(r"""
|
LOG_RE = re.compile(r"""
|
||||||
(?P<host>[^ ]*)
|
(?P<host>[^ ]*)
|
||||||
\ ([^ ]*) # ignored
|
\ ([^ ]*) # ignored
|
||||||
@ -67,6 +58,69 @@ class HTTPBin(testprocess.Process):
|
|||||||
\ (?P<size>[^ ]*)
|
\ (?P<size>[^ ]*)
|
||||||
""", re.VERBOSE)
|
""", re.VERBOSE)
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__(data)
|
||||||
|
match = self.LOG_RE.match(data)
|
||||||
|
if match is None:
|
||||||
|
raise testprocess.InvalidLine(data)
|
||||||
|
|
||||||
|
assert match.group('host') == '127.0.0.1'
|
||||||
|
assert match.group('user') == '-'
|
||||||
|
self.timestamp = match.group('date')
|
||||||
|
self.verb = match.group('verb')
|
||||||
|
|
||||||
|
# FIXME do we need to allow other options?
|
||||||
|
assert match.group('protocol') == 'HTTP/1.1'
|
||||||
|
assert self.verb == 'GET'
|
||||||
|
|
||||||
|
self.path = match.group('path')
|
||||||
|
self.status = int(match.group('status'))
|
||||||
|
|
||||||
|
missing_paths = ['/favicon.ico', '/does-not-exist']
|
||||||
|
|
||||||
|
if self.path in missing_paths:
|
||||||
|
assert self.status == 404
|
||||||
|
else:
|
||||||
|
assert self.status < 400
|
||||||
|
|
||||||
|
assert match.group('size') == '-'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class ExpectedRequest:
|
||||||
|
|
||||||
|
"""Class to compare expected requests easily."""
|
||||||
|
|
||||||
|
def __init__(self, verb, path):
|
||||||
|
self.verb = verb
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, (Request, ExpectedRequest)):
|
||||||
|
return (self.verb == other.verb and
|
||||||
|
self.path == other.path)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBin(testprocess.Process):
|
||||||
|
|
||||||
|
"""Abstraction over a running HTTPbin server process.
|
||||||
|
|
||||||
|
Reads the log from its stdout and parses it.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
new_request: Emitted when there's a new request received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_request = pyqtSignal(Request)
|
||||||
|
Request = Request # So it can be used from the fixture easily.
|
||||||
|
ExpectedRequest = ExpectedRequest
|
||||||
|
|
||||||
|
KEYS = ['verb', 'path']
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.port = self._get_port()
|
self.port = self._get_port()
|
||||||
@ -91,14 +145,7 @@ class HTTPBin(testprocess.Process):
|
|||||||
'quit)'.format(self.port)):
|
'quit)'.format(self.port)):
|
||||||
self.ready.emit()
|
self.ready.emit()
|
||||||
return None
|
return None
|
||||||
|
return Request(line)
|
||||||
match = self.LOG_RE.match(line)
|
|
||||||
if match is None:
|
|
||||||
raise testprocess.InvalidLine
|
|
||||||
# FIXME do we need to allow other options?
|
|
||||||
assert match.group('protocol') == 'HTTP/1.1'
|
|
||||||
|
|
||||||
return Request(verb=match.group('verb'), path=match.group('path'))
|
|
||||||
|
|
||||||
def _executable_args(self):
|
def _executable_args(self):
|
||||||
if hasattr(sys, 'frozen'):
|
if hasattr(sys, 'frozen'):
|
||||||
|
@ -55,7 +55,7 @@ class HeaderChecker:
|
|||||||
"""Check if the passed header is ignored."""
|
"""Check if the passed header is ignored."""
|
||||||
reply = self.stubs.FakeNetworkReply(
|
reply = self.stubs.FakeNetworkReply(
|
||||||
headers={'Content-Disposition': header})
|
headers={'Content-Disposition': header})
|
||||||
with self.caplog.atLevel(logging.ERROR, 'rfc6266'):
|
with self.caplog.at_level(logging.ERROR, 'rfc6266'):
|
||||||
# with self.assertLogs(log.rfc6266, logging.ERROR):
|
# with self.assertLogs(log.rfc6266, logging.ERROR):
|
||||||
cd_inline, cd_filename = http.parse_content_disposition(reply)
|
cd_inline, cd_filename = http.parse_content_disposition(reply)
|
||||||
assert cd_filename == DEFAULT_NAME
|
assert cd_filename == DEFAULT_NAME
|
||||||
|
@ -41,7 +41,7 @@ def test_parse_content_disposition(caplog, template, stubs, s):
|
|||||||
"""Test parsing headers based on templates which hypothesis completes."""
|
"""Test parsing headers based on templates which hypothesis completes."""
|
||||||
header = template.format(s)
|
header = template.format(s)
|
||||||
reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header})
|
reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header})
|
||||||
with caplog.atLevel(logging.ERROR, 'rfc6266'):
|
with caplog.at_level(logging.ERROR, 'rfc6266'):
|
||||||
http.parse_content_disposition(reply)
|
http.parse_content_disposition(reply)
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
|
|||||||
|
|
||||||
disk_cache = cache.DiskCache(str(tmpdir))
|
disk_cache = cache.DiskCache(str(tmpdir))
|
||||||
cache_file = disk_cache.fileMetaData("nosuchfile")
|
cache_file = disk_cache.fileMetaData("nosuchfile")
|
||||||
assert cache_file.isValid() == False
|
assert not cache_file.isValid()
|
||||||
|
|
||||||
|
|
||||||
def test_cache_deactivated_metadata_file(config_stub, tmpdir):
|
def test_cache_deactivated_metadata_file(config_stub, tmpdir):
|
||||||
@ -207,7 +207,7 @@ def test_cache_deactivated_remove_data(config_stub, tmpdir):
|
|||||||
disk_cache = cache.DiskCache(str(tmpdir))
|
disk_cache = cache.DiskCache(str(tmpdir))
|
||||||
|
|
||||||
url = QUrl('http://www.example.com/')
|
url = QUrl('http://www.example.com/')
|
||||||
assert disk_cache.remove(url) == False
|
assert not disk_cache.remove(url)
|
||||||
|
|
||||||
|
|
||||||
def test_cache_insert_data(config_stub, tmpdir):
|
def test_cache_insert_data(config_stub, tmpdir):
|
||||||
|
@ -106,13 +106,12 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
|
|||||||
tabbed_browser.current_index = 0
|
tabbed_browser.current_index = 0
|
||||||
tabbed_browser.index_of = index_of
|
tabbed_browser.index_of = index_of
|
||||||
|
|
||||||
with caplog.atLevel(logging.DEBUG, logger='signals'):
|
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||||
objects.signaller.signal.emit('foo')
|
objects.signaller.signal.emit('foo')
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
|
||||||
expected_msg = "{}: filtered_signal('foo') (tab {})".format(verb, index_of)
|
expected_msg = "{}: filtered_signal('foo') (tab {})".format(verb, index_of)
|
||||||
assert records[0].msg == expected_msg
|
assert caplog.records[0].msg == expected_msg
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('index_of', [0, 1])
|
@pytest.mark.parametrize('index_of', [0, 1])
|
||||||
@ -120,10 +119,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
|
|||||||
tabbed_browser.current_index = 0
|
tabbed_browser.current_index = 0
|
||||||
tabbed_browser.index_of = index_of
|
tabbed_browser.index_of = index_of
|
||||||
|
|
||||||
with caplog.atLevel(logging.DEBUG, logger='signals'):
|
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||||
objects.signaller.statusbar_message.emit('foo')
|
objects.signaller.statusbar_message.emit('foo')
|
||||||
|
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_error(objects, tabbed_browser):
|
def test_runtime_error(objects, tabbed_browser):
|
||||||
|
@ -42,3 +42,12 @@ class TestCommandRunner:
|
|||||||
else:
|
else:
|
||||||
with pytest.raises(cmdexc.NoSuchCommandError):
|
with pytest.raises(cmdexc.NoSuchCommandError):
|
||||||
list(cr.parse_all(cmdline_test.cmd, aliases=False))
|
list(cr.parse_all(cmdline_test.cmd, aliases=False))
|
||||||
|
|
||||||
|
def test_parse_with_count(self):
|
||||||
|
"""Test parsing of commands with a count."""
|
||||||
|
cr = runners.CommandRunner(0)
|
||||||
|
result = cr.parse('20:scroll down', aliases=False)
|
||||||
|
assert result.cmd.name == 'scroll'
|
||||||
|
assert result.count == 20
|
||||||
|
assert result.args == ['down']
|
||||||
|
assert result.cmdline == ['scroll', 'down']
|
||||||
|
@ -64,12 +64,11 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
|||||||
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
|
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
|
||||||
obj = Obj("{{ font['foo'] }}")
|
obj = Obj("{{ font['foo'] }}")
|
||||||
|
|
||||||
with caplog.atLevel(9): # VDEBUG
|
with caplog.at_level(9): # VDEBUG
|
||||||
style.set_register_stylesheet(obj)
|
style.set_register_stylesheet(obj)
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
assert caplog.records[0].message == 'stylesheet for Obj: bar'
|
||||||
assert records[0].message == 'stylesheet for Obj: bar'
|
|
||||||
|
|
||||||
assert obj.rendered_stylesheet == 'bar'
|
assert obj.rendered_stylesheet == 'bar'
|
||||||
|
|
||||||
@ -104,11 +103,10 @@ class TestColorDict:
|
|||||||
|
|
||||||
def test_key_error(self, caplog):
|
def test_key_error(self, caplog):
|
||||||
d = style.ColorDict()
|
d = style.ColorDict()
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
d['foo'] # pylint: disable=pointless-statement
|
d['foo'] # pylint: disable=pointless-statement
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
assert caplog.records[0].message == 'No color defined for foo!'
|
||||||
assert records[0].message == 'No color defined for foo!'
|
|
||||||
|
|
||||||
def test_qcolor(self):
|
def test_qcolor(self):
|
||||||
d = style.ColorDict()
|
d = style.ColorDict()
|
||||||
|
@ -74,14 +74,14 @@ class TestDebugLog:
|
|||||||
|
|
||||||
def test_log(self, keyparser, caplog):
|
def test_log(self, keyparser, caplog):
|
||||||
keyparser._debug_log('foo')
|
keyparser._debug_log('foo')
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
record = caplog.records()[0]
|
record = caplog.records[0]
|
||||||
assert record.message == 'foo'
|
assert record.message == 'foo'
|
||||||
|
|
||||||
def test_no_log(self, keyparser, caplog):
|
def test_no_log(self, keyparser, caplog):
|
||||||
keyparser.do_log = False
|
keyparser.do_log = False
|
||||||
keyparser._debug_log('foo')
|
keyparser._debug_log('foo')
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('input_key, supports_count, expected', [
|
@pytest.mark.parametrize('input_key, supports_count, expected', [
|
||||||
@ -161,10 +161,10 @@ class TestReadConfig:
|
|||||||
0, supports_count=False, supports_chains=False)
|
0, supports_count=False, supports_chains=False)
|
||||||
kp._warn_on_keychains = warn_on_keychains
|
kp._warn_on_keychains = warn_on_keychains
|
||||||
|
|
||||||
with caplog.atLevel(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
kp.read_config('normal')
|
kp.read_config('normal')
|
||||||
|
|
||||||
assert bool(caplog.records()) == warn_on_keychains
|
assert bool(caplog.records) == warn_on_keychains
|
||||||
|
|
||||||
|
|
||||||
class TestSpecialKeys:
|
class TestSpecialKeys:
|
||||||
|
@ -174,16 +174,16 @@ def test_start_logging(fake_proc, caplog):
|
|||||||
"""Make sure that starting logs the executed commandline."""
|
"""Make sure that starting logs the executed commandline."""
|
||||||
cmd = 'does_not_exist'
|
cmd = 'does_not_exist'
|
||||||
args = ['arg', 'arg with spaces']
|
args = ['arg', 'arg with spaces']
|
||||||
with caplog.atLevel(logging.DEBUG):
|
with caplog.at_level(logging.DEBUG):
|
||||||
fake_proc.start(cmd, args)
|
fake_proc.start(cmd, args)
|
||||||
msgs = [e.msg for e in caplog.records()]
|
msgs = [e.msg for e in caplog.records]
|
||||||
assert msgs == ["Starting process.",
|
assert msgs == ["Starting process.",
|
||||||
"Executing: does_not_exist arg 'arg with spaces'"]
|
"Executing: does_not_exist arg 'arg with spaces'"]
|
||||||
|
|
||||||
|
|
||||||
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
||||||
"""Test the process emitting an error."""
|
"""Test the process emitting an error."""
|
||||||
with caplog.atLevel(logging.ERROR, 'message'):
|
with caplog.at_level(logging.ERROR, 'message'):
|
||||||
with qtbot.waitSignal(proc.error, raising=True, timeout=5000):
|
with qtbot.waitSignal(proc.error, raising=True, timeout=5000):
|
||||||
proc.start('this_does_not_exist_either', [])
|
proc.start('this_does_not_exist_either', [])
|
||||||
|
|
||||||
|
@ -358,11 +358,10 @@ class TestListen:
|
|||||||
|
|
||||||
@pytest.mark.posix
|
@pytest.mark.posix
|
||||||
def test_atime_update_no_name(self, qtbot, caplog, ipc_server):
|
def test_atime_update_no_name(self, qtbot, caplog, ipc_server):
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
ipc_server.update_atime()
|
ipc_server.update_atime()
|
||||||
|
|
||||||
records = caplog.records()
|
assert caplog.records[-1].msg == "In update_atime with no server path!"
|
||||||
assert records[-1].msg == "In update_atime with no server path!"
|
|
||||||
|
|
||||||
@pytest.mark.posix
|
@pytest.mark.posix
|
||||||
def test_atime_shutdown_typeerror(self, qtbot, ipc_server):
|
def test_atime_shutdown_typeerror(self, qtbot, ipc_server):
|
||||||
@ -408,22 +407,21 @@ class TestHandleConnection:
|
|||||||
|
|
||||||
def test_no_connection(self, ipc_server, caplog):
|
def test_no_connection(self, ipc_server, caplog):
|
||||||
ipc_server.handle_connection()
|
ipc_server.handle_connection()
|
||||||
record = caplog.records()[-1]
|
assert caplog.records[-1].message == "No new connection to handle."
|
||||||
assert record.message == "No new connection to handle."
|
|
||||||
|
|
||||||
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
|
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
|
||||||
ipc_server._socket = qlocalsocket
|
ipc_server._socket = qlocalsocket
|
||||||
ipc_server.handle_connection()
|
ipc_server.handle_connection()
|
||||||
message = ("Got new connection but ignoring it because we're still "
|
message = ("Got new connection but ignoring it because we're still "
|
||||||
"handling another one.")
|
"handling another one.")
|
||||||
assert message in [rec.message for rec in caplog.records()]
|
assert message in [rec.message for rec in caplog.records]
|
||||||
|
|
||||||
def test_disconnected_immediately(self, ipc_server, caplog):
|
def test_disconnected_immediately(self, ipc_server, caplog):
|
||||||
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
||||||
ipc_server._server = FakeServer(socket)
|
ipc_server._server = FakeServer(socket)
|
||||||
ipc_server.handle_connection()
|
ipc_server.handle_connection()
|
||||||
msg = "Socket was disconnected immediately."
|
msg = "Socket was disconnected immediately."
|
||||||
all_msgs = [r.message for r in caplog.records()]
|
all_msgs = [r.message for r in caplog.records]
|
||||||
assert msg in all_msgs
|
assert msg in all_msgs
|
||||||
|
|
||||||
def test_error_immediately(self, ipc_server, caplog):
|
def test_error_immediately(self, ipc_server, caplog):
|
||||||
@ -436,7 +434,7 @@ class TestHandleConnection:
|
|||||||
exc_msg = 'Error while handling IPC connection: Error string (error 7)'
|
exc_msg = 'Error while handling IPC connection: Error string (error 7)'
|
||||||
assert str(excinfo.value) == exc_msg
|
assert str(excinfo.value) == exc_msg
|
||||||
msg = "We got an error immediately."
|
msg = "We got an error immediately."
|
||||||
all_msgs = [r.message for r in caplog.records()]
|
all_msgs = [r.message for r in caplog.records]
|
||||||
assert msg in all_msgs
|
assert msg in all_msgs
|
||||||
|
|
||||||
def test_read_line_immediately(self, qtbot, ipc_server, caplog):
|
def test_read_line_immediately(self, qtbot, ipc_server, caplog):
|
||||||
@ -454,7 +452,7 @@ class TestHandleConnection:
|
|||||||
assert spy[0][0] == ['foo']
|
assert spy[0][0] == ['foo']
|
||||||
assert spy[0][1] == 'tab'
|
assert spy[0][1] == 'tab'
|
||||||
|
|
||||||
all_msgs = [r.message for r in caplog.records()]
|
all_msgs = [r.message for r in caplog.records]
|
||||||
assert "We can read a line immediately." in all_msgs
|
assert "We can read a line immediately." in all_msgs
|
||||||
|
|
||||||
|
|
||||||
@ -505,11 +503,11 @@ def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
|
|||||||
got_args_spy = QSignalSpy(ipc_server.got_args)
|
got_args_spy = QSignalSpy(ipc_server.got_args)
|
||||||
|
|
||||||
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
|
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignals(signals, raising=True):
|
with qtbot.waitSignals(signals, raising=True):
|
||||||
connected_socket.write(data)
|
connected_socket.write(data)
|
||||||
|
|
||||||
messages = [r.message for r in caplog.records()]
|
messages = [r.message for r in caplog.records]
|
||||||
assert messages[-1] == 'Ignoring invalid IPC data.'
|
assert messages[-1] == 'Ignoring invalid IPC data.'
|
||||||
assert messages[-2].startswith(msg)
|
assert messages[-2].startswith(msg)
|
||||||
assert not got_args_spy
|
assert not got_args_spy
|
||||||
@ -542,11 +540,11 @@ class TestSendToRunningInstance:
|
|||||||
def test_no_server(self, caplog):
|
def test_no_server(self, caplog):
|
||||||
sent = ipc.send_to_running_instance('qute-test', [], None)
|
sent = ipc.send_to_running_instance('qute-test', [], None)
|
||||||
assert not sent
|
assert not sent
|
||||||
msg = caplog.records()[-1].message
|
msg = caplog.records[-1].message
|
||||||
assert msg == "No existing instance present (error 2)"
|
assert msg == "No existing instance present (error 2)"
|
||||||
|
|
||||||
@pytest.mark.parametrize('has_cwd', [True, False])
|
@pytest.mark.parametrize('has_cwd', [True, False])
|
||||||
@pytest.mark.posix(reason="Causes random trouble on Windows")
|
@pytest.mark.linux(reason="Causes random trouble on Windows and OS X")
|
||||||
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
||||||
ipc_server.listen()
|
ipc_server.listen()
|
||||||
spy = QSignalSpy(ipc_server.got_args)
|
spy = QSignalSpy(ipc_server.got_args)
|
||||||
@ -610,12 +608,12 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
|
|||||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
||||||
qlocalsocket.connectToServer('qute-test')
|
qlocalsocket.connectToServer('qute-test')
|
||||||
|
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
|
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
|
||||||
timeout=5000):
|
timeout=5000):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert caplog.records()[-1].message == "IPC connection timed out."
|
assert caplog.records[-1].message == "IPC connection timed out."
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('method, args, is_warning', [
|
@pytest.mark.parametrize('method, args, is_warning', [
|
||||||
@ -628,13 +626,13 @@ def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
|
|||||||
assert ipc_server._socket is None
|
assert ipc_server._socket is None
|
||||||
|
|
||||||
if is_warning:
|
if is_warning:
|
||||||
with caplog.atLevel(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
func(*args)
|
func(*args)
|
||||||
else:
|
else:
|
||||||
func(*args)
|
func(*args)
|
||||||
|
|
||||||
msg = "In {} with None socket!".format(method)
|
msg = "In {} with None socket!".format(method)
|
||||||
assert msg in [r.message for r in caplog.records()]
|
assert msg in [r.message for r in caplog.records]
|
||||||
|
|
||||||
|
|
||||||
class TestSendOrListen:
|
class TestSendOrListen:
|
||||||
@ -683,7 +681,7 @@ class TestSendOrListen:
|
|||||||
def test_normal_connection(self, caplog, qtbot, args):
|
def test_normal_connection(self, caplog, qtbot, args):
|
||||||
ret_server = ipc.send_or_listen(args)
|
ret_server = ipc.send_or_listen(args)
|
||||||
assert isinstance(ret_server, ipc.IPCServer)
|
assert isinstance(ret_server, ipc.IPCServer)
|
||||||
msgs = [e.message for e in caplog.records()]
|
msgs = [e.message for e in caplog.records]
|
||||||
assert "Starting IPC server..." in msgs
|
assert "Starting IPC server..." in msgs
|
||||||
objreg_server = objreg.get('ipc-server')
|
objreg_server = objreg.get('ipc-server')
|
||||||
assert objreg_server is ret_server
|
assert objreg_server is ret_server
|
||||||
@ -698,7 +696,7 @@ class TestSendOrListen:
|
|||||||
with qtbot.waitSignal(legacy_server.got_args, raising=True):
|
with qtbot.waitSignal(legacy_server.got_args, raising=True):
|
||||||
ret = ipc.send_or_listen(args)
|
ret = ipc.send_or_listen(args)
|
||||||
assert ret is None
|
assert ret is None
|
||||||
msgs = [e.message for e in caplog.records()]
|
msgs = [e.message for e in caplog.records]
|
||||||
assert "Connecting to {}".format(legacy_server._socketname) in msgs
|
assert "Connecting to {}".format(legacy_server._socketname) in msgs
|
||||||
|
|
||||||
@pytest.mark.posix(reason="Unneeded on Windows")
|
@pytest.mark.posix(reason="Unneeded on Windows")
|
||||||
@ -775,7 +773,7 @@ class TestSendOrListen:
|
|||||||
|
|
||||||
ret = ipc.send_or_listen(args)
|
ret = ipc.send_or_listen(args)
|
||||||
assert ret is None
|
assert ret is None
|
||||||
msgs = [e.message for e in caplog.records()]
|
msgs = [e.message for e in caplog.records]
|
||||||
assert "Got AddressInUseError, trying again." in msgs
|
assert "Got AddressInUseError, trying again." in msgs
|
||||||
|
|
||||||
@pytest.mark.parametrize('has_error, exc_name, exc_msg', [
|
@pytest.mark.parametrize('has_error, exc_name, exc_msg', [
|
||||||
@ -812,12 +810,11 @@ class TestSendOrListen:
|
|||||||
QLocalSocket.ConnectionRefusedError, # error() gets called twice
|
QLocalSocket.ConnectionRefusedError, # error() gets called twice
|
||||||
]
|
]
|
||||||
|
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with pytest.raises(ipc.Error):
|
with pytest.raises(ipc.Error):
|
||||||
ipc.send_or_listen(args)
|
ipc.send_or_listen(args)
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
|
||||||
|
|
||||||
error_msgs = [
|
error_msgs = [
|
||||||
'Handling fatal misc.ipc.{} with --no-err-windows!'.format(
|
'Handling fatal misc.ipc.{} with --no-err-windows!'.format(
|
||||||
@ -828,7 +825,7 @@ class TestSendOrListen:
|
|||||||
'post_text: Maybe another instance is running but frozen?',
|
'post_text: Maybe another instance is running but frozen?',
|
||||||
'exception text: {}'.format(exc_msg),
|
'exception text: {}'.format(exc_msg),
|
||||||
]
|
]
|
||||||
assert records[0].msg == '\n'.join(error_msgs)
|
assert caplog.records[0].msg == '\n'.join(error_msgs)
|
||||||
|
|
||||||
@pytest.mark.posix(reason="Flaky on Windows")
|
@pytest.mark.posix(reason="Flaky on Windows")
|
||||||
def test_error_while_listening(self, qlocalserver_mock, caplog, args):
|
def test_error_while_listening(self, qlocalserver_mock, caplog, args):
|
||||||
@ -837,12 +834,11 @@ class TestSendOrListen:
|
|||||||
err = QAbstractSocket.SocketResourceError
|
err = QAbstractSocket.SocketResourceError
|
||||||
qlocalserver_mock().serverError.return_value = err
|
qlocalserver_mock().serverError.return_value = err
|
||||||
|
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with pytest.raises(ipc.Error):
|
with pytest.raises(ipc.Error):
|
||||||
ipc.send_or_listen(args)
|
ipc.send_or_listen(args)
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
|
||||||
|
|
||||||
error_msgs = [
|
error_msgs = [
|
||||||
'Handling fatal misc.ipc.ListenError with --no-err-windows!',
|
'Handling fatal misc.ipc.ListenError with --no-err-windows!',
|
||||||
@ -853,7 +849,7 @@ class TestSendOrListen:
|
|||||||
'exception text: Error while listening to IPC server: Error '
|
'exception text: Error while listening to IPC server: Error '
|
||||||
'string (error 4)',
|
'string (error 4)',
|
||||||
]
|
]
|
||||||
assert records[0].msg == '\n'.join(error_msgs)
|
assert caplog.records[0].msg == '\n'.join(error_msgs)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.windows
|
@pytest.mark.windows
|
||||||
|
@ -821,10 +821,9 @@ class TestSessionDelete:
|
|||||||
tmpdir.chmod(0o555) # unwritable
|
tmpdir.chmod(0o555) # unwritable
|
||||||
|
|
||||||
with pytest.raises(cmdexc.CommandError) as excinfo:
|
with pytest.raises(cmdexc.CommandError) as excinfo:
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
sess_man.session_delete('foo')
|
sess_man.session_delete('foo')
|
||||||
|
|
||||||
assert str(excinfo.value).startswith('Error while deleting session: ')
|
assert str(excinfo.value).startswith('Error while deleting session: ')
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
assert caplog.records[0].message == 'Error while deleting session!'
|
||||||
assert records[0].message == 'Error while deleting session!'
|
|
||||||
|
@ -51,7 +51,8 @@ class CovtestHelper:
|
|||||||
coveragerc = str(self._testdir.tmpdir / 'coveragerc')
|
coveragerc = str(self._testdir.tmpdir / 'coveragerc')
|
||||||
return self._testdir.runpytest('--cov=module',
|
return self._testdir.runpytest('--cov=module',
|
||||||
'--cov-config={}'.format(coveragerc),
|
'--cov-config={}'.format(coveragerc),
|
||||||
'--cov-report=xml')
|
'--cov-report=xml',
|
||||||
|
plugins=['no:faulthandler'])
|
||||||
|
|
||||||
def check(self, perfect_files=None):
|
def check(self, perfect_files=None):
|
||||||
"""Run check_coverage.py and run its return value."""
|
"""Run check_coverage.py and run its return value."""
|
||||||
@ -165,6 +166,7 @@ def test_tested_unlisted(covtest):
|
|||||||
@pytest.mark.parametrize('args, reason', [
|
@pytest.mark.parametrize('args, reason', [
|
||||||
(['-k', 'foo'], "because -k is given."),
|
(['-k', 'foo'], "because -k is given."),
|
||||||
(['-m', 'foo'], "because -m is given."),
|
(['-m', 'foo'], "because -m is given."),
|
||||||
|
(['--lf'], "because --lf is given."),
|
||||||
(['blah', '-m', 'foo'], "because -m is given."),
|
(['blah', '-m', 'foo'], "because -m is given."),
|
||||||
(['tests/foo'], "because there is nothing to check."),
|
(['tests/foo'], "because there is nothing to check."),
|
||||||
])
|
])
|
||||||
|
@ -41,9 +41,8 @@ def test_log_events(qapp, caplog):
|
|||||||
obj = EventObject()
|
obj = EventObject()
|
||||||
qapp.postEvent(obj, QEvent(QEvent.User))
|
qapp.postEvent(obj, QEvent(QEvent.User))
|
||||||
qapp.processEvents()
|
qapp.processEvents()
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
assert caplog.records[0].msg == 'Event in test_debug.EventObject: User'
|
||||||
assert records[0].msg == 'Event in test_debug.EventObject: User'
|
|
||||||
|
|
||||||
|
|
||||||
class SignalObject(QObject):
|
class SignalObject(QObject):
|
||||||
@ -75,10 +74,9 @@ def test_log_signals(caplog, signal_obj):
|
|||||||
signal_obj.signal1.emit()
|
signal_obj.signal1.emit()
|
||||||
signal_obj.signal2.emit('foo', 'bar')
|
signal_obj.signal2.emit('foo', 'bar')
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 2
|
||||||
assert len(records) == 2
|
assert caplog.records[0].msg == 'Signal in <repr>: signal1()'
|
||||||
assert records[0].msg == 'Signal in <repr>: signal1()'
|
assert caplog.records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
|
||||||
assert records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
|
|
||||||
|
|
||||||
|
|
||||||
class TestLogTime:
|
class TestLogTime:
|
||||||
@ -86,15 +84,14 @@ class TestLogTime:
|
|||||||
def test_duration(self, caplog):
|
def test_duration(self, caplog):
|
||||||
logger_name = 'qt-tests'
|
logger_name = 'qt-tests'
|
||||||
|
|
||||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
with caplog.at_level(logging.DEBUG, logger_name):
|
||||||
with debug.log_time(logger_name, action='foobar'):
|
with debug.log_time(logger_name, action='foobar'):
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
|
||||||
|
|
||||||
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
|
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
|
||||||
match = pattern.match(records[0].msg)
|
match = pattern.match(caplog.records[0].msg)
|
||||||
assert match
|
assert match
|
||||||
|
|
||||||
duration = float(match.group(1))
|
duration = float(match.group(1))
|
||||||
@ -104,11 +101,11 @@ class TestLogTime:
|
|||||||
"""Test with an explicit logger instead of a name."""
|
"""Test with an explicit logger instead of a name."""
|
||||||
logger_name = 'qt-tests'
|
logger_name = 'qt-tests'
|
||||||
|
|
||||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
with caplog.at_level(logging.DEBUG, logger_name):
|
||||||
with debug.log_time(logging.getLogger(logger_name)):
|
with debug.log_time(logging.getLogger(logger_name)):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
|
|
||||||
def test_decorator(self, caplog):
|
def test_decorator(self, caplog):
|
||||||
logger_name = 'qt-tests'
|
logger_name = 'qt-tests'
|
||||||
@ -118,12 +115,11 @@ class TestLogTime:
|
|||||||
assert arg == 1
|
assert arg == 1
|
||||||
assert kwarg == 2
|
assert kwarg == 2
|
||||||
|
|
||||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
with caplog.at_level(logging.DEBUG, logger_name):
|
||||||
func(1, kwarg=2)
|
func(1, kwarg=2)
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
assert caplog.records[0].msg.startswith('Foo took')
|
||||||
assert records[0].msg.startswith('Foo took')
|
|
||||||
|
|
||||||
|
|
||||||
class TestQEnumKey:
|
class TestQEnumKey:
|
||||||
|
@ -52,12 +52,11 @@ def test_no_err_windows(caplog, exc, name, exc_text):
|
|||||||
try:
|
try:
|
||||||
raise exc
|
raise exc
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with caplog.atLevel(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
error.handle_fatal_exc(e, Args(no_err_windows=True), 'title',
|
error.handle_fatal_exc(e, Args(no_err_windows=True), 'title',
|
||||||
pre_text='pre', post_text='post')
|
pre_text='pre', post_text='post')
|
||||||
|
|
||||||
records = caplog.records()
|
assert len(caplog.records) == 1
|
||||||
assert len(records) == 1
|
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
'Handling fatal {} with --no-err-windows!'.format(name),
|
'Handling fatal {} with --no-err-windows!'.format(name),
|
||||||
@ -67,7 +66,7 @@ def test_no_err_windows(caplog, exc, name, exc_text):
|
|||||||
'post_text: post',
|
'post_text: post',
|
||||||
'exception text: {}'.format(exc_text),
|
'exception text: {}'.format(exc_text),
|
||||||
]
|
]
|
||||||
assert records[0].msg == '\n'.join(expected)
|
assert caplog.records[0].msg == '\n'.join(expected)
|
||||||
|
|
||||||
|
|
||||||
# This happens on Xvfb for some reason
|
# This happens on Xvfb for some reason
|
||||||
|
@ -25,7 +25,7 @@ import itertools
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_capturelog # pylint: disable=import-error
|
import pytest_catchlog # pylint: disable=import-error
|
||||||
|
|
||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
@ -60,10 +60,11 @@ def restore_loggers():
|
|||||||
while root_logger.handlers:
|
while root_logger.handlers:
|
||||||
h = root_logger.handlers[0]
|
h = root_logger.handlers[0]
|
||||||
root_logger.removeHandler(h)
|
root_logger.removeHandler(h)
|
||||||
h.close()
|
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||||
|
h.close()
|
||||||
root_logger.setLevel(original_logging_level)
|
root_logger.setLevel(original_logging_level)
|
||||||
for h in root_handlers:
|
for h in root_handlers:
|
||||||
if not isinstance(h, pytest_capturelog.CaptureLogHandler):
|
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/856
|
# https://github.com/The-Compiler/qutebrowser/issues/856
|
||||||
root_logger.addHandler(h)
|
root_logger.addHandler(h)
|
||||||
logging._acquireLock()
|
logging._acquireLock()
|
||||||
@ -238,30 +239,30 @@ class TestHideQtWarning:
|
|||||||
def test_unfiltered(self, logger, caplog):
|
def test_unfiltered(self, logger, caplog):
|
||||||
"""Test a message which is not filtered."""
|
"""Test a message which is not filtered."""
|
||||||
with log.hide_qt_warning("World", 'qt-tests'):
|
with log.hide_qt_warning("World", 'qt-tests'):
|
||||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||||
logger.warning("Hello World")
|
logger.warning("Hello World")
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
record = caplog.records()[0]
|
record = caplog.records[0]
|
||||||
assert record.levelname == 'WARNING'
|
assert record.levelname == 'WARNING'
|
||||||
assert record.message == "Hello World"
|
assert record.message == "Hello World"
|
||||||
|
|
||||||
def test_filtered_exact(self, logger, caplog):
|
def test_filtered_exact(self, logger, caplog):
|
||||||
"""Test a message which is filtered (exact match)."""
|
"""Test a message which is filtered (exact match)."""
|
||||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||||
logger.warning("Hello")
|
logger.warning("Hello")
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
def test_filtered_start(self, logger, caplog):
|
def test_filtered_start(self, logger, caplog):
|
||||||
"""Test a message which is filtered (match at line start)."""
|
"""Test a message which is filtered (match at line start)."""
|
||||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||||
logger.warning("Hello World")
|
logger.warning("Hello World")
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
def test_filtered_whitespace(self, logger, caplog):
|
def test_filtered_whitespace(self, logger, caplog):
|
||||||
"""Test a message which is filtered (match with whitespace)."""
|
"""Test a message which is filtered (match with whitespace)."""
|
||||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||||
logger.warning(" Hello World ")
|
logger.warning(" Hello World ")
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
@ -263,10 +263,10 @@ class TestInitCacheDirTag:
|
|||||||
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
|
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
|
||||||
lambda: str(tmpdir))
|
lambda: str(tmpdir))
|
||||||
mocker.patch('builtins.open', side_effect=OSError)
|
mocker.patch('builtins.open', side_effect=OSError)
|
||||||
with caplog.atLevel(logging.ERROR, 'init'):
|
with caplog.at_level(logging.ERROR, 'init'):
|
||||||
standarddir._init_cachedir_tag()
|
standarddir._init_cachedir_tag()
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG'
|
assert caplog.records[0].message == 'Failed to create CACHEDIR.TAG'
|
||||||
assert not tmpdir.listdir()
|
assert not tmpdir.listdir()
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,11 +241,11 @@ class TestActuteWarning:
|
|||||||
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
|
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
|
||||||
create=True)
|
create=True)
|
||||||
|
|
||||||
with caplog.atLevel(logging.ERROR, 'init'):
|
with caplog.at_level(logging.ERROR, 'init'):
|
||||||
utils.actute_warning()
|
utils.actute_warning()
|
||||||
|
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
assert caplog.records()[0].message == 'Failed to read Compose file'
|
assert caplog.records[0].message == 'Failed to read Compose file'
|
||||||
out, _err = capsys.readouterr()
|
out, _err = capsys.readouterr()
|
||||||
assert not out
|
assert not out
|
||||||
|
|
||||||
@ -427,8 +427,7 @@ class TestFormatSize:
|
|||||||
|
|
||||||
class TestKeyToString:
|
class TestKeyToString:
|
||||||
|
|
||||||
KEYS = [(k, v) for k, v in sorted(vars(Qt).items())
|
"""Test key_to_string."""
|
||||||
if isinstance(v, Qt.Key)]
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('key, expected', [
|
@pytest.mark.parametrize('key, expected', [
|
||||||
(Qt.Key_Blue, 'Blue'),
|
(Qt.Key_Blue, 'Blue'),
|
||||||
@ -449,13 +448,15 @@ class TestKeyToString:
|
|||||||
# want to know if the mapping still behaves properly.
|
# want to know if the mapping still behaves properly.
|
||||||
assert utils.key_to_string(Qt.Key_A) == 'A'
|
assert utils.key_to_string(Qt.Key_A) == 'A'
|
||||||
|
|
||||||
@pytest.mark.parametrize('key', [e[1] for e in KEYS],
|
def test_all(self):
|
||||||
ids=[e[0] for e in KEYS])
|
|
||||||
def test_all(self, key):
|
|
||||||
"""Make sure there's some sensible output for all keys."""
|
"""Make sure there's some sensible output for all keys."""
|
||||||
string = utils.key_to_string(key)
|
for name, value in sorted(vars(Qt).items()):
|
||||||
assert string
|
if not isinstance(value, Qt.Key):
|
||||||
string.encode('utf-8') # make sure it's encodable
|
continue
|
||||||
|
print(name)
|
||||||
|
string = utils.key_to_string(value)
|
||||||
|
assert string
|
||||||
|
string.encode('utf-8') # make sure it's encodable
|
||||||
|
|
||||||
|
|
||||||
class TestKeyEventToString:
|
class TestKeyEventToString:
|
||||||
@ -669,12 +670,12 @@ class TestPreventExceptions:
|
|||||||
|
|
||||||
def test_raising(self, caplog):
|
def test_raising(self, caplog):
|
||||||
"""Test with a raising function."""
|
"""Test with a raising function."""
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
ret = self.func_raising()
|
ret = self.func_raising()
|
||||||
assert ret == 42
|
assert ret == 42
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
expected = 'Error in test_utils.TestPreventExceptions.func_raising'
|
expected = 'Error in test_utils.TestPreventExceptions.func_raising'
|
||||||
actual = caplog.records()[0].message
|
actual = caplog.records[0].message
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
@utils.prevent_exceptions(42)
|
@utils.prevent_exceptions(42)
|
||||||
@ -683,10 +684,10 @@ class TestPreventExceptions:
|
|||||||
|
|
||||||
def test_not_raising(self, caplog):
|
def test_not_raising(self, caplog):
|
||||||
"""Test with a non-raising function."""
|
"""Test with a non-raising function."""
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
ret = self.func_not_raising()
|
ret = self.func_not_raising()
|
||||||
assert ret == 23
|
assert ret == 23
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
@utils.prevent_exceptions(42, True)
|
@utils.prevent_exceptions(42, True)
|
||||||
def func_predicate_true(self):
|
def func_predicate_true(self):
|
||||||
@ -694,10 +695,10 @@ class TestPreventExceptions:
|
|||||||
|
|
||||||
def test_predicate_true(self, caplog):
|
def test_predicate_true(self, caplog):
|
||||||
"""Test with a True predicate."""
|
"""Test with a True predicate."""
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
ret = self.func_predicate_true()
|
ret = self.func_predicate_true()
|
||||||
assert ret == 42
|
assert ret == 42
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
|
|
||||||
@utils.prevent_exceptions(42, False)
|
@utils.prevent_exceptions(42, False)
|
||||||
def func_predicate_false(self):
|
def func_predicate_false(self):
|
||||||
@ -705,10 +706,10 @@ class TestPreventExceptions:
|
|||||||
|
|
||||||
def test_predicate_false(self, caplog):
|
def test_predicate_false(self, caplog):
|
||||||
"""Test with a False predicate."""
|
"""Test with a False predicate."""
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
self.func_predicate_false()
|
self.func_predicate_false()
|
||||||
assert not caplog.records()
|
assert not caplog.records
|
||||||
|
|
||||||
|
|
||||||
class Obj:
|
class Obj:
|
||||||
|
@ -108,7 +108,7 @@ class TestGitStr:
|
|||||||
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
|
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
|
||||||
raising=False)
|
raising=False)
|
||||||
commit_file_mock.side_effect = OSError
|
commit_file_mock.side_effect = OSError
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
assert version._git_str() is None
|
assert version._git_str() is None
|
||||||
|
|
||||||
@pytest.mark.not_frozen
|
@pytest.mark.not_frozen
|
||||||
@ -136,7 +136,7 @@ class TestGitStr:
|
|||||||
m.path.join.side_effect = OSError
|
m.path.join.side_effect = OSError
|
||||||
mocker.patch('qutebrowser.utils.version.utils.read_file',
|
mocker.patch('qutebrowser.utils.version.utils.read_file',
|
||||||
side_effect=OSError)
|
side_effect=OSError)
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
assert version._git_str() is None
|
assert version._git_str() is None
|
||||||
|
|
||||||
@pytest.mark.not_frozen
|
@pytest.mark.not_frozen
|
||||||
@ -145,10 +145,10 @@ class TestGitStr:
|
|||||||
"""Test with undefined __file__ but available git-commit-id."""
|
"""Test with undefined __file__ but available git-commit-id."""
|
||||||
monkeypatch.delattr('qutebrowser.utils.version.__file__')
|
monkeypatch.delattr('qutebrowser.utils.version.__file__')
|
||||||
commit_file_mock.return_value = '0deadcode'
|
commit_file_mock.return_value = '0deadcode'
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
assert version._git_str() == '0deadcode'
|
assert version._git_str() == '0deadcode'
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
assert caplog.records()[0].message == "Error while getting git path"
|
assert caplog.records[0].message == "Error while getting git path"
|
||||||
|
|
||||||
|
|
||||||
def _has_git():
|
def _has_git():
|
||||||
@ -294,11 +294,11 @@ def test_release_info(files, expected, caplog, monkeypatch):
|
|||||||
fake = ReleaseInfoFake(files)
|
fake = ReleaseInfoFake(files)
|
||||||
monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake)
|
monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake)
|
||||||
monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
|
monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
assert version._release_info() == expected
|
assert version._release_info() == expected
|
||||||
if files is None:
|
if files is None:
|
||||||
assert len(caplog.records()) == 1
|
assert len(caplog.records) == 1
|
||||||
assert caplog.records()[0].message == "Error while reading fake-file."
|
assert caplog.records[0].message == "Error while reading fake-file."
|
||||||
|
|
||||||
|
|
||||||
class ImportFake:
|
class ImportFake:
|
||||||
|
@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog):
|
|||||||
"""Test Question.abort() with .emit() raising a TypeError."""
|
"""Test Question.abort() with .emit() raising a TypeError."""
|
||||||
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
|
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
|
||||||
signal_mock.emit.side_effect = TypeError
|
signal_mock.emit.side_effect = TypeError
|
||||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
with caplog.at_level(logging.ERROR, 'misc'):
|
||||||
question.abort()
|
question.abort()
|
||||||
assert caplog.records()[0].message == 'Error while aborting question'
|
assert caplog.records[0].message == 'Error while aborting question'
|
||||||
|
13
tox.ini
13
tox.ini
@ -26,11 +26,11 @@ deps =
|
|||||||
parse==1.6.6
|
parse==1.6.6
|
||||||
parse-type==0.3.4
|
parse-type==0.3.4
|
||||||
py==1.4.30
|
py==1.4.30
|
||||||
pytest==2.7.3 # rq.filter: <2.8.0
|
pytest==2.8.2
|
||||||
pytest-bdd==2.15.0
|
pytest-bdd==2.15.0
|
||||||
pytest-capturelog==0.7
|
pytest-catchlog==1.2.0
|
||||||
pytest-cov==2.2.0
|
pytest-cov==2.2.0
|
||||||
pytest-faulthandler==1.0.1
|
pytest-faulthandler==1.1.0
|
||||||
pytest-html==1.7
|
pytest-html==1.7
|
||||||
pytest-mock==0.8.1
|
pytest-mock==0.8.1
|
||||||
pytest-qt==1.9.0
|
pytest-qt==1.9.0
|
||||||
@ -39,12 +39,12 @@ deps =
|
|||||||
six==1.10.0
|
six==1.10.0
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
vulture==0.8.1
|
vulture==0.8.1
|
||||||
Werkzeug==0.11.1
|
Werkzeug==0.11.2
|
||||||
wheel==0.26.0
|
wheel==0.26.0
|
||||||
xvfbwrapper==0.2.5
|
xvfbwrapper==0.2.5
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m py.test --strict -rfEsw --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||||
{envpython} scripts/dev/check_coverage.py {posargs}
|
{envpython} scripts/dev/check_coverage.py {posargs}
|
||||||
|
|
||||||
[testenv:mkvenv]
|
[testenv:mkvenv]
|
||||||
@ -105,6 +105,7 @@ deps =
|
|||||||
astroid==1.3.8
|
astroid==1.3.8
|
||||||
pylint==1.4.4
|
pylint==1.4.4
|
||||||
logilab-common==1.1.0
|
logilab-common==1.1.0
|
||||||
|
requests==2.8.1
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
{envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||||
@ -204,7 +205,7 @@ basepython = python3
|
|||||||
skip_install = true
|
skip_install = true
|
||||||
passenv =
|
passenv =
|
||||||
deps =
|
deps =
|
||||||
check-manifest==0.27
|
check-manifest==0.28
|
||||||
commands =
|
commands =
|
||||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||||
|
|
||||||
|
17
www/header.asciidoc
Normal file
17
www/header.asciidoc
Normal 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>
|
||||||
|
+++
|
BIN
www/media/OpenSans-Bold.woff2
Normal file
BIN
www/media/OpenSans-Bold.woff2
Normal file
Binary file not shown.
255
www/media/OpenSans-License.asciidoc
Normal file
255
www/media/OpenSans-License.asciidoc
Normal 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.
|
||||||
|
----
|
BIN
www/media/OpenSans-Regular.woff2
Normal file
BIN
www/media/OpenSans-Regular.woff2
Normal file
Binary file not shown.
BIN
www/media/favicon.png
Normal file
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
11
www/media/font.css
Normal 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
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
181
www/qute.css
Normal 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>
|
Loading…
Reference in New Issue
Block a user