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