# -*- coding: utf-8 -*- """ Common test functionality """ from __future__ import print_function import os import sys import codecs import time import select import pytest from ptyprocess import PtyProcess import regex from prompt_toolkit.eventloop.posix import PosixEventLoop from prompt_toolkit.input import PipeInput from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.output import DummyOutput from PyInquirer import style_from_dict, Token from PyInquirer import prompts # http://code.activestate.com/recipes/52308-the-simple-but-handy-collector-of-a-bunch-of-named/?in=user-97991 class Bunch: def __init__(self, **kwds): self.__dict__.update(kwds) # use Bunch to create group of variables: # cf = Bunch(datum=y, squared=y*y, coord=x) keys = Bunch( DOWN='\x1b[B', UP='\x1b[A', LEFT='\x1b[D', RIGHT='\x1b[C', ENTER='\x0a', # ControlJ (Identical to '\n') ESCAPE='\x1b', CONTROLC='\x03', BACK='\x7f') style = style_from_dict({ Token.QuestionMark: '#FF9D00 bold', Token.Selected: '#5F819D bold', Token.Instruction: '', # default Token.Answer: '#5F819D bold', Token.Question: '', }) def feed_app_with_input(type, message, text, **kwargs): """ Create a CommandLineInterface, feed it with the given user input and return the CLI object. This returns a (result, CLI) tuple. note: this only works if you import your prompt and then this function!! """ # If the given text doesn't end with a newline, the interface won't finish. assert text.endswith('\n') application = getattr(prompts, type).question(message, **kwargs) loop = PosixEventLoop() try: inp = PipeInput() inp.send_text(text) cli = CommandLineInterface( application=application, eventloop=loop, input=inp, output=DummyOutput()) result = cli.run() return result, cli finally: loop.close() inp.close() def remove_ansi_escape_sequences(text): # http://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python # remove all ansi escape sequences #return regex.sub(r'(\x9b|\x1b\[)[0-?]*[ -\/]*[@-~]|[ ]*\r', '', text) text = regex.sub(r'(\x9b|\x1b\[)[0-?]*[ -\/]*[@-~]', '', text) text = regex.sub(r'[ \r]*\n', '\n', text) # also clean up the line endings return text # helper for running sut as subprocess within pty # does two things # * test app running in pty in subprocess # * get test coverage from subprocess # docu: # http://blog.fizyk.net.pl/blog/gathering-tests-coverage-for-subprocesses-in-python.html PY3 = sys.version_info[0] >= 3 if PY3: basestring = str class SimplePty(PtyProcess): """Simple wrapper around a process running in a pseudoterminal. This class exposes a similar interface to :class:`PtyProcess`, but its read methods return unicode, and its :meth:`write` accepts unicode. """ def __init__(self, pid, fd, encoding='utf-8', codec_errors='strict'): super(SimplePty, self).__init__(pid, fd) self.encoding = encoding self.codec_errors = codec_errors self.decoder = codecs.getincrementaldecoder(encoding)(errors=codec_errors) def read(self, size=1024): """Read at most ``size`` bytes from the pty, return them as unicode. Can block if there is nothing to read. Raises :exc:`EOFError` if the terminal was closed. The size argument still refers to bytes, not unicode code points. """ b = super(SimplePty, self).read(size) if not b: return '' if self.skip_cr: b = b.replace(b'\r', b'') #if self.skip_ansi: # b = remove_ansi_escape_sequences(b) return self.decoder.decode(b, final=False) def readline(self): """Read one line from the pseudoterminal, and return it as unicode. Can block if there is nothing to read. Raises :exc:`EOFError` if the terminal was closed. note: this is a specialized version that does not have \r\n at the end """ # TODO implement a timeout b = super(SimplePty, self).readline().strip() s = self.decoder.decode(b, final=False) if self.skip_ansi: s = remove_ansi_escape_sequences(s) return s def write(self, s): """Write the unicode string ``s`` to the pseudoterminal. This intends to make tests a little less verbose. Returns the number of bytes written. """ if isinstance(s, basestring): b = s.encode(self.encoding) count = super(SimplePty, self).write(b) return count def writeline(self, s): """Syntactic sugar to add a '\n' at the end of the . Returns the number of bytes written. """ if not s.endswith('\n'): s += '\n' return self.write(s) @classmethod def spawn( cls, argv, cwd=None, env=None, echo=False, preexec_fn=None, dimensions=(24, 80), skip_cr=True, skip_ansi=True, timeout=1.0): """ :param argv: :param cwd: :param env: :param echo: default is False so we do not have to deal with the echo :param preexec_fn: :param dimensions: :param skip_cr: skip carriage return '/r' characters when comparing equality :param skip_ansi: skip ansi escape sequences when comparing equality :param timeout: read timeout in seconds :return: subprocess handle """ if env is None: env = os.environ inst = super(SimplePty, cls).spawn(argv, cwd, env, echo, preexec_fn, dimensions) inst.skip_cr = skip_cr inst.skip_ansi = skip_ansi inst.timeout = timeout # in seconds return inst def expect(self, text): """Read until equals text or timeout.""" # inspired by pexpect/pty_spawn and pexpect/expect.py expect_loop end_time = time.time() + self.timeout buf = '' while (end_time - time.time()) > 0.0: # switch to nonblocking read reads, _, _ = select.select([self.fd], [], [], end_time - time.time()) if len(reads) > 0: try: buf = remove_ansi_escape_sequences(buf + self.read()) except EOFError: print('len: %d' % len(buf)) assert buf == text if buf == text: return elif len(buf) >= len(text): break else: # do not eat up CPU when waiting for the timeout to expire time.sleep(self.timeout/10) #print(repr(buf)) # debug ansi code handling assert buf == text def expect_regex(self, pattern): """Read until matches pattern or timeout.""" # inspired by pexpect/pty_spawn and pexpect/expect.py expect_loop end_time = time.time() + self.timeout buf = '' prog = regex.compile(pattern) while (end_time - time.time()) > 0.0: # switch to nonblocking read reads, _, _ = select.select([self.fd], [], [], end_time - time.time()) if len(reads) > 0: try: buf = remove_ansi_escape_sequences(buf + self.read()) except EOFError: assert prog.match(buf) is not None, \ 'output was:\n%s\nexpect regex pattern:\n%s' % (buf, pattern) if prog.match(buf): return True else: # do not eat up CPU when waiting for the timeout to expire time.sleep(self.timeout/10) assert prog.match(buf) is not None, \ 'output was:\n%s\nexpect regex pattern:\n%s' % (buf, pattern) def create_example_fixture(example): """Create a pytest fixture to run the example in pty subprocess & cleanup. :param example: relative path like 'examples/input.py' :return: pytest fixture """ @pytest.fixture def example_app(): p = SimplePty.spawn(['python', example]) yield p # it takes some time to collect the coverage data # if the main process exits too early the coverage data is not available time.sleep(p.delayafterterminate) try: p.sendintr() # in case the subprocess was not ended by the test except OSError as e: if e.errno != 5: raise p.wait() # without wait() the coverage info never arrives return example_app