##// END OF EJS Templates
Allow drilling down to individual tests using iptest command
Thomas Kluyver -
Show More
@@ -1,425 +1,430 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 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2009-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27 from __future__ import print_function
28 28
29 29 # Stdlib
30 30 import glob
31 31 import os
32 32 import os.path as path
33 33 import re
34 34 import sys
35 35 import warnings
36 36
37 37 # Now, proceed to import nose itself
38 38 import nose.plugins.builtin
39 39 from nose.plugins.xunit import Xunit
40 40 from nose import SkipTest
41 41 from nose.core import TestProgram
42 42 from nose.plugins import Plugin
43 43
44 44 # Our own imports
45 45 from IPython.utils.importstring import import_item
46 46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
47 47 from IPython.external.decorators import KnownFailure, knownfailureif
48 48
49 49 pjoin = path.join
50 50
51 51
52 52 #-----------------------------------------------------------------------------
53 53 # Globals
54 54 #-----------------------------------------------------------------------------
55 55
56 56
57 57 #-----------------------------------------------------------------------------
58 58 # Warnings control
59 59 #-----------------------------------------------------------------------------
60 60
61 61 # Twisted generates annoying warnings with Python 2.6, as will do other code
62 62 # that imports 'sets' as of today
63 63 warnings.filterwarnings('ignore', 'the sets module is deprecated',
64 64 DeprecationWarning )
65 65
66 66 # This one also comes from Twisted
67 67 warnings.filterwarnings('ignore', 'the sha module is deprecated',
68 68 DeprecationWarning)
69 69
70 70 # Wx on Fedora11 spits these out
71 71 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
72 72 UserWarning)
73 73
74 74 # ------------------------------------------------------------------------------
75 75 # Monkeypatch Xunit to count known failures as skipped.
76 76 # ------------------------------------------------------------------------------
77 77 def monkeypatch_xunit():
78 78 try:
79 79 knownfailureif(True)(lambda: None)()
80 80 except Exception as e:
81 81 KnownFailureTest = type(e)
82 82
83 83 def addError(self, test, err, capt=None):
84 84 if issubclass(err[0], KnownFailureTest):
85 85 err = (SkipTest,) + err[1:]
86 86 return self.orig_addError(test, err, capt)
87 87
88 88 Xunit.orig_addError = Xunit.addError
89 89 Xunit.addError = addError
90 90
91 91 #-----------------------------------------------------------------------------
92 92 # Check which dependencies are installed and greater than minimum version.
93 93 #-----------------------------------------------------------------------------
94 94 def extract_version(mod):
95 95 return mod.__version__
96 96
97 97 def test_for(item, min_version=None, callback=extract_version):
98 98 """Test to see if item is importable, and optionally check against a minimum
99 99 version.
100 100
101 101 If min_version is given, the default behavior is to check against the
102 102 `__version__` attribute of the item, but specifying `callback` allows you to
103 103 extract the value you are interested in. e.g::
104 104
105 105 In [1]: import sys
106 106
107 107 In [2]: from IPython.testing.iptest import test_for
108 108
109 109 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
110 110 Out[3]: True
111 111
112 112 """
113 113 try:
114 114 check = import_item(item)
115 115 except (ImportError, RuntimeError):
116 116 # GTK reports Runtime error if it can't be initialized even if it's
117 117 # importable.
118 118 return False
119 119 else:
120 120 if min_version:
121 121 if callback:
122 122 # extra processing step to get version to compare
123 123 check = callback(check)
124 124
125 125 return check >= min_version
126 126 else:
127 127 return True
128 128
129 129 # Global dict where we can store information on what we have and what we don't
130 130 # have available at test run time
131 131 have = {}
132 132
133 133 have['curses'] = test_for('_curses')
134 134 have['matplotlib'] = test_for('matplotlib')
135 135 have['numpy'] = test_for('numpy')
136 136 have['pexpect'] = test_for('IPython.external.pexpect')
137 137 have['pymongo'] = test_for('pymongo')
138 138 have['pygments'] = test_for('pygments')
139 139 have['qt'] = test_for('IPython.external.qt')
140 140 have['rpy2'] = test_for('rpy2')
141 141 have['sqlite3'] = test_for('sqlite3')
142 142 have['cython'] = test_for('Cython')
143 143 have['oct2py'] = test_for('oct2py')
144 144 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
145 145 have['jinja2'] = test_for('jinja2')
146 146 have['wx'] = test_for('wx')
147 147 have['wx.aui'] = test_for('wx.aui')
148 148 have['azure'] = test_for('azure')
149 149 have['sphinx'] = test_for('sphinx')
150 150
151 151 min_zmq = (2,1,11)
152 152
153 153 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
154 154
155 155 #-----------------------------------------------------------------------------
156 156 # Test suite definitions
157 157 #-----------------------------------------------------------------------------
158 158
159 159 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
160 160 'extensions', 'lib', 'terminal', 'testing', 'utils',
161 161 'nbformat', 'qt', 'html', 'nbconvert'
162 162 ]
163 163
164 164 class TestSection(object):
165 165 def __init__(self, name, includes):
166 166 self.name = name
167 167 self.includes = includes
168 168 self.excludes = []
169 169 self.dependencies = []
170 170 self.enabled = True
171 171
172 172 def exclude(self, module):
173 173 if not module.startswith('IPython'):
174 174 module = self.includes[0] + "." + module
175 175 self.excludes.append(module.replace('.', os.sep))
176 176
177 177 def requires(self, *packages):
178 178 self.dependencies.extend(packages)
179 179
180 180 @property
181 181 def will_run(self):
182 182 return self.enabled and all(have[p] for p in self.dependencies)
183 183
184 184 # Name -> (include, exclude, dependencies_met)
185 185 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
186 186
187 187 # Exclusions and dependencies
188 188 # ---------------------------
189 189
190 190 # core:
191 191 sec = test_sections['core']
192 192 if not have['sqlite3']:
193 193 sec.exclude('tests.test_history')
194 194 sec.exclude('history')
195 195 if not have['matplotlib']:
196 196 sec.exclude('pylabtools'),
197 197 sec.exclude('tests.test_pylabtools')
198 198
199 199 # lib:
200 200 sec = test_sections['lib']
201 201 if not have['wx']:
202 202 sec.exclude('inputhookwx')
203 203 if not have['pexpect']:
204 204 sec.exclude('irunner')
205 205 sec.exclude('tests.test_irunner')
206 206 if not have['zmq']:
207 207 sec.exclude('kernel')
208 208 # We do this unconditionally, so that the test suite doesn't import
209 209 # gtk, changing the default encoding and masking some unicode bugs.
210 210 sec.exclude('inputhookgtk')
211 211 # Testing inputhook will need a lot of thought, to figure out
212 212 # how to have tests that don't lock up with the gui event
213 213 # loops in the picture
214 214 sec.exclude('inputhook')
215 215
216 216 # testing:
217 217 sec = test_sections['lib']
218 218 # This guy is probably attic material
219 219 sec.exclude('mkdoctests')
220 220 # These have to be skipped on win32 because the use echo, rm, cd, etc.
221 221 # See ticket https://github.com/ipython/ipython/issues/87
222 222 if sys.platform == 'win32':
223 223 sec.exclude('plugin.test_exampleip')
224 224 sec.exclude('plugin.dtexample')
225 225
226 226 # terminal:
227 227 if (not have['pexpect']) or (not have['zmq']):
228 228 test_sections['terminal'].exclude('console')
229 229
230 230 # parallel
231 231 sec = test_sections['parallel']
232 232 sec.requires('zmq')
233 233 if not have['pymongo']:
234 234 sec.exclude('controller.mongodb')
235 235 sec.exclude('tests.test_mongodb')
236 236
237 237 # kernel:
238 238 sec = test_sections['kernel']
239 239 sec.requires('zmq')
240 240 # The in-process kernel tests are done in a separate section
241 241 sec.exclude('inprocess')
242 242 # importing gtk sets the default encoding, which we want to avoid
243 243 sec.exclude('zmq.gui.gtkembed')
244 244 if not have['matplotlib']:
245 245 sec.exclude('zmq.pylab')
246 246
247 247 # kernel.inprocess:
248 248 test_sections['kernel.inprocess'].requires('zmq')
249 249
250 250 # extensions:
251 251 sec = test_sections['extensions']
252 252 if not have['cython']:
253 253 sec.exclude('cythonmagic')
254 254 sec.exclude('tests.test_cythonmagic')
255 255 if not have['oct2py']:
256 256 sec.exclude('octavemagic')
257 257 sec.exclude('tests.test_octavemagic')
258 258 if not have['rpy2'] or not have['numpy']:
259 259 sec.exclude('rmagic')
260 260 sec.exclude('tests.test_rmagic')
261 261 # autoreload does some strange stuff, so move it to its own test section
262 262 sec.exclude('autoreload')
263 263 sec.exclude('tests.test_autoreload')
264 264 test_sections['autoreload'] = TestSection('autoreload',
265 265 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
266 266 test_group_names.append('autoreload')
267 267
268 268 # qt:
269 269 test_sections['qt'].requires('zmq', 'qt', 'pygments')
270 270
271 271 # html:
272 272 sec = test_sections['html']
273 273 sec.requires('zmq', 'tornado')
274 274 # The notebook 'static' directory contains JS, css and other
275 275 # files for web serving. Occasionally projects may put a .py
276 276 # file in there (MathJax ships a conf.py), so we might as
277 277 # well play it safe and skip the whole thing.
278 278 sec.exclude('static')
279 279 sec.exclude('fabfile')
280 280 if not have['jinja2']:
281 281 sec.exclude('notebookapp')
282 282 if not have['azure']:
283 283 sec.exclude('services.notebooks.azurenbmanager')
284 284
285 285 # config:
286 286 # Config files aren't really importable stand-alone
287 287 test_sections['config'].exclude('profile')
288 288
289 289 # nbconvert:
290 290 sec = test_sections['nbconvert']
291 291 sec.requires('pygments', 'jinja2', 'sphinx')
292 292 # Exclude nbconvert directories containing config files used to test.
293 293 # Executing the config files with iptest would cause an exception.
294 294 sec.exclude('tests.files')
295 295 sec.exclude('exporters.tests.files')
296 296
297 297 #-----------------------------------------------------------------------------
298 298 # Functions and classes
299 299 #-----------------------------------------------------------------------------
300 300
301 301 def check_exclusions_exist():
302 302 from IPython.utils.path import get_ipython_package_dir
303 303 from IPython.utils.warn import warn
304 304 parent = os.path.dirname(get_ipython_package_dir())
305 305 for sec in test_sections:
306 306 for pattern in sec.exclusions:
307 307 fullpath = pjoin(parent, pattern)
308 308 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
309 309 warn("Excluding nonexistent file: %r" % pattern)
310 310
311 311
312 312 class ExclusionPlugin(Plugin):
313 313 """A nose plugin to effect our exclusions of files and directories.
314 314 """
315 315 name = 'exclusions'
316 316 score = 3000 # Should come before any other plugins
317 317
318 318 def __init__(self, exclude_patterns=None):
319 319 """
320 320 Parameters
321 321 ----------
322 322
323 323 exclude_patterns : sequence of strings, optional
324 324 These patterns are compiled as regular expressions, subsequently used
325 325 to exclude any filename which matches them from inclusion in the test
326 326 suite (using pattern.search(), NOT pattern.match() ).
327 327 """
328 328
329 329 if exclude_patterns is None:
330 330 exclude_patterns = []
331 331 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
332 332 super(ExclusionPlugin, self).__init__()
333 333
334 334 def options(self, parser, env=os.environ):
335 335 Plugin.options(self, parser, env)
336 336
337 337 def configure(self, options, config):
338 338 Plugin.configure(self, options, config)
339 339 # Override nose trying to disable plugin.
340 340 self.enabled = True
341 341
342 342 def wantFile(self, filename):
343 343 """Return whether the given filename should be scanned for tests.
344 344 """
345 345 if any(pat.search(filename) for pat in self.exclude_patterns):
346 346 return False
347 347 return None
348 348
349 349 def wantDirectory(self, directory):
350 350 """Return whether the given directory should be scanned for tests.
351 351 """
352 352 if any(pat.search(directory) for pat in self.exclude_patterns):
353 353 return False
354 354 return None
355 355
356 356
357 357 def run_iptest():
358 358 """Run the IPython test suite using nose.
359 359
360 360 This function is called when this script is **not** called with the form
361 361 `iptest all`. It simply calls nose with appropriate command line flags
362 362 and accepts all of the standard nose arguments.
363 363 """
364 364 # Apply our monkeypatch to Xunit
365 365 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
366 366 monkeypatch_xunit()
367 367
368 368 warnings.filterwarnings('ignore',
369 369 'This will be removed soon. Use IPython.testing.util instead')
370 370
371 section = test_sections[sys.argv[1]]
372 sys.argv[1:2] = section.includes
371 if sys.argv[1] in test_sections:
372 section = test_sections[sys.argv[1]]
373 sys.argv[1:2] = section.includes
374 else:
375 arg1 = sys.argv[1]
376 section = TestSection(arg1, includes=[arg1])
377
373 378
374 379 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
375 380
376 381 '--with-ipdoctest',
377 382 '--ipdoctest-tests','--ipdoctest-extension=txt',
378 383
379 384 # We add --exe because of setuptools' imbecility (it
380 385 # blindly does chmod +x on ALL files). Nose does the
381 386 # right thing and it tries to avoid executables,
382 387 # setuptools unfortunately forces our hand here. This
383 388 # has been discussed on the distutils list and the
384 389 # setuptools devs refuse to fix this problem!
385 390 '--exe',
386 391 ]
387 392 if '-a' not in argv and '-A' not in argv:
388 393 argv = argv + ['-a', '!crash']
389 394
390 395 if nose.__version__ >= '0.11':
391 396 # I don't fully understand why we need this one, but depending on what
392 397 # directory the test suite is run from, if we don't give it, 0 tests
393 398 # get run. Specifically, if the test suite is run from the source dir
394 399 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
395 400 # even if the same call done in this directory works fine). It appears
396 401 # that if the requested package is in the current dir, nose bails early
397 402 # by default. Since it's otherwise harmless, leave it in by default
398 403 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
399 404 argv.append('--traverse-namespace')
400 405
401 406 # use our plugin for doctesting. It will remove the standard doctest plugin
402 407 # if it finds it enabled
403 408 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
404 409
405 410 # Use working directory set by parent process (see iptestcontroller)
406 411 if 'IPTEST_WORKING_DIR' in os.environ:
407 412 os.chdir(os.environ['IPTEST_WORKING_DIR'])
408 413
409 414 # We need a global ipython running in this process, but the special
410 415 # in-process group spawns its own IPython kernels, so for *that* group we
411 416 # must avoid also opening the global one (otherwise there's a conflict of
412 417 # singletons). Ultimately the solution to this problem is to refactor our
413 418 # assumptions about what needs to be a singleton and what doesn't (app
414 419 # objects should, individual shells shouldn't). But for now, this
415 420 # workaround allows the test suite for the inprocess module to complete.
416 if section.name != 'kernel.inprocess':
421 if 'kernel.inprocess' not in section.name:
417 422 from IPython.testing import globalipapp
418 423 globalipapp.start_ipython()
419 424
420 425 # Now nose can run
421 426 TestProgram(argv=argv, addplugins=plugins)
422 427
423 428 if __name__ == '__main__':
424 429 run_iptest()
425 430
@@ -1,382 +1,383 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2009-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19 from __future__ import print_function
20 20
21 21 import argparse
22 22 import multiprocessing.pool
23 23 import os
24 24 import shutil
25 25 import signal
26 26 import sys
27 27 import subprocess
28 28 import time
29 29
30 30 from .iptest import have, test_group_names, test_sections
31 31 from IPython.utils.py3compat import bytes_to_str
32 32 from IPython.utils.sysinfo import sys_info
33 33 from IPython.utils.tempdir import TemporaryDirectory
34 34
35 35
36 36 class TestController(object):
37 37 """Run tests in a subprocess
38 38 """
39 39 #: str, IPython test suite to be executed.
40 40 section = None
41 41 #: list, command line arguments to be executed
42 42 cmd = None
43 43 #: dict, extra environment variables to set for the subprocess
44 44 env = None
45 45 #: list, TemporaryDirectory instances to clear up when the process finishes
46 46 dirs = None
47 47 #: subprocess.Popen instance
48 48 process = None
49 49 #: str, process stdout+stderr
50 50 stdout = None
51 51 #: bool, whether to capture process stdout & stderr
52 52 buffer_output = False
53 53
54 54 def __init__(self):
55 55 self.cmd = []
56 56 self.env = {}
57 57 self.dirs = []
58 58
59 59 @property
60 60 def will_run(self):
61 61 """Override in subclasses to check for dependencies."""
62 62 return False
63 63
64 64 def launch(self):
65 65 # print('*** ENV:', self.env) # dbg
66 66 # print('*** CMD:', self.cmd) # dbg
67 67 env = os.environ.copy()
68 68 env.update(self.env)
69 69 output = subprocess.PIPE if self.buffer_output else None
70 70 stdout = subprocess.STDOUT if self.buffer_output else None
71 71 self.process = subprocess.Popen(self.cmd, stdout=output,
72 72 stderr=stdout, env=env)
73 73
74 74 def wait(self):
75 75 self.stdout, _ = self.process.communicate()
76 76 return self.process.returncode
77 77
78 78 def cleanup_process(self):
79 79 """Cleanup on exit by killing any leftover processes."""
80 80 subp = self.process
81 81 if subp is None or (subp.poll() is not None):
82 82 return # Process doesn't exist, or is already dead.
83 83
84 84 try:
85 85 print('Cleaning up stale PID: %d' % subp.pid)
86 86 subp.kill()
87 87 except: # (OSError, WindowsError) ?
88 88 # This is just a best effort, if we fail or the process was
89 89 # really gone, ignore it.
90 90 pass
91 91 else:
92 92 for i in range(10):
93 93 if subp.poll() is None:
94 94 time.sleep(0.1)
95 95 else:
96 96 break
97 97
98 98 if subp.poll() is None:
99 99 # The process did not die...
100 100 print('... failed. Manual cleanup may be required.')
101 101
102 102 def cleanup(self):
103 103 "Kill process if it's still alive, and clean up temporary directories"
104 104 self.cleanup_process()
105 105 for td in self.dirs:
106 106 td.cleanup()
107 107
108 108 __del__ = cleanup
109 109
110 110 class PyTestController(TestController):
111 111 """Run Python tests using IPython.testing.iptest"""
112 112 #: str, Python command to execute in subprocess
113 113 pycmd = None
114 114
115 115 def __init__(self, section):
116 116 """Create new test runner."""
117 117 TestController.__init__(self)
118 118 self.section = section
119 119 # pycmd is put into cmd[2] in PyTestController.launch()
120 120 self.cmd = [sys.executable, '-c', None, section]
121 121 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
122 122 ipydir = TemporaryDirectory()
123 123 self.dirs.append(ipydir)
124 124 self.env['IPYTHONDIR'] = ipydir.name
125 125 self.workingdir = workingdir = TemporaryDirectory()
126 126 self.dirs.append(workingdir)
127 127 self.env['IPTEST_WORKING_DIR'] = workingdir.name
128 128 # This means we won't get odd effects from our own matplotlib config
129 129 self.env['MPLCONFIGDIR'] = workingdir.name
130 130
131 131 @property
132 132 def will_run(self):
133 133 return test_sections[self.section].will_run
134 134
135 135 def add_xunit(self):
136 136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138 138
139 139 def add_coverage(self):
140 140 coverage_rc = ("[run]\n"
141 141 "data_file = {data_file}\n"
142 142 "source =\n"
143 143 " {source}\n"
144 144 ).format(data_file=os.path.abspath('.coverage.'+self.section),
145 145 source="\n ".join(test_sections[self.section].includes))
146 146
147 147 config_file = os.path.join(self.workingdir.name, '.coveragerc')
148 148 with open(config_file, 'w') as f:
149 149 f.write(coverage_rc)
150 150
151 151 self.env['COVERAGE_PROCESS_START'] = config_file
152 152 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
153 153
154 154 def launch(self):
155 155 self.cmd[2] = self.pycmd
156 156 super(PyTestController, self).launch()
157 157
158 158
159 159 def prepare_py_test_controllers(inc_slow=False, xunit=False, coverage=False):
160 160 """Returns an ordered list of PyTestController instances to be run."""
161 161 to_run, not_run = [], []
162 162 if not inc_slow:
163 163 test_sections['parallel'].enabled = False
164 164
165 165 for name in test_group_names:
166 166 controller = PyTestController(name)
167 167 if xunit:
168 168 controller.add_xunit()
169 169 if coverage:
170 170 controller.add_coverage()
171 171 if controller.will_run:
172 172 to_run.append(controller)
173 173 else:
174 174 not_run.append(controller)
175 175 return to_run, not_run
176 176
177 177 def do_run(controller):
178 178 try:
179 179 try:
180 180 controller.launch()
181 181 except Exception:
182 182 import traceback
183 183 traceback.print_exc()
184 184 return controller, 1 # signal failure
185 185
186 186 exitcode = controller.wait()
187 187 return controller, exitcode
188 188
189 189 except KeyboardInterrupt:
190 190 return controller, -signal.SIGINT
191 191 finally:
192 192 controller.cleanup()
193 193
194 194 def report():
195 195 """Return a string with a summary report of test-related variables."""
196 196
197 197 out = [ sys_info(), '\n']
198 198
199 199 avail = []
200 200 not_avail = []
201 201
202 202 for k, is_avail in have.items():
203 203 if is_avail:
204 204 avail.append(k)
205 205 else:
206 206 not_avail.append(k)
207 207
208 208 if avail:
209 209 out.append('\nTools and libraries available at test time:\n')
210 210 avail.sort()
211 211 out.append(' ' + ' '.join(avail)+'\n')
212 212
213 213 if not_avail:
214 214 out.append('\nTools and libraries NOT available at test time:\n')
215 215 not_avail.sort()
216 216 out.append(' ' + ' '.join(not_avail)+'\n')
217 217
218 218 return ''.join(out)
219 219
220 220 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
221 221 """Run the entire IPython test suite by calling nose and trial.
222 222
223 223 This function constructs :class:`IPTester` instances for all IPython
224 224 modules and package and then runs each of them. This causes the modules
225 225 and packages of IPython to be tested each in their own subprocess using
226 226 nose.
227 227
228 228 Parameters
229 229 ----------
230 230
231 231 inc_slow : bool, optional
232 232 Include slow tests, like IPython.parallel. By default, these tests aren't
233 233 run.
234 234
235 235 fast : bool, option
236 236 Run the test suite in parallel, if True, using as many threads as there
237 237 are processors
238 238 """
239 239 if jobs != 1:
240 240 TestController.buffer_output = True
241 241
242 242 to_run, not_run = prepare_py_test_controllers(inc_slow=inc_slow, xunit=xunit_out,
243 243 coverage=coverage_out)
244 244
245 245 def justify(ltext, rtext, width=70, fill='-'):
246 246 ltext += ' '
247 247 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
248 248 return ltext + rtext
249 249
250 250 # Run all test runners, tracking execution time
251 251 failed = []
252 252 t_start = time.time()
253 253
254 254 print('*'*70)
255 255 if jobs == 1:
256 256 for controller in to_run:
257 257 print('IPython test group:', controller.section)
258 258 controller, res = do_run(controller)
259 259 if res:
260 260 failed.append(controller)
261 261 if res == -signal.SIGINT:
262 262 print("Interrupted")
263 263 break
264 264 print()
265 265
266 266 else:
267 267 try:
268 268 pool = multiprocessing.pool.ThreadPool(jobs)
269 269 for (controller, res) in pool.imap_unordered(do_run, to_run):
270 270 res_string = 'OK' if res == 0 else 'FAILED'
271 271 print(justify('IPython test group: ' + controller.section, res_string))
272 272 if res:
273 273 print(bytes_to_str(controller.stdout))
274 274 failed.append(controller)
275 275 if res == -signal.SIGINT:
276 276 print("Interrupted")
277 277 break
278 278 except KeyboardInterrupt:
279 279 return
280 280
281 281 for controller in not_run:
282 282 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
283 283
284 284 t_end = time.time()
285 285 t_tests = t_end - t_start
286 286 nrunners = len(to_run)
287 287 nfail = len(failed)
288 288 # summarize results
289 289 print('*'*70)
290 290 print('Test suite completed for system with the following information:')
291 291 print(report())
292 292 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
293 293 print()
294 294 print('Status:')
295 295 if not failed:
296 296 print('OK')
297 297 else:
298 298 # If anything went wrong, point out what command to rerun manually to
299 299 # see the actual errors and individual summary
300 300 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
301 301 for controller in failed:
302 302 print('-'*40)
303 303 print('Runner failed:', controller.section)
304 304 print('You may wish to rerun this one individually, with:')
305 305 print(' iptest', *controller.cmd[3:])
306 306 print()
307 307
308 308 if coverage_out:
309 309 from coverage import coverage
310 310 cov = coverage(data_file='.coverage')
311 311 cov.combine()
312 312 cov.save()
313 313
314 314 # Coverage HTML report
315 315 if coverage_out == 'html':
316 316 html_dir = 'ipy_htmlcov'
317 317 shutil.rmtree(html_dir, ignore_errors=True)
318 318 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
319 319 sys.stdout.flush()
320 320
321 321 # Custom HTML reporter to clean up module names.
322 322 from coverage.html import HtmlReporter
323 323 class CustomHtmlReporter(HtmlReporter):
324 324 def find_code_units(self, morfs):
325 325 super(CustomHtmlReporter, self).find_code_units(morfs)
326 326 for cu in self.code_units:
327 327 nameparts = cu.name.split(os.sep)
328 328 if 'IPython' not in nameparts:
329 329 continue
330 330 ix = nameparts.index('IPython')
331 331 cu.name = '.'.join(nameparts[ix:])
332 332
333 333 # Reimplement the html_report method with our custom reporter
334 334 cov._harvest_data()
335 335 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
336 336 html_title='IPython test coverage',
337 337 )
338 338 reporter = CustomHtmlReporter(cov, cov.config)
339 339 reporter.report(None)
340 340 print('done.')
341 341
342 342 # Coverage XML report
343 343 elif coverage_out == 'xml':
344 344 cov.xml_report(outfile='ipy_coverage.xml')
345 345
346 346 if failed:
347 347 # Ensure that our exit code indicates failure
348 348 sys.exit(1)
349 349
350 350
351 351 def main():
352 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
352 if len(sys.argv) > 1 and \
353 ((sys.argv[1] in test_sections) or sys.argv[1].startswith('IPython')):
353 354 from .iptest import run_iptest
354 355 # This is in-process
355 356 run_iptest()
356 357 return
357 358
358 359 parser = argparse.ArgumentParser(description='Run IPython test suite')
359 360 parser.add_argument('--all', action='store_true',
360 361 help='Include slow tests not run by default.')
361 362 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
362 363 help='Run test sections in parallel.')
363 364 parser.add_argument('--xunit', action='store_true',
364 365 help='Produce Xunit XML results')
365 366 parser.add_argument('--coverage', nargs='?', const=True, default=False,
366 367 help="Measure test coverage. Specify 'html' or "
367 368 "'xml' to get reports.")
368 369
369 370 options = parser.parse_args()
370 371
371 372 try:
372 373 jobs = int(options.fast)
373 374 except TypeError:
374 375 jobs = options.fast
375 376
376 377 # This starts subprocesses
377 378 run_iptestall(inc_slow=options.all, jobs=jobs,
378 379 xunit_out=options.xunit, coverage_out=options.coverage)
379 380
380 381
381 382 if __name__ == '__main__':
382 383 main()
General Comments 0
You need to be logged in to leave comments. Login now