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