##// END OF EJS Templates
Merge pull request #4180 from takluyver/iptest-refactor...
Paul Ivanov -
r12626:2b69cefc merge
parent child Browse files
Show More
@@ -0,0 +1,404 b''
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
3
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
6
7 """
8
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
20
21 import argparse
22 import multiprocessing.pool
23 import os
24 import shutil
25 import signal
26 import sys
27 import subprocess
28 import time
29
30 from .iptest import have, test_group_names, test_sections
31 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.sysinfo import sys_info
33 from IPython.utils.tempdir import TemporaryDirectory
34
35
36 class TestController(object):
37 """Run tests in a subprocess
38 """
39 #: str, IPython test suite to be executed.
40 section = None
41 #: list, command line arguments to be executed
42 cmd = None
43 #: dict, extra environment variables to set for the subprocess
44 env = None
45 #: list, TemporaryDirectory instances to clear up when the process finishes
46 dirs = None
47 #: subprocess.Popen instance
48 process = None
49 #: str, process stdout+stderr
50 stdout = None
51 #: bool, whether to capture process stdout & stderr
52 buffer_output = False
53
54 def __init__(self):
55 self.cmd = []
56 self.env = {}
57 self.dirs = []
58
59 @property
60 def will_run(self):
61 """Override in subclasses to check for dependencies."""
62 return False
63
64 def launch(self):
65 # print('*** ENV:', self.env) # dbg
66 # print('*** CMD:', self.cmd) # dbg
67 env = os.environ.copy()
68 env.update(self.env)
69 output = subprocess.PIPE if self.buffer_output else None
70 stdout = subprocess.STDOUT if self.buffer_output else None
71 self.process = subprocess.Popen(self.cmd, stdout=output,
72 stderr=stdout, env=env)
73
74 def wait(self):
75 self.stdout, _ = self.process.communicate()
76 return self.process.returncode
77
78 def cleanup_process(self):
79 """Cleanup on exit by killing any leftover processes."""
80 subp = self.process
81 if subp is None or (subp.poll() is not None):
82 return # Process doesn't exist, or is already dead.
83
84 try:
85 print('Cleaning up stale PID: %d' % subp.pid)
86 subp.kill()
87 except: # (OSError, WindowsError) ?
88 # This is just a best effort, if we fail or the process was
89 # really gone, ignore it.
90 pass
91 else:
92 for i in range(10):
93 if subp.poll() is None:
94 time.sleep(0.1)
95 else:
96 break
97
98 if subp.poll() is None:
99 # The process did not die...
100 print('... failed. Manual cleanup may be required.')
101
102 def cleanup(self):
103 "Kill process if it's still alive, and clean up temporary directories"
104 self.cleanup_process()
105 for td in self.dirs:
106 td.cleanup()
107
108 __del__ = cleanup
109
110 class PyTestController(TestController):
111 """Run Python tests using IPython.testing.iptest"""
112 #: str, Python command to execute in subprocess
113 pycmd = None
114
115 def __init__(self, section):
116 """Create new test runner."""
117 TestController.__init__(self)
118 self.section = section
119 # pycmd is put into cmd[2] in PyTestController.launch()
120 self.cmd = [sys.executable, '-c', None, section]
121 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
122 ipydir = TemporaryDirectory()
123 self.dirs.append(ipydir)
124 self.env['IPYTHONDIR'] = ipydir.name
125 self.workingdir = workingdir = TemporaryDirectory()
126 self.dirs.append(workingdir)
127 self.env['IPTEST_WORKING_DIR'] = workingdir.name
128 # This means we won't get odd effects from our own matplotlib config
129 self.env['MPLCONFIGDIR'] = workingdir.name
130
131 @property
132 def will_run(self):
133 try:
134 return test_sections[self.section].will_run
135 except KeyError:
136 return True
137
138 def add_xunit(self):
139 xunit_file = os.path.abspath(self.section + '.xunit.xml')
140 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
141
142 def add_coverage(self):
143 try:
144 sources = test_sections[self.section].includes
145 except KeyError:
146 sources = ['IPython']
147
148 coverage_rc = ("[run]\n"
149 "data_file = {data_file}\n"
150 "source =\n"
151 " {source}\n"
152 ).format(data_file=os.path.abspath('.coverage.'+self.section),
153 source="\n ".join(sources))
154 config_file = os.path.join(self.workingdir.name, '.coveragerc')
155 with open(config_file, 'w') as f:
156 f.write(coverage_rc)
157
158 self.env['COVERAGE_PROCESS_START'] = config_file
159 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
160
161 def launch(self):
162 self.cmd[2] = self.pycmd
163 super(PyTestController, self).launch()
164
165
166 def prepare_py_test_controllers(inc_slow=False):
167 """Returns an ordered list of PyTestController instances to be run."""
168 to_run, not_run = [], []
169 if not inc_slow:
170 test_sections['parallel'].enabled = False
171
172 for name in test_group_names:
173 controller = PyTestController(name)
174 if controller.will_run:
175 to_run.append(controller)
176 else:
177 not_run.append(controller)
178 return to_run, not_run
179
180 def configure_controllers(controllers, xunit=False, coverage=False):
181 """Apply options for a collection of TestController objects."""
182 for controller in controllers:
183 if xunit:
184 controller.add_xunit()
185 if coverage:
186 controller.add_coverage()
187
188 def do_run(controller):
189 try:
190 try:
191 controller.launch()
192 except Exception:
193 import traceback
194 traceback.print_exc()
195 return controller, 1 # signal failure
196
197 exitcode = controller.wait()
198 return controller, exitcode
199
200 except KeyboardInterrupt:
201 return controller, -signal.SIGINT
202 finally:
203 controller.cleanup()
204
205 def report():
206 """Return a string with a summary report of test-related variables."""
207
208 out = [ sys_info(), '\n']
209
210 avail = []
211 not_avail = []
212
213 for k, is_avail in have.items():
214 if is_avail:
215 avail.append(k)
216 else:
217 not_avail.append(k)
218
219 if avail:
220 out.append('\nTools and libraries available at test time:\n')
221 avail.sort()
222 out.append(' ' + ' '.join(avail)+'\n')
223
224 if not_avail:
225 out.append('\nTools and libraries NOT available at test time:\n')
226 not_avail.sort()
227 out.append(' ' + ' '.join(not_avail)+'\n')
228
229 return ''.join(out)
230
231 def run_iptestall(options):
232 """Run the entire IPython test suite by calling nose and trial.
233
234 This function constructs :class:`IPTester` instances for all IPython
235 modules and package and then runs each of them. This causes the modules
236 and packages of IPython to be tested each in their own subprocess using
237 nose.
238
239 Parameters
240 ----------
241
242 All parameters are passed as attributes of the options object.
243
244 testgroups : list of str
245 Run only these sections of the test suite. If empty, run all the available
246 sections.
247
248 fast : int or None
249 Run the test suite in parallel, using n simultaneous processes. If None
250 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
251
252 inc_slow : bool
253 Include slow tests, like IPython.parallel. By default, these tests aren't
254 run.
255
256 xunit : bool
257 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
258
259 coverage : bool or str
260 Measure code coverage from tests. True will store the raw coverage data,
261 or pass 'html' or 'xml' to get reports.
262 """
263 if options.fast != 1:
264 # If running in parallel, capture output so it doesn't get interleaved
265 TestController.buffer_output = True
266
267 if options.testgroups:
268 to_run = [PyTestController(name) for name in options.testgroups]
269 not_run = []
270 else:
271 to_run, not_run = prepare_py_test_controllers(inc_slow=options.all)
272
273 configure_controllers(to_run, xunit=options.xunit, coverage=options.coverage)
274
275 def justify(ltext, rtext, width=70, fill='-'):
276 ltext += ' '
277 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
278 return ltext + rtext
279
280 # Run all test runners, tracking execution time
281 failed = []
282 t_start = time.time()
283
284 print()
285 if options.fast == 1:
286 # This actually means sequential, i.e. with 1 job
287 for controller in to_run:
288 print('IPython test group:', controller.section)
289 controller, res = do_run(controller)
290 if res:
291 failed.append(controller)
292 if res == -signal.SIGINT:
293 print("Interrupted")
294 break
295 print()
296
297 else:
298 # Run tests concurrently
299 try:
300 pool = multiprocessing.pool.ThreadPool(options.fast)
301 for (controller, res) in pool.imap_unordered(do_run, to_run):
302 res_string = 'OK' if res == 0 else 'FAILED'
303 print(justify('IPython test group: ' + controller.section, res_string))
304 if res:
305 print(bytes_to_str(controller.stdout))
306 failed.append(controller)
307 if res == -signal.SIGINT:
308 print("Interrupted")
309 break
310 except KeyboardInterrupt:
311 return
312
313 for controller in not_run:
314 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
315
316 t_end = time.time()
317 t_tests = t_end - t_start
318 nrunners = len(to_run)
319 nfail = len(failed)
320 # summarize results
321 print('_'*70)
322 print('Test suite completed for system with the following information:')
323 print(report())
324 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
325 print()
326 print('Status: ', end='')
327 if not failed:
328 print('OK')
329 else:
330 # If anything went wrong, point out what command to rerun manually to
331 # see the actual errors and individual summary
332 failed_sections = [c.section for c in failed]
333 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
334 nrunners, ', '.join(failed_sections)))
335 print()
336 print('You may wish to rerun these, with:')
337 print(' iptest', *failed_sections)
338 print()
339
340 if options.coverage:
341 from coverage import coverage
342 cov = coverage(data_file='.coverage')
343 cov.combine()
344 cov.save()
345
346 # Coverage HTML report
347 if options.coverage == 'html':
348 html_dir = 'ipy_htmlcov'
349 shutil.rmtree(html_dir, ignore_errors=True)
350 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
351 sys.stdout.flush()
352
353 # Custom HTML reporter to clean up module names.
354 from coverage.html import HtmlReporter
355 class CustomHtmlReporter(HtmlReporter):
356 def find_code_units(self, morfs):
357 super(CustomHtmlReporter, self).find_code_units(morfs)
358 for cu in self.code_units:
359 nameparts = cu.name.split(os.sep)
360 if 'IPython' not in nameparts:
361 continue
362 ix = nameparts.index('IPython')
363 cu.name = '.'.join(nameparts[ix:])
364
365 # Reimplement the html_report method with our custom reporter
366 cov._harvest_data()
367 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
368 html_title='IPython test coverage',
369 )
370 reporter = CustomHtmlReporter(cov, cov.config)
371 reporter.report(None)
372 print('done.')
373
374 # Coverage XML report
375 elif options.coverage == 'xml':
376 cov.xml_report(outfile='ipy_coverage.xml')
377
378 if failed:
379 # Ensure that our exit code indicates failure
380 sys.exit(1)
381
382
383 def main():
384 parser = argparse.ArgumentParser(description='Run IPython test suite')
385 parser.add_argument('testgroups', nargs='*',
386 help='Run specified groups of tests. If omitted, run '
387 'all tests.')
388 parser.add_argument('--all', action='store_true',
389 help='Include slow tests not run by default.')
390 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
391 help='Run test sections in parallel.')
392 parser.add_argument('--xunit', action='store_true',
393 help='Produce Xunit XML results')
394 parser.add_argument('--coverage', nargs='?', const=True, default=False,
395 help="Measure test coverage. Specify 'html' or "
396 "'xml' to get reports.")
397
398 options = parser.parse_args()
399
400 run_iptestall(options)
401
402
403 if __name__ == '__main__':
404 main()
@@ -10,5 +10,5 b' before_install:'
10 10 install:
11 11 - python setup.py install -q
12 12 script:
13 - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then iptest -w /tmp; fi
14 - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then iptest3 -w /tmp; fi
13 - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cd /tmp; iptest; fi
14 - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then cd /tmp; iptest3; fi
@@ -117,8 +117,7 b' def test_ipdb_magics():'
117 117 ipdb> pinfo a
118 118 Type: ExampleClass
119 119 String Form:ExampleClass()
120 Namespace: Locals
121 File: ...
120 Namespace: Local...
122 121 Docstring: Docstring for ExampleClass.
123 122 Constructor Docstring:Docstring for ExampleClass.__init__
124 123 ipdb> continue
@@ -20,5 +20,5 b' Exiting."""'
20 20 import sys
21 21 print >> sys.stderr, error
22 22 else:
23 from IPython.testing import iptest
24 iptest.main()
23 from IPython.testing import iptestcontroller
24 iptestcontroller.main()
This diff has been collapsed as it changes many lines, (644 lines changed) Show them Hide them
@@ -30,30 +30,19 b' from __future__ import print_function'
30 30 import glob
31 31 import os
32 32 import os.path as path
33 import signal
33 import re
34 34 import sys
35 import subprocess
36 import tempfile
37 import time
38 35 import warnings
39 import multiprocessing.pool
40 36
41 37 # Now, proceed to import nose itself
42 38 import nose.plugins.builtin
43 39 from nose.plugins.xunit import Xunit
44 40 from nose import SkipTest
45 41 from nose.core import TestProgram
42 from nose.plugins import Plugin
46 43
47 44 # Our own imports
48 from IPython.utils import py3compat
49 45 from IPython.utils.importstring import import_item
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
51 from IPython.utils.process import pycmd2argv
52 from IPython.utils.sysinfo import sys_info
53 from IPython.utils.tempdir import TemporaryDirectory
54 from IPython.utils.warn import warn
55
56 from IPython.testing import globalipapp
57 46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
58 47 from IPython.external.decorators import KnownFailure, knownfailureif
59 48
@@ -100,7 +89,7 b' def monkeypatch_xunit():'
100 89 Xunit.addError = addError
101 90
102 91 #-----------------------------------------------------------------------------
103 # Logic for skipping doctests
92 # Check which dependencies are installed and greater than minimum version.
104 93 #-----------------------------------------------------------------------------
105 94 def extract_version(mod):
106 95 return mod.__version__
@@ -164,321 +153,208 b' min_zmq = (2,1,11)'
164 153 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
165 154
166 155 #-----------------------------------------------------------------------------
167 # Functions and classes
156 # Test suite definitions
168 157 #-----------------------------------------------------------------------------
169 158
170 def report():
171 """Return a string with a summary report of test-related variables."""
172
173 out = [ sys_info(), '\n']
174
175 avail = []
176 not_avail = []
177
178 for k, is_avail in have.items():
179 if is_avail:
180 avail.append(k)
181 else:
182 not_avail.append(k)
183
184 if avail:
185 out.append('\nTools and libraries available at test time:\n')
186 avail.sort()
187 out.append(' ' + ' '.join(avail)+'\n')
188
189 if not_avail:
190 out.append('\nTools and libraries NOT available at test time:\n')
191 not_avail.sort()
192 out.append(' ' + ' '.join(not_avail)+'\n')
193
194 return ''.join(out)
195
196
197 def make_exclude():
198 """Make patterns of modules and packages to exclude from testing.
199
200 For the IPythonDoctest plugin, we need to exclude certain patterns that
201 cause testing problems. We should strive to minimize the number of
202 skipped modules, since this means untested code.
203
204 These modules and packages will NOT get scanned by nose at all for tests.
205 """
206 # Simple utility to make IPython paths more readably, we need a lot of
207 # these below
208 ipjoin = lambda *paths: pjoin('IPython', *paths)
209
210 exclusions = [ipjoin('external'),
211 ipjoin('quarantine'),
212 ipjoin('deathrow'),
213 # This guy is probably attic material
214 ipjoin('testing', 'mkdoctests'),
215 # Testing inputhook will need a lot of thought, to figure out
216 # how to have tests that don't lock up with the gui event
217 # loops in the picture
218 ipjoin('lib', 'inputhook'),
219 # Config files aren't really importable stand-alone
220 ipjoin('config', 'profile'),
221 # The notebook 'static' directory contains JS, css and other
222 # files for web serving. Occasionally projects may put a .py
223 # file in there (MathJax ships a conf.py), so we might as
224 # well play it safe and skip the whole thing.
225 ipjoin('html', 'static'),
226 ipjoin('html', 'fabfile'),
227 ]
228 if not have['sqlite3']:
229 exclusions.append(ipjoin('core', 'tests', 'test_history'))
230 exclusions.append(ipjoin('core', 'history'))
231 if not have['wx']:
232 exclusions.append(ipjoin('lib', 'inputhookwx'))
159 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
160 'extensions', 'lib', 'terminal', 'testing', 'utils',
161 'nbformat', 'qt', 'html', 'nbconvert'
162 ]
163
164 class TestSection(object):
165 def __init__(self, name, includes):
166 self.name = name
167 self.includes = includes
168 self.excludes = []
169 self.dependencies = []
170 self.enabled = True
233 171
234 if 'IPython.kernel.inprocess' not in sys.argv:
235 exclusions.append(ipjoin('kernel', 'inprocess'))
172 def exclude(self, module):
173 if not module.startswith('IPython'):
174 module = self.includes[0] + "." + module
175 self.excludes.append(module.replace('.', os.sep))
236 176
237 # FIXME: temporarily disable autoreload tests, as they can produce
238 # spurious failures in subsequent tests (cythonmagic).
239 exclusions.append(ipjoin('extensions', 'autoreload'))
240 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
241
242 # We do this unconditionally, so that the test suite doesn't import
243 # gtk, changing the default encoding and masking some unicode bugs.
244 exclusions.append(ipjoin('lib', 'inputhookgtk'))
245 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
246
247 #Also done unconditionally, exclude nbconvert directories containing
248 #config files used to test. Executing the config files with iptest would
249 #cause an exception.
250 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
251 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
252
253 # These have to be skipped on win32 because the use echo, rm, cd, etc.
254 # See ticket https://github.com/ipython/ipython/issues/87
255 if sys.platform == 'win32':
256 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
257 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
258
259 if not have['pexpect']:
260 exclusions.extend([ipjoin('lib', 'irunner'),
261 ipjoin('lib', 'tests', 'test_irunner'),
262 ipjoin('terminal', 'console'),
263 ])
264
265 if not have['zmq']:
266 exclusions.append(ipjoin('lib', 'kernel'))
267 exclusions.append(ipjoin('kernel'))
268 exclusions.append(ipjoin('qt'))
269 exclusions.append(ipjoin('html'))
270 exclusions.append(ipjoin('consoleapp.py'))
271 exclusions.append(ipjoin('terminal', 'console'))
272 exclusions.append(ipjoin('parallel'))
273 elif not have['qt'] or not have['pygments']:
274 exclusions.append(ipjoin('qt'))
275
276 if not have['pymongo']:
277 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
278 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
279
280 if not have['matplotlib']:
281 exclusions.extend([ipjoin('core', 'pylabtools'),
282 ipjoin('core', 'tests', 'test_pylabtools'),
283 ipjoin('kernel', 'zmq', 'pylab'),
284 ])
285
286 if not have['cython']:
287 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
288 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
289
290 if not have['oct2py']:
291 exclusions.extend([ipjoin('extensions', 'octavemagic')])
292 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
293
294 if not have['tornado']:
295 exclusions.append(ipjoin('html'))
296 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
297 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
298
299 if not have['jinja2']:
300 exclusions.append(ipjoin('html', 'notebookapp'))
301
302 if not have['rpy2'] or not have['numpy']:
303 exclusions.append(ipjoin('extensions', 'rmagic'))
304 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
305
306 if not have['azure']:
307 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
308
309 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
310 exclusions.append(ipjoin('nbconvert'))
311
312 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
313 if sys.platform == 'win32':
314 exclusions = [s.replace('\\','\\\\') for s in exclusions]
177 def requires(self, *packages):
178 self.dependencies.extend(packages)
315 179
316 # check for any exclusions that don't seem to exist:
317 parent, _ = os.path.split(get_ipython_package_dir())
318 for exclusion in exclusions:
319 if exclusion.endswith(('deathrow', 'quarantine')):
320 # ignore deathrow/quarantine, which exist in dev, but not install
321 continue
322 fullpath = pjoin(parent, exclusion)
323 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
324 warn("Excluding nonexistent file: %r" % exclusion)
325
326 return exclusions
327
328
329 class IPTester(object):
330 """Call that calls iptest or trial in a subprocess.
331 """
332 #: string, name of test runner that will be called
333 runner = None
334 #: list, parameters for test runner
335 params = None
336 #: list, arguments of system call to be made to call test runner
337 call_args = None
338 #: list, subprocesses we start (for cleanup)
339 processes = None
340 #: str, coverage xml output file
341 coverage_xml = None
342 buffer_output = False
343
344 def __init__(self, runner='iptest', params=None):
345 """Create new test runner."""
346 p = os.path
347 if runner == 'iptest':
348 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
349 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
350 else:
351 raise Exception('Not a valid test runner: %s' % repr(runner))
352 if params is None:
353 params = []
354 if isinstance(params, str):
355 params = [params]
356 self.params = params
357
358 # Assemble call
359 self.call_args = self.runner+self.params
360
361 # Find the section we're testing (IPython.foo)
362 for sect in self.params:
363 if sect.startswith('IPython') or sect in special_test_suites: break
364 else:
365 raise ValueError("Section not found", self.params)
366
367 if '--with-xunit' in self.call_args:
368
369 self.call_args.append('--xunit-file')
370 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
371 xunit_file = path.abspath(sect+'.xunit.xml')
372 if sys.platform == 'win32':
373 xunit_file = '"%s"' % xunit_file
374 self.call_args.append(xunit_file)
375
376 if '--with-xml-coverage' in self.call_args:
377 self.coverage_xml = path.abspath(sect+".coverage.xml")
378 self.call_args.remove('--with-xml-coverage')
379 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
380
381 # Store anything we start to clean up on deletion
382 self.processes = []
383
384 def _run_cmd(self):
385 with TemporaryDirectory() as IPYTHONDIR:
386 env = os.environ.copy()
387 env['IPYTHONDIR'] = IPYTHONDIR
388 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
389 output = subprocess.PIPE if self.buffer_output else None
390 subp = subprocess.Popen(self.call_args, stdout=output,
391 stderr=output, env=env)
392 self.processes.append(subp)
393 # If this fails, the process will be left in self.processes and
394 # cleaned up later, but if the wait call succeeds, then we can
395 # clear the stored process.
396 retcode = subp.wait()
397 self.processes.pop()
398 self.stdout = subp.stdout
399 self.stderr = subp.stderr
400 return retcode
401
402 def run(self):
403 """Run the stored commands"""
404 try:
405 retcode = self._run_cmd()
406 except KeyboardInterrupt:
407 return -signal.SIGINT
408 except:
409 import traceback
410 traceback.print_exc()
411 return 1 # signal failure
412
413 if self.coverage_xml:
414 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
415 return retcode
416
417 def __del__(self):
418 """Cleanup on exit by killing any leftover processes."""
419 for subp in self.processes:
420 if subp.poll() is not None:
421 continue # process is already dead
422
423 try:
424 print('Cleaning up stale PID: %d' % subp.pid)
425 subp.kill()
426 except: # (OSError, WindowsError) ?
427 # This is just a best effort, if we fail or the process was
428 # really gone, ignore it.
429 pass
430 else:
431 for i in range(10):
432 if subp.poll() is None:
433 time.sleep(0.1)
434 else:
435 break
436
437 if subp.poll() is None:
438 # The process did not die...
439 print('... failed. Manual cleanup may be required.')
440
441
442 special_test_suites = {
443 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
444 }
445
446 def make_runners(inc_slow=False):
447 """Define the top-level packages that need to be tested.
448 """
449
450 # Packages to be tested via nose, that only depend on the stdlib
451 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
452 'testing', 'utils', 'nbformat']
453
454 if have['qt']:
455 nose_pkg_names.append('qt')
456
457 if have['tornado']:
458 nose_pkg_names.append('html')
459
460 if have['zmq']:
461 nose_pkg_names.insert(0, 'kernel')
462 nose_pkg_names.insert(1, 'kernel.inprocess')
463 if inc_slow:
464 nose_pkg_names.insert(0, 'parallel')
180 @property
181 def will_run(self):
182 return self.enabled and all(have[p] for p in self.dependencies)
183
184 # Name -> (include, exclude, dependencies_met)
185 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
186
187 # Exclusions and dependencies
188 # ---------------------------
189
190 # core:
191 sec = test_sections['core']
192 if not have['sqlite3']:
193 sec.exclude('tests.test_history')
194 sec.exclude('history')
195 if not have['matplotlib']:
196 sec.exclude('pylabtools'),
197 sec.exclude('tests.test_pylabtools')
198
199 # lib:
200 sec = test_sections['lib']
201 if not have['wx']:
202 sec.exclude('inputhookwx')
203 if not have['pexpect']:
204 sec.exclude('irunner')
205 sec.exclude('tests.test_irunner')
206 if not have['zmq']:
207 sec.exclude('kernel')
208 # We do this unconditionally, so that the test suite doesn't import
209 # gtk, changing the default encoding and masking some unicode bugs.
210 sec.exclude('inputhookgtk')
211 # Testing inputhook will need a lot of thought, to figure out
212 # how to have tests that don't lock up with the gui event
213 # loops in the picture
214 sec.exclude('inputhook')
215
216 # testing:
217 sec = test_sections['lib']
218 # This guy is probably attic material
219 sec.exclude('mkdoctests')
220 # These have to be skipped on win32 because the use echo, rm, cd, etc.
221 # See ticket https://github.com/ipython/ipython/issues/87
222 if sys.platform == 'win32':
223 sec.exclude('plugin.test_exampleip')
224 sec.exclude('plugin.dtexample')
225
226 # terminal:
227 if (not have['pexpect']) or (not have['zmq']):
228 test_sections['terminal'].exclude('console')
229
230 # parallel
231 sec = test_sections['parallel']
232 sec.requires('zmq')
233 if not have['pymongo']:
234 sec.exclude('controller.mongodb')
235 sec.exclude('tests.test_mongodb')
236
237 # kernel:
238 sec = test_sections['kernel']
239 sec.requires('zmq')
240 # The in-process kernel tests are done in a separate section
241 sec.exclude('inprocess')
242 # importing gtk sets the default encoding, which we want to avoid
243 sec.exclude('zmq.gui.gtkembed')
244 if not have['matplotlib']:
245 sec.exclude('zmq.pylab')
246
247 # kernel.inprocess:
248 test_sections['kernel.inprocess'].requires('zmq')
249
250 # extensions:
251 sec = test_sections['extensions']
252 if not have['cython']:
253 sec.exclude('cythonmagic')
254 sec.exclude('tests.test_cythonmagic')
255 if not have['oct2py']:
256 sec.exclude('octavemagic')
257 sec.exclude('tests.test_octavemagic')
258 if not have['rpy2'] or not have['numpy']:
259 sec.exclude('rmagic')
260 sec.exclude('tests.test_rmagic')
261 # autoreload does some strange stuff, so move it to its own test section
262 sec.exclude('autoreload')
263 sec.exclude('tests.test_autoreload')
264 test_sections['autoreload'] = TestSection('autoreload',
265 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
266 test_group_names.append('autoreload')
267
268 # qt:
269 test_sections['qt'].requires('zmq', 'qt', 'pygments')
270
271 # html:
272 sec = test_sections['html']
273 sec.requires('zmq', 'tornado')
274 # The notebook 'static' directory contains JS, css and other
275 # files for web serving. Occasionally projects may put a .py
276 # file in there (MathJax ships a conf.py), so we might as
277 # well play it safe and skip the whole thing.
278 sec.exclude('static')
279 sec.exclude('fabfile')
280 if not have['jinja2']:
281 sec.exclude('notebookapp')
282 if not have['azure']:
283 sec.exclude('services.notebooks.azurenbmanager')
284
285 # config:
286 # Config files aren't really importable stand-alone
287 test_sections['config'].exclude('profile')
288
289 # nbconvert:
290 sec = test_sections['nbconvert']
291 sec.requires('pygments', 'jinja2', 'sphinx')
292 # Exclude nbconvert directories containing config files used to test.
293 # Executing the config files with iptest would cause an exception.
294 sec.exclude('tests.files')
295 sec.exclude('exporters.tests.files')
296 if not have['tornado']:
297 sec.exclude('nbconvert.post_processors.serve')
298 sec.exclude('nbconvert.post_processors.tests.test_serve')
465 299
466 if all((have['pygments'], have['jinja2'], have['sphinx'])):
467 nose_pkg_names.append('nbconvert')
300 #-----------------------------------------------------------------------------
301 # Functions and classes
302 #-----------------------------------------------------------------------------
468 303
469 # For debugging this code, only load quick stuff
470 #nose_pkg_names = ['core', 'extensions'] # dbg
304 def check_exclusions_exist():
305 from IPython.utils.path import get_ipython_package_dir
306 from IPython.utils.warn import warn
307 parent = os.path.dirname(get_ipython_package_dir())
308 for sec in test_sections:
309 for pattern in sec.exclusions:
310 fullpath = pjoin(parent, pattern)
311 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
312 warn("Excluding nonexistent file: %r" % pattern)
471 313
472 # Make fully qualified package names prepending 'IPython.' to our name lists
473 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
474 314
475 # Make runners
476 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
315 class ExclusionPlugin(Plugin):
316 """A nose plugin to effect our exclusions of files and directories.
317 """
318 name = 'exclusions'
319 score = 3000 # Should come before any other plugins
320
321 def __init__(self, exclude_patterns=None):
322 """
323 Parameters
324 ----------
325
326 exclude_patterns : sequence of strings, optional
327 These patterns are compiled as regular expressions, subsequently used
328 to exclude any filename which matches them from inclusion in the test
329 suite (using pattern.search(), NOT pattern.match() ).
330 """
331
332 if exclude_patterns is None:
333 exclude_patterns = []
334 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
335 super(ExclusionPlugin, self).__init__()
336
337 def options(self, parser, env=os.environ):
338 Plugin.options(self, parser, env)
477 339
478 for name in special_test_suites:
479 runners.append((name, IPTester('iptest', params=name)))
340 def configure(self, options, config):
341 Plugin.configure(self, options, config)
342 # Override nose trying to disable plugin.
343 self.enabled = True
344
345 def wantFile(self, filename):
346 """Return whether the given filename should be scanned for tests.
347 """
348 if any(pat.search(filename) for pat in self.exclude_patterns):
349 return False
350 return None
480 351
481 return runners
352 def wantDirectory(self, directory):
353 """Return whether the given directory should be scanned for tests.
354 """
355 if any(pat.search(directory) for pat in self.exclude_patterns):
356 return False
357 return None
482 358
483 359
484 360 def run_iptest():
@@ -495,11 +371,16 b' def run_iptest():'
495 371 warnings.filterwarnings('ignore',
496 372 'This will be removed soon. Use IPython.testing.util instead')
497 373
498 if sys.argv[1] in special_test_suites:
499 sys.argv[1:2] = special_test_suites[sys.argv[1]]
500 special_suite = True
374 arg1 = sys.argv[1]
375 if arg1 in test_sections:
376 section = test_sections[arg1]
377 sys.argv[1:2] = section.includes
378 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
379 section = test_sections[arg1[8:]]
380 sys.argv[1:2] = section.includes
501 381 else:
502 special_suite = False
382 section = TestSection(arg1, includes=[arg1])
383
503 384
504 385 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
505 386
@@ -530,8 +411,11 b' def run_iptest():'
530 411
531 412 # use our plugin for doctesting. It will remove the standard doctest plugin
532 413 # if it finds it enabled
533 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
534 plugins = [ipdt, KnownFailure()]
414 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
415
416 # Use working directory set by parent process (see iptestcontroller)
417 if 'IPTEST_WORKING_DIR' in os.environ:
418 os.chdir(os.environ['IPTEST_WORKING_DIR'])
535 419
536 420 # We need a global ipython running in this process, but the special
537 421 # in-process group spawns its own IPython kernels, so for *that* group we
@@ -540,117 +424,13 b' def run_iptest():'
540 424 # assumptions about what needs to be a singleton and what doesn't (app
541 425 # objects should, individual shells shouldn't). But for now, this
542 426 # workaround allows the test suite for the inprocess module to complete.
543 if not 'IPython.kernel.inprocess' in sys.argv:
427 if 'kernel.inprocess' not in section.name:
428 from IPython.testing import globalipapp
544 429 globalipapp.start_ipython()
545 430
546 431 # Now nose can run
547 432 TestProgram(argv=argv, addplugins=plugins)
548 433
549 def do_run(x):
550 print('IPython test group:',x[0])
551 ret = x[1].run()
552 return ret
553
554 def run_iptestall(inc_slow=False, fast=False):
555 """Run the entire IPython test suite by calling nose and trial.
556
557 This function constructs :class:`IPTester` instances for all IPython
558 modules and package and then runs each of them. This causes the modules
559 and packages of IPython to be tested each in their own subprocess using
560 nose.
561
562 Parameters
563 ----------
564
565 inc_slow : bool, optional
566 Include slow tests, like IPython.parallel. By default, these tests aren't
567 run.
568
569 fast : bool, option
570 Run the test suite in parallel, if True, using as many threads as there
571 are processors
572 """
573 if fast:
574 p = multiprocessing.pool.ThreadPool()
575 else:
576 p = multiprocessing.pool.ThreadPool(1)
577
578 runners = make_runners(inc_slow=inc_slow)
579
580 # Run the test runners in a temporary dir so we can nuke it when finished
581 # to clean up any junk files left over by accident. This also makes it
582 # robust against being run in non-writeable directories by mistake, as the
583 # temp dir will always be user-writeable.
584 curdir = os.getcwdu()
585 testdir = tempfile.gettempdir()
586 os.chdir(testdir)
587
588 # Run all test runners, tracking execution time
589 failed = []
590 t_start = time.time()
591
592 try:
593 all_res = p.map(do_run, runners)
594 print('*'*70)
595 for ((name, runner), res) in zip(runners, all_res):
596 tgroup = 'IPython test group: ' + name
597 res_string = 'OK' if res == 0 else 'FAILED'
598 res_string = res_string.rjust(70 - len(tgroup), '.')
599 print(tgroup + res_string)
600 if res:
601 failed.append( (name, runner) )
602 if res == -signal.SIGINT:
603 print("Interrupted")
604 break
605 finally:
606 os.chdir(curdir)
607 t_end = time.time()
608 t_tests = t_end - t_start
609 nrunners = len(runners)
610 nfail = len(failed)
611 # summarize results
612 print()
613 print('*'*70)
614 print('Test suite completed for system with the following information:')
615 print(report())
616 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
617 print()
618 print('Status:')
619 if not failed:
620 print('OK')
621 else:
622 # If anything went wrong, point out what command to rerun manually to
623 # see the actual errors and individual summary
624 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
625 for name, failed_runner in failed:
626 print('-'*40)
627 print('Runner failed:',name)
628 print('You may wish to rerun this one individually, with:')
629 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
630 print(u' '.join(failed_call_args))
631 print()
632 # Ensure that our exit code indicates failure
633 sys.exit(1)
634
635
636 def main():
637 for arg in sys.argv[1:]:
638 if arg.startswith('IPython') or arg in special_test_suites:
639 # This is in-process
640 run_iptest()
641 else:
642 inc_slow = "--all" in sys.argv
643 if inc_slow:
644 sys.argv.remove("--all")
645
646 fast = "--fast" in sys.argv
647 if fast:
648 sys.argv.remove("--fast")
649 IPTester.buffer_output = True
650
651 # This starts subprocesses
652 run_iptestall(inc_slow=inc_slow, fast=fast)
653
654
655 434 if __name__ == '__main__':
656 main()
435 run_iptest()
436
@@ -597,23 +597,6 b' class ExtensionDoctest(doctests.Doctest):'
597 597 name = 'extdoctest' # call nosetests with --with-extdoctest
598 598 enabled = True
599 599
600 def __init__(self,exclude_patterns=None):
601 """Create a new ExtensionDoctest plugin.
602
603 Parameters
604 ----------
605
606 exclude_patterns : sequence of strings, optional
607 These patterns are compiled as regular expressions, subsequently used
608 to exclude any filename which matches them from inclusion in the test
609 suite (using pattern.search(), NOT pattern.match() ).
610 """
611
612 if exclude_patterns is None:
613 exclude_patterns = []
614 self.exclude_patterns = map(re.compile,exclude_patterns)
615 doctests.Doctest.__init__(self)
616
617 600 def options(self, parser, env=os.environ):
618 601 Plugin.options(self, parser, env)
619 602 parser.add_option('--doctest-tests', action='store_true',
@@ -716,35 +699,6 b' class ExtensionDoctest(doctests.Doctest):'
716 699 else:
717 700 yield False # no tests to load
718 701
719 def wantFile(self,filename):
720 """Return whether the given filename should be scanned for tests.
721
722 Modified version that accepts extension modules as valid containers for
723 doctests.
724 """
725 #print '*** ipdoctest- wantFile:',filename # dbg
726
727 for pat in self.exclude_patterns:
728 if pat.search(filename):
729 # print '###>>> SKIP:',filename # dbg
730 return False
731
732 if is_extension_module(filename):
733 return True
734 else:
735 return doctests.Doctest.wantFile(self,filename)
736
737 def wantDirectory(self, directory):
738 """Return whether the given directory should be scanned for tests.
739
740 Modified version that supports exclusions.
741 """
742
743 for pat in self.exclude_patterns:
744 if pat.search(directory):
745 return False
746 return True
747
748 702
749 703 class IPythonDoctest(ExtensionDoctest):
750 704 """Nose Plugin that supports doctests in extension modules.
@@ -5,6 +5,8 b' This is copied from the stdlib and will be standard in Python 3.2 and onwards.'
5 5 from __future__ import print_function
6 6
7 7 import os as _os
8 import warnings as _warnings
9 import sys as _sys
8 10
9 11 # This code should only be used in Python versions < 3.2, since after that we
10 12 # can rely on the stdlib itself.
@@ -49,7 +51,7 b' except ImportError:'
49 51 self._closed = True
50 52 if _warn:
51 53 self._warn("Implicitly cleaning up {!r}".format(self),
52 ResourceWarning)
54 Warning)
53 55
54 56 def __exit__(self, exc, value, tb):
55 57 self.cleanup()
@@ -69,6 +71,7 b' except ImportError:'
69 71 _remove = staticmethod(_os.remove)
70 72 _rmdir = staticmethod(_os.rmdir)
71 73 _os_error = _os.error
74 _warn = _warnings.warn
72 75
73 76 def _rmtree(self, path):
74 77 # Essentially a stripped down version of shutil.rmtree. We can't
@@ -323,7 +323,7 b" def find_scripts(entry_points=False, suffix=''):"
323 323 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
324 324 'iplogger%s = IPython.parallel.apps.iploggerapp:launch_new_instance',
325 325 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
326 'iptest%s = IPython.testing.iptest:main',
326 'iptest%s = IPython.testing.iptestcontroller:main',
327 327 'irunner%s = IPython.lib.irunner:main',
328 328 ]]
329 329 gui_scripts = []
General Comments 0
You need to be logged in to leave comments. Login now