import time import logging import pexpect from pexpect.exceptions import ExceptionPexpect, TIMEOUT logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) class IpaTestExpect(pexpect.spawn): """A wrapper class around pexpect.spawn for easier usage in automated tests Please see pexpect documentation at https://pexpect.readthedocs.io/en/stable/api/index.html for general usage instructions. Note that usage of "+", "*" and '?' at the end of regular expressions arguments to .expect() is meaningless. This wrapper adds ability to use the class as a context manager, which will take care of verifying process return status and terminating the process if it did not do it normally. The context manager is the recommended way of using the class in tests. Basic usage example: ``` with IpaTestExpect('some_command') as e: e.expect_exact('yes or no?') e.sendline('yes') ``` At exit from context manager the following checks are performed by default: 1. there is nothing in output since last call to .expect() 2. the process has terminated 3. return code is 0 If any check fails, an exceptio is raised. If you want to override checks 1 and 3 you can call .expect_exit() explicitly: ``` with IpaTestExpect('some_command') as e: ... e.expect_exit(ok_returncode=1, ignore_remaining_output=True) ``` All .expect* methods are strict, meaning that if they do not find the pattern in the output during given amount of time, the exception is raised. So they can directly be used to verify output for presence of specific strings. Another addition is .get_last_output() method which can be used get process output from penultimate up to the last call to .expect(). The result can be used for more complex checks which can not be expressed as simple regexes, for example we can check for absence of string in output: ``` with IpaTestExpect('some_command') as e: ... e.expect('All done') output = e.get_last_output() assert 'WARNING' not in output ``` """ def __init__(self, argv, default_timeout=10, encoding='utf-8'): if isinstance(argv, str): command = argv args = [] else: command = argv[0] args = argv[1:] logger.debug('Expect will spawn command "%s" with args %s', command, args) super().__init__( command, args, timeout=default_timeout, encoding=encoding, echo=False ) def expect_exit(self, timeout=-1, ok_returncode=0, raiseonerr=True, ignore_remaining_output=False): if timeout == -1: timeout = self.timeout wait_to_exit_until = time.time() + timeout if not self.eof(): self.expect(pexpect.EOF, timeout) errors = [] if not ignore_remaining_output and self.before.strip(): errors.append('Unexpected output at program exit: {!r}' .format(self.before)) while time.time() < wait_to_exit_until: if not self.isalive(): break time.sleep(0.1) else: errors.append('Program did not exit after waiting for {} seconds' .format(self.timeout)) if (not self.isalive() and raiseonerr and self.exitstatus != ok_returncode): errors.append('Program exited with unexpected status {}' .format(self.exitstatus)) self.exit_checked = True if errors: raise ExceptionPexpect( 'Program exited with an unexpected state:\n' + '\n'.join(errors)) def send(self, s): """Wrapper to provide logging input string""" logger.debug('Sending %r', s) return super().send(s) def expect_list(self, pattern_list, *args, **kwargs): """Wrapper to provide logging output string and expected patterns""" try: result = super().expect_list(pattern_list, *args, **kwargs) finally: self._log_output(pattern_list) return result def expect_exact(self, pattern_list, *args, **kwargs): """Wrapper to provide logging output string and expected patterns""" try: result = super().expect_exact(pattern_list, *args, **kwargs) finally: self._log_output(pattern_list) return result def get_last_output(self): """Return output consumed by last call to .expect*()""" output = self.before if isinstance(self.after, str): output += self.after return output def _log_output(self, expected): logger.debug('Output received: %r, expected: "%s", ', self.get_last_output(), expected) def __enter__(self): self.exit_checked = False return self def __exit__(self, exc_type, exc_val, exc_tb): exception_occurred = bool(exc_type) try: if not self.exit_checked: self.expect_exit(raiseonerr=not exception_occurred, ignore_remaining_output=exception_occurred) except TIMEOUT: if not exception_occurred: raise finally: if self.isalive(): logger.error('Command still active, terminating.') self.terminate(True)