Merge branch 'master' into visual
Conflicts: qutebrowser/browser/commands.py
This commit is contained in:
commit
640f758605
@ -28,6 +28,14 @@ Changed
|
||||
- `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
|
||||
- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior.
|
||||
|
||||
v0.2.2 (unreleased)
|
||||
-------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
|
@ -135,8 +135,8 @@ Contributors, sorted by the number of commits in descending order:
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Bruno Oliveira
|
||||
* Joel Torstensson
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Claude
|
||||
* ZDarian
|
||||
* Peter Vilim
|
||||
|
@ -947,6 +947,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|
||||
|==============
|
||||
[[debug-all-objects]]
|
||||
=== debug-all-objects
|
||||
@ -995,3 +996,17 @@ Trace executed code via hunter.
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-webaction]]
|
||||
=== debug-webaction
|
||||
Syntax: +:debug-webaction 'action'+
|
||||
|
||||
Execute a webaction.
|
||||
|
||||
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
|
||||
|
||||
==== positional arguments
|
||||
* +'action'+: The action to execute, e.g. MoveToNextChar.
|
||||
|
||||
==== count
|
||||
How many times to repeat the action.
|
||||
|
||||
|
@ -1341,3 +1341,23 @@ class CommandDispatcher:
|
||||
def drop_selection(self):
|
||||
"""Drop selection and stay in visual mode."""
|
||||
self._current_widget().triggerPageAction(QWebPage.MoveToNextChar)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
count='count', debug=True)
|
||||
def debug_webaction(self, action, count=1):
|
||||
"""Execute a webaction.
|
||||
|
||||
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the
|
||||
available actions.
|
||||
|
||||
Args:
|
||||
action: The action to execute, e.g. MoveToNextChar.
|
||||
count: How many times to repeat the action.
|
||||
"""
|
||||
member = getattr(QWebPage, action, None)
|
||||
if not isinstance(member, QWebPage.WebAction):
|
||||
raise cmdexc.CommandError("{} is not a valid web action!".format(
|
||||
action))
|
||||
view = self._current_widget()
|
||||
for _ in range(count):
|
||||
view.triggerPageAction(member)
|
||||
|
@ -161,7 +161,9 @@ def _init_key_config(parent):
|
||||
parent: The parent to use for the KeyConfigParser.
|
||||
"""
|
||||
try:
|
||||
args = objreg.get('args')
|
||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||
args.relaxed_config,
|
||||
parent=parent)
|
||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
||||
log.init.exception(e)
|
||||
|
@ -75,12 +75,13 @@ class KeyConfigParser(QObject):
|
||||
config_dirty = pyqtSignal()
|
||||
UNBOUND_COMMAND = '<unbound>'
|
||||
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: The directory to save the configs in.
|
||||
fname: The filename of the config.
|
||||
relaxed: If given, unknwon commands are ignored.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.is_dirty = False
|
||||
@ -95,7 +96,7 @@ class KeyConfigParser(QObject):
|
||||
if self._configfile is None or not os.path.exists(self._configfile):
|
||||
self._load_default()
|
||||
else:
|
||||
self._read()
|
||||
self._read(relaxed)
|
||||
self._load_default(only_new=True)
|
||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
||||
|
||||
@ -267,8 +268,12 @@ class KeyConfigParser(QObject):
|
||||
else:
|
||||
return True
|
||||
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
def _read(self, relaxed=False):
|
||||
"""Read the config file from disk and parse it.
|
||||
|
||||
Args:
|
||||
relaxed: Ignore unknown commands.
|
||||
"""
|
||||
try:
|
||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
@ -287,8 +292,11 @@ class KeyConfigParser(QObject):
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
e.lineno = i
|
||||
raise
|
||||
if relaxed:
|
||||
continue
|
||||
else:
|
||||
e.lineno = i
|
||||
raise
|
||||
except OSError:
|
||||
log.keyboard.exception("Failed to read key bindings!")
|
||||
for sectname in self.keybindings:
|
||||
|
@ -163,8 +163,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
"""Execute the command currently in the commandline."""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search ',
|
||||
'?': 'search -r ',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
@ -127,7 +127,7 @@ def split(s, keep=False):
|
||||
"""Split a string via ShellLexer.
|
||||
|
||||
Args:
|
||||
keep: Whether to keep are special chars in the split output.
|
||||
keep: Whether to keep special chars in the split output.
|
||||
"""
|
||||
lexer = ShellLexer(s)
|
||||
lexer.keep = keep
|
||||
|
44
tests/commands/test_runners.py
Normal file
44
tests/commands/test_runners.py
Normal file
@ -0,0 +1,44 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for qutebrowser.commands.runners."""
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
|
||||
|
||||
class TestCommandRunner:
|
||||
|
||||
"""Tests for CommandRunner."""
|
||||
|
||||
def test_parse_all(self, cmdline_test):
|
||||
"""Test parsing of commands.
|
||||
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/615
|
||||
|
||||
Args:
|
||||
cmdline_test: A pytest fixture which provides testcases.
|
||||
"""
|
||||
cr = runners.CommandRunner(0)
|
||||
if cmdline_test.valid:
|
||||
list(cr.parse_all(cmdline_test.cmd, aliases=False))
|
||||
else:
|
||||
with pytest.raises(cmdexc.NoSuchCommandError):
|
||||
list(cr.parse_all(cmdline_test.cmd, aliases=False))
|
@ -31,7 +31,9 @@ from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtGui import QColor
|
||||
import pytest
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.config import config, configexc, configdata
|
||||
from qutebrowser.config.parsers import keyconf
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.utils import objreg, standarddir
|
||||
|
||||
|
||||
@ -155,6 +157,27 @@ class TestConfigParser:
|
||||
self.cfg.get('general', 'bar') # pylint: disable=bad-config-call
|
||||
|
||||
|
||||
class TestKeyConfigParser:
|
||||
|
||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||
|
||||
def test_cmd_binding(self, cmdline_test):
|
||||
"""Test various command bindings.
|
||||
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/615
|
||||
|
||||
Args:
|
||||
cmdline_test: A pytest fixture which provides testcases.
|
||||
"""
|
||||
kcp = keyconf.KeyConfigParser(None, None)
|
||||
kcp._cur_section = 'normal'
|
||||
if cmdline_test.valid:
|
||||
kcp._read_command(cmdline_test.cmd)
|
||||
else:
|
||||
with pytest.raises(keyconf.KeyConfigError):
|
||||
kcp._read_command(cmdline_test.cmd)
|
||||
|
||||
|
||||
class TestDefaultConfig:
|
||||
|
||||
"""Test validating of the default config."""
|
||||
@ -164,6 +187,16 @@ class TestDefaultConfig:
|
||||
conf = config.ConfigManager(None, None)
|
||||
conf._validate_all()
|
||||
|
||||
def test_default_key_config(self):
|
||||
"""Test validating of the default key config."""
|
||||
# We import qutebrowser.app so the cmdutils.register decorators run.
|
||||
import qutebrowser.app # pylint: disable=unused-variable
|
||||
conf = keyconf.KeyConfigParser(None, None)
|
||||
runner = runners.CommandRunner(win_id=0)
|
||||
for sectname in configdata.KEY_DATA:
|
||||
for cmd in conf.get_bindings_for(sectname).values():
|
||||
runner.parse(cmd, aliases=False)
|
||||
|
||||
|
||||
class TestConfigInit:
|
||||
|
||||
|
@ -19,6 +19,9 @@
|
||||
|
||||
"""The qutebrowser test suite contest file."""
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@ -49,15 +52,23 @@ def unicode_encode_err():
|
||||
'fake exception') # reason
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def qnam():
|
||||
"""Session-wide QNetworkAccessManager."""
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager
|
||||
nam = QNetworkAccessManager()
|
||||
nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
|
||||
return nam
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webpage():
|
||||
def webpage(qnam):
|
||||
"""Get a new QWebPage object."""
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager
|
||||
|
||||
page = QWebPage()
|
||||
nam = page.networkAccessManager()
|
||||
nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
|
||||
page.networkAccessManager().deleteLater()
|
||||
page.setNetworkAccessManager(qnam)
|
||||
return page
|
||||
|
||||
|
||||
@ -76,3 +87,63 @@ def fake_keyevent_factory():
|
||||
return evtmock
|
||||
|
||||
return fake_keyevent
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""Automatically add a 'gui' marker to all gui-related tests.
|
||||
|
||||
pytest hook called after collection has been performed, adds a marker
|
||||
named "gui" which can be used to filter gui tests from the command line.
|
||||
For example:
|
||||
|
||||
py.test -m "not gui" # run all tests except gui tests
|
||||
py.test -m "gui" # run only gui tests
|
||||
|
||||
Args:
|
||||
items: list of _pytest.main.Node items, where each item represents
|
||||
a python test that will be executed.
|
||||
|
||||
Reference:
|
||||
http://pytest.org/latest/plugins.html
|
||||
"""
|
||||
for item in items:
|
||||
if 'qtbot' in getattr(item, 'fixturenames', ()):
|
||||
item.add_marker('gui')
|
||||
|
||||
|
||||
def _generate_cmdline_tests():
|
||||
"""Generate testcases for test_split_binding."""
|
||||
# pylint: disable=invalid-name
|
||||
TestCase = collections.namedtuple('TestCase', 'cmd, valid')
|
||||
separators = [';;', ' ;; ', ';; ', ' ;;']
|
||||
invalid = ['foo', '']
|
||||
valid = ['leave-mode', 'hint all']
|
||||
# Valid command only -> valid
|
||||
for item in valid:
|
||||
yield TestCase(''.join(item), True)
|
||||
# Invalid command only -> invalid
|
||||
for item in valid:
|
||||
yield TestCase(''.join(item), True)
|
||||
# Invalid command combined with invalid command -> invalid
|
||||
for item in itertools.product(invalid, separators, invalid):
|
||||
yield TestCase(''.join(item), False)
|
||||
# Valid command combined with valid command -> valid
|
||||
for item in itertools.product(valid, separators, valid):
|
||||
yield TestCase(''.join(item), True)
|
||||
# Valid command combined with invalid command -> invalid
|
||||
for item in itertools.product(valid, separators, invalid):
|
||||
yield TestCase(''.join(item), False)
|
||||
# Invalid command combined with valid command -> invalid
|
||||
for item in itertools.product(invalid, separators, valid):
|
||||
yield TestCase(''.join(item), False)
|
||||
# Command with no_cmd_split combined with an "invalid" command -> valid
|
||||
for item in itertools.product(['bind x open'], separators, invalid):
|
||||
yield TestCase(''.join(item), True)
|
||||
|
||||
|
||||
@pytest.fixture(params=_generate_cmdline_tests())
|
||||
def cmdline_test(request):
|
||||
"""Fixture which generates tests for things validating commandlines."""
|
||||
# Import qutebrowser.app so all cmdutils.register decorators get run.
|
||||
import qutebrowser.app # pylint: disable=unused-variable
|
||||
return request.param
|
||||
|
@ -63,44 +63,27 @@ class TestSplitCount:
|
||||
|
||||
"""Test the _split_count method.
|
||||
|
||||
Attributes:
|
||||
kp: The BaseKeyParser we're testing.
|
||||
Class Attributes:
|
||||
TESTS: list of parameters for the tests, as tuples of
|
||||
(input_key, supports_count, expected)
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.kp = basekeyparser.BaseKeyParser(0, supports_count=True)
|
||||
TESTS = [
|
||||
# (input_key, supports_count, expected)
|
||||
('10', True, (10, '')),
|
||||
('10foo', True, (10, 'foo')),
|
||||
('-1foo', True, (None, '-1foo')),
|
||||
('10e4foo', True, (10, 'e4foo')),
|
||||
('foo', True, (None, 'foo')),
|
||||
('10foo', False, (None, '10foo')),
|
||||
]
|
||||
|
||||
def test_onlycount(self):
|
||||
@pytest.mark.parametrize('input_key, supports_count, expected', TESTS)
|
||||
def test_splitcount(self, input_key, supports_count, expected):
|
||||
"""Test split_count with only a count."""
|
||||
self.kp._keystring = '10'
|
||||
assert self.kp._split_count() == (10, '')
|
||||
|
||||
def test_normalcount(self):
|
||||
"""Test split_count with count and text."""
|
||||
self.kp._keystring = '10foo'
|
||||
assert self.kp._split_count() == (10, 'foo')
|
||||
|
||||
def test_minuscount(self):
|
||||
"""Test split_count with a negative count."""
|
||||
self.kp._keystring = '-1foo'
|
||||
assert self.kp._split_count() == (None, '-1foo')
|
||||
|
||||
def test_expcount(self):
|
||||
"""Test split_count with an exponential count."""
|
||||
self.kp._keystring = '10e4foo'
|
||||
assert self.kp._split_count() == (10, 'e4foo')
|
||||
|
||||
def test_nocount(self):
|
||||
"""Test split_count with only a command."""
|
||||
self.kp._keystring = 'foo'
|
||||
assert self.kp._split_count() == (None, 'foo')
|
||||
|
||||
def test_nosupport(self):
|
||||
"""Test split_count with a count when counts aren't supported."""
|
||||
self.kp._supports_count = False
|
||||
self.kp._keystring = '10foo'
|
||||
assert self.kp._split_count() == (None, '10foo')
|
||||
kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
|
||||
kp._keystring = input_key
|
||||
assert kp._split_count() == expected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('fake_keyconfig', 'mock_timer')
|
||||
|
90
tests/misc/test_miscwidgets.py
Normal file
90
tests/misc/test_miscwidgets.py
Normal file
@ -0,0 +1,90 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Test widgets in miscwidgets module."""
|
||||
|
||||
from unittest import mock
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import pytest
|
||||
|
||||
from qutebrowser.misc.miscwidgets import CommandLineEdit
|
||||
|
||||
|
||||
class TestCommandLineEdit:
|
||||
|
||||
"""Tests for CommandLineEdit widget."""
|
||||
|
||||
@pytest.yield_fixture
|
||||
def cmd_edit(self, qtbot):
|
||||
"""Fixture to initialize a CommandLineEdit."""
|
||||
cmd_edit = CommandLineEdit(None)
|
||||
cmd_edit.set_prompt(':')
|
||||
qtbot.add_widget(cmd_edit)
|
||||
assert cmd_edit.text() == ''
|
||||
yield cmd_edit
|
||||
|
||||
@pytest.fixture
|
||||
def mock_clipboard(self, mocker):
|
||||
"""Fixture to mock QApplication.clipboard.
|
||||
|
||||
Return:
|
||||
The mocked QClipboard object.
|
||||
"""
|
||||
mocker.patch.object(QApplication, 'clipboard')
|
||||
clipboard = mock.MagicMock()
|
||||
clipboard.supportsSelection.return_value = True
|
||||
QApplication.clipboard.return_value = clipboard
|
||||
return clipboard
|
||||
|
||||
def test_position(self, qtbot, cmd_edit):
|
||||
"""Test cursor position based on the prompt."""
|
||||
qtbot.keyClicks(cmd_edit, ':hello')
|
||||
assert cmd_edit.text() == ':hello'
|
||||
assert cmd_edit.cursorPosition() == len(':hello')
|
||||
|
||||
cmd_edit.home(mark=True)
|
||||
assert cmd_edit.cursorPosition() == len(':hello')
|
||||
qtbot.keyClick(cmd_edit, Qt.Key_Delete)
|
||||
assert cmd_edit.text() == ':'
|
||||
qtbot.keyClick(cmd_edit, Qt.Key_Backspace)
|
||||
assert cmd_edit.text() == ':'
|
||||
|
||||
qtbot.keyClicks(cmd_edit, 'hey again')
|
||||
assert cmd_edit.text() == ':hey again'
|
||||
|
||||
def test_invalid_prompt(self, qtbot, cmd_edit):
|
||||
"""Test preventing of an invalid prompt being entered."""
|
||||
qtbot.keyClicks(cmd_edit, '$hello')
|
||||
assert cmd_edit.text() == ''
|
||||
|
||||
def test_clipboard_paste(self, qtbot, cmd_edit, mock_clipboard):
|
||||
"""Test pasting commands from clipboard."""
|
||||
mock_clipboard.text.return_value = ':command'
|
||||
qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
|
||||
assert cmd_edit.text() == ':command'
|
||||
|
||||
mock_clipboard.text.return_value = ' param1'
|
||||
qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
|
||||
assert cmd_edit.text() == ':command param1'
|
||||
|
||||
cmd_edit.clear()
|
||||
mock_clipboard.text.return_value = '$ command'
|
||||
qtbot.keyClick(cmd_edit, Qt.Key_Insert, Qt.ShiftModifier)
|
||||
assert cmd_edit.text() == ':command param1'
|
@ -18,8 +18,9 @@
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for qutebrowser.misc.split."""
|
||||
import collections
|
||||
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
from qutebrowser.misc import split
|
||||
|
||||
@ -29,7 +30,7 @@ from qutebrowser.misc import split
|
||||
|
||||
# Format: input/split|output|without|keep/split|output|with|keep/
|
||||
|
||||
test_data = r"""
|
||||
test_data_str = r"""
|
||||
one two/one|two/one| two/
|
||||
one "two three" four/one|two three|four/one| "two three"| four/
|
||||
one 'two three' four/one|two three|four/one| 'two three'| four/
|
||||
@ -104,36 +105,56 @@ foo\ bar/foo bar/foo\ bar/
|
||||
"""
|
||||
|
||||
|
||||
class SplitTests(unittest.TestCase):
|
||||
def _parse_split_test_data_str():
|
||||
"""
|
||||
Parse the test data set into a namedtuple to use in tests.
|
||||
|
||||
Returns:
|
||||
A list of namedtuples with str attributes: input, keep, no_keep
|
||||
"""
|
||||
tuple_class = collections.namedtuple('TestCase', 'input, keep, no_keep')
|
||||
|
||||
result = []
|
||||
for line in test_data_str.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
data = line.split('/')
|
||||
item = tuple_class(input=data[0], keep=data[1].split('|'),
|
||||
no_keep=data[2].split('|'))
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
class TestSplit:
|
||||
|
||||
"""Test split."""
|
||||
|
||||
def test_split(self):
|
||||
@pytest.fixture(params=_parse_split_test_data_str())
|
||||
def split_test_case(self, request):
|
||||
"""Fixture to automatically parametrize all depending tests.
|
||||
|
||||
It will use the test data from test_data_str, parsed using
|
||||
_parse_split_test_data_str().
|
||||
"""
|
||||
return request.param
|
||||
|
||||
def test_split(self, split_test_case):
|
||||
"""Test splitting."""
|
||||
for case in test_data.strip().splitlines():
|
||||
cmd, out = case.split('/')[:-2]
|
||||
with self.subTest(cmd=cmd):
|
||||
items = split.split(cmd)
|
||||
self.assertEqual(items, out.split('|'))
|
||||
items = split.split(split_test_case.input)
|
||||
assert items == split_test_case.keep
|
||||
|
||||
def test_split_keep_original(self):
|
||||
def test_split_keep_original(self, split_test_case):
|
||||
"""Test if splitting with keep=True yields the original string."""
|
||||
for case in test_data.strip().splitlines():
|
||||
cmd = case.split('/')[0]
|
||||
with self.subTest(cmd=cmd):
|
||||
items = split.split(cmd, keep=True)
|
||||
self.assertEqual(''.join(items), cmd)
|
||||
items = split.split(split_test_case.input, keep=True)
|
||||
assert ''.join(items) == split_test_case.input
|
||||
|
||||
def test_split_keep(self):
|
||||
def test_split_keep(self, split_test_case):
|
||||
"""Test splitting with keep=True."""
|
||||
for case in test_data.strip().splitlines():
|
||||
cmd, _mid, out = case.split('/')[:-1]
|
||||
with self.subTest(cmd=cmd):
|
||||
items = split.split(cmd, keep=True)
|
||||
self.assertEqual(items, out.split('|'))
|
||||
items = split.split(split_test_case.input, keep=True)
|
||||
assert items == split_test_case.no_keep
|
||||
|
||||
|
||||
class SimpleSplitTests(unittest.TestCase):
|
||||
class TestSimpleSplit:
|
||||
|
||||
"""Test simple_split."""
|
||||
|
||||
@ -145,27 +166,20 @@ class SimpleSplitTests(unittest.TestCase):
|
||||
'foo\nbar': ['foo', '\nbar'],
|
||||
}
|
||||
|
||||
def test_str_split(self):
|
||||
@pytest.mark.parametrize('test', TESTS)
|
||||
def test_str_split(self, test):
|
||||
"""Test if the behavior matches str.split."""
|
||||
for test in self.TESTS:
|
||||
with self.subTest(string=test):
|
||||
self.assertEqual(split.simple_split(test),
|
||||
test.rstrip().split())
|
||||
assert split.simple_split(test) == test.rstrip().split()
|
||||
|
||||
def test_str_split_maxsplit_1(self):
|
||||
"""Test if the behavior matches str.split with maxsplit=1."""
|
||||
string = "foo bar baz"
|
||||
self.assertEqual(split.simple_split(string, maxsplit=1),
|
||||
string.rstrip().split(maxsplit=1))
|
||||
@pytest.mark.parametrize('s, maxsplit',
|
||||
[("foo bar baz", 1), (" foo bar baz ", 0)])
|
||||
def test_str_split_maxsplit(self, s, maxsplit):
|
||||
"""Test if the behavior matches str.split with given maxsplit."""
|
||||
actual = split.simple_split(s, maxsplit=maxsplit)
|
||||
expected = s.rstrip().split(maxsplit=maxsplit)
|
||||
assert actual == expected
|
||||
|
||||
def test_str_split_maxsplit_0(self):
|
||||
"""Test if the behavior matches str.split with maxsplit=0."""
|
||||
string = " foo bar baz "
|
||||
self.assertEqual(split.simple_split(string, maxsplit=0),
|
||||
string.rstrip().split(maxsplit=0))
|
||||
|
||||
def test_split_keep(self):
|
||||
@pytest.mark.parametrize('test, expected', TESTS.items())
|
||||
def test_split_keep(self, test, expected):
|
||||
"""Test splitting with keep=True."""
|
||||
for test, expected in self.TESTS.items():
|
||||
with self.subTest(string=test):
|
||||
self.assertEqual(split.simple_split(test, keep=True), expected)
|
||||
assert split.simple_split(test, keep=True) == expected
|
||||
|
70
tests/utils/overflow_test_cases.py
Normal file
70
tests/utils/overflow_test_cases.py
Normal file
@ -0,0 +1,70 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Provides test data for overflow checking.
|
||||
|
||||
Module attributes:
|
||||
INT32_MIN: Minimum valid value for a signed int32.
|
||||
INT32_MAX: Maximum valid value for a signed int32.
|
||||
INT64_MIN: Minimum valid value for a signed int64.
|
||||
INT64_MAX: Maximum valid value for a signed int64.
|
||||
GOOD_VALUES: A dict of types mapped to a list of good values.
|
||||
BAD_VALUES: A dict of types mapped to a list of bad values.
|
||||
"""
|
||||
|
||||
INT32_MIN = -(2 ** 31)
|
||||
INT32_MAX = 2 ** 31 - 1
|
||||
INT64_MIN = -(2 ** 63)
|
||||
INT64_MAX = 2 ** 63 - 1
|
||||
|
||||
GOOD_VALUES = {
|
||||
'int': [-1, 0, 1, 23.42, INT32_MIN, INT32_MAX],
|
||||
'int64': [-1, 0, 1, 23.42, INT64_MIN, INT64_MAX],
|
||||
}
|
||||
|
||||
BAD_VALUES = {
|
||||
'int': [(INT32_MIN - 1, INT32_MIN),
|
||||
(INT32_MAX + 1, INT32_MAX),
|
||||
(float(INT32_MAX + 1), INT32_MAX)],
|
||||
'int64': [(INT64_MIN - 1, INT64_MIN),
|
||||
(INT64_MAX + 1, INT64_MAX),
|
||||
(float(INT64_MAX + 1), INT64_MAX)],
|
||||
}
|
||||
|
||||
|
||||
def iter_good_values():
|
||||
"""Yield "good" (C data type, value) tuples.
|
||||
|
||||
Those should pass overflow checking.
|
||||
"""
|
||||
for ctype, values in GOOD_VALUES.items():
|
||||
for value in values:
|
||||
yield ctype, value
|
||||
|
||||
|
||||
def iter_bad_values():
|
||||
"""Yield pairs of "bad" (C type, value, repl) tuples.
|
||||
|
||||
Theose should not pass overflow checking. The third value is the value they
|
||||
should be replaced with if overflow checking should not be fatal.
|
||||
"""
|
||||
for ctype, values in BAD_VALUES.items():
|
||||
for value, repl in values:
|
||||
yield ctype, value, repl
|
@ -20,47 +20,42 @@
|
||||
"""Tests for qutebrowser.utils.jinja."""
|
||||
|
||||
import os.path
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
def _read_file(path):
|
||||
"""Mocked utils.read_file."""
|
||||
if path == os.path.join('html', 'test.html'):
|
||||
return """Hello {{var}}"""
|
||||
else:
|
||||
raise ValueError("Invalid path {}!".format(path))
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_read_file(monkeypatch):
|
||||
"""pytest fixture to patch utils.read_file."""
|
||||
def _read_file(path):
|
||||
"""A read_file which returns a simple template if the path is right."""
|
||||
if path == os.path.join('html', 'test.html'):
|
||||
return """Hello {{var}}"""
|
||||
else:
|
||||
raise ValueError("Invalid path {}!".format(path))
|
||||
|
||||
monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file)
|
||||
|
||||
|
||||
@unittest.mock.patch('qutebrowser.utils.jinja.utils.read_file')
|
||||
class JinjaTests(unittest.TestCase):
|
||||
|
||||
"""Tests for getting template via jinja."""
|
||||
|
||||
def test_simple_template(self, readfile_mock):
|
||||
"""Test with a simple template."""
|
||||
readfile_mock.side_effect = _read_file
|
||||
template = jinja.env.get_template('test.html')
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
data = template.render(var='World') # pylint: disable=no-member
|
||||
self.assertEqual(data, "Hello World")
|
||||
|
||||
def test_utf8(self, readfile_mock):
|
||||
"""Test rendering with an UTF8 template.
|
||||
|
||||
This was an attempt to get a failing test case for #127 but it seems
|
||||
the issue is elsewhere.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/issues/127
|
||||
"""
|
||||
readfile_mock.side_effect = _read_file
|
||||
template = jinja.env.get_template('test.html')
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
data = template.render(var='\u2603') # pylint: disable=no-member
|
||||
self.assertEqual(data, "Hello \u2603")
|
||||
def test_simple_template():
|
||||
"""Test with a simple template."""
|
||||
template = jinja.env.get_template('test.html')
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
data = template.render(var='World') # pylint: disable=no-member
|
||||
assert data == "Hello World"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
def test_utf8():
|
||||
"""Test rendering with an UTF8 template.
|
||||
|
||||
This was an attempt to get a failing test case for #127 but it seems
|
||||
the issue is elsewhere.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/issues/127
|
||||
"""
|
||||
template = jinja.env.get_template('test.html')
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
data = template.render(var='\u2603') # pylint: disable=no-member
|
||||
assert data == "Hello \u2603"
|
||||
|
@ -20,107 +20,74 @@
|
||||
"""Tests for qutebrowser.utils.qtutils."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser.utils import qtutils
|
||||
import overflow_test_cases
|
||||
|
||||
|
||||
class CheckOverflowTests(unittest.TestCase):
|
||||
class TestCheckOverflow:
|
||||
|
||||
"""Test check_overflow.
|
||||
"""Test check_overflow."""
|
||||
|
||||
Class attributes:
|
||||
INT32_MIN: Minimum valid value for a signed int32.
|
||||
INT32_MAX: Maximum valid value for a signed int32.
|
||||
INT64_MIN: Minimum valid value for a signed int64.
|
||||
INT64_MAX: Maximum valid value for a signed int64.
|
||||
GOOD_VALUES: A dict of types mapped to a list of good values.
|
||||
BAD_VALUES: A dict of types mapped to a list of bad values.
|
||||
"""
|
||||
|
||||
INT32_MIN = -(2 ** 31)
|
||||
INT32_MAX = 2 ** 31 - 1
|
||||
INT64_MIN = -(2 ** 63)
|
||||
INT64_MAX = 2 ** 63 - 1
|
||||
|
||||
GOOD_VALUES = {
|
||||
'int': [-1, 0, 1, 23.42, INT32_MIN, INT32_MAX],
|
||||
'int64': [-1, 0, 1, 23.42, INT64_MIN, INT64_MAX],
|
||||
}
|
||||
|
||||
BAD_VALUES = {
|
||||
'int': [(INT32_MIN - 1, INT32_MIN),
|
||||
(INT32_MAX + 1, INT32_MAX),
|
||||
(float(INT32_MAX + 1), INT32_MAX)],
|
||||
'int64': [(INT64_MIN - 1, INT64_MIN),
|
||||
(INT64_MAX + 1, INT64_MAX),
|
||||
(float(INT64_MAX + 1), INT64_MAX)],
|
||||
}
|
||||
|
||||
def test_good_values(self):
|
||||
@pytest.mark.parametrize('ctype, val',
|
||||
overflow_test_cases.iter_good_values())
|
||||
def test_good_values(self, ctype, val):
|
||||
"""Test values which are inside bounds."""
|
||||
for ctype, vals in self.GOOD_VALUES.items():
|
||||
for val in vals:
|
||||
with self.subTest(ctype=ctype, val=val):
|
||||
qtutils.check_overflow(val, ctype)
|
||||
qtutils.check_overflow(val, ctype)
|
||||
|
||||
def test_bad_values_fatal(self):
|
||||
@pytest.mark.parametrize('ctype, val',
|
||||
[(ctype, val) for (ctype, val, _) in
|
||||
overflow_test_cases.iter_bad_values()])
|
||||
def test_bad_values_fatal(self, ctype, val):
|
||||
"""Test values which are outside bounds with fatal=True."""
|
||||
for ctype, vals in self.BAD_VALUES.items():
|
||||
for (val, _) in vals:
|
||||
with self.subTest(ctype=ctype, val=val):
|
||||
with self.assertRaises(OverflowError):
|
||||
qtutils.check_overflow(val, ctype)
|
||||
with pytest.raises(OverflowError):
|
||||
qtutils.check_overflow(val, ctype)
|
||||
|
||||
def test_bad_values_nonfatal(self):
|
||||
@pytest.mark.parametrize('ctype, val, repl',
|
||||
overflow_test_cases.iter_bad_values())
|
||||
def test_bad_values_nonfatal(self, ctype, val, repl):
|
||||
"""Test values which are outside bounds with fatal=False."""
|
||||
for ctype, vals in self.BAD_VALUES.items():
|
||||
for (val, replacement) in vals:
|
||||
with self.subTest(ctype=ctype, val=val):
|
||||
newval = qtutils.check_overflow(val, ctype, fatal=False)
|
||||
self.assertEqual(newval, replacement)
|
||||
newval = qtutils.check_overflow(val, ctype, fatal=False)
|
||||
assert newval == repl
|
||||
|
||||
|
||||
def argparser_exit(status=0, message=None): # pylint: disable=unused-argument
|
||||
"""Function to monkey-patch .exit() of the argparser so it doesn't exit."""
|
||||
raise Exception
|
||||
|
||||
|
||||
class GetQtArgsTests(unittest.TestCase):
|
||||
class TestGetQtArgs:
|
||||
|
||||
"""Tests for get_args."""
|
||||
|
||||
def setUp(self):
|
||||
self.parser = qutebrowser.get_argparser()
|
||||
self.parser.exit = argparser_exit
|
||||
@pytest.fixture
|
||||
def parser(self, mocker):
|
||||
"""Fixture to provide an argparser.
|
||||
|
||||
def test_no_qt_args(self):
|
||||
Monkey-patches .exit() of the argparser so it doesn't exit on errors.
|
||||
"""
|
||||
parser = qutebrowser.get_argparser()
|
||||
mocker.patch.object(parser, 'exit', side_effect=Exception)
|
||||
return parser
|
||||
|
||||
def test_no_qt_args(self, parser):
|
||||
"""Test commandline with no Qt arguments given."""
|
||||
args = self.parser.parse_args(['--debug'])
|
||||
self.assertEqual(qtutils.get_args(args), [sys.argv[0]])
|
||||
args = parser.parse_args(['--debug'])
|
||||
assert qtutils.get_args(args) == [sys.argv[0]]
|
||||
|
||||
def test_qt_flag(self):
|
||||
def test_qt_flag(self, parser):
|
||||
"""Test commandline with a Qt flag."""
|
||||
args = self.parser.parse_args(['--debug', '--qt-reverse', '--nocolor'])
|
||||
self.assertEqual(qtutils.get_args(args), [sys.argv[0], '-reverse'])
|
||||
args = parser.parse_args(['--debug', '--qt-reverse', '--nocolor'])
|
||||
assert qtutils.get_args(args) == [sys.argv[0], '-reverse']
|
||||
|
||||
def test_qt_arg(self):
|
||||
def test_qt_arg(self, parser):
|
||||
"""Test commandline with a Qt argument."""
|
||||
args = self.parser.parse_args(['--qt-stylesheet', 'foobar'])
|
||||
self.assertEqual(qtutils.get_args(args), [sys.argv[0], '-stylesheet',
|
||||
'foobar'])
|
||||
args = parser.parse_args(['--qt-stylesheet', 'foobar'])
|
||||
assert qtutils.get_args(args) == [sys.argv[0], '-stylesheet', 'foobar']
|
||||
|
||||
def test_qt_both(self):
|
||||
def test_qt_both(self, parser):
|
||||
"""Test commandline with a Qt argument and flag."""
|
||||
args = self.parser.parse_args(['--qt-stylesheet', 'foobar',
|
||||
'--qt-reverse'])
|
||||
args = parser.parse_args(['--qt-stylesheet', 'foobar', '--qt-reverse'])
|
||||
qt_args = qtutils.get_args(args)
|
||||
self.assertEqual(qt_args[0], sys.argv[0])
|
||||
self.assertIn('-reverse', qt_args)
|
||||
self.assertIn('-stylesheet', qt_args)
|
||||
self.assertIn('foobar', qt_args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
assert qt_args[0] == sys.argv[0]
|
||||
assert '-reverse' in qt_args
|
||||
assert '-stylesheet' in qt_args
|
||||
assert 'foobar' in qt_args
|
||||
|
8
tox.ini
8
tox.ini
@ -26,7 +26,7 @@ deps =
|
||||
# on Ubuntu Trusty.
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m py.test {posargs}
|
||||
{envpython} -m py.test --strict {posargs}
|
||||
|
||||
[testenv:coverage]
|
||||
deps =
|
||||
@ -36,7 +36,7 @@ deps =
|
||||
cov-core==1.15.0
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||
{envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||
|
||||
[testenv:misc]
|
||||
commands =
|
||||
@ -82,7 +82,7 @@ commands =
|
||||
[testenv:pyroma]
|
||||
skip_install = true
|
||||
deps =
|
||||
pyroma==1.7
|
||||
pyroma==1.8.1
|
||||
docutils==0.12
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
@ -109,3 +109,5 @@ commands =
|
||||
|
||||
[pytest]
|
||||
norecursedirs = .tox .venv
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
|
Loading…
Reference in New Issue
Block a user