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

qutebrowser

+ A keyboard-driven browser. +
+
+ ++++ diff --git a/www/media/OpenSans-Bold.woff2 b/www/media/OpenSans-Bold.woff2 new file mode 100644 index 000000000..be4c25b42 Binary files /dev/null and b/www/media/OpenSans-Bold.woff2 differ diff --git a/www/media/OpenSans-License.asciidoc b/www/media/OpenSans-License.asciidoc new file mode 100644 index 000000000..eee8aa73c --- /dev/null +++ b/www/media/OpenSans-License.asciidoc @@ -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. +---- \ No newline at end of file diff --git a/www/media/OpenSans-Regular.woff2 b/www/media/OpenSans-Regular.woff2 new file mode 100644 index 000000000..5287058cf Binary files /dev/null and b/www/media/OpenSans-Regular.woff2 differ diff --git a/www/media/favicon.png b/www/media/favicon.png new file mode 100644 index 000000000..7efee9a05 Binary files /dev/null and b/www/media/favicon.png differ diff --git a/www/media/font.css b/www/media/font.css new file mode 100644 index 000000000..c2f216991 --- /dev/null +++ b/www/media/font.css @@ -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"); +} diff --git a/www/media/qutebrowser.svg b/www/media/qutebrowser.svg new file mode 100644 index 000000000..bf75e2462 --- /dev/null +++ b/www/media/qutebrowser.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/www/qute.css b/www/qute.css new file mode 100644 index 000000000..b8384d7fc --- /dev/null +++ b/www/qute.css @@ -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; + } +} + + + + + +