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