summaryrefslogtreecommitdiff
path: root/docs/features/transcripts.rst
blob: 9bab89968c804b6ca7845a799253809645ef8a1e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
Transcripts
===========

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 From History
---------------------

A transcript can automatically generated based upon commands previously
executed in the *history* using ``history -t``::

    (Cmd) help
    ...
    (Cmd) help history
    ...
    (Cmd) history 1:2 -t transcript.txt
    2 commands and outputs saved to transcript file 'transcript.txt'

This is by far the easiest way to generate a transcript.

.. warning::

   Make sure you use the **poutput()** method in your ``cmd2`` application for
   generating command output.  This method of the ``cmd2.Cmd`` class ensure
   that output is properly redirected when redirecting to a file, piping to a
   shell command, and when generating a transcript.


Creating From A Script File
---------------------------

A transcript can also be automatically generated from a script file using
``run_script -t``::

    (Cmd) run_script scripts/script.txt -t transcript.txt
    2 commands and their outputs saved to transcript file 'transcript.txt'
    (Cmd)

This is a particularly attractive option for automatically regenerating
transcripts for regression testing as your ``cmd2`` application changes.


Creating Manually
-----------------

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.11/site-packages
   /usr/local/lib/python3.11/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.11/site-packages
   \/usr\/local\/lib\/python3.11\/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 editor
      editor: vim/ /

   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:`features/redirection:Output Redirection and Pipes` 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 passed an ``allow_cli_args`` parameter containing `False` to
   :meth:`cmd2.Cmd.__init__` 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()