summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-09-30 11:17:04 -0400
committerTodd Leonhardt <todd.leonhardt@gmail.com>2018-09-30 11:17:04 -0400
commit81ad085cffc8f8af7fb7884bcc5db6fcede09df6 (patch)
treea197a6c81767f10b62d6ac6acfd46e42357a8289 /tests
parent85a21ef39467dc24a6f6f8c4f3ac656f24d6ff90 (diff)
parent61d5703cd3586b3460669a6260cf903c9863b240 (diff)
downloadcmd2-git-81ad085cffc8f8af7fb7884bcc5db6fcede09df6.tar.gz
Merged master into transcript_fixes branch and resolved conflicts
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py86
-rw-r--r--tests/scripts/postcmds.txt2
-rw-r--r--tests/scripts/precmds.txt2
-rw-r--r--tests/test_argparse.py9
-rw-r--r--tests/test_autocompletion.py16
-rw-r--r--tests/test_cmd2.py659
-rw-r--r--tests/test_completion.py117
-rw-r--r--tests/test_parsing.py101
-rw-r--r--tests/test_plugin.py79
-rw-r--r--tests/test_pyscript.py48
-rw-r--r--tests/test_transcript.py16
-rw-r--r--tests/test_utils.py120
-rw-r--r--tests/transcripts/from_cmdloop.txt4
-rw-r--r--tests/transcripts/regex_set.txt4
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