summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py114
-rw-r--r--docs/freefeatures.rst36
-rw-r--r--docs/index.rst1
-rw-r--r--docs/transcript.rst161
-rwxr-xr-xexamples/example.py52
-rw-r--r--examples/transcript_regex.txt7
-rw-r--r--tests/multiline_transcript.txt10
-rw-r--r--tests/test_transcript.py148
-rw-r--r--tests/transcripts/bol_eol.txt6
-rw-r--r--tests/transcripts/characterclass.txt6
-rw-r--r--tests/transcripts/dotstar.txt4
-rw-r--r--tests/transcripts/extension_notation.txt4
-rw-r--r--tests/transcripts/from_cmdloop.txt (renamed from tests/transcript.txt)13
-rw-r--r--tests/transcripts/multiline_no_regex.txt6
-rw-r--r--tests/transcripts/multiline_regex.txt6
-rw-r--r--tests/transcripts/regex_set.txt (renamed from tests/transcript_regex.txt)6
-rw-r--r--tests/transcripts/singleslash.txt5
-rw-r--r--tests/transcripts/slashes_escaped.txt6
-rw-r--r--tests/transcripts/slashslash.txt4
-rw-r--r--tests/transcripts/spaces.txt8
-rw-r--r--tests/transcripts/word_boundaries.txt6
21 files changed, 466 insertions, 143 deletions
diff --git a/cmd2.py b/cmd2.py
index 1bfab961..f4eb3b00 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -2021,10 +2021,12 @@ class ParserManager:
class HistoryItem(str):
"""Class used to represent an item in the History list.
- Thing wrapper around str class which adds a custom format for printing. It also keeps track of its index in the
- list as well as a lowercase representation of itself for convenience/efficiency.
+ Thin wrapper around str class which adds a custom format for printing. It
+ also keeps track of its index in the list as well as a lowercase
+ representation of itself for convenience/efficiency.
+
"""
- listformat = '-------------------------[%d]\n%s\n'
+ listformat = '-------------------------[{}]\n{}\n'
# noinspection PyUnusedLocal
def __init__(self, instr):
@@ -2037,7 +2039,7 @@ class HistoryItem(str):
:return: str - pretty print string version of a HistoryItem
"""
- return self.listformat % (self.idx, str(self))
+ return self.listformat.format(self.idx, str(self).rstrip())
class History(list):
@@ -2230,12 +2232,6 @@ class Cmd2TestCase(unittest.TestCase):
that will execute the commands in a transcript file and expect the results shown.
See example.py"""
cmdapp = None
- regexPattern = pyparsing.QuotedString(quoteChar=r'/', escChar='\\', multiline=True, unquoteResults=True)
- regexPattern.ignore(pyparsing.cStyleComment)
- notRegexPattern = pyparsing.Word(pyparsing.printables)
- notRegexPattern.setParseAction(lambda t: re.escape(t[0]))
- expectationParser = regexPattern | notRegexPattern
- anyWhitespace = re.compile(r'\s', re.DOTALL | re.MULTILINE)
def fetchTranscripts(self):
self.transcripts = {}
@@ -2295,8 +2291,8 @@ class Cmd2TestCase(unittest.TestCase):
result = self.cmdapp.stdout.read()
# Read the expected result from transcript
if strip_ansi(line).startswith(self.cmdapp.visible_prompt):
- message = '\nFile %s, line %d\nCommand was:\n%r\nExpected: (nothing)\nGot:\n%r\n' % \
- (fname, line_num, command, result)
+ message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
+ fname, line_num, command, result)
self.assert_(not (result.strip()), message)
continue
expected = []
@@ -2309,15 +2305,95 @@ class Cmd2TestCase(unittest.TestCase):
break
line_num += 1
expected = ''.join(expected)
- # Compare actual result to expected
- message = '\nFile %s, line %d\nCommand was:\n%s\nExpected:\n%s\nGot:\n%s\n' % \
- (fname, line_num, command, expected, result)
- expected = self.expectationParser.transformString(expected)
- # checking whitespace is a pain - let's skip it
- expected = self.anyWhitespace.sub('', expected)
- result = self.anyWhitespace.sub('', result)
+
+ # transform the expected text into a valid regular expression
+ expected = self._transform_transcript_expected(expected)
+ message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format(
+ fname, line_num, command, expected, result)
self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message)
+ def _transform_transcript_expected(self, s):
+ """parse the string with slashed regexes into a valid regex"""
+ slash = '/'
+ backslash = '\\'
+ regex = ''
+ start = 0
+
+ while True:
+ (regex, first_slash_pos, start) = self._escaped_find(regex, s, start, False)
+ if first_slash_pos == -1:
+ # no more slashes, add the rest of the string and bail
+ regex += re.escape(s[start:])
+ break
+ else:
+ # there is a slash, add everything we have found so far
+ # add stuff before the first slash as plain text
+ regex += re.escape(s[start:first_slash_pos])
+ start = first_slash_pos+1
+ # and go find the next one
+ (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True)
+ if second_slash_pos > 0:
+ # add everything between the slashes (but not the slashes)
+ # as a regular expression
+ regex += s[start:second_slash_pos]
+ # and change where we start looking for slashed on the
+ # turn through the loop
+ start = second_slash_pos + 1
+ else:
+ # No closing slash, we have to add the first slash,
+ # and the rest of the text
+ regex += re.escape(s[start-1:])
+ break
+ return regex
+
+ def _escaped_find(self, regex, s, start, in_regex):
+ """
+ Find the next slash in {s} after {start} that is not preceded by a backslash.
+
+ If we find an escaped slash, add everything up to and including it to regex,
+ updating {start}. {start} therefore serves two purposes, tells us where to start
+ looking for the next thing, and also tells us where in {s} we have already
+ added things to {regex}
+
+ {in_regex} specifies whether we are currently searching in a regex, we behave
+ differently if we are or if we aren't.
+ """
+
+ while True:
+ pos = s.find('/', start)
+ if pos == -1:
+ # no match, return to caller
+ break
+ elif pos == 0:
+ # slash at the beginning of the string, so it can't be
+ # escaped. We found it.
+ break
+ else:
+ # check if the slash is preceeded by a backslash
+ if s[pos-1:pos] == '\\':
+ # it is.
+ if in_regex:
+ # add everything up to the backslash as a
+ # regular expression
+ regex += s[start:pos-1]
+ # skip the backslash, and add the slash
+ regex += s[pos]
+ else:
+ # add everything up to the backslash as escaped
+ # plain text
+ regex += re.escape(s[start:pos-1])
+ # and then add the slash as escaped
+ # plain text
+ regex += re.escape(s[pos])
+ # update start to show we have handled everything
+ # before it
+ start = pos+1
+ # and continue to look
+ else:
+ # slash is not escaped, this is what we are looking for
+ break
+ return (regex, pos, start)
+
def tearDown(self):
if self.cmdapp:
# Restore stdout
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 7b6762ad..efb74316 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -82,6 +82,8 @@ quotation marks if it is more than a one-word command.
.. _Argparse: https://docs.python.org/3/library/argparse.html
+.. _output_redirection:
+
Output redirection
==================
@@ -301,34 +303,20 @@ is equivalent to ``shell ls``.)
Transcript-based testing
========================
-If the entire transcript (input and output) of a successful session of
-a ``cmd2``-based app is copied from the screen and pasted into a text
-file, ``transcript.txt``, then a transcript test can be run against it::
-
- python app.py --test transcript.txt
+A transcript is both the input and output of a successful session of a
+``cmd2``-based app which is saved to a text file. The transcript can be played
+back into the app as a unit test.
-Any non-whitespace deviations between the output prescribed in ``transcript.txt`` and
-the actual output from a fresh run of the application will be reported
-as a unit test failure. (Whitespace is ignored during the comparison.)
+.. code-block:: none
-Regular expressions can be embedded in the transcript inside paired ``/``
-slashes. These regular expressions should not include any whitespace
-expressions.
-
-.. note::
+ $ python example.py --test transcript_regex.txt
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.013s
- If you have set ``allow_cli_args`` to False in order to disable parsing of command line arguments at invocation,
- then the use of ``-t`` or ``--test`` to run transcript testing is automatically disabled. In this case, you can
- alternatively provide a value for the optional ``transcript_files`` when constructing the instance of your
- ``cmd2.Cmd`` derived class in order to cause a transcript test to run::
-
- from cmd2 import Cmd
- class App(Cmd):
- # customized attributes and methods here
+ OK
- if __name__ == '__main__':
- app = App(transcript_files=['exampleSession.txt'])
- app.cmdloop()
+See :doc:`transcript` for more details.
Tab-Completion
diff --git a/docs/index.rst b/docs/index.rst
index e89be557..1f2bf96c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -66,6 +66,7 @@ Contents:
freefeatures
settingchanges
unfreefeatures
+ transcript
integrating
hooks
alternatives
diff --git a/docs/transcript.rst b/docs/transcript.rst
new file mode 100644
index 00000000..f1e3e63f
--- /dev/null
+++ b/docs/transcript.rst
@@ -0,0 +1,161 @@
+========================
+Transcript based testing
+========================
+
+A transcript is both the input and output of a successful session of a
+``cmd2``-based app which is saved to a text file. With no extra work on your
+part, your app can play back these transcripts as a unit test. Transcripts can
+contain regular expressions, which provide the flexibility to match responses
+from commands that produce dynamic or variable output.
+
+.. highlight:: none
+
+Creating a transcript
+=====================
+
+Here's a transcript created from ``python examples/example.py``::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ (Cmd) mumble maybe we could go to lunch
+ like maybe we ... could go to hmmm lunch
+ (Cmd) mumble maybe we could go to lunch
+ well maybe we could like go to er lunch right?
+
+This transcript has three commands: they are on the lines that begin with the
+prompt. The first command looks like this::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+
+Following each command is the output generated by that command.
+
+The transcript ignores all lines in the file until it reaches the first line
+that begins with the prompt. You can take advantage of this by using the first
+lines of the transcript as comments::
+
+ # Lines at the beginning of the transcript that do not
+ ; start with the prompt i.e. '(Cmd) ' are ignored.
+ /* You can use them for comments. */
+
+ All six of these lines before the first prompt are treated as comments.
+
+ (Cmd) say -r 3 Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ (Cmd) mumble maybe we could go to lunch
+ like maybe we ... could go to hmmm lunch
+ (Cmd) mumble maybe we could go to lunch
+ maybe we could like go to er lunch right?
+
+In this example I've used several different commenting styles, and even bare
+text. It doesn't matter what you put on those beginning lines. Everything before::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+
+will be ignored.
+
+
+Regular Expressions
+===================
+
+If we used the above transcript as-is, it would likely fail. As you can see,
+the ``mumble`` command doesn't always return the same thing: it inserts random
+words into the input.
+
+Regular expressions can be included in the response portion of a transcript,
+and are surrounded by slashes::
+
+ (Cmd) mumble maybe we could go to lunch
+ /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
+ (Cmd) mumble maybe we could go to lunch
+ /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
+
+Without creating a tutorial on regular expressions, this one matches anything
+that has the words ``maybe``, ``could``, and ``lunch`` in that order. It doesn't
+ensure that ``we`` or ``go`` or ``to`` appear in the output, but it does work if
+mumble happens to add words to the beginning or the end of the output.
+
+Since the output could be multiple lines long, ``cmd2`` uses multiline regular
+expression matching, and also uses the ``DOTALL`` flag. These two flags subtly
+change the behavior of commonly used special characters like ``.``, ``^`` and
+``$``, so you may want to double check the `Python regular expression
+documentation <https://docs.python.org/3/library/re.html>`_.
+
+If your output has slashes in it, you will need to escape those slashes so the
+stuff between them is not interpred as a regular expression. In this transcript::
+
+ (Cmd) say cd /usr/local/lib/python3.6/site-packages
+ /usr/local/lib/python3.6/site-packages
+
+the output contains slashes. The text between the first slash and the second
+slash, will be interpreted as a regular expression, and those two slashes will
+not be included in the comparison. When replayed, this transcript would
+therefore fail. To fix it, we could either write a regular expression to match
+the path instead of specifying it verbatim, or we can escape the slashes::
+
+ (Cmd) say cd /usr/local/lib/python3.6/site-packages
+ \/usr\/local\/lib\/python3.6\/site-packages
+
+.. warning::
+
+ Be aware of trailing spaces and newlines. Your commands might output
+ trailing spaces which are impossible to see. Instead of leaving them
+ invisible, you can add a regular expression to match them, so that you can
+ see where they are when you look at the transcript::
+
+ (Cmd) set prompt
+ prompt: (Cmd)/ /
+
+ Some terminal emulators strip trailing space when you copy text from them.
+ This could make the actual data generated by your app different than the
+ text you pasted into the transcript, and it might not be readily obvious why
+ the transcript is not passing. Consider using :ref:`output_redirection` to
+ the clipboard or to a file to ensure you accurately capture the output of
+ your command.
+
+ If you aren't using regular expressions, make sure the newlines at the end
+ of your transcript exactly match the output of your commands. A common cause
+ of a failing transcript is an extra or missing newline.
+
+ If you are using regular expressions, be aware that depending on how you
+ write your regex, the newlines after the regex may or may not matter.
+ ``\Z`` matches *after* the newline at the end of the string, whereas
+ ``$`` matches the end of the string *or* just before a newline.
+
+
+Running a transcript
+====================
+
+Once you have created a transcript, it's easy to have your application play it
+back and check the output. From within the ``examples/`` directory::
+
+ $ python example.py --test transcript_regex.txt
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.013s
+
+ OK
+
+The output will look familiar if you use ``unittest``, because that's exactly
+what happens. Each command in the transcript is run, and we ``assert`` the
+output matches the expected result from the transcript.
+
+.. note::
+
+ If you have set ``allow_cli_args`` to False in order to disable parsing of
+ command line arguments at invocation, then the use of ``-t`` or ``--test``
+ to run transcript testing is automatically disabled. In this case, you can
+ alternatively provide a value for the optional ``transcript_files`` when
+ constructing the instance of your ``cmd2.Cmd`` derived class in order to
+ cause a transcript test to run::
+
+ from cmd2 import Cmd
+ class App(Cmd):
+ # customized attributes and methods here
+
+ if __name__ == '__main__':
+ app = App(transcript_files=['exampleSession.txt'])
+ app.cmdloop()
diff --git a/examples/example.py b/examples/example.py
index 482788cc..e4158f13 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -1,14 +1,18 @@
#!/usr/bin/env python
# coding=utf-8
-"""A sample application for cmd2.
+"""
+A sample application for cmd2.
-Thanks to cmd2's built-in transcript testing capability, it also serves as a test suite for example.py when used with
- the exampleSession.txt transcript.
+Thanks to cmd2's built-in transcript testing capability, it also serves as a
+test suite for example.py when used with the exampleSession.txt transcript.
-Running `python example.py -t exampleSession.txt` will run all the commands in the transcript against example.py,
-verifying that the output produced matches the transcript.
+Running `python example.py -t exampleSession.txt` will run all the commands in
+the transcript against example.py, verifying that the output produced matches
+the transcript.
"""
+import random
+
from cmd2 import Cmd, make_option, options, set_use_arg_list
@@ -17,13 +21,16 @@ class CmdLineApp(Cmd):
# Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
# default_to_shell = True
+ MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
+ MUMBLE_FIRST = ['so', 'like', 'well']
+ MUMBLE_LAST = ['right?']
def __init__(self):
self.abbrev = True
self.multilineCommands = ['orate']
self.maxrepeats = 3
- # Add stuff to settable and shortcutgs before calling base class initializer
+ # Add stuff to settable and shortcuts before calling base class initializer
self.settable['maxrepeats'] = 'max repetitions for speak command'
self.shortcuts.update({'&': 'speak'})
@@ -33,10 +40,11 @@ class CmdLineApp(Cmd):
# For option commands, pass a single argument string instead of a list of argument strings to the do_* methods
set_use_arg_list(False)
- @options([make_option('-p', '--piglatin', action="store_true", help="atinLay"),
- make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
- make_option('-r', '--repeat', type="int", help="output [n] times")
- ])
+ opts = [make_option('-p', '--piglatin', action="store_true", help="atinLay"),
+ make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"),
+ make_option('-r', '--repeat', type="int", help="output [n] times")]
+
+ @options(opts, arg_desc='(text to say)')
def do_speak(self, arg, opts=None):
"""Repeats what you tell me to."""
arg = ''.join(arg)
@@ -46,14 +54,30 @@ class CmdLineApp(Cmd):
arg = arg.upper()
repetitions = opts.repeat or 1
for i in range(min(repetitions, self.maxrepeats)):
- self.stdout.write(arg)
- self.stdout.write('\n')
- # self.stdout.write is better than "print", because Cmd can be
- # initialized with a non-standard output destination
+ self.poutput(arg)
+ # recommend using the poutput function instead of
+ # self.stdout.write or "print", because Cmd allows the user
+ # to redirect output
do_say = do_speak # now "say" is a synonym for "speak"
do_orate = do_speak # another synonym, but this one takes multi-line input
+ @options([ make_option('-r', '--repeat', type="int", help="output [n] times") ])
+ def do_mumble(self, arg, opts=None):
+ """Mumbles what you tell me to."""
+ repetitions = opts.repeat or 1
+ arg = arg.split()
+ for i in range(min(repetitions, self.maxrepeats)):
+ output = []
+ if (random.random() < .33):
+ output.append(random.choice(self.MUMBLE_FIRST))
+ for word in arg:
+ if (random.random() < .40):
+ output.append(random.choice(self.MUMBLES))
+ output.append(word)
+ if (random.random() < .25):
+ output.append(random.choice(self.MUMBLE_LAST))
+ self.poutput(' '.join(output))
if __name__ == '__main__':
c = CmdLineApp()
diff --git a/examples/transcript_regex.txt b/examples/transcript_regex.txt
index a310224b..27b4c639 100644
--- a/examples/transcript_regex.txt
+++ b/examples/transcript_regex.txt
@@ -1,17 +1,18 @@
# Run this transcript with "python example.py -t transcript_regex.txt"
# The regex for colors is because no color on Windows.
# The regex for editor will match whatever program you use.
+# regexes on prompts just make the trailing space obvious
(Cmd) set
abbrev: True
autorun_on_edit: False
colors: /(True|False)/
-continuation_prompt: >
+continuation_prompt: >/ /
debug: False
echo: False
-editor: /.*/
+editor: /.*?/
feedback_to_output: False
locals_in_py: True
maxrepeats: 3
-prompt: (Cmd)
+prompt: (Cmd)/ /
quiet: False
timing: False
diff --git a/tests/multiline_transcript.txt b/tests/multiline_transcript.txt
deleted file mode 100644
index 5fe9122c..00000000
--- a/tests/multiline_transcript.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# cmd2 will skip any lines
-# which occur before a valid
-# command prompt
-
-(Cmd) orate This is a test
-> of the
-> emergency broadcast system
-This is a test
-of the
-emergency broadcast system
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 2400066e..5092a2cd 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -7,6 +7,7 @@ Released under MIT license, see LICENSE file
"""
import os
import sys
+import random
import mock
import pytest
@@ -15,11 +16,17 @@ import six
# Used for sm.input: raw_input() for Python 2 or input() for Python 3
import six.moves as sm
-from cmd2 import Cmd, make_option, options, Cmd2TestCase, set_use_arg_list, set_posix_shlex, set_strip_quotes
+from cmd2 import (Cmd, make_option, options, Cmd2TestCase, set_use_arg_list,
+ set_posix_shlex, set_strip_quotes)
from conftest import run_cmd, StdOut, normalize
class CmdLineApp(Cmd):
+
+ MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
+ MUMBLE_FIRST = ['so', 'like', 'well']
+ MUMBLE_LAST = ['right?']
+
def __init__(self, *args, **kwargs):
self.abbrev = True
self.multilineCommands = ['orate']
@@ -47,19 +54,36 @@ class CmdLineApp(Cmd):
"""Repeats what you tell me to."""
arg = ''.join(arg)
if opts.piglatin:
- arg = '%s%say' % (arg[1:].rstrip(), arg[0])
+ arg = '%s%say' % (arg[1:], arg[0])
if opts.shout:
arg = arg.upper()
repetitions = opts.repeat or 1
for i in range(min(repetitions, self.maxrepeats)):
- self.stdout.write(arg)
- self.stdout.write('\n')
- # self.stdout.write is better than "print", because Cmd can be
- # initialized with a non-standard output destination
+ self.poutput(arg)
+ # recommend using the poutput function instead of
+ # self.stdout.write or "print", because Cmd allows the user
+ # to redirect output
do_say = do_speak # now "say" is a synonym for "speak"
do_orate = do_speak # another synonym, but this one takes multi-line input
+ @options([ make_option('-r', '--repeat', type="int", help="output [n] times") ])
+ def do_mumble(self, arg, opts=None):
+ """Mumbles what you tell me to."""
+ repetitions = opts.repeat or 1
+ arg = arg.split()
+ for i in range(min(repetitions, self.maxrepeats)):
+ output = []
+ if (random.random() < .33):
+ output.append(random.choice(self.MUMBLE_FIRST))
+ for word in arg:
+ if (random.random() < .40):
+ output.append(random.choice(self.MUMBLES))
+ output.append(word)
+ if (random.random() < .25):
+ output.append(random.choice(self.MUMBLE_LAST))
+ self.poutput(' '.join(output))
+
class DemoApp(Cmd):
@options(make_option('-n', '--name', action="store", help="your name"))
@@ -107,8 +131,9 @@ def test_base_with_transcript(_cmdline_app):
Documented commands (type help <topic>):
========================================
-_relative_load edit history orate pyscript run say shell show
-cmdenvironment help load py quit save set shortcuts speak
+_relative_load help mumble pyscript save shell speak
+cmdenvironment history orate quit say shortcuts
+edit load py run set show
(Cmd) help say
Repeats what you tell me to.
@@ -232,60 +257,6 @@ def test_commands_at_invocation():
out = app.stdout.buffer
assert out == expected
-
-def test_transcript_from_cmdloop(request, capsys):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
- app = CmdLineApp()
- app.feedback_to_output = True
-
- # Get location of the transcript
- test_dir = os.path.dirname(request.module.__file__)
- transcript_file = os.path.join(test_dir, 'transcript.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
- app.cmdloop()
-
- # Check for the unittest "OK" condition for the 1 test which ran
- expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in"
- expected_end = "s\n\nOK\n"
- out, err = capsys.readouterr()
- if six.PY3:
- assert err.startswith(expected_start)
- assert err.endswith(expected_end)
- else:
- assert err == ''
- assert out == ''
-
-
-def test_multiline_command_transcript_with_comments_at_beginning(request, capsys):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
- app = CmdLineApp()
-
- # Get location of the transcript
- test_dir = os.path.dirname(request.module.__file__)
- transcript_file = os.path.join(test_dir, 'multiline_transcript.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
- app.cmdloop()
-
- # Check for the unittest "OK" condition for the 1 test which ran
- expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in"
- expected_end = "s\n\nOK\n"
- out, err = capsys.readouterr()
- if six.PY3:
- assert err.startswith(expected_start)
- assert err.endswith(expected_end)
- else:
- assert err == ''
- assert out == ''
-
-
def test_invalid_syntax(_cmdline_app, capsys):
run_cmd(_cmdline_app, 'speak "')
out, err = capsys.readouterr()
@@ -293,15 +264,33 @@ def test_invalid_syntax(_cmdline_app, capsys):
assert normalize(str(err)) == expected
-def test_regex_transcript(request, capsys):
- # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
+@pytest.mark.parametrize('filename, feedback_to_output', [
+ ('bol_eol.txt', False),
+ ('characterclass.txt', False),
+ ('dotstar.txt', False),
+ ('extension_notation.txt', False),
+ ('from_cmdloop.txt', True),
+ ('multiline_no_regex.txt', False),
+ ('multiline_regex.txt', False),
+ ('regex_set.txt', False),
+ ('singleslash.txt', False),
+ ('slashes_escaped.txt', False),
+ ('slashslash.txt', False),
+ ('spaces.txt', False),
+ ('word_boundaries.txt', False),
+ ])
+def test_transcript(request, capsys, filename, feedback_to_output):
+ # Create a cmd2.Cmd() instance and make sure basic settings are
+ # like we want for test
app = CmdLineApp()
+ app.feedback_to_output = feedback_to_output
# Get location of the transcript
test_dir = os.path.dirname(request.module.__file__)
- transcript_file = os.path.join(test_dir, 'transcript_regex.txt')
+ transcript_file = os.path.join(test_dir, 'transcripts', filename)
- # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ # 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
@@ -317,3 +306,30 @@ def test_regex_transcript(request, capsys):
else:
assert err == ''
assert out == ''
+
+
+@pytest.mark.parametrize('expected, transformed', [
+ ( 'text with no slashes', 'text\ with\ no\ slashes' ),
+ # stuff with just one slash
+ ( 'use 2/3 cup', 'use\ 2\/3\ cup' ),
+ ( '/tmp is nice', '\/tmp\ is\ nice'),
+ ( 'slash at end/', 'slash\ at\ end\/'),
+ # regexes
+ ( 'specials .*', 'specials\ \.\*' ),
+ ( '/.*/', '.*' ),
+ ( 'specials ^ and + /[0-9]+/', 'specials\ \^\ and\ \+\ [0-9]+' ),
+ ( '/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}\ but\ not\ \/a\{6\}\ with\ .*?\ more' ),
+ ( 'not this slash\/ or this one\/', 'not\ this\ slash\\/\ or\ this\ one\\/' ),
+ ( 'not \/, use /\|?/, not \/', 'not\ \\/\,\ use\ \|?\,\ not\ \\/' ),
+ # inception: slashes in our regex. backslashed on input, bare on output
+ ( 'not \/, use /\/?/, not \/', 'not\ \\/\,\ use\ /?\,\ not\ \\/' ),
+ ( 'the /\/?/ more /.*/ stuff', 'the\ /?\ more\ .*\ stuff' ),
+ ])
+def test_parse_transcript_expected(expected, transformed):
+ app = CmdLineApp()
+
+ class TestMyAppCase(Cmd2TestCase):
+ cmdapp = app
+
+ testcase = TestMyAppCase()
+ assert testcase._transform_transcript_expected(expected) == transformed
diff --git a/tests/transcripts/bol_eol.txt b/tests/transcripts/bol_eol.txt
new file mode 100644
index 00000000..da21ac86
--- /dev/null
+++ b/tests/transcripts/bol_eol.txt
@@ -0,0 +1,6 @@
+# match the text with regular expressions and the newlines as literal text
+
+(Cmd) say -r 3 -s yabba dabba do
+/^Y.*?$/
+/^Y.*?$/
+/^Y.*?$/
diff --git a/tests/transcripts/characterclass.txt b/tests/transcripts/characterclass.txt
new file mode 100644
index 00000000..756044ea
--- /dev/null
+++ b/tests/transcripts/characterclass.txt
@@ -0,0 +1,6 @@
+# match using character classes and special sequence for digits (\d)
+
+(Cmd) say 555-1212
+/[0-9]{3}-[0-9]{4}/
+(Cmd) say 555-1212
+/\d{3}-\d{4}/
diff --git a/tests/transcripts/dotstar.txt b/tests/transcripts/dotstar.txt
new file mode 100644
index 00000000..55c15b75
--- /dev/null
+++ b/tests/transcripts/dotstar.txt
@@ -0,0 +1,4 @@
+# ensure the old standby .* works. We use the non-greedy flavor
+
+(Cmd) say Adopt the pace of nature: her secret is patience.
+Adopt the pace of /.*?/ is patience.
diff --git a/tests/transcripts/extension_notation.txt b/tests/transcripts/extension_notation.txt
new file mode 100644
index 00000000..68e728ca
--- /dev/null
+++ b/tests/transcripts/extension_notation.txt
@@ -0,0 +1,4 @@
+# inception: a regular expression that matches itself
+
+(Cmd) say (?:fred)
+/(?:\(\?:fred\))/
diff --git a/tests/transcript.txt b/tests/transcripts/from_cmdloop.txt
index e95b9b39..5a12ca03 100644
--- a/tests/transcript.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -1,9 +1,13 @@
+# responses with trailing spaces have been matched with a regex
+# so you can see where they are.
+
(Cmd) help
Documented commands (type help <topic>):
========================================
-_relative_load edit history orate pyscript run say shell show
-cmdenvironment help load py quit save set shortcuts speak
+_relative_load help mumble pyscript save shell speak
+cmdenvironment history orate quit say shortcuts
+edit load py run set show/ */
(Cmd) help say
Repeats what you tell me to.
@@ -46,12 +50,11 @@ set maxrepeats 5
say -ps --repeat=5 goodnight, Gracie
(Cmd) run 4
say -ps --repeat=5 goodnight, Gracie
-
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
(Cmd) set prompt "---> "
-prompt - was: (Cmd)
-now: --->
+prompt - was: (Cmd)/ /
+now: --->/ /
diff --git a/tests/transcripts/multiline_no_regex.txt b/tests/transcripts/multiline_no_regex.txt
new file mode 100644
index 00000000..490870cf
--- /dev/null
+++ b/tests/transcripts/multiline_no_regex.txt
@@ -0,0 +1,6 @@
+# test a multi-line command
+
+(Cmd) orate This is a test
+> of the
+> emergency broadcast system
+This is a test of the emergency broadcast system
diff --git a/tests/transcripts/multiline_regex.txt b/tests/transcripts/multiline_regex.txt
new file mode 100644
index 00000000..3487335f
--- /dev/null
+++ b/tests/transcripts/multiline_regex.txt
@@ -0,0 +1,6 @@
+# these regular expressions match multiple lines of text
+
+(Cmd) say -r 3 -s yabba dabba do
+/\A(YA.*?DO\n?){3}/
+(Cmd) say -r 5 -s yabba dabba do
+/\A([A-Z\s]*$){3}/
diff --git a/tests/transcript_regex.txt b/tests/transcripts/regex_set.txt
index a310224b..3a4a234d 100644
--- a/tests/transcript_regex.txt
+++ b/tests/transcripts/regex_set.txt
@@ -1,17 +1,19 @@
# Run this transcript with "python example.py -t transcript_regex.txt"
# The regex for colors is because no color on Windows.
# The regex for editor will match whatever program you use.
+# Regexes on prompts just make the trailing space obvious
+
(Cmd) set
abbrev: True
autorun_on_edit: False
colors: /(True|False)/
-continuation_prompt: >
+continuation_prompt: >/ /
debug: False
echo: False
editor: /.*/
feedback_to_output: False
locals_in_py: True
maxrepeats: 3
-prompt: (Cmd)
+prompt: (Cmd)/ /
quiet: False
timing: False
diff --git a/tests/transcripts/singleslash.txt b/tests/transcripts/singleslash.txt
new file mode 100644
index 00000000..f3b291f9
--- /dev/null
+++ b/tests/transcripts/singleslash.txt
@@ -0,0 +1,5 @@
+# even if you only have a single slash, you have
+# to escape it
+
+(Cmd) say use 2/3 cup of sugar
+use 2\/3 cup of sugar
diff --git a/tests/transcripts/slashes_escaped.txt b/tests/transcripts/slashes_escaped.txt
new file mode 100644
index 00000000..09bbe3bb
--- /dev/null
+++ b/tests/transcripts/slashes_escaped.txt
@@ -0,0 +1,6 @@
+# escape those slashes
+
+(Cmd) say /some/unix/path
+\/some\/unix\/path
+(Cmd) say mix 2/3 c. sugar, 1/2 c. butter, and 1/2 tsp. salt
+mix 2\/3 c. sugar, 1\/2 c. butter, and 1\/2 tsp. salt
diff --git a/tests/transcripts/slashslash.txt b/tests/transcripts/slashslash.txt
new file mode 100644
index 00000000..2504b0ba
--- /dev/null
+++ b/tests/transcripts/slashslash.txt
@@ -0,0 +1,4 @@
+# ensure consecutive slashes are parsed correctly
+
+(Cmd) say //
+\/\/
diff --git a/tests/transcripts/spaces.txt b/tests/transcripts/spaces.txt
new file mode 100644
index 00000000..615fcbd7
--- /dev/null
+++ b/tests/transcripts/spaces.txt
@@ -0,0 +1,8 @@
+# check spaces in all their forms
+
+(Cmd) say how many spaces
+how many spaces
+(Cmd) say how many spaces
+how/\s{1}/many/\s{1}/spaces
+(Cmd) say "how many spaces"
+how/\s+/many/\s+/spaces
diff --git a/tests/transcripts/word_boundaries.txt b/tests/transcripts/word_boundaries.txt
new file mode 100644
index 00000000..e79cfc4f
--- /dev/null
+++ b/tests/transcripts/word_boundaries.txt
@@ -0,0 +1,6 @@
+# use word boundaries to check for key words in the output
+
+(Cmd) mumble maybe we could go to lunch
+/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
+(Cmd) mumble maybe we could go to lunch
+/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/