From 0fc727852247677304968d3b7f3b550c44b0af00 2013-11-07 00:03:55 From: Thomas Kluyver Date: 2013-11-07 00:03:55 Subject: [PATCH] Mechanism for testing terminal interact loop in-process. While this is complex, I much prefer it to starting a separate process and piping commands in. --- diff --git a/IPython/terminal/tests/test_interactivshell.py b/IPython/terminal/tests/test_interactivshell.py index 6ab4acb..9437aff 100644 --- a/IPython/terminal/tests/test_interactivshell.py +++ b/IPython/terminal/tests/test_interactivshell.py @@ -17,12 +17,68 @@ Authors #----------------------------------------------------------------------------- # stdlib import sys +import types import unittest +from IPython.core.inputtransformer import InputTransformer from IPython.testing.decorators import skipif from IPython.utils import py3compat from IPython.testing import tools as tt +# Decorator for interaction loop tests ----------------------------------------- + +class mock_input_helper(object): + """Machinery for tests of the main interact loop. + + Used by the mock_input decorator. + """ + def __init__(self, testgen): + self.testgen = testgen + self.exception = None + self.ip = get_ipython() + + def __enter__(self): + self.orig_raw_input = self.ip.raw_input + self.ip.raw_input = self.fake_input + return self + + def __exit__(self, etype, value, tb): + self.ip.raw_input = self.orig_raw_input + + def fake_input(self, prompt): + try: + return next(self.testgen) + except StopIteration: + self.ip.exit_now = True + return u'' + except: + self.exception = sys.exc_info() + self.ip.exit_now = True + return u'' + +def mock_input(testfunc): + """Decorator for tests of the main interact loop. + + Write the test as a generator, yield-ing the input strings, which IPython + will see as if they were typed in at the prompt. + """ + def test_method(self): + testgen = testfunc(self) + with mock_input_helper(testgen) as mih: + mih.ip.interact(display_banner=False) + + if mih.exception is not None: + # Re-raise captured exception + etype, value, tb = mih.exception + import traceback + traceback.print_tb(tb, file=sys.stdout) + del tb # Avoid reference loop + raise value + + return test_method + +# Test classes ----------------------------------------------------------------- + class InteractiveShellTestCase(unittest.TestCase): def rl_hist_entries(self, rl, n): """Get last n readline history entries as a list""" @@ -171,6 +227,42 @@ class InteractiveShellTestCase(unittest.TestCase): expected = [ py3compat.unicode_to_str(e, enc) for e in expected ] self.assertEqual(hist, expected) + @mock_input + def test_inputtransformer_syntaxerror(self): + ip = get_ipython() + transformer = SyntaxErrorTransformer() + ip.input_splitter.python_line_transforms.append(transformer) + ip.input_transformer_manager.python_line_transforms.append(transformer) + + try: + #raise Exception + with tt.AssertPrints('4', suppress=False): + yield u'print(2*2)' + + with tt.AssertPrints('SyntaxError: input contains', suppress=False): + yield u'print(2345) # syntaxerror' + + with tt.AssertPrints('16', suppress=False): + yield u'print(4*4)' + + finally: + ip.input_splitter.python_line_transforms.remove(transformer) + ip.input_transformer_manager.python_line_transforms.remove(transformer) + + +class SyntaxErrorTransformer(InputTransformer): + def push(self, line): + pos = line.find('syntaxerror') + if pos >= 0: + e = SyntaxError('input contains "syntaxerror"') + e.text = line + e.offset = pos + 1 + raise e + return line + + def reset(self): + pass + class TerminalMagicsTestCase(unittest.TestCase): def test_paste_magics_message(self): """Test that an IndentationError while using paste magics doesn't diff --git a/IPython/terminal/tests/test_terminal.py b/IPython/terminal/tests/test_terminal.py deleted file mode 100644 index 20c5eef..0000000 --- a/IPython/terminal/tests/test_terminal.py +++ /dev/null @@ -1,58 +0,0 @@ -# coding: utf-8 -"""Tests for the IPython terminal""" - -import os -import tempfile -import shutil - -import nose.tools as nt - -from IPython.testing.tools import make_tempfile, ipexec - - -TEST_SYNTAX_ERROR_CMDS = """ -from IPython.core.inputtransformer import InputTransformer - -%cpaste -class SyntaxErrorTransformer(InputTransformer): - - def push(self, line): - pos = line.find('syntaxerror') - if pos >= 0: - e = SyntaxError('input contains "syntaxerror"') - e.text = line - e.offset = pos + 1 - raise e - return line - - def reset(self): - pass --- - -ip = get_ipython() -transformer = SyntaxErrorTransformer() -ip.input_splitter.python_line_transforms.append(transformer) -ip.input_transformer_manager.python_line_transforms.append(transformer) - -# now the actual commands -1234 -2345 # syntaxerror <- triggered here -3456 -""" - -def test_syntax_error(): - """Check that the IPython terminal does not abort if a SyntaxError is raised in an InputTransformer""" - try: - tmp = tempfile.mkdtemp() - filename = os.path.join(tmp, 'test_syntax_error.py') - with open(filename, 'w') as f: - f.write(TEST_SYNTAX_ERROR_CMDS) - out, err = ipexec(filename, pipe=True) - nt.assert_equal(err, '') - nt.assert_in('1234', out) - nt.assert_in(' 2345 # syntaxerror <- triggered here', out) - nt.assert_in(' ^', out) - nt.assert_in('SyntaxError: input contains "syntaxerror"', out) - nt.assert_in('3456', out) - finally: - shutil.rmtree(tmp) diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py index aa63b5f..d17ce42 100644 --- a/IPython/testing/tools.py +++ b/IPython/testing/tools.py @@ -173,7 +173,7 @@ def get_ipython_cmd(as_string=False): return ipython_cmd -def ipexec(fname, options=None, pipe=False): +def ipexec(fname, options=None): """Utility to call 'ipython filename'. Starts IPython with a minimal and safe configuration to make startup as fast @@ -189,9 +189,6 @@ def ipexec(fname, options=None, pipe=False): options : optional, list Extra command-line flags to be passed to IPython. - pipe : optional, boolean - Pipe fname into IPython as stdin instead of calling it as external file - Returns ------- (stdout, stderr) of ipython subprocess. @@ -211,12 +208,8 @@ def ipexec(fname, options=None, pipe=False): ipython_cmd = get_ipython_cmd() # Absolute path for filename full_fname = os.path.join(test_dir, fname) - if pipe: - full_cmd = ipython_cmd + cmdargs - p = Popen(full_cmd, stdin=open(full_fname), stdout=PIPE, stderr=PIPE) - else: - full_cmd = ipython_cmd + cmdargs + [full_fname] - p = Popen(full_cmd, stdout=PIPE, stderr=PIPE) + full_cmd = ipython_cmd + cmdargs + [full_fname] + p = Popen(full_cmd, stdout=PIPE, stderr=PIPE) out, err = p.communicate() out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err) # `import readline` causes 'ESC[?1034h' to be output sometimes, diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 46296be..2166ef7 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -42,6 +42,11 @@ class IOStream: for meth in filter(clone, dir(stream)): setattr(self, meth, getattr(stream, meth)) + def __repr__(self): + cls = self.__class__ + tpl = '{mod}.{cls}({args})' + return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream) + def write(self,data): try: self._swrite(data)