diff options
| author | Eric Lin <anselor@gmail.com> | 2018-05-16 10:39:49 -0400 |
|---|---|---|
| committer | Eric Lin <anselor@gmail.com> | 2018-05-16 10:39:49 -0400 |
| commit | 371284d20370a8e85dd8527d9bbcc6267b335cde (patch) | |
| tree | 002ac709eca15522a0b1abaa235aae04975b08e6 /tests | |
| parent | ab8194e92b9c3728d8f86cb9c81de180b6884eee (diff) | |
| parent | a9b712108e5af49937b0af3aa51db2ebe5c159e4 (diff) | |
| download | cmd2-git-371284d20370a8e85dd8527d9bbcc6267b335cde.tar.gz | |
Merge branch 'master' into pyscript
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_argparse.py | 13 | ||||
| -rw-r--r-- | tests/test_bashcompletion.py | 232 | ||||
| -rw-r--r-- | tests/test_cmd2.py | 38 | ||||
| -rw-r--r-- | tests/test_completion.py | 133 | ||||
| -rw-r--r-- | tests/test_parsing.py | 177 | ||||
| -rw-r--r-- | tests/test_submenu.py | 181 | ||||
| -rw-r--r-- | tests/test_transcript.py | 175 | ||||
| -rw-r--r-- | tests/transcripts/from_cmdloop.txt | 1 |
8 files changed, 450 insertions, 500 deletions
diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 94a7b5ed..f1a2b357 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -119,6 +119,11 @@ def argparse_app(): return app +def test_invalid_syntax(argparse_app, capsys): + run_cmd(argparse_app, 'speak "') + out, err = capsys.readouterr() + assert err == "ERROR: Invalid syntax: No closing quotation\n" + def test_argparse_basic_command(argparse_app): out = run_cmd(argparse_app, 'say hello') assert out == ['hello'] @@ -135,6 +140,14 @@ def test_argparse_with_list_and_empty_doc(argparse_app): out = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] +def test_argparse_comment_stripping(argparse_app): + out = run_cmd(argparse_app, 'speak it was /* not */ delicious! # Yuck!') + assert out == ['it was delicious!'] + +def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app): + out = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!") + assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!'] + def test_argparse_quoted_arguments_multiple(argparse_app): out = run_cmd(argparse_app, 'say "hello there" "rick & morty"') assert out == ['hello there rick & morty'] diff --git a/tests/test_bashcompletion.py b/tests/test_bashcompletion.py new file mode 100644 index 00000000..ceae2aa9 --- /dev/null +++ b/tests/test_bashcompletion.py @@ -0,0 +1,232 @@ +# coding=utf-8 +""" +Unit/functional testing for argparse completer in cmd2 + +Copyright 2018 Eric Lin <anselor@gmail.com> +Released under MIT license, see LICENSE file +""" +import os +import pytest +import sys +from typing import List + +from cmd2.argparse_completer import ACArgumentParser, AutoCompleter + + +try: + from cmd2.argcomplete_bridge import CompletionFinder + skip_reason1 = False + skip_reason = '' +except ImportError: + # Don't test if argcomplete isn't present (likely on Windows) + skip_reason1 = True + skip_reason = "argcomplete isn't installed\n" + +skip_reason2 = "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true" +if skip_reason2: + skip_reason += 'These tests cannot run on TRAVIS\n' + +skip_reason3 = sys.platform.startswith('win') +if skip_reason3: + skip_reason = 'argcomplete doesn\'t support Windows' + +skip = skip_reason1 or skip_reason2 or skip_reason3 + +skip_mac = sys.platform.startswith('dar') + + +actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', + 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', + 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', + 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] + + +def query_actors() -> List[str]: + """Simulating a function that queries and returns a completion values""" + return actors + + +@pytest.fixture +def parser1(): + """creates a argparse object to test completion against""" + ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] + + def _do_media_movies(self, args) -> None: + if not args.command: + self.do_help('media movies') + else: + print('media movies ' + str(args.__dict__)) + + def _do_media_shows(self, args) -> None: + if not args.command: + self.do_help('media shows') + + if not args.command: + self.do_help('media shows') + else: + print('media shows ' + str(args.__dict__)) + + media_parser = ACArgumentParser(prog='media') + + media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type') + + movies_parser = media_types_subparsers.add_parser('movies') + movies_parser.set_defaults(func=_do_media_movies) + + movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command') + + movies_list_parser = movies_commands_subparsers.add_parser('list') + + movies_list_parser.add_argument('-t', '--title', help='Title Filter') + movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', + choices=ratings_types) + movies_list_parser.add_argument('-d', '--director', help='Director Filter') + movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') + + movies_add_parser = movies_commands_subparsers.add_parser('add') + movies_add_parser.add_argument('title', help='Movie Title') + movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) + movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) + movies_add_parser.add_argument('actor', help='Actors', nargs='*') + + movies_commands_subparsers.add_parser('delete') + + shows_parser = media_types_subparsers.add_parser('shows') + shows_parser.set_defaults(func=_do_media_shows) + + shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command') + + shows_commands_subparsers.add_parser('list') + + return media_parser + + +# noinspection PyShadowingNames +@pytest.mark.skipif(skip, reason=skip_reason) +def test_bash_nocomplete(parser1): + completer = CompletionFinder() + result = completer(parser1, AutoCompleter(parser1)) + assert result is None + + +# save the real os.fdopen +os_fdopen = os.fdopen + + +def my_fdopen(fd, mode, *args): + """mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing""" + if fd > 7: + return os_fdopen(fd - 7, mode, *args) + return os_fdopen(fd, mode) + + +# noinspection PyShadowingNames +@pytest.mark.skipif(skip, reason=skip_reason) +def test_invalid_ifs(parser1, mock): + completer = CompletionFinder() + + mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', + '_ARGCOMPLETE_IFS': '\013\013'}) + + mock.patch.object(os, 'fdopen', my_fdopen) + + with pytest.raises(SystemExit): + completer(parser1, AutoCompleter(parser1), exit_method=sys.exit) + + +# noinspection PyShadowingNames +@pytest.mark.skipif(skip or skip_mac, reason=skip_reason) +@pytest.mark.parametrize('comp_line, exp_out, exp_err', [ + ('media ', 'movies\013shows', ''), + ('media mo', 'movies', ''), + ('media movies add ', '\013\013 ', ''' +Hint: + TITLE Movie Title'''), + ('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''), + ('media movies list ', '', '') +]) +def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err): + completer = CompletionFinder() + + mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', + '_ARGCOMPLETE_IFS': '\013', + 'COMP_TYPE': '63', + 'COMP_LINE': comp_line, + 'COMP_POINT': str(len(comp_line))}) + + mock.patch.object(os, 'fdopen', my_fdopen) + + with pytest.raises(SystemExit): + choices = {'actor': query_actors, # function + } + autocompleter = AutoCompleter(parser1, arg_choices=choices) + completer(parser1, autocompleter, exit_method=sys.exit) + + out, err = capfd.readouterr() + assert out == exp_out + assert err == exp_err + + +def fdopen_fail_8(fd, mode, *args): + """mock fdopen that forces failure if fd == 8""" + if fd == 8: + raise IOError() + return my_fdopen(fd, mode, *args) + + +# noinspection PyShadowingNames +@pytest.mark.skipif(skip, reason=skip_reason) +def test_fail_alt_stdout(parser1, mock): + completer = CompletionFinder() + + comp_line = 'media movies list ' + mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', + '_ARGCOMPLETE_IFS': '\013', + 'COMP_TYPE': '63', + 'COMP_LINE': comp_line, + 'COMP_POINT': str(len(comp_line))}) + mock.patch.object(os, 'fdopen', fdopen_fail_8) + + try: + choices = {'actor': query_actors, # function + } + autocompleter = AutoCompleter(parser1, arg_choices=choices) + completer(parser1, autocompleter, exit_method=sys.exit) + except SystemExit as err: + assert err.code == 1 + + +def fdopen_fail_9(fd, mode, *args): + """mock fdopen that forces failure if fd == 9""" + if fd == 9: + raise IOError() + return my_fdopen(fd, mode, *args) + + +# noinspection PyShadowingNames +@pytest.mark.skipif(skip or skip_mac, reason=skip_reason) +def test_fail_alt_stderr(parser1, capfd, mock): + completer = CompletionFinder() + + comp_line = 'media movies add ' + exp_out = '\013\013 ' + exp_err = ''' +Hint: + TITLE Movie Title''' + + mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', + '_ARGCOMPLETE_IFS': '\013', + 'COMP_TYPE': '63', + 'COMP_LINE': comp_line, + 'COMP_POINT': str(len(comp_line))}) + mock.patch.object(os, 'fdopen', fdopen_fail_9) + + with pytest.raises(SystemExit): + choices = {'actor': query_actors, # function + } + autocompleter = AutoCompleter(parser1, arg_choices=choices) + completer(parser1, autocompleter, exit_method=sys.exit) + + out, err = capfd.readouterr() + assert out == exp_out + assert err == exp_err diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index b6416005..6e4a5a3e 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1430,7 +1430,7 @@ def test_clipboard_failure(capsys): # Make sure we got the error output out, err = capsys.readouterr() assert out == '' - assert 'Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' in err + assert "Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable" in err class CmdResultApp(cmd2.Cmd): @@ -1688,12 +1688,6 @@ def test_alias_lookup_invalid_alias(base_app, capsys): out, err = capsys.readouterr() assert "not found" in err -def test_alias_with_invalid_name(base_app, capsys): - run_cmd(base_app, 'alias @ help') - out, err = capsys.readouterr() - assert "can only contain the following characters" in err - - def test_unalias(base_app): # Create an alias run_cmd(base_app, 'alias fake pyscript') @@ -1711,6 +1705,18 @@ def test_unalias_non_existing(base_app, capsys): out, err = capsys.readouterr() assert "does not exist" in err +@pytest.mark.parametrize('alias_name', [ + '">"', + '"no>pe"', + '"no spaces"', + '"nopipe|"', + '"noterm;"', + 'noembedded"quotes', +]) +def test_create_invalid_alias(base_app, alias_name, capsys): + run_cmd(base_app, 'alias {} help'.format(alias_name)) + out, err = capsys.readouterr() + assert "can not contain" in err def test_ppaged(base_app): msg = 'testing...' @@ -1718,3 +1724,21 @@ def test_ppaged(base_app): base_app.ppaged(msg) out = base_app.stdout.buffer assert out == 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 +# parseline just calls StatementParser.parse_command_only() +def test_parseline_empty(base_app): + statement = '' + command, args, line = base_app.parseline(statement) + assert not command + assert not args + assert not line + +def test_parseline(base_app): + statement = " command with 'partially completed quotes " + command, args, line = base_app.parseline(statement) + assert command == 'command' + assert args == "with 'partially completed quotes" + assert line == statement.strip() diff --git a/tests/test_completion.py b/tests/test_completion.py index a027d780..c7650dbb 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -887,7 +887,7 @@ def test_cmd2_subcmd_with_unknown_completion_nomatch(scu_app): assert first_match is None -def test_cmd2_help_subcommand_completion_single(scu_app): +def test_cmd2_help_subcommand_completion_single_scu(scu_app): text = 'base' line = 'help {}'.format(text) endidx = len(line) @@ -895,7 +895,7 @@ def test_cmd2_help_subcommand_completion_single(scu_app): assert scu_app.complete_help(text, line, begidx, endidx) == ['base'] -def test_cmd2_help_subcommand_completion_multiple(scu_app): +def test_cmd2_help_subcommand_completion_multiple_scu(scu_app): text = '' line = 'help base {}'.format(text) endidx = len(line) @@ -905,7 +905,7 @@ def test_cmd2_help_subcommand_completion_multiple(scu_app): assert matches == ['bar', 'foo', 'sport'] -def test_cmd2_help_subcommand_completion_nomatch(scu_app): +def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app): text = 'z' line = 'help base {}'.format(text) endidx = len(line) @@ -913,7 +913,7 @@ def test_cmd2_help_subcommand_completion_nomatch(scu_app): assert scu_app.complete_help(text, line, begidx, endidx) == [] -def test_subcommand_tab_completion(scu_app): +def test_subcommand_tab_completion_scu(scu_app): # This makes sure the correct completer for the sport subcommand is called text = 'Foot' line = 'base sport {}'.format(text) @@ -926,7 +926,7 @@ def test_subcommand_tab_completion(scu_app): assert first_match is not None and scu_app.completion_matches == ['Football '] -def test_subcommand_tab_completion_with_no_completer(scu_app): +def test_subcommand_tab_completion_with_no_completer_scu(scu_app): # This tests what happens when a subcommand has no completer # In this case, the foo subcommand has no completer defined text = 'Foot' @@ -938,7 +938,7 @@ def test_subcommand_tab_completion_with_no_completer(scu_app): assert first_match is None -def test_subcommand_tab_completion_space_in_text(scu_app): +def test_subcommand_tab_completion_space_in_text_scu(scu_app): text = 'B' line = 'base sport "Space {}'.format(text) endidx = len(line) @@ -949,124 +949,3 @@ def test_subcommand_tab_completion_space_in_text(scu_app): assert first_match is not None and \ scu_app.completion_matches == ['Ball" '] and \ scu_app.display_matches == ['Space Ball'] - -#################################################### - - -class SecondLevel(cmd2.Cmd): - """To be used as a second level command class. """ - - def __init__(self, *args, **kwargs): - cmd2.Cmd.__init__(self, *args, **kwargs) - self.prompt = '2ndLevel ' - - def do_foo(self, line): - self.poutput("You called a command in SecondLevel with '%s'. " % line) - - def help_foo(self): - self.poutput("This is a second level menu. Options are qwe, asd, zxc") - - def complete_foo(self, text, line, begidx, endidx): - return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] - - -second_level_cmd = SecondLevel() - - -@cmd2.AddSubmenu(second_level_cmd, - command='second', - require_predefined_shares=False) -class SubmenuApp(cmd2.Cmd): - """To be used as the main / top level command class that will contain other submenus.""" - - def __init__(self, *args, **kwargs): - cmd2.Cmd.__init__(self, *args, **kwargs) - self.prompt = 'TopLevel ' - - -@pytest.fixture -def sb_app(): - app = SubmenuApp() - return app - - -def test_cmd2_submenu_completion_single_end(sb_app): - text = 'f' - line = 'second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - first_match = complete_tester(text, line, begidx, endidx, sb_app) - - # It is at end of line, so extra space is present - assert first_match is not None and sb_app.completion_matches == ['foo '] - - -def test_cmd2_submenu_completion_multiple(sb_app): - text = 'e' - line = 'second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - expected = ['edit', 'eof', 'eos'] - first_match = complete_tester(text, line, begidx, endidx, sb_app) - - assert first_match is not None and sb_app.completion_matches == expected - - -def test_cmd2_submenu_completion_nomatch(sb_app): - text = 'z' - line = 'second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - first_match = complete_tester(text, line, begidx, endidx, sb_app) - assert first_match is None - - -def test_cmd2_submenu_completion_after_submenu_match(sb_app): - text = 'a' - line = 'second foo {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - first_match = complete_tester(text, line, begidx, endidx, sb_app) - assert first_match is not None and sb_app.completion_matches == ['asd '] - - -def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app): - text = 'b' - line = 'second foo {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - first_match = complete_tester(text, line, begidx, endidx, sb_app) - assert first_match is None - - -def test_cmd2_help_submenu_completion_multiple(sb_app): - text = 'p' - line = 'help second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - matches = sorted(sb_app.complete_help(text, line, begidx, endidx)) - assert matches == ['py', 'pyscript'] - - -def test_cmd2_help_submenu_completion_nomatch(sb_app): - text = 'fake' - line = 'help second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - assert sb_app.complete_help(text, line, begidx, endidx) == [] - - -def test_cmd2_help_submenu_completion_subcommands(sb_app): - text = 'p' - line = 'help second {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - matches = sorted(sb_app.complete_help(text, line, begidx, endidx)) - assert matches == ['py', 'pyscript'] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 7940bbd8..41966c71 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -16,7 +16,7 @@ from cmd2 import utils def parser(): parser = StatementParser( allow_redirection=True, - terminators=[';'], + terminators=[';', '&'], multiline_commands=['multiline'], aliases={'helpalias': 'help', '42': 'theanswer', @@ -38,12 +38,22 @@ def test_parse_empty_string(parser): ('command /* with some comment */ arg', ['command', 'arg']), ('command arg1 arg2 # comment at the end', ['command', 'arg1', 'arg2']), ('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']), - ('l', ['shell', 'ls', '-al']) + ('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 +def test_tokenize_unclosed_quotes(parser): + with pytest.raises(ValueError): + _ = parser.tokenize('command with "unclosed quotes') + @pytest.mark.parametrize('tokens,command,args', [ ([], None, None), (['command'], 'command', None), @@ -59,35 +69,45 @@ def test_command_and_args(parser, tokens, command, args): '"one word"', "'one word'", ]) -def test_single_word(parser, line): +def test_parse_single_word(parser, line): statement = parser.parse(line) assert statement.command == line assert not statement.args assert statement.argv == [utils.strip_quotes(line)] -def test_word_plus_terminator(parser): - line = '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' - assert statement.terminator == ';' + assert statement.terminator == terminator assert statement.argv == ['termbare'] -def test_suffix_after_terminator(parser): - line = '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' - assert statement.terminator == ';' + assert statement.terminator == terminator assert statement.suffix == 'suffx' assert statement.argv == ['termbare'] -def test_command_with_args(parser): +def test_parse_command_with_args(parser): line = 'command with args' statement = parser.parse(line) assert statement.command == 'command' assert statement.args == 'with args' assert statement.argv == ['command', 'with', 'args'] -def test_command_with_quoted_args(parser): +def test_parse_command_with_quoted_args(parser): line = 'command with "quoted args" and "some not"' statement = parser.parse(line) assert statement.command == 'command' @@ -103,20 +123,20 @@ def test_parse_command_with_args_terminator_and_suffix(parser): assert statement.suffix == 'and suffix' assert statement.argv == ['command', 'with', 'args', 'and', 'terminator'] -def test_hashcomment(parser): +def test_parse_hashcomment(parser): statement = parser.parse('hi # this is all a comment') assert statement.command == 'hi' assert not statement.args assert statement.argv == ['hi'] -def test_c_comment(parser): +def test_parse_c_comment(parser): statement = parser.parse('hi /* this is | all a comment */') assert statement.command == 'hi' assert not statement.args assert not statement.pipe_to assert statement.argv == ['hi'] -def test_c_comment_empty(parser): +def test_parse_c_comment_empty(parser): statement = parser.parse('/* this is | all a comment */') assert not statement.command assert not statement.args @@ -130,14 +150,18 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser): assert not statement.pipe_to assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?'] -def test_simple_piped(parser): - statement = parser.parse('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' assert not statement.args assert statement.argv == ['simple'] - assert statement.pipe_to == 'piped' + assert statement.pipe_to == ['piped'] -def test_double_pipe_is_not_a_pipe(parser): +def test_parse_double_pipe_is_not_a_pipe(parser): line = 'double-pipe || is not a pipe' statement = parser.parse(line) assert statement.command == 'double-pipe' @@ -145,17 +169,30 @@ def test_double_pipe_is_not_a_pipe(parser): assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe'] assert not statement.pipe_to -def test_complex_pipe(parser): - line = 'command with args, terminator;sufx | piped' +def test_parse_complex_pipe(parser): + line = 'command with args, terminator&sufx | piped' statement = parser.parse(line) assert statement.command == 'command' assert statement.args == "with args, terminator" assert statement.argv == ['command', 'with', 'args,', 'terminator'] - assert statement.terminator == ';' + assert statement.terminator == '&' assert statement.suffix == 'sufx' - assert statement.pipe_to == 'piped' + assert statement.pipe_to == ['piped'] + +@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' + assert not statement.args + assert statement.output == output + assert statement.output_to == 'out.txt' -def test_output_redirect(parser): +def test_parse_redirect_with_args(parser): line = 'output into > afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -164,7 +201,7 @@ def test_output_redirect(parser): assert statement.output == '>' assert statement.output_to == 'afile.txt' -def test_output_redirect_with_dash_in_path(parser): +def test_parse_redirect_with_dash_in_path(parser): line = 'output into > python-cmd2/afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -173,7 +210,7 @@ def test_output_redirect_with_dash_in_path(parser): assert statement.output == '>' assert statement.output_to == 'python-cmd2/afile.txt' -def test_output_redirect_append(parser): +def test_parse_redirect_append(parser): line = 'output appended to >> /tmp/afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -182,7 +219,7 @@ def test_output_redirect_append(parser): assert statement.output == '>>' assert statement.output_to == '/tmp/afile.txt' -def test_pipe_and_redirect(parser): +def test_parse_pipe_and_redirect(parser): line = 'output into;sufx | pipethrume plz > afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -190,9 +227,9 @@ def test_pipe_and_redirect(parser): assert statement.argv == ['output', 'into'] assert statement.terminator == ';' assert statement.suffix == 'sufx' - assert statement.pipe_to == 'pipethrume plz' - assert statement.output == '>' - assert statement.output_to == 'afile.txt' + assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt'] + assert not statement.output + assert not statement.output_to def test_parse_output_to_paste_buffer(parser): line = 'output to paste buffer >> ' @@ -202,9 +239,10 @@ def test_parse_output_to_paste_buffer(parser): assert statement.argv == ['output', 'to', 'paste', 'buffer'] assert statement.output == '>>' -def test_has_redirect_inside_terminator(parser): - """The terminator designates the end of the commmand/arguments portion. If a redirector - occurs before a terminator, then it will be treated as part of the arguments and not as a redirector.""" +def test_parse_redirect_inside_terminator(parser): + """The terminator designates the end of the commmand/arguments portion. + If a redirector occurs before a terminator, then it will be treated as + part of the arguments and not as a redirector.""" line = 'has > inside;' statement = parser.parse(line) assert statement.command == 'has' @@ -221,13 +259,16 @@ def test_parse_unfinished_multiliine_command(parser): assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command'] assert not statement.terminator -def test_parse_multiline_command_ignores_redirectors_within_it(parser): - line = 'multiline has > inside;' +@pytest.mark.parametrize('line,terminator',[ + ('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' assert statement.args == 'has > inside' assert statement.argv == ['multiline', 'has', '>', 'inside'] - assert statement.terminator == ';' + assert statement.terminator == terminator def test_parse_multiline_with_incomplete_comment(parser): """A terminator within a comment will be ignored and won't terminate a multiline command. @@ -290,6 +331,10 @@ def test_parse_redirect_to_unicode_filename(parser): assert statement.output == '>' assert statement.output_to == 'café' +def test_parse_unclosed_quotes(parser): + with pytest.raises(ValueError): + _ = parser.tokenize("command with 'unclosed quotes") + def test_empty_statement_raises_exception(): app = cmd2.Cmd() with pytest.raises(cmd2.EmptyStatement): @@ -307,12 +352,12 @@ def test_empty_statement_raises_exception(): ('!ls -al /tmp', 'shell', 'ls -al /tmp'), ('l', 'shell', 'ls -al') ]) -def test_alias_and_shortcut_expansion(parser, line, command, args): +def test_parse_alias_and_shortcut_expansion(parser, line, command, args): statement = parser.parse(line) assert statement.command == command assert statement.args == args -def test_alias_on_multiline_command(parser): +def test_parse_alias_on_multiline_command(parser): line = 'anothermultiline has > inside an unfinished command' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -320,12 +365,41 @@ def test_alias_on_multiline_command(parser): assert statement.args == 'has > inside an unfinished command' assert not statement.terminator +@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' + assert not statement.args + assert statement.output == output + assert statement.output_to == 'out.txt' + +@pytest.mark.parametrize('line', [ + 'helpalias | less', + 'helpalias|less', +]) +def test_parse_alias_pipe(parser, line): + statement = parser.parse(line) + assert statement.command == 'help' + assert not statement.args + assert statement.pipe_to == ['less'] + +def test_parse_alias_terminator_no_whitespace(parser): + line = 'helpalias;' + statement = parser.parse(line) + assert statement.command == 'help' + assert not statement.args + assert statement.terminator == ';' + def test_parse_command_only_command_and_args(parser): line = 'help history' statement = parser.parse_command_only(line) assert statement.command == 'help' assert statement.args == 'history' - assert statement.argv == ['help', 'history'] assert statement.command_and_args == line def test_parse_command_only_emptyline(parser): @@ -345,7 +419,6 @@ def test_parse_command_only_strips_line(parser): statement = parser.parse_command_only(line) assert statement.command == 'help' assert statement.args == 'history' - assert statement.argv == ['help', 'history'] assert statement.command_and_args == line.strip() def test_parse_command_only_expands_alias(parser): @@ -353,14 +426,12 @@ def test_parse_command_only_expands_alias(parser): statement = parser.parse_command_only(line) assert statement.command == 'pyscript' assert statement.args == 'foobar.py' - assert statement.argv == ['pyscript', 'foobar.py'] def test_parse_command_only_expands_shortcuts(parser): line = '!cat foobar.txt' statement = parser.parse_command_only(line) assert statement.command == 'shell' assert statement.args == 'cat foobar.txt' - assert statement.argv == ['shell', 'cat', 'foobar.txt'] assert statement.command_and_args == 'shell cat foobar.txt' def test_parse_command_only_quoted_args(parser): @@ -369,3 +440,27 @@ def test_parse_command_only_quoted_args(parser): assert statement.command == 'shell' assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"' assert statement.command_and_args == line.replace('l', 'shell ls -al') + +@pytest.mark.parametrize('line', [ + 'helpalias > out.txt', + 'helpalias>out.txt', + 'helpalias >> out.txt', + 'helpalias>>out.txt', + 'help|less', + 'helpalias;', +]) +def test_parse_command_only_specialchars(parser, line): + statement = parser.parse_command_only(line) + assert statement.command == 'help' + +@pytest.mark.parametrize('line', [ + ';', + '>', + "'", + '"', + '|', +]) +def test_parse_command_only_none(parser, line): + statement = parser.parse_command_only(line) + assert statement.command == None + assert statement.args == None diff --git a/tests/test_submenu.py b/tests/test_submenu.py deleted file mode 100644 index db334daa..00000000 --- a/tests/test_submenu.py +++ /dev/null @@ -1,181 +0,0 @@ -# coding=utf-8 -""" -Cmd2 testing for argument parsing -""" -import pytest - -from cmd2 import cmd2 -from .conftest import run_cmd, StdOut, normalize - - -class SecondLevelB(cmd2.Cmd): - """To be used as a second level command class. """ - - def __init__(self, *args, **kwargs): - cmd2.Cmd.__init__(self, *args, **kwargs) - self.prompt = '2ndLevel B ' - - def do_get_top_level_attr(self, line): - self.poutput(str(self.top_level_attr)) - - def do_set_top_level_attr(self, line): - self.top_level_attr = 987654321 - - -class SecondLevel(cmd2.Cmd): - """To be used as a second level command class. """ - - def __init__(self, *args, **kwargs): - cmd2.Cmd.__init__(self, *args, **kwargs) - self.prompt = '2ndLevel ' - self.top_level_attr = None - - def do_say(self, line): - self.poutput("You called a command in SecondLevel with '%s'. " % line) - - def help_say(self): - self.poutput("This is a second level menu. Options are qwe, asd, zxc") - - def complete_say(self, text, line, begidx, endidx): - return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] - - def do_get_top_level_attr(self, line): - self.poutput(str(self.top_level_attr)) - - def do_get_prompt(self, line): - self.poutput(self.prompt) - - -second_level_cmd = SecondLevel() -second_level_b_cmd = SecondLevelB() - - -@cmd2.AddSubmenu(SecondLevelB(), - command='should_work_with_default_kwargs') -@cmd2.AddSubmenu(second_level_b_cmd, - command='secondb', - shared_attributes=dict(top_level_attr='top_level_attr'), - require_predefined_shares=False, - preserve_shares=True - ) -@cmd2.AddSubmenu(second_level_cmd, - command='second', - aliases=('second_alias',), - shared_attributes=dict(top_level_attr='top_level_attr')) -class SubmenuApp(cmd2.Cmd): - """To be used as the main / top level command class that will contain other submenus.""" - - def __init__(self, *args, **kwargs): - cmd2.Cmd.__init__(self, *args, **kwargs) - self.prompt = 'TopLevel ' - self.top_level_attr = 123456789 - - def do_say(self, line): - self.poutput("You called a command in TopLevel with '%s'. " % line) - - def help_say(self): - self.poutput("This is a top level submenu. Options are qwe, asd, zxc") - - def complete_say(self, text, line, begidx, endidx): - return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] - - -@pytest.fixture -def submenu_app(): - app = SubmenuApp() - app.stdout = StdOut() - second_level_cmd.stdout = StdOut() - second_level_b_cmd.stdout = StdOut() - return app - - -@pytest.fixture -def secondlevel_app(): - app = SecondLevel() - app.stdout = StdOut() - return app - - -@pytest.fixture -def secondlevel_app_b(): - app = SecondLevelB() - app.stdout = StdOut() - return app - - -def run_submenu_cmd(app, second_level_app, cmd): - """ Clear StdOut buffers, run the command, extract the buffer contents.""" - app.stdout.clear() - second_level_app.stdout.clear() - app.onecmd_plus_hooks(cmd) - out1 = app.stdout.buffer - out2 = second_level_app.stdout.buffer - app.stdout.clear() - second_level_app.stdout.clear() - return normalize(out1), normalize(out2) - - -def test_submenu_say_from_top_level(submenu_app): - line = 'testing' - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'say ' + line) - assert len(out1) == 1 - assert len(out2) == 0 - assert out1[0] == "You called a command in TopLevel with {!r}.".format(line) - - -def test_submenu_second_say_from_top_level(submenu_app): - line = 'testing' - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second say ' + line) - - # No output expected from the top level - assert out1 == [] - - # Output expected from the second level - assert len(out2) == 1 - assert out2[0] == "You called a command in SecondLevel with {!r}.".format(line) - - -def test_submenu_say_from_second_level(secondlevel_app): - line = 'testing' - out = run_cmd(secondlevel_app, 'say ' + line) - assert out == ["You called a command in SecondLevel with '%s'." % line] - - -def test_submenu_help_second_say_from_top_level(submenu_app): - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say') - # No output expected from the top level - assert out1 == [] - - # Output expected from the second level - assert out2 == ["This is a second level menu. Options are qwe, asd, zxc"] - - -def test_submenu_help_say_from_second_level(secondlevel_app): - out = run_cmd(secondlevel_app, 'help say') - assert out == ["This is a second level menu. Options are qwe, asd, zxc"] - - -def test_submenu_help_second(submenu_app): - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second') - out3 = run_cmd(second_level_cmd, 'help') - assert out2 == out3 - - -def test_submenu_from_top_help_second_say(submenu_app): - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say') - out3 = run_cmd(second_level_cmd, 'help say') - assert out2 == out3 - - -def test_submenu_shared_attribute(submenu_app): - out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second get_top_level_attr') - assert out2 == [str(submenu_app.top_level_attr)] - - -def test_submenu_shared_attribute_preserve(submenu_app): - out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr') - assert out2 == [str(submenu_app.top_level_attr)] - out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb set_top_level_attr') - assert submenu_app.top_level_attr == 987654321 - out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr') - assert out2 == [str(987654321)] diff --git a/tests/test_transcript.py b/tests/test_transcript.py index c0fb49c1..70658161 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -10,12 +10,13 @@ import os import sys import re import random +import tempfile from unittest import mock import pytest from cmd2 import cmd2 -from .conftest import run_cmd, StdOut, normalize +from .conftest import run_cmd, StdOut class CmdLineApp(cmd2.Cmd): @@ -26,7 +27,6 @@ class CmdLineApp(cmd2.Cmd): def __init__(self, *args, **kwargs): self.multiline_commands = ['orate'] self.maxrepeats = 3 - self.redirector = '->' # Add stuff to settable and/or shortcuts before calling base class initializer self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed' @@ -63,7 +63,7 @@ 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 i in range(min(repetitions, self.maxrepeats)): output = [] if random.random() < .33: @@ -77,136 +77,6 @@ class CmdLineApp(cmd2.Cmd): self.poutput(' '.join(output)) -class DemoApp(cmd2.Cmd): - hello_parser = argparse.ArgumentParser() - hello_parser.add_argument('-n', '--name', help="your name") - @cmd2.with_argparser_and_unknown_args(hello_parser) - def do_hello(self, opts, arg): - """Says hello.""" - if opts.name: - self.stdout.write('Hello {}\n'.format(opts.name)) - else: - self.stdout.write('Hello Nobody\n') - - -@pytest.fixture -def _cmdline_app(): - c = CmdLineApp() - c.stdout = StdOut() - return c - - -@pytest.fixture -def _demo_app(): - c = DemoApp() - c.stdout = StdOut() - return c - - -def _get_transcript_blocks(transcript): - cmd = None - expected = '' - for line in transcript.splitlines(): - if line.startswith('(Cmd) '): - if cmd is not None: - yield cmd, normalize(expected) - - cmd = line[6:] - expected = '' - else: - expected += line + '\n' - yield cmd, normalize(expected) - - -def test_base_with_transcript(_cmdline_app): - app = _cmdline_app - transcript = """ -(Cmd) help - -Documented commands (type help <topic>): -======================================== -alias help load orate pyscript say shell speak -edit history mumble py quit set shortcuts unalias - -(Cmd) help say -usage: speak [-h] [-p] [-s] [-r REPEAT] - -Repeats what you tell me to. - -optional arguments: - -h, --help show this help message and exit - -p, --piglatin atinLay - -s, --shout N00B EMULATION MODE - -r REPEAT, --repeat REPEAT - output [n] times - -(Cmd) say goodnight, Gracie -goodnight, Gracie -(Cmd) say -ps --repeat=5 goodnight, Gracie -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -(Cmd) set maxrepeats 5 -maxrepeats - was: 3 -now: 5 -(Cmd) say -ps --repeat=5 goodnight, Gracie -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -(Cmd) history --------------------------[1] -help --------------------------[2] -help say --------------------------[3] -say goodnight, Gracie --------------------------[4] -say -ps --repeat=5 goodnight, Gracie --------------------------[5] -set maxrepeats 5 --------------------------[6] -say -ps --repeat=5 goodnight, Gracie -(Cmd) history -r 4 -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -OODNIGHT, GRACIEGAY -(Cmd) set prompt "---> " -prompt - was: (Cmd) -now: ---> -""" - - for cmd, expected in _get_transcript_blocks(transcript): - out = run_cmd(app, cmd) - assert out == expected - - -class TestMyAppCase(cmd2.Cmd2TestCase): - CmdApp = CmdLineApp - CmdApp.testfiles = ['tests/transcript.txt'] - - -def test_comment_stripping(_cmdline_app): - out = run_cmd(_cmdline_app, 'speak it was /* not */ delicious! # Yuck!') - expected = normalize("""it was delicious!""") - assert out == expected - - -def test_argparser_correct_args_with_quotes_and_midline_options(_cmdline_app): - out = run_cmd(_cmdline_app, "speak 'This is a' -s test of the emergency broadcast system!") - expected = normalize("""THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!""") - assert out == expected - - -def test_argparser_options_with_spaces_in_quotes(_demo_app): - out = run_cmd(_demo_app, "hello foo -n 'Bugs Bunny' bar baz") - expected = normalize("""Hello Bugs Bunny""") - assert out == expected - - def test_commands_at_invocation(): testargs = ["prog", "say hello", "say Gracie", "quit"] expected = "This is an intro banner ...\nhello\nGracie\n" @@ -217,19 +87,12 @@ def test_commands_at_invocation(): out = app.stdout.buffer assert out == expected -def test_invalid_syntax(_cmdline_app, capsys): - run_cmd(_cmdline_app, 'speak "') - out, err = capsys.readouterr() - expected = normalize("""ERROR: Invalid syntax: No closing quotation""") - assert normalize(str(err)) == expected - - -@pytest.mark.parametrize('filename, feedback_to_output', [ +@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), + ('from_cmdloop.txt', True), ('multiline_no_regex.txt', False), ('multiline_regex.txt', False), ('regex_set.txt', False), @@ -237,7 +100,7 @@ def test_invalid_syntax(_cmdline_app, capsys): ('slashes_escaped.txt', False), ('slashslash.txt', False), ('spaces.txt', False), - # ('word_boundaries.txt', False), + ('word_boundaries.txt', False), ]) def test_transcript(request, capsys, filename, feedback_to_output): # Create a cmd2.Cmd() instance and make sure basic settings are @@ -263,6 +126,32 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert err.startswith(expected_start) assert err.endswith(expected_end) +def test_history_transcript(request, capsys): + app = CmdLineApp() + app.stdout = StdOut() + run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') + run_cmd(app, 'speak /tmp/file.txt is not a regex') + + expected = r"""(Cmd) orate this is +> a /multiline/ +> command; +this is a \/multiline\/ command +(Cmd) speak /tmp/file.txt is not a regex +\/tmp\/file.txt is not a regex +""" + + # make a tmp file + fd, history_fname = tempfile.mkstemp(prefix='', suffix='.txt') + os.close(fd) + + # tell the history command to create a transcript + run_cmd(app, 'history -t "{}"'.format(history_fname)) + + # read in the transcript created by the history command + with open(history_fname) as f: + transcript = f.read() + + assert transcript == expected @pytest.mark.parametrize('expected, transformed', [ # strings with zero or one slash or with escaped slashes means no regular diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt index 13b61b00..5f22d756 100644 --- a/tests/transcripts/from_cmdloop.txt +++ b/tests/transcripts/from_cmdloop.txt @@ -19,7 +19,6 @@ optional arguments:/ */ -s, --shout N00B EMULATION MODE/ */ -r REPEAT, --repeat REPEAT/ */ output [n] times - (Cmd) say goodnight, Gracie goodnight, Gracie (Cmd) say -ps --repeat=5 goodnight, Gracie |
