diff options
| author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-09-30 11:17:04 -0400 |
|---|---|---|
| committer | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-09-30 11:17:04 -0400 |
| commit | 81ad085cffc8f8af7fb7884bcc5db6fcede09df6 (patch) | |
| tree | a197a6c81767f10b62d6ac6acfd46e42357a8289 /tests | |
| parent | 85a21ef39467dc24a6f6f8c4f3ac656f24d6ff90 (diff) | |
| parent | 61d5703cd3586b3460669a6260cf903c9863b240 (diff) | |
| download | cmd2-git-81ad085cffc8f8af7fb7884bcc5db6fcede09df6.tar.gz | |
Merged master into transcript_fixes branch and resolved conflicts
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/conftest.py | 86 | ||||
| -rw-r--r-- | tests/scripts/postcmds.txt | 2 | ||||
| -rw-r--r-- | tests/scripts/precmds.txt | 2 | ||||
| -rw-r--r-- | tests/test_argparse.py | 9 | ||||
| -rw-r--r-- | tests/test_autocompletion.py | 16 | ||||
| -rw-r--r-- | tests/test_cmd2.py | 659 | ||||
| -rw-r--r-- | tests/test_completion.py | 117 | ||||
| -rw-r--r-- | tests/test_parsing.py | 101 | ||||
| -rw-r--r-- | tests/test_plugin.py | 79 | ||||
| -rw-r--r-- | tests/test_pyscript.py | 48 | ||||
| -rw-r--r-- | tests/test_transcript.py | 16 | ||||
| -rw-r--r-- | tests/test_utils.py | 120 | ||||
| -rw-r--r-- | tests/transcripts/from_cmdloop.txt | 4 | ||||
| -rw-r--r-- | tests/transcripts/regex_set.txt | 4 |
14 files changed, 968 insertions, 295 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index a23c44d0..da7e8b08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,11 +6,13 @@ Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com> Released under MIT license, see LICENSE file """ import sys +from typing import Optional +from unittest import mock from pytest import fixture -from unittest import mock import cmd2 +from cmd2.utils import StdSim # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: @@ -27,29 +29,29 @@ except ImportError: # Help text for base cmd2.Cmd application BASE_HELP = """Documented commands (type help <topic>): ======================================== -alias help load pyscript set shortcuts -edit history py quit shell unalias +alias help load py quit shell +edit history macro pyscript set shortcuts """ BASE_HELP_VERBOSE = """ Documented commands (type help <topic>): ================================================================================ -alias Define or display aliases +alias Manage aliases edit Edit a file in a text editor -help List available commands with "help" or detailed help with "help cmd" +help List available commands or provide detailed help for a specific command history View, run, edit, save, or clear previously entered commands -load Runs commands in script file that is encoded as either ASCII or UTF-8 text -py Invoke python command, shell, or script -pyscript Runs a python script file inside the console -quit Exits this application -set Sets a settable parameter or shows current settings of parameters +load Run commands in script file that is encoded as either ASCII or UTF-8 text +macro Manage macros +py Invoke Python command or shell +pyscript Run a Python script file inside the console +quit Exit this application +set Set a settable parameter or show current settings of parameters shell Execute a command as if at the OS prompt -shortcuts Lists shortcuts available -unalias Unsets aliases +shortcuts List available shortcuts """ # Help text for the history command -HELP_HISTORY = """Usage: history [arg] [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] +HELP_HISTORY = """Usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] [arg] View, run, edit, save, or clear previously entered commands @@ -64,12 +66,12 @@ optional arguments: -h, --help show this help message and exit -r, --run run selected history items -e, --edit edit and then run selected history items - -s, --script script format; no separation lines + -s, --script output commands in script format -o, --output-file FILE output commands to a script file -t, --transcript TRANSCRIPT output commands and results to a transcript file - -c, --clear clears all history + -c, --clear clear all history """ # Output from the shortcuts command with default built-in shortcuts @@ -80,11 +82,8 @@ SHORTCUTS_TXT = """Shortcuts for other commands: @@: _relative_load """ -expect_colors = True -if sys.platform.startswith('win'): - expect_colors = False # Output from the show command with default settings -SHOW_TXT = """colors: {} +SHOW_TXT = """colors: Terminal continuation_prompt: > debug: False echo: False @@ -94,14 +93,10 @@ locals_in_py: False prompt: (Cmd) quiet: False timing: False -""".format(expect_colors) +""" -if expect_colors: - color_str = 'True ' -else: - color_str = 'False' SHOW_LONG = """ -colors: {} # Colorized output (*nix only) +colors: Terminal # Allow colorized output (valid values: Terminal, Always, Never) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output @@ -111,23 +106,7 @@ locals_in_py: False # Allow access to your application in py via self prompt: (Cmd) # The prompt issued to solicit input quiet: False # Don't print nonessential feedback timing: False # Report execution times -""".format(color_str) - - -class StdOut(object): - """ Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """ - def __init__(self): - self.buffer = '' - - def write(self, s): - self.buffer += s - - def read(self): - raise NotImplementedError - - def clear(self): - self.buffer = '' - +""" def normalize(block): """ Normalize a block of text to perform comparison. @@ -141,10 +120,10 @@ def normalize(block): def run_cmd(app, cmd): - """ Clear StdOut buffer, run the command, extract the buffer contents, """ + """ Clear StdSim buffer, run the command, extract the buffer contents, """ app.stdout.clear() app.onecmd_plus_hooks(cmd) - out = app.stdout.buffer + out = app.stdout.getvalue() app.stdout.clear() return normalize(out) @@ -152,21 +131,21 @@ def run_cmd(app, cmd): @fixture def base_app(): c = cmd2.Cmd() - c.stdout = StdOut() + c.stdout = StdSim(c.stdout) return c -def complete_tester(text, line, begidx, endidx, app): +def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: """ This is a convenience function to test cmd2.complete() since in a unit test environment there is no actual console readline is monitoring. Therefore we use mock to provide readline data to complete(). - :param text: str - the string prefix we are attempting to match - :param line: str - the current input line with leading whitespace removed - :param begidx: int - the beginning index of the prefix text - :param endidx: int - the ending index of the prefix text + :param text: the string prefix we are attempting to match + :param line: the current input line with leading whitespace removed + :param begidx: the beginning index of the prefix text + :param endidx: the ending index of the prefix text :param app: the cmd2 app that will run completions :return: The first matched string or None if there are no matches Matches are stored in app.completion_matches @@ -181,11 +160,8 @@ def complete_tester(text, line, begidx, endidx, app): def get_endidx(): return endidx - first_match = None + # Run the readline tab-completion function with readline mocks in place with mock.patch.object(readline, 'get_line_buffer', get_line): with mock.patch.object(readline, 'get_begidx', get_begidx): with mock.patch.object(readline, 'get_endidx', get_endidx): - # Run the readline tab-completion function with readline mocks in place - first_match = app.complete(text, 0) - - return first_match + return app.complete(text, 0) diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt index 2b478b57..dea8f265 100644 --- a/tests/scripts/postcmds.txt +++ b/tests/scripts/postcmds.txt @@ -1 +1 @@ -set colors off +set colors Never diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt index d0b27fb6..0ae7eae8 100644 --- a/tests/scripts/precmds.txt +++ b/tests/scripts/precmds.txt @@ -1 +1 @@ -set colors on +set colors Always diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 469cbe76..fdd16bcc 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -6,9 +6,9 @@ import argparse import pytest import cmd2 -from unittest import mock +from cmd2.utils import StdSim -from .conftest import run_cmd, StdOut +from .conftest import run_cmd # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: @@ -115,7 +115,7 @@ class ArgparseApp(cmd2.Cmd): @pytest.fixture def argparse_app(): app = ArgparseApp() - app.stdout = StdOut() + app.stdout = StdSim(app.stdout) return app @@ -222,7 +222,7 @@ class SubcommandApp(cmd2.Cmd): @pytest.fixture def subcommand_app(): app = SubcommandApp() - app.stdout = StdOut() + app.stdout = StdSim(app.stdout) return app @@ -258,4 +258,3 @@ def test_subcommand_help(subcommand_app): def test_subcommand_invalid_help(subcommand_app): out = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('usage: base') - assert out[1].startswith("base: error: invalid choice: 'baz'") diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py index 8aa26e0e..c6c1d1f6 100644 --- a/tests/test_autocompletion.py +++ b/tests/test_autocompletion.py @@ -1,3 +1,4 @@ +# coding=utf-8 """ Unit/functional testing for argparse completer in cmd2 @@ -5,16 +6,17 @@ Copyright 2018 Eric Lin <anselor@gmail.com> Released under MIT license, see LICENSE file """ import pytest -from .conftest import run_cmd, normalize, StdOut, complete_tester + +from cmd2.utils import StdSim +from .conftest import run_cmd, normalize, complete_tester from examples.tab_autocompletion import TabCompleteExample @pytest.fixture def cmd2_app(): - c = TabCompleteExample() - c.stdout = StdOut() - - return c + app = TabCompleteExample() + app.stdout = StdSim(app.stdout) + return app SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}] @@ -32,9 +34,9 @@ optional arguments: single value - maximum duration [a, b] - duration range''' -MEDIA_MOVIES_ADD_HELP = '''Usage: media movies add title {G, PG, PG-13, R, NC-17} [actor [...]] - -d DIRECTOR{1..2} +MEDIA_MOVIES_ADD_HELP = '''Usage: media movies add -d DIRECTOR{1..2} [-h] + title {G, PG, PG-13, R, NC-17} [actor [...]] positional arguments: title Movie Title diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index fdf0f661..3ce7a11d 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -13,6 +13,7 @@ import os import sys import tempfile +from colorama import Fore, Back, Style import pytest # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available @@ -25,7 +26,7 @@ import cmd2 from cmd2 import clipboard from cmd2 import utils from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ - HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut + HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG def test_version(base_app): @@ -46,6 +47,11 @@ def test_base_help_verbose(base_app): expected = normalize(BASE_HELP_VERBOSE) assert out == expected + # Make sure :param type lines are filtered out of help summary + help_doc = base_app.do_help.__func__.__doc__ + help_doc += "\n:param fake param" + base_app.do_help.__func__.__doc__ = help_doc + out = run_cmd(base_app, 'help --verbose') assert out == expected @@ -64,7 +70,7 @@ def test_base_argparse_help(base_app, capsys): assert out1 == out2 assert out1[0].startswith('Usage: set') assert out1[1] == '' - assert out1[2].startswith('Sets a settable parameter') + assert out1[2].startswith('Set a settable parameter') def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'set -z') @@ -72,7 +78,7 @@ def test_base_invalid_option(base_app, capsys): out = normalize(out) err = normalize(err) assert 'Error: unrecognized arguments: -z' in err[0] - assert out[0] == 'Usage: set settable{0..2} [-h] [-a] [-l]' + assert out[0] == 'Usage: set [-h] [-a] [-l] [param] [value]' def test_base_shortcuts(base_app): out = run_cmd(base_app, 'shortcuts') @@ -178,6 +184,30 @@ now: True assert out == ['quiet: True'] +class OnChangeHookApp(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _onchange_quiet(self, old, new) -> None: + """Runs when quiet is changed via set command""" + self.poutput("You changed quiet") + +@pytest.fixture +def onchange_app(): + app = OnChangeHookApp() + app.stdout = utils.StdSim(app.stdout) + return app + +def test_set_onchange_hook(onchange_app): + out = run_cmd(onchange_app, 'set quiet True') + expected = normalize(""" +quiet - was: False +now: True +You changed quiet +""") + assert out == expected + + def test_base_shell(base_app, monkeypatch): m = mock.Mock() monkeypatch.setattr("{}.Popen".format('subprocess'), m) @@ -215,13 +245,13 @@ def test_base_run_pyscript(base_app, capsys, request): out, err = capsys.readouterr() assert out == expected -def test_recursive_pyscript_not_allowed(base_app, capsys, request): +def test_recursive_pyscript_not_allowed(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'scripts', 'recursive.py') - expected = 'ERROR: Recursively entering interactive Python consoles is not allowed.\n' + expected = 'Recursively entering interactive Python consoles is not allowed.' run_cmd(base_app, "pyscript {}".format(python_script)) - out, err = capsys.readouterr() + err = base_app._last_result.stderr assert err == expected def test_pyscript_with_nonexist_file(base_app, capsys): @@ -241,7 +271,7 @@ def test_pyscript_with_exception(base_app, capsys, request): def test_pyscript_requires_an_argument(base_app, capsys): run_cmd(base_app, "pyscript") out, err = capsys.readouterr() - assert err.startswith('ERROR: pyscript command requires at least 1 argument ...') + assert "the following arguments are required: script_path" in err def test_base_error(base_app): @@ -471,7 +501,7 @@ def test_load_with_empty_args(base_app, capsys): out, err = capsys.readouterr() # The load command requires a file path argument, so we should get an error message - assert "load command requires a file path" in str(err) + assert "the following arguments are required" in str(err) assert base_app.cmdqueue == [] @@ -559,11 +589,11 @@ def test_load_nested_loads(base_app, request): expected = """ %s _relative_load precmds.txt -set colors on +set colors Always help shortcuts _relative_load postcmds.txt -set colors off""" % initial_load +set colors Never""" % initial_load assert run_cmd(base_app, 'history -s') == normalize(expected) @@ -581,11 +611,11 @@ def test_base_runcmds_plus_hooks(base_app, request): 'load ' + postfilepath]) expected = """ load %s -set colors on +set colors Always help shortcuts load %s -set colors off""" % (prefilepath, postfilepath) +set colors Never""" % (prefilepath, postfilepath) assert run_cmd(base_app, 'history -s') == normalize(expected) @@ -608,8 +638,7 @@ def test_base_relative_load(base_app, request): def test_relative_load_requires_an_argument(base_app, capsys): run_cmd(base_app, '_relative_load') out, err = capsys.readouterr() - assert out == '' - assert err.startswith('ERROR: _relative_load command requires a file path:\n') + assert 'Error: the following arguments' in err assert base_app.cmdqueue == [] @@ -812,12 +841,7 @@ def test_base_colorize(base_app): # But if we create a fresh Cmd() instance, it will fresh_app = cmd2.Cmd() color_test = fresh_app.colorize('Test', 'red') - # Actually, colorization only ANSI escape codes is only applied on non-Windows systems - if sys.platform == 'win32': - assert color_test == 'Test' - else: - assert color_test == '\x1b[31mTest\x1b[39m' - + assert color_test == '\x1b[31mTest\x1b[39m' def _expected_no_editor_error(): expected_exception = 'OSError' @@ -857,7 +881,8 @@ def test_edit_file(base_app, request, monkeypatch): run_cmd(base_app, 'edit {}'.format(filename)) # We think we have an editor, so should expect a system call - m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) + m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor), + utils.quote_string_if_needed(filename))) 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 @@ -873,7 +898,8 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch): run_cmd(base_app, 'edit "{}"'.format(filename)) # We think we have an editor, so should expect a system call - m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) + m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor), + utils.quote_string_if_needed(filename))) 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 @@ -930,7 +956,7 @@ def test_base_cmdloop_with_queue(): app.use_rawinput = True intro = 'Hello World, this is an intro ...' app.cmdqueue.append('quit\n') - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] @@ -938,7 +964,7 @@ def test_base_cmdloop_with_queue(): with mock.patch.object(sys, 'argv', testargs): # Run the command loop with custom intro app.cmdloop(intro=intro) - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected @@ -947,7 +973,7 @@ def test_base_cmdloop_without_queue(): app = cmd2.Cmd() app.use_rawinput = True app.intro = 'Hello World, this is an intro ...' - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='quit') @@ -959,7 +985,7 @@ def test_base_cmdloop_without_queue(): with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected @@ -969,7 +995,7 @@ def test_cmdloop_without_rawinput(): app.use_rawinput = False app.echo = False app.intro = 'Hello World, this is an intro ...' - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='quit') @@ -981,22 +1007,25 @@ def test_cmdloop_without_rawinput(): with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # register a postparsing hook method + self.register_postparsing_hook(self.postparsing_precmd) - def postparsing_precmd(self, statement): + def postparsing_precmd(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: """Simulate precmd hook failure.""" - return True, statement + data.stop = True + return data @pytest.fixture def hook_failure(): app = HookFailureApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_precmd_hook_success(base_app): @@ -1020,7 +1049,7 @@ class SayApp(cmd2.Cmd): def say_app(): app = SayApp() app.allow_cli_args = False - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_interrupt_quit(say_app): @@ -1034,7 +1063,7 @@ def test_interrupt_quit(say_app): say_app.cmdloop() # And verify the expected output to stdout - out = say_app.stdout.buffer + out = say_app.stdout.getvalue() assert out == 'hello\n' def test_interrupt_noquit(say_app): @@ -1048,7 +1077,7 @@ def test_interrupt_noquit(say_app): say_app.cmdloop() # And verify the expected output to stdout - out = say_app.stdout.buffer + out = say_app.stdout.getvalue() assert out == 'hello\n^C\ngoodbye\n' @@ -1060,7 +1089,7 @@ class ShellApp(cmd2.Cmd): @pytest.fixture def shell_app(): app = ShellApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_default_to_shell_unknown(shell_app): @@ -1093,28 +1122,30 @@ def test_default_to_shell_failure(capsys): def test_ansi_prompt_not_esacped(base_app): + from cmd2.rl_utils import rl_make_safe_prompt prompt = '(Cmd) ' - assert base_app._surround_ansi_escapes(prompt) == prompt + 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' + color = Fore.CYAN prompt = 'InColor' - color_prompt = app.colorize(prompt, color) + color_prompt = color + prompt + Fore.RESET readline_hack_start = "\x01" readline_hack_end = "\x02" - readline_safe_prompt = app._surround_ansi_escapes(color_prompt) + readline_safe_prompt = rl_make_safe_prompt(color_prompt) + assert prompt != color_prompt if sys.platform.startswith('win'): - # colorize() does nothing on Windows due to lack of ANSI color support - assert prompt == color_prompt - assert readline_safe_prompt == prompt + # PyReadline on Windows doesn't suffer from the GNU readline bug which requires the hack + assert readline_safe_prompt.startswith(color) + assert readline_safe_prompt.endswith(Fore.RESET) else: - assert prompt != color_prompt - assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end) - assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end) + assert readline_safe_prompt.startswith(readline_hack_start + color + readline_hack_end) + assert readline_safe_prompt.endswith(readline_hack_start + Fore.RESET + readline_hack_end) class HelpApp(cmd2.Cmd): @@ -1140,7 +1171,7 @@ class HelpApp(cmd2.Cmd): @pytest.fixture def help_app(): app = HelpApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_custom_command_help(help_app): @@ -1153,8 +1184,8 @@ def test_custom_help_menu(help_app): expected = normalize(""" Documented commands (type help <topic>): ======================================== -alias help load pyscript set shortcuts unalias -edit history py quit shell squat +alias help load py quit shell squat +edit history macro pyscript set shortcuts Undocumented commands: ====================== @@ -1203,7 +1234,7 @@ class HelpCategoriesApp(cmd2.Cmd): @pytest.fixture def helpcat_app(): app = HelpCategoriesApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_help_cat_base(helpcat_app): @@ -1220,7 +1251,7 @@ diddly Other ===== -alias help history load py pyscript quit set shell shortcuts unalias +alias help history load macro py pyscript quit set shell shortcuts Undocumented commands: ====================== @@ -1243,17 +1274,17 @@ diddly This command does diddly Other ================================================================================ -alias Define or display aliases -help List available commands with "help" or detailed help with "help cmd" +alias Manage aliases +help List available commands or provide detailed help for a specific command history View, run, edit, save, or clear previously entered commands -load Runs commands in script file that is encoded as either ASCII or UTF-8 text -py Invoke python command, shell, or script -pyscript Runs a python script file inside the console -quit Exits this application -set Sets a settable parameter or shows current settings of parameters +load Run commands in script file that is encoded as either ASCII or UTF-8 text +macro Manage macros +py Invoke Python command or shell +pyscript Run a Python script file inside the console +quit Exit this application +set Set a settable parameter or show current settings of parameters shell Execute a command as if at the OS prompt -shortcuts Lists shortcuts available -unalias Unsets aliases +shortcuts List available shortcuts Undocumented commands: ====================== @@ -1296,7 +1327,7 @@ class SelectApp(cmd2.Cmd): @pytest.fixture def select_app(): app = SelectApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_select_options(select_app): @@ -1461,7 +1492,7 @@ class MultilineApp(cmd2.Cmd): @pytest.fixture def multiline_app(): app = MultilineApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_multiline_complete_empty_statement_raises_exception(multiline_app): @@ -1522,7 +1553,7 @@ class CommandResultApp(cmd2.Cmd): @pytest.fixture def commandresult_app(): app = CommandResultApp() - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) return app def test_commandresult_truthy(commandresult_app): @@ -1550,7 +1581,7 @@ def test_is_text_file_bad_input(base_app): def test_eof(base_app): # Only thing to verify is that it returns True - assert base_app.do_eof('dont care') + assert base_app.do_eof('') def test_eos(base_app): sdir = 'dummy_dir' @@ -1558,7 +1589,7 @@ def test_eos(base_app): assert len(base_app._script_dir) == 1 # Assert that it does NOT return true - assert not base_app.do_eos('dont care') + assert not base_app.do_eos('') # And make sure it reduced the length of the script dir list assert len(base_app._script_dir) == 0 @@ -1628,7 +1659,7 @@ def piped_rawinput_true(capsys, echo, command): # run the cmdloop, which should pull input from our mock app._cmdloop() out, err = capsys.readouterr() - return (app, out) + return app, out # using the decorator puts the original input function back when this unit test returns @mock.patch('builtins.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) @@ -1658,7 +1689,7 @@ def piped_rawinput_false(capsys, echo, command): app.echo = echo app._cmdloop() out, err = capsys.readouterr() - return (app, out) + return app, out def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys): command = 'set' @@ -1715,100 +1746,358 @@ def test_empty_stdin_input(): def test_poutput_string(base_app): msg = 'This is a test' base_app.poutput(msg) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() expected = msg + '\n' assert out == expected def test_poutput_zero(base_app): msg = 0 base_app.poutput(msg) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() expected = str(msg) + '\n' assert out == expected def test_poutput_empty_string(base_app): msg = '' base_app.poutput(msg) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() expected = msg assert out == expected def test_poutput_none(base_app): msg = None base_app.poutput(msg) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() expected = '' assert out == expected +def test_poutput_color_always(base_app): + msg = 'Hello World' + color = Fore.CYAN + base_app.colors = 'Always' + base_app.poutput(msg, color=color) + out = base_app.stdout.getvalue() + expected = color + msg + '\n' + Fore.RESET + assert out == expected -def test_alias(base_app, capsys): +def test_poutput_color_never(base_app): + msg = 'Hello World' + color = Fore.CYAN + base_app.colors = 'Never' + base_app.poutput(msg, color=color) + out = base_app.stdout.getvalue() + expected = msg + '\n' + assert out == expected + + +# These are invalid names for aliases and macros +invalid_command_name = [ + '""', # Blank name + '!no_shortcut', + '">"', + '"no>pe"', + '"no spaces"', + '"nopipe|"', + '"noterm;"', + 'noembedded"quotes', +] + +def test_get_alias_names(base_app): + assert len(base_app.aliases) == 0 + run_cmd(base_app, 'alias create fake pyscript') + run_cmd(base_app, 'alias create ls !ls -hal') + assert len(base_app.aliases) == 2 + assert sorted(base_app.get_alias_names()) == ['fake', 'ls'] + +def test_get_macro_names(base_app): + assert len(base_app.macros) == 0 + run_cmd(base_app, 'macro create foo !echo foo') + run_cmd(base_app, 'macro create bar !echo bar') + assert len(base_app.macros) == 2 + assert sorted(base_app.get_macro_names()) == ['bar', 'foo'] + +def test_alias_no_subcommand(base_app, capsys): + out = run_cmd(base_app, 'alias') + assert "Usage: alias [-h]" in out[0] + +def test_alias_create(base_app, capsys): # Create the alias - out = run_cmd(base_app, 'alias fake pyscript') + out = run_cmd(base_app, 'alias create fake pyscript') assert out == normalize("Alias 'fake' created") # Use the alias run_cmd(base_app, 'fake') out, err = capsys.readouterr() - assert "pyscript command requires at least 1 argument" in err + assert "the following arguments are required: script_path" in err # See a list of aliases - out = run_cmd(base_app, 'alias') - assert out == normalize('alias fake pyscript') + out = run_cmd(base_app, 'alias list') + assert out == normalize('alias create fake pyscript') + + # Look up the new alias + out = run_cmd(base_app, 'alias list fake') + assert out == normalize('alias create fake pyscript') - # Lookup the new alias - out = run_cmd(base_app, 'alias fake') - assert out == normalize('alias fake pyscript') +def test_alias_quoted_name(base_app, capsys): + """Demonstrate that names can be quoted in alias commands because they will be stripped""" + # Create the alias + out = run_cmd(base_app, 'alias create "fake" pyscript') + + # The quotes on names are stripped + assert out == normalize("Alias 'fake' created") + + # Look up the new alias and quote the name + out = run_cmd(base_app, 'alias list "fake"') + assert out == normalize('alias create fake pyscript') + + # Delete the alias using quotes + out = run_cmd(base_app, 'alias delete "fake"') + assert out == normalize("Alias 'fake' deleted") + +def test_alias_create_with_quoted_value(base_app, capsys): + """Demonstrate that quotes in alias value will be preserved (except for redirectors)""" -def test_alias_with_quotes(base_app, capsys): # Create the alias - out = run_cmd(base_app, 'alias fake help ">" "out file.txt"') + out = run_cmd(base_app, 'alias create fake help ">" "out file.txt"') assert out == normalize("Alias 'fake' created") - # Lookup the new alias (Only the redirector should be unquoted) - out = run_cmd(base_app, 'alias fake') - assert out == normalize('alias fake help > "out file.txt"') + # Look up the new alias (Only the redirector should be unquoted) + out = run_cmd(base_app, 'alias list fake') + assert out == normalize('alias create fake help > "out file.txt"') + +@pytest.mark.parametrize('alias_name', invalid_command_name) +def test_alias_create_invalid_name(base_app, alias_name, capsys): + run_cmd(base_app, 'alias create {} help'.format(alias_name)) + out, err = capsys.readouterr() + assert "Invalid alias name" in err + +def test_alias_create_with_macro_name(base_app, capsys): + macro = "my_macro" + run_cmd(base_app, 'macro create {} help'.format(macro)) + run_cmd(base_app, 'alias create {} help'.format(macro)) + out, err = capsys.readouterr() + assert "Alias cannot have the same name as a macro" in err -def test_alias_lookup_invalid_alias(base_app, capsys): - # Lookup invalid alias - out = run_cmd(base_app, 'alias invalid') +def test_alias_list_invalid_alias(base_app, capsys): + # Look up invalid alias + out = run_cmd(base_app, 'alias list invalid') out, err = capsys.readouterr() - assert "not found" in err + assert "Alias 'invalid' not found" in err -def test_unalias(base_app): +def test_alias_delete(base_app): # Create an alias - run_cmd(base_app, 'alias fake pyscript') + run_cmd(base_app, 'alias create fake pyscript') - # Remove the alias - out = run_cmd(base_app, 'unalias fake') - assert out == normalize("Alias 'fake' cleared") + # Delete the alias + out = run_cmd(base_app, 'alias delete fake') + assert out == normalize("Alias 'fake' deleted") -def test_unalias_all(base_app): - out = run_cmd(base_app, 'unalias -a') - assert out == normalize("All aliases cleared") +def test_alias_delete_all(base_app): + out = run_cmd(base_app, 'alias delete --all') + assert out == normalize("All aliases deleted") -def test_unalias_non_existing(base_app, capsys): - run_cmd(base_app, 'unalias fake') +def test_alias_delete_non_existing(base_app, capsys): + run_cmd(base_app, 'alias delete fake') out, err = capsys.readouterr() - assert "does not exist" in err + assert "Alias 'fake' does not exist" in err + +def test_alias_delete_no_name(base_app, capsys): + out = run_cmd(base_app, 'alias delete') + assert "Usage: alias delete" in out[0] + +def test_multiple_aliases(base_app): + alias1 = 'h1' + alias2 = 'h2' + run_cmd(base_app, 'alias create {} help'.format(alias1)) + run_cmd(base_app, 'alias create {} help -v'.format(alias2)) + out = run_cmd(base_app, alias1) + expected = normalize(BASE_HELP) + assert out == expected -@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 = run_cmd(base_app, alias2) + expected = normalize(BASE_HELP_VERBOSE) + assert out == expected + +def test_macro_no_subcommand(base_app, capsys): + out = run_cmd(base_app, 'macro') + assert "Usage: macro [-h]" in out[0] + +def test_macro_create(base_app, capsys): + # Create the macro + out = run_cmd(base_app, 'macro create fake pyscript') + assert out == normalize("Macro 'fake' created") + + # Use the macro + run_cmd(base_app, 'fake') + out, err = capsys.readouterr() + assert "the following arguments are required: script_path" in err + + # See a list of macros + out = run_cmd(base_app, 'macro list') + assert out == normalize('macro create fake pyscript') + + # Look up the new macro + out = run_cmd(base_app, 'macro list fake') + assert out == normalize('macro create fake pyscript') + +def test_macro_create_quoted_name(base_app, capsys): + """Demonstrate that names can be quoted in macro commands because they will be stripped""" + # Create the macro + out = run_cmd(base_app, 'macro create "fake" pyscript') + + # The quotes on names are stripped + assert out == normalize("Macro 'fake' created") + + # Look up the new macro and quote the name + out = run_cmd(base_app, 'macro list "fake"') + assert out == normalize('macro create fake pyscript') + + # Delete the macro using quotes + out = run_cmd(base_app, 'macro delete "fake"') + assert out == normalize("Macro 'fake' deleted") + +def test_macro_create_with_quoted_value(base_app, capsys): + """Demonstrate that quotes in macro value will be preserved (except for redirectors)""" + # Create the macro + out = run_cmd(base_app, 'macro create fake help ">" "out file.txt"') + assert out == normalize("Macro 'fake' created") + + # Look up the new macro (Only the redirector should be unquoted) + out = run_cmd(base_app, 'macro list fake') + assert out == normalize('macro create fake help > "out file.txt"') + +@pytest.mark.parametrize('macro_name', invalid_command_name) +def test_macro_create_invalid_name(base_app, macro_name, capsys): + run_cmd(base_app, 'macro create {} help'.format(macro_name)) + out, err = capsys.readouterr() + assert "Invalid macro name" in err + +def test_macro_create_with_alias_name(base_app, capsys): + macro = "my_macro" + run_cmd(base_app, 'alias create {} help'.format(macro)) + run_cmd(base_app, 'macro create {} help'.format(macro)) + out, err = capsys.readouterr() + assert "Macro cannot have the same name as an alias" in err + +def test_macro_create_with_command_name(base_app, capsys): + macro = "my_macro" + run_cmd(base_app, 'macro create help stuff') + out, err = capsys.readouterr() + assert "Macro cannot have the same name as a command" in err + +def test_macro_create_with_args(base_app, capsys): + # Create the macro + out = run_cmd(base_app, 'macro create fake {1} {2}') + assert out == normalize("Macro 'fake' created") + + # Run the macro + out = run_cmd(base_app, 'fake help -v') + expected = normalize(BASE_HELP_VERBOSE) + assert out == expected + +def test_macro_create_with_escaped_args(base_app, capsys): + # Create the macro + out = run_cmd(base_app, 'macro create fake help {{1}}') + assert out == normalize("Macro 'fake' created") + + # Run the macro + out = run_cmd(base_app, 'fake') + assert 'No help on {1}' in out[0] + +def test_macro_create_with_missing_arg_nums(base_app, capsys): + # Create the macro + run_cmd(base_app, 'macro create fake help {1} {3}') out, err = capsys.readouterr() - assert "can not contain" in err + assert "Not all numbers between 1 and 3" in err + +def test_macro_create_with_invalid_arg_num(base_app, capsys): + # Create the macro + run_cmd(base_app, 'macro create fake help {1} {-1} {0}') + out, err = capsys.readouterr() + assert "Argument numbers must be greater than 0" in err + +def test_macro_create_with_wrong_arg_count(base_app, capsys): + # Create the macro + out = run_cmd(base_app, 'macro create fake help {1} {2}') + assert out == normalize("Macro 'fake' created") + + # Run the macro + run_cmd(base_app, 'fake arg1') + out, err = capsys.readouterr() + assert "expects 2 argument(s)" in err + +def test_macro_create_with_unicode_numbered_arg(base_app, capsys): + # Create the macro expecting 1 argument + out = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}') + assert out == normalize("Macro 'fake' created") + + # Run the macro + out = run_cmd(base_app, 'fake') + out, err = capsys.readouterr() + assert "expects 1 argument(s)" in err + +def test_macro_create_with_missing_unicode_arg_nums(base_app, capsys): + run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}') + out, err = capsys.readouterr() + assert "Not all numbers between 1 and 3" in err + +def test_macro_list_invalid_macro(base_app, capsys): + # Look up invalid macro + run_cmd(base_app, 'macro list invalid') + out, err = capsys.readouterr() + assert "Macro 'invalid' not found" in err + +def test_macro_delete(base_app): + # Create an macro + run_cmd(base_app, 'macro create fake pyscript') + + # Delete the macro + out = run_cmd(base_app, 'macro delete fake') + assert out == normalize("Macro 'fake' deleted") + +def test_macro_delete_all(base_app): + out = run_cmd(base_app, 'macro delete --all') + assert out == normalize("All macros deleted") + +def test_macro_delete_non_existing(base_app, capsys): + run_cmd(base_app, 'macro delete fake') + out, err = capsys.readouterr() + assert "Macro 'fake' does not exist" in err + +def test_macro_delete_no_name(base_app, capsys): + out = run_cmd(base_app, 'macro delete') + assert "Usage: macro delete" in out[0] + +def test_multiple_macros(base_app): + macro1 = 'h1' + macro2 = 'h2' + run_cmd(base_app, 'macro create {} help'.format(macro1)) + run_cmd(base_app, 'macro create {} help -v'.format(macro2)) + out = run_cmd(base_app, macro1) + expected = normalize(BASE_HELP) + assert out == expected + + out = run_cmd(base_app, macro2) + expected = normalize(BASE_HELP_VERBOSE) + assert out == expected + +def test_nonexistent_macro(base_app, capsys): + from cmd2.parsing import StatementParser + exception = None + + try: + base_app._run_macro(StatementParser().parse('fake')) + except KeyError as e: + exception = e + + assert exception is not None + def test_ppaged(base_app): msg = 'testing...' end = '\n' base_app.ppaged(msg) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() assert out == msg + end # we override cmd.parseline() so we always get consistent @@ -1841,14 +2130,14 @@ def test_readline_remove_history_item(base_app): def test_onecmd_raw_str_continue(base_app): line = "help" stop = base_app.onecmd(line) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() assert not stop assert out.strip() == BASE_HELP.strip() def test_onecmd_raw_str_quit(base_app): line = "quit" stop = base_app.onecmd(line) - out = base_app.stdout.buffer + out = base_app.stdout.getvalue() assert stop assert out == '' @@ -1925,12 +2214,11 @@ def test_bad_history_file_path(capsys, request): assert 'readline cannot read' in err - 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_load', 'alias', 'edit', 'eof', 'eos', 'help', 'history', 'load', 'py', 'pyscript', - 'quit', 'set', 'shell', 'shortcuts', 'unalias'] + expected_commands = ['_relative_load', 'alias', 'edit', 'eof', 'eos', 'help', 'history', 'load', 'macro', + 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts'] assert commands == expected_commands def test_get_help_topics(base_app): @@ -1978,7 +2266,7 @@ def test_exit_code_default(exit_code_repl): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = exit_code_repl app.use_rawinput = True - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='exit') @@ -1990,14 +2278,14 @@ def test_exit_code_default(exit_code_repl): with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected def test_exit_code_nonzero(exit_code_repl): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = exit_code_repl app.use_rawinput = True - app.stdout = StdOut() + app.stdout = utils.StdSim(app.stdout) # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='exit 23') @@ -2010,5 +2298,138 @@ def test_exit_code_nonzero(exit_code_repl): # Run the command loop with pytest.raises(SystemExit): app.cmdloop() - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected + + +class ColorsApp(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def do_echo(self, args): + self.poutput(args) + self.perror(args, False) + + def do_echo_error(self, args): + color_on = Fore.RED + Back.BLACK + color_off = Style.RESET_ALL + self.poutput(color_on + args + color_off) + # perror uses colors by default + self.perror(args, False) + +def test_colors_default(): + app = ColorsApp() + assert app.colors == cmd2.constants.COLORS_TERMINAL + +def test_colors_pouterr_always_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_ALWAYS + mocker.patch.object(app.stdout, 'isatty', return_value=True) + mocker.patch.object(sys.stderr, 'isatty', return_value=True) + + app.onecmd_plus_hooks('echo_error oopsie') + out, err = capsys.readouterr() + # if colors are on, the output should have some escape sequences in it + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + # errors always have colors + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_pouterr_always_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_ALWAYS + mocker.patch.object(app.stdout, 'isatty', return_value=False) + mocker.patch.object(sys.stderr, 'isatty', return_value=False) + + app.onecmd_plus_hooks('echo_error oopsie') + out, err = capsys.readouterr() + # if colors are on, the output should have some escape sequences in it + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + # errors always have colors + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_terminal_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_TERMINAL + mocker.patch.object(app.stdout, 'isatty', return_value=True) + mocker.patch.object(sys.stderr, 'isatty', return_value=True) + + app.onecmd_plus_hooks('echo_error oopsie') + # if colors are on, the output should have some escape sequences in it + out, err = capsys.readouterr() + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_terminal_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_TERMINAL + mocker.patch.object(app.stdout, 'isatty', return_value=False) + mocker.patch.object(sys.stderr, 'isatty', return_value=False) + + app.onecmd_plus_hooks('echo_error oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + +def test_colors_never_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_NEVER + mocker.patch.object(app.stdout, 'isatty', return_value=True) + mocker.patch.object(sys.stderr, 'isatty', return_value=True) + + app.onecmd_plus_hooks('echo_error oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + +def test_colors_never_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_NEVER + mocker.patch.object(app.stdout, 'isatty', return_value=False) + mocker.patch.object(sys.stderr, 'isatty', return_value=False) + + app.onecmd_plus_hooks('echo_error oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' diff --git a/tests/test_completion.py b/tests/test_completion.py index 00a120cc..1b7b65d2 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -15,7 +15,7 @@ import sys import pytest import cmd2 from cmd2 import utils -from .conftest import complete_tester, StdOut +from .conftest import base_app, complete_tester, normalize, run_cmd from examples.subcommands import SubcommandsExample # List of strings used with completion functions @@ -98,11 +98,10 @@ def test_complete_empty_arg(cmd2_app): endidx = len(line) begidx = endidx - len(text) - expected = sorted(cmd2_app.complete_help(text, line, begidx, endidx)) + expected = sorted(cmd2_app.get_visible_commands()) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - cmd2_app.completion_matches == expected + assert first_match is not None and cmd2_app.completion_matches == expected def test_complete_bogus_command(cmd2_app): text = '' @@ -113,13 +112,24 @@ def test_complete_bogus_command(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None +def test_complete_macro(base_app, request): + # Create the macro + out = run_cmd(base_app, 'macro create fake pyscript {1}') + assert out == normalize("Macro 'fake' created") + + # Macros do path completion + test_dir = os.path.dirname(request.module.__file__) + + text = os.path.join(test_dir, 's') + line = 'fake {}'.format(text) -def test_cmd2_command_completion_single(cmd2_app): - text = 'hel' - line = text endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] + + expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] + first_match = complete_tester(text, line, begidx, endidx, base_app) + assert first_match is not None and base_app.completion_matches + def test_cmd2_command_completion_multiple(cmd2_app): text = 'h' @@ -141,7 +151,11 @@ def test_cmd2_help_completion_single(cmd2_app): line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help'] + + first_match = complete_tester(text, line, begidx, endidx, 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' @@ -149,15 +163,18 @@ def test_cmd2_help_completion_multiple(cmd2_app): endidx = len(line) begidx = endidx - len(text) - matches = sorted(cmd2_app.complete_help(text, line, begidx, endidx)) - assert matches == ['help', 'history'] + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + assert first_match is not None and cmd2_app.completion_matches == ['help', 'history'] + def test_cmd2_help_completion_nomatch(cmd2_app): text = 'fakecommand' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_help(text, line, begidx, endidx) == [] + + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + assert first_match is None def test_shell_command_completion_shortcut(cmd2_app): @@ -191,7 +208,9 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] + + 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": @@ -204,21 +223,27 @@ def test_shell_command_completion_multiple(cmd2_app): line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert expected in cmd2_app.complete_shell(text, line, begidx, endidx) + + 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) endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] + + 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) endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] + + 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__) @@ -229,7 +254,8 @@ def test_shell_command_completion_does_path_completion_when_after_command(cmd2_a endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.complete_shell(text, line, begidx, endidx) == [text + '.py'] + 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_path_completion_single_end(cmd2_app, request): @@ -694,8 +720,7 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): @pytest.fixture def sc_app(): c = SubcommandsExample() - c.stdout = StdOut() - + c.stdout = utils.StdSim(c.stdout) return c def test_cmd2_subcommand_completion_single_end(sc_app): @@ -733,7 +758,11 @@ def test_cmd2_help_subcommand_completion_single(sc_app): line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert sc_app.complete_help(text, line, begidx, endidx) == ['base'] + + first_match = complete_tester(text, line, begidx, endidx, 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_cmd2_help_subcommand_completion_multiple(sc_app): text = '' @@ -741,8 +770,8 @@ def test_cmd2_help_subcommand_completion_multiple(sc_app): endidx = len(line) begidx = endidx - len(text) - matches = sorted(sc_app.complete_help(text, line, begidx, endidx)) - assert matches == ['bar', 'foo', 'sport'] + 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_help_subcommand_completion_nomatch(sc_app): @@ -750,7 +779,9 @@ def test_cmd2_help_subcommand_completion_nomatch(sc_app): line = 'help base {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert sc_app.complete_help(text, line, begidx, endidx) == [] + + 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 @@ -892,7 +923,11 @@ def test_cmd2_help_subcommand_completion_single_scu(scu_app): line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert scu_app.complete_help(text, line, begidx, endidx) == ['base'] + + first_match = complete_tester(text, line, begidx, endidx, scu_app) + + # It is at end of line, so extra space is present + assert first_match is not None and scu_app.completion_matches == ['base '] def test_cmd2_help_subcommand_completion_multiple_scu(scu_app): @@ -901,8 +936,34 @@ def test_cmd2_help_subcommand_completion_multiple_scu(scu_app): endidx = len(line) begidx = endidx - len(text) - matches = sorted(scu_app.complete_help(text, line, begidx, endidx)) - assert matches == ['bar', 'foo', 'sport'] + first_match = complete_tester(text, line, begidx, endidx, scu_app) + assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + +def test_cmd2_help_subcommand_completion_with_flags_before_command(scu_app): + text = '' + line = 'help -h -v base {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, scu_app) + assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + +def test_complete_help_subcommand_with_no_command(scu_app): + # No command because not enough tokens + text = '' + line = 'help ' + endidx = len(line) + begidx = endidx - len(text) + + assert not scu_app.complete_help_subcommand(text, line, begidx, endidx) + + # No command because everything is a flag + text = '-v' + line = 'help -f -v' + endidx = len(line) + begidx = endidx - len(text) + + assert not scu_app.complete_help_subcommand(text, line, begidx, endidx) def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app): @@ -910,7 +971,9 @@ def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app): line = 'help base {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert scu_app.complete_help(text, line, begidx, endidx) == [] + + first_match = complete_tester(text, line, begidx, endidx, scu_app) + assert first_match == None def test_subcommand_tab_completion_scu(scu_app): diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 9cf9429a..1fa460a5 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -754,3 +754,104 @@ def test_statement_is_immutable(): statement.args = 'bar' with pytest.raises(attr.exceptions.FrozenInstanceError): statement.raw = 'baz' + + +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 + matches = pattern.findall('{5}') + assert matches == ['{5}'] + + matches = pattern.findall('{233}') + assert matches == ['{233}'] + + matches = pattern.findall('{{{{{4}') + assert matches == ['{4}'] + + matches = pattern.findall('{2}}}}}') + assert matches == ['{2}'] + + matches = pattern.findall('{3}{4}{5}') + assert matches == ['{3}', '{4}', '{5}'] + + matches = pattern.findall('{3} {4} {5}') + assert matches == ['{3}', '{4}', '{5}'] + + matches = pattern.findall('{3} {{{4} {5}}}}') + assert matches == ['{3}', '{4}', '{5}'] + + matches = pattern.findall('{3} text {4} stuff {5}}}}') + assert matches == ['{3}', '{4}', '{5}'] + + # Unicode digit + matches = pattern.findall('{\N{ARABIC-INDIC DIGIT ONE}}') + assert matches == ['{\N{ARABIC-INDIC DIGIT ONE}}'] + + # Invalid strings + matches = pattern.findall('5') + assert not matches + + matches = pattern.findall('{5') + assert not matches + + matches = pattern.findall('5}') + assert not matches + + matches = pattern.findall('{{5}}') + assert not matches + + matches = pattern.findall('{5text}') + assert not matches + +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 + matches = pattern.findall('{{5}}') + assert matches == ['{{5}}'] + + matches = pattern.findall('{{233}}') + assert matches == ['{{233}}'] + + matches = pattern.findall('{{{{{4}}') + assert matches == ['{{4}}'] + + matches = pattern.findall('{{2}}}}}') + assert matches == ['{{2}}'] + + matches = pattern.findall('{{3}}{{4}}{{5}}') + assert matches == ['{{3}}', '{{4}}', '{{5}}'] + + matches = pattern.findall('{{3}} {{4}} {{5}}') + assert matches == ['{{3}}', '{{4}}', '{{5}}'] + + matches = pattern.findall('{{3}} {{{4}} {{5}}}}') + assert matches == ['{{3}}', '{{4}}', '{{5}}'] + + matches = pattern.findall('{{3}} text {{4}} stuff {{5}}}}') + assert matches == ['{{3}}', '{{4}}', '{{5}}'] + + # Unicode digit + matches = pattern.findall('{{\N{ARABIC-INDIC DIGIT ONE}}}') + assert matches == ['{{\N{ARABIC-INDIC DIGIT ONE}}}'] + + # Invalid strings + matches = pattern.findall('5') + assert not matches + + matches = pattern.findall('{{5') + assert not matches + + matches = pattern.findall('5}}') + assert not matches + + matches = pattern.findall('{5}') + assert not matches + + matches = pattern.findall('{{5text}}') + assert not matches diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e401e837..81dd7683 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -5,18 +5,14 @@ Test plugin infrastructure and hooks. Copyright 2018 Jared Crapo <jared@kotfu.net> Released under MIT license, see LICENSE file """ - -from typing import Tuple - import pytest import cmd2 from cmd2 import plugin -from .conftest import StdOut class Plugin: - "A mixin class for testing hook registration and calling" + """A mixin class for testing hook registration and calling""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.reset_counters() @@ -35,19 +31,19 @@ class Plugin: # ### def prepost_hook_one(self) -> None: - "Method used for preloop or postloop hooks" + """Method used for preloop or postloop hooks""" self.poutput("one") def prepost_hook_two(self) -> None: - "Another method used for preloop or postloop hooks" + """Another method used for preloop or postloop hooks""" self.poutput("two") def prepost_hook_too_many_parameters(self, param) -> None: - "A preloop or postloop hook with too many parameters" + """A preloop or postloop hook with too many parameters""" pass def prepost_hook_with_wrong_return_annotation(self) -> bool: - "A preloop or postloop hook with incorrect return type" + """A preloop or postloop hook with incorrect return type""" pass ### @@ -55,10 +51,10 @@ class Plugin: # preparse hook # ### - def preparse(self, line: str) -> str: - "Preparsing hook" + def preparse(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: + """Preparsing hook""" self.called_preparse += 1 - return line + return data ### # @@ -66,44 +62,44 @@ class Plugin: # ### def postparse_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - "A postparsing hook" + """A postparsing hook""" self.called_postparsing += 1 return data def postparse_hook_stop(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - "A postparsing hook with requests application exit" + """A postparsing hook with requests application exit""" self.called_postparsing += 1 data.stop = True return data def postparse_hook_emptystatement(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - "A postparsing hook with raises an EmptyStatement exception" + """A postparsing hook with raises an EmptyStatement exception""" self.called_postparsing += 1 raise cmd2.EmptyStatement def postparse_hook_exception(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - "A postparsing hook which raises an exception" + """A postparsing hook which raises an exception""" self.called_postparsing += 1 raise ValueError def postparse_hook_too_many_parameters(self, data1, data2) -> cmd2.plugin.PostparsingData: - "A postparsing hook with too many parameters" + """A postparsing hook with too many parameters""" pass def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData: - "A postparsing hook with an undeclared parameter type" + """A postparsing hook with an undeclared parameter type""" pass def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData: - "A postparsing hook with the wrong parameter type" + """A postparsing hook with the wrong parameter type""" pass def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData): - "A postparsing hook with an undeclared return type" + """A postparsing hook with an undeclared return type""" pass def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str: - "A postparsing hook with the wrong return type" + """A postparsing hook with the wrong return type""" pass ### @@ -112,43 +108,43 @@ class Plugin: # ### def precmd(self, statement: cmd2.Statement) -> cmd2.Statement: - "Override cmd.Cmd method" + """Override cmd.Cmd method""" self.called_precmd += 1 return statement def precmd_hook(self, data: plugin.PrecommandData) -> plugin.PrecommandData: - "A precommand hook" + """A precommand hook""" self.called_precmd += 1 return data def precmd_hook_emptystatement(self, data: plugin.PrecommandData) -> plugin.PrecommandData: - "A precommand hook which raises an EmptyStatement exception" + """A precommand hook which raises an EmptyStatement exception""" self.called_precmd += 1 raise cmd2.EmptyStatement def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.PrecommandData: - "A precommand hook which raises an exception" + """A precommand hook which raises an exception""" self.called_precmd += 1 raise ValueError def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData: - "A precommand hook with no parameters" + """A precommand hook with no parameters""" pass def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData: - "A precommand hook with too many parameters" + """A precommand hook with too many parameters""" return one def precmd_hook_no_parameter_annotation(self, data) -> plugin.PrecommandData: - "A precommand hook with no type annotation on the parameter" + """A precommand hook with no type annotation on the parameter""" return data def precmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PrecommandData: - "A precommand hook with the incorrect type annotation on the parameter" + """A precommand hook with the incorrect type annotation on the parameter""" return data def precmd_hook_no_return_annotation(self, data: plugin.PrecommandData): - "A precommand hook with no type annotation on the return value" + """A precommand hook with no type annotation on the return value""" return data def precmd_hook_wrong_return_annotation(self, data: plugin.PrecommandData) -> cmd2.Statement: @@ -160,38 +156,38 @@ class Plugin: # ### def postcmd(self, stop: bool, statement: cmd2.Statement) -> bool: - "Override cmd.Cmd method" + """Override cmd.Cmd method""" self.called_postcmd += 1 return stop def postcmd_hook(self, data: plugin.PostcommandData) -> plugin.PostcommandData: - "A postcommand hook" + """A postcommand hook""" self.called_postcmd += 1 return data def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.PostcommandData: - "A postcommand hook with raises an exception" + """A postcommand hook with raises an exception""" self.called_postcmd += 1 raise ZeroDivisionError def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData: - "A precommand hook with no parameters" + """A precommand hook with no parameters""" pass def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData: - "A precommand hook with too many parameters" + """A precommand hook with too many parameters""" return one def postcmd_hook_no_parameter_annotation(self, data) -> plugin.PostcommandData: - "A precommand hook with no type annotation on the parameter" + """A precommand hook with no type annotation on the parameter""" return data def postcmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PostcommandData: - "A precommand hook with the incorrect type annotation on the parameter" + """A precommand hook with the incorrect type annotation on the parameter""" return data def postcmd_hook_no_return_annotation(self, data: plugin.PostcommandData): - "A precommand hook with no type annotation on the return value" + """A precommand hook with no type annotation on the return value""" return data def postcmd_hook_wrong_return_annotation(self, data: plugin.PostcommandData) -> cmd2.Statement: @@ -208,13 +204,13 @@ class Plugin: return data def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData: - "A postparsing hook which requests application exit" + """A postparsing hook which requests application exit""" self.called_cmdfinalization += 1 data.stop = True return data def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData: - "A postparsing hook which raises an exception" + """A postparsing hook which raises an exception""" self.called_cmdfinalization += 1 raise ValueError @@ -244,7 +240,7 @@ class Plugin: class PluggedApp(Plugin, cmd2.Cmd): - "A sample app with a plugin mixed in" + """A sample app with a plugin mixed in""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -326,6 +322,7 @@ def test_postloop_hooks(capsys): ### def test_preparse(capsys): app = PluggedApp() + app.register_postparsing_hook(app.preparse) app.onecmd_plus_hooks('say hello') out, err = capsys.readouterr() assert out == 'hello\n' diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py index 73c1a62a..36e48598 100644 --- a/tests/test_pyscript.py +++ b/tests/test_pyscript.py @@ -1,3 +1,4 @@ +# coding=utf-8 """ Unit/functional testing for argparse completer in cmd2 @@ -8,8 +9,9 @@ import os import pytest from cmd2.cmd2 import Cmd, with_argparser from cmd2 import argparse_completer -from .conftest import run_cmd, StdOut -from cmd2.utils import namedtuple_with_defaults +from .conftest import run_cmd +from cmd2.utils import namedtuple_with_defaults, StdSim + class PyscriptExample(Cmd): ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] @@ -18,7 +20,7 @@ class PyscriptExample(Cmd): if not args.command: self.do_help('media movies') else: - print('media movies ' + str(args.__dict__)) + self.poutput('media movies ' + str(args.__dict__)) def _do_media_shows(self, args) -> None: if not args.command: @@ -27,7 +29,7 @@ class PyscriptExample(Cmd): if not args.command: self.do_help('media shows') else: - print('media shows ' + str(args.__dict__)) + self.poutput('media shows ' + str(args.__dict__)) media_parser = argparse_completer.ACArgumentParser(prog='media') @@ -82,7 +84,7 @@ class PyscriptExample(Cmd): @with_argparser(foo_parser) def do_foo(self, args): - print('foo ' + str(args.__dict__)) + self.poutput('foo ' + str(sorted(args.__dict__))) if self._in_py: FooResult = namedtuple_with_defaults('FooResult', ['counter', 'trueval', 'constval', @@ -108,14 +110,13 @@ class PyscriptExample(Cmd): out += '{' for key in keys: out += "'{}':'{}'".format(key, arg_dict[key]) - print(out) + self.poutput(out) @pytest.fixture def ps_app(): c = PyscriptExample() - c.stdout = StdOut() - + c.stdout = StdSim(c.stdout) return c @@ -125,14 +126,13 @@ class PyscriptCustomNameExample(Cmd): self.pyscript_name = 'custom' def do_echo(self, out): - print(out) + self.poutput(out) @pytest.fixture def ps_echo(): c = PyscriptCustomNameExample() - c.stdout = StdOut() - + c.stdout = StdSim(c.stdout) return c @@ -140,7 +140,7 @@ def ps_echo(): ('help', 'help.py'), ('help media', 'help_media.py'), ]) -def test_pyscript_help(ps_app, capsys, request, command, pyscript_file): +def test_pyscript_help(ps_app, request, command, pyscript_file): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', pyscript_file) expected = run_cmd(ps_app, command) @@ -169,16 +169,14 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file): ('foo 11 22 33 44 55 66 -ccc', 'foo3.py'), ('bar 11 22', 'bar1.py'), ]) -def test_pyscript_out(ps_app, capsys, request, command, pyscript_file): +def test_pyscript_out(ps_app, request, command, pyscript_file): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', pyscript_file) - run_cmd(ps_app, command) - expected, _ = capsys.readouterr() + expected = run_cmd(ps_app, command) + assert expected - assert len(expected) > 0 - run_cmd(ps_app, 'pyscript {}'.format(python_script)) - out, _ = capsys.readouterr() - assert len(out) > 0 + out = run_cmd(ps_app, 'pyscript {}'.format(python_script)) + assert out assert out == expected @@ -212,7 +210,7 @@ def test_pyscript_results(ps_app, capsys, request, pyscript_file, exp_out): @pytest.mark.parametrize('expected, pyscript_file', [ - ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts', 'unalias']", + ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'macro', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts']", 'pyscript_dir1.py'), ("['movies', 'shows']", 'pyscript_dir2.py') ]) @@ -227,14 +225,12 @@ def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file): assert out == expected -def test_pyscript_custom_name(ps_echo, capsys, request): +def test_pyscript_custom_name(ps_echo, request): message = 'blah!' test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py') - run_cmd(ps_echo, 'pyscript {}'.format(python_script)) - expected, _ = capsys.readouterr() - assert len(expected) > 0 - expected = expected.splitlines() - assert message == expected[0] + out = run_cmd(ps_echo, 'pyscript {}'.format(python_script)) + assert out + assert message == out[0] diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 84b9cec8..9a63cf09 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -16,8 +16,10 @@ from unittest import mock import pytest import cmd2 -from .conftest import run_cmd, StdOut +from .conftest import run_cmd from cmd2 import transcript +from cmd2.utils import StdSim + class CmdLineApp(cmd2.Cmd): @@ -87,9 +89,9 @@ def test_commands_at_invocation(): expected = "This is an intro banner ...\nhello\nGracie\n" with mock.patch.object(sys, 'argv', testargs): app = CmdLineApp() - app.stdout = StdOut() + app.stdout = StdSim(app.stdout) app.cmdloop() - out = app.stdout.buffer + out = app.stdout.getvalue() assert out == expected @pytest.mark.parametrize('filename,feedback_to_output', [ @@ -134,7 +136,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): def test_history_transcript(request, capsys): app = CmdLineApp() - app.stdout = StdOut() + app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') run_cmd(app, 'speak /tmp/file.txt is not a regex') @@ -155,13 +157,13 @@ this is a \/multiline\/ command # read in the transcript created by the history command with open(history_fname) as f: - transcript = f.read() + xscript = f.read() - assert transcript == expected + assert xscript == expected def test_history_transcript_bad_filename(request, capsys): app = CmdLineApp() - app.stdout = StdOut() + app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') run_cmd(app, 'speak /tmp/file.txt is not a regex') diff --git a/tests/test_utils.py b/tests/test_utils.py index 61fd8373..53031567 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,10 @@ Unit testing for cmd2/utils.py module. Copyright 2018 Todd Leonhardt <todd.leonhardt@gmail.com> Released under MIT license, see LICENSE file """ +import sys + +import pytest + from colorama import Fore import cmd2.utils as cu @@ -13,7 +17,7 @@ HELLO_WORLD = 'Hello, world!' def test_strip_ansi(): base_str = HELLO_WORLD - ansi_str = Fore.GREEN + base_str + Fore.RESET + ansi_str = Fore.GREEN + base_str + Fore.RESET assert base_str != ansi_str assert base_str == cu.strip_ansi(ansi_str) @@ -48,5 +52,117 @@ def test_unicode_casefold(): assert cu.norm_fold(micro) == cu.norm_fold(micro_cf) def test_alphabetical_sort(): - my_list = ['café', 'µ', 'A' , 'micro', 'unity', 'cafeteria'] + 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 + str1 = 'ABC' + assert cu.try_int_or_force_to_lower_case(str1) == 'abc' + str1 = 'X19' + assert cu.try_int_or_force_to_lower_case(str1) == 'x19' + 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) + assert my_list == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ'] + my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] + 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) + your_str = "of the emergengy broadcast system'" + assert not cu.is_quoted(your_str) + simple_str = "hello world" + assert not cu.is_quoted(simple_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_quot_string_if_needed_no(): + my_str = "HelloWorld" + assert cu.quote_string_if_needed(my_str) == my_str + your_str = "'Hello World'" + assert cu.quote_string_if_needed(your_str) == your_str + + +@pytest.fixture +def stdout_sim(): + stdsim = cu.StdSim(sys.stdout) + return stdsim + +def test_stdsim_write_str(stdout_sim): + my_str = 'Hello World' + 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() + +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) + # getvalue() returns the value and leaves it unaffected internally + assert stdout_sim.getvalue() == my_str + # read() returns the value and then clears the internal buffer + assert stdout_sim.read() == my_str + assert stdout_sim.getvalue() == '' + +def test_stdsim_clear(stdout_sim): + my_str = 'Hello World' + stdout_sim.write(my_str) + assert stdout_sim.getvalue() == my_str + 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' + stdout_sim.write(my_str) + 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() + diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt index e3ccbc5e..8c0dd007 100644 --- a/tests/transcripts/from_cmdloop.txt +++ b/tests/transcripts/from_cmdloop.txt @@ -5,8 +5,8 @@ Documented commands (type help <topic>): ======================================== -alias help load nothing py quit set shortcuts unalias/ */ -edit history mumble orate pyscript say shell speak/ */ +alias help load mumble orate pyscript say shell speak/ */ +edit history macro nothing py quit set shortcuts/ */ (Cmd) help say usage: speak [-h] [-p] [-s] [-r REPEAT]/ */ diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index b818c464..d45672a7 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -1,10 +1,10 @@ # Run this transcript with "python example.py -t transcript_regex.txt" -# The regex for colors is because no color on Windows. +# The regex for colors shows all possible settings for colors # The regex for editor will match whatever program you use. # Regexes on prompts just make the trailing space obvious (Cmd) set -colors: /(True|False)/ +colors: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False |
