##// 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 install:
10 install:
11 - python setup.py install -q
11 - python setup.py install -q
12 script:
12 script:
13 - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then iptest -w /tmp; fi
13 - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cd /tmp; iptest; fi
14 - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then iptest3 -w /tmp; fi
14 - if [[ $TRAVIS_PYTHON_VERSION == '3.'* ]]; then cd /tmp; iptest3; fi
@@ -117,8 +117,7 b' def test_ipdb_magics():'
117 ipdb> pinfo a
117 ipdb> pinfo a
118 Type: ExampleClass
118 Type: ExampleClass
119 String Form:ExampleClass()
119 String Form:ExampleClass()
120 Namespace: Locals
120 Namespace: Local...
121 File: ...
122 Docstring: Docstring for ExampleClass.
121 Docstring: Docstring for ExampleClass.
123 Constructor Docstring:Docstring for ExampleClass.__init__
122 Constructor Docstring:Docstring for ExampleClass.__init__
124 ipdb> continue
123 ipdb> continue
@@ -20,5 +20,5 b' Exiting."""'
20 import sys
20 import sys
21 print >> sys.stderr, error
21 print >> sys.stderr, error
22 else:
22 else:
23 from IPython.testing import iptest
23 from IPython.testing import iptestcontroller
24 iptest.main()
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 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import signal
33 import re
34 import sys
34 import sys
35 import subprocess
36 import tempfile
37 import time
38 import warnings
35 import warnings
39 import multiprocessing.pool
40
36
41 # Now, proceed to import nose itself
37 # Now, proceed to import nose itself
42 import nose.plugins.builtin
38 import nose.plugins.builtin
43 from nose.plugins.xunit import Xunit
39 from nose.plugins.xunit import Xunit
44 from nose import SkipTest
40 from nose import SkipTest
45 from nose.core import TestProgram
41 from nose.core import TestProgram
42 from nose.plugins import Plugin
46
43
47 # Our own imports
44 # Our own imports
48 from IPython.utils import py3compat
49 from IPython.utils.importstring import import_item
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 from IPython.testing.plugin.ipdoctest import IPythonDoctest
46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
58 from IPython.external.decorators import KnownFailure, knownfailureif
47 from IPython.external.decorators import KnownFailure, knownfailureif
59
48
@@ -100,7 +89,7 b' def monkeypatch_xunit():'
100 Xunit.addError = addError
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 def extract_version(mod):
94 def extract_version(mod):
106 return mod.__version__
95 return mod.__version__
@@ -164,321 +153,208 b' min_zmq = (2,1,11)'
164 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
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():
159 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
171 """Return a string with a summary report of test-related variables."""
160 'extensions', 'lib', 'terminal', 'testing', 'utils',
172
161 'nbformat', 'qt', 'html', 'nbconvert'
173 out = [ sys_info(), '\n']
162 ]
174
163
175 avail = []
164 class TestSection(object):
176 not_avail = []
165 def __init__(self, name, includes):
177
166 self.name = name
178 for k, is_avail in have.items():
167 self.includes = includes
179 if is_avail:
168 self.excludes = []
180 avail.append(k)
169 self.dependencies = []
181 else:
170 self.enabled = True
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'))
233
171
234 if 'IPython.kernel.inprocess' not in sys.argv:
172 def exclude(self, module):
235 exclusions.append(ipjoin('kernel', 'inprocess'))
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
177 def requires(self, *packages):
238 # spurious failures in subsequent tests (cythonmagic).
178 self.dependencies.extend(packages)
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]
315
179
316 # check for any exclusions that don't seem to exist:
180 @property
317 parent, _ = os.path.split(get_ipython_package_dir())
181 def will_run(self):
318 for exclusion in exclusions:
182 return self.enabled and all(have[p] for p in self.dependencies)
319 if exclusion.endswith(('deathrow', 'quarantine')):
183
320 # ignore deathrow/quarantine, which exist in dev, but not install
184 # Name -> (include, exclude, dependencies_met)
321 continue
185 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
322 fullpath = pjoin(parent, exclusion)
186
323 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
187 # Exclusions and dependencies
324 warn("Excluding nonexistent file: %r" % exclusion)
188 # ---------------------------
325
189
326 return exclusions
190 # core:
327
191 sec = test_sections['core']
328
192 if not have['sqlite3']:
329 class IPTester(object):
193 sec.exclude('tests.test_history')
330 """Call that calls iptest or trial in a subprocess.
194 sec.exclude('history')
331 """
195 if not have['matplotlib']:
332 #: string, name of test runner that will be called
196 sec.exclude('pylabtools'),
333 runner = None
197 sec.exclude('tests.test_pylabtools')
334 #: list, parameters for test runner
198
335 params = None
199 # lib:
336 #: list, arguments of system call to be made to call test runner
200 sec = test_sections['lib']
337 call_args = None
201 if not have['wx']:
338 #: list, subprocesses we start (for cleanup)
202 sec.exclude('inputhookwx')
339 processes = None
203 if not have['pexpect']:
340 #: str, coverage xml output file
204 sec.exclude('irunner')
341 coverage_xml = None
205 sec.exclude('tests.test_irunner')
342 buffer_output = False
206 if not have['zmq']:
343
207 sec.exclude('kernel')
344 def __init__(self, runner='iptest', params=None):
208 # We do this unconditionally, so that the test suite doesn't import
345 """Create new test runner."""
209 # gtk, changing the default encoding and masking some unicode bugs.
346 p = os.path
210 sec.exclude('inputhookgtk')
347 if runner == 'iptest':
211 # Testing inputhook will need a lot of thought, to figure out
348 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
212 # how to have tests that don't lock up with the gui event
349 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
213 # loops in the picture
350 else:
214 sec.exclude('inputhook')
351 raise Exception('Not a valid test runner: %s' % repr(runner))
215
352 if params is None:
216 # testing:
353 params = []
217 sec = test_sections['lib']
354 if isinstance(params, str):
218 # This guy is probably attic material
355 params = [params]
219 sec.exclude('mkdoctests')
356 self.params = params
220 # These have to be skipped on win32 because the use echo, rm, cd, etc.
357
221 # See ticket https://github.com/ipython/ipython/issues/87
358 # Assemble call
222 if sys.platform == 'win32':
359 self.call_args = self.runner+self.params
223 sec.exclude('plugin.test_exampleip')
360
224 sec.exclude('plugin.dtexample')
361 # Find the section we're testing (IPython.foo)
225
362 for sect in self.params:
226 # terminal:
363 if sect.startswith('IPython') or sect in special_test_suites: break
227 if (not have['pexpect']) or (not have['zmq']):
364 else:
228 test_sections['terminal'].exclude('console')
365 raise ValueError("Section not found", self.params)
229
366
230 # parallel
367 if '--with-xunit' in self.call_args:
231 sec = test_sections['parallel']
368
232 sec.requires('zmq')
369 self.call_args.append('--xunit-file')
233 if not have['pymongo']:
370 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
234 sec.exclude('controller.mongodb')
371 xunit_file = path.abspath(sect+'.xunit.xml')
235 sec.exclude('tests.test_mongodb')
372 if sys.platform == 'win32':
236
373 xunit_file = '"%s"' % xunit_file
237 # kernel:
374 self.call_args.append(xunit_file)
238 sec = test_sections['kernel']
375
239 sec.requires('zmq')
376 if '--with-xml-coverage' in self.call_args:
240 # The in-process kernel tests are done in a separate section
377 self.coverage_xml = path.abspath(sect+".coverage.xml")
241 sec.exclude('inprocess')
378 self.call_args.remove('--with-xml-coverage')
242 # importing gtk sets the default encoding, which we want to avoid
379 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
243 sec.exclude('zmq.gui.gtkembed')
380
244 if not have['matplotlib']:
381 # Store anything we start to clean up on deletion
245 sec.exclude('zmq.pylab')
382 self.processes = []
246
383
247 # kernel.inprocess:
384 def _run_cmd(self):
248 test_sections['kernel.inprocess'].requires('zmq')
385 with TemporaryDirectory() as IPYTHONDIR:
249
386 env = os.environ.copy()
250 # extensions:
387 env['IPYTHONDIR'] = IPYTHONDIR
251 sec = test_sections['extensions']
388 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
252 if not have['cython']:
389 output = subprocess.PIPE if self.buffer_output else None
253 sec.exclude('cythonmagic')
390 subp = subprocess.Popen(self.call_args, stdout=output,
254 sec.exclude('tests.test_cythonmagic')
391 stderr=output, env=env)
255 if not have['oct2py']:
392 self.processes.append(subp)
256 sec.exclude('octavemagic')
393 # If this fails, the process will be left in self.processes and
257 sec.exclude('tests.test_octavemagic')
394 # cleaned up later, but if the wait call succeeds, then we can
258 if not have['rpy2'] or not have['numpy']:
395 # clear the stored process.
259 sec.exclude('rmagic')
396 retcode = subp.wait()
260 sec.exclude('tests.test_rmagic')
397 self.processes.pop()
261 # autoreload does some strange stuff, so move it to its own test section
398 self.stdout = subp.stdout
262 sec.exclude('autoreload')
399 self.stderr = subp.stderr
263 sec.exclude('tests.test_autoreload')
400 return retcode
264 test_sections['autoreload'] = TestSection('autoreload',
401
265 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
402 def run(self):
266 test_group_names.append('autoreload')
403 """Run the stored commands"""
267
404 try:
268 # qt:
405 retcode = self._run_cmd()
269 test_sections['qt'].requires('zmq', 'qt', 'pygments')
406 except KeyboardInterrupt:
270
407 return -signal.SIGINT
271 # html:
408 except:
272 sec = test_sections['html']
409 import traceback
273 sec.requires('zmq', 'tornado')
410 traceback.print_exc()
274 # The notebook 'static' directory contains JS, css and other
411 return 1 # signal failure
275 # files for web serving. Occasionally projects may put a .py
412
276 # file in there (MathJax ships a conf.py), so we might as
413 if self.coverage_xml:
277 # well play it safe and skip the whole thing.
414 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
278 sec.exclude('static')
415 return retcode
279 sec.exclude('fabfile')
416
280 if not have['jinja2']:
417 def __del__(self):
281 sec.exclude('notebookapp')
418 """Cleanup on exit by killing any leftover processes."""
282 if not have['azure']:
419 for subp in self.processes:
283 sec.exclude('services.notebooks.azurenbmanager')
420 if subp.poll() is not None:
284
421 continue # process is already dead
285 # config:
422
286 # Config files aren't really importable stand-alone
423 try:
287 test_sections['config'].exclude('profile')
424 print('Cleaning up stale PID: %d' % subp.pid)
288
425 subp.kill()
289 # nbconvert:
426 except: # (OSError, WindowsError) ?
290 sec = test_sections['nbconvert']
427 # This is just a best effort, if we fail or the process was
291 sec.requires('pygments', 'jinja2', 'sphinx')
428 # really gone, ignore it.
292 # Exclude nbconvert directories containing config files used to test.
429 pass
293 # Executing the config files with iptest would cause an exception.
430 else:
294 sec.exclude('tests.files')
431 for i in range(10):
295 sec.exclude('exporters.tests.files')
432 if subp.poll() is None:
296 if not have['tornado']:
433 time.sleep(0.1)
297 sec.exclude('nbconvert.post_processors.serve')
434 else:
298 sec.exclude('nbconvert.post_processors.tests.test_serve')
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')
465
299
466 if all((have['pygments'], have['jinja2'], have['sphinx'])):
300 #-----------------------------------------------------------------------------
467 nose_pkg_names.append('nbconvert')
301 # Functions and classes
302 #-----------------------------------------------------------------------------
468
303
469 # For debugging this code, only load quick stuff
304 def check_exclusions_exist():
470 #nose_pkg_names = ['core', 'extensions'] # dbg
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
315 class ExclusionPlugin(Plugin):
476 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
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:
340 def configure(self, options, config):
479 runners.append((name, IPTester('iptest', params=name)))
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 def run_iptest():
360 def run_iptest():
@@ -495,11 +371,16 b' def run_iptest():'
495 warnings.filterwarnings('ignore',
371 warnings.filterwarnings('ignore',
496 'This will be removed soon. Use IPython.testing.util instead')
372 'This will be removed soon. Use IPython.testing.util instead')
497
373
498 if sys.argv[1] in special_test_suites:
374 arg1 = sys.argv[1]
499 sys.argv[1:2] = special_test_suites[sys.argv[1]]
375 if arg1 in test_sections:
500 special_suite = True
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 else:
381 else:
502 special_suite = False
382 section = TestSection(arg1, includes=[arg1])
383
503
384
504 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
385 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
505
386
@@ -530,8 +411,11 b' def run_iptest():'
530
411
531 # use our plugin for doctesting. It will remove the standard doctest plugin
412 # use our plugin for doctesting. It will remove the standard doctest plugin
532 # if it finds it enabled
413 # if it finds it enabled
533 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
414 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
534 plugins = [ipdt, 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 # We need a global ipython running in this process, but the special
420 # We need a global ipython running in this process, but the special
537 # in-process group spawns its own IPython kernels, so for *that* group we
421 # in-process group spawns its own IPython kernels, so for *that* group we
@@ -540,117 +424,13 b' def run_iptest():'
540 # assumptions about what needs to be a singleton and what doesn't (app
424 # assumptions about what needs to be a singleton and what doesn't (app
541 # objects should, individual shells shouldn't). But for now, this
425 # objects should, individual shells shouldn't). But for now, this
542 # workaround allows the test suite for the inprocess module to complete.
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 globalipapp.start_ipython()
429 globalipapp.start_ipython()
545
430
546 # Now nose can run
431 # Now nose can run
547 TestProgram(argv=argv, addplugins=plugins)
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 if __name__ == '__main__':
434 if __name__ == '__main__':
656 main()
435 run_iptest()
436
@@ -597,23 +597,6 b' class ExtensionDoctest(doctests.Doctest):'
597 name = 'extdoctest' # call nosetests with --with-extdoctest
597 name = 'extdoctest' # call nosetests with --with-extdoctest
598 enabled = True
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 def options(self, parser, env=os.environ):
600 def options(self, parser, env=os.environ):
618 Plugin.options(self, parser, env)
601 Plugin.options(self, parser, env)
619 parser.add_option('--doctest-tests', action='store_true',
602 parser.add_option('--doctest-tests', action='store_true',
@@ -716,35 +699,6 b' class ExtensionDoctest(doctests.Doctest):'
716 else:
699 else:
717 yield False # no tests to load
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 class IPythonDoctest(ExtensionDoctest):
703 class IPythonDoctest(ExtensionDoctest):
750 """Nose Plugin that supports doctests in extension modules.
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 from __future__ import print_function
5 from __future__ import print_function
6
6
7 import os as _os
7 import os as _os
8 import warnings as _warnings
9 import sys as _sys
8
10
9 # This code should only be used in Python versions < 3.2, since after that we
11 # This code should only be used in Python versions < 3.2, since after that we
10 # can rely on the stdlib itself.
12 # can rely on the stdlib itself.
@@ -49,7 +51,7 b' except ImportError:'
49 self._closed = True
51 self._closed = True
50 if _warn:
52 if _warn:
51 self._warn("Implicitly cleaning up {!r}".format(self),
53 self._warn("Implicitly cleaning up {!r}".format(self),
52 ResourceWarning)
54 Warning)
53
55
54 def __exit__(self, exc, value, tb):
56 def __exit__(self, exc, value, tb):
55 self.cleanup()
57 self.cleanup()
@@ -69,6 +71,7 b' except ImportError:'
69 _remove = staticmethod(_os.remove)
71 _remove = staticmethod(_os.remove)
70 _rmdir = staticmethod(_os.rmdir)
72 _rmdir = staticmethod(_os.rmdir)
71 _os_error = _os.error
73 _os_error = _os.error
74 _warn = _warnings.warn
72
75
73 def _rmtree(self, path):
76 def _rmtree(self, path):
74 # Essentially a stripped down version of shutil.rmtree. We can't
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 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
323 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
324 'iplogger%s = IPython.parallel.apps.iploggerapp:launch_new_instance',
324 'iplogger%s = IPython.parallel.apps.iploggerapp:launch_new_instance',
325 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
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 'irunner%s = IPython.lib.irunner:main',
327 'irunner%s = IPython.lib.irunner:main',
328 ]]
328 ]]
329 gui_scripts = []
329 gui_scripts = []
General Comments 0
You need to be logged in to leave comments. Login now