1# coding=utf-8 2# flake8: noqa E302 3""" 4Cmd2 functional testing based on transcript 5""" 6import os 7import random 8import re 9import sys 10import tempfile 11from unittest import ( 12 mock, 13) 14 15import pytest 16 17import cmd2 18from cmd2 import ( 19 transcript, 20) 21from cmd2.utils import ( 22 Settable, 23 StdSim, 24) 25 26from .conftest import ( 27 run_cmd, 28 verify_help_text, 29) 30 31 32class CmdLineApp(cmd2.Cmd): 33 34 MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] 35 MUMBLE_FIRST = ['so', 'like', 'well'] 36 MUMBLE_LAST = ['right?'] 37 38 def __init__(self, *args, **kwargs): 39 self.maxrepeats = 3 40 41 super().__init__(*args, multiline_commands=['orate'], **kwargs) 42 43 # Make maxrepeats settable at runtime 44 self.add_settable(Settable('maxrepeats', int, 'Max number of `--repeat`s allowed', self)) 45 46 self.intro = 'This is an intro banner ...' 47 48 speak_parser = cmd2.Cmd2ArgumentParser() 49 speak_parser.add_argument('-p', '--piglatin', action="store_true", help="atinLay") 50 speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") 51 speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") 52 53 @cmd2.with_argparser(speak_parser, with_unknown_args=True) 54 def do_speak(self, opts, arg): 55 """Repeats what you tell me to.""" 56 arg = ' '.join(arg) 57 if opts.piglatin: 58 arg = '%s%say' % (arg[1:], arg[0]) 59 if opts.shout: 60 arg = arg.upper() 61 repetitions = opts.repeat or 1 62 for _ in range(min(repetitions, self.maxrepeats)): 63 self.poutput(arg) 64 # recommend using the poutput function instead of 65 # self.stdout.write or "print", because Cmd allows the user 66 # to redirect output 67 68 do_say = do_speak # now "say" is a synonym for "speak" 69 do_orate = do_speak # another synonym, but this one takes multi-line input 70 71 mumble_parser = cmd2.Cmd2ArgumentParser() 72 mumble_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") 73 74 @cmd2.with_argparser(mumble_parser, with_unknown_args=True) 75 def do_mumble(self, opts, arg): 76 """Mumbles what you tell me to.""" 77 repetitions = opts.repeat or 1 78 # arg = arg.split() 79 for _ in range(min(repetitions, self.maxrepeats)): 80 output = [] 81 if random.random() < 0.33: 82 output.append(random.choice(self.MUMBLE_FIRST)) 83 for word in arg: 84 if random.random() < 0.40: 85 output.append(random.choice(self.MUMBLES)) 86 output.append(word) 87 if random.random() < 0.25: 88 output.append(random.choice(self.MUMBLE_LAST)) 89 self.poutput(' '.join(output)) 90 91 def do_nothing(self, statement): 92 """Do nothing and output nothing""" 93 pass 94 95 def do_keyboard_interrupt(self, _): 96 raise KeyboardInterrupt('Interrupting this command') 97 98 99def test_commands_at_invocation(): 100 testargs = ["prog", "say hello", "say Gracie", "quit"] 101 expected = "This is an intro banner ...\nhello\nGracie\n" 102 with mock.patch.object(sys, 'argv', testargs): 103 app = CmdLineApp() 104 105 app.stdout = StdSim(app.stdout) 106 app.cmdloop() 107 out = app.stdout.getvalue() 108 assert out == expected 109 110 111@pytest.mark.parametrize( 112 'filename,feedback_to_output', 113 [ 114 ('bol_eol.txt', False), 115 ('characterclass.txt', False), 116 ('dotstar.txt', False), 117 ('extension_notation.txt', False), 118 ('from_cmdloop.txt', True), 119 ('multiline_no_regex.txt', False), 120 ('multiline_regex.txt', False), 121 ('no_output.txt', False), 122 ('no_output_last.txt', False), 123 ('regex_set.txt', False), 124 ('singleslash.txt', False), 125 ('slashes_escaped.txt', False), 126 ('slashslash.txt', False), 127 ('spaces.txt', False), 128 ('word_boundaries.txt', False), 129 ], 130) 131def test_transcript(request, capsys, filename, feedback_to_output): 132 # Get location of the transcript 133 test_dir = os.path.dirname(request.module.__file__) 134 transcript_file = os.path.join(test_dir, 'transcripts', filename) 135 136 # Need to patch sys.argv so cmd2 doesn't think it was called with 137 # arguments equal to the py.test args 138 testargs = ['prog', '-t', transcript_file] 139 with mock.patch.object(sys, 'argv', testargs): 140 # Create a cmd2.Cmd() instance and make sure basic settings are 141 # like we want for test 142 app = CmdLineApp() 143 144 app.feedback_to_output = feedback_to_output 145 146 # Run the command loop 147 sys_exit_code = app.cmdloop() 148 assert sys_exit_code == 0 149 150 # Check for the unittest "OK" condition for the 1 test which ran 151 expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in" 152 expected_end = "s\n\nOK\n" 153 _, err = capsys.readouterr() 154 assert err.startswith(expected_start) 155 assert err.endswith(expected_end) 156 157 158def test_history_transcript(): 159 app = CmdLineApp() 160 app.stdout = StdSim(app.stdout) 161 run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') 162 run_cmd(app, 'speak /tmp/file.txt is not a regex') 163 164 expected = r"""(Cmd) orate this is 165> a /multiline/ 166> command; 167this is a \/multiline\/ command 168(Cmd) speak /tmp/file.txt is not a regex 169\/tmp\/file.txt is not a regex 170""" 171 172 # make a tmp file 173 fd, history_fname = tempfile.mkstemp(prefix='', suffix='.txt') 174 os.close(fd) 175 176 # tell the history command to create a transcript 177 run_cmd(app, 'history -t "{}"'.format(history_fname)) 178 179 # read in the transcript created by the history command 180 with open(history_fname) as f: 181 xscript = f.read() 182 183 assert xscript == expected 184 185 186def test_history_transcript_bad_path(mocker): 187 app = CmdLineApp() 188 app.stdout = StdSim(app.stdout) 189 run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') 190 run_cmd(app, 'speak /tmp/file.txt is not a regex') 191 192 # Bad directory 193 history_fname = '~/fakedir/this_does_not_exist.txt' 194 out, err = run_cmd(app, 'history -t "{}"'.format(history_fname)) 195 assert "is not a directory" in err[0] 196 197 # Cause os.open to fail and make sure error gets printed 198 mock_remove = mocker.patch('builtins.open') 199 mock_remove.side_effect = OSError 200 201 history_fname = 'outfile.txt' 202 out, err = run_cmd(app, 'history -t "{}"'.format(history_fname)) 203 assert "Error saving transcript file" in err[0] 204 205 206def test_run_script_record_transcript(base_app, request): 207 test_dir = os.path.dirname(request.module.__file__) 208 filename = os.path.join(test_dir, 'scripts', 'help.txt') 209 210 assert base_app._script_dir == [] 211 assert base_app._current_script_dir is None 212 213 # make a tmp file to use as a transcript 214 fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn') 215 os.close(fd) 216 217 # Execute the run_script command with the -t option to generate a transcript 218 run_cmd(base_app, 'run_script {} -t {}'.format(filename, transcript_fname)) 219 220 assert base_app._script_dir == [] 221 assert base_app._current_script_dir is None 222 223 # read in the transcript created by the history command 224 with open(transcript_fname) as f: 225 xscript = f.read() 226 227 assert xscript.startswith('(Cmd) help -v\n') 228 verify_help_text(base_app, xscript) 229 230 231def test_generate_transcript_stop(capsys): 232 # Verify transcript generation stops when a command returns True for stop 233 app = CmdLineApp() 234 235 # Make a tmp file to use as a transcript 236 fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn') 237 os.close(fd) 238 239 # This should run all commands 240 commands = ['help', 'set'] 241 app._generate_transcript(commands, transcript_fname) 242 _, err = capsys.readouterr() 243 assert err.startswith("2 commands") 244 245 # Since quit returns True for stop, only the first 2 commands will run 246 commands = ['help', 'quit', 'set'] 247 app._generate_transcript(commands, transcript_fname) 248 _, err = capsys.readouterr() 249 assert err.startswith("Command 2 triggered a stop") 250 251 # keyboard_interrupt command should stop the loop and not run the third command 252 commands = ['help', 'keyboard_interrupt', 'set'] 253 app._generate_transcript(commands, transcript_fname) 254 _, err = capsys.readouterr() 255 assert err.startswith("Interrupting this command\nCommand 2 triggered a stop") 256 257 258@pytest.mark.parametrize( 259 'expected, transformed', 260 [ 261 # strings with zero or one slash or with escaped slashes means no regular 262 # expression present, so the result should just be what re.escape returns. 263 # we don't use static strings in these tests because re.escape behaves 264 # differently in python 3.7 than in prior versions 265 ('text with no slashes', re.escape('text with no slashes')), 266 ('specials .*', re.escape('specials .*')), 267 ('use 2/3 cup', re.escape('use 2/3 cup')), 268 ('/tmp is nice', re.escape('/tmp is nice')), 269 ('slash at end/', re.escape('slash at end/')), 270 # escaped slashes 271 (r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/')), 272 # regexes 273 ('/.*/', '.*'), 274 ('specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+'), 275 (r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more')), 276 (r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /')), 277 # inception: slashes in our regex. backslashed on input, bare on output 278 (r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /')), 279 (r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff')), 280 ], 281) 282def test_parse_transcript_expected(expected, transformed): 283 app = CmdLineApp() 284 285 class TestMyAppCase(transcript.Cmd2TestCase): 286 cmdapp = app 287 288 testcase = TestMyAppCase() 289 assert testcase._transform_transcript_expected(expected) == transformed 290 291 292def test_transcript_failure(request, capsys): 293 # Get location of the transcript 294 test_dir = os.path.dirname(request.module.__file__) 295 transcript_file = os.path.join(test_dir, 'transcripts', 'failure.txt') 296 297 # Need to patch sys.argv so cmd2 doesn't think it was called with 298 # arguments equal to the py.test args 299 testargs = ['prog', '-t', transcript_file] 300 with mock.patch.object(sys, 'argv', testargs): 301 # Create a cmd2.Cmd() instance and make sure basic settings are 302 # like we want for test 303 app = CmdLineApp() 304 305 app.feedback_to_output = False 306 307 # Run the command loop 308 sys_exit_code = app.cmdloop() 309 assert sys_exit_code != 0 310 311 expected_start = "File " 312 expected_end = "s\n\nFAILED (failures=1)\n\n" 313 _, err = capsys.readouterr() 314 assert err.startswith(expected_start) 315 assert err.endswith(expected_end) 316 317 318def test_transcript_no_file(request, capsys): 319 # Need to patch sys.argv so cmd2 doesn't think it was called with 320 # arguments equal to the py.test args 321 testargs = ['prog', '-t'] 322 with mock.patch.object(sys, 'argv', testargs): 323 app = CmdLineApp() 324 325 app.feedback_to_output = False 326 327 # Run the command loop 328 sys_exit_code = app.cmdloop() 329 assert sys_exit_code != 0 330 331 # Check for the unittest "OK" condition for the 1 test which ran 332 expected = 'No test files found - nothing to test\n' 333 _, err = capsys.readouterr() 334 assert err == expected 335