##// END OF EJS Templates
Add failing test for SyntaxError display
Thomas Kluyver -
Show More
@@ -1,115 +1,140 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.core.ultratb
3 3 """
4 4 import io
5 5 import os.path
6 6 import unittest
7 7
8 8 from IPython.testing import tools as tt
9 9 from IPython.testing.decorators import onlyif_unicode_paths
10 10 from IPython.utils.syspathcontext import prepended_to_syspath
11 11 from IPython.utils.tempdir import TemporaryDirectory
12 12
13 13 ip = get_ipython()
14 14
15 15 file_1 = """1
16 16 2
17 17 3
18 18 def f():
19 19 1/0
20 20 """
21 21
22 22 file_2 = """def f():
23 23 1/0
24 24 """
25 25
26 26 class ChangedPyFileTest(unittest.TestCase):
27 27 def test_changing_py_file(self):
28 28 """Traceback produced if the line where the error occurred is missing?
29 29
30 30 https://github.com/ipython/ipython/issues/1456
31 31 """
32 32 with TemporaryDirectory() as td:
33 33 fname = os.path.join(td, "foo.py")
34 34 with open(fname, "w") as f:
35 35 f.write(file_1)
36 36
37 37 with prepended_to_syspath(td):
38 38 ip.run_cell("import foo")
39 39
40 40 with tt.AssertPrints("ZeroDivisionError"):
41 41 ip.run_cell("foo.f()")
42 42
43 43 # Make the file shorter, so the line of the error is missing.
44 44 with open(fname, "w") as f:
45 45 f.write(file_2)
46 46
47 47 # For some reason, this was failing on the *second* call after
48 48 # changing the file, so we call f() twice.
49 49 with tt.AssertNotPrints("Internal Python error", channel='stderr'):
50 50 with tt.AssertPrints("ZeroDivisionError"):
51 51 ip.run_cell("foo.f()")
52 52 with tt.AssertPrints("ZeroDivisionError"):
53 53 ip.run_cell("foo.f()")
54 54
55 55 iso_8859_5_file = u'''# coding: iso-8859-5
56 56
57 57 def fail():
58 58 """Π΄Π±Π˜Π–"""
59 59 1/0 # Π΄Π±Π˜Π–
60 60 '''
61 61
62 62 class NonAsciiTest(unittest.TestCase):
63 63 @onlyif_unicode_paths
64 64 def test_nonascii_path(self):
65 65 # Non-ascii directory name as well.
66 66 with TemporaryDirectory(suffix=u'Γ©') as td:
67 67 fname = os.path.join(td, u"fooΓ©.py")
68 68 with open(fname, "w") as f:
69 69 f.write(file_1)
70 70
71 71 with prepended_to_syspath(td):
72 72 ip.run_cell("import foo")
73 73
74 74 with tt.AssertPrints("ZeroDivisionError"):
75 75 ip.run_cell("foo.f()")
76 76
77 77 def test_iso8859_5(self):
78 78 with TemporaryDirectory() as td:
79 79 fname = os.path.join(td, 'dfghjkl.py')
80 80
81 81 with io.open(fname, 'w', encoding='iso-8859-5') as f:
82 82 f.write(iso_8859_5_file)
83 83
84 84 with prepended_to_syspath(td):
85 85 ip.run_cell("from dfghjkl import fail")
86 86
87 87 with tt.AssertPrints("ZeroDivisionError"):
88 88 with tt.AssertPrints(u'Π΄Π±Π˜Π–', suppress=False):
89 89 ip.run_cell('fail()')
90 90
91 91 indentationerror_file = """if True:
92 92 zoon()
93 93 """
94 94
95 95 class IndentationErrorTest(unittest.TestCase):
96 96 def test_indentationerror_shows_line(self):
97 97 # See issue gh-2398
98 98 with tt.AssertPrints("IndentationError"):
99 99 with tt.AssertPrints("zoon()", suppress=False):
100 100 ip.run_cell(indentationerror_file)
101 101
102 102 with TemporaryDirectory() as td:
103 103 fname = os.path.join(td, "foo.py")
104 104 with open(fname, "w") as f:
105 105 f.write(indentationerror_file)
106 106
107 107 with tt.AssertPrints("IndentationError"):
108 108 with tt.AssertPrints("zoon()", suppress=False):
109 109 ip.magic('run %s' % fname)
110 110
111 se_file_1 = """1
112 2
113 7/
114 """
115
116 se_file_2 = """7/
117 """
118
111 119 class SyntaxErrorTest(unittest.TestCase):
112 120 def test_syntaxerror_without_lineno(self):
113 121 with tt.AssertNotPrints("TypeError"):
114 122 with tt.AssertPrints("line unknown"):
115 123 ip.run_cell("raise SyntaxError()")
124
125 def test_changing_py_file(self):
126 with TemporaryDirectory() as td:
127 fname = os.path.join(td, "foo.py")
128 with open(fname, 'w') as f:
129 f.write(se_file_1)
130
131 with tt.AssertPrints(["7/", "SyntaxError"]):
132 ip.magic("run " + fname)
133
134 # Modify the file
135 with open(fname, 'w') as f:
136 f.write(se_file_2)
137
138 # The SyntaxError should point to the correct line
139 with tt.AssertPrints(["7/", "SyntaxError"]):
140 ip.magic("run " + fname)
@@ -1,435 +1,439 b''
1 1 """Generic testing tools.
2 2
3 3 Authors
4 4 -------
5 5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 6 """
7 7
8 8 from __future__ import absolute_import
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2009 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 import inspect
22 22 import os
23 23 import re
24 24 import sys
25 25 import tempfile
26 26
27 27 from contextlib import contextmanager
28 28 from io import StringIO
29 29 from subprocess import Popen, PIPE
30 30
31 31 try:
32 32 # These tools are used by parts of the runtime, so we make the nose
33 33 # dependency optional at this point. Nose is a hard dependency to run the
34 34 # test suite, but NOT to use ipython itself.
35 35 import nose.tools as nt
36 36 has_nose = True
37 37 except ImportError:
38 38 has_nose = False
39 39
40 40 from IPython.config.loader import Config
41 41 from IPython.utils.process import get_output_error_code
42 42 from IPython.utils.text import list_strings
43 43 from IPython.utils.io import temp_pyfile, Tee
44 44 from IPython.utils import py3compat
45 45 from IPython.utils.encoding import DEFAULT_ENCODING
46 46
47 47 from . import decorators as dec
48 48 from . import skipdoctest
49 49
50 50 #-----------------------------------------------------------------------------
51 51 # Functions and classes
52 52 #-----------------------------------------------------------------------------
53 53
54 54 # The docstring for full_path doctests differently on win32 (different path
55 55 # separator) so just skip the doctest there. The example remains informative.
56 56 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
57 57
58 58 @doctest_deco
59 59 def full_path(startPath,files):
60 60 """Make full paths for all the listed files, based on startPath.
61 61
62 62 Only the base part of startPath is kept, since this routine is typically
63 63 used with a script's __file__ variable as startPath. The base of startPath
64 64 is then prepended to all the listed files, forming the output list.
65 65
66 66 Parameters
67 67 ----------
68 68 startPath : string
69 69 Initial path to use as the base for the results. This path is split
70 70 using os.path.split() and only its first component is kept.
71 71
72 72 files : string or list
73 73 One or more files.
74 74
75 75 Examples
76 76 --------
77 77
78 78 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
79 79 ['/foo/a.txt', '/foo/b.txt']
80 80
81 81 >>> full_path('/foo',['a.txt','b.txt'])
82 82 ['/a.txt', '/b.txt']
83 83
84 84 If a single file is given, the output is still a list:
85 85 >>> full_path('/foo','a.txt')
86 86 ['/a.txt']
87 87 """
88 88
89 89 files = list_strings(files)
90 90 base = os.path.split(startPath)[0]
91 91 return [ os.path.join(base,f) for f in files ]
92 92
93 93
94 94 def parse_test_output(txt):
95 95 """Parse the output of a test run and return errors, failures.
96 96
97 97 Parameters
98 98 ----------
99 99 txt : str
100 100 Text output of a test run, assumed to contain a line of one of the
101 101 following forms::
102 102
103 103 'FAILED (errors=1)'
104 104 'FAILED (failures=1)'
105 105 'FAILED (errors=1, failures=1)'
106 106
107 107 Returns
108 108 -------
109 109 nerr, nfail: number of errors and failures.
110 110 """
111 111
112 112 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
113 113 if err_m:
114 114 nerr = int(err_m.group(1))
115 115 nfail = 0
116 116 return nerr, nfail
117 117
118 118 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
119 119 if fail_m:
120 120 nerr = 0
121 121 nfail = int(fail_m.group(1))
122 122 return nerr, nfail
123 123
124 124 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
125 125 re.MULTILINE)
126 126 if both_m:
127 127 nerr = int(both_m.group(1))
128 128 nfail = int(both_m.group(2))
129 129 return nerr, nfail
130 130
131 131 # If the input didn't match any of these forms, assume no error/failures
132 132 return 0, 0
133 133
134 134
135 135 # So nose doesn't think this is a test
136 136 parse_test_output.__test__ = False
137 137
138 138
139 139 def default_argv():
140 140 """Return a valid default argv for creating testing instances of ipython"""
141 141
142 142 return ['--quick', # so no config file is loaded
143 143 # Other defaults to minimize side effects on stdout
144 144 '--colors=NoColor', '--no-term-title','--no-banner',
145 145 '--autocall=0']
146 146
147 147
148 148 def default_config():
149 149 """Return a config object with good defaults for testing."""
150 150 config = Config()
151 151 config.TerminalInteractiveShell.colors = 'NoColor'
152 152 config.TerminalTerminalInteractiveShell.term_title = False,
153 153 config.TerminalInteractiveShell.autocall = 0
154 154 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
155 155 config.HistoryManager.db_cache_size = 10000
156 156 return config
157 157
158 158
159 159 def get_ipython_cmd(as_string=False):
160 160 """
161 161 Return appropriate IPython command line name. By default, this will return
162 162 a list that can be used with subprocess.Popen, for example, but passing
163 163 `as_string=True` allows for returning the IPython command as a string.
164 164
165 165 Parameters
166 166 ----------
167 167 as_string: bool
168 168 Flag to allow to return the command as a string.
169 169 """
170 170 ipython_cmd = [sys.executable, "-m", "IPython"]
171 171
172 172 if as_string:
173 173 ipython_cmd = " ".join(ipython_cmd)
174 174
175 175 return ipython_cmd
176 176
177 177 def ipexec(fname, options=None):
178 178 """Utility to call 'ipython filename'.
179 179
180 180 Starts IPython with a minimal and safe configuration to make startup as fast
181 181 as possible.
182 182
183 183 Note that this starts IPython in a subprocess!
184 184
185 185 Parameters
186 186 ----------
187 187 fname : str
188 188 Name of file to be executed (should have .py or .ipy extension).
189 189
190 190 options : optional, list
191 191 Extra command-line flags to be passed to IPython.
192 192
193 193 Returns
194 194 -------
195 195 (stdout, stderr) of ipython subprocess.
196 196 """
197 197 if options is None: options = []
198 198
199 199 # For these subprocess calls, eliminate all prompt printing so we only see
200 200 # output from script execution
201 201 prompt_opts = [ '--PromptManager.in_template=""',
202 202 '--PromptManager.in2_template=""',
203 203 '--PromptManager.out_template=""'
204 204 ]
205 205 cmdargs = default_argv() + prompt_opts + options
206 206
207 207 test_dir = os.path.dirname(__file__)
208 208
209 209 ipython_cmd = get_ipython_cmd()
210 210 # Absolute path for filename
211 211 full_fname = os.path.join(test_dir, fname)
212 212 full_cmd = ipython_cmd + cmdargs + [full_fname]
213 213 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE)
214 214 out, err = p.communicate()
215 215 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
216 216 # `import readline` causes 'ESC[?1034h' to be output sometimes,
217 217 # so strip that out before doing comparisons
218 218 if out:
219 219 out = re.sub(r'\x1b\[[^h]+h', '', out)
220 220 return out, err
221 221
222 222
223 223 def ipexec_validate(fname, expected_out, expected_err='',
224 224 options=None):
225 225 """Utility to call 'ipython filename' and validate output/error.
226 226
227 227 This function raises an AssertionError if the validation fails.
228 228
229 229 Note that this starts IPython in a subprocess!
230 230
231 231 Parameters
232 232 ----------
233 233 fname : str
234 234 Name of the file to be executed (should have .py or .ipy extension).
235 235
236 236 expected_out : str
237 237 Expected stdout of the process.
238 238
239 239 expected_err : optional, str
240 240 Expected stderr of the process.
241 241
242 242 options : optional, list
243 243 Extra command-line flags to be passed to IPython.
244 244
245 245 Returns
246 246 -------
247 247 None
248 248 """
249 249
250 250 import nose.tools as nt
251 251
252 252 out, err = ipexec(fname, options)
253 253 #print 'OUT', out # dbg
254 254 #print 'ERR', err # dbg
255 255 # If there are any errors, we must check those befor stdout, as they may be
256 256 # more informative than simply having an empty stdout.
257 257 if err:
258 258 if expected_err:
259 259 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
260 260 else:
261 261 raise ValueError('Running file %r produced error: %r' %
262 262 (fname, err))
263 263 # If no errors or output on stderr was expected, match stdout
264 264 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
265 265
266 266
267 267 class TempFileMixin(object):
268 268 """Utility class to create temporary Python/IPython files.
269 269
270 270 Meant as a mixin class for test cases."""
271 271
272 272 def mktmp(self, src, ext='.py'):
273 273 """Make a valid python temp file."""
274 274 fname, f = temp_pyfile(src, ext)
275 275 self.tmpfile = f
276 276 self.fname = fname
277 277
278 278 def tearDown(self):
279 279 if hasattr(self, 'tmpfile'):
280 280 # If the tmpfile wasn't made because of skipped tests, like in
281 281 # win32, there's nothing to cleanup.
282 282 self.tmpfile.close()
283 283 try:
284 284 os.unlink(self.fname)
285 285 except:
286 286 # On Windows, even though we close the file, we still can't
287 287 # delete it. I have no clue why
288 288 pass
289 289
290 290 pair_fail_msg = ("Testing {0}\n\n"
291 291 "In:\n"
292 292 " {1!r}\n"
293 293 "Expected:\n"
294 294 " {2!r}\n"
295 295 "Got:\n"
296 296 " {3!r}\n")
297 297 def check_pairs(func, pairs):
298 298 """Utility function for the common case of checking a function with a
299 299 sequence of input/output pairs.
300 300
301 301 Parameters
302 302 ----------
303 303 func : callable
304 304 The function to be tested. Should accept a single argument.
305 305 pairs : iterable
306 306 A list of (input, expected_output) tuples.
307 307
308 308 Returns
309 309 -------
310 310 None. Raises an AssertionError if any output does not match the expected
311 311 value.
312 312 """
313 313 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
314 314 for inp, expected in pairs:
315 315 out = func(inp)
316 316 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
317 317
318 318
319 319 if py3compat.PY3:
320 320 MyStringIO = StringIO
321 321 else:
322 322 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
323 323 # so we need a class that can handle both.
324 324 class MyStringIO(StringIO):
325 325 def write(self, s):
326 326 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
327 327 super(MyStringIO, self).write(s)
328 328
329 329 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
330 330 -------
331 331 {2!s}
332 332 -------
333 333 """
334 334
335 335 class AssertPrints(object):
336 336 """Context manager for testing that code prints certain text.
337 337
338 338 Examples
339 339 --------
340 340 >>> with AssertPrints("abc", suppress=False):
341 341 ... print "abcd"
342 342 ... print "def"
343 343 ...
344 344 abcd
345 345 def
346 346 """
347 347 def __init__(self, s, channel='stdout', suppress=True):
348 348 self.s = s
349 if isinstance(self.s, str):
350 self.s = [self.s]
349 351 self.channel = channel
350 352 self.suppress = suppress
351 353
352 354 def __enter__(self):
353 355 self.orig_stream = getattr(sys, self.channel)
354 356 self.buffer = MyStringIO()
355 357 self.tee = Tee(self.buffer, channel=self.channel)
356 358 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
357 359
358 360 def __exit__(self, etype, value, traceback):
359 361 self.tee.flush()
360 362 setattr(sys, self.channel, self.orig_stream)
361 363 printed = self.buffer.getvalue()
362 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
364 for s in self.s:
365 assert s in printed, notprinted_msg.format(s, self.channel, printed)
363 366 return False
364 367
365 368 printed_msg = """Found {0!r} in printed output (on {1}):
366 369 -------
367 370 {2!s}
368 371 -------
369 372 """
370 373
371 374 class AssertNotPrints(AssertPrints):
372 375 """Context manager for checking that certain output *isn't* produced.
373 376
374 377 Counterpart of AssertPrints"""
375 378 def __exit__(self, etype, value, traceback):
376 379 self.tee.flush()
377 380 setattr(sys, self.channel, self.orig_stream)
378 381 printed = self.buffer.getvalue()
379 assert self.s not in printed, printed_msg.format(self.s, self.channel, printed)
382 for s in self.s:
383 assert s not in printed, printed_msg.format(s, self.channel, printed)
380 384 return False
381 385
382 386 @contextmanager
383 387 def mute_warn():
384 388 from IPython.utils import warn
385 389 save_warn = warn.warn
386 390 warn.warn = lambda *a, **kw: None
387 391 try:
388 392 yield
389 393 finally:
390 394 warn.warn = save_warn
391 395
392 396 @contextmanager
393 397 def make_tempfile(name):
394 398 """ Create an empty, named, temporary file for the duration of the context.
395 399 """
396 400 f = open(name, 'w')
397 401 f.close()
398 402 try:
399 403 yield
400 404 finally:
401 405 os.unlink(name)
402 406
403 407
404 408 @contextmanager
405 409 def monkeypatch(obj, name, attr):
406 410 """
407 411 Context manager to replace attribute named `name` in `obj` with `attr`.
408 412 """
409 413 orig = getattr(obj, name)
410 414 setattr(obj, name, attr)
411 415 yield
412 416 setattr(obj, name, orig)
413 417
414 418
415 419 def help_output_test(subcommand=''):
416 420 """test that `ipython [subcommand] -h` works"""
417 421 cmd = ' '.join(get_ipython_cmd() + [subcommand, '-h'])
418 422 out, err, rc = get_output_error_code(cmd)
419 423 nt.assert_equal(rc, 0, err)
420 424 nt.assert_not_in("Traceback", err)
421 425 nt.assert_in("Options", out)
422 426 nt.assert_in("--help-all", out)
423 427 return out, err
424 428
425 429
426 430 def help_all_output_test(subcommand=''):
427 431 """test that `ipython [subcommand] --help-all` works"""
428 432 cmd = ' '.join(get_ipython_cmd() + [subcommand, '--help-all'])
429 433 out, err, rc = get_output_error_code(cmd)
430 434 nt.assert_equal(rc, 0, err)
431 435 nt.assert_not_in("Traceback", err)
432 436 nt.assert_in("Options", out)
433 437 nt.assert_in("Class parameters", out)
434 438 return out, err
435 439
General Comments 0
You need to be logged in to leave comments. Login now