# coding=utf-8 """ Cmd2 unit/functional testing Copyright 2016 Federico Ceratto Released under MIT license, see LICENSE file """ import argparse import builtins from code import InteractiveConsole import os import sys import io import tempfile import pytest # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available try: import mock except ImportError: from unittest import mock import cmd2 #from conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ # HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut def test_ver(): assert cmd2.__version__ == '0.9.0' def test_empty_statement(base_app): out = run_cmd(base_app, '') expected = normalize('') assert out == expected def test_base_help(base_app): out = run_cmd(base_app, 'help') expected = normalize(BASE_HELP) assert out == expected def test_base_help_verbose(base_app): out = run_cmd(base_app, 'help -v') expected = normalize(BASE_HELP_VERBOSE) assert out == expected out = run_cmd(base_app, 'help --verbose') assert out == expected def test_base_help_history(base_app): out = run_cmd(base_app, 'help history') assert out == normalize(HELP_HISTORY) def test_base_argparse_help(base_app, capsys): # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense run_cmd(base_app, 'set -h') out, err = capsys.readouterr() out1 = normalize(str(out)) out2 = run_cmd(base_app, 'help set') assert out1 == out2 assert out1[0].startswith('usage: set') assert out1[1] == '' assert out1[2].startswith('Sets a settable parameter') def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'set -z') out, err = capsys.readouterr() expected = ['usage: set [-h] [-a] [-l] [settable [settable ...]]', 'set: error: unrecognized arguments: -z'] assert normalize(str(err)) == expected def test_base_shortcuts(base_app): out = run_cmd(base_app, 'shortcuts') expected = normalize(SHORTCUTS_TXT) assert out == expected def test_base_show(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out = run_cmd(base_app, 'set') expected = normalize(SHOW_TXT) assert out == expected def test_base_show_long(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out = run_cmd(base_app, 'set -l') expected = normalize(SHOW_LONG) assert out == expected def test_base_show_readonly(base_app): base_app.editor = 'vim' out = run_cmd(base_app, 'set -a') expected = normalize(SHOW_TXT + '\nRead only settings:' + """ Commands may be terminated with: {} Arguments at invocation allowed: {} Output redirection and pipes allowed: {} Parsing of command arguments: Shell lexer mode for command argument splitting: {} Strip Quotes after splitting arguments: {} """.format(base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection, "POSIX" if cmd2.POSIX_SHLEX else "non-POSIX", "True" if cmd2.STRIP_QUOTES_FOR_NON_POSIX and not cmd2.POSIX_SHLEX else "False")) assert out == expected def test_base_set(base_app): out = run_cmd(base_app, 'set quiet True') expected = normalize(""" quiet - was: False now: True """) assert out == expected out = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] def test_set_not_supported(base_app, capsys): run_cmd(base_app, 'set qqq True') out, err = capsys.readouterr() expected = normalize(""" EXCEPTION of type 'LookupError' occurred with message: 'Parameter 'qqq' not supported (type 'show' for list of parameters).' To enable full traceback, run the following command: 'set debug true' """) assert normalize(str(err)) == expected def test_set_quiet(base_app): out = run_cmd(base_app, 'set quie True') expected = normalize(""" quiet - was: False now: True """) assert out == expected out = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] def test_base_shell(base_app, monkeypatch): m = mock.Mock() monkeypatch.setattr("{}.Popen".format('subprocess'), m) out = run_cmd(base_app, 'shell echo a') assert out == [] assert m.called def test_base_py(base_app, capsys): run_cmd(base_app, 'py qqq=3') out, err = capsys.readouterr() assert out == '' run_cmd(base_app, 'py print(qqq)') out, err = capsys.readouterr() assert out.rstrip() == '3' @pytest.mark.skipif(sys.platform == 'win32', reason="Unit test doesn't work on win32, but feature does") def test_base_run_python_script(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') expected = 'This is a python script running ...\n' run_cmd(base_app, "py run('{}')".format(python_script)) out, err = capsys.readouterr() assert out == expected def test_base_run_pyscript(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') expected = 'This is a python script running ...\n' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert out == expected def test_recursive_pyscript_not_allowed(base_app, capsys, 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' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err == expected def test_pyscript_with_nonexist_file(base_app, capsys): python_script = 'does_not_exist.py' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err.startswith('ERROR: [Errno 2] No such file or directory:') def test_pyscript_with_exception(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py') run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err.startswith('Traceback') assert err.endswith("TypeError: unsupported operand type(s) for +: 'int' and 'str'\n") 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 ...') def test_base_error(base_app): out = run_cmd(base_app, 'meow') assert out == ["*** Unknown syntax: meow"] def test_base_history(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected out = run_cmd(base_app, 'history he') expected = normalize(""" -------------------------[1] help """) assert out == expected out = run_cmd(base_app, 'history sh') expected = normalize(""" -------------------------[2] shortcuts """) assert out == expected def test_history_script_format(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history -s') expected = normalize(""" help shortcuts """) assert out == expected def test_history_with_string_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history help') expected = normalize(""" -------------------------[1] help -------------------------[3] help history """) assert out == expected def test_history_with_integer_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history 1') expected = normalize(""" -------------------------[1] help """) assert out == expected def test_history_with_integer_span(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history 1..2') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected def test_history_with_span_start(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history 2:') expected = normalize(""" -------------------------[2] shortcuts -------------------------[3] help history """) assert out == expected def test_history_with_span_end(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history :2') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected def test_history_with_span_index_error(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, '!ls -hal :') out = run_cmd(base_app, 'history "hal :"') expected = normalize(""" -------------------------[3] !ls -hal : """) assert out == expected def test_history_output_file(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') fd, fname = tempfile.mkstemp(prefix='', suffix='.txt') os.close(fd) run_cmd(base_app, 'history -o "{}"'.format(fname)) expected = normalize('\n'.join(['help', 'shortcuts', 'help history'])) with open(fname) as f: content = normalize(f.read()) assert content == expected def test_history_edit(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really # going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) # Run help command just so we have a command in history run_cmd(base_app, 'help') run_cmd(base_app, 'history -e 1') # We have an editor, so should expect a system call m.assert_called_once() def test_history_run_all_commands(base_app): # make sure we refuse to run all commands as a default run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history -r') # this should generate an error, but we don't currently have a way to # capture stderr in these tests. So we assume that if we got nothing on # standard out, that the error occured because if the commaned executed # then we should have a list of shortcuts in our output assert out == [] def test_history_run_one_command(base_app): expected = run_cmd(base_app, 'help') output = run_cmd(base_app, 'history -r 1') assert output == expected def test_base_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) assert base_app.cmdqueue == ['help history', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir def test_load_with_empty_args(base_app, capsys): # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load') out, err = capsys.readouterr() # The load command requires a file path argument, so we should get an error message expected = normalize("""ERROR: load command requires a file path:\n""") assert normalize(str(err)) == expected assert base_app.cmdqueue == [] def test_load_with_nonexistent_file(base_app, capsys): # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load does_not_exist.txt') out, err = capsys.readouterr() # The load command requires a path to an existing file assert str(err).startswith("ERROR") assert "does not exist or is not a file" in str(err) assert base_app.cmdqueue == [] def test_load_with_empty_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'empty.txt') # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load {}'.format(filename)) out, err = capsys.readouterr() # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is empty" in str(err) assert base_app.cmdqueue == [] def test_load_with_binary_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'binary.bin') # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load {}'.format(filename)) out, err = capsys.readouterr() # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is not an ASCII or UTF-8 encoded text file" in str(err) assert base_app.cmdqueue == [] def test_load_with_utf8_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) assert base_app.cmdqueue == ['!echo γνωρίζω', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir def test_load_nested_loads(base_app, request): # Verify that loading a script with nested load commands works correctly, # and loads the nested script commands in the correct order. The recursive # loads don't happen all at once, but as the commands are interpreted. So, # we will need to drain the cmdqueue and inspect the stdout to see if all # steps were executed in the expected order. test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'nested.txt') assert base_app.cmdqueue == [] # Load the top level script and then run the command queue until all # commands have been exhausted. initial_load = 'load ' + filename run_cmd(base_app, initial_load) while base_app.cmdqueue: base_app.onecmd_plus_hooks(base_app.cmdqueue.pop(0)) # Check that the right commands were executed. expected = """ %s _relative_load precmds.txt set colors on help shortcuts _relative_load postcmds.txt set colors off""" % initial_load assert run_cmd(base_app, 'history -s') == normalize(expected) def test_base_runcmds_plus_hooks(base_app, request): # Make sure that runcmds_plus_hooks works as intended. I.E. to run multiple # commands and process any commands added, by them, to the command queue. test_dir = os.path.dirname(request.module.__file__) prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt') postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt') assert base_app.cmdqueue == [] base_app.runcmds_plus_hooks(['load ' + prefilepath, 'help', 'shortcuts', 'load ' + postfilepath]) expected = """ load %s set colors on help shortcuts load %s set colors off""" % (prefilepath, postfilepath) assert run_cmd(base_app, 'history -s') == normalize(expected) def test_base_relative_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, '_relative_load {}'.format(filename)) assert base_app.cmdqueue == ['help history', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir 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 base_app.cmdqueue == [] def test_output_redirection(base_app): fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(fd) try: # Verify that writing to a file works run_cmd(base_app, 'help > {}'.format(filename)) expected = normalize(BASE_HELP) with open(filename) as f: content = normalize(f.read()) assert content == expected # Verify that appending to a file also works run_cmd(base_app, 'help history >> {}'.format(filename)) expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) with open(filename) as f: content = normalize(f.read()) assert content == expected except: raise finally: os.remove(filename) def test_feedback_to_output_true(base_app): base_app.feedback_to_output = True base_app.timing = True f, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(f) try: run_cmd(base_app, 'help > {}'.format(filename)) with open(filename) as f: content = f.readlines() assert content[-1].startswith('Elapsed: ') except: raise finally: os.remove(filename) def test_feedback_to_output_false(base_app, capsys): base_app.feedback_to_output = False base_app.timing = True f, filename = tempfile.mkstemp(prefix='feedback_to_output', suffix='.txt') os.close(f) try: run_cmd(base_app, 'help > {}'.format(filename)) out, err = capsys.readouterr() with open(filename) as f: content = f.readlines() assert not content[-1].startswith('Elapsed: ') assert err.startswith('Elapsed') except: raise finally: os.remove(filename) def test_allow_redirection(base_app): # Set allow_redirection to False base_app.allow_redirection = False filename = 'test_allow_redirect.txt' # Verify output wasn't redirected out = run_cmd(base_app, 'help > {}'.format(filename)) expected = normalize(BASE_HELP) assert out == expected # Verify that no file got created assert not os.path.exists(filename) def test_input_redirection(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'redirect.txt') # NOTE: File 'redirect.txt" contains 1 word "history" # Verify that redirecting input ffom a file works out = run_cmd(base_app, 'help < {}'.format(filename)) assert out == normalize(HELP_HISTORY) def test_pipe_to_shell(base_app, capsys): if sys.platform == "win32": # Windows command = 'help | sort' else: # Mac and Linux # Get help on help and pipe it's output to the input of the word count shell command command = 'help help | wc' # # Mac and Linux wc behave the same when piped from shell, but differently when piped stdin from file directly # if sys.platform == 'darwin': # expected = "1 11 70" # else: # expected = "1 11 70" # assert out.strip() == expected.strip() run_cmd(base_app, command) out, err = capsys.readouterr() # Unfortunately with the improved way of piping output to a subprocess, there isn't any good way of getting # access to the output produced by that subprocess within a unit test, but we can verify that no error occured assert not err def test_pipe_to_shell_error(base_app, capsys): # Try to pipe command output to a shell command that doesn't exist in order to produce an error run_cmd(base_app, 'help | foobarbaz.this_does_not_exist') out, err = capsys.readouterr() assert not out expected_error = 'FileNotFoundError' assert err.startswith("EXCEPTION of type '{}' occurred with message:".format(expected_error)) @pytest.mark.skipif(not cmd2.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system") def test_send_to_paste_buffer(base_app): # Test writing to the PasteBuffer/Clipboard run_cmd(base_app, 'help >') expected = normalize(BASE_HELP) assert normalize(cmd2.get_paste_buffer()) == expected # Test appending to the PasteBuffer/Clipboard run_cmd(base_app, 'help history >>') expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) assert normalize(cmd2.get_paste_buffer()) == expected def test_base_timing(base_app, capsys): base_app.feedback_to_output = False out = run_cmd(base_app, 'set timing True') expected = normalize("""timing - was: False now: True """) assert out == expected out, err = capsys.readouterr() if sys.platform == 'win32': assert err.startswith('Elapsed: 0:00:00') else: assert err.startswith('Elapsed: 0:00:00.0') def test_base_debug(base_app, capsys): # Try to set a non-existent parameter with debug set to False by default run_cmd(base_app, 'set does_not_exist 5') out, err = capsys.readouterr() assert err.startswith('EXCEPTION') # Set debug true out = run_cmd(base_app, 'set debug True') expected = normalize(""" debug - was: False now: True """) assert out == expected # Verify that we now see the exception traceback run_cmd(base_app, 'set does_not_exist 5') out, err = capsys.readouterr() assert str(err).startswith('Traceback (most recent call last):') def test_base_colorize(base_app): # If using base_app test fixture it won't get colorized because we replaced self.stdout color_test = base_app.colorize('Test', 'red') assert color_test == 'Test' # 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' def _expected_no_editor_error(): expected_exception = 'OSError' # If PyPy, expect a different exception than with Python 3 if hasattr(sys, "pypy_translation_info"): expected_exception = 'EnvironmentError' expected_text = normalize(""" EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.' To enable full traceback, run the following command: 'set debug true' """.format(expected_exception)) return expected_text def test_edit_no_editor(base_app, capsys): # Purposely set the editor to None base_app.editor = None # Make sure we get an exception, but cmd2 handles it run_cmd(base_app, 'edit') out, err = capsys.readouterr() expected = _expected_no_editor_error() assert normalize(str(err)) == expected def test_edit_file(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') 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)) def test_edit_file_with_spaces(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'my commands.txt') 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)) def test_edit_blank(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) run_cmd(base_app, 'edit') # We have an editor, so should expect a system call m.assert_called_once() def test_base_py_interactive(base_app): # Mock out the InteractiveConsole.interact() call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='interact') InteractiveConsole.interact = m run_cmd(base_app, "py") # Make sure our mock was called once and only once m.assert_called_once() def test_exclude_from_history(base_app, monkeypatch): # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) # Run edit command run_cmd(base_app, 'edit') # Run history command run_cmd(base_app, 'history') # Verify that the history is empty out = run_cmd(base_app, 'history') assert out == [] # Now run a command which isn't excluded from the history run_cmd(base_app, 'help') # And verify we have a history now ... out = run_cmd(base_app, 'history') expected = normalize("""-------------------------[1] help""") assert out == expected def test_base_cmdloop_with_queue(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = True intro = 'Hello World, this is an intro ...' app.cmdqueue.append('quit\n') app.stdout = StdOut() # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop with custom intro app.cmdloop(intro=intro) out = app.stdout.buffer assert out == expected def test_base_cmdloop_without_queue(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = True app.intro = 'Hello World, this is an intro ...' app.stdout = 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') builtins.input = m # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = app.intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() out = app.stdout.buffer assert out == expected def test_cmdloop_without_rawinput(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = False app.echo = False app.intro = 'Hello World, this is an intro ...' app.stdout = 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') builtins.input = m # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = app.intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() out = app.stdout.buffer assert out == expected class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def postparsing_precmd(self, statement): """Simulate precmd hook failure.""" return True, statement @pytest.fixture def hook_failure(): app = HookFailureApp() app.stdout = StdOut() return app def test_precmd_hook_success(base_app): out = base_app.onecmd_plus_hooks('help') assert out is None def test_precmd_hook_failure(hook_failure): out = hook_failure.onecmd_plus_hooks('help') assert out == True class SayApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def do_say(self, arg): self.poutput(arg) @pytest.fixture def say_app(): app = SayApp() app.stdout = StdOut() return app def test_interrupt_quit(say_app): say_app.quit_on_sigint = True # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] builtins.input = m say_app.cmdloop() # And verify the expected output to stdout out = say_app.stdout.buffer assert out == 'hello\n' def test_interrupt_noquit(say_app): say_app.quit_on_sigint = False # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] builtins.input = m say_app.cmdloop() # And verify the expected output to stdout out = say_app.stdout.buffer assert out == 'hello\n^C\ngoodbye\n' class ShellApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.default_to_shell = True @pytest.fixture def shell_app(): app = ShellApp() app.stdout = StdOut() return app def test_default_to_shell_unknown(shell_app): unknown_command = 'zyxcw23' out = run_cmd(shell_app, unknown_command) assert out == ["*** Unknown syntax: {}".format(unknown_command)] def test_default_to_shell_good(capsys): app = cmd2.Cmd() app.default_to_shell = True if sys.platform.startswith('win'): line = 'dir' else: line = 'ls' statement = app.parser_manager.parsed(line) retval = app.default(statement) assert not retval out, err = capsys.readouterr() assert out == '' def test_default_to_shell_failure(capsys): app = cmd2.Cmd() app.default_to_shell = True line = 'ls does_not_exist.xyz' statement = app.parser_manager.parsed(line) retval = app.default(statement) assert not retval out, err = capsys.readouterr() assert out == "*** Unknown syntax: {}\n".format(line) def test_ansi_prompt_not_esacped(base_app): prompt = '(Cmd) ' assert base_app._surround_ansi_escapes(prompt) == prompt def test_ansi_prompt_escaped(): app = cmd2.Cmd() color = 'cyan' prompt = 'InColor' color_prompt = app.colorize(prompt, color) readline_hack_start = "\x01" readline_hack_end = "\x02" readline_safe_prompt = app._surround_ansi_escapes(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 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) class HelpApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def do_squat(self, arg): """This docstring help will never be shown because the help_squat method overrides it.""" pass def help_squat(self): self.stdout.write('This command does diddly squat...\n') def do_edit(self, arg): """This overrides the edit command and does nothing.""" pass # This command will be in the "undocumented" section of the help menu def do_undoc(self, arg): pass @pytest.fixture def help_app(): app = HelpApp() app.stdout = StdOut() return app def test_custom_command_help(help_app): out = run_cmd(help_app, 'help squat') expected = normalize('This command does diddly squat...') assert out == expected def test_custom_help_menu(help_app): out = run_cmd(help_app, 'help') expected = normalize(""" Documented commands (type help ): ======================================== alias help load pyscript set shortcuts unalias edit history py quit shell squat Undocumented commands: ====================== undoc """) assert out == expected def test_help_undocumented(help_app): out = run_cmd(help_app, 'help undoc') expected = normalize('*** No help on undoc') assert out == expected def test_help_overridden_method(help_app): out = run_cmd(help_app, 'help edit') expected = normalize('This overrides the edit command and does nothing.') assert out == expected class HelpCategoriesApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @cmd2.with_category('Some Category') def do_diddly(self, arg): """This command does diddly""" pass def do_squat(self, arg): """This docstring help will never be shown because the help_squat method overrides it.""" pass def help_squat(self): self.stdout.write('This command does diddly squat...\n') def do_edit(self, arg): """This overrides the edit command and does nothing.""" pass cmd2.categorize((do_squat, do_edit), 'Custom Category') # This command will be in the "undocumented" section of the help menu def do_undoc(self, arg): pass @pytest.fixture def helpcat_app(): app = HelpCategoriesApp() app.stdout = StdOut() return app def test_help_cat_base(helpcat_app): out = run_cmd(helpcat_app, 'help') expected = normalize("""Documented commands (type help ): Custom Category =============== edit squat Some Category ============= diddly Other ===== alias help history load py pyscript quit set shell shortcuts unalias Undocumented commands: ====================== undoc """) assert out == expected def test_help_cat_verbose(helpcat_app): out = run_cmd(helpcat_app, 'help --verbose') expected = normalize("""Documented commands (type help ): Custom Category ================================================================================ edit This overrides the edit command and does nothing. squat This command does diddly squat... Some Category ================================================================================ diddly This command does diddly Other ================================================================================ alias Define or display aliases help List available commands with "help" or detailed help with "help cmd". history View, run, edit, and save 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. shell Execute a command as if at the OS prompt. shortcuts Lists shortcuts (aliases) available. unalias Unsets aliases Undocumented commands: ====================== undoc """) assert out == expected class SelectApp(cmd2.Cmd): def do_eat(self, arg): """Eat something, with a selection of sauces to choose from.""" # Pass in a single string of space-separated selections sauce = self.select('sweet salty', 'Sauce? ') result = '{food} with {sauce} sauce, yum!' result = result.format(food=arg, sauce=sauce) self.stdout.write(result + '\n') def do_study(self, arg): """Learn something, with a selection of subjects to choose from.""" # Pass in a list of strings for selections subject = self.select(['math', 'science'], 'Subject? ') result = 'Good luck learning {}!\n'.format(subject) self.stdout.write(result) def do_procrastinate(self, arg): """Waste time in your manner of choice.""" # Pass in a list of tuples for selections leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('Porn', 'WebSurfing')], 'How would you like to procrastinate? ') result = 'Have fun procrasinating with {}!\n'.format(leisure_activity) self.stdout.write(result) def do_play(self, arg): """Play your favorite musical instrument.""" # Pass in an uneven list of tuples for selections instrument = self.select([('Guitar', 'Electric Guitar'), ('Drums',)], 'Instrument? ') result = 'Charm us with the {}...\n'.format(instrument) self.stdout.write(result) @pytest.fixture def select_app(): app = SelectApp() app.stdout = StdOut() return app def test_select_options(select_app): # 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='2') builtins.input = m food = 'bacon' out = run_cmd(select_app, "eat {}".format(food)) expected = normalize(""" 1. sweet 2. salty {} with salty sauce, yum! """.format(food)) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Sauce? ') # And verify the expected output to stdout assert out == expected def test_select_invalid_option(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') # If side_effect is an iterable then each call to the mock will return the next value from the iterable. m.side_effect = ['3', '1'] # First pass and invalid selection, then pass a valid one builtins.input = m food = 'fish' out = run_cmd(select_app, "eat {}".format(food)) expected = normalize(""" 1. sweet 2. salty 3 isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! """.format(food)) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' calls = [mock.call(arg), mock.call(arg)] m.assert_has_calls(calls) # And verify the expected output to stdout assert out == expected def test_select_list_of_strings(select_app): # 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='2') builtins.input = m out = run_cmd(select_app, "study") expected = normalize(""" 1. math 2. science Good luck learning {}! """.format('science')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Subject? ') # And verify the expected output to stdout assert out == expected def test_select_list_of_tuples(select_app): # 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='2') builtins.input = m out = run_cmd(select_app, "procrastinate") expected = normalize(""" 1. Netflix 2. WebSurfing Have fun procrasinating with {}! """.format('Porn')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('How would you like to procrastinate? ') # And verify the expected output to stdout assert out == expected def test_select_uneven_list_of_tuples(select_app): # 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='2') builtins.input = m out = run_cmd(select_app, "play") expected = normalize(""" 1. Electric Guitar 2. Drums Charm us with the {}... """.format('Drums')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Instrument? ') # And verify the expected output to stdout assert out == expected class HelpNoDocstringApp(cmd2.Cmd): greet_parser = argparse.ArgumentParser() greet_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") @cmd2.with_argparser_and_unknown_args(greet_parser) def do_greet(self, opts, arg): arg = ''.join(arg) if opts.shout: arg = arg.upper() self.stdout.write(arg + '\n') def test_help_with_no_docstring(capsys): app = HelpNoDocstringApp() app.onecmd_plus_hooks('greet -h') out, err = capsys.readouterr() assert err == '' assert out == """usage: greet [-h] [-s] optional arguments: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE """ @pytest.mark.skipif(sys.platform.startswith('win'), reason="cmd2._which function only used on Mac and Linux") def test_which_editor_good(): editor = 'vi' path = cmd2._which(editor) # Assert that the vi editor was found because it should exist on all Mac and Linux systems assert path @pytest.mark.skipif(sys.platform.startswith('win'), reason="cmd2._which function only used on Mac and Linux") def test_which_editor_bad(): editor = 'notepad.exe' path = cmd2._which(editor) # Assert that the editor wasn't found because no notepad.exe on non-Windows systems ;-) assert path is None class MultilineApp(cmd2.Cmd): def __init__(self, *args, **kwargs): self.multilineCommands = ['orate'] super().__init__(*args, **kwargs) orate_parser = argparse.ArgumentParser() orate_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") @cmd2.with_argparser_and_unknown_args(orate_parser) def do_orate(self, opts, arg): arg = ''.join(arg) if opts.shout: arg = arg.upper() self.stdout.write(arg + '\n') @pytest.fixture def multiline_app(): app = MultilineApp() app.stdout = StdOut() return app def test_multiline_complete_empty_statement_raises_exception(multiline_app): with pytest.raises(cmd2.EmptyStatement): multiline_app._complete_statement('') def test_multiline_complete_statement_without_terminator(multiline_app): # Mock out the input call so we don't actually wait for a user's response on stdin when it looks for more input m = mock.MagicMock(name='input', return_value='\n') builtins.input = m command = 'orate' args = 'hello world' line = '{} {}'.format(command, args) statement = multiline_app._complete_statement(line) assert statement == args assert statement.parsed.command == command def test_clipboard_failure(capsys): # Force cmd2 clipboard to be disabled cmd2.can_clip = False app = cmd2.Cmd() # Redirect command output to the clipboard when a clipboard isn't present app.onecmd_plus_hooks('help > ') # 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 class CmdResultApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def do_affirmative(self, arg): self._last_result = cmd2.CmdResult(arg) def do_negative(self, arg): self._last_result = cmd2.CmdResult('', arg) @pytest.fixture def cmdresult_app(): app = CmdResultApp() app.stdout = StdOut() return app def test_cmdresult(cmdresult_app): arg = 'foo' run_cmd(cmdresult_app, 'affirmative {}'.format(arg)) assert cmdresult_app._last_result assert cmdresult_app._last_result == cmd2.CmdResult(arg) arg = 'bar' run_cmd(cmdresult_app, 'negative {}'.format(arg)) assert not cmdresult_app._last_result assert cmdresult_app._last_result == cmd2.CmdResult('', arg) def test_is_text_file_bad_input(base_app): # Test with a non-existent file file_is_valid = base_app.is_text_file('does_not_exist.txt') assert not file_is_valid # Test with a directory dir_is_valid = base_app.is_text_file('.') assert not dir_is_valid def test_eof(base_app): # Only thing to verify is that it returns True assert base_app.do_eof('dont care') def test_eos(base_app): sdir = 'dummy_dir' base_app._script_dir.append(sdir) assert len(base_app._script_dir) == 1 # Assert that it does NOT return true assert not base_app.do_eos('dont care') # And make sure it reduced the length of the script dir list assert len(base_app._script_dir) == 0 def test_echo(capsys): app = cmd2.Cmd() # Turn echo on and pre-stage some commands in the queue, simulating like we are in the middle of a script app.echo = True command = 'help history' app.cmdqueue = [command, 'quit', 'eos'] app._script_dir.append('some_dir') assert app._current_script_dir is not None # Run the inner _cmdloop app._cmdloop() out, err = capsys.readouterr() # Check the output assert app.cmdqueue == [] assert app._current_script_dir is None assert out.startswith('{}{}\n'.format(app.prompt, command) + HELP_HISTORY.split()[0]) def test_pseudo_raw_input_tty_rawinput_true(): # use context managers so original functions get put back when we are done # we dont use decorators because we need m_input for the assertion with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)): with mock.patch('builtins.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) as m_input: # run the cmdloop, which should pull input from our mocks app = cmd2.Cmd() app.use_rawinput = True app._cmdloop() # because we mocked the input() call, we won't get the prompt # or the name of the command in the output, so we can't check # if its there. We assume that if input got called twice, once # for the 'set' command, and once for the 'quit' command, # that the rest of it worked assert m_input.call_count == 2 def test_pseudo_raw_input_tty_rawinput_false(): # gin up some input like it's coming from a tty fakein = io.StringIO(u'{}'.format('set\n')) mtty = mock.MagicMock(name='isatty', return_value=True) fakein.isatty = mtty mreadline = mock.MagicMock(name='readline', wraps=fakein.readline) fakein.readline = mreadline # run the cmdloop, telling it where to get input from app = cmd2.Cmd(stdin=fakein) app.use_rawinput = False app._cmdloop() # because we mocked the readline() call, we won't get the prompt # or the name of the command in the output, so we can't check # if its there. We assume that if readline() got called twice, once # for the 'set' command, and once for the 'quit' command, # that the rest of it worked assert mreadline.call_count == 2 # the next helper function and two tests check for piped # input when use_rawinput is True. def piped_rawinput_true(capsys, echo, command): app = cmd2.Cmd() app.use_rawinput = True app.echo = echo # run the cmdloop, which should pull input from our mock app._cmdloop() out, err = capsys.readouterr() 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])) def test_pseudo_raw_input_piped_rawinput_true_echo_true(capsys): command = 'set' app, out = piped_rawinput_true(capsys, True, command) out = out.splitlines() assert out[0] == '{}{}'.format(app.prompt, command) assert out[1].startswith('colors:') # 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])) def test_pseudo_raw_input_piped_rawinput_true_echo_false(capsys): command = 'set' app, out = piped_rawinput_true(capsys, False, command) firstline = out.splitlines()[0] assert firstline.startswith('colors:') assert not '{}{}'.format(app.prompt, command) in out # the next helper function and two tests check for piped # input when use_rawinput=False def piped_rawinput_false(capsys, echo, command): fakein = io.StringIO(u'{}'.format(command)) # run the cmdloop, telling it where to get input from app = cmd2.Cmd(stdin=fakein) app.use_rawinput = False app.echo = echo app._cmdloop() out, err = capsys.readouterr() return (app, out) def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys): command = 'set' app, out = piped_rawinput_false(capsys, True, command) out = out.splitlines() assert out[0] == '{}{}'.format(app.prompt, command) assert out[1].startswith('colors:') def test_pseudo_raw_input_piped_rawinput_false_echo_false(capsys): command = 'set' app, out = piped_rawinput_false(capsys, False, command) firstline = out.splitlines()[0] assert firstline.startswith('colors:') assert not '{}{}'.format(app.prompt, command) in out # # other input tests def test_raw_input(base_app): base_app.use_raw_input = True fake_input = 'quit' # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.Mock(name='input', return_value=fake_input) builtins.input = m line = base_app.pseudo_raw_input('(cmd2)') assert line == fake_input def test_stdin_input(): app = cmd2.Cmd() app.use_rawinput = False fake_input = 'quit' # Mock out the readline call so we don't actually read from stdin m = mock.Mock(name='readline', return_value=fake_input) app.stdin.readline = m line = app.pseudo_raw_input('(cmd2)') assert line == fake_input def test_empty_stdin_input(): app = cmd2.Cmd() app.use_rawinput = False fake_input = '' # Mock out the readline call so we don't actually read from stdin m = mock.Mock(name='readline', return_value=fake_input) app.stdin.readline = m line = app.pseudo_raw_input('(cmd2)') assert line == 'eof' def test_poutput_string(base_app): msg = 'This is a test' base_app.poutput(msg) out = base_app.stdout.buffer expected = msg + '\n' assert out == expected def test_poutput_zero(base_app): msg = 0 base_app.poutput(msg) out = base_app.stdout.buffer expected = str(msg) + '\n' assert out == expected def test_poutput_empty_string(base_app): msg = '' base_app.poutput(msg) out = base_app.stdout.buffer expected = msg assert out == expected def test_poutput_none(base_app): msg = None base_app.poutput(msg) out = base_app.stdout.buffer expected = '' assert out == expected def test_alias(base_app, capsys): # Create the alias out = run_cmd(base_app, 'alias 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 # See a list of aliases out = run_cmd(base_app, 'alias') assert out == normalize('alias fake pyscript') # Lookup the new alias out = run_cmd(base_app, 'alias fake') assert out == normalize('alias fake pyscript') def test_alias_lookup_invalid_alias(base_app, capsys): # Lookup invalid alias out = run_cmd(base_app, 'alias invalid') 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') # Remove the alias out = run_cmd(base_app, 'unalias fake') assert out == normalize("Alias 'fake' cleared") def test_unalias_all(base_app): out = run_cmd(base_app, 'unalias -a') assert out == normalize("All aliases cleared") def test_unalias_non_existing(base_app, capsys): run_cmd(base_app, 'unalias fake') out, err = capsys.readouterr() assert "does not exist" in err