##// END OF EJS Templates
BUG: Explicitly close Tee in AssertPrints and AssertNotPrints...
Scott Sanderson -
Show More
@@ -1,458 +1,467 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 216 env = os.environ.copy()
217 217 env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
218 218 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, env=env)
219 219 out, err = p.communicate()
220 220 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
221 221 # `import readline` causes 'ESC[?1034h' to be output sometimes,
222 222 # so strip that out before doing comparisons
223 223 if out:
224 224 out = re.sub(r'\x1b\[[^h]+h', '', out)
225 225 return out, err
226 226
227 227
228 228 def ipexec_validate(fname, expected_out, expected_err='',
229 229 options=None):
230 230 """Utility to call 'ipython filename' and validate output/error.
231 231
232 232 This function raises an AssertionError if the validation fails.
233 233
234 234 Note that this starts IPython in a subprocess!
235 235
236 236 Parameters
237 237 ----------
238 238 fname : str
239 239 Name of the file to be executed (should have .py or .ipy extension).
240 240
241 241 expected_out : str
242 242 Expected stdout of the process.
243 243
244 244 expected_err : optional, str
245 245 Expected stderr of the process.
246 246
247 247 options : optional, list
248 248 Extra command-line flags to be passed to IPython.
249 249
250 250 Returns
251 251 -------
252 252 None
253 253 """
254 254
255 255 import nose.tools as nt
256 256
257 257 out, err = ipexec(fname, options)
258 258 #print 'OUT', out # dbg
259 259 #print 'ERR', err # dbg
260 260 # If there are any errors, we must check those befor stdout, as they may be
261 261 # more informative than simply having an empty stdout.
262 262 if err:
263 263 if expected_err:
264 264 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
265 265 else:
266 266 raise ValueError('Running file %r produced error: %r' %
267 267 (fname, err))
268 268 # If no errors or output on stderr was expected, match stdout
269 269 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
270 270
271 271
272 272 class TempFileMixin(object):
273 273 """Utility class to create temporary Python/IPython files.
274 274
275 275 Meant as a mixin class for test cases."""
276 276
277 277 def mktmp(self, src, ext='.py'):
278 278 """Make a valid python temp file."""
279 279 fname, f = temp_pyfile(src, ext)
280 280 self.tmpfile = f
281 281 self.fname = fname
282 282
283 283 def tearDown(self):
284 284 if hasattr(self, 'tmpfile'):
285 285 # If the tmpfile wasn't made because of skipped tests, like in
286 286 # win32, there's nothing to cleanup.
287 287 self.tmpfile.close()
288 288 try:
289 289 os.unlink(self.fname)
290 290 except:
291 291 # On Windows, even though we close the file, we still can't
292 292 # delete it. I have no clue why
293 293 pass
294 294
295 295 pair_fail_msg = ("Testing {0}\n\n"
296 296 "In:\n"
297 297 " {1!r}\n"
298 298 "Expected:\n"
299 299 " {2!r}\n"
300 300 "Got:\n"
301 301 " {3!r}\n")
302 302 def check_pairs(func, pairs):
303 303 """Utility function for the common case of checking a function with a
304 304 sequence of input/output pairs.
305 305
306 306 Parameters
307 307 ----------
308 308 func : callable
309 309 The function to be tested. Should accept a single argument.
310 310 pairs : iterable
311 311 A list of (input, expected_output) tuples.
312 312
313 313 Returns
314 314 -------
315 315 None. Raises an AssertionError if any output does not match the expected
316 316 value.
317 317 """
318 318 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
319 319 for inp, expected in pairs:
320 320 out = func(inp)
321 321 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
322 322
323 323
324 324 if py3compat.PY3:
325 325 MyStringIO = StringIO
326 326 else:
327 327 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
328 328 # so we need a class that can handle both.
329 329 class MyStringIO(StringIO):
330 330 def write(self, s):
331 331 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
332 332 super(MyStringIO, self).write(s)
333 333
334 334 _re_type = type(re.compile(r''))
335 335
336 336 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
337 337 -------
338 338 {2!s}
339 339 -------
340 340 """
341 341
342 342 class AssertPrints(object):
343 343 """Context manager for testing that code prints certain text.
344 344
345 345 Examples
346 346 --------
347 347 >>> with AssertPrints("abc", suppress=False):
348 348 ... print("abcd")
349 349 ... print("def")
350 350 ...
351 351 abcd
352 352 def
353 353 """
354 354 def __init__(self, s, channel='stdout', suppress=True):
355 355 self.s = s
356 356 if isinstance(self.s, (py3compat.string_types, _re_type)):
357 357 self.s = [self.s]
358 358 self.channel = channel
359 359 self.suppress = suppress
360 360
361 361 def __enter__(self):
362 362 self.orig_stream = getattr(sys, self.channel)
363 363 self.buffer = MyStringIO()
364 364 self.tee = Tee(self.buffer, channel=self.channel)
365 365 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
366 366
367 367 def __exit__(self, etype, value, traceback):
368 try:
368 369 if value is not None:
369 370 # If an error was raised, don't check anything else
370 371 return False
371 372 self.tee.flush()
372 373 setattr(sys, self.channel, self.orig_stream)
373 374 printed = self.buffer.getvalue()
374 375 for s in self.s:
375 376 if isinstance(s, _re_type):
376 377 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
377 378 else:
378 379 assert s in printed, notprinted_msg.format(s, self.channel, printed)
379 380 return False
381 finally:
382 self.tee.close()
380 383
381 384 printed_msg = """Found {0!r} in printed output (on {1}):
382 385 -------
383 386 {2!s}
384 387 -------
385 388 """
386 389
387 390 class AssertNotPrints(AssertPrints):
388 391 """Context manager for checking that certain output *isn't* produced.
389 392
390 393 Counterpart of AssertPrints"""
391 394 def __exit__(self, etype, value, traceback):
395 try:
392 396 if value is not None:
393 397 # If an error was raised, don't check anything else
398 self.tee.close()
394 399 return False
395 400 self.tee.flush()
396 401 setattr(sys, self.channel, self.orig_stream)
397 402 printed = self.buffer.getvalue()
398 403 for s in self.s:
399 404 if isinstance(s, _re_type):
400 assert not s.search(printed), printed_msg.format(s.pattern, self.channel, printed)
405 assert not s.search(printed),printed_msg.format(
406 s.pattern, self.channel, printed)
401 407 else:
402 assert s not in printed, printed_msg.format(s, self.channel, printed)
408 assert s not in printed, printed_msg.format(
409 s, self.channel, printed)
403 410 return False
411 finally:
412 self.tee.close()
404 413
405 414 @contextmanager
406 415 def mute_warn():
407 416 from IPython.utils import warn
408 417 save_warn = warn.warn
409 418 warn.warn = lambda *a, **kw: None
410 419 try:
411 420 yield
412 421 finally:
413 422 warn.warn = save_warn
414 423
415 424 @contextmanager
416 425 def make_tempfile(name):
417 426 """ Create an empty, named, temporary file for the duration of the context.
418 427 """
419 428 f = open(name, 'w')
420 429 f.close()
421 430 try:
422 431 yield
423 432 finally:
424 433 os.unlink(name)
425 434
426 435
427 436 @contextmanager
428 437 def monkeypatch(obj, name, attr):
429 438 """
430 439 Context manager to replace attribute named `name` in `obj` with `attr`.
431 440 """
432 441 orig = getattr(obj, name)
433 442 setattr(obj, name, attr)
434 443 yield
435 444 setattr(obj, name, orig)
436 445
437 446
438 447 def help_output_test(subcommand=''):
439 448 """test that `ipython [subcommand] -h` works"""
440 449 cmd = get_ipython_cmd() + [subcommand, '-h']
441 450 out, err, rc = get_output_error_code(cmd)
442 451 nt.assert_equal(rc, 0, err)
443 452 nt.assert_not_in("Traceback", err)
444 453 nt.assert_in("Options", out)
445 454 nt.assert_in("--help-all", out)
446 455 return out, err
447 456
448 457
449 458 def help_all_output_test(subcommand=''):
450 459 """test that `ipython [subcommand] --help-all` works"""
451 460 cmd = get_ipython_cmd() + [subcommand, '--help-all']
452 461 out, err, rc = get_output_error_code(cmd)
453 462 nt.assert_equal(rc, 0, err)
454 463 nt.assert_not_in("Traceback", err)
455 464 nt.assert_in("Options", out)
456 465 nt.assert_in("Class parameters", out)
457 466 return out, err
458 467
General Comments 0
You need to be logged in to leave comments. Login now