summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2018-05-06 13:58:05 -0600
committerkotfu <kotfu@kotfu.net>2018-05-06 14:02:33 -0600
commitdb41cc3743eb45f0c53a387265f8cf496bbacd29 (patch)
tree38b5f78efab648e8b52b6c15284413a7d1970de9
parenta1cbef5b4af0831ad57e4eaa75bdd77c15cb004b (diff)
downloadcmd2-git-db41cc3743eb45f0c53a387265f8cf496bbacd29.tar.gz
Defer import of unittest
-rwxr-xr-xcmd2/cmd2.py214
-rw-r--r--cmd2/transcript.py213
-rw-r--r--tests/test_transcript.py3
3 files changed, 218 insertions, 212 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 1378f052..9428afb3 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -22,6 +22,7 @@ is used in place of `print`.
Git repository on GitHub at https://github.com/python-cmd2/cmd2
"""
+# many imports are lazy-loaded when they are needed
import argparse
import atexit
import cmd
@@ -43,7 +44,6 @@ import sys
import tempfile
import traceback
from typing import Callable, List, Optional, Union, Tuple
-import unittest
from code import InteractiveConsole
import pyperclip
@@ -3174,6 +3174,8 @@ Script should contain one command per line, just like command would be typed in
:param callargs: List[str] - list of transcript test file names
"""
+ import unittest
+ from .transcript import Cmd2TestCase
class TestMyAppCase(Cmd2TestCase):
cmdapp = self
@@ -3416,216 +3418,6 @@ class Statekeeper(object):
setattr(self.obj, attrib, getattr(self, attrib))
-class OutputTrap(object):
- """Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing."""
-
- def __init__(self):
- self.contents = ''
-
- def write(self, txt):
- """Add text to the internal contents.
-
- :param txt: str
- """
- self.contents += txt
-
- def read(self):
- """Read from the internal contents and then clear them out.
-
- :return: str - text from the internal contents
- """
- result = self.contents
- self.contents = ''
- return result
-
-
-class Cmd2TestCase(unittest.TestCase):
- """Subclass this, setting CmdApp, to make a unittest.TestCase class
- that will execute the commands in a transcript file and expect the results shown.
- See example.py"""
- cmdapp = None
-
- def fetchTranscripts(self):
- self.transcripts = {}
- for fileset in self.cmdapp.testfiles:
- for fname in glob.glob(fileset):
- tfile = open(fname)
- self.transcripts[fname] = iter(tfile.readlines())
- tfile.close()
- if not len(self.transcripts):
- raise Exception("No test files found - nothing to test.")
-
- def setUp(self):
- if self.cmdapp:
- self.fetchTranscripts()
-
- # Trap stdout
- self._orig_stdout = self.cmdapp.stdout
- self.cmdapp.stdout = OutputTrap()
-
- def runTest(self): # was testall
- if self.cmdapp:
- its = sorted(self.transcripts.items())
- for (fname, transcript) in its:
- self._test_transcript(fname, transcript)
-
- def _test_transcript(self, fname, transcript):
- line_num = 0
- finished = False
- line = utils.strip_ansi(next(transcript))
- line_num += 1
- while not finished:
- # Scroll forward to where actual commands begin
- while not line.startswith(self.cmdapp.visible_prompt):
- try:
- line = utils.strip_ansi(next(transcript))
- except StopIteration:
- finished = True
- break
- line_num += 1
- command = [line[len(self.cmdapp.visible_prompt):]]
- line = next(transcript)
- # Read the entirety of a multi-line command
- while line.startswith(self.cmdapp.continuation_prompt):
- command.append(line[len(self.cmdapp.continuation_prompt):])
- try:
- line = next(transcript)
- except StopIteration:
- raise (StopIteration,
- 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num,
- command[0])
- )
- line_num += 1
- command = ''.join(command)
- # Send the command into the application and capture the resulting output
- # TODO: Should we get the return value and act if stop == True?
- self.cmdapp.onecmd_plus_hooks(command)
- result = self.cmdapp.stdout.read()
- # Read the expected result from transcript
- if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
- 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 = []
- while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
- expected.append(line)
- try:
- line = next(transcript)
- except StopIteration:
- finished = True
- break
- line_num += 1
- expected = ''.join(expected)
-
- # 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
-
- Given a string like:
-
- Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/
-
- Turn it into a valid regular expression which matches the literal text
- of the string and the regular expression. We have to remove the slashes
- because they differentiate between plain text and a regular expression.
- Unless the slashes are escaped, in which case they are interpreted as
- plain text, or there is only one slash, which is treated as plain text
- also.
-
- Check the tests in tests/test_transcript.py to see all the edge
- cases.
- """
- 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
-
- @staticmethod
- def _escaped_find(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
- self.cmdapp.stdout = self._orig_stdout
-
-
def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')):
"""Wrapper around namedtuple which lets you treat the last value as optional.
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
new file mode 100644
index 00000000..7cf15d7d
--- /dev/null
+++ b/cmd2/transcript.py
@@ -0,0 +1,213 @@
+#
+# -*- coding: utf-8 -*-
+import re
+import glob
+import unittest
+
+from . import utils
+
+class Cmd2TestCase(unittest.TestCase):
+ """Subclass this, setting CmdApp, to make a unittest.TestCase class
+ that will execute the commands in a transcript file and expect the results shown.
+ See example.py"""
+ cmdapp = None
+
+ def fetchTranscripts(self):
+ self.transcripts = {}
+ for fileset in self.cmdapp.testfiles:
+ for fname in glob.glob(fileset):
+ tfile = open(fname)
+ self.transcripts[fname] = iter(tfile.readlines())
+ tfile.close()
+ if not len(self.transcripts):
+ raise Exception("No test files found - nothing to test.")
+
+ def setUp(self):
+ if self.cmdapp:
+ self.fetchTranscripts()
+
+ # Trap stdout
+ self._orig_stdout = self.cmdapp.stdout
+ self.cmdapp.stdout = OutputTrap()
+
+ def runTest(self): # was testall
+ if self.cmdapp:
+ its = sorted(self.transcripts.items())
+ for (fname, transcript) in its:
+ self._test_transcript(fname, transcript)
+
+ def _test_transcript(self, fname, transcript):
+ line_num = 0
+ finished = False
+ line = utils.strip_ansi(next(transcript))
+ line_num += 1
+ while not finished:
+ # Scroll forward to where actual commands begin
+ while not line.startswith(self.cmdapp.visible_prompt):
+ try:
+ line = utils.strip_ansi(next(transcript))
+ except StopIteration:
+ finished = True
+ break
+ line_num += 1
+ command = [line[len(self.cmdapp.visible_prompt):]]
+ line = next(transcript)
+ # Read the entirety of a multi-line command
+ while line.startswith(self.cmdapp.continuation_prompt):
+ command.append(line[len(self.cmdapp.continuation_prompt):])
+ try:
+ line = next(transcript)
+ except StopIteration as exc:
+ msg = 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, command[0])
+ raise StopIteration(msg) from exc
+ line_num += 1
+ command = ''.join(command)
+ # Send the command into the application and capture the resulting output
+ # TODO: Should we get the return value and act if stop == True?
+ self.cmdapp.onecmd_plus_hooks(command)
+ result = self.cmdapp.stdout.read()
+ # Read the expected result from transcript
+ if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
+ 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 = []
+ while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
+ expected.append(line)
+ try:
+ line = next(transcript)
+ except StopIteration:
+ finished = True
+ break
+ line_num += 1
+ expected = ''.join(expected)
+
+ # 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
+
+ Given a string like:
+
+ Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/
+
+ Turn it into a valid regular expression which matches the literal text
+ of the string and the regular expression. We have to remove the slashes
+ because they differentiate between plain text and a regular expression.
+ Unless the slashes are escaped, in which case they are interpreted as
+ plain text, or there is only one slash, which is treated as plain text
+ also.
+
+ Check the tests in tests/test_transcript.py to see all the edge
+ cases.
+ """
+ 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
+
+ @staticmethod
+ def _escaped_find(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
+ self.cmdapp.stdout = self._orig_stdout
+
+class OutputTrap(object):
+ """Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing."""
+
+ def __init__(self):
+ self.contents = ''
+
+ def write(self, txt):
+ """Add text to the internal contents.
+
+ :param txt: str
+ """
+ self.contents += txt
+
+ def read(self):
+ """Read from the internal contents and then clear them out.
+
+ :return: str - text from the internal contents
+ """
+ result = self.contents
+ self.contents = ''
+ return result
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 70658161..99d4735e 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -17,6 +17,7 @@ import pytest
from cmd2 import cmd2
from .conftest import run_cmd, StdOut
+from cmd2 import transcript
class CmdLineApp(cmd2.Cmd):
@@ -177,7 +178,7 @@ this is a \/multiline\/ command
def test_parse_transcript_expected(expected, transformed):
app = CmdLineApp()
- class TestMyAppCase(cmd2.Cmd2TestCase):
+ class TestMyAppCase(transcript.Cmd2TestCase):
cmdapp = app
testcase = TestMyAppCase()