summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2018-04-19 21:51:24 -0600
committerkotfu <kotfu@kotfu.net>2018-04-19 21:51:24 -0600
commit477666d0b3e097fb831729644b8861a983805981 (patch)
tree02217d3d2bb03499e511d8dd1774d161bb019a0f /tests
parentb7cfb130c7c914478936366b748b04234b031119 (diff)
parent58fdd089cc064e71502dc1f094fd906d30523886 (diff)
downloadcmd2-git-477666d0b3e097fb831729644b8861a983805981.tar.gz
Merge branch 'master' into ply
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py47
-rw-r--r--tests/test_acargparse.py53
-rw-r--r--tests/test_autocompletion.py256
-rw-r--r--tests/test_completion.py225
4 files changed, 532 insertions, 49 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
index 837e7504..ed76cba9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,9 +8,21 @@ Released under MIT license, see LICENSE file
import sys
from pytest import fixture
+from unittest import mock
import cmd2
+# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
+try:
+ import gnureadline as readline
+except ImportError:
+ # Try to import readline, but allow failure for convenience in Windows unit testing
+ # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
+ try:
+ # noinspection PyUnresolvedReferences
+ import readline
+ except ImportError:
+ pass
# Help text for base cmd2.Cmd application
BASE_HELP = """Documented commands (type help <topic>):
@@ -141,3 +153,38 @@ def base_app():
c = cmd2.Cmd()
c.stdout = StdOut()
return c
+
+
+def complete_tester(text, line, begidx, endidx, app):
+ """
+ This is a convenience function to test cmd2.complete() since
+ in a unit test environment there is no actual console readline
+ is monitoring. Therefore we use mock to provide readline data
+ to complete().
+
+ :param text: str - the string prefix we are attempting to match
+ :param line: str - the current input line with leading whitespace removed
+ :param begidx: int - the beginning index of the prefix text
+ :param endidx: int - the ending index of the prefix text
+ :param app: the cmd2 app that will run completions
+ :return: The first matched string or None if there are no matches
+ Matches are stored in app.completion_matches
+ These matches also have been sorted by complete()
+ """
+ def get_line():
+ return line
+
+ def get_begidx():
+ return begidx
+
+ def get_endidx():
+ return endidx
+
+ first_match = None
+ with mock.patch.object(readline, 'get_line_buffer', get_line):
+ with mock.patch.object(readline, 'get_begidx', get_begidx):
+ with mock.patch.object(readline, 'get_endidx', get_endidx):
+ # Run the readline tab-completion function with readline mocks in place
+ first_match = app.complete(text, 0)
+
+ return first_match
diff --git a/tests/test_acargparse.py b/tests/test_acargparse.py
new file mode 100644
index 00000000..be3e8b97
--- /dev/null
+++ b/tests/test_acargparse.py
@@ -0,0 +1,53 @@
+"""
+Unit/functional testing for argparse customizations in cmd2
+
+Copyright 2018 Eric Lin <anselor@gmail.com>
+Released under MIT license, see LICENSE file
+"""
+import pytest
+from cmd2.argparse_completer import ACArgumentParser
+
+
+def test_acarg_narg_empty_tuple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=())
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_acarg_narg_single_tuple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(1,))
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_acarg_narg_tuple_triple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(1, 2, 3))
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_acarg_narg_tuple_order():
+ with pytest.raises(ValueError) as excinfo:
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(2, 1))
+ assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
+
+
+def test_acarg_narg_tuple_negative():
+ with pytest.raises(ValueError) as excinfo:
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(-1, 1))
+ assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
+
+
+def test_acarg_narg_tuple_zero_base():
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('tuple', nargs=(0, 3))
+
+
+def test_acarg_narg_tuple_zero_to_one():
+ parser = ACArgumentParser(prog='test')
+ parser.add_argument('tuple', nargs=(0, 1))
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
new file mode 100644
index 00000000..e68bc104
--- /dev/null
+++ b/tests/test_autocompletion.py
@@ -0,0 +1,256 @@
+"""
+Unit/functional testing for argparse completer in cmd2
+
+Copyright 2018 Eric Lin <anselor@gmail.com>
+Released under MIT license, see LICENSE file
+"""
+import pytest
+from .conftest import run_cmd, normalize, StdOut, complete_tester
+
+from examples.tab_autocompletion import TabCompleteExample
+
+@pytest.fixture
+def cmd2_app():
+ c = TabCompleteExample()
+ c.stdout = StdOut()
+
+ return c
+
+
+SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]
+
+Suggest command demonstrates argparse customizations See hybrid_suggest and
+orig_suggest to compare the help output.
+
+required arguments:
+ -t, --type {movie, show}
+
+optional arguments:
+ -h, --help show this help message and exit
+ -d, --duration DURATION{1..2}
+ Duration constraint in minutes.
+ single value - maximum duration
+ [a, b] - duration range'''
+
+MEDIA_MOVIES_ADD_HELP = '''Usage: media movies add title {G, PG, PG-13, R, NC-17} [actor [...]]
+ -d DIRECTOR{1..2}
+ [-h]
+
+positional arguments:
+ title Movie Title
+ {G, PG, PG-13, R, NC-17}
+ Movie Rating
+ actor Actors
+
+required arguments:
+ -d, --director DIRECTOR{1..2}
+ Director
+
+optional arguments:
+ -h, --help show this help message and exit'''
+
+def test_help_required_group(cmd2_app, capsys):
+ run_cmd(cmd2_app, 'suggest -h')
+ out, err = capsys.readouterr()
+ out1 = normalize(str(out))
+
+ out2 = run_cmd(cmd2_app, 'help suggest')
+
+ assert out1 == out2
+ assert out1[0].startswith('Usage: suggest')
+ assert out1[1] == ''
+ assert out1[2].startswith('Suggest command demonstrates argparse customizations ')
+ assert out1 == normalize(SUGGEST_HELP)
+
+
+def test_help_required_group_long(cmd2_app, capsys):
+ run_cmd(cmd2_app, 'media movies add -h')
+ out, err = capsys.readouterr()
+ out1 = normalize(str(out))
+
+ out2 = run_cmd(cmd2_app, 'help media movies add')
+
+ assert out1 == out2
+ assert out1[0].startswith('Usage: media movies add')
+ assert out1 == normalize(MEDIA_MOVIES_ADD_HELP)
+
+
+def test_autocomp_flags(cmd2_app):
+ text = '-'
+ line = 'suggest {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['--duration', '--help', '--type', '-d', '-h', '-t']
+
+def test_autcomp_hint(cmd2_app, capsys):
+ text = ''
+ line = 'suggest -d {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ out, err = capsys.readouterr()
+
+ assert out == '''
+Hint:
+ -d, --duration DURATION Duration constraint in minutes.
+ single value - maximum duration
+ [a, b] - duration range
+
+'''
+
+def test_autcomp_flag_comp(cmd2_app, capsys):
+ text = '--d'
+ line = 'suggest {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['--duration ']
+
+
+def test_autocomp_flags_choices(cmd2_app):
+ text = ''
+ line = 'suggest -t {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['movie', 'show']
+
+
+def test_autcomp_hint_in_narg_range(cmd2_app, capsys):
+ text = ''
+ line = 'suggest -d 2 {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ out, err = capsys.readouterr()
+
+ assert out == '''
+Hint:
+ -d, --duration DURATION Duration constraint in minutes.
+ single value - maximum duration
+ [a, b] - duration range
+
+'''
+
+def test_autocomp_flags_narg_max(cmd2_app):
+ text = ''
+ line = 'suggest d 2 3 {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is None
+
+
+def test_autcomp_narg_beyond_max(cmd2_app, capsys):
+ run_cmd(cmd2_app, 'suggest -t movie -d 3 4 5')
+ out, err = capsys.readouterr()
+
+ assert 'Error: unrecognized arguments: 5' in err
+
+
+def test_autocomp_subcmd_nested(cmd2_app):
+ text = ''
+ line = 'media movies {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['add', 'delete', 'list']
+
+
+def test_autocomp_subcmd_flag_choices_append(cmd2_app):
+ text = ''
+ line = 'media movies list -r {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['G', 'NC-17', 'PG', 'PG-13', 'R']
+
+def test_autocomp_subcmd_flag_choices_append_exclude(cmd2_app):
+ text = ''
+ line = 'media movies list -r PG PG-13 {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['G', 'NC-17', 'R']
+
+
+def test_autocomp_subcmd_flag_comp_func(cmd2_app):
+ text = 'A'
+ line = 'media movies list -a "{}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['Adam Driver', 'Alec Guinness', 'Andy Serkis', 'Anthony Daniels']
+
+
+def test_autocomp_subcmd_flag_comp_list(cmd2_app):
+ text = 'G'
+ line = 'media movies list -d {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and first_match == '"Gareth Edwards'
+
+
+def test_autcomp_pos_consumed(cmd2_app):
+ text = ''
+ line = 'library movie add SW_EP01 {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is None
+
+
+def test_autcomp_pos_after_flag(cmd2_app):
+ text = 'Joh'
+ line = 'media movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['John Boyega" ']
+
+
+def test_autcomp_custom_func_list_arg(cmd2_app):
+ text = 'SW_'
+ line = 'library show add {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['SW_CW', 'SW_REB', 'SW_TCW']
+
+
+def test_autcomp_custom_func_list_and_dict_arg(cmd2_app):
+ text = ''
+ line = 'library show add SW_REB {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is not None and \
+ cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 5e76aee6..a01d1166 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -13,21 +13,8 @@ import os
import sys
import cmd2
-from unittest import mock
import pytest
-
-# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
-try:
- import gnureadline as readline
-except ImportError:
- # Try to import readline, but allow failure for convenience in Windows unit testing
- # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
- try:
- # noinspection PyUnresolvedReferences
- import readline
- except ImportError:
- pass
-
+from .conftest import complete_tester
# List of strings used with completion functions
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
@@ -87,41 +74,6 @@ def cmd2_app():
return c
-def complete_tester(text, line, begidx, endidx, app):
- """
- This is a convenience function to test cmd2.complete() since
- in a unit test environment there is no actual console readline
- is monitoring. Therefore we use mock to provide readline data
- to complete().
-
- :param text: str - the string prefix we are attempting to match
- :param line: str - the current input line with leading whitespace removed
- :param begidx: int - the beginning index of the prefix text
- :param endidx: int - the ending index of the prefix text
- :param app: the cmd2 app that will run completions
- :return: The first matched string or None if there are no matches
- Matches are stored in app.completion_matches
- These matches also have been sorted by complete()
- """
- def get_line():
- return line
-
- def get_begidx():
- return begidx
-
- def get_endidx():
- return endidx
-
- first_match = None
- with mock.patch.object(readline, 'get_line_buffer', get_line):
- with mock.patch.object(readline, 'get_begidx', get_begidx):
- with mock.patch.object(readline, 'get_endidx', get_endidx):
- # Run the readline tab-completion function with readline mocks in place
- first_match = app.complete(text, 0)
-
- return first_match
-
-
def test_cmd2_command_completion_single(cmd2_app):
text = 'he'
line = text
@@ -911,6 +863,7 @@ def test_subcommand_tab_completion(sc_app):
# It is at end of line, so extra space is present
assert first_match is not None and sc_app.completion_matches == ['Football ']
+
def test_subcommand_tab_completion_with_no_completer(sc_app):
# This tests what happens when a subcommand has no completer
# In this case, the foo subcommand has no completer defined
@@ -922,6 +875,7 @@ def test_subcommand_tab_completion_with_no_completer(sc_app):
first_match = complete_tester(text, line, begidx, endidx, sc_app)
assert first_match is None
+
def test_subcommand_tab_completion_space_in_text(sc_app):
text = 'B'
line = 'base sport "Space {}'.format(text)
@@ -934,6 +888,179 @@ def test_subcommand_tab_completion_space_in_text(sc_app):
sc_app.completion_matches == ['Ball" '] and \
sc_app.display_matches == ['Space Ball']
+####################################################
+
+
+class SubcommandsWithUnknownExample(cmd2.Cmd):
+ """
+ Example cmd2 application where we a base command which has a couple subcommands
+ and the "sport" subcommand has tab completion enabled.
+ """
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ # subcommand functions for the base command
+ def base_foo(self, args):
+ """foo subcommand of base command"""
+ self.poutput(args.x * args.y)
+
+ def base_bar(self, args):
+ """bar subcommand of base command"""
+ self.poutput('((%s))' % args.z)
+
+ def base_sport(self, args):
+ """sport subcommand of base command"""
+ self.poutput('Sport is {}'.format(args.sport))
+
+ # noinspection PyUnusedLocal
+ def complete_base_sport(self, text, line, begidx, endidx):
+ """ Adds tab completion to base sport subcommand """
+ index_dict = {1: sport_item_strs}
+ return self.index_based_complete(text, line, begidx, endidx, index_dict)
+
+ # create the top-level parser for the base command
+ base_parser = argparse.ArgumentParser(prog='base')
+ base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
+
+ # create the parser for the "foo" subcommand
+ parser_foo = base_subparsers.add_parser('foo', help='foo help')
+ parser_foo.add_argument('-x', type=int, default=1, help='integer')
+ parser_foo.add_argument('y', type=float, help='float')
+ parser_foo.set_defaults(func=base_foo)
+
+ # create the parser for the "bar" subcommand
+ parser_bar = base_subparsers.add_parser('bar', help='bar help')
+ parser_bar.add_argument('z', help='string')
+ parser_bar.set_defaults(func=base_bar)
+
+ # create the parser for the "sport" subcommand
+ parser_sport = base_subparsers.add_parser('sport', help='sport help')
+ parser_sport.add_argument('sport', help='Enter name of a sport')
+
+ # Set both a function and tab completer for the "sport" subcommand
+ parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
+
+ @cmd2.with_argparser_and_unknown_args(base_parser)
+ def do_base(self, args):
+ """Base command help"""
+ func = getattr(args, 'func', None)
+ if func is not None:
+ # Call whatever subcommand function was selected
+ func(self, args)
+ else:
+ # No subcommand was provided, so call help
+ self.do_help('base')
+
+ # Enable tab completion of base to make sure the subcommands' completers get called.
+ complete_base = cmd2.Cmd.cmd_with_subs_completer
+
+
+@pytest.fixture
+def scu_app():
+ """Declare test fixture for with_argparser_and_unknown_args"""
+ app = SubcommandsWithUnknownExample()
+ return app
+
+
+def test_cmd2_subcmd_with_unknown_completion_single_end(scu_app):
+ text = 'f'
+ line = 'base {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+
+ # It is at end of line, so extra space is present
+ assert first_match is not None and scu_app.completion_matches == ['foo ']
+
+
+def test_cmd2_subcmd_with_unknown_completion_multiple(scu_app):
+ text = ''
+ line = 'base {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+ assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']
+
+
+def test_cmd2_subcmd_with_unknown_completion_nomatch(scu_app):
+ text = 'z'
+ line = 'base {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+ assert first_match is None
+
+
+def test_cmd2_help_subcommand_completion_single(scu_app):
+ text = 'base'
+ line = 'help {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert scu_app.complete_help(text, line, begidx, endidx) == ['base']
+
+
+def test_cmd2_help_subcommand_completion_multiple(scu_app):
+ text = ''
+ line = 'help base {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ matches = sorted(scu_app.complete_help(text, line, begidx, endidx))
+ assert matches == ['bar', 'foo', 'sport']
+
+
+def test_cmd2_help_subcommand_completion_nomatch(scu_app):
+ text = 'z'
+ line = 'help base {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+ assert scu_app.complete_help(text, line, begidx, endidx) == []
+
+
+def test_subcommand_tab_completion(scu_app):
+ # This makes sure the correct completer for the sport subcommand is called
+ text = 'Foot'
+ line = 'base sport {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+
+ # It is at end of line, so extra space is present
+ assert first_match is not None and scu_app.completion_matches == ['Football ']
+
+
+def test_subcommand_tab_completion_with_no_completer(scu_app):
+ # This tests what happens when a subcommand has no completer
+ # In this case, the foo subcommand has no completer defined
+ text = 'Foot'
+ line = 'base foo {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+ assert first_match is None
+
+
+def test_subcommand_tab_completion_space_in_text(scu_app):
+ text = 'B'
+ line = 'base sport "Space {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, scu_app)
+
+ assert first_match is not None and \
+ scu_app.completion_matches == ['Ball" '] and \
+ scu_app.display_matches == ['Space Ball']
+
+####################################################
+
+
class SecondLevel(cmd2.Cmd):
"""To be used as a second level command class. """