|
22 | 22 |
|
23 | 23 | Git repository on GitHub at https://github.com/python-cmd2/cmd2 |
24 | 24 | """ |
| 25 | +# many imports are lazy-loaded when they are needed |
25 | 26 | import argparse |
26 | 27 | import atexit |
27 | 28 | import cmd |
|
43 | 44 | import tempfile |
44 | 45 | import traceback |
45 | 46 | from typing import Callable, List, Optional, Union, Tuple |
46 | | -import unittest |
47 | 47 | from code import InteractiveConsole |
48 | 48 |
|
49 | 49 | import pyperclip |
@@ -3174,6 +3174,8 @@ def run_transcript_tests(self, callargs): |
3174 | 3174 |
|
3175 | 3175 | :param callargs: List[str] - list of transcript test file names |
3176 | 3176 | """ |
| 3177 | + import unittest |
| 3178 | + from .transcript import Cmd2TestCase |
3177 | 3179 | class TestMyAppCase(Cmd2TestCase): |
3178 | 3180 | cmdapp = self |
3179 | 3181 |
|
@@ -3416,216 +3418,6 @@ def restore(self): |
3416 | 3418 | setattr(self.obj, attrib, getattr(self, attrib)) |
3417 | 3419 |
|
3418 | 3420 |
|
3419 | | -class OutputTrap(object): |
3420 | | - """Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing.""" |
3421 | | - |
3422 | | - def __init__(self): |
3423 | | - self.contents = '' |
3424 | | - |
3425 | | - def write(self, txt): |
3426 | | - """Add text to the internal contents. |
3427 | | -
|
3428 | | - :param txt: str |
3429 | | - """ |
3430 | | - self.contents += txt |
3431 | | - |
3432 | | - def read(self): |
3433 | | - """Read from the internal contents and then clear them out. |
3434 | | -
|
3435 | | - :return: str - text from the internal contents |
3436 | | - """ |
3437 | | - result = self.contents |
3438 | | - self.contents = '' |
3439 | | - return result |
3440 | | - |
3441 | | - |
3442 | | -class Cmd2TestCase(unittest.TestCase): |
3443 | | - """Subclass this, setting CmdApp, to make a unittest.TestCase class |
3444 | | - that will execute the commands in a transcript file and expect the results shown. |
3445 | | - See example.py""" |
3446 | | - cmdapp = None |
3447 | | - |
3448 | | - def fetchTranscripts(self): |
3449 | | - self.transcripts = {} |
3450 | | - for fileset in self.cmdapp.testfiles: |
3451 | | - for fname in glob.glob(fileset): |
3452 | | - tfile = open(fname) |
3453 | | - self.transcripts[fname] = iter(tfile.readlines()) |
3454 | | - tfile.close() |
3455 | | - if not len(self.transcripts): |
3456 | | - raise Exception("No test files found - nothing to test.") |
3457 | | - |
3458 | | - def setUp(self): |
3459 | | - if self.cmdapp: |
3460 | | - self.fetchTranscripts() |
3461 | | - |
3462 | | - # Trap stdout |
3463 | | - self._orig_stdout = self.cmdapp.stdout |
3464 | | - self.cmdapp.stdout = OutputTrap() |
3465 | | - |
3466 | | - def runTest(self): # was testall |
3467 | | - if self.cmdapp: |
3468 | | - its = sorted(self.transcripts.items()) |
3469 | | - for (fname, transcript) in its: |
3470 | | - self._test_transcript(fname, transcript) |
3471 | | - |
3472 | | - def _test_transcript(self, fname, transcript): |
3473 | | - line_num = 0 |
3474 | | - finished = False |
3475 | | - line = utils.strip_ansi(next(transcript)) |
3476 | | - line_num += 1 |
3477 | | - while not finished: |
3478 | | - # Scroll forward to where actual commands begin |
3479 | | - while not line.startswith(self.cmdapp.visible_prompt): |
3480 | | - try: |
3481 | | - line = utils.strip_ansi(next(transcript)) |
3482 | | - except StopIteration: |
3483 | | - finished = True |
3484 | | - break |
3485 | | - line_num += 1 |
3486 | | - command = [line[len(self.cmdapp.visible_prompt):]] |
3487 | | - line = next(transcript) |
3488 | | - # Read the entirety of a multi-line command |
3489 | | - while line.startswith(self.cmdapp.continuation_prompt): |
3490 | | - command.append(line[len(self.cmdapp.continuation_prompt):]) |
3491 | | - try: |
3492 | | - line = next(transcript) |
3493 | | - except StopIteration: |
3494 | | - raise (StopIteration, |
3495 | | - 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, |
3496 | | - command[0]) |
3497 | | - ) |
3498 | | - line_num += 1 |
3499 | | - command = ''.join(command) |
3500 | | - # Send the command into the application and capture the resulting output |
3501 | | - # TODO: Should we get the return value and act if stop == True? |
3502 | | - self.cmdapp.onecmd_plus_hooks(command) |
3503 | | - result = self.cmdapp.stdout.read() |
3504 | | - # Read the expected result from transcript |
3505 | | - if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt): |
3506 | | - message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format( |
3507 | | - fname, line_num, command, result) |
3508 | | - self.assert_(not (result.strip()), message) |
3509 | | - continue |
3510 | | - expected = [] |
3511 | | - while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt): |
3512 | | - expected.append(line) |
3513 | | - try: |
3514 | | - line = next(transcript) |
3515 | | - except StopIteration: |
3516 | | - finished = True |
3517 | | - break |
3518 | | - line_num += 1 |
3519 | | - expected = ''.join(expected) |
3520 | | - |
3521 | | - # transform the expected text into a valid regular expression |
3522 | | - expected = self._transform_transcript_expected(expected) |
3523 | | - message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format( |
3524 | | - fname, line_num, command, expected, result) |
3525 | | - self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message) |
3526 | | - |
3527 | | - def _transform_transcript_expected(self, s): |
3528 | | - """parse the string with slashed regexes into a valid regex |
3529 | | -
|
3530 | | - Given a string like: |
3531 | | -
|
3532 | | - Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/ |
3533 | | -
|
3534 | | - Turn it into a valid regular expression which matches the literal text |
3535 | | - of the string and the regular expression. We have to remove the slashes |
3536 | | - because they differentiate between plain text and a regular expression. |
3537 | | - Unless the slashes are escaped, in which case they are interpreted as |
3538 | | - plain text, or there is only one slash, which is treated as plain text |
3539 | | - also. |
3540 | | -
|
3541 | | - Check the tests in tests/test_transcript.py to see all the edge |
3542 | | - cases. |
3543 | | - """ |
3544 | | - regex = '' |
3545 | | - start = 0 |
3546 | | - |
3547 | | - while True: |
3548 | | - (regex, first_slash_pos, start) = self._escaped_find(regex, s, start, False) |
3549 | | - if first_slash_pos == -1: |
3550 | | - # no more slashes, add the rest of the string and bail |
3551 | | - regex += re.escape(s[start:]) |
3552 | | - break |
3553 | | - else: |
3554 | | - # there is a slash, add everything we have found so far |
3555 | | - # add stuff before the first slash as plain text |
3556 | | - regex += re.escape(s[start:first_slash_pos]) |
3557 | | - start = first_slash_pos+1 |
3558 | | - # and go find the next one |
3559 | | - (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True) |
3560 | | - if second_slash_pos > 0: |
3561 | | - # add everything between the slashes (but not the slashes) |
3562 | | - # as a regular expression |
3563 | | - regex += s[start:second_slash_pos] |
3564 | | - # and change where we start looking for slashed on the |
3565 | | - # turn through the loop |
3566 | | - start = second_slash_pos + 1 |
3567 | | - else: |
3568 | | - # No closing slash, we have to add the first slash, |
3569 | | - # and the rest of the text |
3570 | | - regex += re.escape(s[start-1:]) |
3571 | | - break |
3572 | | - return regex |
3573 | | - |
3574 | | - @staticmethod |
3575 | | - def _escaped_find(regex, s, start, in_regex): |
3576 | | - """ |
3577 | | - Find the next slash in {s} after {start} that is not preceded by a backslash. |
3578 | | -
|
3579 | | - If we find an escaped slash, add everything up to and including it to regex, |
3580 | | - updating {start}. {start} therefore serves two purposes, tells us where to start |
3581 | | - looking for the next thing, and also tells us where in {s} we have already |
3582 | | - added things to {regex} |
3583 | | -
|
3584 | | - {in_regex} specifies whether we are currently searching in a regex, we behave |
3585 | | - differently if we are or if we aren't. |
3586 | | - """ |
3587 | | - |
3588 | | - while True: |
3589 | | - pos = s.find('/', start) |
3590 | | - if pos == -1: |
3591 | | - # no match, return to caller |
3592 | | - break |
3593 | | - elif pos == 0: |
3594 | | - # slash at the beginning of the string, so it can't be |
3595 | | - # escaped. We found it. |
3596 | | - break |
3597 | | - else: |
3598 | | - # check if the slash is preceeded by a backslash |
3599 | | - if s[pos-1:pos] == '\\': |
3600 | | - # it is. |
3601 | | - if in_regex: |
3602 | | - # add everything up to the backslash as a |
3603 | | - # regular expression |
3604 | | - regex += s[start:pos-1] |
3605 | | - # skip the backslash, and add the slash |
3606 | | - regex += s[pos] |
3607 | | - else: |
3608 | | - # add everything up to the backslash as escaped |
3609 | | - # plain text |
3610 | | - regex += re.escape(s[start:pos-1]) |
3611 | | - # and then add the slash as escaped |
3612 | | - # plain text |
3613 | | - regex += re.escape(s[pos]) |
3614 | | - # update start to show we have handled everything |
3615 | | - # before it |
3616 | | - start = pos+1 |
3617 | | - # and continue to look |
3618 | | - else: |
3619 | | - # slash is not escaped, this is what we are looking for |
3620 | | - break |
3621 | | - return regex, pos, start |
3622 | | - |
3623 | | - def tearDown(self): |
3624 | | - if self.cmdapp: |
3625 | | - # Restore stdout |
3626 | | - self.cmdapp.stdout = self._orig_stdout |
3627 | | - |
3628 | | - |
3629 | 3421 | def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')): |
3630 | 3422 | """Wrapper around namedtuple which lets you treat the last value as optional. |
3631 | 3423 |
|
|
0 commit comments