Skip to content

Commit 132a788

Browse files
authored
Merge pull request #387 from python-cmd2/transcript_tests
Unit test coverage for transcripts from history
2 parents 92fdf41 + f826598 commit 132a788

File tree

4 files changed

+108
-172
lines changed

4 files changed

+108
-172
lines changed

cmd2/cmd2.py

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3002,34 +3002,7 @@ def do_history(self, args):
30023002
except Exception as e:
30033003
self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False)
30043004
elif args.transcript:
3005-
# Make sure echo is on so commands print to standard out
3006-
saved_echo = self.echo
3007-
self.echo = True
3008-
3009-
# Redirect stdout to the transcript file
3010-
saved_self_stdout = self.stdout
3011-
self.stdout = open(args.transcript, 'w')
3012-
3013-
# Run all of the commands in the history with output redirected to transcript and echo on
3014-
self.runcmds_plus_hooks(history)
3015-
3016-
# Restore stdout to its original state
3017-
self.stdout.close()
3018-
self.stdout = saved_self_stdout
3019-
3020-
# Set echo back to its original state
3021-
self.echo = saved_echo
3022-
3023-
# Post-process the file to escape un-escaped "/" regex escapes
3024-
with open(args.transcript, 'r') as fin:
3025-
data = fin.read()
3026-
post_processed_data = data.replace('/', '\/')
3027-
with open(args.transcript, 'w') as fout:
3028-
fout.write(post_processed_data)
3029-
3030-
plural = 's' if len(history) > 1 else ''
3031-
self.pfeedback('{} command{} and outputs saved to transcript file {!r}'.format(len(history), plural,
3032-
args.transcript))
3005+
self._generate_transcript(history, args.transcript)
30333006
else:
30343007
# Display the history items retrieved
30353008
for hi in history:
@@ -3038,6 +3011,68 @@ def do_history(self, args):
30383011
else:
30393012
self.poutput(hi.pr())
30403013

3014+
def _generate_transcript(self, history, transcript_file):
3015+
"""Generate a transcript file from a given history of commands."""
3016+
# Save the current echo state, and turn it off. We inject commands into the
3017+
# output using a different mechanism
3018+
saved_echo = self.echo
3019+
self.echo = False
3020+
3021+
# Redirect stdout to the transcript file
3022+
saved_self_stdout = self.stdout
3023+
3024+
# The problem with supporting regular expressions in transcripts
3025+
# is that they shouldn't be processed in the command, just the output.
3026+
# In addition, when we generate a transcript, any slashes in the output
3027+
# are not really intended to indicate regular expressions, so they should
3028+
# be escaped.
3029+
#
3030+
# We have to jump through some hoops here in order to catch the commands
3031+
# separately from the output and escape the slashes in the output.
3032+
transcript = ''
3033+
for history_item in history:
3034+
# build the command, complete with prompts. When we replay
3035+
# the transcript, we look for the prompts to separate
3036+
# the command from the output
3037+
first = True
3038+
command = ''
3039+
for line in history_item.splitlines():
3040+
if first:
3041+
command += '{}{}\n'.format(self.prompt, line)
3042+
first = False
3043+
else:
3044+
command += '{}{}\n'.format(self.continuation_prompt, line)
3045+
transcript += command
3046+
# create a new string buffer and set it to stdout to catch the output
3047+
# of the command
3048+
membuf = io.StringIO()
3049+
self.stdout = membuf
3050+
# then run the command and let the output go into our buffer
3051+
self.onecmd_plus_hooks(history_item)
3052+
# rewind the buffer to the beginning
3053+
membuf.seek(0)
3054+
# get the output out of the buffer
3055+
output = membuf.read()
3056+
# and add the regex-escaped output to the transcript
3057+
transcript += output.replace('/', '\/')
3058+
3059+
# Restore stdout to its original state
3060+
self.stdout = saved_self_stdout
3061+
# Set echo back to its original state
3062+
self.echo = saved_echo
3063+
3064+
# finally, we can write the transcript out to the file
3065+
with open(transcript_file, 'w') as fout:
3066+
fout.write(transcript)
3067+
3068+
# and let the user know what we did
3069+
if len(history) > 1:
3070+
plural = 'commands and their outputs'
3071+
else:
3072+
plural = 'command and its output'
3073+
msg = '{} {} saved to transcript file {!r}'
3074+
self.pfeedback(msg.format(len(history), plural, transcript_file))
3075+
30413076
@with_argument_list
30423077
def do_edit(self, arglist):
30433078
"""Edit a file in a text editor.

tests/test_argparse.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ def argparse_app():
119119
return app
120120

121121

122+
def test_invalid_syntax(argparse_app, capsys):
123+
run_cmd(argparse_app, 'speak "')
124+
out, err = capsys.readouterr()
125+
assert err == "ERROR: Invalid syntax: No closing quotation\n"
126+
122127
def test_argparse_basic_command(argparse_app):
123128
out = run_cmd(argparse_app, 'say hello')
124129
assert out == ['hello']
@@ -135,6 +140,14 @@ def test_argparse_with_list_and_empty_doc(argparse_app):
135140
out = run_cmd(argparse_app, 'speak -s hello world!')
136141
assert out == ['HELLO WORLD!']
137142

143+
def test_argparse_comment_stripping(argparse_app):
144+
out = run_cmd(argparse_app, 'speak it was /* not */ delicious! # Yuck!')
145+
assert out == ['it was delicious!']
146+
147+
def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app):
148+
out = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!")
149+
assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!']
150+
138151
def test_argparse_quoted_arguments_multiple(argparse_app):
139152
out = run_cmd(argparse_app, 'say "hello there" "rick & morty"')
140153
assert out == ['hello there rick & morty']

tests/test_transcript.py

Lines changed: 32 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
import sys
1111
import re
1212
import random
13+
import tempfile
1314

1415
from unittest import mock
1516
import pytest
1617

1718
from cmd2 import cmd2
18-
from .conftest import run_cmd, StdOut, normalize
19+
from .conftest import run_cmd, StdOut
1920

2021
class CmdLineApp(cmd2.Cmd):
2122

@@ -26,7 +27,6 @@ class CmdLineApp(cmd2.Cmd):
2627
def __init__(self, *args, **kwargs):
2728
self.multiline_commands = ['orate']
2829
self.maxrepeats = 3
29-
self.redirector = '->'
3030

3131
# Add stuff to settable and/or shortcuts before calling base class initializer
3232
self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
@@ -63,7 +63,7 @@ def do_speak(self, opts, arg):
6363
def do_mumble(self, opts, arg):
6464
"""Mumbles what you tell me to."""
6565
repetitions = opts.repeat or 1
66-
arg = arg.split()
66+
#arg = arg.split()
6767
for i in range(min(repetitions, self.maxrepeats)):
6868
output = []
6969
if random.random() < .33:
@@ -77,136 +77,6 @@ def do_mumble(self, opts, arg):
7777
self.poutput(' '.join(output))
7878

7979

80-
class DemoApp(cmd2.Cmd):
81-
hello_parser = argparse.ArgumentParser()
82-
hello_parser.add_argument('-n', '--name', help="your name")
83-
@cmd2.with_argparser_and_unknown_args(hello_parser)
84-
def do_hello(self, opts, arg):
85-
"""Says hello."""
86-
if opts.name:
87-
self.stdout.write('Hello {}\n'.format(opts.name))
88-
else:
89-
self.stdout.write('Hello Nobody\n')
90-
91-
92-
@pytest.fixture
93-
def _cmdline_app():
94-
c = CmdLineApp()
95-
c.stdout = StdOut()
96-
return c
97-
98-
99-
@pytest.fixture
100-
def _demo_app():
101-
c = DemoApp()
102-
c.stdout = StdOut()
103-
return c
104-
105-
106-
def _get_transcript_blocks(transcript):
107-
cmd = None
108-
expected = ''
109-
for line in transcript.splitlines():
110-
if line.startswith('(Cmd) '):
111-
if cmd is not None:
112-
yield cmd, normalize(expected)
113-
114-
cmd = line[6:]
115-
expected = ''
116-
else:
117-
expected += line + '\n'
118-
yield cmd, normalize(expected)
119-
120-
121-
def test_base_with_transcript(_cmdline_app):
122-
app = _cmdline_app
123-
transcript = """
124-
(Cmd) help
125-
126-
Documented commands (type help <topic>):
127-
========================================
128-
alias help load orate pyscript say shell speak
129-
edit history mumble py quit set shortcuts unalias
130-
131-
(Cmd) help say
132-
usage: speak [-h] [-p] [-s] [-r REPEAT]
133-
134-
Repeats what you tell me to.
135-
136-
optional arguments:
137-
-h, --help show this help message and exit
138-
-p, --piglatin atinLay
139-
-s, --shout N00B EMULATION MODE
140-
-r REPEAT, --repeat REPEAT
141-
output [n] times
142-
143-
(Cmd) say goodnight, Gracie
144-
goodnight, Gracie
145-
(Cmd) say -ps --repeat=5 goodnight, Gracie
146-
OODNIGHT, GRACIEGAY
147-
OODNIGHT, GRACIEGAY
148-
OODNIGHT, GRACIEGAY
149-
(Cmd) set maxrepeats 5
150-
maxrepeats - was: 3
151-
now: 5
152-
(Cmd) say -ps --repeat=5 goodnight, Gracie
153-
OODNIGHT, GRACIEGAY
154-
OODNIGHT, GRACIEGAY
155-
OODNIGHT, GRACIEGAY
156-
OODNIGHT, GRACIEGAY
157-
OODNIGHT, GRACIEGAY
158-
(Cmd) history
159-
-------------------------[1]
160-
help
161-
-------------------------[2]
162-
help say
163-
-------------------------[3]
164-
say goodnight, Gracie
165-
-------------------------[4]
166-
say -ps --repeat=5 goodnight, Gracie
167-
-------------------------[5]
168-
set maxrepeats 5
169-
-------------------------[6]
170-
say -ps --repeat=5 goodnight, Gracie
171-
(Cmd) history -r 4
172-
OODNIGHT, GRACIEGAY
173-
OODNIGHT, GRACIEGAY
174-
OODNIGHT, GRACIEGAY
175-
OODNIGHT, GRACIEGAY
176-
OODNIGHT, GRACIEGAY
177-
(Cmd) set prompt "---> "
178-
prompt - was: (Cmd)
179-
now: --->
180-
"""
181-
182-
for cmd, expected in _get_transcript_blocks(transcript):
183-
out = run_cmd(app, cmd)
184-
assert out == expected
185-
186-
187-
class TestMyAppCase(cmd2.Cmd2TestCase):
188-
CmdApp = CmdLineApp
189-
CmdApp.testfiles = ['tests/transcript.txt']
190-
191-
192-
def test_comment_stripping(_cmdline_app):
193-
out = run_cmd(_cmdline_app, 'speak it was /* not */ delicious! # Yuck!')
194-
expected = normalize("""it was delicious!""")
195-
assert out == expected
196-
197-
198-
def test_argparser_correct_args_with_quotes_and_midline_options(_cmdline_app):
199-
out = run_cmd(_cmdline_app, "speak 'This is a' -s test of the emergency broadcast system!")
200-
expected = normalize("""THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!""")
201-
assert out == expected
202-
203-
204-
def test_argparser_options_with_spaces_in_quotes(_demo_app):
205-
out = run_cmd(_demo_app, "hello foo -n 'Bugs Bunny' bar baz")
206-
expected = normalize("""Hello Bugs Bunny""")
207-
assert out == expected
208-
209-
21080
def test_commands_at_invocation():
21181
testargs = ["prog", "say hello", "say Gracie", "quit"]
21282
expected = "This is an intro banner ...\nhello\nGracie\n"
@@ -217,27 +87,20 @@ def test_commands_at_invocation():
21787
out = app.stdout.buffer
21888
assert out == expected
21989

220-
def test_invalid_syntax(_cmdline_app, capsys):
221-
run_cmd(_cmdline_app, 'speak "')
222-
out, err = capsys.readouterr()
223-
expected = normalize("""ERROR: Invalid syntax: No closing quotation""")
224-
assert normalize(str(err)) == expected
225-
226-
227-
@pytest.mark.parametrize('filename, feedback_to_output', [
90+
@pytest.mark.parametrize('filename,feedback_to_output', [
22891
('bol_eol.txt', False),
22992
('characterclass.txt', False),
23093
('dotstar.txt', False),
23194
('extension_notation.txt', False),
232-
# ('from_cmdloop.txt', True),
95+
('from_cmdloop.txt', True),
23396
('multiline_no_regex.txt', False),
23497
('multiline_regex.txt', False),
23598
('regex_set.txt', False),
23699
('singleslash.txt', False),
237100
('slashes_escaped.txt', False),
238101
('slashslash.txt', False),
239102
('spaces.txt', False),
240-
# ('word_boundaries.txt', False),
103+
('word_boundaries.txt', False),
241104
])
242105
def test_transcript(request, capsys, filename, feedback_to_output):
243106
# Create a cmd2.Cmd() instance and make sure basic settings are
@@ -263,6 +126,32 @@ def test_transcript(request, capsys, filename, feedback_to_output):
263126
assert err.startswith(expected_start)
264127
assert err.endswith(expected_end)
265128

129+
def test_history_transcript(request, capsys):
130+
app = CmdLineApp()
131+
app.stdout = StdOut()
132+
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
133+
run_cmd(app, 'speak /tmp/file.txt is not a regex')
134+
135+
expected = r"""(Cmd) orate this is
136+
> a /multiline/
137+
> command;
138+
this is a \/multiline\/ command
139+
(Cmd) speak /tmp/file.txt is not a regex
140+
\/tmp\/file.txt is not a regex
141+
"""
142+
143+
# make a tmp file
144+
fd, history_fname = tempfile.mkstemp(prefix='', suffix='.txt')
145+
os.close(fd)
146+
147+
# tell the history command to create a transcript
148+
run_cmd(app, 'history -t "{}"'.format(history_fname))
149+
150+
# read in the transcript created by the history command
151+
with open(history_fname) as f:
152+
transcript = f.read()
153+
154+
assert transcript == expected
266155

267156
@pytest.mark.parametrize('expected, transformed', [
268157
# strings with zero or one slash or with escaped slashes means no regular

tests/transcripts/from_cmdloop.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ optional arguments:/ */
1919
-s, --shout N00B EMULATION MODE/ */
2020
-r REPEAT, --repeat REPEAT/ */
2121
output [n] times
22-
2322
(Cmd) say goodnight, Gracie
2423
goodnight, Gracie
2524
(Cmd) say -ps --repeat=5 goodnight, Gracie

0 commit comments

Comments
 (0)