##// END OF EJS Templates
Split out iptestcontroller to control test process.
Thomas Kluyver -
Show More
@@ -0,0 +1,319 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 multiprocessing.pool
22 import os
23 import signal
24 import sys
25 import subprocess
26 import tempfile
27 import time
28
29 from .iptest import have, special_test_suites
30 from IPython.utils import py3compat
31 from IPython.utils.path import get_ipython_module_path
32 from IPython.utils.process import pycmd2argv
33 from IPython.utils.sysinfo import sys_info
34 from IPython.utils.tempdir import TemporaryDirectory
35
36
37 class IPTester(object):
38 """Call that calls iptest or trial in a subprocess.
39 """
40 #: string, name of test runner that will be called
41 runner = None
42 #: list, parameters for test runner
43 params = None
44 #: list, arguments of system call to be made to call test runner
45 call_args = None
46 #: list, subprocesses we start (for cleanup)
47 processes = None
48 #: str, coverage xml output file
49 coverage_xml = None
50 buffer_output = False
51
52 def __init__(self, runner='iptest', params=None):
53 """Create new test runner."""
54 if runner == 'iptest':
55 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
56 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
57 else:
58 raise Exception('Not a valid test runner: %s' % repr(runner))
59 if params is None:
60 params = []
61 if isinstance(params, str):
62 params = [params]
63 self.params = params
64
65 # Assemble call
66 self.call_args = self.runner+self.params
67
68 # Find the section we're testing (IPython.foo)
69 for sect in self.params:
70 if sect.startswith('IPython') or sect in special_test_suites: break
71 else:
72 raise ValueError("Section not found", self.params)
73
74 if '--with-xunit' in self.call_args:
75
76 self.call_args.append('--xunit-file')
77 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
78 xunit_file = os.path.abspath(sect+'.xunit.xml')
79 if sys.platform == 'win32':
80 xunit_file = '"%s"' % xunit_file
81 self.call_args.append(xunit_file)
82
83 if '--with-xml-coverage' in self.call_args:
84 self.coverage_xml = os.path.abspath(sect+".coverage.xml")
85 self.call_args.remove('--with-xml-coverage')
86 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
87
88 # Store anything we start to clean up on deletion
89 self.processes = []
90
91 def _run_cmd(self):
92 with TemporaryDirectory() as IPYTHONDIR:
93 env = os.environ.copy()
94 env['IPYTHONDIR'] = IPYTHONDIR
95 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
96 output = subprocess.PIPE if self.buffer_output else None
97 subp = subprocess.Popen(self.call_args, stdout=output,
98 stderr=output, env=env)
99 self.processes.append(subp)
100 # If this fails, the process will be left in self.processes and
101 # cleaned up later, but if the wait call succeeds, then we can
102 # clear the stored process.
103 retcode = subp.wait()
104 self.processes.pop()
105 self.stdout = subp.stdout
106 self.stderr = subp.stderr
107 return retcode
108
109 def run(self):
110 """Run the stored commands"""
111 try:
112 retcode = self._run_cmd()
113 except KeyboardInterrupt:
114 return -signal.SIGINT
115 except:
116 import traceback
117 traceback.print_exc()
118 return 1 # signal failure
119
120 if self.coverage_xml:
121 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
122 return retcode
123
124 def __del__(self):
125 """Cleanup on exit by killing any leftover processes."""
126 for subp in self.processes:
127 if subp.poll() is not None:
128 continue # process is already dead
129
130 try:
131 print('Cleaning up stale PID: %d' % subp.pid)
132 subp.kill()
133 except: # (OSError, WindowsError) ?
134 # This is just a best effort, if we fail or the process was
135 # really gone, ignore it.
136 pass
137 else:
138 for i in range(10):
139 if subp.poll() is None:
140 time.sleep(0.1)
141 else:
142 break
143
144 if subp.poll() is None:
145 # The process did not die...
146 print('... failed. Manual cleanup may be required.')
147
148 def make_runners(inc_slow=False):
149 """Define the top-level packages that need to be tested.
150 """
151
152 # Packages to be tested via nose, that only depend on the stdlib
153 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
154 'testing', 'utils', 'nbformat']
155
156 if have['qt']:
157 nose_pkg_names.append('qt')
158
159 if have['tornado']:
160 nose_pkg_names.append('html')
161
162 if have['zmq']:
163 nose_pkg_names.insert(0, 'kernel')
164 nose_pkg_names.insert(1, 'kernel.inprocess')
165 if inc_slow:
166 nose_pkg_names.insert(0, 'parallel')
167
168 if all((have['pygments'], have['jinja2'], have['sphinx'])):
169 nose_pkg_names.append('nbconvert')
170
171 # For debugging this code, only load quick stuff
172 #nose_pkg_names = ['core', 'extensions'] # dbg
173
174 # Make fully qualified package names prepending 'IPython.' to our name lists
175 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
176
177 # Make runners
178 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
179
180 for name in special_test_suites:
181 runners.append((name, IPTester('iptest', params=name)))
182
183 return runners
184
185 def do_run(x):
186 print('IPython test group:',x[0])
187 ret = x[1].run()
188 return ret
189
190 def report():
191 """Return a string with a summary report of test-related variables."""
192
193 out = [ sys_info(), '\n']
194
195 avail = []
196 not_avail = []
197
198 for k, is_avail in have.items():
199 if is_avail:
200 avail.append(k)
201 else:
202 not_avail.append(k)
203
204 if avail:
205 out.append('\nTools and libraries available at test time:\n')
206 avail.sort()
207 out.append(' ' + ' '.join(avail)+'\n')
208
209 if not_avail:
210 out.append('\nTools and libraries NOT available at test time:\n')
211 not_avail.sort()
212 out.append(' ' + ' '.join(not_avail)+'\n')
213
214 return ''.join(out)
215
216 def run_iptestall(inc_slow=False, fast=False):
217 """Run the entire IPython test suite by calling nose and trial.
218
219 This function constructs :class:`IPTester` instances for all IPython
220 modules and package and then runs each of them. This causes the modules
221 and packages of IPython to be tested each in their own subprocess using
222 nose.
223
224 Parameters
225 ----------
226
227 inc_slow : bool, optional
228 Include slow tests, like IPython.parallel. By default, these tests aren't
229 run.
230
231 fast : bool, option
232 Run the test suite in parallel, if True, using as many threads as there
233 are processors
234 """
235 if fast:
236 p = multiprocessing.pool.ThreadPool()
237 else:
238 p = multiprocessing.pool.ThreadPool(1)
239
240 runners = make_runners(inc_slow=inc_slow)
241
242 # Run the test runners in a temporary dir so we can nuke it when finished
243 # to clean up any junk files left over by accident. This also makes it
244 # robust against being run in non-writeable directories by mistake, as the
245 # temp dir will always be user-writeable.
246 curdir = os.getcwdu()
247 testdir = tempfile.gettempdir()
248 os.chdir(testdir)
249
250 # Run all test runners, tracking execution time
251 failed = []
252 t_start = time.time()
253
254 try:
255 all_res = p.map(do_run, runners)
256 print('*'*70)
257 for ((name, runner), res) in zip(runners, all_res):
258 tgroup = 'IPython test group: ' + name
259 res_string = 'OK' if res == 0 else 'FAILED'
260 res_string = res_string.rjust(70 - len(tgroup), '.')
261 print(tgroup + res_string)
262 if res:
263 failed.append( (name, runner) )
264 if res == -signal.SIGINT:
265 print("Interrupted")
266 break
267 finally:
268 os.chdir(curdir)
269 t_end = time.time()
270 t_tests = t_end - t_start
271 nrunners = len(runners)
272 nfail = len(failed)
273 # summarize results
274 print()
275 print('*'*70)
276 print('Test suite completed for system with the following information:')
277 print(report())
278 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
279 print()
280 print('Status:')
281 if not failed:
282 print('OK')
283 else:
284 # If anything went wrong, point out what command to rerun manually to
285 # see the actual errors and individual summary
286 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
287 for name, failed_runner in failed:
288 print('-'*40)
289 print('Runner failed:',name)
290 print('You may wish to rerun this one individually, with:')
291 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
292 print(u' '.join(failed_call_args))
293 print()
294 # Ensure that our exit code indicates failure
295 sys.exit(1)
296
297
298 def main():
299 for arg in sys.argv[1:]:
300 if arg.startswith('IPython') or arg in special_test_suites:
301 from .iptest import run_iptest
302 # This is in-process
303 run_iptest()
304 else:
305 inc_slow = "--all" in sys.argv
306 if inc_slow:
307 sys.argv.remove("--all")
308
309 fast = "--fast" in sys.argv
310 if fast:
311 sys.argv.remove("--fast")
312 IPTester.buffer_output = True
313
314 # This starts subprocesses
315 run_iptestall(inc_slow=inc_slow, fast=fast)
316
317
318 if __name__ == '__main__':
319 main()
@@ -1,24 +1,24 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 """IPython Test Suite Runner.
3 """IPython Test Suite Runner.
4 """
4 """
5
5
6 # The tests can't even run if nose isn't available, so might as well give the
6 # The tests can't even run if nose isn't available, so might as well give the
7 # user a civilized error message in that case.
7 # user a civilized error message in that case.
8
8
9 try:
9 try:
10 import nose
10 import nose
11 except ImportError:
11 except ImportError:
12 error = """\
12 error = """\
13 ERROR: The IPython test suite requires nose to run.
13 ERROR: The IPython test suite requires nose to run.
14
14
15 Please install nose on your system first and try again.
15 Please install nose on your system first and try again.
16 For information on installing nose, see:
16 For information on installing nose, see:
17 http://somethingaboutorange.com/mrl/projects/nose
17 http://somethingaboutorange.com/mrl/projects/nose
18
18
19 Exiting."""
19 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()
@@ -1,656 +1,365 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
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
34 import sys
33 import sys
35 import subprocess
36 import tempfile
37 import time
38 import warnings
34 import warnings
39 import multiprocessing.pool
40
35
41 # Now, proceed to import nose itself
36 # Now, proceed to import nose itself
42 import nose.plugins.builtin
37 import nose.plugins.builtin
43 from nose.plugins.xunit import Xunit
38 from nose.plugins.xunit import Xunit
44 from nose import SkipTest
39 from nose import SkipTest
45 from nose.core import TestProgram
40 from nose.core import TestProgram
46
41
47 # Our own imports
42 # Our own imports
48 from IPython.utils import py3compat
49 from IPython.utils.importstring import import_item
43 from IPython.utils.importstring import import_item
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
44 from IPython.utils.path import 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
45 from IPython.utils.warn import warn
55
46
56 from IPython.testing import globalipapp
47 from IPython.testing import globalipapp
57 from IPython.testing.plugin.ipdoctest import IPythonDoctest
48 from IPython.testing.plugin.ipdoctest import IPythonDoctest
58 from IPython.external.decorators import KnownFailure, knownfailureif
49 from IPython.external.decorators import KnownFailure, knownfailureif
59
50
60 pjoin = path.join
51 pjoin = path.join
61
52
62
53
63 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
64 # Globals
55 # Globals
65 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
66
57
67
58
68 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
69 # Warnings control
60 # Warnings control
70 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
71
62
72 # Twisted generates annoying warnings with Python 2.6, as will do other code
63 # Twisted generates annoying warnings with Python 2.6, as will do other code
73 # that imports 'sets' as of today
64 # that imports 'sets' as of today
74 warnings.filterwarnings('ignore', 'the sets module is deprecated',
65 warnings.filterwarnings('ignore', 'the sets module is deprecated',
75 DeprecationWarning )
66 DeprecationWarning )
76
67
77 # This one also comes from Twisted
68 # This one also comes from Twisted
78 warnings.filterwarnings('ignore', 'the sha module is deprecated',
69 warnings.filterwarnings('ignore', 'the sha module is deprecated',
79 DeprecationWarning)
70 DeprecationWarning)
80
71
81 # Wx on Fedora11 spits these out
72 # Wx on Fedora11 spits these out
82 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
73 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
83 UserWarning)
74 UserWarning)
84
75
85 # ------------------------------------------------------------------------------
76 # ------------------------------------------------------------------------------
86 # Monkeypatch Xunit to count known failures as skipped.
77 # Monkeypatch Xunit to count known failures as skipped.
87 # ------------------------------------------------------------------------------
78 # ------------------------------------------------------------------------------
88 def monkeypatch_xunit():
79 def monkeypatch_xunit():
89 try:
80 try:
90 knownfailureif(True)(lambda: None)()
81 knownfailureif(True)(lambda: None)()
91 except Exception as e:
82 except Exception as e:
92 KnownFailureTest = type(e)
83 KnownFailureTest = type(e)
93
84
94 def addError(self, test, err, capt=None):
85 def addError(self, test, err, capt=None):
95 if issubclass(err[0], KnownFailureTest):
86 if issubclass(err[0], KnownFailureTest):
96 err = (SkipTest,) + err[1:]
87 err = (SkipTest,) + err[1:]
97 return self.orig_addError(test, err, capt)
88 return self.orig_addError(test, err, capt)
98
89
99 Xunit.orig_addError = Xunit.addError
90 Xunit.orig_addError = Xunit.addError
100 Xunit.addError = addError
91 Xunit.addError = addError
101
92
102 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
103 # Logic for skipping doctests
94 # Logic for skipping doctests
104 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
105 def extract_version(mod):
96 def extract_version(mod):
106 return mod.__version__
97 return mod.__version__
107
98
108 def test_for(item, min_version=None, callback=extract_version):
99 def test_for(item, min_version=None, callback=extract_version):
109 """Test to see if item is importable, and optionally check against a minimum
100 """Test to see if item is importable, and optionally check against a minimum
110 version.
101 version.
111
102
112 If min_version is given, the default behavior is to check against the
103 If min_version is given, the default behavior is to check against the
113 `__version__` attribute of the item, but specifying `callback` allows you to
104 `__version__` attribute of the item, but specifying `callback` allows you to
114 extract the value you are interested in. e.g::
105 extract the value you are interested in. e.g::
115
106
116 In [1]: import sys
107 In [1]: import sys
117
108
118 In [2]: from IPython.testing.iptest import test_for
109 In [2]: from IPython.testing.iptest import test_for
119
110
120 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
111 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
121 Out[3]: True
112 Out[3]: True
122
113
123 """
114 """
124 try:
115 try:
125 check = import_item(item)
116 check = import_item(item)
126 except (ImportError, RuntimeError):
117 except (ImportError, RuntimeError):
127 # GTK reports Runtime error if it can't be initialized even if it's
118 # GTK reports Runtime error if it can't be initialized even if it's
128 # importable.
119 # importable.
129 return False
120 return False
130 else:
121 else:
131 if min_version:
122 if min_version:
132 if callback:
123 if callback:
133 # extra processing step to get version to compare
124 # extra processing step to get version to compare
134 check = callback(check)
125 check = callback(check)
135
126
136 return check >= min_version
127 return check >= min_version
137 else:
128 else:
138 return True
129 return True
139
130
140 # Global dict where we can store information on what we have and what we don't
131 # Global dict where we can store information on what we have and what we don't
141 # have available at test run time
132 # have available at test run time
142 have = {}
133 have = {}
143
134
144 have['curses'] = test_for('_curses')
135 have['curses'] = test_for('_curses')
145 have['matplotlib'] = test_for('matplotlib')
136 have['matplotlib'] = test_for('matplotlib')
146 have['numpy'] = test_for('numpy')
137 have['numpy'] = test_for('numpy')
147 have['pexpect'] = test_for('IPython.external.pexpect')
138 have['pexpect'] = test_for('IPython.external.pexpect')
148 have['pymongo'] = test_for('pymongo')
139 have['pymongo'] = test_for('pymongo')
149 have['pygments'] = test_for('pygments')
140 have['pygments'] = test_for('pygments')
150 have['qt'] = test_for('IPython.external.qt')
141 have['qt'] = test_for('IPython.external.qt')
151 have['rpy2'] = test_for('rpy2')
142 have['rpy2'] = test_for('rpy2')
152 have['sqlite3'] = test_for('sqlite3')
143 have['sqlite3'] = test_for('sqlite3')
153 have['cython'] = test_for('Cython')
144 have['cython'] = test_for('Cython')
154 have['oct2py'] = test_for('oct2py')
145 have['oct2py'] = test_for('oct2py')
155 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
146 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
156 have['jinja2'] = test_for('jinja2')
147 have['jinja2'] = test_for('jinja2')
157 have['wx'] = test_for('wx')
148 have['wx'] = test_for('wx')
158 have['wx.aui'] = test_for('wx.aui')
149 have['wx.aui'] = test_for('wx.aui')
159 have['azure'] = test_for('azure')
150 have['azure'] = test_for('azure')
160 have['sphinx'] = test_for('sphinx')
151 have['sphinx'] = test_for('sphinx')
161
152
162 min_zmq = (2,1,11)
153 min_zmq = (2,1,11)
163
154
164 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
165
156
166 #-----------------------------------------------------------------------------
157 #-----------------------------------------------------------------------------
167 # Functions and classes
158 # Functions and classes
168 #-----------------------------------------------------------------------------
159 #-----------------------------------------------------------------------------
169
160
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
161
197 def make_exclude():
162 def make_exclude():
198 """Make patterns of modules and packages to exclude from testing.
163 """Make patterns of modules and packages to exclude from testing.
199
164
200 For the IPythonDoctest plugin, we need to exclude certain patterns that
165 For the IPythonDoctest plugin, we need to exclude certain patterns that
201 cause testing problems. We should strive to minimize the number of
166 cause testing problems. We should strive to minimize the number of
202 skipped modules, since this means untested code.
167 skipped modules, since this means untested code.
203
168
204 These modules and packages will NOT get scanned by nose at all for tests.
169 These modules and packages will NOT get scanned by nose at all for tests.
205 """
170 """
206 # Simple utility to make IPython paths more readably, we need a lot of
171 # Simple utility to make IPython paths more readably, we need a lot of
207 # these below
172 # these below
208 ipjoin = lambda *paths: pjoin('IPython', *paths)
173 ipjoin = lambda *paths: pjoin('IPython', *paths)
209
174
210 exclusions = [ipjoin('external'),
175 exclusions = [ipjoin('external'),
211 ipjoin('quarantine'),
176 ipjoin('quarantine'),
212 ipjoin('deathrow'),
177 ipjoin('deathrow'),
213 # This guy is probably attic material
178 # This guy is probably attic material
214 ipjoin('testing', 'mkdoctests'),
179 ipjoin('testing', 'mkdoctests'),
215 # Testing inputhook will need a lot of thought, to figure out
180 # 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
181 # how to have tests that don't lock up with the gui event
217 # loops in the picture
182 # loops in the picture
218 ipjoin('lib', 'inputhook'),
183 ipjoin('lib', 'inputhook'),
219 # Config files aren't really importable stand-alone
184 # Config files aren't really importable stand-alone
220 ipjoin('config', 'profile'),
185 ipjoin('config', 'profile'),
221 # The notebook 'static' directory contains JS, css and other
186 # The notebook 'static' directory contains JS, css and other
222 # files for web serving. Occasionally projects may put a .py
187 # files for web serving. Occasionally projects may put a .py
223 # file in there (MathJax ships a conf.py), so we might as
188 # file in there (MathJax ships a conf.py), so we might as
224 # well play it safe and skip the whole thing.
189 # well play it safe and skip the whole thing.
225 ipjoin('html', 'static'),
190 ipjoin('html', 'static'),
226 ipjoin('html', 'fabfile'),
191 ipjoin('html', 'fabfile'),
227 ]
192 ]
228 if not have['sqlite3']:
193 if not have['sqlite3']:
229 exclusions.append(ipjoin('core', 'tests', 'test_history'))
194 exclusions.append(ipjoin('core', 'tests', 'test_history'))
230 exclusions.append(ipjoin('core', 'history'))
195 exclusions.append(ipjoin('core', 'history'))
231 if not have['wx']:
196 if not have['wx']:
232 exclusions.append(ipjoin('lib', 'inputhookwx'))
197 exclusions.append(ipjoin('lib', 'inputhookwx'))
233
198
234 if 'IPython.kernel.inprocess' not in sys.argv:
199 if 'IPython.kernel.inprocess' not in sys.argv:
235 exclusions.append(ipjoin('kernel', 'inprocess'))
200 exclusions.append(ipjoin('kernel', 'inprocess'))
236
201
237 # FIXME: temporarily disable autoreload tests, as they can produce
202 # FIXME: temporarily disable autoreload tests, as they can produce
238 # spurious failures in subsequent tests (cythonmagic).
203 # spurious failures in subsequent tests (cythonmagic).
239 exclusions.append(ipjoin('extensions', 'autoreload'))
204 exclusions.append(ipjoin('extensions', 'autoreload'))
240 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
205 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
241
206
242 # We do this unconditionally, so that the test suite doesn't import
207 # We do this unconditionally, so that the test suite doesn't import
243 # gtk, changing the default encoding and masking some unicode bugs.
208 # gtk, changing the default encoding and masking some unicode bugs.
244 exclusions.append(ipjoin('lib', 'inputhookgtk'))
209 exclusions.append(ipjoin('lib', 'inputhookgtk'))
245 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
210 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
246
211
247 #Also done unconditionally, exclude nbconvert directories containing
212 #Also done unconditionally, exclude nbconvert directories containing
248 #config files used to test. Executing the config files with iptest would
213 #config files used to test. Executing the config files with iptest would
249 #cause an exception.
214 #cause an exception.
250 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
215 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
251 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
216 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
252
217
253 # These have to be skipped on win32 because the use echo, rm, cd, etc.
218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
254 # See ticket https://github.com/ipython/ipython/issues/87
219 # See ticket https://github.com/ipython/ipython/issues/87
255 if sys.platform == 'win32':
220 if sys.platform == 'win32':
256 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
257 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
258
223
259 if not have['pexpect']:
224 if not have['pexpect']:
260 exclusions.extend([ipjoin('lib', 'irunner'),
225 exclusions.extend([ipjoin('lib', 'irunner'),
261 ipjoin('lib', 'tests', 'test_irunner'),
226 ipjoin('lib', 'tests', 'test_irunner'),
262 ipjoin('terminal', 'console'),
227 ipjoin('terminal', 'console'),
263 ])
228 ])
264
229
265 if not have['zmq']:
230 if not have['zmq']:
266 exclusions.append(ipjoin('lib', 'kernel'))
231 exclusions.append(ipjoin('lib', 'kernel'))
267 exclusions.append(ipjoin('kernel'))
232 exclusions.append(ipjoin('kernel'))
268 exclusions.append(ipjoin('qt'))
233 exclusions.append(ipjoin('qt'))
269 exclusions.append(ipjoin('html'))
234 exclusions.append(ipjoin('html'))
270 exclusions.append(ipjoin('consoleapp.py'))
235 exclusions.append(ipjoin('consoleapp.py'))
271 exclusions.append(ipjoin('terminal', 'console'))
236 exclusions.append(ipjoin('terminal', 'console'))
272 exclusions.append(ipjoin('parallel'))
237 exclusions.append(ipjoin('parallel'))
273 elif not have['qt'] or not have['pygments']:
238 elif not have['qt'] or not have['pygments']:
274 exclusions.append(ipjoin('qt'))
239 exclusions.append(ipjoin('qt'))
275
240
276 if not have['pymongo']:
241 if not have['pymongo']:
277 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
278 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
279
244
280 if not have['matplotlib']:
245 if not have['matplotlib']:
281 exclusions.extend([ipjoin('core', 'pylabtools'),
246 exclusions.extend([ipjoin('core', 'pylabtools'),
282 ipjoin('core', 'tests', 'test_pylabtools'),
247 ipjoin('core', 'tests', 'test_pylabtools'),
283 ipjoin('kernel', 'zmq', 'pylab'),
248 ipjoin('kernel', 'zmq', 'pylab'),
284 ])
249 ])
285
250
286 if not have['cython']:
251 if not have['cython']:
287 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
252 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
288 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
253 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
289
254
290 if not have['oct2py']:
255 if not have['oct2py']:
291 exclusions.extend([ipjoin('extensions', 'octavemagic')])
256 exclusions.extend([ipjoin('extensions', 'octavemagic')])
292 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
257 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
293
258
294 if not have['tornado']:
259 if not have['tornado']:
295 exclusions.append(ipjoin('html'))
260 exclusions.append(ipjoin('html'))
296 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
261 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
297 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
262 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
298
263
299 if not have['jinja2']:
264 if not have['jinja2']:
300 exclusions.append(ipjoin('html', 'notebookapp'))
265 exclusions.append(ipjoin('html', 'notebookapp'))
301
266
302 if not have['rpy2'] or not have['numpy']:
267 if not have['rpy2'] or not have['numpy']:
303 exclusions.append(ipjoin('extensions', 'rmagic'))
268 exclusions.append(ipjoin('extensions', 'rmagic'))
304 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
269 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
305
270
306 if not have['azure']:
271 if not have['azure']:
307 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
272 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
308
273
309 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
274 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
310 exclusions.append(ipjoin('nbconvert'))
275 exclusions.append(ipjoin('nbconvert'))
311
276
312 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
277 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
313 if sys.platform == 'win32':
278 if sys.platform == 'win32':
314 exclusions = [s.replace('\\','\\\\') for s in exclusions]
279 exclusions = [s.replace('\\','\\\\') for s in exclusions]
315
280
316 # check for any exclusions that don't seem to exist:
281 # check for any exclusions that don't seem to exist:
317 parent, _ = os.path.split(get_ipython_package_dir())
282 parent, _ = os.path.split(get_ipython_package_dir())
318 for exclusion in exclusions:
283 for exclusion in exclusions:
319 if exclusion.endswith(('deathrow', 'quarantine')):
284 if exclusion.endswith(('deathrow', 'quarantine')):
320 # ignore deathrow/quarantine, which exist in dev, but not install
285 # ignore deathrow/quarantine, which exist in dev, but not install
321 continue
286 continue
322 fullpath = pjoin(parent, exclusion)
287 fullpath = pjoin(parent, exclusion)
323 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
288 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
324 warn("Excluding nonexistent file: %r" % exclusion)
289 warn("Excluding nonexistent file: %r" % exclusion)
325
290
326 return exclusions
291 return exclusions
327
292
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 = {
293 special_test_suites = {
443 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
444 }
295 }
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
466 if all((have['pygments'], have['jinja2'], have['sphinx'])):
467 nose_pkg_names.append('nbconvert')
468
469 # For debugging this code, only load quick stuff
470 #nose_pkg_names = ['core', 'extensions'] # dbg
471
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
475 # Make runners
476 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
477
478 for name in special_test_suites:
479 runners.append((name, IPTester('iptest', params=name)))
480
481 return runners
482
296
483
297
484 def run_iptest():
298 def run_iptest():
485 """Run the IPython test suite using nose.
299 """Run the IPython test suite using nose.
486
300
487 This function is called when this script is **not** called with the form
301 This function is called when this script is **not** called with the form
488 `iptest all`. It simply calls nose with appropriate command line flags
302 `iptest all`. It simply calls nose with appropriate command line flags
489 and accepts all of the standard nose arguments.
303 and accepts all of the standard nose arguments.
490 """
304 """
491 # Apply our monkeypatch to Xunit
305 # Apply our monkeypatch to Xunit
492 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
306 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
493 monkeypatch_xunit()
307 monkeypatch_xunit()
494
308
495 warnings.filterwarnings('ignore',
309 warnings.filterwarnings('ignore',
496 'This will be removed soon. Use IPython.testing.util instead')
310 'This will be removed soon. Use IPython.testing.util instead')
497
311
498 if sys.argv[1] in special_test_suites:
312 if sys.argv[1] in special_test_suites:
499 sys.argv[1:2] = special_test_suites[sys.argv[1]]
313 sys.argv[1:2] = special_test_suites[sys.argv[1]]
500 special_suite = True
314 special_suite = True
501 else:
315 else:
502 special_suite = False
316 special_suite = False
503
317
504 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
318 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
505
319
506 '--with-ipdoctest',
320 '--with-ipdoctest',
507 '--ipdoctest-tests','--ipdoctest-extension=txt',
321 '--ipdoctest-tests','--ipdoctest-extension=txt',
508
322
509 # We add --exe because of setuptools' imbecility (it
323 # We add --exe because of setuptools' imbecility (it
510 # blindly does chmod +x on ALL files). Nose does the
324 # blindly does chmod +x on ALL files). Nose does the
511 # right thing and it tries to avoid executables,
325 # right thing and it tries to avoid executables,
512 # setuptools unfortunately forces our hand here. This
326 # setuptools unfortunately forces our hand here. This
513 # has been discussed on the distutils list and the
327 # has been discussed on the distutils list and the
514 # setuptools devs refuse to fix this problem!
328 # setuptools devs refuse to fix this problem!
515 '--exe',
329 '--exe',
516 ]
330 ]
517 if '-a' not in argv and '-A' not in argv:
331 if '-a' not in argv and '-A' not in argv:
518 argv = argv + ['-a', '!crash']
332 argv = argv + ['-a', '!crash']
519
333
520 if nose.__version__ >= '0.11':
334 if nose.__version__ >= '0.11':
521 # I don't fully understand why we need this one, but depending on what
335 # I don't fully understand why we need this one, but depending on what
522 # directory the test suite is run from, if we don't give it, 0 tests
336 # directory the test suite is run from, if we don't give it, 0 tests
523 # get run. Specifically, if the test suite is run from the source dir
337 # get run. Specifically, if the test suite is run from the source dir
524 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
338 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
525 # even if the same call done in this directory works fine). It appears
339 # even if the same call done in this directory works fine). It appears
526 # that if the requested package is in the current dir, nose bails early
340 # that if the requested package is in the current dir, nose bails early
527 # by default. Since it's otherwise harmless, leave it in by default
341 # by default. Since it's otherwise harmless, leave it in by default
528 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
342 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
529 argv.append('--traverse-namespace')
343 argv.append('--traverse-namespace')
530
344
531 # use our plugin for doctesting. It will remove the standard doctest plugin
345 # use our plugin for doctesting. It will remove the standard doctest plugin
532 # if it finds it enabled
346 # if it finds it enabled
533 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
347 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
534 plugins = [ipdt, KnownFailure()]
348 plugins = [ipdt, KnownFailure()]
535
349
536 # We need a global ipython running in this process, but the special
350 # 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
351 # in-process group spawns its own IPython kernels, so for *that* group we
538 # must avoid also opening the global one (otherwise there's a conflict of
352 # must avoid also opening the global one (otherwise there's a conflict of
539 # singletons). Ultimately the solution to this problem is to refactor our
353 # singletons). Ultimately the solution to this problem is to refactor our
540 # assumptions about what needs to be a singleton and what doesn't (app
354 # assumptions about what needs to be a singleton and what doesn't (app
541 # objects should, individual shells shouldn't). But for now, this
355 # objects should, individual shells shouldn't). But for now, this
542 # workaround allows the test suite for the inprocess module to complete.
356 # workaround allows the test suite for the inprocess module to complete.
543 if not 'IPython.kernel.inprocess' in sys.argv:
357 if not 'IPython.kernel.inprocess' in sys.argv:
544 globalipapp.start_ipython()
358 globalipapp.start_ipython()
545
359
546 # Now nose can run
360 # Now nose can run
547 TestProgram(argv=argv, addplugins=plugins)
361 TestProgram(argv=argv, addplugins=plugins)
548
362
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
363
655 if __name__ == '__main__':
364 if __name__ == '__main__':
656 main()
365 run_iptest()
General Comments 0
You need to be logged in to leave comments. Login now