##// END OF EJS Templates
Correctly teardown test case, avoid filehandle leaking.
Matthias Bussonnier -
Show More
@@ -1,432 +1,434 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 # Copyright (c) IPython Development Team.
18 18 # Distributed under the terms of the Modified BSD License.
19 19
20 20 from __future__ import print_function
21 21
22 22 import glob
23 23 from io import BytesIO
24 24 import os
25 25 import os.path as path
26 26 import sys
27 27 from threading import Thread, Lock, Event
28 28 import warnings
29 29
30 30 import nose.plugins.builtin
31 31 from nose.plugins.xunit import Xunit
32 32 from nose import SkipTest
33 33 from nose.core import TestProgram
34 34 from nose.plugins import Plugin
35 35 from nose.util import safe_str
36 36
37 37 from IPython import version_info
38 38 from IPython.utils.py3compat import bytes_to_str
39 39 from IPython.utils.importstring import import_item
40 40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
41 41 from IPython.external.decorators import KnownFailure, knownfailureif
42 42
43 43 pjoin = path.join
44 44
45 45
46 46 # Enable printing all warnings raise by IPython's modules
47 if sys.version_info > (3,0):
48 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
47 49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
48 50
49 51 if version_info < (6,):
50 52 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
51 53 # warning with the runner they also import from standard import library. (as of Dec 2015)
52 54 # Ignore, let's revisit that in a couple of years for IPython 6.
53 55 warnings.filterwarnings('ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
54 56
55 57
56 58 # ------------------------------------------------------------------------------
57 59 # Monkeypatch Xunit to count known failures as skipped.
58 60 # ------------------------------------------------------------------------------
59 61 def monkeypatch_xunit():
60 62 try:
61 63 knownfailureif(True)(lambda: None)()
62 64 except Exception as e:
63 65 KnownFailureTest = type(e)
64 66
65 67 def addError(self, test, err, capt=None):
66 68 if issubclass(err[0], KnownFailureTest):
67 69 err = (SkipTest,) + err[1:]
68 70 return self.orig_addError(test, err, capt)
69 71
70 72 Xunit.orig_addError = Xunit.addError
71 73 Xunit.addError = addError
72 74
73 75 #-----------------------------------------------------------------------------
74 76 # Check which dependencies are installed and greater than minimum version.
75 77 #-----------------------------------------------------------------------------
76 78 def extract_version(mod):
77 79 return mod.__version__
78 80
79 81 def test_for(item, min_version=None, callback=extract_version):
80 82 """Test to see if item is importable, and optionally check against a minimum
81 83 version.
82 84
83 85 If min_version is given, the default behavior is to check against the
84 86 `__version__` attribute of the item, but specifying `callback` allows you to
85 87 extract the value you are interested in. e.g::
86 88
87 89 In [1]: import sys
88 90
89 91 In [2]: from IPython.testing.iptest import test_for
90 92
91 93 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
92 94 Out[3]: True
93 95
94 96 """
95 97 try:
96 98 check = import_item(item)
97 99 except (ImportError, RuntimeError):
98 100 # GTK reports Runtime error if it can't be initialized even if it's
99 101 # importable.
100 102 return False
101 103 else:
102 104 if min_version:
103 105 if callback:
104 106 # extra processing step to get version to compare
105 107 check = callback(check)
106 108
107 109 return check >= min_version
108 110 else:
109 111 return True
110 112
111 113 # Global dict where we can store information on what we have and what we don't
112 114 # have available at test run time
113 115 have = {'matplotlib': test_for('matplotlib'),
114 116 'pygments': test_for('pygments'),
115 117 'sqlite3': test_for('sqlite3')}
116 118
117 119 #-----------------------------------------------------------------------------
118 120 # Test suite definitions
119 121 #-----------------------------------------------------------------------------
120 122
121 123 test_group_names = ['core',
122 124 'extensions', 'lib', 'terminal', 'testing', 'utils',
123 125 ]
124 126
125 127 class TestSection(object):
126 128 def __init__(self, name, includes):
127 129 self.name = name
128 130 self.includes = includes
129 131 self.excludes = []
130 132 self.dependencies = []
131 133 self.enabled = True
132 134
133 135 def exclude(self, module):
134 136 if not module.startswith('IPython'):
135 137 module = self.includes[0] + "." + module
136 138 self.excludes.append(module.replace('.', os.sep))
137 139
138 140 def requires(self, *packages):
139 141 self.dependencies.extend(packages)
140 142
141 143 @property
142 144 def will_run(self):
143 145 return self.enabled and all(have[p] for p in self.dependencies)
144 146
145 147 # Name -> (include, exclude, dependencies_met)
146 148 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
147 149
148 150
149 151 # Exclusions and dependencies
150 152 # ---------------------------
151 153
152 154 # core:
153 155 sec = test_sections['core']
154 156 if not have['sqlite3']:
155 157 sec.exclude('tests.test_history')
156 158 sec.exclude('history')
157 159 if not have['matplotlib']:
158 160 sec.exclude('pylabtools'),
159 161 sec.exclude('tests.test_pylabtools')
160 162
161 163 # lib:
162 164 sec = test_sections['lib']
163 165 sec.exclude('kernel')
164 166 if not have['pygments']:
165 167 sec.exclude('tests.test_lexers')
166 168 # We do this unconditionally, so that the test suite doesn't import
167 169 # gtk, changing the default encoding and masking some unicode bugs.
168 170 sec.exclude('inputhookgtk')
169 171 # We also do this unconditionally, because wx can interfere with Unix signals.
170 172 # There are currently no tests for it anyway.
171 173 sec.exclude('inputhookwx')
172 174 # Testing inputhook will need a lot of thought, to figure out
173 175 # how to have tests that don't lock up with the gui event
174 176 # loops in the picture
175 177 sec.exclude('inputhook')
176 178
177 179 # testing:
178 180 sec = test_sections['testing']
179 181 # These have to be skipped on win32 because they use echo, rm, cd, etc.
180 182 # See ticket https://github.com/ipython/ipython/issues/87
181 183 if sys.platform == 'win32':
182 184 sec.exclude('plugin.test_exampleip')
183 185 sec.exclude('plugin.dtexample')
184 186
185 187 # don't run jupyter_console tests found via shim
186 188 test_sections['terminal'].exclude('console')
187 189
188 190 # extensions:
189 191 sec = test_sections['extensions']
190 192 # This is deprecated in favour of rpy2
191 193 sec.exclude('rmagic')
192 194 # autoreload does some strange stuff, so move it to its own test section
193 195 sec.exclude('autoreload')
194 196 sec.exclude('tests.test_autoreload')
195 197 test_sections['autoreload'] = TestSection('autoreload',
196 198 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
197 199 test_group_names.append('autoreload')
198 200
199 201
200 202 #-----------------------------------------------------------------------------
201 203 # Functions and classes
202 204 #-----------------------------------------------------------------------------
203 205
204 206 def check_exclusions_exist():
205 207 from IPython.paths import get_ipython_package_dir
206 208 from warnings import warn
207 209 parent = os.path.dirname(get_ipython_package_dir())
208 210 for sec in test_sections:
209 211 for pattern in sec.exclusions:
210 212 fullpath = pjoin(parent, pattern)
211 213 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
212 214 warn("Excluding nonexistent file: %r" % pattern)
213 215
214 216
215 217 class ExclusionPlugin(Plugin):
216 218 """A nose plugin to effect our exclusions of files and directories.
217 219 """
218 220 name = 'exclusions'
219 221 score = 3000 # Should come before any other plugins
220 222
221 223 def __init__(self, exclude_patterns=None):
222 224 """
223 225 Parameters
224 226 ----------
225 227
226 228 exclude_patterns : sequence of strings, optional
227 229 Filenames containing these patterns (as raw strings, not as regular
228 230 expressions) are excluded from the tests.
229 231 """
230 232 self.exclude_patterns = exclude_patterns or []
231 233 super(ExclusionPlugin, self).__init__()
232 234
233 235 def options(self, parser, env=os.environ):
234 236 Plugin.options(self, parser, env)
235 237
236 238 def configure(self, options, config):
237 239 Plugin.configure(self, options, config)
238 240 # Override nose trying to disable plugin.
239 241 self.enabled = True
240 242
241 243 def wantFile(self, filename):
242 244 """Return whether the given filename should be scanned for tests.
243 245 """
244 246 if any(pat in filename for pat in self.exclude_patterns):
245 247 return False
246 248 return None
247 249
248 250 def wantDirectory(self, directory):
249 251 """Return whether the given directory should be scanned for tests.
250 252 """
251 253 if any(pat in directory for pat in self.exclude_patterns):
252 254 return False
253 255 return None
254 256
255 257
256 258 class StreamCapturer(Thread):
257 259 daemon = True # Don't hang if main thread crashes
258 260 started = False
259 261 def __init__(self, echo=False):
260 262 super(StreamCapturer, self).__init__()
261 263 self.echo = echo
262 264 self.streams = []
263 265 self.buffer = BytesIO()
264 266 self.readfd, self.writefd = os.pipe()
265 267 self.buffer_lock = Lock()
266 268 self.stop = Event()
267 269
268 270 def run(self):
269 271 self.started = True
270 272
271 273 while not self.stop.is_set():
272 274 chunk = os.read(self.readfd, 1024)
273 275
274 276 with self.buffer_lock:
275 277 self.buffer.write(chunk)
276 278 if self.echo:
277 279 sys.stdout.write(bytes_to_str(chunk))
278 280
279 281 os.close(self.readfd)
280 282 os.close(self.writefd)
281 283
282 284 def reset_buffer(self):
283 285 with self.buffer_lock:
284 286 self.buffer.truncate(0)
285 287 self.buffer.seek(0)
286 288
287 289 def get_buffer(self):
288 290 with self.buffer_lock:
289 291 return self.buffer.getvalue()
290 292
291 293 def ensure_started(self):
292 294 if not self.started:
293 295 self.start()
294 296
295 297 def halt(self):
296 298 """Safely stop the thread."""
297 299 if not self.started:
298 300 return
299 301
300 302 self.stop.set()
301 303 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
302 304 self.join()
303 305
304 306 class SubprocessStreamCapturePlugin(Plugin):
305 307 name='subprocstreams'
306 308 def __init__(self):
307 309 Plugin.__init__(self)
308 310 self.stream_capturer = StreamCapturer()
309 311 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
310 312 # This is ugly, but distant parts of the test machinery need to be able
311 313 # to redirect streams, so we make the object globally accessible.
312 314 nose.iptest_stdstreams_fileno = self.get_write_fileno
313 315
314 316 def get_write_fileno(self):
315 317 if self.destination == 'capture':
316 318 self.stream_capturer.ensure_started()
317 319 return self.stream_capturer.writefd
318 320 elif self.destination == 'discard':
319 321 return os.open(os.devnull, os.O_WRONLY)
320 322 else:
321 323 return sys.__stdout__.fileno()
322 324
323 325 def configure(self, options, config):
324 326 Plugin.configure(self, options, config)
325 327 # Override nose trying to disable plugin.
326 328 if self.destination == 'capture':
327 329 self.enabled = True
328 330
329 331 def startTest(self, test):
330 332 # Reset log capture
331 333 self.stream_capturer.reset_buffer()
332 334
333 335 def formatFailure(self, test, err):
334 336 # Show output
335 337 ec, ev, tb = err
336 338 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
337 339 if captured.strip():
338 340 ev = safe_str(ev)
339 341 out = [ev, '>> begin captured subprocess output <<',
340 342 captured,
341 343 '>> end captured subprocess output <<']
342 344 return ec, '\n'.join(out), tb
343 345
344 346 return err
345 347
346 348 formatError = formatFailure
347 349
348 350 def finalize(self, result):
349 351 self.stream_capturer.halt()
350 352
351 353
352 354 def run_iptest():
353 355 """Run the IPython test suite using nose.
354 356
355 357 This function is called when this script is **not** called with the form
356 358 `iptest all`. It simply calls nose with appropriate command line flags
357 359 and accepts all of the standard nose arguments.
358 360 """
359 361 # Apply our monkeypatch to Xunit
360 362 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
361 363 monkeypatch_xunit()
362 364
363 365 warnings.filterwarnings('ignore',
364 366 'This will be removed soon. Use IPython.testing.util instead')
365 367
366 368 arg1 = sys.argv[1]
367 369 if arg1 in test_sections:
368 370 section = test_sections[arg1]
369 371 sys.argv[1:2] = section.includes
370 372 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
371 373 section = test_sections[arg1[8:]]
372 374 sys.argv[1:2] = section.includes
373 375 else:
374 376 section = TestSection(arg1, includes=[arg1])
375 377
376 378
377 379 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
378 380 # We add --exe because of setuptools' imbecility (it
379 381 # blindly does chmod +x on ALL files). Nose does the
380 382 # right thing and it tries to avoid executables,
381 383 # setuptools unfortunately forces our hand here. This
382 384 # has been discussed on the distutils list and the
383 385 # setuptools devs refuse to fix this problem!
384 386 '--exe',
385 387 ]
386 388 if '-a' not in argv and '-A' not in argv:
387 389 argv = argv + ['-a', '!crash']
388 390
389 391 if nose.__version__ >= '0.11':
390 392 # I don't fully understand why we need this one, but depending on what
391 393 # directory the test suite is run from, if we don't give it, 0 tests
392 394 # get run. Specifically, if the test suite is run from the source dir
393 395 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
394 396 # even if the same call done in this directory works fine). It appears
395 397 # that if the requested package is in the current dir, nose bails early
396 398 # by default. Since it's otherwise harmless, leave it in by default
397 399 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
398 400 argv.append('--traverse-namespace')
399 401
400 402 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
401 403 SubprocessStreamCapturePlugin() ]
402 404
403 405 # we still have some vestigial doctests in core
404 406 if (section.name.startswith(('core', 'IPython.core'))):
405 407 plugins.append(IPythonDoctest())
406 408 argv.extend([
407 409 '--with-ipdoctest',
408 410 '--ipdoctest-tests',
409 411 '--ipdoctest-extension=txt',
410 412 ])
411 413
412 414
413 415 # Use working directory set by parent process (see iptestcontroller)
414 416 if 'IPTEST_WORKING_DIR' in os.environ:
415 417 os.chdir(os.environ['IPTEST_WORKING_DIR'])
416 418
417 419 # We need a global ipython running in this process, but the special
418 420 # in-process group spawns its own IPython kernels, so for *that* group we
419 421 # must avoid also opening the global one (otherwise there's a conflict of
420 422 # singletons). Ultimately the solution to this problem is to refactor our
421 423 # assumptions about what needs to be a singleton and what doesn't (app
422 424 # objects should, individual shells shouldn't). But for now, this
423 425 # workaround allows the test suite for the inprocess module to complete.
424 426 if 'kernel.inprocess' not in section.name:
425 427 from IPython.testing import globalipapp
426 428 globalipapp.start_ipython()
427 429
428 430 # Now nose can run
429 431 TestProgram(argv=argv, addplugins=plugins)
430 432
431 433 if __name__ == '__main__':
432 434 run_iptest()
@@ -1,134 +1,140 b''
1 1 # encoding: utf-8
2 2 """
3 3 Tests for testing.tools
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import with_statement
17 17 from __future__ import print_function
18 18
19 19 import os
20 20 import unittest
21 21
22 22 import nose.tools as nt
23 23
24 24 from IPython.testing import decorators as dec
25 25 from IPython.testing import tools as tt
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Tests
29 29 #-----------------------------------------------------------------------------
30 30
31 31 @dec.skip_win32
32 32 def test_full_path_posix():
33 33 spath = '/foo/bar.py'
34 34 result = tt.full_path(spath,['a.txt','b.txt'])
35 35 nt.assert_equal(result, ['/foo/a.txt', '/foo/b.txt'])
36 36 spath = '/foo'
37 37 result = tt.full_path(spath,['a.txt','b.txt'])
38 38 nt.assert_equal(result, ['/a.txt', '/b.txt'])
39 39 result = tt.full_path(spath,'a.txt')
40 40 nt.assert_equal(result, ['/a.txt'])
41 41
42 42
43 43 @dec.skip_if_not_win32
44 44 def test_full_path_win32():
45 45 spath = 'c:\\foo\\bar.py'
46 46 result = tt.full_path(spath,['a.txt','b.txt'])
47 47 nt.assert_equal(result, ['c:\\foo\\a.txt', 'c:\\foo\\b.txt'])
48 48 spath = 'c:\\foo'
49 49 result = tt.full_path(spath,['a.txt','b.txt'])
50 50 nt.assert_equal(result, ['c:\\a.txt', 'c:\\b.txt'])
51 51 result = tt.full_path(spath,'a.txt')
52 52 nt.assert_equal(result, ['c:\\a.txt'])
53 53
54 54
55 55 def test_parser():
56 56 err = ("FAILED (errors=1)", 1, 0)
57 57 fail = ("FAILED (failures=1)", 0, 1)
58 58 both = ("FAILED (errors=1, failures=1)", 1, 1)
59 59 for txt, nerr, nfail in [err, fail, both]:
60 60 nerr1, nfail1 = tt.parse_test_output(txt)
61 61 nt.assert_equal(nerr, nerr1)
62 62 nt.assert_equal(nfail, nfail1)
63 63
64 64
65 65 def test_temp_pyfile():
66 66 src = 'pass\n'
67 67 fname, fh = tt.temp_pyfile(src)
68 68 assert os.path.isfile(fname)
69 69 fh.close()
70 70 with open(fname) as fh2:
71 71 src2 = fh2.read()
72 72 nt.assert_equal(src2, src)
73 73
74 74 class TestAssertPrints(unittest.TestCase):
75 75 def test_passing(self):
76 76 with tt.AssertPrints("abc"):
77 77 print("abcd")
78 78 print("def")
79 79 print(b"ghi")
80 80
81 81 def test_failing(self):
82 82 def func():
83 83 with tt.AssertPrints("abc"):
84 84 print("acd")
85 85 print("def")
86 86 print(b"ghi")
87 87
88 88 self.assertRaises(AssertionError, func)
89 89
90 90
91 91 class Test_ipexec_validate(unittest.TestCase, tt.TempFileMixin):
92 92 def test_main_path(self):
93 93 """Test with only stdout results.
94 94 """
95 95 self.mktmp("print('A')\n"
96 96 "print('B')\n"
97 97 )
98 98 out = "A\nB"
99 99 tt.ipexec_validate(self.fname, out)
100 100
101 101 def test_main_path2(self):
102 102 """Test with only stdout results, expecting windows line endings.
103 103 """
104 104 self.mktmp("print('A')\n"
105 105 "print('B')\n"
106 106 )
107 107 out = "A\r\nB"
108 108 tt.ipexec_validate(self.fname, out)
109 109
110 110 def test_exception_path(self):
111 111 """Test exception path in exception_validate.
112 112 """
113 113 self.mktmp("from __future__ import print_function\n"
114 114 "import sys\n"
115 115 "print('A')\n"
116 116 "print('B')\n"
117 117 "print('C', file=sys.stderr)\n"
118 118 "print('D', file=sys.stderr)\n"
119 119 )
120 120 out = "A\nB"
121 121 tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\nD")
122 122
123 123 def test_exception_path2(self):
124 124 """Test exception path in exception_validate, expecting windows line endings.
125 125 """
126 126 self.mktmp("from __future__ import print_function\n"
127 127 "import sys\n"
128 128 "print('A')\n"
129 129 "print('B')\n"
130 130 "print('C', file=sys.stderr)\n"
131 131 "print('D', file=sys.stderr)\n"
132 132 )
133 133 out = "A\r\nB"
134 134 tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\r\nD")
135
136
137 def tearDown(self):
138 # tear down correctly the mixin,
139 # unittest.TestCase.tearDown does nothing
140 tt.TempFileMixin.tearDown(self)
General Comments 0
You need to be logged in to leave comments. Login now