diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/conftest.py | 14 | ||||
-rw-r--r-- | tests/pyscript/recursive.py | 2 | ||||
-rw-r--r-- | tests/test_ansi.py | 51 | ||||
-rw-r--r-- | tests/test_argparse.py | 17 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 797 | ||||
-rw-r--r-- | tests/test_argparse_custom.py | 62 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 351 | ||||
-rwxr-xr-x | tests/test_completion.py | 236 | ||||
-rwxr-xr-x | tests/test_history.py | 189 | ||||
-rwxr-xr-x | tests/test_parsing.py | 263 | ||||
-rw-r--r-- | tests/test_plugin.py | 67 | ||||
-rw-r--r-- | tests/test_run_pyscript.py | 16 | ||||
-rw-r--r-- | tests/test_table_creator.py | 298 | ||||
-rw-r--r-- | tests/test_transcript.py | 95 | ||||
-rw-r--r-- | tests/test_utils.py | 83 | ||||
-rw-r--r-- | tests/test_utils_defining_class.py | 4 |
16 files changed, 1593 insertions, 952 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 73080b5c..1116539d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,9 +25,9 @@ except ImportError: pass -def verify_help_text(cmd2_app: cmd2.Cmd, - help_output: Union[str, List[str]], - verbose_strings: Optional[List[str]] = None) -> None: +def verify_help_text( + cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None +) -> None: """This function verifies that all expected commands are present in the help text. :param cmd2_app: instance of cmd2.Cmd @@ -158,12 +158,7 @@ def base_app(): # These are odd file names for testing quoting of them -odd_file_names = [ - 'nothingweird', - 'has spaces', - '"is_double_quoted"', - "'is_single_quoted'" -] +odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: @@ -182,6 +177,7 @@ def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Opti Matches are stored in app.completion_matches These matches also have been sorted by complete() """ + def get_line(): return line diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py index 7f02bb78..b88ba5a5 100644 --- a/tests/pyscript/recursive.py +++ b/tests/pyscript/recursive.py @@ -8,5 +8,5 @@ import os import sys app.cmd_echo = True -my_dir = (os.path.dirname(os.path.realpath(sys.argv[0]))) +my_dir = os.path.dirname(os.path.realpath(sys.argv[0])) app('run_pyscript {}'.format(os.path.join(my_dir, 'stop.py'))) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 4a28b1a0..8051b248 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -65,11 +65,19 @@ def test_style_multi(): base_str = HELLO_WORLD fg_color = 'blue' bg_color = 'green' - ansi_str = (ansi.fg[fg_color].value + ansi.bg[bg_color].value + - ansi.INTENSITY_BRIGHT + ansi.INTENSITY_DIM + ansi.UNDERLINE_ENABLE + - base_str + - ansi.FG_RESET + ansi.BG_RESET + - ansi.INTENSITY_NORMAL + ansi.INTENSITY_NORMAL + ansi.UNDERLINE_DISABLE) + ansi_str = ( + ansi.fg[fg_color].value + + ansi.bg[bg_color].value + + ansi.INTENSITY_BRIGHT + + ansi.INTENSITY_DIM + + ansi.UNDERLINE_ENABLE + + base_str + + ansi.FG_RESET + + ansi.BG_RESET + + ansi.INTENSITY_NORMAL + + ansi.INTENSITY_NORMAL + + ansi.UNDERLINE_DISABLE + ) assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, dim=True, underline=True) == ansi_str @@ -110,14 +118,23 @@ def test_set_title_str(): assert ansi.set_title_str(title) == OSC + '2;' + title + BEL -@pytest.mark.parametrize('cols, prompt, line, cursor, msg, expected', [ - (127, '(Cmd) ', 'help his', 12, ansi.style('Hello World!', fg='magenta'), '\x1b[2K\r\x1b[35mHello World!\x1b[39m'), - (127, '\n(Cmd) ', 'help ', 5, 'foo', '\x1b[2K\x1b[1A\x1b[2K\rfoo'), - (10, '(Cmd) ', 'help history of the american republic', 4, 'boo', '\x1b[3B\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\rboo') -]) +@pytest.mark.parametrize( + 'cols, prompt, line, cursor, msg, expected', + [ + (127, '(Cmd) ', 'help his', 12, ansi.style('Hello World!', fg='magenta'), '\x1b[2K\r\x1b[35mHello World!\x1b[39m'), + (127, '\n(Cmd) ', 'help ', 5, 'foo', '\x1b[2K\x1b[1A\x1b[2K\rfoo'), + ( + 10, + '(Cmd) ', + 'help history of the american republic', + 4, + 'boo', + '\x1b[3B\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\rboo', + ), + ], +) def test_async_alert_str(cols, prompt, line, cursor, msg, expected): - alert_str = ansi.async_alert_str(terminal_columns=cols, prompt=prompt, line=line, cursor_offset=cursor, - alert_msg=msg) + alert_str = ansi.async_alert_str(terminal_columns=cols, prompt=prompt, line=line, cursor_offset=cursor, alert_msg=msg) assert alert_str == expected @@ -127,19 +144,23 @@ def test_cast_color_as_str(): def test_color_str_building(): - from cmd2.ansi import fg, bg + from cmd2.ansi import bg, fg + assert fg.blue + "hello" == fg.blue.value + "hello" assert bg.blue + "hello" == bg.blue.value + "hello" assert fg.blue + "hello" + fg.reset == fg.blue.value + "hello" + fg.reset.value assert bg.blue + "hello" + bg.reset == bg.blue.value + "hello" + bg.reset.value - assert fg.blue + bg.white + "hello" + fg.reset + bg.reset == \ - fg.blue.value + bg.white.value + "hello" + fg.reset.value + bg.reset.value + assert ( + fg.blue + bg.white + "hello" + fg.reset + bg.reset + == fg.blue.value + bg.white.value + "hello" + fg.reset.value + bg.reset.value + ) def test_color_nonunique_values(): class Matching(ansi.ColorBase): magenta = ansi.fg_lookup('magenta') purple = ansi.fg_lookup('magenta') + assert sorted(Matching.colors()) == ['magenta', 'purple'] diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 7059e9d3..e91b4dba 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -49,7 +49,7 @@ class ArgparseApp(cmd2.Cmd): if word is None: word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -101,7 +101,7 @@ class ArgparseApp(cmd2.Cmd): if word is None: word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -308,8 +308,9 @@ class SubcommandApp(cmd2.Cmd): helpless_subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="A subcommand with no help") - @cmd2.as_subcommand_to('test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser, - help=helpless_subcmd_parser.description.lower()) + @cmd2.as_subcommand_to( + 'test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser, help=helpless_subcmd_parser.description.lower() + ) def helpless_subcmd_func(self, args: argparse.Namespace): # Make sure vars(Namespace) works. The way we originally added cmd2_hander to it resulted in a RecursionError. self.poutput(vars(args)) @@ -425,6 +426,7 @@ def test_subcmd_decorator(subcommand_app): def test_unittest_mock(): from unittest import mock + from cmd2 import CommandSetRegistrationError with mock.patch.object(ArgparseApp, 'namespace_provider'): @@ -449,12 +451,7 @@ def test_pytest_mock_invalid(mocker): app = ArgparseApp() -@pytest.mark.parametrize('spec_param', [ - {'spec': True}, - {'spec_set': True}, - {'autospec': True}, -]) +@pytest.mark.parametrize('spec_param', [{'spec': True}, {'spec_set': True}, {'autospec': True},]) def test_pytest_mock_valid(mocker, spec_param): mocker.patch.object(ArgparseApp, 'namespace_provider', **spec_param) app = ArgparseApp() - diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index dd86163b..3898de85 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -12,6 +12,7 @@ import pytest import cmd2 from cmd2 import Cmd2ArgumentParser, CompletionItem, with_argparser from cmd2.utils import CompletionError, StdSim, basic_complete + from .conftest import complete_tester, run_cmd # Lists used in our tests (there is a mix of sorted and unsorted on purpose) @@ -48,8 +49,7 @@ def choices_takes_arg_tokens(arg_tokens: argparse.Namespace) -> List[str]: return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] -def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, - arg_tokens: argparse.Namespace) -> List[str]: +def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, arg_tokens: argparse.Namespace) -> List[str]: """Completer function that receives arg_tokens from ArgparseCompleter""" match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] return basic_complete(text, line, begidx, endidx, match_against) @@ -58,6 +58,7 @@ def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, # noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember class ArgparseCompleterTester(cmd2.Cmd): """Cmd2 app that exercises ArgparseCompleter class""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -129,34 +130,47 @@ class ArgparseCompleterTester(cmd2.Cmd): choices_parser = Cmd2ArgumentParser() # 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('-d', "--desc_header", help='this arg has a descriptive header', - choices_method=completion_item_method, - descriptive_header=CUSTOM_DESC_HEADER) - choices_parser.add_argument('-n', "--no_header", help='this arg has no descriptive header', - choices_method=completion_item_method, metavar=STR_METAVAR) - choices_parser.add_argument('-t', "--tuple_metavar", help='this arg has tuple for a metavar', - choices_method=completion_item_method, metavar=TUPLE_METAVAR, - nargs=argparse.ONE_OR_MORE) - choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', - choices=int_choices) + 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( + '-d', + "--desc_header", + help='this arg has a descriptive header', + choices_method=completion_item_method, + descriptive_header=CUSTOM_DESC_HEADER, + ) + choices_parser.add_argument( + '-n', + "--no_header", + help='this arg has no descriptive header', + choices_method=completion_item_method, + metavar=STR_METAVAR, + ) + choices_parser.add_argument( + '-t', + "--tuple_metavar", + help='this arg has tuple for a metavar', + choices_method=completion_item_method, + metavar=TUPLE_METAVAR, + nargs=argparse.ONE_OR_MORE, + ) + choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', choices=int_choices) # 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) - choices_parser.add_argument('non_negative_int', type=int, help='a positional with non-negative int choices', - choices=non_negative_int_choices) - choices_parser.add_argument('empty_choices', help='a positional with empty choices', - choices=[]) + 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 + ) + choices_parser.add_argument( + 'non_negative_int', type=int, help='a positional with non-negative int choices', choices=non_negative_int_choices + ) + choices_parser.add_argument('empty_choices', help='a positional with empty choices', choices=[]) @with_argparser(choices_parser) def do_choices(self, args: argparse.Namespace) -> None: @@ -172,16 +186,18 @@ class ArgparseCompleterTester(cmd2.Cmd): completer_parser = Cmd2ArgumentParser() # 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) + 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) + 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: @@ -193,22 +209,23 @@ class ArgparseCompleterTester(cmd2.Cmd): nargs_parser = Cmd2ArgumentParser() # 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("--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 + ) # noinspection PyTypeChecker - 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("--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) + 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: @@ -241,10 +258,8 @@ class ArgparseCompleterTester(cmd2.Cmd): raise CompletionError('choice broke something') comp_error_parser = Cmd2ArgumentParser() - comp_error_parser.add_argument('completer', help='positional arg', - completer_method=completer_raise_error) - comp_error_parser.add_argument('--choice', help='flag arg', - choices_method=choice_raise_error) + comp_error_parser.add_argument('completer', help='positional arg', completer_method=completer_raise_error) + comp_error_parser.add_argument('--choice', help='flag arg', choices_method=choice_raise_error) @with_argparser(comp_error_parser) def do_raise_completion_error(self, args: argparse.Namespace) -> None: @@ -294,12 +309,7 @@ def ac_app(): return app -@pytest.mark.parametrize('command', [ - 'music', - 'music create', - 'music create rock', - 'music create jazz' -]) +@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)) @@ -314,16 +324,19 @@ def test_bad_subcommand_help(ac_app): assert out1 == out2 -@pytest.mark.parametrize('command, text, completions', [ - ('', 'mus', ['music ']), - ('music', 'cre', ['create ']), - ('music', 'creab', []), - ('music create', '', ['jazz', 'rock']), - ('music crea', 'jazz', []), - ('music create', 'foo', []), - ('fake create', '', []), - ('music fake', '', []) -]) +@pytest.mark.parametrize( + 'command, text, completions', + [ + ('', 'mus', ['music ']), + ('music', 'cre', ['create ']), + ('music', 'creab', []), + ('music create', '', ['jazz', 'rock']), + ('music crea', 'jazz', []), + ('music create', 'foo', []), + ('fake create', '', []), + ('music fake', '', []), + ], +) def test_complete_help(ac_app, command, text, completions): line = 'help {} {}'.format(command, text) endidx = len(line) @@ -338,12 +351,10 @@ def test_complete_help(ac_app, command, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('subcommand, text, completions', [ - ('create', '', ['jazz', 'rock']), - ('create', 'ja', ['jazz ']), - ('create', 'foo', []), - ('creab', 'ja', []) -]) +@pytest.mark.parametrize( + 'subcommand, text, completions', + [('create', '', ['jazz', 'rock']), ('create', 'ja', ['jazz ']), ('create', 'foo', []), ('creab', 'ja', [])], +) def test_subcommand_completions(ac_app, subcommand, text, completions): line = 'music {} {}'.format(subcommand, text) endidx = len(line) @@ -358,64 +369,132 @@ def test_subcommand_completions(ac_app, subcommand, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, completion_matches, display_matches', [ - # Complete all flags (suppressed will not show) - ('flag', '-', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', - '--remainder_flag', '--required_flag', '-a', '-c', '-h', '-n', '-o', '-q', '-r'], - ['-q, --required_flag', '[-o, --append_const_flag]', '[-a, --append_flag]', '[-c, --count_flag]', '[-h, --help]', - '[-n, --normal_flag]', '[-r, --remainder_flag]']), - ('flag', '--', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', - '--normal_flag', '--remainder_flag', '--required_flag'], - ['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', - '[--normal_flag]', '[--remainder_flag]']), - - # Complete individual flag - ('flag', '-n', ['-n '], ['[-n]']), - ('flag', '--n', ['--normal_flag '], ['[--normal_flag]']), - - # No flags should complete until current flag has its args - ('flag --append_flag', '-', [], []), - - # Complete REMAINDER flag name - ('flag', '-r', ['-r '], ['[-r]']), - ('flag', '--rem', ['--remainder_flag '], ['[--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', '--required_flag'], - ['--required_flag', '[--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', '--required_flag'], - ['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', - '[--normal_flag]', '[--remainder_flag]']), - - # Non-default flag prefix character (+) - ('plus_flag', '+', - ['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'], - ['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]']), - ('plus_flag', '++', - ['++help', '++normal_flag', '++required_flag'], - ['++required_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 --', '++', [], []) -]) +@pytest.mark.parametrize( + 'command_and_args, text, completion_matches, display_matches', + [ + # Complete all flags (suppressed will not show) + ( + 'flag', + '-', + [ + '--append_const_flag', + '--append_flag', + '--count_flag', + '--help', + '--normal_flag', + '--remainder_flag', + '--required_flag', + '-a', + '-c', + '-h', + '-n', + '-o', + '-q', + '-r', + ], + [ + '-q, --required_flag', + '[-o, --append_const_flag]', + '[-a, --append_flag]', + '[-c, --count_flag]', + '[-h, --help]', + '[-n, --normal_flag]', + '[-r, --remainder_flag]', + ], + ), + ( + 'flag', + '--', + [ + '--append_const_flag', + '--append_flag', + '--count_flag', + '--help', + '--normal_flag', + '--remainder_flag', + '--required_flag', + ], + [ + '--required_flag', + '[--append_const_flag]', + '[--append_flag]', + '[--count_flag]', + '[--help]', + '[--normal_flag]', + '[--remainder_flag]', + ], + ), + # Complete individual flag + ('flag', '-n', ['-n '], ['[-n]']), + ('flag', '--n', ['--normal_flag '], ['[--normal_flag]']), + # No flags should complete until current flag has its args + ('flag --append_flag', '-', [], []), + # Complete REMAINDER flag name + ('flag', '-r', ['-r '], ['[-r]']), + ('flag', '--rem', ['--remainder_flag '], ['[--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', '--required_flag'], + [ + '--required_flag', + '[--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', + '--required_flag', + ], + [ + '--required_flag', + '[--append_const_flag]', + '[--append_flag]', + '[--count_flag]', + '[--help]', + '[--normal_flag]', + '[--remainder_flag]', + ], + ), + # Non-default flag prefix character (+) + ( + 'plus_flag', + '+', + ['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'], + ['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]'], + ), + ( + 'plus_flag', + '++', + ['++help', '++normal_flag', '++required_flag'], + ['++required_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, completion_matches, display_matches): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -427,22 +506,26 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matc else: assert first_match is None - assert (ac_app.completion_matches == sorted(completion_matches, key=ac_app.default_sort_key) and - ac_app.display_matches == sorted(display_matches, key=ac_app.default_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', '', int_choices), - ('--int', '1', ['1 ']), - ('--int', '-', [-1, -2, -12]), - ('--int', '-1', [-1, -12]) -]) + assert ac_app.completion_matches == sorted( + completion_matches, key=ac_app.default_sort_key + ) and ac_app.display_matches == sorted(display_matches, key=ac_app.default_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', '', int_choices), + ('--int', '1', ['1 ']), + ('--int', '-', [-1, -2, -12]), + ('--int', '-1', [-1, -12]), + ], +) def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): line = 'choices {} {}'.format(flag, text) endidx = len(line) @@ -464,17 +547,20 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): assert ac_app.completion_matches == completions -@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']), - (4, '', non_negative_int_choices), - (4, '2', [2, 22]), - (5, '', []), -]) +@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']), + (4, '', non_negative_int_choices), + (4, '2', [2, 22]), + (5, '', []), + ], +) 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) @@ -518,12 +604,15 @@ def test_flag_sorting(ac_app): assert first_match is not None and ac_app.completion_matches == option_strings -@pytest.mark.parametrize('flag, text, completions', [ - ('-f', '', completions_from_function), - ('--function', 'f', ['function', 'fairly']), - ('-m', '', completions_from_method), - ('--method', 'm', ['method', 'missed']) -]) +@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) @@ -538,12 +627,15 @@ def test_autocomp_flag_completers(ac_app, flag, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('pos, text, completions', [ - (1, '', completions_from_function), - (1, 'c', ['completions', 'complete']), - (2, '', completions_from_method), - (2, 'm', ['method', 'missed']) -]) +@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) @@ -588,14 +680,17 @@ def test_autocomp_blank_token(ac_app): assert completions == completions_from_method -@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) -]) +@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): @@ -617,71 +712,57 @@ def test_completion_items(ac_app, num_aliases, show_description): 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), - - # 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 - ('--range choice --', 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', ['choices', 'value']), - - # Intermixed flag and positional with flag finishing - ('positional --set_value set value', ['the', 'choices']), - ('positional --range choice --', ['the', 'choices']), - - # REMAINDER positional - ('the positional', remainder_choices), - ('the positional remainder', ['choices ']), - ('the positional remainder choices', []), - - # REMAINDER positional. Flags don't work in REMAINDER - ('the positional --set_value', remainder_choices), - ('the positional remainder --set_value', ['choices ']) -]) +@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), + # 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 + ('--range choice --', 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', ['choices', 'value']), + # Intermixed flag and positional with flag finishing + ('positional --set_value set value', ['the', 'choices']), + ('positional --range choice --', ['the', 'choices']), + # REMAINDER positional + ('the positional', remainder_choices), + ('the positional remainder', ['choices ']), + ('the positional remainder choices', []), + # REMAINDER positional. Flags don't work in REMAINDER + ('the positional --set_value', remainder_choices), + ('the positional remainder --set_value', ['choices ']), + ], +) def test_autcomp_nargs(ac_app, args, completions): text = '' line = 'nargs {} {}'.format(args, text) @@ -697,43 +778,39 @@ def test_autcomp_nargs(ac_app, args, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, is_error', [ - # Flag is finished before moving on - ('hint --flag foo --', '', False), - ('hint --flag foo --help', '', False), - ('hint --flag foo', '--', False), - - ('nargs --one_or_more one --', '', False), - ('nargs --one_or_more one or --set_value', '', False), - ('nargs --one_or_more one or more', '--', False), - - ('nargs --set_value set value --', '', False), - ('nargs --set_value set value --one_or_more', '', False), - ('nargs --set_value set value', '--', False), - ('nargs --set_val set value', '--', False), # This exercises our abbreviated flag detection - - ('nargs --range choices --', '', False), - ('nargs --range choices range --set_value', '', False), - ('nargs --range range', '--', False), - - # Flag is not finished before moving on - ('hint --flag --', '', True), - ('hint --flag --help', '', True), - ('hint --flag', '--', True), - - ('nargs --one_or_more --', '', True), - ('nargs --one_or_more --set_value', '', True), - ('nargs --one_or_more', '--', True), - - ('nargs --set_value set --', '', True), - ('nargs --set_value set --one_or_more', '', True), - ('nargs --set_value set', '--', True), - ('nargs --set_val set', '--', True), # This exercises our abbreviated flag detection - - ('nargs --range --', '', True), - ('nargs --range --set_value', '', True), - ('nargs --range', '--', True), -]) +@pytest.mark.parametrize( + 'command_and_args, text, is_error', + [ + # Flag is finished before moving on + ('hint --flag foo --', '', False), + ('hint --flag foo --help', '', False), + ('hint --flag foo', '--', False), + ('nargs --one_or_more one --', '', False), + ('nargs --one_or_more one or --set_value', '', False), + ('nargs --one_or_more one or more', '--', False), + ('nargs --set_value set value --', '', False), + ('nargs --set_value set value --one_or_more', '', False), + ('nargs --set_value set value', '--', False), + ('nargs --set_val set value', '--', False), # This exercises our abbreviated flag detection + ('nargs --range choices --', '', False), + ('nargs --range choices range --set_value', '', False), + ('nargs --range range', '--', False), + # Flag is not finished before moving on + ('hint --flag --', '', True), + ('hint --flag --help', '', True), + ('hint --flag', '--', True), + ('nargs --one_or_more --', '', True), + ('nargs --one_or_more --set_value', '', True), + ('nargs --one_or_more', '--', True), + ('nargs --set_value set --', '', True), + ('nargs --set_value set --one_or_more', '', True), + ('nargs --set_value set', '--', True), + ('nargs --set_val set', '--', True), # This exercises our abbreviated flag detection + ('nargs --range --', '', True), + ('nargs --range --set_value', '', True), + ('nargs --range', '--', True), + ], +) def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -816,35 +893,32 @@ def test_completion_items_descriptive_header(ac_app): assert DEFAULT_DESCRIPTIVE_HEADER in ac_app.completion_header -@pytest.mark.parametrize('command_and_args, text, has_hint', [ - # Normal cases - ('hint', '', True), - ('hint --flag', '', True), - ('hint --suppressed_help', '', False), - ('hint --suppressed_hint', '', False), - - # Hint because flag does not have enough values to be considered finished - ('nargs --one_or_more', '-', True), - - # This flag has reached its minimum value count and therefore a new flag could start. - # However the flag can still consume values and the text is not a single prefix character. - # Therefor a hint will be shown. - ('nargs --one_or_more choices', 'bad_completion', True), - - # Like the previous case, but this time text is a single prefix character which will cause flag - # name completion to occur instead of a hint for the current flag. - ('nargs --one_or_more choices', '-', False), - - # Hint because this is a REMAINDER flag and therefore no more flag name completions occur. - ('nargs --remainder', '-', True), - - # No hint for the positional because text is a single prefix character which results in flag name completion - ('hint', '-', False), - - # Hint because this is a REMAINDER positional and therefore no more flag name completions occur. - ('nargs the choices', '-', True), - ('nargs the choices remainder', '-', True), -]) +@pytest.mark.parametrize( + 'command_and_args, text, has_hint', + [ + # Normal cases + ('hint', '', True), + ('hint --flag', '', True), + ('hint --suppressed_help', '', False), + ('hint --suppressed_hint', '', False), + # Hint because flag does not have enough values to be considered finished + ('nargs --one_or_more', '-', True), + # This flag has reached its minimum value count and therefore a new flag could start. + # However the flag can still consume values and the text is not a single prefix character. + # Therefor a hint will be shown. + ('nargs --one_or_more choices', 'bad_completion', True), + # Like the previous case, but this time text is a single prefix character which will cause flag + # name completion to occur instead of a hint for the current flag. + ('nargs --one_or_more choices', '-', False), + # Hint because this is a REMAINDER flag and therefore no more flag name completions occur. + ('nargs --remainder', '-', True), + # No hint for the positional because text is a single prefix character which results in flag name completion + ('hint', '-', False), + # Hint because this is a REMAINDER positional and therefore no more flag name completions occur. + ('nargs the choices', '-', True), + ('nargs the choices remainder', '-', True), + ], +) def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -868,20 +942,25 @@ def test_autocomp_hint_no_help_text(ac_app, capsys): out, err = capsys.readouterr() assert first_match is None - assert not out == ''' + assert ( + not out + == ''' Hint: NO_HELP_POS ''' - - -@pytest.mark.parametrize('args, text', [ - # Exercise a flag arg and choices function that raises a CompletionError - ('--choice ', 'choice'), - - # Exercise a positional arg and completer that raises a CompletionError - ('', 'completer') -]) + ) + + +@pytest.mark.parametrize( + 'args, text', + [ + # Exercise a flag arg and choices function that raises a CompletionError + ('--choice ', 'choice'), + # Exercise a positional arg and completer that raises a CompletionError + ('', 'completer'), + ], +) def test_completion_error(ac_app, capsys, args, text): line = 'raise_completion_error {} {}'.format(args, text) endidx = len(line) @@ -894,16 +973,17 @@ def test_completion_error(ac_app, capsys, args, text): assert "{} broke something".format(text) in out -@pytest.mark.parametrize('command_and_args, completions', [ - # Exercise a choices function that receives arg_tokens dictionary - ('arg_tokens choice subcmd', ['choice', 'subcmd']), - - # Exercise a completer that receives arg_tokens dictionary - ('arg_tokens completer subcmd fake', ['completer', 'subcmd']), - - # Exercise overriding parent_arg from the subcommand - ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']) -]) +@pytest.mark.parametrize( + 'command_and_args, completions', + [ + # Exercise a choices function that receives arg_tokens dictionary + ('arg_tokens choice subcmd', ['choice', 'subcmd']), + # Exercise a completer that receives arg_tokens dictionary + ('arg_tokens completer subcmd fake', ['completer', 'subcmd']), + # Exercise overriding parent_arg from the subcommand + ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']), + ], +) def test_arg_tokens(ac_app, command_and_args, completions): text = '' line = '{} {}'.format(command_and_args, text) @@ -919,34 +999,29 @@ def test_arg_tokens(ac_app, command_and_args, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, output_contains, first_match', [ - # Group isn't done. Hint will show for optional positional and no completions returned - ('mutex', '', 'the optional positional', None), - - # Group isn't done. Flag name will still complete. - ('mutex', '--fl', '', '--flag '), - - # Group isn't done. Flag hint will show. - ('mutex --flag', '', 'the flag arg', None), - - # Group finished by optional positional. No flag name will complete. - ('mutex pos_val', '--fl', '', None), - - # Group finished by optional positional. Error will display trying to complete the flag's value. - ('mutex pos_val --flag', '', 'f/--flag: not allowed with argument optional_pos', None), - - # Group finished by --flag. Optional positional will be skipped and last_arg will show its hint. - ('mutex --flag flag_val', '', 'the last arg', None), - - # Group finished by --flag. Other flag name won't complete. - ('mutex --flag flag_val', '--oth', '', None), - - # Group finished by --flag. Error will display trying to complete other flag's value. - ('mutex --flag flag_val --other', '', '-o/--other_flag: not allowed with argument -f/--flag', None), - - # Group finished by --flag. That same flag can be used again so it's hint will show. - ('mutex --flag flag_val --flag', '', 'the flag arg', None) -]) +@pytest.mark.parametrize( + 'command_and_args, text, output_contains, first_match', + [ + # Group isn't done. Hint will show for optional positional and no completions returned + ('mutex', '', 'the optional positional', None), + # Group isn't done. Flag name will still complete. + ('mutex', '--fl', '', '--flag '), + # Group isn't done. Flag hint will show. + ('mutex --flag', '', 'the flag arg', None), + # Group finished by optional positional. No flag name will complete. + ('mutex pos_val', '--fl', '', None), + # Group finished by optional positional. Error will display trying to complete the flag's value. + ('mutex pos_val --flag', '', 'f/--flag: not allowed with argument optional_pos', None), + # Group finished by --flag. Optional positional will be skipped and last_arg will show its hint. + ('mutex --flag flag_val', '', 'the last arg', None), + # Group finished by --flag. Other flag name won't complete. + ('mutex --flag flag_val', '--oth', '', None), + # Group finished by --flag. Error will display trying to complete other flag's value. + ('mutex --flag flag_val --other', '', '-o/--other_flag: not allowed with argument -f/--flag', None), + # Group finished by --flag. That same flag can be used again so it's hint will show. + ('mutex --flag flag_val --flag', '', 'the flag arg', None), + ], +) def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, first_match, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -960,6 +1035,7 @@ def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, f def test_single_prefix_char(): from cmd2.argparse_completer import _single_prefix_char + parser = Cmd2ArgumentParser(prefix_chars='-+') # Invalid @@ -976,6 +1052,7 @@ def test_single_prefix_char(): def test_looks_like_flag(): from cmd2.argparse_completer import _looks_like_flag + parser = Cmd2ArgumentParser() # Does not start like a flag diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index e2b3bb97..24a037fa 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -15,6 +15,7 @@ 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) @@ -40,15 +41,18 @@ def fake_func(): pass -@pytest.mark.parametrize('kwargs, is_valid', [ - ({'choices_function': fake_func}, True), - ({'choices_method': fake_func}, True), - ({'completer_function': fake_func}, True), - ({'completer_method': fake_func}, True), - ({'choices_function': fake_func, 'choices_method': fake_func}, False), - ({'choices_method': fake_func, 'completer_function': fake_func}, False), - ({'completer_function': fake_func, 'completer_method': fake_func}, False), -]) +@pytest.mark.parametrize( + 'kwargs, is_valid', + [ + ({'choices_function': fake_func}, True), + ({'choices_method': fake_func}, True), + ({'completer_function': fake_func}, True), + ({'completer_method': fake_func}, True), + ({'choices_function': fake_func, 'choices_method': fake_func}, False), + ({'choices_method': fake_func, 'completer_function': fake_func}, False), + ({'completer_function': fake_func, 'completer_method': fake_func}, False), + ], +) def test_apcustom_choices_callable_count(kwargs, is_valid): parser = Cmd2ArgumentParser() try: @@ -59,12 +63,15 @@ def test_apcustom_choices_callable_count(kwargs, is_valid): assert 'Only one of the following parameters' in str(ex) -@pytest.mark.parametrize('kwargs', [ - ({'choices_function': fake_func}), - ({'choices_method': fake_func}), - ({'completer_function': fake_func}), - ({'completer_method': fake_func}) -]) +@pytest.mark.parametrize( + 'kwargs', + [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}), + ], +) def test_apcustom_no_choices_callables_alongside_choices(kwargs): with pytest.raises(TypeError) as excinfo: parser = Cmd2ArgumentParser() @@ -72,12 +79,15 @@ def test_apcustom_no_choices_callables_alongside_choices(kwargs): assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value) -@pytest.mark.parametrize('kwargs', [ - ({'choices_function': fake_func}), - ({'choices_method': fake_func}), - ({'completer_function': fake_func}), - ({'completer_method': fake_func}) -]) +@pytest.mark.parametrize( + 'kwargs', + [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}), + ], +) def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs): with pytest.raises(TypeError) as excinfo: parser = Cmd2ArgumentParser() @@ -119,12 +129,7 @@ def test_apcustom_nargs_range_validation(cust_app): assert not err -@pytest.mark.parametrize('nargs_tuple', [ - (), - ('f', 5), - (5, 'f'), - (1, 2, 3), -]) +@pytest.mark.parametrize('nargs_tuple', [(), ('f', 5), (5, 'f'), (1, 2, 3),]) def test_apcustom_narg_invalid_tuples(nargs_tuple): with pytest.raises(ValueError) as excinfo: parser = Cmd2ArgumentParser() @@ -200,6 +205,7 @@ def test_apcustom_narg_tuple_other_ranges(): def test_apcustom_print_message(capsys): import sys + test_message = 'The test message' # Specify the file @@ -247,6 +253,7 @@ def test_apcustom_required_options(): def test_override_parser(): import importlib + from cmd2 import DEFAULT_ARGUMENT_PARSER # The standard parser is Cmd2ArgumentParser @@ -259,6 +266,7 @@ def test_override_parser(): # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser from examples.custom_parser import CustomParser + assert DEFAULT_ARGUMENT_PARSER == CustomParser diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 2f24f4d7..ae911474 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -40,15 +40,19 @@ def CreateOutsimApp(): c.stdout = utils.StdSim(c.stdout) return c + @pytest.fixture def outsim_app(): return CreateOutsimApp() + def test_version(base_app): assert cmd2.__version__ + def test_not_in_main_thread(base_app, capsys): import threading + cli_thread = threading.Thread(name='cli_thread', target=base_app.cmdloop) cli_thread.start() @@ -56,15 +60,18 @@ def test_not_in_main_thread(base_app, capsys): out, err = capsys.readouterr() assert "cmdloop must be run in the main thread" in err + def test_empty_statement(base_app): out, err = run_cmd(base_app, '') expected = normalize('') assert out == expected + def test_base_help(base_app): out, err = run_cmd(base_app, 'help') verify_help_text(base_app, out) + def test_base_help_verbose(base_app): out, err = run_cmd(base_app, 'help -v') verify_help_text(base_app, out) @@ -78,6 +85,7 @@ def test_base_help_verbose(base_app): verify_help_text(base_app, out) assert ':param' not in ''.join(out) + def test_base_argparse_help(base_app): # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense out1, err1 = run_cmd(base_app, 'set -h') @@ -88,21 +96,25 @@ def test_base_argparse_help(base_app): assert out1[1] == '' assert out1[2].startswith('Set a settable parameter') + def test_base_invalid_option(base_app): out, err = run_cmd(base_app, 'set -z') assert err[0] == 'Usage: set [-h] [-v] [param] [value]' assert 'Error: unrecognized arguments: -z' in err[1] + def test_base_shortcuts(base_app): out, err = run_cmd(base_app, 'shortcuts') expected = normalize(SHORTCUTS_TXT) assert out == expected + def test_command_starts_with_shortcut(): with pytest.raises(ValueError) as excinfo: app = cmd2.Cmd(shortcuts={'help': 'fake'}) assert "Invalid command name 'help'" in str(excinfo.value) + def test_base_show(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' @@ -121,30 +133,37 @@ def test_base_show_long(base_app): def test_set(base_app): out, err = run_cmd(base_app, 'set quiet True') - expected = normalize(""" + expected = normalize( + """ quiet - was: False now: True -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] + def test_set_val_empty(base_app): base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor ""') assert base_app.editor == '' + def test_set_val_is_flag(base_app): base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor "-h"') assert base_app.editor == '-h' + def test_set_not_supported(base_app): out, err = run_cmd(base_app, 'set qqq True') - expected = normalize(""" + expected = normalize( + """ Parameter 'qqq' not supported (type 'set' for list of parameters). -""") +""" + ) assert err == expected @@ -155,15 +174,18 @@ def test_set_no_settables(base_app): assert err == expected -@pytest.mark.parametrize('new_val, is_valid, expected', [ - (ansi.STYLE_NEVER, True, ansi.STYLE_NEVER), - ('neVeR', True, ansi.STYLE_NEVER), - (ansi.STYLE_TERMINAL, True, ansi.STYLE_TERMINAL), - ('TeRMInal', True, ansi.STYLE_TERMINAL), - (ansi.STYLE_ALWAYS, True, ansi.STYLE_ALWAYS), - ('AlWaYs', True, ansi.STYLE_ALWAYS), - ('invalid', False, ansi.STYLE_TERMINAL), -]) +@pytest.mark.parametrize( + 'new_val, is_valid, expected', + [ + (ansi.STYLE_NEVER, True, ansi.STYLE_NEVER), + ('neVeR', True, ansi.STYLE_NEVER), + (ansi.STYLE_TERMINAL, True, ansi.STYLE_TERMINAL), + ('TeRMInal', True, ansi.STYLE_TERMINAL), + (ansi.STYLE_ALWAYS, True, ansi.STYLE_ALWAYS), + ('AlWaYs', True, ansi.STYLE_ALWAYS), + ('invalid', False, ansi.STYLE_TERMINAL), + ], +) def test_set_allow_style(base_app, new_val, is_valid, expected): # Initialize allow_style for this test ansi.allow_style = ansi.STYLE_TERMINAL @@ -190,18 +212,22 @@ class OnChangeHookApp(cmd2.Cmd): """Runs when quiet is changed via set command""" self.poutput("You changed " + name) + @pytest.fixture def onchange_app(): app = OnChangeHookApp() return app + def test_set_onchange_hook(onchange_app): out, err = run_cmd(onchange_app, 'set quiet True') - expected = normalize(""" + expected = normalize( + """ quiet - was: False now: True You changed quiet -""") +""" + ) assert out == expected @@ -212,6 +238,7 @@ def test_base_shell(base_app, monkeypatch): assert out == [] assert m.called + def test_shell_last_result(base_app): base_app.last_result = None run_cmd(base_app, 'shell fake') @@ -220,11 +247,7 @@ def test_shell_last_result(base_app): def test_shell_manual_call(base_app): # Verifies crash from Issue #986 doesn't happen - cmds = [ - 'echo "hi"', - 'echo "there"', - 'echo "cmd2!"' - ] + cmds = ['echo "hi"', 'echo "there"', 'echo "cmd2!"'] cmd = ';'.join(cmds) base_app.do_shell(cmd) @@ -299,31 +322,37 @@ def test_run_script(base_app, request): assert script_out == manual_out assert script_err == manual_err + def test_run_script_with_empty_args(base_app): out, err = run_cmd(base_app, 'run_script') assert "the following arguments are required" in err[1] + def test_run_script_with_nonexistent_file(base_app, capsys): out, err = run_cmd(base_app, 'run_script does_not_exist.txt') assert "does not exist" in err[0] + def test_run_script_with_directory(base_app, request): test_dir = os.path.dirname(request.module.__file__) out, err = run_cmd(base_app, 'run_script {}'.format(test_dir)) assert "is not a file" in err[0] + def test_run_script_with_empty_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'empty.txt') out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert not out and not err + def test_run_script_with_binary_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'binary.bin') out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert "is not an ASCII or UTF-8 encoded text file" in err[0] + def test_run_script_with_python_file(base_app, request): m = mock.MagicMock(name='input', return_value='2') builtins.input = m @@ -333,6 +362,7 @@ def test_run_script_with_python_file(base_app, request): out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert "appears to be a Python file" in err[0] + def test_run_script_with_utf8_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') @@ -360,6 +390,7 @@ def test_run_script_with_utf8_file(base_app, request): assert script_out == manual_out assert script_err == manual_err + def test_run_script_nested_run_scripts(base_app, request): # Verify that running a script with nested run_script commands works correctly, # and runs the nested script commands in the correct order. @@ -371,43 +402,48 @@ def test_run_script_nested_run_scripts(base_app, request): run_cmd(base_app, initial_run) # Check that the right commands were executed. - expected = """ + expected = ( + """ %s _relative_run_script precmds.txt set allow_style Always help shortcuts _relative_run_script postcmds.txt -set allow_style Never""" % initial_run +set allow_style Never""" + % initial_run + ) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) + def test_runcmds_plus_hooks(base_app, request): test_dir = os.path.dirname(request.module.__file__) prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt') postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt') - base_app.runcmds_plus_hooks(['run_script ' + prefilepath, - 'help', - 'shortcuts', - 'run_script ' + postfilepath]) + base_app.runcmds_plus_hooks(['run_script ' + prefilepath, 'help', 'shortcuts', 'run_script ' + postfilepath]) expected = """ -run_script %s +run_script {} set allow_style Always help shortcuts -run_script %s -set allow_style Never""" % (prefilepath, postfilepath) +run_script {} +set allow_style Never""".format( + prefilepath, postfilepath, + ) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) + def test_runcmds_plus_hooks_ctrl_c(base_app, capsys): """Test Ctrl-C while in runcmds_plus_hooks""" import types def do_keyboard_interrupt(self, _): raise KeyboardInterrupt('Interrupting this command') + setattr(base_app, 'do_keyboard_interrupt', types.MethodType(do_keyboard_interrupt, base_app)) # Default behavior is to stop command loop on Ctrl-C @@ -424,6 +460,7 @@ def test_runcmds_plus_hooks_ctrl_c(base_app, capsys): assert not err assert len(base_app.history) == 3 + def test_relative_run_script(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') @@ -451,6 +488,7 @@ def test_relative_run_script(base_app, request): assert script_out == manual_out assert script_err == manual_err + @pytest.mark.parametrize('file_name', odd_file_names) def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch): """Test file names with various patterns""" @@ -461,10 +499,12 @@ def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatc run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name))) run_script_mock.assert_called_once_with(utils.quote_string(file_name)) + def test_relative_run_script_requires_an_argument(base_app): out, err = run_cmd(base_app, '_relative_run_script') assert 'Error: the following arguments' in err[1] + def test_in_script(request): class HookApp(cmd2.Cmd): def __init__(self, *args, **kwargs): @@ -483,17 +523,20 @@ def test_in_script(request): assert "WE ARE IN SCRIPT" in out[-1] + def test_system_exit_in_command(base_app, capsys): """Test raising SystemExit from a command""" import types def do_system_exit(self, _): raise SystemExit + setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app)) stop = base_app.onecmd_plus_hooks('system_exit') assert stop + def test_output_redirection(base_app): fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(fd) @@ -516,6 +559,7 @@ def test_output_redirection(base_app): finally: os.remove(filename) + def test_output_redirection_to_nonexistent_directory(base_app): filename = '~/fakedir/this_does_not_exist.txt' @@ -525,12 +569,15 @@ def test_output_redirection_to_nonexistent_directory(base_app): out, err = run_cmd(base_app, 'help >> {}'.format(filename)) assert 'Failed to redirect' in err[0] + def test_output_redirection_to_too_long_filename(base_app): - filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' \ - 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' \ - 'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu' \ - 'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie' \ - 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + filename = ( + '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' + 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' + 'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu' + 'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie' + 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + ) out, err = run_cmd(base_app, 'help > {}'.format(filename)) assert 'Failed to redirect' in err[0] @@ -588,6 +635,7 @@ def test_disallow_redirection(base_app): # Verify that no file got created assert not os.path.exists(filename) + def test_pipe_to_shell(base_app): if sys.platform == "win32": # Windows @@ -600,6 +648,7 @@ def test_pipe_to_shell(base_app): out, err = run_cmd(base_app, command) assert out and not err + def test_pipe_to_shell_and_redirect(base_app): filename = 'out.txt' if sys.platform == "win32": @@ -615,14 +664,15 @@ def test_pipe_to_shell_and_redirect(base_app): assert os.path.exists(filename) os.remove(filename) + def test_pipe_to_shell_error(base_app): # Try to pipe command output to a shell command that doesn't exist in order to produce an error out, err = run_cmd(base_app, 'help | foobarbaz.this_does_not_exist') assert not out assert "Pipe process exited with code" in err[0] -@pytest.mark.skipif(not clipboard.can_clip, - reason="Pyperclip could not find a copy/paste mechanism for your system") + +@pytest.mark.skipif(not clipboard.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system") def test_send_to_paste_buffer(base_app): # Test writing to the PasteBuffer/Clipboard run_cmd(base_app, 'help >') @@ -639,9 +689,11 @@ def test_send_to_paste_buffer(base_app): def test_base_timing(base_app): base_app.feedback_to_output = False out, err = run_cmd(base_app, 'set timing True') - expected = normalize("""timing - was: False + expected = normalize( + """timing - was: False now: True -""") +""" + ) assert out == expected if sys.platform == 'win32': @@ -656,13 +708,18 @@ def _expected_no_editor_error(): if hasattr(sys, "pypy_translation_info"): expected_exception = 'EnvironmentError' - expected_text = normalize(""" + expected_text = normalize( + """ EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.' To enable full traceback, run the following command: 'set debug true' -""".format(expected_exception)) +""".format( + expected_exception + ) + ) return expected_text + def test_base_debug(base_app): # Purposely set the editor to None base_app.editor = None @@ -675,16 +732,19 @@ def test_base_debug(base_app): # Set debug true out, err = run_cmd(base_app, 'set debug True') - expected = normalize(""" + expected = normalize( + """ debug - was: False now: True -""") +""" + ) assert out == expected # Verify that we now see the exception traceback out, err = run_cmd(base_app, 'edit') assert err[0].startswith('Traceback (most recent call last):') + def test_debug_not_settable(base_app): # Set debug to False and make it unsettable base_app.debug = False @@ -696,10 +756,12 @@ def test_debug_not_settable(base_app): # Since debug is unsettable, the user will not be given the option to enable a full traceback assert err == ['Invalid syntax: No closing quotation'] + def test_remove_settable_keyerror(base_app): with pytest.raises(KeyError): base_app.remove_settable('fake') + def test_edit_file(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -716,6 +778,7 @@ def test_edit_file(base_app, request, monkeypatch): # We think we have an editor, so should expect a Popen call m.assert_called_once() + @pytest.mark.parametrize('file_name', odd_file_names) def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): """Test editor and file names with various patterns""" @@ -728,6 +791,7 @@ def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): run_cmd(base_app, "edit {}".format(utils.quote_string(file_name))) shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name))) + def test_edit_file_with_spaces(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -744,6 +808,7 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch): # We think we have an editor, so should expect a Popen call m.assert_called_once() + def test_edit_blank(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -830,8 +895,8 @@ def test_cmdloop_without_rawinput(): out = app.stdout.getvalue() assert out == expected -@pytest.mark.skipif(sys.platform.startswith('win'), - reason="stty sane only run on Linux/Mac") + +@pytest.mark.skipif(sys.platform.startswith('win'), reason="stty sane only run on Linux/Mac") def test_stty_sane(base_app, monkeypatch): """Make sure stty sane is run on Linux/Mac after each command if stdin is a terminal""" with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)): @@ -842,6 +907,7 @@ def test_stty_sane(base_app, monkeypatch): base_app.onecmd_plus_hooks('help') m.assert_called_once_with(['stty', 'sane']) + class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -853,11 +919,13 @@ class HookFailureApp(cmd2.Cmd): data.stop = True return data + @pytest.fixture def hook_failure(): app = HookFailureApp() return app + def test_precmd_hook_success(base_app): out = base_app.onecmd_plus_hooks('help') assert out is False @@ -875,12 +943,14 @@ class SayApp(cmd2.Cmd): def do_say(self, arg): self.poutput(arg) + @pytest.fixture def say_app(): app = SayApp(allow_cli_args=False) app.stdout = utils.StdSim(app.stdout) return app + def test_interrupt_quit(say_app): say_app.quit_on_sigint = True @@ -898,6 +968,7 @@ def test_interrupt_quit(say_app): out = say_app.stdout.getvalue() assert out == 'hello\n' + def test_interrupt_noquit(say_app): say_app.quit_on_sigint = False @@ -921,6 +992,7 @@ class ShellApp(cmd2.Cmd): super().__init__(*args, **kwargs) self.default_to_shell = True + def test_default_to_shell(base_app, monkeypatch): if sys.platform.startswith('win'): line = 'dir' @@ -934,14 +1006,17 @@ def test_default_to_shell(base_app, monkeypatch): assert out == [] assert m.called + def test_ansi_prompt_not_esacped(base_app): from cmd2.rl_utils import rl_make_safe_prompt + prompt = '(Cmd) ' assert rl_make_safe_prompt(prompt) == prompt def test_ansi_prompt_escaped(): from cmd2.rl_utils import rl_make_safe_prompt + app = cmd2.Cmd() color = 'cyan' prompt = 'InColor' @@ -963,6 +1038,7 @@ def test_ansi_prompt_escaped(): class HelpApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -996,24 +1072,29 @@ def help_app(): app = HelpApp() return app + def test_custom_command_help(help_app): out, err = run_cmd(help_app, 'help squat') expected = normalize('This command does diddly squat...') assert out == expected + def test_custom_help_menu(help_app): out, err = run_cmd(help_app, 'help') verify_help_text(help_app, out) + def test_help_undocumented(help_app): out, err = run_cmd(help_app, 'help undoc') assert err[0].startswith("No help on undoc") + def test_help_overridden_method(help_app): out, err = run_cmd(help_app, 'help edit') expected = normalize('This overrides the edit command and does nothing.') assert out == expected + def test_help_multiline_docstring(help_app): out, err = run_cmd(help_app, 'help multiline_docstr') expected = normalize('This documentation\nis multiple lines\nand there are no\ntabs') @@ -1022,6 +1103,7 @@ def test_help_multiline_docstring(help_app): class HelpCategoriesApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1052,15 +1134,18 @@ class HelpCategoriesApp(cmd2.Cmd): def do_undoc(self, arg): pass + @pytest.fixture def helpcat_app(): app = HelpCategoriesApp() return app + def test_help_cat_base(helpcat_app): out, err = run_cmd(helpcat_app, 'help') verify_help_text(helpcat_app, out) + def test_help_cat_verbose(helpcat_app): out, err = run_cmd(helpcat_app, 'help --verbose') verify_help_text(helpcat_app, out) @@ -1085,8 +1170,9 @@ class SelectApp(cmd2.Cmd): def do_procrastinate(self, arg): """Waste time in your manner of choice.""" # Pass in a list of tuples for selections - leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')], - 'How would you like to procrastinate? ') + leisure_activity = self.select( + [('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')], 'How would you like to procrastinate? ' + ) result = 'Have fun procrasinating with {}!\n'.format(leisure_activity) self.stdout.write(result) @@ -1097,11 +1183,13 @@ class SelectApp(cmd2.Cmd): result = 'Charm us with the {}...\n'.format(instrument) self.stdout.write(result) + @pytest.fixture def select_app(): app = SelectApp() return app + def test_select_options(select_app, monkeypatch): # Mock out the read_input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') @@ -1109,11 +1197,15 @@ def test_select_options(select_app, monkeypatch): food = 'bacon' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty {} with salty sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Sauce? ') @@ -1121,6 +1213,7 @@ def test_select_options(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_invalid_option_too_big(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input') @@ -1131,12 +1224,16 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): food = 'fish' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty '3' isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' @@ -1147,6 +1244,7 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_invalid_option_too_small(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input') @@ -1157,12 +1255,16 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): food = 'fish' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty '0' isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' @@ -1173,17 +1275,22 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_list_of_strings(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "study") - expected = normalize(""" + expected = normalize( + """ 1. math 2. science Good luck learning {}! -""".format('science')) +""".format( + 'science' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Subject? ') @@ -1191,17 +1298,22 @@ Good luck learning {}! # And verify the expected output to stdout assert out == expected + def test_select_list_of_tuples(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "procrastinate") - expected = normalize(""" + expected = normalize( + """ 1. Netflix 2. WebSurfing Have fun procrasinating with {}! -""".format('YouTube')) +""".format( + 'YouTube' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('How would you like to procrastinate? ') @@ -1216,11 +1328,15 @@ def test_select_uneven_list_of_tuples(select_app, monkeypatch): monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "play") - expected = normalize(""" + expected = normalize( + """ 1. Electric Guitar 2. Drums Charm us with the {}... -""".format('Drums')) +""".format( + 'Drums' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Instrument? ') @@ -1228,6 +1344,7 @@ Charm us with the {}... # And verify the expected output to stdout assert out == expected + def test_select_eof(select_app, monkeypatch): # Ctrl-D during select causes an EOFError that just reprompts the user read_input_mock = mock.MagicMock(name='read_input', side_effect=[EOFError, 2]) @@ -1242,6 +1359,7 @@ def test_select_eof(select_app, monkeypatch): read_input_mock.assert_has_calls(calls) assert read_input_mock.call_count == 2 + def test_select_ctrl_c(outsim_app, monkeypatch, capsys): # Ctrl-C during select prints ^C and raises a KeyboardInterrupt read_input_mock = mock.MagicMock(name='read_input', side_effect=KeyboardInterrupt) @@ -1253,9 +1371,11 @@ def test_select_ctrl_c(outsim_app, monkeypatch, capsys): out = outsim_app.stdout.getvalue() assert out.rstrip().endswith('^C') + class HelpNoDocstringApp(cmd2.Cmd): greet_parser = argparse.ArgumentParser() greet_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") + @cmd2.with_argparser(greet_parser, with_unknown_args=True) def do_greet(self, opts, arg): arg = ''.join(arg) @@ -1263,17 +1383,22 @@ class HelpNoDocstringApp(cmd2.Cmd): arg = arg.upper() self.stdout.write(arg + '\n') + def test_help_with_no_docstring(capsys): app = HelpNoDocstringApp() app.onecmd_plus_hooks('greet -h') out, err = capsys.readouterr() assert err == '' - assert out == """usage: greet [-h] [-s] + assert ( + out + == """usage: greet [-h] [-s] optional arguments: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE """ + ) + class MultilineApp(cmd2.Cmd): def __init__(self, *args, **kwargs): @@ -1289,15 +1414,18 @@ class MultilineApp(cmd2.Cmd): arg = arg.upper() self.stdout.write(arg + '\n') + @pytest.fixture def multiline_app(): app = MultilineApp() return app + def test_multiline_complete_empty_statement_raises_exception(multiline_app): with pytest.raises(exceptions.EmptyStatement): multiline_app._complete_statement('') + def test_multiline_complete_statement_without_terminator(multiline_app): # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input @@ -1312,6 +1440,7 @@ def test_multiline_complete_statement_without_terminator(multiline_app): assert statement.command == command assert statement.multiline_command == command + def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input @@ -1325,6 +1454,7 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): assert statement.multiline_command == 'orate' assert statement.terminator == ';' + def test_multiline_input_line_to_statement(multiline_app): # Verify _input_line_to_statement saves the fully entered input line for multiline commands @@ -1340,6 +1470,7 @@ def test_multiline_input_line_to_statement(multiline_app): assert statement.command == 'orate' assert statement.multiline_command == 'orate' + def test_clipboard_failure(base_app, capsys): # Force cmd2 clipboard to be disabled base_app._can_clip = False @@ -1369,11 +1500,13 @@ class CommandResultApp(cmd2.Cmd): def do_negative_no_data(self, arg): self.last_result = cmd2.CommandResult('', arg) + @pytest.fixture def commandresult_app(): app = CommandResultApp() return app + def test_commandresult_truthy(commandresult_app): arg = 'foo' run_cmd(commandresult_app, 'affirmative {}'.format(arg)) @@ -1384,6 +1517,7 @@ def test_commandresult_truthy(commandresult_app): assert commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult(arg) + def test_commandresult_falsy(commandresult_app): arg = 'bar' run_cmd(commandresult_app, 'negative {}'.format(arg)) @@ -1409,6 +1543,7 @@ def test_eof(base_app): # Only thing to verify is that it returns True assert base_app.do_eof('') + def test_echo(capsys): app = cmd2.Cmd() app.echo = True @@ -1419,6 +1554,7 @@ def test_echo(capsys): out, err = capsys.readouterr() assert out.startswith('{}{}\n'.format(app.prompt, commands[0]) + HELP_HISTORY.split()[0]) + def test_read_input_rawinput_true(capsys, monkeypatch): prompt_str = 'the_prompt' input_str = 'some input' @@ -1450,6 +1586,7 @@ def test_read_input_rawinput_true(capsys, monkeypatch): assert line == input_str assert not out + def test_read_input_rawinput_false(capsys, monkeypatch): prompt_str = 'the_prompt' input_str = 'some input' @@ -1502,6 +1639,7 @@ def test_read_input_rawinput_false(capsys, monkeypatch): assert line == 'eof' assert not out + def test_read_command_line_eof(base_app, monkeypatch): read_input_mock = mock.MagicMock(name='read_input', side_effect=EOFError) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) @@ -1509,6 +1647,7 @@ def test_read_command_line_eof(base_app, monkeypatch): line = base_app._read_command_line("Prompt> ") assert line == 'eof' + def test_poutput_string(outsim_app): msg = 'This is a test' outsim_app.poutput(msg) @@ -1516,6 +1655,7 @@ def test_poutput_string(outsim_app): expected = msg + '\n' assert out == expected + def test_poutput_zero(outsim_app): msg = 0 outsim_app.poutput(msg) @@ -1523,6 +1663,7 @@ def test_poutput_zero(outsim_app): expected = str(msg) + '\n' assert out == expected + def test_poutput_empty_string(outsim_app): msg = '' outsim_app.poutput(msg) @@ -1530,6 +1671,7 @@ def test_poutput_empty_string(outsim_app): expected = '\n' assert out == expected + def test_poutput_none(outsim_app): msg = None outsim_app.poutput(msg) @@ -1537,6 +1679,7 @@ def test_poutput_none(outsim_app): expected = 'None\n' assert out == expected + def test_poutput_ansi_always(outsim_app): msg = 'Hello World' ansi.allow_style = ansi.STYLE_ALWAYS @@ -1547,6 +1690,7 @@ def test_poutput_ansi_always(outsim_app): assert colored_msg != msg assert out == expected + def test_poutput_ansi_never(outsim_app): msg = 'Hello World' ansi.allow_style = ansi.STYLE_NEVER @@ -1571,6 +1715,7 @@ invalid_command_name = [ 'noembedded"quotes', ] + 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') @@ -1582,6 +1727,7 @@ def test_get_alias_completion_items(base_app): 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') @@ -1593,17 +1739,20 @@ def test_get_macro_completion_items(base_app): 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.settables assert cur_res.description == base_app.settables[cur_res].description + def test_alias_no_subcommand(base_app): out, err = run_cmd(base_app, 'alias') assert "Usage: alias [-h]" in err[0] assert "Error: the following arguments are required: SUBCOMMAND" in err[1] + def test_alias_create(base_app): # Create the alias out, err = run_cmd(base_app, 'alias create fake run_pyscript') @@ -1636,6 +1785,7 @@ def test_alias_create(base_app): out, err = run_cmd(base_app, 'alias list --with_silent fake') assert out == normalize('alias create --silent fake set') + def test_alias_create_with_quoted_tokens(base_app): """Demonstrate that quotes in alias value will be preserved""" create_command = 'alias create fake help ">" "out file.txt" ";"' @@ -1648,21 +1798,25 @@ def test_alias_create_with_quoted_tokens(base_app): out, err = run_cmd(base_app, 'alias list fake') assert out == normalize(create_command) + @pytest.mark.parametrize('alias_name', invalid_command_name) def test_alias_create_invalid_name(base_app, alias_name, capsys): out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name)) assert "Invalid alias name" in err[0] + def test_alias_create_with_command_name(base_app): out, err = run_cmd(base_app, 'alias create help stuff') assert "Alias cannot have the same name as a command" in err[0] + def test_alias_create_with_macro_name(base_app): macro = "my_macro" run_cmd(base_app, 'macro create {} help'.format(macro)) out, err = run_cmd(base_app, 'alias create {} help'.format(macro)) assert "Alias cannot have the same name as a macro" in err[0] + def test_alias_that_resolves_into_comment(base_app): # Create the alias out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah') @@ -1673,11 +1827,13 @@ def test_alias_that_resolves_into_comment(base_app): assert not out assert not err + def test_alias_list_invalid_alias(base_app): # Look up invalid alias out, err = run_cmd(base_app, 'alias list invalid') assert "Alias 'invalid' not found" in err[0] + def test_alias_delete(base_app): # Create an alias run_cmd(base_app, 'alias create fake run_pyscript') @@ -1686,18 +1842,22 @@ def test_alias_delete(base_app): out, err = run_cmd(base_app, 'alias delete fake') assert out == normalize("Alias 'fake' deleted") + def test_alias_delete_all(base_app): out, err = run_cmd(base_app, 'alias delete --all') assert out == normalize("All aliases deleted") + def test_alias_delete_non_existing(base_app): out, err = run_cmd(base_app, 'alias delete fake') assert "Alias 'fake' does not exist" in err[0] + def test_alias_delete_no_name(base_app): out, err = run_cmd(base_app, 'alias delete') assert "Either --all or alias name(s)" in err[0] + def test_multiple_aliases(base_app): alias1 = 'h1' alias2 = 'h2' @@ -1709,11 +1869,13 @@ def test_multiple_aliases(base_app): out, err = run_cmd(base_app, alias2) verify_help_text(base_app, out) + def test_macro_no_subcommand(base_app): out, err = run_cmd(base_app, 'macro') assert "Usage: macro [-h]" in err[0] assert "Error: the following arguments are required: SUBCOMMAND" in err[1] + def test_macro_create(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript') @@ -1746,6 +1908,7 @@ def test_macro_create(base_app): out, err = run_cmd(base_app, 'macro list --with_silent fake') assert out == normalize('macro create --silent fake set') + def test_macro_create_with_quoted_tokens(base_app): """Demonstrate that quotes in macro value will be preserved""" create_command = 'macro create fake help ">" "out file.txt" ";"' @@ -1758,21 +1921,25 @@ def test_macro_create_with_quoted_tokens(base_app): out, err = run_cmd(base_app, 'macro list fake') assert out == normalize(create_command) + @pytest.mark.parametrize('macro_name', invalid_command_name) def test_macro_create_invalid_name(base_app, macro_name): out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name)) assert "Invalid macro name" in err[0] + def test_macro_create_with_command_name(base_app): out, err = run_cmd(base_app, 'macro create help stuff') assert "Macro cannot have the same name as a command" in err[0] + def test_macro_create_with_alias_name(base_app): macro = "my_macro" run_cmd(base_app, 'alias create {} help'.format(macro)) out, err = run_cmd(base_app, 'macro create {} help'.format(macro)) assert "Macro cannot have the same name as an alias" in err[0] + def test_macro_create_with_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} {2}') @@ -1782,6 +1949,7 @@ def test_macro_create_with_args(base_app): out, err = run_cmd(base_app, 'fake help -v') verify_help_text(base_app, out) + def test_macro_create_with_escaped_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {{1}}') @@ -1791,6 +1959,7 @@ def test_macro_create_with_escaped_args(base_app): out, err = run_cmd(base_app, 'fake') assert err[0].startswith('No help on {1}') + def test_macro_usage_with_missing_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {2}') @@ -1800,6 +1969,7 @@ def test_macro_usage_with_missing_args(base_app): out, err = run_cmd(base_app, 'fake arg1') assert "expects at least 2 argument(s)" in err[0] + def test_macro_usage_with_exta_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1}') @@ -1809,16 +1979,19 @@ def test_macro_usage_with_exta_args(base_app): out, err = run_cmd(base_app, 'fake alias create') assert "Usage: alias create" in out[0] + def test_macro_create_with_missing_arg_nums(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {3}') assert "Not all numbers between 1 and 3" in err[0] + def test_macro_create_with_invalid_arg_num(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}') assert "Argument numbers must be greater than 0" in err[0] + def test_macro_create_with_unicode_numbered_arg(base_app): # Create the macro expecting 1 argument out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}') @@ -1828,10 +2001,12 @@ def test_macro_create_with_unicode_numbered_arg(base_app): out, err = run_cmd(base_app, 'fake') assert "expects at least 1 argument(s)" in err[0] + def test_macro_create_with_missing_unicode_arg_nums(base_app): out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}') assert "Not all numbers between 1 and 3" in err[0] + def test_macro_that_resolves_into_comment(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} blah blah') @@ -1842,11 +2017,13 @@ def test_macro_that_resolves_into_comment(base_app): assert not out assert not err + def test_macro_list_invalid_macro(base_app): # Look up invalid macro out, err = run_cmd(base_app, 'macro list invalid') assert "Macro 'invalid' not found" in err[0] + def test_macro_delete(base_app): # Create an macro run_cmd(base_app, 'macro create fake run_pyscript') @@ -1855,18 +2032,22 @@ def test_macro_delete(base_app): out, err = run_cmd(base_app, 'macro delete fake') assert out == normalize("Macro 'fake' deleted") + def test_macro_delete_all(base_app): out, err = run_cmd(base_app, 'macro delete --all') assert out == normalize("All macros deleted") + def test_macro_delete_non_existing(base_app): out, err = run_cmd(base_app, 'macro delete fake') assert "Macro 'fake' does not exist" in err[0] + def test_macro_delete_no_name(base_app): out, err = run_cmd(base_app, 'macro delete') assert "Either --all or macro name(s)" in err[0] + def test_multiple_macros(base_app): macro1 = 'h1' macro2 = 'h2' @@ -1879,8 +2060,10 @@ def test_multiple_macros(base_app): verify_help_text(base_app, out2) assert len(out2) > len(out) + def test_nonexistent_macro(base_app): from cmd2.parsing import StatementParser + exception = None try: @@ -1890,6 +2073,7 @@ def test_nonexistent_macro(base_app): assert exception is not None + def test_perror_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1898,6 +2082,7 @@ def test_perror_style(base_app, capsys): out, err = capsys.readouterr() assert err == ansi.style_error(msg) + end + def test_perror_no_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1906,6 +2091,7 @@ def test_perror_no_style(base_app, capsys): out, err = capsys.readouterr() assert err == msg + end + def test_pwarning_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1914,6 +2100,7 @@ def test_pwarning_style(base_app, capsys): out, err = capsys.readouterr() assert err == ansi.style_warning(msg) + end + def test_pwarning_no_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1922,6 +2109,7 @@ def test_pwarning_no_style(base_app, capsys): out, err = capsys.readouterr() assert err == msg + end + def test_ppaged(outsim_app): msg = 'testing...' end = '\n' @@ -1929,18 +2117,21 @@ def test_ppaged(outsim_app): out = outsim_app.stdout.getvalue() assert out == msg + end + def test_ppaged_blank(outsim_app): msg = '' outsim_app.ppaged(msg) out = outsim_app.stdout.getvalue() assert not out + def test_ppaged_none(outsim_app): msg = None outsim_app.ppaged(msg) out = outsim_app.stdout.getvalue() assert not out + def test_ppaged_strips_ansi_when_redirecting(outsim_app): msg = 'testing...' end = '\n' @@ -1950,6 +2141,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app): out = outsim_app.stdout.getvalue() assert out == msg + end + def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): msg = 'testing...' end = '\n' @@ -1960,6 +2152,7 @@ def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): out = outsim_app.stdout.getvalue() assert out == colored_msg + end + # we override cmd.parseline() so we always get consistent # command parsing by parent methods we don't override # don't need to test all the parsing logic here, because @@ -1971,6 +2164,7 @@ def test_parseline_empty(base_app): assert not args assert not line + def test_parseline(base_app): statement = " command with 'partially completed quotes " command, args, line = base_app.parseline(statement) @@ -1986,6 +2180,7 @@ def test_onecmd_raw_str_continue(outsim_app): assert not stop verify_help_text(outsim_app, out) + def test_onecmd_raw_str_quit(outsim_app): line = "quit" stop = outsim_app.onecmd(line) @@ -1993,6 +2188,7 @@ def test_onecmd_raw_str_quit(outsim_app): assert stop assert out == '' + def test_onecmd_add_to_history(outsim_app): line = "help" saved_hist_len = len(outsim_app.history) @@ -2009,18 +2205,35 @@ def test_onecmd_add_to_history(outsim_app): new_hist_len = len(outsim_app.history) assert new_hist_len == saved_hist_len + def test_get_all_commands(base_app): # Verify that the base app has the expected commands commands = base_app.get_all_commands() - expected_commands = ['_relative_run_script', 'alias', 'edit', 'eof', 'help', 'history', 'macro', - 'py', 'quit', 'run_pyscript', 'run_script', 'set', 'shell', 'shortcuts'] + expected_commands = [ + '_relative_run_script', + 'alias', + 'edit', + 'eof', + 'help', + 'history', + 'macro', + 'py', + 'quit', + 'run_pyscript', + 'run_script', + 'set', + 'shell', + 'shortcuts', + ] assert commands == expected_commands + def test_get_help_topics(base_app): # Verify that the base app has no additional help_foo methods custom_help = base_app.get_help_topics() assert len(custom_help) == 0 + def test_get_help_topics_hidden(): # Verify get_help_topics() filters out hidden commands class TestApp(cmd2.Cmd): @@ -2039,6 +2252,7 @@ def test_get_help_topics_hidden(): app.hidden_commands.append('my_cmd') assert 'my_cmd' not in app.get_help_topics() + class ReplWithExitCode(cmd2.Cmd): """ Example cmd2 application where we can specify an exit code when existing.""" @@ -2068,12 +2282,14 @@ Usage: exit [exit_code] """Hook method executed once when the cmdloop() method is about to return.""" self.poutput('exiting with code: {}'.format(self.exit_code)) + @pytest.fixture def exit_code_repl(): app = ReplWithExitCode() app.stdout = utils.StdSim(app.stdout) return app + def test_exit_code_default(exit_code_repl): app = exit_code_repl app.use_rawinput = True @@ -2089,6 +2305,7 @@ def test_exit_code_default(exit_code_repl): out = app.stdout.getvalue() assert out == expected + def test_exit_code_nonzero(exit_code_repl): app = exit_code_repl app.use_rawinput = True @@ -2118,6 +2335,7 @@ class AnsiApp(cmd2.Cmd): # perror uses colors by default self.perror(args) + def test_ansi_pouterr_always_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_ALWAYS @@ -2140,6 +2358,7 @@ def test_ansi_pouterr_always_tty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_pouterr_always_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_ALWAYS @@ -2162,6 +2381,7 @@ def test_ansi_pouterr_always_notty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_terminal_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_TERMINAL @@ -2183,6 +2403,7 @@ def test_ansi_terminal_tty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_terminal_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_TERMINAL @@ -2197,6 +2418,7 @@ def test_ansi_terminal_notty(mocker, capsys): out, err = capsys.readouterr() assert out == err == 'oopsie\n' + def test_ansi_never_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_NEVER @@ -2211,6 +2433,7 @@ def test_ansi_never_tty(mocker, capsys): out, err = capsys.readouterr() assert out == err == 'oopsie\n' + def test_ansi_never_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_NEVER @@ -2228,6 +2451,7 @@ def test_ansi_never_notty(mocker, capsys): class DisableCommandsApp(cmd2.Cmd): """Class for disabling commands""" + category_name = "Test Category" def __init__(self, *args, **kwargs): @@ -2346,6 +2570,7 @@ def test_disable_and_enable_category(disable_commands_app): help_topics = disable_commands_app.get_help_topics() assert 'has_helper_funcs' in help_topics + def test_enable_enabled_command(disable_commands_app): # Test enabling a command that is not disabled saved_len = len(disable_commands_app.disabled_commands) @@ -2354,10 +2579,12 @@ def test_enable_enabled_command(disable_commands_app): # The number of disabled_commands should not have changed assert saved_len == len(disable_commands_app.disabled_commands) + def test_disable_fake_command(disable_commands_app): with pytest.raises(AttributeError): disable_commands_app.disable_command('fake', 'fake message') + def test_disable_command_twice(disable_commands_app): saved_len = len(disable_commands_app.disabled_commands) message_to_print = 'These commands are currently disabled' @@ -2373,6 +2600,7 @@ def test_disable_command_twice(disable_commands_app): new_len = len(disable_commands_app.disabled_commands) assert saved_len == new_len + def test_disabled_command_not_in_history(disable_commands_app): message_to_print = 'These commands are currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) @@ -2381,6 +2609,7 @@ def test_disabled_command_not_in_history(disable_commands_app): run_cmd(disable_commands_app, 'has_helper_funcs') assert saved_len == len(disable_commands_app.history) + def test_disabled_message_command_name(disable_commands_app): message_to_print = '{} is currently disabled'.format(COMMAND_NAME) disable_commands_app.disable_command('has_helper_funcs', message_to_print) diff --git a/tests/test_completion.py b/tests/test_completion.py index db243f48..785bb49d 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -28,44 +28,42 @@ from .conftest import complete_tester, normalize, run_cmd # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato', 'Cheese "Pizza"'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] -delimited_strs = \ - [ - '/home/user/file.txt', - '/home/user/file space.txt', - '/home/user/prog.c', - '/home/other user/maps', - '/home/other user/tests' - ] +delimited_strs = [ + '/home/user/file.txt', + '/home/user/file space.txt', + '/home/user/prog.c', + '/home/other user/maps', + '/home/other user/tests', +] # Dictionary used with flag based completion functions -flag_dict = \ - { - # Tab complete food items after -f and --food flag in command line - '-f': food_item_strs, - '--food': food_item_strs, - - # Tab complete sport items after -s and --sport flag in command line - '-s': sport_item_strs, - '--sport': sport_item_strs, - } +flag_dict = { + # Tab complete food items after -f and --food flag in command line + '-f': food_item_strs, + '--food': food_item_strs, + # Tab complete sport items after -s and --sport flag in command line + '-s': sport_item_strs, + '--sport': sport_item_strs, +} # Dictionary used with index based completion functions -index_dict = \ - { - 1: food_item_strs, # Tab complete food items at index 1 in command line - 2: sport_item_strs, # Tab complete sport items at index 2 in command line - } +index_dict = { + 1: food_item_strs, # Tab complete food items at index 1 in command line + 2: sport_item_strs, # Tab complete sport items at index 2 in command line +} class CompletionsExample(cmd2.Cmd): """ Example cmd2 application used to exercise tab completion tests """ + def __init__(self): cmd2.Cmd.__init__(self, multiline_commands=['test_multiline']) self.foo = 'bar' - self.add_settable(utils.Settable('foo', str, description="a settable param", - completer_method=CompletionsExample.complete_foo_val)) + self.add_settable( + utils.Settable('foo', str, description="a settable param", completer_method=CompletionsExample.complete_foo_val) + ) def do_test_basic(self, args): pass @@ -130,6 +128,7 @@ def test_cmd2_command_completion_single(cmd2_app): begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] + def test_complete_command_single(cmd2_app): text = 'he' line = text @@ -139,6 +138,7 @@ def test_complete_command_single(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['help '] + def test_complete_empty_arg(cmd2_app): text = '' line = 'help {}'.format(text) @@ -150,6 +150,7 @@ def test_complete_empty_arg(cmd2_app): assert first_match is not None and cmd2_app.completion_matches == expected + def test_complete_bogus_command(cmd2_app): text = '' line = 'fizbuzz {}'.format(text) @@ -160,6 +161,7 @@ def test_complete_bogus_command(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_complete_exception(cmd2_app, capsys): text = '' line = 'test_raise_exception {}'.format(text) @@ -172,6 +174,7 @@ def test_complete_exception(cmd2_app, capsys): assert first_match is None assert "IndexError" in err + def test_complete_macro(base_app, request): # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}') @@ -217,6 +220,7 @@ def test_cmd2_command_completion_multiple(cmd2_app): begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] + def test_cmd2_command_completion_nomatch(cmd2_app): text = 'fakecommand' line = text @@ -236,6 +240,7 @@ def test_cmd2_help_completion_single(cmd2_app): # It is at end of line, so extra space is present assert first_match is not None and cmd2_app.completion_matches == ['help '] + def test_cmd2_help_completion_multiple(cmd2_app): text = 'h' line = 'help {}'.format(text) @@ -274,9 +279,8 @@ def test_shell_command_completion_shortcut(cmd2_app): begidx = 0 first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - cmd2_app.completion_matches == expected and \ - cmd2_app.display_matches == expected_display + assert first_match is not None and cmd2_app.completion_matches == expected and cmd2_app.display_matches == expected_display + def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): if sys.platform == "win32": @@ -291,6 +295,7 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_multiple(cmd2_app): if sys.platform == "win32": text = 'c' @@ -306,6 +311,7 @@ def test_shell_command_completion_multiple(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and expected in cmd2_app.completion_matches + def test_shell_command_completion_nomatch(cmd2_app): text = 'zzzz' line = 'shell {}'.format(text) @@ -315,6 +321,7 @@ def test_shell_command_completion_nomatch(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): text = '' line = 'shell {}'.format(text) @@ -324,6 +331,7 @@ def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -336,6 +344,7 @@ def test_shell_command_completion_does_path_completion_when_after_command(cmd2_a first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == [text + '.py '] + def test_shell_commmand_complete_in_path(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -363,6 +372,7 @@ def test_path_completion_single_end(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx) == [text + '.py'] + def test_path_completion_multiple(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -376,6 +386,7 @@ def test_path_completion_multiple(cmd2_app, request): expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] assert matches == expected + def test_path_completion_nomatch(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -431,6 +442,7 @@ def test_path_completion_no_text(cmd2_app): assert completions_no_text == completions_cwd assert completions_cwd + def test_path_completion_no_path(cmd2_app): # Run path complete with search text that isn't preceded by a path. This should use CWD as the path. text = 's' @@ -471,6 +483,7 @@ def test_path_completion_cwd_is_root_dir(cmd2_app): # Restore CWD os.chdir(cwd) + def test_path_completion_doesnt_match_wildcards(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -483,10 +496,14 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request): # Currently path completion doesn't accept wildcards, so will always return empty results assert cmd2_app.path_complete(text, line, begidx, endidx) == [] -@pytest.mark.skipif(sys.platform == 'win32', reason="getpass.getuser() does not work on Windows in AppVeyor because " - "no user name environment variables are set") + +@pytest.mark.skipif( + sys.platform == 'win32', + reason="getpass.getuser() does not work on Windows in AppVeyor because " "no user name environment variables are set", +) def test_path_completion_complete_user(cmd2_app): import getpass + user = getpass.getuser() text = '~{}'.format(user) @@ -498,6 +515,7 @@ def test_path_completion_complete_user(cmd2_app): expected = text + os.path.sep assert expected in completions + def test_path_completion_user_path_expansion(cmd2_app): # Run path with a tilde and a slash if sys.platform.startswith('win'): @@ -510,8 +528,7 @@ def test_path_completion_user_path_expansion(cmd2_app): line = 'shell {} {}'.format(cmd, text) endidx = len(line) begidx = endidx - len(text) - completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, - begidx, endidx)] + completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] # Run path complete on the user's home directory text = os.path.expanduser('~') + os.path.sep @@ -522,6 +539,7 @@ def test_path_completion_user_path_expansion(cmd2_app): assert completions_tilde_slash == completions_home + def test_path_completion_directories_only(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -535,6 +553,7 @@ def test_path_completion_directories_only(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) == expected + def test_basic_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) @@ -543,6 +562,7 @@ def test_basic_completion_single(cmd2_app): assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] + def test_basic_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) @@ -552,6 +572,7 @@ def test_basic_completion_multiple(cmd2_app): 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): text = 'q' line = 'list_food -f {}'.format(text) @@ -560,6 +581,7 @@ def test_basic_completion_nomatch(cmd2_app): assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == [] + def test_delimiter_completion(cmd2_app): text = '/home/' line = 'run_script {}'.format(text) @@ -574,6 +596,7 @@ def test_delimiter_completion(cmd2_app): assert display_list == ['other user', 'user'] + def test_flag_based_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) @@ -582,6 +605,7 @@ def test_flag_based_completion_single(cmd2_app): assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza'] + def test_flag_based_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) @@ -591,6 +615,7 @@ def test_flag_based_completion_multiple(cmd2_app): matches = sorted(cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict)) assert matches == sorted(food_item_strs) + def test_flag_based_completion_nomatch(cmd2_app): text = 'q' line = 'list_food -f {}'.format(text) @@ -599,6 +624,7 @@ def test_flag_based_completion_nomatch(cmd2_app): assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [] + def test_flag_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -608,8 +634,10 @@ def test_flag_based_default_completer(cmd2_app, request): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.flag_based_complete(text, line, begidx, endidx, - flag_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] + assert cmd2_app.flag_based_complete(text, line, begidx, endidx, 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__) @@ -621,8 +649,7 @@ def test_flag_based_callable_completer(cmd2_app, request): begidx = endidx - len(text) flag_dict['-o'] = cmd2_app.path_complete - assert cmd2_app.flag_based_complete(text, line, begidx, endidx, - flag_dict) == [text + 'onftest.py'] + assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [text + 'onftest.py'] def test_index_based_completion_single(cmd2_app): @@ -633,6 +660,7 @@ def test_index_based_completion_single(cmd2_app): assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == ['Football'] + def test_index_based_completion_multiple(cmd2_app): text = '' line = 'command Pizza {}'.format(text) @@ -642,6 +670,7 @@ def test_index_based_completion_multiple(cmd2_app): matches = sorted(cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict)) assert matches == sorted(sport_item_strs) + def test_index_based_completion_nomatch(cmd2_app): text = 'q' line = 'command {}'.format(text) @@ -649,6 +678,7 @@ def test_index_based_completion_nomatch(cmd2_app): begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [] + def test_index_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -658,8 +688,10 @@ def test_index_based_default_completer(cmd2_app, request): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.index_based_complete(text, line, begidx, endidx, - index_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] + assert cmd2_app.index_based_complete(text, line, begidx, endidx, 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__) @@ -687,6 +719,7 @@ def test_tokens_for_completion_quoted(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_unclosed_quote(cmd2_app): text = 'Pi' line = 'list_food "{}'.format(text) @@ -700,6 +733,7 @@ def test_tokens_for_completion_unclosed_quote(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_punctuation(cmd2_app): """Test that redirectors and terminators are word delimiters""" text = 'file' @@ -714,6 +748,7 @@ def test_tokens_for_completion_punctuation(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_quoted_punctuation(cmd2_app): """Test that quoted punctuation characters are not word delimiters""" text = '>file' @@ -728,6 +763,7 @@ def test_tokens_for_completion_quoted_punctuation(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_add_opening_quote_basic_no_text(cmd2_app): text = '' line = 'test_basic {}'.format(text) @@ -736,8 +772,8 @@ def test_add_opening_quote_basic_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, - key=cmd2_app.default_sort_key) + assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, key=cmd2_app.default_sort_key) + def test_add_opening_quote_basic_nothing_added(cmd2_app): text = 'P' @@ -748,6 +784,7 @@ def test_add_opening_quote_basic_nothing_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['Pizza', 'Potato'] + def test_add_opening_quote_basic_quote_added(cmd2_app): text = 'Ha' line = 'test_basic {}'.format(text) @@ -758,6 +795,7 @@ def test_add_opening_quote_basic_quote_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_basic_single_quote_added(cmd2_app): text = 'Ch' line = 'test_basic {}'.format(text) @@ -768,6 +806,7 @@ def test_add_opening_quote_basic_single_quote_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches text = 'Ham' @@ -779,6 +818,7 @@ def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_delimited_no_text(cmd2_app): text = '' line = 'test_delimited {}'.format(text) @@ -787,8 +827,8 @@ def test_add_opening_quote_delimited_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, - key=cmd2_app.default_sort_key) + assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key) + def test_add_opening_quote_delimited_nothing_added(cmd2_app): text = '/ho' @@ -800,9 +840,12 @@ def test_add_opening_quote_delimited_nothing_added(cmd2_app): expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - cmd2_app.completion_matches == expected_matches and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and cmd2_app.completion_matches == expected_matches + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_quote_added(cmd2_app): text = '/home/user/fi' @@ -814,9 +857,12 @@ def test_add_opening_quote_delimited_quote_added(cmd2_app): expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches @@ -829,9 +875,12 @@ def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): # This test when a space appears before the part of the string that is the display match @@ -844,9 +893,12 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): expected_display = ['maps', 'tests'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_no_completer(cmd2_app): text = '' @@ -858,6 +910,7 @@ def test_no_completer(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_quote_as_command(cmd2_app): text = '' line = '" {}'.format(text) @@ -898,36 +951,39 @@ def test_complete_multiline_on_multiple_lines(cmd2_app): # Used by redirect_complete tests class RedirCompType(enum.Enum): - SHELL_CMD = 1, - PATH = 2, - DEFAULT = 3, + SHELL_CMD = (1,) + PATH = (2,) + DEFAULT = (3,) NONE = 4 -@pytest.mark.parametrize('line, comp_type', [ - ('fake', RedirCompType.DEFAULT), - ('fake arg', RedirCompType.DEFAULT), - ('fake |', RedirCompType.SHELL_CMD), - ('fake | grep', RedirCompType.PATH), - ('fake | grep arg', RedirCompType.PATH), - ('fake | grep >', RedirCompType.PATH), - ('fake | grep > >', RedirCompType.NONE), - ('fake | grep > file', RedirCompType.NONE), - ('fake | grep > file >', RedirCompType.NONE), - ('fake | grep > file |', RedirCompType.SHELL_CMD), - ('fake | grep > file | grep', RedirCompType.PATH), - ('fake | |', RedirCompType.NONE), - ('fake | >', RedirCompType.NONE), - ('fake >', RedirCompType.PATH), - ('fake >>', RedirCompType.PATH), - ('fake > >', RedirCompType.NONE), - ('fake > |', RedirCompType.SHELL_CMD), - ('fake >> file |', RedirCompType.SHELL_CMD), - ('fake >> file | grep', RedirCompType.PATH), - ('fake > file', RedirCompType.NONE), - ('fake > file >', RedirCompType.NONE), - ('fake > file >>', RedirCompType.NONE), -]) +@pytest.mark.parametrize( + 'line, comp_type', + [ + ('fake', RedirCompType.DEFAULT), + ('fake arg', RedirCompType.DEFAULT), + ('fake |', RedirCompType.SHELL_CMD), + ('fake | grep', RedirCompType.PATH), + ('fake | grep arg', RedirCompType.PATH), + ('fake | grep >', RedirCompType.PATH), + ('fake | grep > >', RedirCompType.NONE), + ('fake | grep > file', RedirCompType.NONE), + ('fake | grep > file >', RedirCompType.NONE), + ('fake | grep > file |', RedirCompType.SHELL_CMD), + ('fake | grep > file | grep', RedirCompType.PATH), + ('fake | |', RedirCompType.NONE), + ('fake | >', RedirCompType.NONE), + ('fake >', RedirCompType.PATH), + ('fake >>', RedirCompType.PATH), + ('fake > >', RedirCompType.NONE), + ('fake > |', RedirCompType.SHELL_CMD), + ('fake >> file |', RedirCompType.SHELL_CMD), + ('fake >> file | grep', RedirCompType.PATH), + ('fake > file', RedirCompType.NONE), + ('fake > file >', RedirCompType.NONE), + ('fake > file >>', RedirCompType.NONE), + ], +) def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): # Test both cases of allow_redirection cmd2_app.allow_redirection = True @@ -974,6 +1030,7 @@ def test_complete_set_value(cmd2_app): assert first_match == "SUCCESS " assert cmd2_app.completion_hint == "Hint:\n value a settable param\n" + def test_complete_set_value_invalid_settable(cmd2_app, capsys): text = '' line = 'set fake {}'.format(text) @@ -986,12 +1043,14 @@ def test_complete_set_value_invalid_settable(cmd2_app, capsys): out, err = capsys.readouterr() assert "fake is not a settable parameter" in out + @pytest.fixture def sc_app(): c = SubcommandsExample() c.stdout = utils.StdSim(c.stdout) return c + def test_cmd2_subcommand_completion_single_end(sc_app): text = 'f' line = 'base {}'.format(text) @@ -1003,6 +1062,7 @@ def test_cmd2_subcommand_completion_single_end(sc_app): # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['foo '] + def test_cmd2_subcommand_completion_multiple(sc_app): text = '' line = 'base {}'.format(text) @@ -1012,6 +1072,7 @@ def test_cmd2_subcommand_completion_multiple(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport'] + def test_cmd2_subcommand_completion_nomatch(sc_app): text = 'z' line = 'base {}'.format(text) @@ -1033,6 +1094,7 @@ def test_help_subcommand_completion_single(sc_app): # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['base '] + def test_help_subcommand_completion_multiple(sc_app): text = '' line = 'help base {}'.format(text) @@ -1052,6 +1114,7 @@ def test_help_subcommand_completion_nomatch(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is None + def test_subcommand_tab_completion(sc_app): # This makes sure the correct completer for the sport subcommand is called text = 'Foot' @@ -1085,9 +1148,8 @@ def test_subcommand_tab_completion_space_in_text(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) - assert first_match is not None and \ - sc_app.completion_matches == ['Ball" '] and \ - sc_app.display_matches == ['Space Ball'] + assert first_match is not None and sc_app.completion_matches == ['Ball" '] and sc_app.display_matches == ['Space Ball'] + #################################################### @@ -1207,6 +1269,7 @@ def test_help_subcommand_completion_multiple_scu(scu_app): 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_help_subcommand_completion_with_flags_before_command(scu_app): text = '' line = 'help -h -v base {}'.format(text) @@ -1216,6 +1279,7 @@ def test_help_subcommand_completion_with_flags_before_command(scu_app): 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_complete_help_subcommands_with_blank_command(scu_app): text = '' line = 'help "" {}'.format(text) @@ -1269,6 +1333,4 @@ def test_subcommand_tab_completion_space_in_text_scu(scu_app): 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'] + assert first_match is not None and scu_app.completion_matches == ['Ball" '] and scu_app.display_matches == ['Space Ball'] diff --git a/tests/test_history.py b/tests/test_history.py index 6fa16ad8..cba6f3ce 100755 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -9,6 +9,7 @@ import tempfile import pytest import cmd2 + # Python 3.5 had some regressions in the unitest.mock module, so use # 3rd party mock if available from cmd2.parsing import StatementParser @@ -21,44 +22,57 @@ except ImportError: from unittest import mock - # # readline tests # def test_readline_remove_history_item(base_app): from cmd2.rl_utils import readline + assert readline.get_current_history_length() == 0 readline.add_history('this is a test') assert readline.get_current_history_length() == 1 readline.remove_history_item(0) assert readline.get_current_history_length() == 0 + # # test History() class # @pytest.fixture def hist(): - from cmd2.parsing import Statement from cmd2.cmd2 import History, HistoryItem - h = History([HistoryItem(Statement('', raw='first'), 1), - HistoryItem(Statement('', raw='second'), 2), - HistoryItem(Statement('', raw='third'), 3), - HistoryItem(Statement('', raw='fourth'),4)]) + from cmd2.parsing import Statement + + h = History( + [ + HistoryItem(Statement('', raw='first'), 1), + HistoryItem(Statement('', raw='second'), 2), + HistoryItem(Statement('', raw='third'), 3), + HistoryItem(Statement('', raw='fourth'), 4), + ] + ) return h + @pytest.fixture def persisted_hist(): - from cmd2.parsing import Statement from cmd2.cmd2 import History, HistoryItem - h = History([HistoryItem(Statement('', raw='first'), 1), - HistoryItem(Statement('', raw='second'), 2), - HistoryItem(Statement('', raw='third'), 3), - HistoryItem(Statement('', raw='fourth'),4)]) + from cmd2.parsing import Statement + + h = History( + [ + HistoryItem(Statement('', raw='first'), 1), + HistoryItem(Statement('', raw='second'), 2), + HistoryItem(Statement('', raw='third'), 3), + HistoryItem(Statement('', raw='fourth'), 4), + ] + ) h.start_session() h.append(Statement('', raw='fifth')) h.append(Statement('', raw='sixth')) return h + def test_history_class_span(hist): for tryit in ['*', ':', '-', 'all', 'ALL']: assert hist.span(tryit) == hist @@ -135,6 +149,7 @@ def test_history_class_span(hist): with pytest.raises(ValueError): hist.span(tryit) + def test_persisted_history_span(persisted_hist): for tryit in ['*', ':', '-', 'all', 'ALL']: assert persisted_hist.span(tryit, include_persisted=True) == persisted_hist @@ -191,6 +206,7 @@ def test_persisted_history_span(persisted_hist): with pytest.raises(ValueError): persisted_hist.span(tryit) + def test_history_class_get(hist): assert hist.get('1').statement.raw == 'first' assert hist.get(3).statement.raw == 'third' @@ -217,6 +233,7 @@ def test_history_class_get(hist): with pytest.raises(TypeError): hist.get(None) + def test_history_str_search(hist): items = hist.str_search('ir') assert len(items) == 2 @@ -227,6 +244,7 @@ def test_history_str_search(hist): assert len(items) == 1 assert items[0].statement.raw == 'fourth' + def test_history_regex_search(hist): items = hist.regex_search('/i.*d/') assert len(items) == 1 @@ -236,52 +254,59 @@ def test_history_regex_search(hist): assert len(items) == 1 assert items[0].statement.raw == 'second' + def test_history_max_length_zero(hist): hist.truncate(0) assert len(hist) == 0 + def test_history_max_length_negative(hist): hist.truncate(-1) assert len(hist) == 0 + def test_history_max_length(hist): hist.truncate(2) assert len(hist) == 2 assert hist.get(1).statement.raw == 'third' assert hist.get(2).statement.raw == 'fourth' + # # test HistoryItem() # @pytest.fixture def histitem(): - from cmd2.parsing import Statement from cmd2.history import HistoryItem - statement = Statement('history', - raw='help history', - command='help', - arg_list=['history'], - ) + from cmd2.parsing import Statement + + statement = Statement('history', raw='help history', command='help', arg_list=['history'],) histitem = HistoryItem(statement, 1) return histitem + @pytest.fixture def parser(): from cmd2.parsing import StatementParser + parser = StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], - aliases={'helpalias': 'help', - '42': 'theanswer', - 'l': '!ls -al', - 'anothermultiline': 'multiline', - 'fake': 'run_pyscript'}, - shortcuts={'?': 'help', '!': 'shell'} + aliases={ + 'helpalias': 'help', + '42': 'theanswer', + 'l': '!ls -al', + 'anothermultiline': 'multiline', + 'fake': 'run_pyscript', + }, + shortcuts={'?': 'help', '!': 'shell'}, ) return parser + def test_multiline_histitem(parser): from cmd2.history import History + line = 'multiline foo\nbar\n\n' statement = parser.parse(line) history = History() @@ -292,8 +317,10 @@ def test_multiline_histitem(parser): pr_lines = hist_item.pr().splitlines() assert pr_lines[0].endswith('multiline foo bar') + def test_multiline_histitem_verbose(parser): from cmd2.history import History + line = 'multiline foo\nbar\n\n' statement = parser.parse(line) history = History() @@ -305,14 +332,12 @@ def test_multiline_histitem_verbose(parser): assert pr_lines[0].endswith('multiline foo') assert pr_lines[1] == 'bar' + def test_history_item_instantiate(): - from cmd2.parsing import Statement from cmd2.history import HistoryItem - statement = Statement('history', - raw='help history', - command='help', - arg_list=['history'], - ) + from cmd2.parsing import Statement + + statement = Statement('history', raw='help history', command='help', arg_list=['history'],) with pytest.raises(TypeError): _ = HistoryItem() with pytest.raises(TypeError): @@ -322,11 +347,13 @@ def test_history_item_instantiate(): with pytest.raises(TypeError): _ = HistoryItem(statement=statement, idx='hi') + def test_history_item_properties(histitem): assert histitem.raw == 'help history' assert histitem.expanded == 'help history' assert str(histitem) == 'help history' + # # test history command # @@ -334,113 +361,144 @@ def test_base_history(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'history he') - expected = normalize(""" + expected = normalize( + """ 1 help -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'history sh') - expected = normalize(""" + expected = normalize( + """ 2 shortcuts -""") +""" + ) assert out == expected + def test_history_script_format(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history -s') - expected = normalize(""" + expected = normalize( + """ help shortcuts -""") +""" + ) assert out == expected + def test_history_with_string_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history help') - expected = normalize(""" + expected = normalize( + """ 1 help 3 help history -""") +""" + ) assert out == expected + def test_history_expanded_with_string_argument(base_app): run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, 'sc') out, err = run_cmd(base_app, 'history -v shortcuts') - expected = normalize(""" + expected = normalize( + """ 1 alias create sc shortcuts 4 sc 4x shortcuts -""") +""" + ) assert out == expected + def test_history_expanded_with_regex_argument(base_app): run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, 'sc') out, err = run_cmd(base_app, 'history -v /sh.*cuts/') - expected = normalize(""" + expected = normalize( + """ 1 alias create sc shortcuts 4 sc 4x shortcuts -""") +""" + ) assert out == expected + def test_history_with_integer_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history 1') - expected = normalize(""" + expected = normalize( + """ 1 help -""") +""" + ) assert out == expected + def test_history_with_integer_span(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history 1..2') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected + def test_history_with_span_start(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history 2:') - expected = normalize(""" + expected = normalize( + """ 2 shortcuts 3 help history -""") +""" + ) assert out == expected + def test_history_with_span_end(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history :2') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected + def test_history_with_span_index_error(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'help history') @@ -448,6 +506,7 @@ def test_history_with_span_index_error(base_app): with pytest.raises(ValueError): base_app.onecmd('history "hal :"') + def test_history_output_file(): app = cmd2.Cmd(multiline_commands=['alias']) run_cmd(app, 'help') @@ -463,6 +522,7 @@ def test_history_output_file(): content = normalize(f.read()) assert content == expected + def test_history_bad_output_file(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') @@ -474,6 +534,7 @@ def test_history_bad_output_file(base_app): assert not out assert "Error saving" in err[0] + def test_history_edit(monkeypatch): app = cmd2.Cmd(multiline_commands=['alias']) @@ -499,6 +560,7 @@ def test_history_edit(monkeypatch): edit_mock.assert_called_once() run_script_mock.assert_called_once() + def test_history_run_all_commands(base_app): # make sure we refuse to run all commands as a default run_cmd(base_app, 'shortcuts') @@ -509,11 +571,13 @@ def test_history_run_all_commands(base_app): # then we should have a list of shortcuts in our output assert out == [] + def test_history_run_one_command(base_app): out1, err1 = run_cmd(base_app, 'help') out2, err2 = run_cmd(base_app, 'history -r 1') assert out1 == out2 + def test_history_clear(hist_file): # Add commands to history app = cmd2.Cmd(persistent_history_file=hist_file) @@ -532,6 +596,7 @@ def test_history_clear(hist_file): assert out == [] assert not os.path.exists(hist_file) + def test_history_verbose_with_other_options(base_app): # make sure -v shows a usage error if any other options are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x'] @@ -541,6 +606,7 @@ def test_history_verbose_with_other_options(base_app): assert out[0] == '-v can not be used with any other options' assert out[1].startswith('Usage:') + def test_history_verbose(base_app): # validate function of -v option run_cmd(base_app, 'alias create s shortcuts') @@ -549,6 +615,7 @@ def test_history_verbose(base_app): assert len(out) == 3 # TODO test for basic formatting once we figure it out + def test_history_script_with_invalid_options(base_app): # make sure -s shows a usage error if -c, -r, -e, -o, or -t are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] @@ -558,6 +625,7 @@ def test_history_script_with_invalid_options(base_app): assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') + def test_history_script(base_app): cmds = ['alias create s shortcuts', 's'] for cmd in cmds: @@ -565,6 +633,7 @@ def test_history_script(base_app): out, err = run_cmd(base_app, 'history -s') assert out == cmds + def test_history_expanded_with_invalid_options(base_app): # make sure -x shows a usage error if -c, -r, -e, -o, or -t are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] @@ -574,6 +643,7 @@ def test_history_expanded_with_invalid_options(base_app): assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') + def test_history_expanded(base_app): # validate function of -x option cmds = ['alias create s shortcuts', 's'] @@ -583,6 +653,7 @@ def test_history_expanded(base_app): expected = [' 1 alias create s shortcuts', ' 2 shortcuts'] assert out == expected + def test_history_script_expanded(base_app): # validate function of -s -x options together cmds = ['alias create s shortcuts', 's'] @@ -592,10 +663,12 @@ def test_history_script_expanded(base_app): expected = ['alias create s shortcuts', 'shortcuts'] assert out == expected + def test_base_help_history(base_app): out, err = run_cmd(base_app, 'help history') assert out == normalize(HELP_HISTORY) + def test_exclude_from_history(base_app, monkeypatch): # Run history command run_cmd(base_app, 'history') @@ -612,6 +685,7 @@ def test_exclude_from_history(base_app, monkeypatch): expected = normalize(""" 1 help""") assert out == expected + # # test history initialization # @@ -626,6 +700,7 @@ def hist_file(): except FileNotFoundError: pass + def test_history_file_is_directory(capsys): with tempfile.TemporaryDirectory() as test_dir: # Create a new cmd2 app @@ -633,6 +708,7 @@ def test_history_file_is_directory(capsys): _, err = capsys.readouterr() assert 'is a directory' in err + def test_history_can_create_directory(mocker): # Mock out atexit.register so the persistent file doesn't written when this function # exists because we will be deleting the directory it needs to go to. @@ -654,6 +730,7 @@ def test_history_can_create_directory(mocker): # Cleanup os.rmdir(hist_file_dir) + def test_history_cannot_create_directory(mocker, capsys): mock_open = mocker.patch('os.makedirs') mock_open.side_effect = OSError @@ -663,6 +740,7 @@ def test_history_cannot_create_directory(mocker, capsys): _, err = capsys.readouterr() assert 'Error creating persistent history file directory' in err + def test_history_file_permission_error(mocker, capsys): mock_open = mocker.patch('builtins.open') mock_open.side_effect = PermissionError @@ -672,6 +750,7 @@ def test_history_file_permission_error(mocker, capsys): assert not out assert 'Can not read' in err + def test_history_file_conversion_no_truncate_on_init(hist_file, capsys): # make sure we don't truncate the plain text history file on init # it shouldn't get converted to pickle format until we save history @@ -688,14 +767,15 @@ def test_history_file_conversion_no_truncate_on_init(hist_file, capsys): # history should be initialized, but the file on disk should # still be plain text with open(hist_file, 'r') as hfobj: - histlist= hfobj.readlines() + histlist = hfobj.readlines() assert len(histlist) == 3 # history.get() is overridden to be one based, not zero based - assert histlist[0]== 'help\n' + assert histlist[0] == 'help\n' assert histlist[1] == 'alias\n' assert histlist[2] == 'alias create s shortcuts\n' + def test_history_populates_readline(hist_file): # - create a cmd2 with persistent history app = cmd2.Cmd(persistent_history_file=hist_file) @@ -718,11 +798,13 @@ def test_history_populates_readline(hist_file): # so we check to make sure that cmd2 populated the readline history # using the same rules from cmd2.rl_utils import readline + assert readline.get_current_history_length() == 3 assert readline.get_history_item(1) == 'help' assert readline.get_history_item(2) == 'shortcuts' assert readline.get_history_item(3) == 'alias' + # # test cmd2's ability to write out history on exit # we are testing the _persist_history_on_exit() method, and @@ -737,6 +819,7 @@ def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys): assert not out assert not err + def test_persist_history_permission_error(hist_file, mocker, capsys): app = cmd2.Cmd(persistent_history_file=hist_file) run_cmd(app, 'help') diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 2eccec7c..379ee2c7 100755 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -16,12 +16,14 @@ def parser(): parser = StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], - aliases={'helpalias': 'help', - '42': 'theanswer', - 'l': '!ls -al', - 'anothermultiline': 'multiline', - 'fake': 'run_pyscript'}, - shortcuts={'?': 'help', '!': 'shell'} + aliases={ + 'helpalias': 'help', + '42': 'theanswer', + 'l': '!ls -al', + 'anothermultiline': 'multiline', + 'fake': 'run_pyscript', + }, + shortcuts={'?': 'help', '!': 'shell'}, ) return parser @@ -68,34 +70,40 @@ def test_parse_empty_string_default(default_parser): assert statement.argv == statement.arg_list -@pytest.mark.parametrize('line,tokens', [ - ('command', ['command']), - (constants.COMMENT_CHAR + 'comment', []), - ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), - ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('termbare& > /tmp/output', ['termbare&', '>', '/tmp/output']), - ('help|less', ['help', '|', 'less']), -]) +@pytest.mark.parametrize( + 'line,tokens', + [ + ('command', ['command']), + (constants.COMMENT_CHAR + 'comment', []), + ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), + ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('termbare& > /tmp/output', ['termbare&', '>', '/tmp/output']), + ('help|less', ['help', '|', 'less']), + ], +) def test_tokenize_default(default_parser, line, tokens): tokens_to_test = default_parser.tokenize(line) assert tokens_to_test == tokens -@pytest.mark.parametrize('line,tokens', [ - ('command', ['command']), - ('# comment', []), - ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), - ('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']), - ('l', ['shell', 'ls', '-al']), - ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('help|less', ['help', '|', 'less']), - ('l|less', ['shell', 'ls', '-al', '|', 'less']), -]) +@pytest.mark.parametrize( + 'line,tokens', + [ + ('command', ['command']), + ('# comment', []), + ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), + ('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']), + ('l', ['shell', 'ls', '-al']), + ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('help|less', ['help', '|', 'less']), + ('l|less', ['shell', 'ls', '-al', '|', 'less']), + ], +) def test_tokenize(parser, line, tokens): tokens_to_test = parser.tokenize(line) assert tokens_to_test == tokens @@ -106,22 +114,16 @@ def test_tokenize_unclosed_quotes(parser): _ = parser.tokenize('command with "unclosed quotes') -@pytest.mark.parametrize('tokens,command,args', [ - ([], '', ''), - (['command'], 'command', ''), - (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2') -]) +@pytest.mark.parametrize( + 'tokens,command,args', [([], '', ''), (['command'], 'command', ''), (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')] +) def test_command_and_args(parser, tokens, command, args): (parsed_command, parsed_args) = parser._command_and_args(tokens) assert command == parsed_command assert args == parsed_args -@pytest.mark.parametrize('line', [ - 'plainword', - '"one word"', - "'one word'", -]) +@pytest.mark.parametrize('line', ['plainword', '"one word"', "'one word'",]) def test_parse_single_word(parser, line): statement = parser.parse(line) assert statement.command == line @@ -139,12 +141,9 @@ def test_parse_single_word(parser, line): assert statement.command_and_args == line -@pytest.mark.parametrize('line,terminator', [ - ('termbare;', ';'), - ('termbare ;', ';'), - ('termbare&', '&'), - ('termbare &', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', [('termbare;', ';'), ('termbare ;', ';'), ('termbare&', '&'), ('termbare &', '&'),] +) def test_parse_word_plus_terminator(parser, line, terminator): statement = parser.parse(line) assert statement.command == 'termbare' @@ -155,12 +154,10 @@ def test_parse_word_plus_terminator(parser, line, terminator): assert statement.expanded_command_line == statement.command + statement.terminator -@pytest.mark.parametrize('line,terminator', [ - ('termbare; suffx', ';'), - ('termbare ;suffx', ';'), - ('termbare& suffx', '&'), - ('termbare &suffx', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [('termbare; suffx', ';'), ('termbare ;suffx', ';'), ('termbare& suffx', '&'), ('termbare &suffx', '&'),], +) def test_parse_suffix_after_terminator(parser, line, terminator): statement = parser.parse(line) assert statement.command == 'termbare' @@ -224,10 +221,7 @@ def test_parse_embedded_comment_char(parser): assert statement.arg_list == statement.argv[1:] -@pytest.mark.parametrize('line', [ - 'simple | piped', - 'simple|piped', -]) +@pytest.mark.parametrize('line', ['simple | piped', 'simple|piped',]) def test_parse_simple_pipe(parser, line): statement = parser.parse(line) assert statement.command == 'simple' @@ -263,12 +257,9 @@ def test_parse_complex_pipe(parser): assert statement.pipe_to == 'piped' -@pytest.mark.parametrize('line,output', [ - ('help > out.txt', '>'), - ('help>out.txt', '>'), - ('help >> out.txt', '>>'), - ('help>>out.txt', '>>'), -]) +@pytest.mark.parametrize( + 'line,output', [('help > out.txt', '>'), ('help>out.txt', '>'), ('help >> out.txt', '>>'), ('help>>out.txt', '>>'),] +) def test_parse_redirect(parser, line, output): statement = parser.parse(line) assert statement.command == 'help' @@ -279,10 +270,7 @@ def test_parse_redirect(parser, line, output): assert statement.expanded_command_line == statement.command + ' ' + statement.output + ' ' + statement.output_to -@pytest.mark.parametrize('dest', [ - 'afile.txt', # without dashes - 'python-cmd2/afile.txt', # with dashes in path -]) +@pytest.mark.parametrize('dest', ['afile.txt', 'python-cmd2/afile.txt',]) # without dashes # with dashes in path def test_parse_redirect_with_args(parser, dest): line = 'output into > {}'.format(dest) statement = parser.parse(line) @@ -482,16 +470,19 @@ def test_parse_redirect_inside_terminator(parser): assert statement.terminator == ';' -@pytest.mark.parametrize('line,terminator', [ - ('multiline with | inside;', ';'), - ('multiline with | inside ;', ';'), - ('multiline with | inside;;;', ';'), - ('multiline with | inside;; ;;', ';'), - ('multiline with | inside&', '&'), - ('multiline with | inside &;', '&'), - ('multiline with | inside&&;', '&'), - ('multiline with | inside &; &;', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline with | inside;', ';'), + ('multiline with | inside ;', ';'), + ('multiline with | inside;;;', ';'), + ('multiline with | inside;; ;;', ';'), + ('multiline with | inside&', '&'), + ('multiline with | inside &;', '&'), + ('multiline with | inside&&;', '&'), + ('multiline with | inside &; &;', '&'), + ], +) def test_parse_multiple_terminators(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -527,13 +518,16 @@ def test_parse_basic_multiline_command(parser): assert statement.terminator == '\n' -@pytest.mark.parametrize('line,terminator', [ - ('multiline has > inside;', ';'), - ('multiline has > inside;;;', ';'), - ('multiline has > inside;; ;;', ';'), - ('multiline has > inside &', '&'), - ('multiline has > inside & &', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline has > inside;', ';'), + ('multiline has > inside;;;', ';'), + ('multiline has > inside;; ;;', ';'), + ('multiline has > inside &', '&'), + ('multiline has > inside & &', '&'), + ], +) def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -556,14 +550,17 @@ def test_parse_multiline_terminated_by_empty_line(parser): assert statement.terminator == '\n' -@pytest.mark.parametrize('line,terminator', [ - ('multiline command "with\nembedded newline";', ';'), - ('multiline command "with\nembedded newline";;;', ';'), - ('multiline command "with\nembedded newline";; ;;', ';'), - ('multiline command "with\nembedded newline" &', '&'), - ('multiline command "with\nembedded newline" & &', '&'), - ('multiline command "with\nembedded newline"\n\n', '\n'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline command "with\nembedded newline";', ';'), + ('multiline command "with\nembedded newline";;;', ';'), + ('multiline command "with\nembedded newline";; ;;', ';'), + ('multiline command "with\nembedded newline" &', '&'), + ('multiline command "with\nembedded newline" & &', '&'), + ('multiline command "with\nembedded newline"\n\n', '\n'), + ], +) def test_parse_multiline_with_embedded_newline(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -633,15 +630,18 @@ def test_empty_statement_raises_exception(): app._complete_statement(' ') -@pytest.mark.parametrize('line,command,args', [ - ('helpalias', 'help', ''), - ('helpalias mycommand', 'help', 'mycommand'), - ('42', 'theanswer', ''), - ('42 arg1 arg2', 'theanswer', 'arg1 arg2'), - ('!ls', 'shell', 'ls'), - ('!ls -al /tmp', 'shell', 'ls -al /tmp'), - ('l', 'shell', 'ls -al') -]) +@pytest.mark.parametrize( + 'line,command,args', + [ + ('helpalias', 'help', ''), + ('helpalias mycommand', 'help', 'mycommand'), + ('42', 'theanswer', ''), + ('42 arg1 arg2', 'theanswer', 'arg1 arg2'), + ('!ls', 'shell', 'ls'), + ('!ls -al /tmp', 'shell', 'ls -al /tmp'), + ('l', 'shell', 'ls -al'), + ], +) def test_parse_alias_and_shortcut_expansion(parser, line, command, args): statement = parser.parse(line) assert statement.command == command @@ -659,12 +659,10 @@ def test_parse_alias_on_multiline_command(parser): assert statement.terminator == '' -@pytest.mark.parametrize('line,output', [ - ('helpalias > out.txt', '>'), - ('helpalias>out.txt', '>'), - ('helpalias >> out.txt', '>>'), - ('helpalias>>out.txt', '>>'), -]) +@pytest.mark.parametrize( + 'line,output', + [('helpalias > out.txt', '>'), ('helpalias>out.txt', '>'), ('helpalias >> out.txt', '>>'), ('helpalias>>out.txt', '>>'),], +) def test_parse_alias_redirection(parser, line, output): statement = parser.parse(line) assert statement.command == 'help' @@ -674,10 +672,7 @@ def test_parse_alias_redirection(parser, line, output): assert statement.output_to == 'out.txt' -@pytest.mark.parametrize('line', [ - 'helpalias | less', - 'helpalias|less', -]) +@pytest.mark.parametrize('line', ['helpalias | less', 'helpalias|less',]) def test_parse_alias_pipe(parser, line): statement = parser.parse(line) assert statement.command == 'help' @@ -686,14 +681,9 @@ def test_parse_alias_pipe(parser, line): assert statement.pipe_to == 'less' -@pytest.mark.parametrize('line', [ - 'helpalias;', - 'helpalias;;', - 'helpalias;; ;', - 'helpalias ;', - 'helpalias ; ;', - 'helpalias ;; ;', -]) +@pytest.mark.parametrize( + 'line', ['helpalias;', 'helpalias;;', 'helpalias;; ;', 'helpalias ;', 'helpalias ; ;', 'helpalias ;; ;',] +) def test_parse_alias_terminator_no_whitespace(parser, line): statement = parser.parse(line) assert statement.command == 'help' @@ -789,16 +779,19 @@ def test_parse_command_only_quoted_args(parser): assert statement.output_to == '' -@pytest.mark.parametrize('line,args', [ - ('helpalias > out.txt', '> out.txt'), - ('helpalias>out.txt', '>out.txt'), - ('helpalias >> out.txt', '>> out.txt'), - ('helpalias>>out.txt', '>>out.txt'), - ('help|less', '|less'), - ('helpalias;', ';'), - ('help ;;', ';;'), - ('help; ;;', '; ;;'), -]) +@pytest.mark.parametrize( + 'line,args', + [ + ('helpalias > out.txt', '> out.txt'), + ('helpalias>out.txt', '>out.txt'), + ('helpalias >> out.txt', '>> out.txt'), + ('helpalias>>out.txt', '>>out.txt'), + ('help|less', '|less'), + ('helpalias;', ';'), + ('help ;;', ';;'), + ('help; ;;', '; ;;'), + ], +) def test_parse_command_only_specialchars(parser, line, args): statement = parser.parse_command_only(line) assert statement == args @@ -814,19 +807,7 @@ def test_parse_command_only_specialchars(parser, line, args): assert statement.output_to == '' -@pytest.mark.parametrize('line', [ - '', - ';', - ';;', - ';; ;', - '&', - '& &', - ' && &', - '>', - "'", - '"', - '|', -]) +@pytest.mark.parametrize('line', ['', ';', ';;', ';; ;', '&', '& &', ' && &', '>', "'", '"', '|',]) def test_parse_command_only_empty(parser, line): statement = parser.parse_command_only(line) assert statement == '' @@ -940,6 +921,7 @@ def test_is_valid_command_valid(parser): def test_macro_normal_arg_pattern(): # This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side from cmd2.parsing import MacroArg + pattern = MacroArg.macro_normal_arg_pattern # Valid strings @@ -991,6 +973,7 @@ def test_macro_normal_arg_pattern(): def test_macro_escaped_arg_pattern(): # This pattern matches digits surrounded by 2 or more braces on both sides from cmd2.parsing import MacroArg + pattern = MacroArg.macro_escaped_arg_pattern # Valid strings diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 279f2f79..6f2b2f32 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -18,9 +18,9 @@ except ImportError: from unittest import mock - class Plugin: """A mixin class for testing hook registration and calling""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.reset_counters() @@ -222,14 +222,16 @@ class Plugin: self.called_cmdfinalization += 1 raise ValueError - def cmdfinalization_hook_system_exit(self, data: cmd2.plugin.CommandFinalizationData) -> \ - cmd2.plugin.CommandFinalizationData: + def cmdfinalization_hook_system_exit( + self, data: cmd2.plugin.CommandFinalizationData + ) -> cmd2.plugin.CommandFinalizationData: """A command finalization hook which raises a SystemExit""" self.called_cmdfinalization += 1 raise SystemExit - def cmdfinalization_hook_keyboard_interrupt(self, data: cmd2.plugin.CommandFinalizationData) -> \ - cmd2.plugin.CommandFinalizationData: + def cmdfinalization_hook_keyboard_interrupt( + self, data: cmd2.plugin.CommandFinalizationData + ) -> cmd2.plugin.CommandFinalizationData: """A command finalization hook which raises a KeyboardInterrupt""" self.called_cmdfinalization += 1 raise KeyboardInterrupt @@ -238,8 +240,9 @@ class Plugin: """A command finalization hook with no parameters.""" pass - def cmdfinalization_hook_too_many_parameters(self, one: plugin.CommandFinalizationData, two: str) -> \ - plugin.CommandFinalizationData: + def cmdfinalization_hook_too_many_parameters( + self, one: plugin.CommandFinalizationData, two: str + ) -> plugin.CommandFinalizationData: """A command finalization hook with too many parameters.""" return one @@ -262,6 +265,7 @@ class Plugin: class PluggedApp(Plugin, cmd2.Cmd): """A sample app with a plugin mixed in""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -281,6 +285,7 @@ class PluggedApp(Plugin, cmd2.Cmd): """Repeat back the arguments""" self.poutput(namespace.cmd2_statement.get()) + ### # # test pre and postloop hooks @@ -291,11 +296,13 @@ def test_register_preloop_hook_too_many_parameters(): with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_too_many_parameters) + def test_register_preloop_hook_with_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation) + def test_preloop_hook(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -309,6 +316,7 @@ def test_preloop_hook(capsys): assert out == 'one\nhello\n' assert not err + def test_preloop_hooks(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -323,16 +331,19 @@ def test_preloop_hooks(capsys): assert out == 'one\ntwo\nhello\n' assert not err + def test_register_postloop_hook_too_many_parameters(): app = PluggedApp() with pytest.raises(TypeError): app.register_postloop_hook(app.prepost_hook_too_many_parameters) + def test_register_postloop_hook_with_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation) + def test_postloop_hook(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -346,6 +357,7 @@ def test_postloop_hook(capsys): assert out == 'hello\none\n' assert not err + def test_postloop_hooks(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -360,6 +372,7 @@ def test_postloop_hooks(capsys): assert out == 'hello\none\ntwo\n' assert not err + ### # # test preparse hook @@ -374,6 +387,7 @@ def test_preparse(capsys): assert not err assert app.called_preparse == 1 + ### # # test postparsing hooks @@ -384,26 +398,31 @@ def test_postparsing_hook_too_many_parameters(): with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_too_many_parameters) + def test_postparsing_hook_undeclared_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_annotation) + def test_postparsing_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_parameter_annotation) + def test_postparsing_hook_undeclared_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_return_annotation) + def test_postparsing_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_return_annotation) + def test_postparsing_hook(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -429,6 +448,7 @@ def test_postparsing_hook(capsys): assert not err assert app.called_postparsing == 2 + def test_postparsing_hook_stop_first(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_stop) @@ -443,6 +463,7 @@ def test_postparsing_hook_stop_first(capsys): assert app.called_postparsing == 1 assert stop + def test_postparsing_hook_stop_second(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) @@ -464,6 +485,7 @@ def test_postparsing_hook_stop_second(capsys): assert app.called_postparsing == 2 assert stop + def test_postparsing_hook_emptystatement_first(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_emptystatement) @@ -484,6 +506,7 @@ def test_postparsing_hook_emptystatement_first(capsys): assert not err assert app.called_postparsing == 1 + def test_postparsing_hook_emptystatement_second(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) @@ -514,6 +537,7 @@ def test_postparsing_hook_emptystatement_second(capsys): assert not err assert app.called_postparsing == 2 + def test_postparsing_hook_exception(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_exception) @@ -534,6 +558,7 @@ def test_postparsing_hook_exception(capsys): assert err assert app.called_postparsing == 1 + ### # # test precmd hooks @@ -546,26 +571,31 @@ def test_register_precmd_hook_parameter_count(): with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_too_many_parameters) + def test_register_precmd_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_parameter_annotation) + def test_register_precmd_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_parameter_annotation) + def test_register_precmd_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_return_annotation) + def test_register_precmd_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_return_annotation) + def test_precmd_hook(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -594,6 +624,7 @@ def test_precmd_hook(capsys): # with two hooks registered, we should get precmd() and both hooks assert app.called_precmd == 3 + def test_precmd_hook_emptystatement_first(capsys): app = PluggedApp() app.register_precmd_hook(app.precmd_hook_emptystatement) @@ -619,6 +650,7 @@ def test_precmd_hook_emptystatement_first(capsys): # called assert app.called_precmd == 1 + def test_precmd_hook_emptystatement_second(capsys): app = PluggedApp() app.register_precmd_hook(app.precmd_hook) @@ -655,6 +687,7 @@ def test_precmd_hook_emptystatement_second(capsys): # if a registered hook throws an exception, precmd() is never called assert app.called_precmd == 2 + ### # # test postcmd hooks @@ -667,26 +700,31 @@ def test_register_postcmd_hook_parameter_count(): with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_too_many_parameters) + def test_register_postcmd_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_parameter_annotation) + def test_register_postcmd_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_parameter_annotation) + def test_register_postcmd_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_return_annotation) + def test_register_postcmd_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_return_annotation) + def test_postcmd(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -715,6 +753,7 @@ def test_postcmd(capsys): # with two hooks registered, we should get precmd() and both hooks assert app.called_postcmd == 3 + def test_postcmd_exception_first(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook_exception) @@ -741,6 +780,7 @@ def test_postcmd_exception_first(capsys): # called assert app.called_postcmd == 1 + def test_postcmd_exception_second(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) @@ -766,6 +806,7 @@ def test_postcmd_exception_second(capsys): # the exception assert app.called_postcmd == 2 + ## # # command finalization @@ -778,26 +819,31 @@ def test_register_cmdfinalization_hook_parameter_count(): with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_too_many_parameters) + def test_register_cmdfinalization_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_parameter_annotation) + def test_register_cmdfinalization_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_parameter_annotation) + def test_register_cmdfinalization_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_return_annotation) + def test_register_cmdfinalization_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_return_annotation) + def test_cmdfinalization(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -822,6 +868,7 @@ def test_cmdfinalization(capsys): assert not err assert app.called_cmdfinalization == 2 + def test_cmdfinalization_stop_first(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop) @@ -833,6 +880,7 @@ def test_cmdfinalization_stop_first(capsys): assert app.called_cmdfinalization == 2 assert stop + def test_cmdfinalization_stop_second(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook) @@ -844,6 +892,7 @@ def test_cmdfinalization_stop_second(capsys): assert app.called_cmdfinalization == 2 assert stop + def test_cmdfinalization_hook_exception(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_exception) @@ -864,6 +913,7 @@ def test_cmdfinalization_hook_exception(capsys): assert err assert app.called_cmdfinalization == 1 + def test_cmdfinalization_hook_system_exit(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit) @@ -871,6 +921,7 @@ def test_cmdfinalization_hook_system_exit(capsys): assert stop assert app.called_cmdfinalization == 1 + def test_cmdfinalization_hook_keyboard_interrupt(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt) @@ -893,6 +944,7 @@ def test_cmdfinalization_hook_keyboard_interrupt(capsys): assert stop assert app.called_cmdfinalization == 1 + def test_skip_postcmd_hooks(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) @@ -905,6 +957,7 @@ def test_skip_postcmd_hooks(capsys): assert app.called_postcmd == 0 assert app.called_cmdfinalization == 1 + def test_cmd2_argparse_exception(capsys): """ Verify Cmd2ArgparseErrors raised after calling a command prevent postcmd events from diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index 8cfd8578..e0b2b3c5 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -20,11 +20,13 @@ except ImportError: HOOK_OUTPUT = "TEST_OUTPUT" + def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData: """A cmdfinalization_hook hook which requests application exit""" print(HOOK_OUTPUT) return data + def test_run_pyscript(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') @@ -33,6 +35,7 @@ def test_run_pyscript(base_app, request): out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert expected in out + def test_run_pyscript_recursive_not_allowed(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'recursive.py') @@ -41,11 +44,13 @@ def test_run_pyscript_recursive_not_allowed(base_app, request): out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert err[0] == expected + def test_run_pyscript_with_nonexist_file(base_app): python_script = 'does_not_exist.py' out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert "Error reading script file" in err[0] + def test_run_pyscript_with_non_python_file(base_app, request): m = mock.MagicMock(name='input', return_value='2') builtins.input = m @@ -55,6 +60,7 @@ def test_run_pyscript_with_non_python_file(base_app, request): out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename)) assert "does not have a .py extension" in err[0] + @pytest.mark.parametrize('python_script', odd_file_names) def test_run_pyscript_with_odd_file_names(base_app, python_script): """ @@ -69,6 +75,7 @@ def test_run_pyscript_with_odd_file_names(base_app, python_script): err = ''.join(err) assert "Error reading script file '{}'".format(python_script) in err + def test_run_pyscript_with_exception(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'raises_exception.py') @@ -76,10 +83,12 @@ def test_run_pyscript_with_exception(base_app, request): assert err[0].startswith('Traceback') assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1] + def test_run_pyscript_requires_an_argument(base_app): out, err = run_cmd(base_app, "run_pyscript") assert "the following arguments are required: script_path" in err[1] + def test_run_pyscript_help(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'help.py') @@ -87,6 +96,7 @@ def test_run_pyscript_help(base_app, request): out2, err2 = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) assert out1 and out1 == out2 + def test_run_pyscript_dir(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py') @@ -94,6 +104,7 @@ def test_run_pyscript_dir(base_app, request): out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) assert out[0] == "['cmd_echo']" + def test_run_pyscript_stdout_capture(base_app, request): base_app.register_cmdfinalization_hook(cmdfinalization_hook) test_dir = os.path.dirname(request.module.__file__) @@ -103,6 +114,7 @@ def test_run_pyscript_stdout_capture(base_app, request): assert out[0] == "PASSED" assert out[1] == "PASSED" + def test_run_pyscript_stop(base_app, request): # Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop test_dir = os.path.dirname(request.module.__file__) @@ -117,6 +129,7 @@ def test_run_pyscript_stop(base_app, request): stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script)) assert stop + def test_run_pyscript_environment(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'environment.py') @@ -124,7 +137,8 @@ def test_run_pyscript_environment(base_app, request): assert out[0] == "PASSED" -def test_run_pyscript_app_echo(base_app, request): + +def test_run_pyscript_app_echo(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'echo.py') out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index 0d2edfb2..c83aee2c 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -46,15 +46,30 @@ def test_column_creation(): def test_column_alignment(): - column_1 = Column("Col 1", width=10, - header_horiz_align=HorizontalAlignment.LEFT, header_vert_align=VerticalAlignment.TOP, - data_horiz_align=HorizontalAlignment.LEFT, data_vert_align=VerticalAlignment.TOP) - column_2 = Column("Col 2", width=10, - header_horiz_align=HorizontalAlignment.CENTER, header_vert_align=VerticalAlignment.MIDDLE, - data_horiz_align=HorizontalAlignment.CENTER, data_vert_align=VerticalAlignment.MIDDLE) - column_3 = Column("Col 3", width=10, - header_horiz_align=HorizontalAlignment.RIGHT, header_vert_align=VerticalAlignment.BOTTOM, - data_horiz_align=HorizontalAlignment.RIGHT, data_vert_align=VerticalAlignment.BOTTOM) + column_1 = Column( + "Col 1", + width=10, + header_horiz_align=HorizontalAlignment.LEFT, + header_vert_align=VerticalAlignment.TOP, + data_horiz_align=HorizontalAlignment.LEFT, + data_vert_align=VerticalAlignment.TOP, + ) + column_2 = Column( + "Col 2", + width=10, + header_horiz_align=HorizontalAlignment.CENTER, + header_vert_align=VerticalAlignment.MIDDLE, + data_horiz_align=HorizontalAlignment.CENTER, + data_vert_align=VerticalAlignment.MIDDLE, + ) + column_3 = Column( + "Col 3", + width=10, + header_horiz_align=HorizontalAlignment.RIGHT, + header_vert_align=VerticalAlignment.BOTTOM, + data_horiz_align=HorizontalAlignment.RIGHT, + data_vert_align=VerticalAlignment.BOTTOM, + ) column_4 = Column("Three\nline\nheader", width=10) columns = [column_1, column_2, column_3, column_4] @@ -68,16 +83,20 @@ def test_column_alignment(): # Create a header row header = tc.generate_row() - assert header == ('Col 1 Three \n' - ' Col 2 line \n' - ' Col 3 header ') + assert header == ( + 'Col 1 Three \n' + ' Col 2 line \n' + ' Col 3 header ' + ) # Create a data row row_data = ["Val 1", "Val 2", "Val 3", "Three\nline\ndata"] row = tc.generate_row(row_data=row_data) - assert row == ('Val 1 Three \n' - ' Val 2 line \n' - ' Val 3 data ') + assert row == ( + 'Val 1 Three \n' + ' Val 2 line \n' + ' Val 3 data ' + ) def test_wrap_text(): @@ -87,19 +106,12 @@ def test_wrap_text(): # Test normal wrapping row_data = ['Some text to wrap\nA new line that will wrap\nNot wrap\n 1 2 3'] row = tc.generate_row(row_data=row_data) - assert row == ('Some text \n' - 'to wrap \n' - 'A new line\n' - 'that will \n' - 'wrap \n' - 'Not wrap \n' - ' 1 2 3 ') + assert row == ('Some text \n' 'to wrap \n' 'A new line\n' 'that will \n' 'wrap \n' 'Not wrap \n' ' 1 2 3 ') # Test preserving a multiple space sequence across a line break row_data = ['First last one'] row = tc.generate_row(row_data=row_data) - assert row == ('First \n' - ' last one ') + assert row == ('First \n' ' last one ') def test_wrap_text_max_lines(): @@ -109,32 +121,27 @@ def test_wrap_text_max_lines(): # Test not needing to truncate the final line row_data = ['First line last line'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last line ') + assert row == ('First line\n' 'last line ') # Test having to truncate the last word because it's too long for the final line row_data = ['First line last lineextratext'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last line…') + assert row == ('First line\n' 'last line…') # Test having to truncate the last word because it fits the final line but there is more text not being included row_data = ['First line thistxtfit extra'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'thistxtfi…') + assert row == ('First line\n' 'thistxtfi…') # Test having to truncate the last word because it fits the final line but there are more lines not being included row_data = ['First line thistxtfit\nextra'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'thistxtfi…') + assert row == ('First line\n' 'thistxtfi…') # Test having space left on the final line and adding an ellipsis because there are more lines not being included row_data = ['First line last line\nextra line'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last line…') + assert row == ('First line\n' 'last line…') def test_wrap_long_word(): @@ -147,8 +154,7 @@ def test_wrap_long_word(): # Test header row header = tc.generate_row() - assert header == ('LongColumn \n' - 'Name Col 2 ') + assert header == ('LongColumn \n' 'Name Col 2 ') # Test data row row_data = list() @@ -160,9 +166,22 @@ def test_wrap_long_word(): row_data.append("Word LongerThan10") row = tc.generate_row(row_data=row_data) - expected = (ansi.RESET_ALL + ansi.fg.green + "LongerThan" + ansi.RESET_ALL + " Word \n" - + ansi.RESET_ALL + ansi.fg.green + "10" + ansi.fg.reset + ansi.RESET_ALL + ' ' + ansi.RESET_ALL + ' LongerThan\n' - ' 10 ') + expected = ( + ansi.RESET_ALL + + ansi.fg.green + + "LongerThan" + + ansi.RESET_ALL + + " Word \n" + + ansi.RESET_ALL + + ansi.fg.green + + "10" + + ansi.fg.reset + + ansi.RESET_ALL + + ' ' + + ansi.RESET_ALL + + ' LongerThan\n' + ' 10 ' + ) assert row == expected @@ -191,8 +210,7 @@ def test_wrap_long_word_max_data_lines(): row_data.append("A LongerThan10RunsOverLast") row = tc.generate_row(row_data=row_data) - assert row == ('LongerThan LongerThan LongerThan A LongerT…\n' - '10FitsLast 10FitsLas… 10RunsOve… ') + assert row == ('LongerThan LongerThan LongerThan A LongerT…\n' '10FitsLast 10FitsLas… 10RunsOve… ') def test_wrap_long_char_wider_than_max_width(): @@ -235,8 +253,7 @@ def test_tabs(): column_2 = Column("Col 2") tc = TableCreator([column_1, column_2], tab_width=2) - row = tc.generate_row(fill_char='\t', pre_line='\t', - inter_cell='\t', post_line='\t') + row = tc.generate_row(fill_char='\t', pre_line='\t', inter_cell='\t', post_line='\t') assert row == ' Col 1 Col 2 ' @@ -252,67 +269,74 @@ def test_simple_table_creation(): st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '----------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Custom divider st = SimpleTable([column_1, column_2], divider_char='─') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '──────────────────────────────────\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '──────────────────────────────────\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # No divider st = SimpleTable([column_1, column_2], divider_char=None) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ' + ) # No row spacing st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, row_spacing=0) - assert table == ('Col 1 Col 2 \n' - '----------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # No header st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, include_header=False) - assert table == ('Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ('Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Wide custom divider (divider needs no padding) st = SimpleTable([column_1, column_2], divider_char='深') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '深深深深深深深深深深深深深深深深深\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '深深深深深深深深深深深深深深深深深\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Wide custom divider (divider needs padding) column_2 = Column("Col 2", width=17) st = SimpleTable([column_1, column_2], divider_char='深') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '深深深深深深深深深深深深深深深深深 \n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '深深深深深深深深深深深深深深深深深 \n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Invalid divider character with pytest.raises(TypeError) as excinfo: @@ -363,44 +387,52 @@ def test_bordered_table_creation(): # Default options bt = BorderedTable([column_1, column_2]) table = bt.generate_table(row_data) - assert table == ('╔═════════════════╤═════════════════╗\n' - '║ Col 1 │ Col 2 ║\n' - '╠═════════════════╪═════════════════╣\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '╟─────────────────┼─────────────────╢\n' - '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═════════════════╧═════════════════╝') + assert table == ( + '╔═════════════════╤═════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═════════════════╪═════════════════╣\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '╟─────────────────┼─────────────────╢\n' + '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' + '╚═════════════════╧═════════════════╝' + ) # No column borders bt = BorderedTable([column_1, column_2], column_borders=False) table = bt.generate_table(row_data) - assert table == ('╔══════════════════════════════════╗\n' - '║ Col 1 Col 2 ║\n' - '╠══════════════════════════════════╣\n' - '║ Col 1 Row 1 Col 2 Row 1 ║\n' - '╟──────────────────────────────────╢\n' - '║ Col 1 Row 2 Col 2 Row 2 ║\n' - '╚══════════════════════════════════╝') + assert table == ( + '╔══════════════════════════════════╗\n' + '║ Col 1 Col 2 ║\n' + '╠══════════════════════════════════╣\n' + '║ Col 1 Row 1 Col 2 Row 1 ║\n' + '╟──────────────────────────────────╢\n' + '║ Col 1 Row 2 Col 2 Row 2 ║\n' + '╚══════════════════════════════════╝' + ) # No header bt = BorderedTable([column_1, column_2]) table = bt.generate_table(row_data, include_header=False) - assert table == ('╔═════════════════╤═════════════════╗\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '╟─────────────────┼─────────────────╢\n' - '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═════════════════╧═════════════════╝') + assert table == ( + '╔═════════════════╤═════════════════╗\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '╟─────────────────┼─────────────────╢\n' + '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' + '╚═════════════════╧═════════════════╝' + ) # Non-default padding bt = BorderedTable([column_1, column_2], padding=2) table = bt.generate_table(row_data) - assert table == ('╔═══════════════════╤═══════════════════╗\n' - '║ Col 1 │ Col 2 ║\n' - '╠═══════════════════╪═══════════════════╣\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '╟───────────────────┼───────────────────╢\n' - '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═══════════════════╧═══════════════════╝') + assert table == ( + '╔═══════════════════╤═══════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═══════════════════╪═══════════════════╣\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '╟───────────────────┼───────────────────╢\n' + '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' + '╚═══════════════════╧═══════════════════╝' + ) # Invalid padding with pytest.raises(ValueError) as excinfo: @@ -457,50 +489,60 @@ def test_alternating_table_creation(): # Default options at = AlternatingTable([column_1, column_2]) table = at.generate_table(row_data) - assert table == ('╔═════════════════╤═════════════════╗\n' - '║ Col 1 │ Col 2 ║\n' - '╠═════════════════╪═════════════════╣\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝') + assert table == ( + '╔═════════════════╤═════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═════════════════╪═════════════════╣\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝' + ) # Other bg colors at = AlternatingTable([column_1, column_2], bg_odd=ansi.bg.bright_blue, bg_even=ansi.bg.green) table = at.generate_table(row_data) - assert table == ('╔═════════════════╤═════════════════╗\n' - '║ Col 1 │ Col 2 ║\n' - '╠═════════════════╪═════════════════╣\n' - '\x1b[104m║ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m │ \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m ║\x1b[49m\n' - '\x1b[42m║ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m │ \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝') + assert table == ( + '╔═════════════════╤═════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═════════════════╪═════════════════╣\n' + '\x1b[104m║ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m │ \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m ║\x1b[49m\n' + '\x1b[42m║ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m │ \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝' + ) # No column borders at = AlternatingTable([column_1, column_2], column_borders=False) table = at.generate_table(row_data) - assert table == ('╔══════════════════════════════════╗\n' - '║ Col 1 Col 2 ║\n' - '╠══════════════════════════════════╣\n' - '║ Col 1 Row 1 Col 2 Row 1 ║\n' - '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚══════════════════════════════════╝') + assert table == ( + '╔══════════════════════════════════╗\n' + '║ Col 1 Col 2 ║\n' + '╠══════════════════════════════════╣\n' + '║ Col 1 Row 1 Col 2 Row 1 ║\n' + '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚══════════════════════════════════╝' + ) # No header at = AlternatingTable([column_1, column_2]) table = at.generate_table(row_data, include_header=False) - assert table == ('╔═════════════════╤═════════════════╗\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝') + assert table == ( + '╔═════════════════╤═════════════════╗\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚═════════════════╧═════════════════╝' + ) # Non-default padding at = AlternatingTable([column_1, column_2], padding=2) table = at.generate_table(row_data) - assert table == ('╔═══════════════════╤═══════════════════╗\n' - '║ Col 1 │ Col 2 ║\n' - '╠═══════════════════╪═══════════════════╣\n' - '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' - '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═══════════════════╧═══════════════════╝') + assert table == ( + '╔═══════════════════╤═══════════════════╗\n' + '║ Col 1 │ Col 2 ║\n' + '╠═══════════════════╪═══════════════════╣\n' + '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' + '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' + '╚═══════════════════╧═══════════════════╝' + ) # Invalid padding with pytest.raises(ValueError) as excinfo: diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 55d60e18..5b19e11e 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -46,7 +46,7 @@ class CmdLineApp(cmd2.Cmd): """Repeats what you tell me to.""" arg = ' '.join(arg) if opts.piglatin: - arg = '%s%say' % (arg[1:], arg[0]) + arg = '{}{}ay'.format(arg[1:], arg[0]) if opts.shout: arg = arg.upper() repetitions = opts.repeat or 1 @@ -66,16 +66,16 @@ class CmdLineApp(cmd2.Cmd): def do_mumble(self, opts, arg): """Mumbles what you tell me to.""" repetitions = opts.repeat or 1 - #arg = arg.split() + # arg = arg.split() for _ in range(min(repetitions, self.maxrepeats)): output = [] - if random.random() < .33: + if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) for word in arg: - if random.random() < .40: + if random.random() < 0.40: output.append(random.choice(self.MUMBLES)) output.append(word) - if random.random() < .25: + if random.random() < 0.25: output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) @@ -98,23 +98,27 @@ def test_commands_at_invocation(): out = app.stdout.getvalue() assert out == expected -@pytest.mark.parametrize('filename,feedback_to_output', [ - ('bol_eol.txt', False), - ('characterclass.txt', False), - ('dotstar.txt', False), - ('extension_notation.txt', False), - ('from_cmdloop.txt', True), - ('multiline_no_regex.txt', False), - ('multiline_regex.txt', False), - ('no_output.txt', False), - ('no_output_last.txt', False), - ('regex_set.txt', False), - ('singleslash.txt', False), - ('slashes_escaped.txt', False), - ('slashslash.txt', False), - ('spaces.txt', False), - ('word_boundaries.txt', False), - ]) + +@pytest.mark.parametrize( + 'filename,feedback_to_output', + [ + ('bol_eol.txt', False), + ('characterclass.txt', False), + ('dotstar.txt', False), + ('extension_notation.txt', False), + ('from_cmdloop.txt', True), + ('multiline_no_regex.txt', False), + ('multiline_regex.txt', False), + ('no_output.txt', False), + ('no_output_last.txt', False), + ('regex_set.txt', False), + ('singleslash.txt', False), + ('slashes_escaped.txt', False), + ('slashslash.txt', False), + ('spaces.txt', False), + ('word_boundaries.txt', False), + ], +) def test_transcript(request, capsys, filename, feedback_to_output): # Get location of the transcript test_dir = os.path.dirname(request.module.__file__) @@ -141,6 +145,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert err.startswith(expected_start) assert err.endswith(expected_end) + def test_history_transcript(): app = CmdLineApp() app.stdout = StdSim(app.stdout) @@ -168,6 +173,7 @@ this is a \/multiline\/ command assert xscript == expected + def test_history_transcript_bad_filename(): app = CmdLineApp() app.stdout = StdSim(app.stdout) @@ -247,27 +253,30 @@ def test_generate_transcript_stop(capsys): assert err.startswith("Interrupting this command\nCommand 2 triggered a stop") -@pytest.mark.parametrize('expected, transformed', [ - # strings with zero or one slash or with escaped slashes means no regular - # expression present, so the result should just be what re.escape returns. - # we don't use static strings in these tests because re.escape behaves - # differently in python 3.7 than in prior versions - ( 'text with no slashes', re.escape('text with no slashes') ), - ( 'specials .*', re.escape('specials .*') ), - ( 'use 2/3 cup', re.escape('use 2/3 cup') ), - ( '/tmp is nice', re.escape('/tmp is nice') ), - ( 'slash at end/', re.escape('slash at end/') ), - # escaped slashes - ( r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/' ) ), - # regexes - ( '/.*/', '.*' ), - ( 'specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+' ), - ( r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more') ), - ( r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /') ), - # inception: slashes in our regex. backslashed on input, bare on output - ( r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /') ), - ( r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff') ), - ]) +@pytest.mark.parametrize( + 'expected, transformed', + [ + # strings with zero or one slash or with escaped slashes means no regular + # expression present, so the result should just be what re.escape returns. + # we don't use static strings in these tests because re.escape behaves + # differently in python 3.7 than in prior versions + ('text with no slashes', re.escape('text with no slashes')), + ('specials .*', re.escape('specials .*')), + ('use 2/3 cup', re.escape('use 2/3 cup')), + ('/tmp is nice', re.escape('/tmp is nice')), + ('slash at end/', re.escape('slash at end/')), + # escaped slashes + (r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/')), + # regexes + ('/.*/', '.*'), + ('specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+'), + (r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more')), + (r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /')), + # inception: slashes in our regex. backslashed on input, bare on output + (r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /')), + (r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff')), + ], +) def test_parse_transcript_expected(expected, transformed): app = CmdLineApp() diff --git a/tests/test_utils.py b/tests/test_utils.py index 5336ccfd..383ea6d7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -26,37 +26,44 @@ def test_strip_quotes_no_quotes(): stripped = cu.strip_quotes(base_str) assert base_str == stripped + def test_strip_quotes_with_quotes(): base_str = '"' + HELLO_WORLD + '"' stripped = cu.strip_quotes(base_str) assert stripped == HELLO_WORLD + def test_remove_duplicates_no_duplicates(): no_dups = [5, 4, 3, 2, 1] assert cu.remove_duplicates(no_dups) == no_dups + def test_remove_duplicates_with_duplicates(): duplicates = [1, 1, 2, 3, 9, 9, 7, 8] assert cu.remove_duplicates(duplicates) == [1, 2, 3, 9, 7, 8] + def test_unicode_normalization(): s1 = 'café' s2 = 'cafe\u0301' assert s1 != s2 assert cu.norm_fold(s1) == cu.norm_fold(s2) + def test_unicode_casefold(): micro = 'µ' micro_cf = micro.casefold() assert micro != micro_cf assert cu.norm_fold(micro) == cu.norm_fold(micro_cf) + def test_alphabetical_sort(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'cafeteria'] assert cu.alphabetical_sort(my_list) == ['A', 'cafeteria', 'café', 'micro', 'unity', 'µ'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.alphabetical_sort(my_list) == ['a1', 'A11', 'A2', 'a22', 'a3'] + def test_try_int_or_force_to_lower_case(): str1 = '17' assert cu.try_int_or_force_to_lower_case(str1) == 17 @@ -67,6 +74,7 @@ def test_try_int_or_force_to_lower_case(): str1 = '' assert cu.try_int_or_force_to_lower_case(str1) == '' + def test_natural_keys(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] my_list.sort(key=cu.natural_keys) @@ -75,24 +83,28 @@ def test_natural_keys(): my_list.sort(key=cu.natural_keys) assert my_list == ['a1', 'A2', 'a3', 'A11', 'a22'] + def test_natural_sort(): my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] assert cu.natural_sort(my_list) == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.natural_sort(my_list) == ['a1', 'A2', 'a3', 'A11', 'a22'] + def test_is_quoted_short(): my_str = '' assert not cu.is_quoted(my_str) your_str = '"' assert not cu.is_quoted(your_str) + def test_is_quoted_yes(): my_str = '"This is a test"' assert cu.is_quoted(my_str) your_str = "'of the emergengy broadcast system'" assert cu.is_quoted(your_str) + def test_is_quoted_no(): my_str = '"This is a test' assert not cu.is_quoted(my_str) @@ -101,6 +113,7 @@ def test_is_quoted_no(): simple_str = "hello world" assert not cu.is_quoted(simple_str) + def test_quote_string(): my_str = "Hello World" assert cu.quote_string(my_str) == '"' + my_str + '"' @@ -111,12 +124,14 @@ def test_quote_string(): my_str = '"Hello World"' assert cu.quote_string(my_str) == "'" + my_str + "'" + def test_quote_string_if_needed_yes(): my_str = "Hello World" assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"' your_str = '"foo" bar' assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'" + def test_quote_string_if_needed_no(): my_str = "HelloWorld" assert cu.quote_string_if_needed(my_str) == my_str @@ -135,22 +150,26 @@ def test_stdsim_write_str(stdout_sim): stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str + def test_stdsim_write_bytes(stdout_sim): b_str = b'Hello World' with pytest.raises(TypeError): stdout_sim.write(b_str) + def test_stdsim_buffer_write_bytes(stdout_sim): b_str = b'Hello World' stdout_sim.buffer.write(b_str) assert stdout_sim.getvalue() == b_str.decode() assert stdout_sim.getbytes() == b_str + def test_stdsim_buffer_write_str(stdout_sim): my_str = 'Hello World' with pytest.raises(TypeError): stdout_sim.buffer.write(my_str) + def test_stdsim_read(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) @@ -176,6 +195,7 @@ def test_stdsim_read_bytes(stdout_sim): assert stdout_sim.readbytes() == b_str assert stdout_sim.getbytes() == b'' + def test_stdsim_clear(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) @@ -183,6 +203,7 @@ def test_stdsim_clear(stdout_sim): stdout_sim.clear() assert stdout_sim.getvalue() == '' + def test_stdsim_getattr_exist(stdout_sim): # Here the StdSim getattr is allowing us to access methods within StdSim my_str = 'Hello World' @@ -190,10 +211,12 @@ def test_stdsim_getattr_exist(stdout_sim): val_func = getattr(stdout_sim, 'getvalue') assert val_func() == my_str + def test_stdsim_getattr_noexist(stdout_sim): # Here the StdSim getattr is allowing us to access methods defined by the inner stream assert not stdout_sim.isatty() + def test_stdsim_pause_storage(stdout_sim): # Test pausing storage for string data my_str = 'Hello World' @@ -217,11 +240,13 @@ def test_stdsim_pause_storage(stdout_sim): stdout_sim.buffer.write(b_str) assert stdout_sim.getbytes() == b'' + def test_stdsim_line_buffering(base_app): # This exercises the case of writing binary data that contains new lines/carriage returns to a StdSim # when line buffering is on. The output should immediately be flushed to the underlying stream. import os import tempfile + file = tempfile.NamedTemporaryFile(mode='wt') file.line_buffering = True @@ -256,6 +281,7 @@ def pr_none(): pr = cu.ProcReader(proc, None, None) return pr + def test_proc_reader_send_sigint(pr_none): assert pr_none._proc.poll() is None pr_none.send_sigint() @@ -274,6 +300,7 @@ def test_proc_reader_send_sigint(pr_none): else: assert ret_code == -signal.SIGINT + def test_proc_reader_terminate(pr_none): assert pr_none._proc.poll() is None pr_none.terminate() @@ -297,11 +324,13 @@ def test_proc_reader_terminate(pr_none): def context_flag(): return cu.ContextFlag() + def test_context_flag_bool(context_flag): assert not context_flag with context_flag: assert context_flag + def test_context_flag_exit_err(context_flag): with pytest.raises(ValueError): context_flag.__exit__() @@ -313,30 +342,35 @@ def test_truncate_line(): truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + def test_truncate_line_already_fits(): line = 'long' max_width = 4 truncated = cu.truncate_line(line, max_width) assert truncated == line + def test_truncate_line_with_newline(): line = 'fo\no' max_width = 2 with pytest.raises(ValueError): cu.truncate_line(line, max_width) + def test_truncate_line_width_is_too_small(): line = 'foo' max_width = 0 with pytest.raises(ValueError): cu.truncate_line(line, max_width) + def test_truncate_line_wide_text(): line = '苹苹other' max_width = 6 truncated = cu.truncate_line(line, max_width) assert truncated == '苹苹o' + HORIZONTAL_ELLIPSIS + def test_truncate_line_split_wide_text(): """Test when truncation results in a string which is shorter than max_width""" line = '1苹2苹' @@ -344,12 +378,14 @@ def test_truncate_line_split_wide_text(): truncated = cu.truncate_line(line, max_width) assert truncated == '1' + HORIZONTAL_ELLIPSIS + def test_truncate_line_tabs(): line = 'has\ttab' max_width = 9 truncated = cu.truncate_line(line, max_width) assert truncated == 'has t' + HORIZONTAL_ELLIPSIS + def test_truncate_with_style(): from cmd2 import ansi @@ -374,6 +410,7 @@ def test_truncate_with_style(): truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + after_style + def test_align_text_fill_char_is_tab(): text = 'foo' fill_char = '\t' @@ -381,6 +418,7 @@ def test_align_text_fill_char_is_tab(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + ' ' + def test_align_text_with_style(): from cmd2 import ansi @@ -422,8 +460,8 @@ def test_align_text_with_style(): line_1_text = ansi.fg.bright_blue + 'line1' line_2_text = ansi.fg.bright_blue + 'line2' + ansi.FG_RESET - assert aligned == (left_fill + line_1_text + right_fill + '\n' + - left_fill + line_2_text + right_fill) + assert aligned == (left_fill + line_1_text + right_fill + '\n' + left_fill + line_2_text + right_fill) + def test_align_text_width_is_too_small(): text = 'foo' @@ -432,6 +470,7 @@ def test_align_text_width_is_too_small(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_fill_char_is_too_long(): text = 'foo' fill_char = 'fill' @@ -439,6 +478,7 @@ def test_align_text_fill_char_is_too_long(): with pytest.raises(TypeError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_fill_char_is_newline(): text = 'foo' fill_char = '\n' @@ -446,6 +486,7 @@ def test_align_text_fill_char_is_newline(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_has_tabs(): text = '\t\tfoo' fill_char = '-' @@ -453,6 +494,7 @@ def test_align_text_has_tabs(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=2) assert aligned == ' ' + 'foo' + '---' + def test_align_text_blank(): text = '' fill_char = '-' @@ -460,6 +502,7 @@ def test_align_text_blank(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == fill_char * width + def test_align_text_wider_than_width(): text = 'long text field' fill_char = '-' @@ -467,6 +510,7 @@ def test_align_text_wider_than_width(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + def test_align_text_wider_than_width_truncate(): text = 'long text field' fill_char = '-' @@ -474,6 +518,7 @@ def test_align_text_wider_than_width_truncate(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == 'long te' + HORIZONTAL_ELLIPSIS + def test_align_text_wider_than_width_truncate_add_fill(): """Test when truncation results in a string which is shorter than width and align_text adds filler""" text = '1苹2苹' @@ -482,6 +527,7 @@ def test_align_text_wider_than_width_truncate_add_fill(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char + def test_align_text_has_unprintable(): text = 'foo\x02' fill_char = '-' @@ -489,9 +535,12 @@ def test_align_text_has_unprintable(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_term_width(): import shutil + from cmd2 import ansi + text = 'foo' fill_char = ' ' @@ -501,6 +550,7 @@ def test_align_text_term_width(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char) assert aligned == text + expected_fill + def test_align_left(): text = 'foo' fill_char = '-' @@ -508,13 +558,14 @@ def test_align_left(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char + def test_align_left_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_left(text, fill_char=fill_char, width=width) - assert aligned == ('foo----\n' - 'shoes--') + assert aligned == ('foo----\n' 'shoes--') + def test_align_left_wide_text(): text = '苹' @@ -523,6 +574,7 @@ def test_align_left_wide_text(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char + def test_align_left_wide_fill(): text = 'foo' fill_char = '苹' @@ -530,6 +582,7 @@ def test_align_left_wide_fill(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + def test_align_left_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' @@ -538,6 +591,7 @@ def test_align_left_wide_fill_needs_padding(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + ' ' + def test_align_center(): text = 'foo' fill_char = '-' @@ -545,13 +599,14 @@ def test_align_center(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_center(text, fill_char=fill_char, width=width) - assert aligned == ('--foo--\n' - '-shoes-') + assert aligned == ('--foo--\n' '-shoes-') + def test_align_center_wide_text(): text = '苹' @@ -560,6 +615,7 @@ def test_align_center_wide_text(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_wide_fill(): text = 'foo' fill_char = '苹' @@ -567,6 +623,7 @@ def test_align_center_wide_fill(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_wide_fill_needs_right_padding(): """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' @@ -575,6 +632,7 @@ def test_align_center_wide_fill_needs_right_padding(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + ' ' + def test_align_center_wide_fill_needs_left_and_right_padding(): """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' @@ -583,6 +641,7 @@ def test_align_center_wide_fill_needs_left_and_right_padding(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text + fill_char + ' ' + def test_align_right(): text = 'foo' fill_char = '-' @@ -590,13 +649,14 @@ def test_align_right(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text + def test_align_right_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_right(text, fill_char=fill_char, width=width) - assert aligned == ('----foo\n' - '--shoes') + assert aligned == ('----foo\n' '--shoes') + def test_align_right_wide_text(): text = '苹' @@ -605,6 +665,7 @@ def test_align_right_wide_text(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text + def test_align_right_wide_fill(): text = 'foo' fill_char = '苹' @@ -612,6 +673,7 @@ def test_align_right_wide_fill(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + def test_align_right_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' @@ -627,26 +689,31 @@ def test_str_to_bool_true(): assert cu.str_to_bool('TRUE') assert cu.str_to_bool('tRuE') + def test_str_to_bool_false(): assert not cu.str_to_bool('false') assert not cu.str_to_bool('False') assert not cu.str_to_bool('FALSE') assert not cu.str_to_bool('fAlSe') + def test_str_to_bool_invalid(): with pytest.raises(ValueError): cu.str_to_bool('other') + def test_str_to_bool_bad_input(): with pytest.raises(ValueError): cu.str_to_bool(1) + def test_find_editor_specified(): expected_editor = os.path.join('fake_dir', 'editor') with mock.patch.dict(os.environ, {'EDITOR': expected_editor}): editor = cu.find_editor() assert editor == expected_editor + def test_find_editor_not_specified(): # Use existing path env setting. Something in the editor list should be found. editor = cu.find_editor() diff --git a/tests/test_utils_defining_class.py b/tests/test_utils_defining_class.py index 0fbcf83b..5d667678 100644 --- a/tests/test_utils_defining_class.py +++ b/tests/test_utils_defining_class.py @@ -77,7 +77,7 @@ def test_get_defining_class(): assert cu.get_defining_class(partial_unbound) is ParentClass assert cu.get_defining_class(nested_partial_unbound) is ParentClass - partial_bound = functools.partial(parent_instance.parent_only_func, 1) - nested_partial_bound = functools.partial(partial_bound, 2) + partial_bound = functools.partial(parent_instance.parent_only_func, 1) + nested_partial_bound = functools.partial(partial_bound, 2) assert cu.get_defining_class(partial_bound) is ParentClass assert cu.get_defining_class(nested_partial_bound) is ParentClass |