diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/conftest.py | 47 | ||||
-rw-r--r-- | tests/test_acargparse.py | 53 | ||||
-rw-r--r-- | tests/test_autocompletion.py | 256 | ||||
-rw-r--r-- | tests/test_completion.py | 225 |
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. """ |