diff options
author | Florian Bruhin <me@the-compiler.org> | 2018-11-29 14:09:06 +0100 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2018-11-29 14:18:11 +0100 |
commit | b7de287e7bf053072eab47551e593d948fa8ced7 (patch) | |
tree | cdfc159315deaaaa6d229625c126e76c739c1d7d /tests/unit/api | |
parent | f9858733c1ba7d4500ad8b20bef281341dc12f12 (diff) | |
download | qutebrowser-b7de287e7bf053072eab47551e593d948fa8ced7.tar.gz qutebrowser-b7de287e7bf053072eab47551e593d948fa8ced7.zip |
Move CommandError to api.cmdutils
Diffstat (limited to 'tests/unit/api')
-rw-r--r-- | tests/unit/api/test_cmdutils.py | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py new file mode 100644 index 000000000..f2318ab46 --- /dev/null +++ b/tests/unit/api/test_cmdutils.py @@ -0,0 +1,428 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2018 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/>. + +# pylint: disable=unused-variable + +"""Tests for qutebrowser.api.cmdutils.""" + +import sys +import logging +import types +import typing +import enum + +import pytest + +from qutebrowser.misc import objects +from qutebrowser.commands import cmdexc, argparser, command +from qutebrowser.api import cmdutils +from qutebrowser.utils import usertypes + + +@pytest.fixture(autouse=True) +def clear_globals(monkeypatch): + monkeypatch.setattr(objects, 'commands', {}) + + +def _get_cmd(*args, **kwargs): + """Get a command object created via @cmdutils.register. + + Args: + Passed to @cmdutils.register decorator + """ + @cmdutils.register(*args, **kwargs) + def fun(): + """Blah.""" + return objects.commands['fun'] + + +class TestCheckOverflow: + + def test_good(self): + cmdutils.check_overflow(1, 'int') + + def test_bad(self): + int32_max = 2 ** 31 - 1 + + with pytest.raises(cmdutils.CommandError, match="Numeric argument is " + "too large for internal int representation."): + cmdutils.check_overflow(int32_max + 1, 'int') + + +class TestCheckExclusive: + + @pytest.mark.parametrize('flags', [[], [False, True], [False, False]]) + def test_good(self, flags): + cmdutils.check_exclusive(flags, []) + + def test_bad(self): + with pytest.raises(cmdutils.CommandError, + match="Only one of -x/-y/-z can be given!"): + cmdutils.check_exclusive([True, True], 'xyz') + + +class TestRegister: + + def test_simple(self): + @cmdutils.register() + def fun(): + """Blah.""" + + cmd = objects.commands['fun'] + assert cmd.handler is fun + assert cmd.name == 'fun' + assert len(objects.commands) == 1 + + def test_underlines(self): + """Make sure the function name is normalized correctly (_ -> -).""" + @cmdutils.register() + def eggs_bacon(): + """Blah.""" + + assert objects.commands['eggs-bacon'].name == 'eggs-bacon' + assert 'eggs_bacon' not in objects.commands + + def test_lowercasing(self): + """Make sure the function name is normalized correctly (uppercase).""" + @cmdutils.register() + def Test(): # noqa: N801,N806 pylint: disable=invalid-name + """Blah.""" + + assert objects.commands['test'].name == 'test' + assert 'Test' not in objects.commands + + def test_explicit_name(self): + """Test register with explicit name.""" + @cmdutils.register(name='foobar') + def fun(): + """Blah.""" + + assert objects.commands['foobar'].name == 'foobar' + assert 'fun' not in objects.commands + assert len(objects.commands) == 1 + + def test_multiple_registrations(self): + """Make sure registering the same name twice raises ValueError.""" + @cmdutils.register(name='foobar') + def fun(): + """Blah.""" + + with pytest.raises(ValueError): + @cmdutils.register(name='foobar') + def fun2(): + """Blah.""" + + def test_instance(self): + """Make sure the instance gets passed to Command.""" + @cmdutils.register(instance='foobar') + def fun(self): + """Blah.""" + assert objects.commands['fun']._instance == 'foobar' + + def test_star_args(self): + """Check handling of *args.""" + @cmdutils.register() + def fun(*args): + """Blah.""" + with pytest.raises(argparser.ArgumentParserError): + objects.commands['fun'].parser.parse_args([]) + + def test_star_args_optional(self): + """Check handling of *args withstar_args_optional.""" + @cmdutils.register(star_args_optional=True) + def fun(*args): + """Blah.""" + assert not args + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args([]) + args, kwargs = cmd._get_call_args(win_id=0) + fun(*args, **kwargs) + + @pytest.mark.parametrize('inp, expected', [ + (['--arg'], True), (['-a'], True), ([], False)]) + def test_flag(self, inp, expected): + @cmdutils.register() + def fun(arg=False): + """Blah.""" + assert arg == expected + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args(inp) + assert cmd.namespace.arg == expected + + def test_flag_argument(self): + @cmdutils.register() + @cmdutils.argument('arg', flag='b') + def fun(arg=False): + """Blah.""" + assert arg + cmd = objects.commands['fun'] + + with pytest.raises(argparser.ArgumentParserError): + cmd.parser.parse_args(['-a']) + + cmd.namespace = cmd.parser.parse_args(['-b']) + assert cmd.namespace.arg + args, kwargs = cmd._get_call_args(win_id=0) + fun(*args, **kwargs) + + def test_partial_arg(self): + """Test with only some arguments decorated with @cmdutils.argument.""" + @cmdutils.register() + @cmdutils.argument('arg1', flag='b') + def fun(arg1=False, arg2=False): + """Blah.""" + + def test_win_id(self): + @cmdutils.register() + @cmdutils.argument('win_id', win_id=True) + def fun(win_id): + """Blah.""" + assert objects.commands['fun']._get_call_args(42) == ([42], {}) + + def test_count(self): + @cmdutils.register() + @cmdutils.argument('count', count=True) + def fun(count=0): + """Blah.""" + assert objects.commands['fun']._get_call_args(42) == ([0], {}) + + def test_count_without_default(self): + with pytest.raises(TypeError, match="fun: handler has count parameter " + "without default!"): + @cmdutils.register() + @cmdutils.argument('count', count=True) + def fun(count): + """Blah.""" + + @pytest.mark.parametrize('hide', [True, False]) + def test_pos_args(self, hide): + @cmdutils.register() + @cmdutils.argument('arg', hide=hide) + def fun(arg): + """Blah.""" + + pos_args = objects.commands['fun'].pos_args + if hide: + assert pos_args == [] + else: + assert pos_args == [('arg', 'arg')] + + Enum = enum.Enum('Test', ['x', 'y']) + + @pytest.mark.parametrize('typ, inp, choices, expected', [ + (int, '42', None, 42), + (int, 'x', None, cmdexc.ArgumentTypeError), + (str, 'foo', None, 'foo'), + + (typing.Union[str, int], 'foo', None, 'foo'), + (typing.Union[str, int], '42', None, 42), + + # Choices + (str, 'foo', ['foo'], 'foo'), + (str, 'bar', ['foo'], cmdexc.ArgumentTypeError), + + # Choices with Union: only checked when it's a str + (typing.Union[str, int], 'foo', ['foo'], 'foo'), + (typing.Union[str, int], 'bar', ['foo'], cmdexc.ArgumentTypeError), + (typing.Union[str, int], '42', ['foo'], 42), + + (Enum, 'x', None, Enum.x), + (Enum, 'z', None, cmdexc.ArgumentTypeError), + ]) + def test_typed_args(self, typ, inp, choices, expected): + @cmdutils.register() + @cmdutils.argument('arg', choices=choices) + def fun(arg: typ): + """Blah.""" + assert arg == expected + + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args([inp]) + + if expected is cmdexc.ArgumentTypeError: + with pytest.raises(cmdexc.ArgumentTypeError): + cmd._get_call_args(win_id=0) + else: + args, kwargs = cmd._get_call_args(win_id=0) + assert args == [expected] + assert kwargs == {} + fun(*args, **kwargs) + + def test_choices_no_annotation(self): + # https://github.com/qutebrowser/qutebrowser/issues/1871 + @cmdutils.register() + @cmdutils.argument('arg', choices=['foo', 'bar']) + def fun(arg): + """Blah.""" + + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args(['fish']) + + with pytest.raises(cmdexc.ArgumentTypeError): + cmd._get_call_args(win_id=0) + + def test_choices_no_annotation_kwonly(self): + # https://github.com/qutebrowser/qutebrowser/issues/1871 + @cmdutils.register() + @cmdutils.argument('arg', choices=['foo', 'bar']) + def fun(*, arg='foo'): + """Blah.""" + + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args(['--arg=fish']) + + with pytest.raises(cmdexc.ArgumentTypeError): + cmd._get_call_args(win_id=0) + + def test_pos_arg_info(self): + @cmdutils.register() + @cmdutils.argument('foo', choices=('a', 'b')) + @cmdutils.argument('bar', choices=('x', 'y')) + @cmdutils.argument('opt') + def fun(foo, bar, opt=False): + """Blah.""" + + cmd = objects.commands['fun'] + assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b')) + assert cmd.get_pos_arg_info(1) == command.ArgInfo(choices=('x', 'y')) + with pytest.raises(IndexError): + cmd.get_pos_arg_info(2) + + def test_keyword_only_without_default(self): + # https://github.com/qutebrowser/qutebrowser/issues/1872 + def fun(*, target): + """Blah.""" + + with pytest.raises(TypeError, match="fun: handler has keyword only " + "argument 'target' without default!"): + fun = cmdutils.register()(fun) + + def test_typed_keyword_only_without_default(self): + # https://github.com/qutebrowser/qutebrowser/issues/1872 + def fun(*, target: int): + """Blah.""" + + with pytest.raises(TypeError, match="fun: handler has keyword only " + "argument 'target' without default!"): + fun = cmdutils.register()(fun) + + +class TestArgument: + + """Test the @cmdutils.argument decorator.""" + + def test_invalid_argument(self): + with pytest.raises(ValueError, match="fun has no argument foo!"): + @cmdutils.argument('foo') + def fun(bar): + """Blah.""" + + def test_storage(self): + @cmdutils.argument('foo', flag='x') + @cmdutils.argument('bar', flag='y') + def fun(foo, bar): + """Blah.""" + expected = { + 'foo': command.ArgInfo(flag='x'), + 'bar': command.ArgInfo(flag='y') + } + assert fun.qute_args == expected + + def test_wrong_order(self): + """When @cmdutils.argument is used above (after) @register, fail.""" + with pytest.raises(ValueError, match=r"@cmdutils.argument got called " + r"above \(after\) @cmdutils.register for fun!"): + @cmdutils.argument('bar', flag='y') + @cmdutils.register() + def fun(bar): + """Blah.""" + + def test_count_and_win_id_same_arg(self): + with pytest.raises(TypeError, + match="Argument marked as both count/win_id!"): + @cmdutils.argument('arg', count=True, win_id=True) + def fun(arg=0): + """Blah.""" + + def test_no_docstring(self, caplog): + with caplog.at_level(logging.WARNING): + @cmdutils.register() + def fun(): + # no docstring + pass + + assert len(caplog.records) == 1 + assert caplog.messages[0].endswith('test_cmdutils.py has no docstring') + + def test_no_docstring_with_optimize(self, monkeypatch): + """With -OO we'd get a warning on start, but no warning afterwards.""" + monkeypatch.setattr(sys, 'flags', types.SimpleNamespace(optimize=2)) + + @cmdutils.register() + def fun(): + # no docstring + pass + + +class TestRun: + + @pytest.fixture(autouse=True) + def patch_backend(self, mode_manager, monkeypatch): + monkeypatch.setattr(command.objects, 'backend', + usertypes.Backend.QtWebKit) + + @pytest.mark.parametrize('backend, used, ok', [ + (usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebEngine, True), + (usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebKit, False), + (usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine, False), + (usertypes.Backend.QtWebKit, usertypes.Backend.QtWebKit, True), + (None, usertypes.Backend.QtWebEngine, True), + (None, usertypes.Backend.QtWebKit, True), + ]) + def test_backend(self, monkeypatch, backend, used, ok): + monkeypatch.setattr(command.objects, 'backend', used) + cmd = _get_cmd(backend=backend) + if ok: + cmd.run(win_id=0) + else: + with pytest.raises(cmdexc.PrerequisitesError, + match=r'.* backend\.'): + cmd.run(win_id=0) + + def test_no_args(self): + cmd = _get_cmd() + cmd.run(win_id=0) + + def test_instance_unavailable_with_backend(self, monkeypatch): + """Test what happens when a backend doesn't have an objreg object. + + For example, QtWebEngine doesn't have 'hintmanager' registered. We make + sure the backend checking happens before resolving the instance, so we + display an error instead of crashing. + """ + @cmdutils.register(instance='doesnotexist', + backend=usertypes.Backend.QtWebEngine) + def fun(self): + """Blah.""" + + monkeypatch.setattr(command.objects, 'backend', + usertypes.Backend.QtWebKit) + cmd = objects.commands['fun'] + with pytest.raises(cmdexc.PrerequisitesError, match=r'.* backend\.'): + cmd.run(win_id=0) |