diff options
-rwxr-xr-x | cmd2.py | 114 | ||||
-rw-r--r-- | docs/freefeatures.rst | 36 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rw-r--r-- | docs/transcript.rst | 161 | ||||
-rwxr-xr-x | examples/example.py | 52 | ||||
-rw-r--r-- | examples/transcript_regex.txt | 7 | ||||
-rw-r--r-- | tests/multiline_transcript.txt | 10 | ||||
-rw-r--r-- | tests/test_transcript.py | 148 | ||||
-rw-r--r-- | tests/transcripts/bol_eol.txt | 6 | ||||
-rw-r--r-- | tests/transcripts/characterclass.txt | 6 | ||||
-rw-r--r-- | tests/transcripts/dotstar.txt | 4 | ||||
-rw-r--r-- | tests/transcripts/extension_notation.txt | 4 | ||||
-rw-r--r-- | tests/transcripts/from_cmdloop.txt (renamed from tests/transcript.txt) | 13 | ||||
-rw-r--r-- | tests/transcripts/multiline_no_regex.txt | 6 | ||||
-rw-r--r-- | tests/transcripts/multiline_regex.txt | 6 | ||||
-rw-r--r-- | tests/transcripts/regex_set.txt (renamed from tests/transcript_regex.txt) | 6 | ||||
-rw-r--r-- | tests/transcripts/singleslash.txt | 5 | ||||
-rw-r--r-- | tests/transcripts/slashes_escaped.txt | 6 | ||||
-rw-r--r-- | tests/transcripts/slashslash.txt | 4 | ||||
-rw-r--r-- | tests/transcripts/spaces.txt | 8 | ||||
-rw-r--r-- | tests/transcripts/word_boundaries.txt | 6 |
21 files changed, 466 insertions, 143 deletions
@@ -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.*/ |