summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rwxr-xr-xREADME.md5
-rw-r--r--cmd2/cmd2.py30
-rw-r--r--cmd2/transcript.py1
-rw-r--r--docs/unfreefeatures.rst4
-rwxr-xr-xexamples/alias_startup.py3
-rwxr-xr-xexamples/arg_print.py3
-rwxr-xr-xexamples/async_printing.py3
-rwxr-xr-xexamples/cmd_as_argument.py7
-rwxr-xr-xexamples/colors.py3
-rwxr-xr-xexamples/decorator_example.py5
-rwxr-xr-xexamples/environment.py4
-rwxr-xr-xexamples/example.py6
-rwxr-xr-xexamples/exit_code.py14
-rwxr-xr-xexamples/hello_cmd2.py3
-rwxr-xr-xexamples/help_categories.py3
-rwxr-xr-xexamples/hooks.py3
-rwxr-xr-xexamples/paged_output.py3
-rwxr-xr-xexamples/persistent_history.py2
-rwxr-xr-xexamples/pirate.py6
-rwxr-xr-xexamples/plumbum_colors.py3
-rwxr-xr-xexamples/python_scripting.py3
-rwxr-xr-xexamples/remove_unused.py3
-rwxr-xr-xexamples/subcommands.py3
-rwxr-xr-xexamples/tab_autocomp_dynamic.py3
-rwxr-xr-xexamples/tab_autocompletion.py3
-rwxr-xr-xexamples/tab_completion.py3
-rwxr-xr-xexamples/table_display.py3
-rw-r--r--tests/test_cmd2.py6
-rw-r--r--tests/test_transcript.py29
-rw-r--r--tests/transcripts/failure.txt4
31 files changed, 123 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fe4e734..2a43e961 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
* Fixed parsing issue in case where output redirection appears before a pipe. In that case, the pipe was given
precedence even though it appeared later in the command.
* Fixed issue where quotes around redirection file paths were being lost in `Statement.expanded_command_line()`
+ * Fixed a bug in how line numbers were calculated for transcript testing
* Enhancements
* Added capability to chain pipe commands and redirect their output (e.g. !ls -l | grep user | wc -l > out.txt)
* `pyscript` limits a command's stdout capture to the same period that redirection does.
@@ -21,11 +22,14 @@
readline. This makes debugging a lot easier since readline suppresses these exceptions.
* Added support for custom Namespaces in the argparse decorators. See description of `ns_provider` argument
for more information.
+ * Transcript testing now sets the `exit_code` returned from `cmdloop` based on Success/Failure
* Potentially breaking changes
* Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix
that allows terminators in alias and macro values.
* Changed `Statement.pipe_to` to a string instead of a list
* `preserve_quotes` is now a keyword-only argument in the argparse decorators
+ * Refactored so that `cmd2.Cmd.cmdloop()` returns the `exit_code` instead of a call to `sys.exit()`
+ * It is now applicaiton developer's responsibility to treat the return value from `cmdloop()` accordingly
* **Python 3.4 EOL notice**
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
* This is the last release of `cmd2` which will support Python 3.4
diff --git a/README.md b/README.md
index 86b9f8ad..b298a04c 100755
--- a/README.md
+++ b/README.md
@@ -227,6 +227,7 @@ A sample application for cmd2.
"""
import argparse
import random
+import sys
import cmd2
class CmdLineApp(cmd2.Cmd):
@@ -294,8 +295,8 @@ class CmdLineApp(cmd2.Cmd):
self.poutput(' '.join(output))
if __name__ == '__main__':
- c = CmdLineApp()
- c.cmdloop()
+ app = CmdLineApp()
+ sys.exit(app.cmdloop())
```
The following is a sample session running example.py.
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index f5a2a844..9d36e1b4 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -553,8 +553,8 @@ class Cmd(cmd.Cmd):
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
self.can_clip = can_clip
- # This determines if a non-zero exit code should be used when exiting the application
- self.exit_code = None
+ # This determines the value returned by cmdloop() when exiting the application
+ self.exit_code = 0
# This lock should be acquired before doing any asynchronous changes to the terminal to
# ensure the updates to the terminal don't interfere with the input being typed or output
@@ -3683,8 +3683,24 @@ class Cmd(cmd.Cmd):
self.__class__.testfiles = callargs
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
testcase = TestMyAppCase()
- runner = unittest.TextTestRunner()
- runner.run(testcase)
+ stream = utils.StdSim(sys.stderr)
+ runner = unittest.TextTestRunner(stream=stream)
+ test_results = runner.run(testcase)
+ if test_results.wasSuccessful():
+ self.decolorized_write(sys.stderr, stream.read())
+ self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX)
+ else:
+ # Strip off the initial trackeback which isn't particularly useful for end users
+ error_str = stream.read()
+ end_of_trace = error_str.find('AssertionError:')
+ file_offset = error_str[end_of_trace:].find('File ')
+ start = end_of_trace + file_offset
+
+ # But print the transcript file name and line number followed by what was expected and what was observed
+ self.perror(error_str[start:], traceback_war=False)
+
+ # Return a failure error code to support automated transcript-based testing
+ self.exit_code = -1
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
"""
@@ -3932,7 +3948,7 @@ class Cmd(cmd.Cmd):
"""
self.decolorized_write(sys.stderr, "{}\n".format(message_to_print))
- def cmdloop(self, intro: Optional[str] = None) -> None:
+ def cmdloop(self, intro: Optional[str] = None) -> int:
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
@@ -3940,6 +3956,7 @@ class Cmd(cmd.Cmd):
- commands at invocation
- transcript testing
- intro banner
+ - exit code
:param intro: if provided this overrides self.intro and serves as the intro banner printed once at start
"""
@@ -4002,8 +4019,7 @@ class Cmd(cmd.Cmd):
# Restore the original signal handler
signal.signal(signal.SIGINT, original_sigint_handler)
- if self.exit_code is not None:
- sys.exit(self.exit_code)
+ return self.exit_code
###
#
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 6a954bce..a635c1d3 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -71,6 +71,7 @@ class Cmd2TestCase(unittest.TestCase):
line = next(transcript)
except StopIteration:
line = ''
+ line_num += 1
# Read the entirety of a multi-line command
while line.startswith(self.cmdapp.continuation_prompt):
command.append(line[len(self.cmdapp.continuation_prompt):])
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index 071a15b2..81be76d8 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -204,8 +204,8 @@ Presents numbered options to user, as bash ``select``.
Exit code to shell
==================
The ``self.exit_code`` attribute of your ``cmd2`` application controls
-what exit code is sent to the shell when your application exits from
-``cmdloop()``.
+what exit code is returned from ``cmdloop()`` when it completes. It is your job to make sure that
+this exit code gets sent to the shell when your application exits by calling ``sys.exit(app.cmdloop())``.
Asynchronous Feedback
diff --git a/examples/alias_startup.py b/examples/alias_startup.py
index 7c70bcd9..b765e34c 100755
--- a/examples/alias_startup.py
+++ b/examples/alias_startup.py
@@ -21,5 +21,6 @@ class AliasAndStartup(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = AliasAndStartup()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/arg_print.py b/examples/arg_print.py
index edcc8444..48bcbd13 100755
--- a/examples/arg_print.py
+++ b/examples/arg_print.py
@@ -63,5 +63,6 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = ArgumentAndOptionPrinter()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/async_printing.py b/examples/async_printing.py
index d0716bbb..3089070f 100755
--- a/examples/async_printing.py
+++ b/examples/async_printing.py
@@ -197,6 +197,7 @@ class AlerterApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = AlerterApp()
app.set_window_title("Asynchronous Printer Test")
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py
index df7e1d76..9eb0befb 100755
--- a/examples/cmd_as_argument.py
+++ b/examples/cmd_as_argument.py
@@ -14,7 +14,6 @@ $ python cmd_as_argument.py speak -p hello there
import argparse
import random
-import sys
import cmd2
@@ -101,13 +100,17 @@ def main(argv=None):
c = CmdLineApp()
+ sys_exit_code = 0
if args.command:
# we have a command, run it and then exit
c.onecmd_plus_hooks('{} {}'.format(args.command, ' '.join(args.command_args)))
else:
# we have no command, drop into interactive mode
- c.cmdloop()
+ sys_exit_code = c.cmdloop()
+
+ return sys_exit_code
if __name__ == '__main__':
+ import sys
sys.exit(main())
diff --git a/examples/colors.py b/examples/colors.py
index ea0bca39..fdc0e0bd 100755
--- a/examples/colors.py
+++ b/examples/colors.py
@@ -138,5 +138,6 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = CmdLineApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index d8088c0a..e268c615 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -11,7 +11,6 @@ all the commands in the transcript against decorator_example.py,
verifying that the output produced matches the transcript.
"""
import argparse
-import sys
from typing import List
import cmd2
@@ -89,6 +88,8 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
+
# You can do your custom Argparse parsing here to meet your application's needs
parser = argparse.ArgumentParser(description='Process the arguments however you like.')
@@ -114,4 +115,4 @@ if __name__ == '__main__':
c = CmdLineApp()
# And run your cmd2 application
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/environment.py b/examples/environment.py
index e899cce8..9e611f08 100755
--- a/examples/environment.py
+++ b/examples/environment.py
@@ -3,7 +3,6 @@
"""
A sample application for cmd2 demonstrating customized environment parameters
"""
-
import cmd2
@@ -34,5 +33,6 @@ class EnvironmentApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = EnvironmentApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/example.py b/examples/example.py
index 9f9c0304..a1ec893c 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -10,9 +10,8 @@ Running `python example.py -t transcript_regex.txt` will run all the commands in
the transcript against example.py, verifying that the output produced matches
the transcript.
"""
-
-import random
import argparse
+import random
import cmd2
@@ -82,5 +81,6 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = CmdLineApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/exit_code.py b/examples/exit_code.py
index 8ae2d310..f4b19091 100755
--- a/examples/exit_code.py
+++ b/examples/exit_code.py
@@ -3,7 +3,6 @@
"""A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application.
"""
import cmd2
-import sys
from typing import List
@@ -29,15 +28,12 @@ Usage: exit [exit_code]
self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
self.exit_code = -1
- self._should_quit = True
- return self._STOP_AND_EXIT
-
- def postloop(self) -> None:
- """Hook method executed once when the cmdloop() method is about to return."""
- code = self.exit_code if self.exit_code is not None else 0
- self.poutput('{!r} exiting with code: {}'.format(sys.argv[0], code))
+ return True
if __name__ == '__main__':
+ import sys
app = ReplWithExitCode()
- app.cmdloop()
+ sys_exit_code = app.cmdloop()
+ app.poutput('{!r} exiting with code: {}'.format(sys.argv[0], sys_exit_code))
+ sys.exit(sys_exit_code)
diff --git a/examples/hello_cmd2.py b/examples/hello_cmd2.py
index 397856a6..395663f2 100755
--- a/examples/hello_cmd2.py
+++ b/examples/hello_cmd2.py
@@ -6,6 +6,7 @@ This is intended to be a completely bare-bones cmd2 application suitable for rap
from cmd2 import cmd2
if __name__ == '__main__':
+ import sys
# If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality.
# Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive
@@ -13,4 +14,4 @@ if __name__ == '__main__':
app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.txt')
app.locals_in_py = True # Enable access to "self" within the py command
app.debug = True # Show traceback if/when an exception occurs
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/help_categories.py b/examples/help_categories.py
index 62351e81..80f367fa 100755
--- a/examples/help_categories.py
+++ b/examples/help_categories.py
@@ -157,5 +157,6 @@ class HelpCategories(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = HelpCategories()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/hooks.py b/examples/hooks.py
index dd21e58a..c533c696 100755
--- a/examples/hooks.py
+++ b/examples/hooks.py
@@ -111,5 +111,6 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = CmdLineApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/paged_output.py b/examples/paged_output.py
index a0674a62..b3824012 100755
--- a/examples/paged_output.py
+++ b/examples/paged_output.py
@@ -54,5 +54,6 @@ class PagedOutput(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = PagedOutput()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/persistent_history.py b/examples/persistent_history.py
index 61e26b9c..12d8b813 100755
--- a/examples/persistent_history.py
+++ b/examples/persistent_history.py
@@ -30,4 +30,4 @@ if __name__ == '__main__':
history_file = sys.argv[1]
app = Cmd2PersistentHistory(hist_file=history_file)
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/pirate.py b/examples/pirate.py
index 994ca245..9abbe4e6 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -55,6 +55,7 @@ class Pirate(cmd2.Cmd):
self.poutput('Now we gots {0} doubloons'.format(self.gold))
if self.gold < 0:
self.poutput("Off to debtorrr's prison.")
+ self.exit_code = -1
stop = True
return stop
@@ -99,6 +100,9 @@ class Pirate(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
# Create an instance of the Pirate derived class and enter the REPL with cmdlooop().
pirate = Pirate()
- pirate.cmdloop()
+ sys_exit_code = pirate.cmdloop()
+ print('Exiting with code: {!r}'.format(sys_exit_code))
+ sys.exit(sys_exit_code)
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
index 6daa5312..774dc7e4 100755
--- a/examples/plumbum_colors.py
+++ b/examples/plumbum_colors.py
@@ -141,5 +141,6 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = CmdLineApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/python_scripting.py b/examples/python_scripting.py
index 7847b8b6..da7d0f6a 100755
--- a/examples/python_scripting.py
+++ b/examples/python_scripting.py
@@ -117,5 +117,6 @@ class CmdLineApp(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
c = CmdLineApp()
- c.cmdloop()
+ sys.exit(c.cmdloop())
diff --git a/examples/remove_unused.py b/examples/remove_unused.py
index 8a567123..62103022 100755
--- a/examples/remove_unused.py
+++ b/examples/remove_unused.py
@@ -26,5 +26,6 @@ class RemoveUnusedBuiltinCommands(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = RemoveUnusedBuiltinCommands()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/subcommands.py b/examples/subcommands.py
index 83c29393..d1b7c9db 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -114,5 +114,6 @@ class SubcommandsExample(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = SubcommandsExample()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/tab_autocomp_dynamic.py b/examples/tab_autocomp_dynamic.py
index 93b72442..03e46f8a 100755
--- a/examples/tab_autocomp_dynamic.py
+++ b/examples/tab_autocomp_dynamic.py
@@ -232,5 +232,6 @@ class TabCompleteExample(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = TabCompleteExample()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index 3f06a274..6883c423 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -540,5 +540,6 @@ class TabCompleteExample(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = TabCompleteExample()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/tab_completion.py b/examples/tab_completion.py
index 77d62988..48d7cb05 100755
--- a/examples/tab_completion.py
+++ b/examples/tab_completion.py
@@ -74,5 +74,6 @@ class TabCompleteExample(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = TabCompleteExample()
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/examples/table_display.py b/examples/table_display.py
index 04415afd..dcde7a81 100755
--- a/examples/table_display.py
+++ b/examples/table_display.py
@@ -195,6 +195,7 @@ class TableDisplay(cmd2.Cmd):
if __name__ == '__main__':
+ import sys
app = TableDisplay()
app.debug = True
- app.cmdloop()
+ sys.exit(app.cmdloop())
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 7a17cfac..1aafefc2 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1950,8 +1950,7 @@ Usage: exit [exit_code]
def postloop(self) -> None:
"""Hook method executed once when the cmdloop() method is about to return."""
- code = self.exit_code if self.exit_code is not None else 0
- self.poutput('exiting with code: {}'.format(code))
+ self.poutput('exiting with code: {}'.format(self.exit_code))
@pytest.fixture
def exit_code_repl():
@@ -1991,8 +1990,7 @@ def test_exit_code_nonzero(exit_code_repl):
expected = 'exiting with code: 23\n'
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
- with pytest.raises(SystemExit):
- app.cmdloop()
+ app.cmdloop()
out = app.stdout.getvalue()
assert out == expected
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index acdbe703..70c9119c 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -127,7 +127,8 @@ def test_transcript(request, capsys, filename, feedback_to_output):
testargs = ['prog', '-t', transcript_file]
with mock.patch.object(sys, 'argv', testargs):
# Run the command loop
- app.cmdloop()
+ sys_exit_code = app.cmdloop()
+ assert sys_exit_code == 0
# Check for the unittest "OK" condition for the 1 test which ran
expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in"
@@ -247,3 +248,29 @@ def test_parse_transcript_expected(expected, transformed):
testcase = TestMyAppCase()
assert testcase._transform_transcript_expected(expected) == transformed
+
+
+def test_transcript_failure(request, capsys):
+ # Create a cmd2.Cmd() instance and make sure basic settings are
+ # like we want for test
+ app = CmdLineApp()
+ app.feedback_to_output = False
+
+ # Get location of the transcript
+ test_dir = os.path.dirname(request.module.__file__)
+ transcript_file = os.path.join(test_dir, 'transcripts', 'failure.txt')
+
+ # Need to patch sys.argv so cmd2 doesn't think it was called with
+ # arguments equal to the py.test args
+ testargs = ['prog', '-t', transcript_file]
+ with mock.patch.object(sys, 'argv', testargs):
+ # Run the command loop
+ sys_exit_code = app.cmdloop()
+ assert sys_exit_code != 0
+
+ # Check for the unittest "OK" condition for the 1 test which ran
+ expected_start = "File "
+ expected_end = "s\n\nFAILED (failures=1)\n\n"
+ _, err = capsys.readouterr()
+ assert err.startswith(expected_start)
+ assert err.endswith(expected_end)
diff --git a/tests/transcripts/failure.txt b/tests/transcripts/failure.txt
new file mode 100644
index 00000000..4ef56e72
--- /dev/null
+++ b/tests/transcripts/failure.txt
@@ -0,0 +1,4 @@
+# This is an example of a transcript test which will fail
+
+(Cmd) say -r 3 -s yabba dabba do
+foo bar baz