Skip to content

Commit 1f2fea6

Browse files
committed
Write documentation for revised transcription feature
1 parent 0fff2be commit 1f2fea6

4 files changed

Lines changed: 176 additions & 34 deletions

File tree

docs/freefeatures.rst

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -301,34 +301,20 @@ is equivalent to ``shell ls``.)
301301
Transcript-based testing
302302
========================
303303

304-
If the entire transcript (input and output) of a successful session of
305-
a ``cmd2``-based app is copied from the screen and pasted into a text
306-
file, ``transcript.txt``, then a transcript test can be run against it::
304+
A transcript is both the input and output of a successful session of a
305+
``cmd2``-based app which is saved to a text file. The transcript can be played
306+
back into the app as a unit test.
307307

308-
python app.py --test transcript.txt
308+
.. code-block:: none
309309
310-
Any non-whitespace deviations between the output prescribed in ``transcript.txt`` and
311-
the actual output from a fresh run of the application will be reported
312-
as a unit test failure. (Whitespace is ignored during the comparison.)
310+
$ python example.py --test transcript_regex.txt
311+
.
312+
----------------------------------------------------------------------
313+
Ran 1 test in 0.013s
313314
314-
Regular expressions can be embedded in the transcript inside paired ``/``
315-
slashes. These regular expressions should not include any whitespace
316-
expressions.
315+
OK
317316
318-
.. note::
319-
320-
If you have set ``allow_cli_args`` to False in order to disable parsing of command line arguments at invocation,
321-
then the use of ``-t`` or ``--test`` to run transcript testing is automatically disabled. In this case, you can
322-
alternatively provide a value for the optional ``transcript_files`` when constructing the instance of your
323-
``cmd2.Cmd`` derived class in order to cause a transcript test to run::
324-
325-
from cmd2 import Cmd
326-
class App(Cmd):
327-
# customized attributes and methods here
328-
329-
if __name__ == '__main__':
330-
app = App(transcript_files=['exampleSession.txt'])
331-
app.cmdloop()
317+
See :doc:`<transcription>` for more details.
332318

333319

334320
Tab-Completion

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Contents:
6666
freefeatures
6767
settingchanges
6868
unfreefeatures
69+
transcription
6970
integrating
7071
hooks
7172
alternatives

docs/transcript.rst

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
========================
2+
Transcript based testing
3+
========================
4+
5+
A transcript is both the input and output of a successful session of a
6+
``cmd2``-based app which is saved to a text file. The transcript can be played
7+
back into the app as a unit test. You can embed regular expressions into the
8+
transcript to accomodate commands that produce dynamic or variable output.
9+
10+
11+
Creating a transcript
12+
=====================
13+
14+
Here's a transcript created from ``python examples/example.py``:
15+
16+
.. code-block:: none
17+
18+
(Cmd) say -r 3 Goodnight, Gracie
19+
Goodnight, Gracie
20+
Goodnight, Gracie
21+
Goodnight, Gracie
22+
(Cmd) mumble maybe we could go to lunch
23+
like maybe we ... could go to hmmm lunch
24+
(Cmd) mumble maybe we could go to lunch
25+
well maybe we could like go to er lunch right?
26+
27+
This transcript has three commands: you can see them on the lines that begin
28+
with the prompt, which in this case is ``(Cmd) ``. Following each command is
29+
the output generated by that command.
30+
31+
Any lines in the transcript before the first line that begins with the prompt
32+
are ignored. You can take advantage of this by using the first lines of the
33+
transcript as comments.
34+
35+
.. code-block:: none
36+
37+
# Lines at the beginning of the transcript that do not
38+
; start with the prompt i.e. '(Cmd) ' are ignored.
39+
/* You can use them for comments. */
40+
41+
All six of these lines before the first prompt are treated as comments.
42+
43+
(Cmd) say -r 3 Goodnight, Gracie
44+
Goodnight, Gracie
45+
Goodnight, Gracie
46+
Goodnight, Gracie
47+
(Cmd) mumble maybe we could go to lunch
48+
like maybe we ... could go to hmmm lunch
49+
(Cmd) mumble maybe we could go to lunch
50+
maybe we could like go to er lunch right?
51+
52+
In this example I've used several different commenting styles, and even bare
53+
text. It doesn't matter what you put on those beginning lines. Everything before
54+
the first line that starts with ``(Cmd) `` will be ignored.
55+
56+
If we used this transcript as-is, it would likely fail. As you can see, the
57+
``mumble`` command doesn't always return the same thing. The ``mumble`` command
58+
inserts random words into the input. Transcripts can include regular
59+
expressions as a way to check for output that can change.
60+
61+
Regular expressions can be included in the response portion of a transcript,
62+
and are surrounded by slashes.
63+
64+
.. code-block:: none
65+
66+
(Cmd) mumble maybe we could go to lunch
67+
/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
68+
(Cmd) mumble maybe we could go to lunch
69+
/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
70+
71+
Without creating a tutorial on regular expressions, this one matches anything
72+
that has the words `maybe`, `could`, and `lunch` in that order. It doesn't
73+
ensure that `we` or `go` or `to` appear in the output, but it does work if
74+
mumble happens to add words to the beginning or the end of the output.
75+
76+
Since the output could be multiple lines long, ``cmd2`` uses multiline regular
77+
expression matching, and also uses the ``DOTALL`` flag, which subtly changes the behavior of commonly
78+
used special characters like `.`, `^` and `$`, so you may want to double check the
79+
`Python regular expression documentation
80+
<https://docs.python.org/3/library/re.html>`_.
81+
82+
If your output has slashes in it, you will need to escape those slashes so the
83+
stuff between them is not interpred as a regular expression. In this transcript::
84+
85+
(Cmd) say cd /usr/local/lib/python3.6/site-packages
86+
/usr/local/lib/python3.6/site-packages
87+
88+
the output contains slashes. The text between the first slash and the second
89+
slash, (``usr``) will be interpreted as a regular expression, and those two
90+
slashes will not be included in the comparison. When replayed, this transcript
91+
would therefore fail. To fix it, we could either write a regular expression to
92+
match the path instead of specifying it verbatim, or we can escape the slashes::
93+
94+
(Cmd) say cd /usr/local/lib/python3.6/site-packages
95+
\/usr\/local\/lib\/python3.6\/site-packages
96+
97+
98+
Running a transcript
99+
====================
100+
101+
Once you have created a transcript, it's easy to have your application play it
102+
back and check the output. From within the ``examples/`` directory:
103+
104+
.. code-block:: none
105+
106+
$ python example.py --test transcript_regex.txt
107+
.
108+
----------------------------------------------------------------------
109+
Ran 1 test in 0.013s
110+
111+
OK
112+
113+
The output will look familiar if you use ``unittest``, because that's exactly
114+
what happens. Each command in the transcript is run, and the output is
115+
``asserted`` to match expected result from the transcript.
116+
117+
.. note::
118+
119+
If you have set ``allow_cli_args`` to False in order to disable parsing of
120+
command line arguments at invocation, then the use of ``-t`` or ``--test``
121+
to run transcript testing is automatically disabled. In this case, you can
122+
alternatively provide a value for the optional ``transcript_files`` when
123+
constructing the instance of your ``cmd2.Cmd`` derived class in order to
124+
cause a transcript test to run::
125+
126+
from cmd2 import Cmd
127+
class App(Cmd):
128+
# customized attributes and methods here
129+
130+
if __name__ == '__main__':
131+
app = App(transcript_files=['exampleSession.txt'])
132+
app.cmdloop()

examples/example.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
#!/usr/bin/env python
22
# coding=utf-8
3-
"""A sample application for cmd2.
3+
"""
4+
A sample application for cmd2.
45
5-
Thanks to cmd2's built-in transcript testing capability, it also serves as a test suite for example.py when used with
6-
the exampleSession.txt transcript.
6+
Thanks to cmd2's built-in transcript testing capability, it also serves as a
7+
test suite for example.py when used with the exampleSession.txt transcript.
78
8-
Running `python example.py -t exampleSession.txt` will run all the commands in the transcript against example.py,
9-
verifying that the output produced matches the transcript.
9+
Running `python example.py -t exampleSession.txt` will run all the commands in
10+
the transcript against example.py, verifying that the output produced matches
11+
the transcript.
1012
"""
1113

14+
import random
15+
1216
from cmd2 import Cmd, make_option, options, set_use_arg_list
1317

1418

@@ -17,13 +21,16 @@ class CmdLineApp(Cmd):
1721

1822
# Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
1923
# default_to_shell = True
24+
MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
25+
MUMBLE_FIRST = ['so', 'like', 'well']
26+
MUMBLE_LAST = ['right?']
2027

2128
def __init__(self):
2229
self.abbrev = True
2330
self.multilineCommands = ['orate']
2431
self.maxrepeats = 3
2532

26-
# Add stuff to settable and shortcutgs before calling base class initializer
33+
# Add stuff to settable and shortcuts before calling base class initializer
2734
self.settable['maxrepeats'] = 'max repetitions for speak command'
2835
self.shortcuts.update({'&': 'speak'})
2936

@@ -46,14 +53,30 @@ def do_speak(self, arg, opts=None):
4653
arg = arg.upper()
4754
repetitions = opts.repeat or 1
4855
for i in range(min(repetitions, self.maxrepeats)):
49-
self.stdout.write(arg)
50-
self.stdout.write('\n')
51-
# self.stdout.write is better than "print", because Cmd can be
52-
# initialized with a non-standard output destination
56+
self.poutput(arg)
57+
# recommend using the poutput function instead of
58+
# self.stdout.write or "print", because Cmd allows the user
59+
# to redirect output
5360

5461
do_say = do_speak # now "say" is a synonym for "speak"
5562
do_orate = do_speak # another synonym, but this one takes multi-line input
5663

64+
@options([ make_option('-r', '--repeat', type="int", help="output [n] times") ])
65+
def do_mumble(self, arg, opts=None):
66+
"""Mumbles what you tell me to."""
67+
repetitions = opts.repeat or 1
68+
arg = arg.split()
69+
for i in range(min(repetitions, self.maxrepeats)):
70+
output = []
71+
if (random.random() < .33):
72+
output.append(random.choice(self.MUMBLE_FIRST))
73+
for word in arg:
74+
if (random.random() < .40):
75+
output.append(random.choice(self.MUMBLES))
76+
output.append(word)
77+
if (random.random() < .25):
78+
output.append(random.choice(self.MUMBLE_LAST))
79+
self.poutput(' '.join(output))
5780

5881
if __name__ == '__main__':
5982
c = CmdLineApp()

0 commit comments

Comments
 (0)