diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/test_acargparse.py | 66 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 648 | ||||
-rw-r--r-- | tests/test_argparse_custom.py | 145 | ||||
-rw-r--r-- | tests/test_autocompletion.py | 345 | ||||
-rw-r--r-- | tests/test_cmd2.py | 31 | ||||
-rw-r--r-- | tests/test_completion.py | 19 |
6 files changed, 823 insertions, 431 deletions
diff --git a/tests/test_acargparse.py b/tests/test_acargparse.py deleted file mode 100644 index 436158db..00000000 --- a/tests/test_acargparse.py +++ /dev/null @@ -1,66 +0,0 @@ -# flake8: noqa E302 -""" -Unit/functional testing for argparse customizations in cmd2 -""" -import pytest -from cmd2.argparse_completer import ACArgumentParser, is_potential_flag - - -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)) - - -def test_is_potential_flag(): - parser = ACArgumentParser() - - # Not valid flags - assert not is_potential_flag('', parser) - assert not is_potential_flag('non-flag', parser) - assert not is_potential_flag('-', parser) - assert not is_potential_flag('--has space', parser) - assert not is_potential_flag('-2', parser) - - # Valid flags - assert is_potential_flag('-flag', parser) - assert is_potential_flag('--flag', parser) diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py new file mode 100644 index 00000000..f1faa66a --- /dev/null +++ b/tests/test_argparse_completer.py @@ -0,0 +1,648 @@ +# coding=utf-8 +# flake8: noqa E302 +""" +Unit/functional testing for argparse completer in cmd2 +""" +import argparse +from typing import List + +import pytest + +import cmd2 +from cmd2 import with_argparser, Cmd2ArgParser, CompletionItem +from cmd2.utils import StdSim, basic_complete +from .conftest import run_cmd, complete_tester + +# Lists used in our tests +static_int_choices_list = [1, 2, 3, 4, 5] +static_choices_list = ['static', 'choices', 'stop', 'here'] +choices_from_function = ['choices', 'function', 'chatty', 'smith'] +choices_from_method = ['choices', 'method', 'most', 'improved'] + +set_value_choices = ['set', 'value', 'choices'] +one_or_more_choices = ['one', 'or', 'more', 'choices'] +optional_choices = ['a', 'few', 'optional', 'choices'] +range_choices = ['some', 'range', 'choices'] +remainder_choices = ['remainder', 'choices'] + +positional_choices = ['the', 'positional', 'choices'] + +completions_from_function = ['completions', 'function', 'fairly', 'complete'] +completions_from_method = ['completions', 'method', 'missed', 'spot'] + + +def choices_function() -> List[str]: + """Function that provides choices""" + return choices_from_function + + +def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[str]: + """Tab completion function""" + return basic_complete(text, line, begidx, endidx, completions_from_function) + + +# noinspection PyMethodMayBeStatic,PyUnusedLocal +class AutoCompleteTester(cmd2.Cmd): + """Cmd2 app that exercises AutoCompleter class""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + ############################################################################################################ + # Begin code related to help and command name completion + ############################################################################################################ + def _music_create(self, args: argparse.Namespace) -> None: + """Implements the 'music create' command""" + self.poutput('music create') + + def _music_create_jazz(self, args: argparse.Namespace) -> None: + """Implements the 'music create jazz' command""" + self.poutput('music create jazz') + + def _music_create_rock(self, args: argparse.Namespace) -> None: + """Implements the 'music create rock' command""" + self.poutput('music create rock') + + # Top level parser for music command + music_parser = Cmd2ArgParser(description='Manage music', prog='music') + + # Add sub-commands to music + music_subparsers = music_parser.add_subparsers() + + # music -> create + music_create_parser = music_subparsers.add_parser('create', help='Create music') + music_create_parser.set_defaults(func=_music_create) + + # Add sub-commands to music -> create + music_create_subparsers = music_create_parser.add_subparsers() + + # music -> create -> jazz + music_create_jazz_parser = music_create_subparsers.add_parser('jazz', help='Create jazz') + music_create_jazz_parser.set_defaults(func=_music_create_jazz) + + # music -> create -> rock + music_create_rock_parser = music_create_subparsers.add_parser('rock', help='Create rocks') + music_create_rock_parser.set_defaults(func=_music_create_rock) + + @with_argparser(music_parser) + def do_music(self, args: argparse.Namespace) -> None: + """Music command""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever sub-command function was selected + func(self, args) + else: + # No sub-command was provided, so call help + # noinspection PyTypeChecker + self.do_help('music') + + ############################################################################################################ + # Begin code related to flag completion + ############################################################################################################ + + # Uses default flag prefix value (-) + flag_parser = Cmd2ArgParser() + flag_parser.add_argument('-n', '--normal_flag', help='A normal flag', action='store_true') + flag_parser.add_argument('-a', '--append_flag', help='Append flag', action='append') + flag_parser.add_argument('-o', '--append_const_flag', help='Append const flag', action='append_const', const=True) + flag_parser.add_argument('-c', '--count_flag', help='Count flag', action='count') + flag_parser.add_argument('-s', '--suppressed_flag', help=argparse.SUPPRESS, action='store_true') + flag_parser.add_argument('-r', '--remainder_flag', nargs=argparse.REMAINDER, help='a remainder flag') + + @with_argparser(flag_parser) + def do_flag(self, args: argparse.Namespace) -> None: + pass + + # Uses non-default flag prefix value (+) + plus_flag_parser = Cmd2ArgParser(prefix_chars='+') + plus_flag_parser.add_argument('+n', '++normal_flag', help='A normal flag', action='store_true') + + @with_argparser(plus_flag_parser) + def do_plus_flag(self, args: argparse.Namespace) -> None: + pass + + ############################################################################################################ + # Begin code related to testing choices, choices_function, and choices_method parameters + ############################################################################################################ + def choices_method(self) -> List[str]: + """Method that provides choices""" + return choices_from_method + + def completion_item_method(self) -> List[CompletionItem]: + """Choices method that returns CompletionItems""" + items = [] + for i in range(0, 10): + main_str = 'main_str{}'.format(i) + items.append(CompletionItem(main_str, desc='blah blah')) + return items + + choices_parser = Cmd2ArgParser() + + # Flag args for choices command. Include string and non-string arg types. + choices_parser.add_argument("-l", "--list", help="a flag populated with a choices list", + choices=static_choices_list) + choices_parser.add_argument("-f", "--function", help="a flag populated with a choices function", + choices_function=choices_function) + choices_parser.add_argument("-m", "--method", help="a flag populated with a choices method", + choices_method=choices_method) + choices_parser.add_argument('-n', "--no_header", help='this arg has a no descriptive header', + choices_method=completion_item_method) + choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', + choices=static_int_choices_list) + + # Positional args for choices command + choices_parser.add_argument("list_pos", help="a positional populated with a choices list", + choices=static_choices_list) + choices_parser.add_argument("function_pos", help="a positional populated with a choices function", + choices_function=choices_function) + choices_parser.add_argument("method_pos", help="a positional populated with a choices method", + choices_method=choices_method) + + @with_argparser(choices_parser) + def do_choices(self, args: argparse.Namespace) -> None: + pass + + ############################################################################################################ + # Begin code related to testing completer_function and completer_method parameters + ############################################################################################################ + def completer_method(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + """Tab completion method""" + return basic_complete(text, line, begidx, endidx, completions_from_method) + + completer_parser = Cmd2ArgParser() + + # Flag args for completer command + completer_parser.add_argument("-f", "--function", help="a flag using a completer function", + completer_function=completer_function) + completer_parser.add_argument("-m", "--method", help="a flag using a completer method", + completer_method=completer_method) + + # Positional args for completer command + completer_parser.add_argument("function_pos", help="a positional using a completer function", + completer_function=completer_function) + completer_parser.add_argument("method_pos", help="a positional using a completer method", + completer_method=completer_method) + + @with_argparser(completer_parser) + def do_completer(self, args: argparse.Namespace) -> None: + pass + + ############################################################################################################ + # Begin code related to nargs + ############################################################################################################ + nargs_parser = Cmd2ArgParser() + + # Flag args for nargs command + nargs_parser.add_argument("--set_value", help="a flag with a set value for nargs", nargs=2, + choices=set_value_choices) + nargs_parser.add_argument("--one_or_more", help="a flag wanting one or more args", nargs=argparse.ONE_OR_MORE, + choices=one_or_more_choices) + nargs_parser.add_argument("--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL, + choices=optional_choices) + nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2), + choices=range_choices) + nargs_parser.add_argument("--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER, + choices=remainder_choices) + + nargs_parser.add_argument("normal_pos", help="a remainder positional", nargs=2, + choices=positional_choices) + nargs_parser.add_argument("remainder_pos", help="a remainder positional", nargs=argparse.REMAINDER, + choices=remainder_choices) + + @with_argparser(nargs_parser) + def do_nargs(self, args: argparse.Namespace) -> None: + pass + + ############################################################################################################ + # Begin code related to testing tab hints + ############################################################################################################ + hint_parser = Cmd2ArgParser() + hint_parser.add_argument('-f', '--flag', help='a flag arg') + hint_parser.add_argument('-s', '--suppressed_help', help=argparse.SUPPRESS) + hint_parser.add_argument('-t', '--suppressed_hint', help='a flag arg', suppress_tab_hint=True) + + hint_parser.add_argument('hint_pos', help='here is a hint\nwith new lines') + hint_parser.add_argument('no_help_pos') + + @with_argparser(hint_parser) + def do_hint(self, args: argparse.Namespace) -> None: + pass + + +@pytest.fixture +def ac_app(): + app = AutoCompleteTester() + app.stdout = StdSim(app.stdout) + return app + + +@pytest.mark.parametrize('command', [ + 'music', + 'music create', + 'music create rock', + 'music create jazz' +]) +def test_help(ac_app, command): + out1, err1 = run_cmd(ac_app, '{} -h'.format(command)) + out2, err2 = run_cmd(ac_app, 'help {}'.format(command)) + assert out1 == out2 + + +@pytest.mark.parametrize('command, text, completions', [ + ('', 'mu', ['music ']), + ('music', 'cre', ['create ']), + ('music create', '', ['jazz', 'rock']) +]) +def test_complete_help(ac_app, command, text, completions): + line = 'help {} {}'.format(command, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('command_and_args, text, completions', [ + # Complete all flags (suppressed will not show) + ('flag', '-', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', + '--remainder_flag', '-a', '-c', '-h', '-n', '-o', '-r']), + ('flag', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', + '--normal_flag', '--remainder_flag']), + + # Complete individual flag + ('flag', '-n', ['-n ']), + ('flag', '--n', ['--normal_flag ']), + + # No flags should complete until current flag has its args + ('flag --append_flag', '-', []), + + # Complete REMAINDER flag name + ('flag', '-r', ['-r ']), + ('flag', '--r', ['--remainder_flag ']), + + # No flags after a REMAINDER should complete + ('flag -r value', '-', []), + ('flag --remainder_flag value', '--', []), + + # Suppressed flag should not complete + ('flag', '-s', []), + ('flag', '--s', []), + + # A used flag should not show in completions + ('flag -n', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag']), + + # Flags with actions set to append, append_const, and count will always show even if they've been used + ('flag --append_const_flag -c --append_flag value', '--', ['--append_const_flag', '--append_flag', '--count_flag', + '--help', '--normal_flag', '--remainder_flag']), + + # Non-default flag prefix character (+) + ('plus_flag', '+', ['++help', '++normal_flag', '+h', '+n']), + ('plus_flag', '++', ['++help', '++normal_flag']), + + # Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags + ('flag --', '--', []), + ('flag --help --', '--', []), + ('plus_flag --', '++', []), + ('plus_flag ++help --', '++', []) +]) +def test_autcomp_flag_completion(ac_app, command_and_args, text, completions): + line = '{} {}'.format(command_and_args, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('flag, text, completions', [ + ('-l', '', static_choices_list), + ('--list', 's', ['static', 'stop']), + ('-f', '', choices_from_function), + ('--function', 'ch', ['choices', 'chatty']), + ('-m', '', choices_from_method), + ('--method', 'm', ['method', 'most']), + ('-i', '', [str(i) for i in static_int_choices_list]), + ('--int', '1', ['1 ']) +]) +def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): + line = 'choices {} {}'.format(flag, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('pos, text, completions', [ + (1, '', static_choices_list), + (1, 's', ['static', 'stop']), + (2, '', choices_from_function), + (2, 'ch', ['choices', 'chatty']), + (3, '', choices_from_method), + (3, 'm', ['method', 'most']) +]) +def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): + # Generate line were preceding positionals are already filled + line = 'choices {} {}'.format('foo ' * (pos - 1), text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('flag, text, completions', [ + ('-f', '', completions_from_function), + ('--function', 'f', ['function', 'fairly']), + ('-m', '', completions_from_method), + ('--method', 'm', ['method', 'missed']) +]) +def test_autocomp_flag_completers(ac_app, flag, text, completions): + line = 'completer {} {}'.format(flag, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('pos, text, completions', [ + (1, '', completions_from_function), + (1, 'c', ['completions', 'complete']), + (2, '', completions_from_method), + (2, 'm', ['method', 'missed']) +]) +def test_autocomp_positional_completers(ac_app, pos, text, completions): + # Generate line were preceding positionals are already filled + line = 'completer {} {}'.format('foo ' * (pos - 1), text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +@pytest.mark.parametrize('num_aliases, show_description', [ + # The number of completion results determines if the description field of CompletionItems gets displayed + # in the tab completions. The count must be greater than 1 and less than ac_app.max_completion_items, + # which defaults to 50. + (1, False), + (5, True), + (100, False) +]) +def test_completion_items(ac_app, num_aliases, show_description): + # Create aliases + for i in range(0, num_aliases): + run_cmd(ac_app, 'alias create fake{} help'.format(i)) + + assert len(ac_app.aliases) == num_aliases + + text = 'fake' + line = 'alias list {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + assert first_match is not None + assert len(ac_app.completion_matches) == num_aliases + assert len(ac_app.display_matches) == num_aliases + + # If show_description is True, the alias's value will be in the display text + assert ('help' in ac_app.display_matches[0]) == show_description + + +@pytest.mark.parametrize('args, completions', [ + # Flag with nargs = 2 + ('--set_value', set_value_choices), + ('--set_value set', ['value', 'choices']), + + # Both args are filled. At positional arg now. + ('--set_value set value', positional_choices), + + # Another flag can't start until all expected args are filled out + ('--set_value --one_or_more', set_value_choices), + + # Using the flag again will reset the choices available + ('--set_value set value --set_value', set_value_choices), + + # Flag with nargs = ONE_OR_MORE + ('--one_or_more', one_or_more_choices), + ('--one_or_more one', ['or', 'more', 'choices']), + + # Flag with nargs = OPTIONAL + ('--optional', optional_choices), + + # Only one arg allowed for an OPTIONAL. At positional now. + ('--optional optional', positional_choices), + + # Flag with nargs range (1, 2) + ('--range', range_choices), + ('--range some', ['range', 'choices']), + + # Already used 2 args so at positional + ('--range some range', positional_choices), + + # Flag with nargs = REMAINDER + ('--remainder', remainder_choices), + ('--remainder remainder ', ['choices ']), + + # No more flags can appear after a REMAINDER flag) + ('--remainder choices --set_value', ['remainder ']), + + # Double dash ends the current flag (even if all expected args aren't entered) + ('--set_value --', positional_choices), + + # Double dash ends a REMAINDER flag + ('--remainder remainder --', positional_choices), + + # No more flags after a double dash + ('-- --one_or_more ', positional_choices), + + # Consume positional + ('', positional_choices), + ('positional', ['the', 'choices']), + + # Intermixed flag and positional + ('positional --set_value', set_value_choices), + ('positional --set_value set', ['value', 'choices']), + + # Intermixed flag and positional with flag finishing + ('positional --set_value set value', ['the', 'choices']), + ('positional --set_value set --', ['the', 'choices']), + + # REMAINDER positional + ('the positional', remainder_choices), + ('the positional remainder', ['choices ']), + + # REMAINDER positional. Flags don't work in REMAINDER + ('the positional --set_value', remainder_choices), +]) +def test_autcomp_nargs(ac_app, args, completions): + text = '' + line = 'nargs {} {}'.format(args, text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + if completions: + assert first_match is not None + else: + assert first_match is None + + assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + + +def test_completion_items_default_header(ac_app): + from cmd2.argparse_completer import DEFAULT_DESCRIPTIVE_HEADER + + text = '' + line = 'choices -n {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + # This positional argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADER + complete_tester(text, line, begidx, endidx, ac_app) + assert DEFAULT_DESCRIPTIVE_HEADER in ac_app.completion_header + + +def test_autocomp_hint_flag(ac_app, capsys): + text = '' + line = 'hint --flag {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + out, err = capsys.readouterr() + + assert first_match is None + assert out == ''' +Hint: + -f, --flag FLAG a flag arg + +''' + + +def test_autocomp_hint_suppressed_help(ac_app, capsys): + text = '' + line = 'hint --suppressed_help {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + out, err = capsys.readouterr() + + assert first_match is None + assert not out + + +def test_autocomp_hint_suppressed_hint(ac_app, capsys): + text = '' + line = 'hint --suppressed_hint {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + out, err = capsys.readouterr() + + assert first_match is None + assert not out + + +def test_autocomp_hint_pos(ac_app, capsys): + text = '' + line = 'hint {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + out, err = capsys.readouterr() + + assert first_match is None + assert out == ''' +Hint: + HINT_POS here is a hint + with new lines + +''' + + +def test_autocomp_hint_no_help(ac_app, capsys): + text = '' + line = 'hint foo {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, ac_app) + out, err = capsys.readouterr() + + assert first_match is None + assert not out == ''' +Hint: + NO_HELP_POS + +''' + +def test_is_potential_flag(): + from cmd2.argparse_completer import is_potential_flag + parser = Cmd2ArgParser() + + # Not potential flags + assert not is_potential_flag('', parser) + assert not is_potential_flag('non-flag', parser) + assert not is_potential_flag('--has space', parser) + assert not is_potential_flag('-2', parser) + + # Potential flags + assert is_potential_flag('-', parser) + assert is_potential_flag('--', parser) + assert is_potential_flag('-flag', parser) + assert is_potential_flag('--flag', parser) + + +def test_complete_command_no_tokens(ac_app): + from cmd2.argparse_completer import AutoCompleter + + parser = Cmd2ArgParser() + ac = AutoCompleter(parser, ac_app) + + completions = ac.complete_command(tokens=[], text='', line='', begidx=0, endidx=0) + assert not completions + + +def test_complete_command_help_no_tokens(ac_app): + from cmd2.argparse_completer import AutoCompleter + + parser = Cmd2ArgParser() + ac = AutoCompleter(parser, ac_app) + + completions = ac.complete_command_help(tokens=[], text='', line='', begidx=0, endidx=0) + assert not completions diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py new file mode 100644 index 00000000..35d97974 --- /dev/null +++ b/tests/test_argparse_custom.py @@ -0,0 +1,145 @@ +# flake8: noqa E302 +""" +Unit/functional testing for argparse customizations in cmd2 +""" +import argparse + +import pytest + +import cmd2 +from cmd2.argparse_custom import Cmd2ArgParser +from .conftest import run_cmd + + +class ApCustomTestApp(cmd2.Cmd): + """Test app for cmd2's argparse customization""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + range_parser = Cmd2ArgParser() + range_parser.add_argument('--arg1', nargs=(2, 3)) + range_parser.add_argument('--arg2', nargs=argparse.ZERO_OR_MORE) + range_parser.add_argument('--arg3', nargs=argparse.ONE_OR_MORE) + + @cmd2.with_argparser(range_parser) + def do_range(self, _): + pass + + +@pytest.fixture +def cust_app(): + return ApCustomTestApp() + + +def fake_func(): + pass + + +@pytest.mark.parametrize('args, is_valid', [ + ({'choices': []}, True), + ({'choices_function': fake_func}, True), + ({'choices_method': fake_func}, True), + ({'completer_function': fake_func}, True), + ({'completer_method': fake_func}, True), + ({'choices': [], 'choices_function': fake_func}, False), + ({'choices': [], 'choices_method': fake_func}, False), + ({'choices_method': fake_func, 'completer_function': fake_func}, False), + ({'choices_method': fake_func, 'completer_method': fake_func}, False), +]) +def test_apcustom_invalid_args(args, is_valid): + parser = Cmd2ArgParser(prog='test') + try: + parser.add_argument('name', **args) + assert is_valid + except ValueError as ex: + assert not is_valid + assert 'Only one of the following may be used' in str(ex) + + +def test_apcustom_usage(): + usage = "A custom usage statement" + parser = Cmd2ArgParser(usage=usage) + help = parser.format_help() + assert usage in help + + +def test_apcustom_nargs_help_format(cust_app): + out, err = run_cmd(cust_app, 'help range') + assert 'Usage: range [-h] [--arg1 ARG1{2..3}] [--arg2 [ARG2 [...]]]' in out[0] + assert ' [--arg3 ARG3 [...]]' in out[1] + + +def test_apcustom_nargs_not_enough(cust_app): + out, err = run_cmd(cust_app, 'range --arg1 one') + assert 'Error: argument --arg1: Expected between 2 and 3 arguments' in err[2] + + +def test_apcustom_narg_empty_tuple(): + with pytest.raises(ValueError) as excinfo: + parser = Cmd2ArgParser(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_apcustom_narg_single_tuple(): + with pytest.raises(ValueError) as excinfo: + parser = Cmd2ArgParser(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_apcustom_narg_tuple_triple(): + with pytest.raises(ValueError) as excinfo: + parser = Cmd2ArgParser(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_apcustom_narg_tuple_order(): + with pytest.raises(ValueError) as excinfo: + parser = Cmd2ArgParser(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_apcustom_narg_tuple_negative(): + with pytest.raises(ValueError) as excinfo: + parser = Cmd2ArgParser(prog='test') + parser.add_argument('invalid_tuple', nargs=(-1, 1)) + assert 'Negative numbers are invalid for nargs range' in str(excinfo.value) + + +def test_apcustom_narg_tuple_zero_base(): + parser = Cmd2ArgParser(prog='test') + parser.add_argument('tuple', nargs=(0, 3)) + + +def test_apcustom_narg_tuple_zero_to_one(): + parser = Cmd2ArgParser(prog='test') + parser.add_argument('tuple', nargs=(0, 1)) + + +def test_apcustom_print_message(capsys): + import sys + test_message = 'The test message' + + # Specify the file + parser = Cmd2ArgParser(prog='test') + parser._print_message(test_message, file=sys.stdout) + out, err = capsys.readouterr() + assert test_message in out + + # Make sure file defaults to sys.stderr + parser = Cmd2ArgParser(prog='test') + parser._print_message(test_message) + out, err = capsys.readouterr() + assert test_message in err + + +def test_apcustom_required_options(): + # Make sure a 'required arguments' section shows when a flag is marked required + parser = Cmd2ArgParser(prog='test') + parser.add_argument('--required_flag', required=True) + help = parser.format_help() + + assert 'required arguments' in help diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py deleted file mode 100644 index 4e1ceff0..00000000 --- a/tests/test_autocompletion.py +++ /dev/null @@ -1,345 +0,0 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit/functional testing for argparse completer in cmd2 -""" -import pytest - -from cmd2.utils import StdSim -from .conftest import run_cmd, normalize, complete_tester - -from examples.tab_autocompletion import TabCompleteExample - -@pytest.fixture -def cmd2_app(): - app = TabCompleteExample() - app.stdout = StdSim(app.stdout) - return app - - -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 -d DIRECTOR{1..2} - [-h] - title {G, PG, PG-13, R, NC-17} ... - -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): - out1, err1 = run_cmd(cmd2_app, 'suggest -h') - out2, err2 = 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): - out1, err1 = run_cmd(cmd2_app, 'media movies add -h') - out2, err2 = 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): - out, err = run_cmd(cmd2_app, 'suggest -t movie -d 3 4 5') - assert 'Error: unrecognized arguments: 5' in err[1] - - -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', 'load'] - - -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_autocomp_subcmd_flag_comp_func_attr(cmd2_app): - text = 'A' - line = 'video 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_attr(cmd2_app): - text = 'G' - line = 'video 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_autocomp_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_autocomp_pos_after_flag(cmd2_app): - text = 'Joh' - line = 'video 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_autocomp_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_autocomp_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'] - - -def test_autocomp_custom_func_dict_arg(cmd2_app): - text = '/home/user/' - line = 'video movies load {}'.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 == ['/home/user/another.db', '/home/user/file space.db', '/home/user/file.db'] - - -def test_argparse_remainder_flag_completion(cmd2_app): - import cmd2 - import argparse - - # Test flag completion as first arg of positional with nargs=argparse.REMAINDER - text = '--h' - line = 'help command {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - # --h should not complete into --help because we are in the argparse.REMAINDER section - assert complete_tester(text, line, begidx, endidx, cmd2_app) is None - - # Test flag completion within an already started positional with nargs=argparse.REMAINDER - text = '--h' - line = 'help command subcommand {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - # --h should not complete into --help because we are in the argparse.REMAINDER section - assert complete_tester(text, line, begidx, endidx, cmd2_app) is None - - # Test a flag with nargs=argparse.REMAINDER - parser = argparse.ArgumentParser() - parser.add_argument('-f', nargs=argparse.REMAINDER) - - # Overwrite eof's parser for this test - cmd2.Cmd.do_eof.argparser = parser - - text = '--h' - line = 'eof -f {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - # --h should not complete into --help because we are in the argparse.REMAINDER section - assert complete_tester(text, line, begidx, endidx, cmd2_app) is None - - -def test_completion_after_double_dash(cmd2_app): - """ - Test completion after --, which argparse says (all args after -- are non-options) - All of these tests occur outside of an argparse.REMAINDER section since those tests - are handled in test_argparse_remainder_flag_completion - """ - - # Test -- as the last token - text = '--' - line = 'help {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - # Since -- is the last token, then it should show flag choices - first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and '--help' in cmd2_app.completion_matches - - # Test -- to end all flag completion - text = '--' - line = 'help -- {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - # Since -- appeared before the -- being completed, nothing should be completed - assert complete_tester(text, line, begidx, endidx, cmd2_app) is None diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 9ffe547a..1bdbea5f 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1504,22 +1504,33 @@ invalid_command_name = [ 'noembedded"quotes', ] -def test_get_alias_names(base_app): - assert len(base_app.aliases) == 0 +def test_get_alias_completion_items(base_app): run_cmd(base_app, 'alias create fake run_pyscript') run_cmd(base_app, 'alias create ls !ls -hal') - assert len(base_app.aliases) == 2 - assert sorted(base_app._get_alias_names()) == ['fake', 'ls'] -def test_get_macro_names(base_app): - assert len(base_app.macros) == 0 + results = base_app._get_alias_completion_items() + assert len(results) == len(base_app.aliases) + + for cur_res in results: + assert cur_res in base_app.aliases + assert cur_res.description == base_app.aliases[cur_res] + +def test_get_macro_completion_items(base_app): run_cmd(base_app, 'macro create foo !echo foo') run_cmd(base_app, 'macro create bar !echo bar') - assert len(base_app.macros) == 2 - assert sorted(base_app._get_macro_names()) == ['bar', 'foo'] -def test_get_settable_names(base_app): - assert sorted(base_app._get_settable_names()) == sorted(base_app.settable.keys()) + results = base_app._get_macro_completion_items() + assert len(results) == len(base_app.macros) + + for cur_res in results: + assert cur_res in base_app.macros + assert cur_res.description == base_app.macros[cur_res].value + +def test_get_settable_completion_items(base_app): + results = base_app._get_settable_completion_items() + for cur_res in results: + assert cur_res in base_app.settable + assert cur_res.description == base_app.settable[cur_res] def test_alias_no_subcommand(base_app): out, err = run_cmd(base_app, 'alias') diff --git a/tests/test_completion.py b/tests/test_completion.py index 5cfc741c..1411cc49 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -67,7 +67,7 @@ class CompletionsExample(cmd2.Cmd): pass def complete_test_basic(self, text, line, begidx, endidx): - return self.basic_complete(text, line, begidx, endidx, food_item_strs) + return utils.basic_complete(text, line, begidx, endidx, food_item_strs) def do_test_delimited(self, args): pass @@ -80,7 +80,7 @@ class CompletionsExample(cmd2.Cmd): def complete_test_sort_key(self, text, line, begidx, endidx): num_strs = ['2', '11', '1'] - return self.basic_complete(text, line, begidx, endidx, num_strs) + return utils.basic_complete(text, line, begidx, endidx, num_strs) def do_test_raise_exception(self, args): pass @@ -516,7 +516,7 @@ def test_path_completion_directories_only(cmd2_app, request): expected = [text + 'cripts' + os.path.sep] - assert cmd2_app.path_complete(text, line, begidx, endidx, os.path.isdir) == expected + assert cmd2_app.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) == expected def test_basic_completion_single(cmd2_app): text = 'Pi' @@ -524,7 +524,7 @@ def test_basic_completion_single(cmd2_app): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] + assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] def test_basic_completion_multiple(cmd2_app): text = '' @@ -532,7 +532,7 @@ def test_basic_completion_multiple(cmd2_app): endidx = len(line) begidx = endidx - len(text) - matches = sorted(cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs)) + matches = sorted(utils.basic_complete(text, line, begidx, endidx, food_item_strs)) assert matches == sorted(food_item_strs) def test_basic_completion_nomatch(cmd2_app): @@ -541,7 +541,7 @@ def test_basic_completion_nomatch(cmd2_app): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == [] + assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == [] def test_delimiter_completion(cmd2_app): text = '/home/' @@ -592,7 +592,7 @@ def test_flag_based_default_completer(cmd2_app, request): begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, - flag_dict, cmd2_app.path_complete) == [text + 'onftest.py'] + flag_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] def test_flag_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -642,7 +642,7 @@ def test_index_based_default_completer(cmd2_app, request): begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, - index_dict, cmd2_app.path_complete) == [text + 'onftest.py'] + index_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] def test_index_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -1072,8 +1072,7 @@ class SubcommandsWithUnknownExample(cmd2.Cmd): # create the parser for the "sport" sub-command parser_sport = base_subparsers.add_parser('sport', help='sport help') - sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') - setattr(sport_arg, 'arg_choices', sport_item_strs) + sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) @cmd2.with_argparser_and_unknown_args(base_parser) def do_base(self, args): |