Skip to content

Commit f3fa226

Browse files
author
Todd Leonhardt
committed
Fixed transcript testing issues
Transcript testing no longer creates an unnecessary 2nd instance of the class derived from cmd2.Cmd. This dramatically simplifies transcript testing for derived classes which have required parameters during construction. As a side effect the, feedback_to_output attribute now defaults to false. This had some minor ripple effects on various unit tests.
1 parent 90330f2 commit f3fa226

File tree

5 files changed

+32
-56
lines changed

5 files changed

+32
-56
lines changed

cmd2.py

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ class Cmd(cmd.Cmd):
618618
for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
619619
if _which(editor):
620620
break
621-
feedback_to_output = False # Do include nonessentials in >, | output
621+
feedback_to_output = True # Do include nonessentials in >, | output
622622
locals_in_py = True
623623
quiet = False # Do not suppress nonessential output
624624
timing = False # Prints elapsed time for each command
@@ -1691,7 +1691,7 @@ def run_transcript_tests(self, callargs):
16911691
:param callargs: List[str] - list of transcript test file names
16921692
"""
16931693
class TestMyAppCase(Cmd2TestCase):
1694-
CmdApp = self.__class__
1694+
cmdapp = self
16951695

16961696
self.__class__.testfiles = callargs
16971697
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
@@ -1731,12 +1731,12 @@ def cmdloop(self, intro=None):
17311731
if callopts.test:
17321732
self._transcript_files = callargs
17331733

1734+
# Always run the preloop first
1735+
self.preloop()
1736+
17341737
if self._transcript_files is not None:
17351738
self.run_transcript_tests(self._transcript_files)
17361739
else:
1737-
# Always run the preloop first
1738-
self.preloop()
1739-
17401740
# If an intro was supplied in the method call, allow it to override the default
17411741
if intro is not None:
17421742
self.intro = intro
@@ -1754,8 +1754,8 @@ def cmdloop(self, intro=None):
17541754
if not stop:
17551755
self._cmdloop()
17561756

1757-
# Run the postloop() no matter what
1758-
self.postloop()
1757+
# Run the postloop() no matter what
1758+
self.postloop()
17591759

17601760

17611761
class HistoryItem(str):
@@ -1960,25 +1960,11 @@ def restore(self):
19601960
setattr(self.obj, attrib, getattr(self, attrib))
19611961

19621962

1963-
class Borg(object):
1964-
"""All instances of any Borg subclass will share state.
1965-
from Python Cookbook, 2nd Ed., recipe 6.16"""
1966-
_shared_state = {}
1967-
1968-
def __new__(cls, *a, **k):
1969-
obj = object.__new__(cls)
1970-
obj.__dict__ = cls._shared_state
1971-
return obj
1972-
1973-
1974-
class OutputTrap(Borg):
1975-
"""Instantiate an OutputTrap to divert/capture ALL stdout output. For use in unit testing.
1976-
Call `tearDown()` to return to normal output."""
1963+
class OutputTrap(object):
1964+
"""Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing."""
19771965

19781966
def __init__(self):
19791967
self.contents = ''
1980-
self.old_stdout = sys.stdout
1981-
sys.stdout = self
19821968

19831969
def write(self, txt):
19841970
"""Add text to the internal contents.
@@ -1996,17 +1982,12 @@ def read(self):
19961982
self.contents = ''
19971983
return result
19981984

1999-
def tear_down(self):
2000-
"""Restores normal output."""
2001-
sys.stdout = self.old_stdout
2002-
self.contents = ''
2003-
20041985

20051986
class Cmd2TestCase(unittest.TestCase):
20061987
"""Subclass this, setting CmdApp, to make a unittest.TestCase class
20071988
that will execute the commands in a transcript file and expect the results shown.
20081989
See example.py"""
2009-
CmdApp = None
1990+
cmdapp = None
20101991
regexPattern = pyparsing.QuotedString(quoteChar=r'/', escChar='\\', multiline=True, unquoteResults=True)
20111992
regexPattern.ignore(pyparsing.cStyleComment)
20121993
notRegexPattern = pyparsing.Word(pyparsing.printables)
@@ -2016,7 +1997,7 @@ class Cmd2TestCase(unittest.TestCase):
20161997

20171998
def fetchTranscripts(self):
20181999
self.transcripts = {}
2019-
for fileset in self.CmdApp.testfiles:
2000+
for fileset in self.cmdapp.testfiles:
20202001
for fname in glob.glob(fileset):
20212002
tfile = open(fname)
20222003
self.transcripts[fname] = iter(tfile.readlines())
@@ -2025,17 +2006,15 @@ def fetchTranscripts(self):
20252006
raise Exception("No test files found - nothing to test.")
20262007

20272008
def setUp(self):
2028-
if self.CmdApp:
2029-
self.outputTrap = OutputTrap()
2030-
self.cmdapp = self.CmdApp()
2009+
if self.cmdapp:
20312010
self.fetchTranscripts()
20322011

2033-
# Make sure any required initialization gets done and flush the output buffer
2034-
self.cmdapp.preloop()
2035-
self.outputTrap.read()
2012+
# Trap stdout
2013+
self._orig_stdout = self.cmdapp.stdout
2014+
self.cmdapp.stdout = OutputTrap()
20362015

20372016
def runTest(self): # was testall
2038-
if self.CmdApp:
2017+
if self.cmdapp:
20392018
its = sorted(self.transcripts.items())
20402019
for (fname, transcript) in its:
20412020
self._test_transcript(fname, transcript)
@@ -2071,7 +2050,7 @@ def _test_transcript(self, fname, transcript):
20712050
# Send the command into the application and capture the resulting output
20722051
# TODO: Should we get the return value and act if stop == True?
20732052
self.cmdapp.onecmd_plus_hooks(command)
2074-
result = self.outputTrap.read()
2053+
result = self.cmdapp.stdout.read()
20752054
# Read the expected result from transcript
20762055
if line.startswith(self.cmdapp.prompt):
20772056
message = '\nFile %s, line %d\nCommand was:\n%r\nExpected: (nothing)\nGot:\n%r\n' % \
@@ -2098,11 +2077,9 @@ def _test_transcript(self, fname, transcript):
20982077
self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message)
20992078

21002079
def tearDown(self):
2101-
if self.CmdApp:
2102-
# Make sure any required cleanup gets done
2103-
self.cmdapp.postloop()
2104-
2105-
self.outputTrap.tear_down()
2080+
if self.cmdapp:
2081+
# Restore stdout
2082+
self.cmdapp.stdout = self._orig_stdout
21062083

21072084

21082085
def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')):

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
default_file_name: command.txt
5656
echo: False
5757
editor: vim
58-
feedback_to_output: False
58+
feedback_to_output: True
5959
locals_in_py: True
6060
prompt: (Cmd)
6161
quiet: False
@@ -75,7 +75,7 @@
7575
default_file_name: command.txt # for ``save``, ``load``, etc.
7676
echo: False # Echo command issued into output
7777
editor: vim # Program used by ``edit``
78-
feedback_to_output: False # include nonessentials in `|`, `>` results
78+
feedback_to_output: True # include nonessentials in `|`, `>` results
7979
locals_in_py: True # Allow access to your application in py via self
8080
prompt: (Cmd) # The prompt issued to solicit input
8181
quiet: False # Don't print nonessential feedback

tests/test_cmd2.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,15 @@ def test_base_relative_load(base_app, request):
266266
assert out == expected
267267

268268

269-
def test_base_save(base_app, capsys):
269+
def test_base_save(base_app):
270270
# TODO: Use a temporary directory for the file
271271
filename = 'deleteme.txt'
272272
run_cmd(base_app, 'help')
273273
run_cmd(base_app, 'help save')
274274

275275
# Test the * form of save which saves all commands from history
276-
run_cmd(base_app, 'save * {}'.format(filename))
277-
out, err = capsys.readouterr()
278-
assert out == 'Saved to {}\n'.format(filename)
276+
out = run_cmd(base_app, 'save * {}'.format(filename))
277+
assert out == normalize('Saved to {}\n'.format(filename))
279278
expected = normalize("""
280279
help
281280
@@ -288,18 +287,16 @@ def test_base_save(base_app, capsys):
288287
assert content == expected
289288

290289
# Test the N form of save which saves a numbered command from history
291-
run_cmd(base_app, 'save 1 {}'.format(filename))
292-
out, err = capsys.readouterr()
293-
assert out == 'Saved to {}\n'.format(filename)
290+
out = run_cmd(base_app, 'save 1 {}'.format(filename))
291+
assert out == normalize('Saved to {}\n'.format(filename))
294292
expected = normalize('help')
295293
with open(filename) as f:
296294
content = normalize(f.read())
297295
assert content == expected
298296

299297
# Test the blank form of save which saves the most recent command from history
300-
run_cmd(base_app, 'save {}'.format(filename))
301-
out, err = capsys.readouterr()
302-
assert out == 'Saved to {}\n'.format(filename)
298+
out = run_cmd(base_app, 'save {}'.format(filename))
299+
assert out == normalize('Saved to {}\n'.format(filename))
303300
expected = normalize('save 1 {}'.format(filename))
304301
with open(filename) as f:
305302
content = normalize(f.read())
@@ -397,6 +394,7 @@ def test_send_to_paste_buffer(base_app):
397394

398395

399396
def test_base_timing(base_app, capsys):
397+
base_app.feedback_to_output = False
400398
out = run_cmd(base_app, 'set timing True')
401399
expected = normalize("""timing - was: False
402400
now: True

tests/test_transcript.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def test_base_with_transcript(_cmdline_app):
149149
-------------------------[6]
150150
say -ps --repeat=5 goodnight, Gracie
151151
(Cmd) run 4
152+
say -ps --repeat=5 goodnight, Gracie
152153
OODNIGHT, GRACIEGAY
153154
OODNIGHT, GRACIEGAY
154155
OODNIGHT, GRACIEGAY

tests/transcript_regex.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ debug: False
1010
default_file_name: command.txt
1111
echo: False
1212
editor: /([^\s]+)/
13-
feedback_to_output: False
13+
feedback_to_output: True
1414
locals_in_py: True
1515
maxrepeats: 3
1616
prompt: (Cmd)

0 commit comments

Comments
 (0)