##// END OF EJS Templates
Improve description of -j option to iptest
Thomas Kluyver -
Show More
@@ -1,568 +1,569 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 from multiprocessing import Process, Queue
23 from multiprocessing import Process, Queue
24 import os
24 import os
25 import shutil
25 import shutil
26 import signal
26 import signal
27 import sys
27 import sys
28 import subprocess
28 import subprocess
29 import time
29 import time
30
30
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 from IPython.utils.path import compress_user
32 from IPython.utils.path import compress_user
33 from IPython.utils.py3compat import bytes_to_str
33 from IPython.utils.py3compat import bytes_to_str
34 from IPython.utils.sysinfo import get_sys_info
34 from IPython.utils.sysinfo import get_sys_info
35 from IPython.utils.tempdir import TemporaryDirectory
35 from IPython.utils.tempdir import TemporaryDirectory
36
36
37
37
38 class TestController(object):
38 class TestController(object):
39 """Run tests in a subprocess
39 """Run tests in a subprocess
40 """
40 """
41 #: str, IPython test suite to be executed.
41 #: str, IPython test suite to be executed.
42 section = None
42 section = None
43 #: list, command line arguments to be executed
43 #: list, command line arguments to be executed
44 cmd = None
44 cmd = None
45 #: dict, extra environment variables to set for the subprocess
45 #: dict, extra environment variables to set for the subprocess
46 env = None
46 env = None
47 #: list, TemporaryDirectory instances to clear up when the process finishes
47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 dirs = None
48 dirs = None
49 #: subprocess.Popen instance
49 #: subprocess.Popen instance
50 process = None
50 process = None
51 #: str, process stdout+stderr
51 #: str, process stdout+stderr
52 stdout = None
52 stdout = None
53
53
54 def __init__(self):
54 def __init__(self):
55 self.cmd = []
55 self.cmd = []
56 self.env = {}
56 self.env = {}
57 self.dirs = []
57 self.dirs = []
58
58
59 def setup(self):
59 def setup(self):
60 """Create temporary directories etc.
60 """Create temporary directories etc.
61
61
62 This is only called when we know the test group will be run. Things
62 This is only called when we know the test group will be run. Things
63 created here may be cleaned up by self.cleanup().
63 created here may be cleaned up by self.cleanup().
64 """
64 """
65 pass
65 pass
66
66
67 def launch(self, buffer_output=False):
67 def launch(self, buffer_output=False):
68 # print('*** ENV:', self.env) # dbg
68 # print('*** ENV:', self.env) # dbg
69 # print('*** CMD:', self.cmd) # dbg
69 # print('*** CMD:', self.cmd) # dbg
70 env = os.environ.copy()
70 env = os.environ.copy()
71 env.update(self.env)
71 env.update(self.env)
72 output = subprocess.PIPE if buffer_output else None
72 output = subprocess.PIPE if buffer_output else None
73 stdout = subprocess.STDOUT if buffer_output else None
73 stdout = subprocess.STDOUT if buffer_output else None
74 self.process = subprocess.Popen(self.cmd, stdout=output,
74 self.process = subprocess.Popen(self.cmd, stdout=output,
75 stderr=stdout, env=env)
75 stderr=stdout, env=env)
76
76
77 def wait(self):
77 def wait(self):
78 self.stdout, _ = self.process.communicate()
78 self.stdout, _ = self.process.communicate()
79 return self.process.returncode
79 return self.process.returncode
80
80
81 def print_extra_info(self):
81 def print_extra_info(self):
82 """Print extra information about this test run.
82 """Print extra information about this test run.
83
83
84 If we're running in parallel and showing the concise view, this is only
84 If we're running in parallel and showing the concise view, this is only
85 called if the test group fails. Otherwise, it's called before the test
85 called if the test group fails. Otherwise, it's called before the test
86 group is started.
86 group is started.
87
87
88 The base implementation does nothing, but it can be overridden by
88 The base implementation does nothing, but it can be overridden by
89 subclasses.
89 subclasses.
90 """
90 """
91 return
91 return
92
92
93 def cleanup_process(self):
93 def cleanup_process(self):
94 """Cleanup on exit by killing any leftover processes."""
94 """Cleanup on exit by killing any leftover processes."""
95 subp = self.process
95 subp = self.process
96 if subp is None or (subp.poll() is not None):
96 if subp is None or (subp.poll() is not None):
97 return # Process doesn't exist, or is already dead.
97 return # Process doesn't exist, or is already dead.
98
98
99 try:
99 try:
100 print('Cleaning up stale PID: %d' % subp.pid)
100 print('Cleaning up stale PID: %d' % subp.pid)
101 subp.kill()
101 subp.kill()
102 except: # (OSError, WindowsError) ?
102 except: # (OSError, WindowsError) ?
103 # This is just a best effort, if we fail or the process was
103 # This is just a best effort, if we fail or the process was
104 # really gone, ignore it.
104 # really gone, ignore it.
105 pass
105 pass
106 else:
106 else:
107 for i in range(10):
107 for i in range(10):
108 if subp.poll() is None:
108 if subp.poll() is None:
109 time.sleep(0.1)
109 time.sleep(0.1)
110 else:
110 else:
111 break
111 break
112
112
113 if subp.poll() is None:
113 if subp.poll() is None:
114 # The process did not die...
114 # The process did not die...
115 print('... failed. Manual cleanup may be required.')
115 print('... failed. Manual cleanup may be required.')
116
116
117 def cleanup(self):
117 def cleanup(self):
118 "Kill process if it's still alive, and clean up temporary directories"
118 "Kill process if it's still alive, and clean up temporary directories"
119 self.cleanup_process()
119 self.cleanup_process()
120 for td in self.dirs:
120 for td in self.dirs:
121 td.cleanup()
121 td.cleanup()
122
122
123 __del__ = cleanup
123 __del__ = cleanup
124
124
125 class PyTestController(TestController):
125 class PyTestController(TestController):
126 """Run Python tests using IPython.testing.iptest"""
126 """Run Python tests using IPython.testing.iptest"""
127 #: str, Python command to execute in subprocess
127 #: str, Python command to execute in subprocess
128 pycmd = None
128 pycmd = None
129
129
130 def __init__(self, section):
130 def __init__(self, section):
131 """Create new test runner."""
131 """Create new test runner."""
132 TestController.__init__(self)
132 TestController.__init__(self)
133 self.section = section
133 self.section = section
134 # pycmd is put into cmd[2] in PyTestController.launch()
134 # pycmd is put into cmd[2] in PyTestController.launch()
135 self.cmd = [sys.executable, '-c', None, section]
135 self.cmd = [sys.executable, '-c', None, section]
136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
137
137
138 def setup(self):
138 def setup(self):
139 ipydir = TemporaryDirectory()
139 ipydir = TemporaryDirectory()
140 self.dirs.append(ipydir)
140 self.dirs.append(ipydir)
141 self.env['IPYTHONDIR'] = ipydir.name
141 self.env['IPYTHONDIR'] = ipydir.name
142 self.workingdir = workingdir = TemporaryDirectory()
142 self.workingdir = workingdir = TemporaryDirectory()
143 self.dirs.append(workingdir)
143 self.dirs.append(workingdir)
144 self.env['IPTEST_WORKING_DIR'] = workingdir.name
144 self.env['IPTEST_WORKING_DIR'] = workingdir.name
145 # This means we won't get odd effects from our own matplotlib config
145 # This means we won't get odd effects from our own matplotlib config
146 self.env['MPLCONFIGDIR'] = workingdir.name
146 self.env['MPLCONFIGDIR'] = workingdir.name
147
147
148 @property
148 @property
149 def will_run(self):
149 def will_run(self):
150 try:
150 try:
151 return test_sections[self.section].will_run
151 return test_sections[self.section].will_run
152 except KeyError:
152 except KeyError:
153 return True
153 return True
154
154
155 def add_xunit(self):
155 def add_xunit(self):
156 xunit_file = os.path.abspath(self.section + '.xunit.xml')
156 xunit_file = os.path.abspath(self.section + '.xunit.xml')
157 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
157 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
158
158
159 def add_coverage(self):
159 def add_coverage(self):
160 try:
160 try:
161 sources = test_sections[self.section].includes
161 sources = test_sections[self.section].includes
162 except KeyError:
162 except KeyError:
163 sources = ['IPython']
163 sources = ['IPython']
164
164
165 coverage_rc = ("[run]\n"
165 coverage_rc = ("[run]\n"
166 "data_file = {data_file}\n"
166 "data_file = {data_file}\n"
167 "source =\n"
167 "source =\n"
168 " {source}\n"
168 " {source}\n"
169 ).format(data_file=os.path.abspath('.coverage.'+self.section),
169 ).format(data_file=os.path.abspath('.coverage.'+self.section),
170 source="\n ".join(sources))
170 source="\n ".join(sources))
171 config_file = os.path.join(self.workingdir.name, '.coveragerc')
171 config_file = os.path.join(self.workingdir.name, '.coveragerc')
172 with open(config_file, 'w') as f:
172 with open(config_file, 'w') as f:
173 f.write(coverage_rc)
173 f.write(coverage_rc)
174
174
175 self.env['COVERAGE_PROCESS_START'] = config_file
175 self.env['COVERAGE_PROCESS_START'] = config_file
176 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
176 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
177
177
178 def launch(self, buffer_output=False):
178 def launch(self, buffer_output=False):
179 self.cmd[2] = self.pycmd
179 self.cmd[2] = self.pycmd
180 super(PyTestController, self).launch(buffer_output=buffer_output)
180 super(PyTestController, self).launch(buffer_output=buffer_output)
181
181
182 js_prefix = 'js/'
182 js_prefix = 'js/'
183
183
184 def get_js_test_dir():
184 def get_js_test_dir():
185 import IPython.html.tests as t
185 import IPython.html.tests as t
186 return os.path.join(os.path.dirname(t.__file__), '')
186 return os.path.join(os.path.dirname(t.__file__), '')
187
187
188 def all_js_groups():
188 def all_js_groups():
189 import glob
189 import glob
190 test_dir = get_js_test_dir()
190 test_dir = get_js_test_dir()
191 all_subdirs = glob.glob(test_dir + '*/')
191 all_subdirs = glob.glob(test_dir + '*/')
192 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
192 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
193
193
194 class JSController(TestController):
194 class JSController(TestController):
195 """Run CasperJS tests """
195 """Run CasperJS tests """
196 def __init__(self, section):
196 def __init__(self, section):
197 """Create new test runner."""
197 """Create new test runner."""
198 TestController.__init__(self)
198 TestController.__init__(self)
199 self.section = section
199 self.section = section
200 js_test_dir = get_js_test_dir()
200 js_test_dir = get_js_test_dir()
201 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
201 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
202 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
202 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
203 self.cmd = ['casperjs', 'test', includes, test_cases]
203 self.cmd = ['casperjs', 'test', includes, test_cases]
204
204
205 def setup(self):
205 def setup(self):
206 self.ipydir = TemporaryDirectory()
206 self.ipydir = TemporaryDirectory()
207 self.nbdir = TemporaryDirectory()
207 self.nbdir = TemporaryDirectory()
208 self.dirs.append(self.ipydir)
208 self.dirs.append(self.ipydir)
209 self.dirs.append(self.nbdir)
209 self.dirs.append(self.nbdir)
210 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
210 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
211 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
211 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
212
212
213 # start the ipython notebook, so we get the port number
213 # start the ipython notebook, so we get the port number
214 self._init_server()
214 self._init_server()
215 self.cmd.append('--port=%s' % self.server_port)
215 self.cmd.append('--port=%s' % self.server_port)
216
216
217 def print_extra_info(self):
217 def print_extra_info(self):
218 print("Running tests with notebook directory %r" % self.nbdir.name)
218 print("Running tests with notebook directory %r" % self.nbdir.name)
219
219
220 @property
220 @property
221 def will_run(self):
221 def will_run(self):
222 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
222 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
223
223
224 def _init_server(self):
224 def _init_server(self):
225 "Start the notebook server in a separate process"
225 "Start the notebook server in a separate process"
226 self.queue = q = Queue()
226 self.queue = q = Queue()
227 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
227 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
228 self.server.start()
228 self.server.start()
229 self.server_port = q.get()
229 self.server_port = q.get()
230
230
231 def cleanup(self):
231 def cleanup(self):
232 self.server.terminate()
232 self.server.terminate()
233 self.server.join()
233 self.server.join()
234 TestController.cleanup(self)
234 TestController.cleanup(self)
235
235
236 def run_webapp(q, ipydir, nbdir, loglevel=0):
236 def run_webapp(q, ipydir, nbdir, loglevel=0):
237 """start the IPython Notebook, and pass port back to the queue"""
237 """start the IPython Notebook, and pass port back to the queue"""
238 import os
238 import os
239 import IPython.html.notebookapp as nbapp
239 import IPython.html.notebookapp as nbapp
240 import sys
240 import sys
241 sys.stderr = open(os.devnull, 'w')
241 sys.stderr = open(os.devnull, 'w')
242 server = nbapp.NotebookApp()
242 server = nbapp.NotebookApp()
243 args = ['--no-browser']
243 args = ['--no-browser']
244 args.extend(['--ipython-dir', ipydir,
244 args.extend(['--ipython-dir', ipydir,
245 '--notebook-dir', nbdir,
245 '--notebook-dir', nbdir,
246 '--log-level', str(loglevel),
246 '--log-level', str(loglevel),
247 ])
247 ])
248 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
248 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
249 # which run afoul of ipc's maximum path length.
249 # which run afoul of ipc's maximum path length.
250 if sys.platform.startswith('linux'):
250 if sys.platform.startswith('linux'):
251 args.append('--KernelManager.transport=ipc')
251 args.append('--KernelManager.transport=ipc')
252 server.initialize(args)
252 server.initialize(args)
253 # communicate the port number to the parent process
253 # communicate the port number to the parent process
254 q.put(server.port)
254 q.put(server.port)
255 server.start()
255 server.start()
256
256
257 def prepare_controllers(options):
257 def prepare_controllers(options):
258 """Returns two lists of TestController instances, those to run, and those
258 """Returns two lists of TestController instances, those to run, and those
259 not to run."""
259 not to run."""
260 testgroups = options.testgroups
260 testgroups = options.testgroups
261
261
262 if testgroups:
262 if testgroups:
263 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
263 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
264 or g.startswith('IPython.')]
264 or g.startswith('IPython.')]
265 if 'js' in testgroups:
265 if 'js' in testgroups:
266 js_testgroups = all_js_groups()
266 js_testgroups = all_js_groups()
267 else:
267 else:
268 js_testgroups = [g for g in testgroups if g not in py_testgroups]
268 js_testgroups = [g for g in testgroups if g not in py_testgroups]
269 else:
269 else:
270 py_testgroups = py_test_group_names
270 py_testgroups = py_test_group_names
271 js_testgroups = all_js_groups()
271 js_testgroups = all_js_groups()
272 if not options.all:
272 if not options.all:
273 test_sections['parallel'].enabled = False
273 test_sections['parallel'].enabled = False
274
274
275 c_js = [JSController(name) for name in js_testgroups]
275 c_js = [JSController(name) for name in js_testgroups]
276 c_py = [PyTestController(name) for name in py_testgroups]
276 c_py = [PyTestController(name) for name in py_testgroups]
277
277
278 configure_py_controllers(c_py, xunit=options.xunit,
278 configure_py_controllers(c_py, xunit=options.xunit,
279 coverage=options.coverage, subproc_streams=options.subproc_streams,
279 coverage=options.coverage, subproc_streams=options.subproc_streams,
280 extra_args=options.extra_args)
280 extra_args=options.extra_args)
281
281
282 controllers = c_py + c_js
282 controllers = c_py + c_js
283 to_run = [c for c in controllers if c.will_run]
283 to_run = [c for c in controllers if c.will_run]
284 not_run = [c for c in controllers if not c.will_run]
284 not_run = [c for c in controllers if not c.will_run]
285 return to_run, not_run
285 return to_run, not_run
286
286
287 def configure_py_controllers(controllers, xunit=False, coverage=False,
287 def configure_py_controllers(controllers, xunit=False, coverage=False,
288 subproc_streams='capture', extra_args=()):
288 subproc_streams='capture', extra_args=()):
289 """Apply options for a collection of TestController objects."""
289 """Apply options for a collection of TestController objects."""
290 for controller in controllers:
290 for controller in controllers:
291 if xunit:
291 if xunit:
292 controller.add_xunit()
292 controller.add_xunit()
293 if coverage:
293 if coverage:
294 controller.add_coverage()
294 controller.add_coverage()
295 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
295 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
296 controller.cmd.extend(extra_args)
296 controller.cmd.extend(extra_args)
297
297
298 def do_run(controller, buffer_output=True):
298 def do_run(controller, buffer_output=True):
299 """Setup and run a test controller.
299 """Setup and run a test controller.
300
300
301 If buffer_output is True, no output is displayed, to avoid it appearing
301 If buffer_output is True, no output is displayed, to avoid it appearing
302 interleaved. In this case, the caller is responsible for displaying test
302 interleaved. In this case, the caller is responsible for displaying test
303 output on failure.
303 output on failure.
304
304
305 Returns
305 Returns
306 -------
306 -------
307 controller : TestController
307 controller : TestController
308 The same controller as passed in, as a convenience for using map() type
308 The same controller as passed in, as a convenience for using map() type
309 APIs.
309 APIs.
310 exitcode : int
310 exitcode : int
311 The exit code of the test subprocess. Non-zero indicates failure.
311 The exit code of the test subprocess. Non-zero indicates failure.
312 """
312 """
313 try:
313 try:
314 try:
314 try:
315 controller.setup()
315 controller.setup()
316 if not buffer_output:
316 if not buffer_output:
317 controller.print_extra_info()
317 controller.print_extra_info()
318 controller.launch(buffer_output=buffer_output)
318 controller.launch(buffer_output=buffer_output)
319 except Exception:
319 except Exception:
320 import traceback
320 import traceback
321 traceback.print_exc()
321 traceback.print_exc()
322 return controller, 1 # signal failure
322 return controller, 1 # signal failure
323
323
324 exitcode = controller.wait()
324 exitcode = controller.wait()
325 return controller, exitcode
325 return controller, exitcode
326
326
327 except KeyboardInterrupt:
327 except KeyboardInterrupt:
328 return controller, -signal.SIGINT
328 return controller, -signal.SIGINT
329 finally:
329 finally:
330 controller.cleanup()
330 controller.cleanup()
331
331
332 def report():
332 def report():
333 """Return a string with a summary report of test-related variables."""
333 """Return a string with a summary report of test-related variables."""
334 inf = get_sys_info()
334 inf = get_sys_info()
335 out = []
335 out = []
336 def _add(name, value):
336 def _add(name, value):
337 out.append((name, value))
337 out.append((name, value))
338
338
339 _add('IPython version', inf['ipython_version'])
339 _add('IPython version', inf['ipython_version'])
340 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
340 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
341 _add('IPython package', compress_user(inf['ipython_path']))
341 _add('IPython package', compress_user(inf['ipython_path']))
342 _add('Python version', inf['sys_version'].replace('\n',''))
342 _add('Python version', inf['sys_version'].replace('\n',''))
343 _add('sys.executable', compress_user(inf['sys_executable']))
343 _add('sys.executable', compress_user(inf['sys_executable']))
344 _add('Platform', inf['platform'])
344 _add('Platform', inf['platform'])
345
345
346 width = max(len(n) for (n,v) in out)
346 width = max(len(n) for (n,v) in out)
347 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
347 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
348
348
349 avail = []
349 avail = []
350 not_avail = []
350 not_avail = []
351
351
352 for k, is_avail in have.items():
352 for k, is_avail in have.items():
353 if is_avail:
353 if is_avail:
354 avail.append(k)
354 avail.append(k)
355 else:
355 else:
356 not_avail.append(k)
356 not_avail.append(k)
357
357
358 if avail:
358 if avail:
359 out.append('\nTools and libraries available at test time:\n')
359 out.append('\nTools and libraries available at test time:\n')
360 avail.sort()
360 avail.sort()
361 out.append(' ' + ' '.join(avail)+'\n')
361 out.append(' ' + ' '.join(avail)+'\n')
362
362
363 if not_avail:
363 if not_avail:
364 out.append('\nTools and libraries NOT available at test time:\n')
364 out.append('\nTools and libraries NOT available at test time:\n')
365 not_avail.sort()
365 not_avail.sort()
366 out.append(' ' + ' '.join(not_avail)+'\n')
366 out.append(' ' + ' '.join(not_avail)+'\n')
367
367
368 return ''.join(out)
368 return ''.join(out)
369
369
370 def run_iptestall(options):
370 def run_iptestall(options):
371 """Run the entire IPython test suite by calling nose and trial.
371 """Run the entire IPython test suite by calling nose and trial.
372
372
373 This function constructs :class:`IPTester` instances for all IPython
373 This function constructs :class:`IPTester` instances for all IPython
374 modules and package and then runs each of them. This causes the modules
374 modules and package and then runs each of them. This causes the modules
375 and packages of IPython to be tested each in their own subprocess using
375 and packages of IPython to be tested each in their own subprocess using
376 nose.
376 nose.
377
377
378 Parameters
378 Parameters
379 ----------
379 ----------
380
380
381 All parameters are passed as attributes of the options object.
381 All parameters are passed as attributes of the options object.
382
382
383 testgroups : list of str
383 testgroups : list of str
384 Run only these sections of the test suite. If empty, run all the available
384 Run only these sections of the test suite. If empty, run all the available
385 sections.
385 sections.
386
386
387 fast : int or None
387 fast : int or None
388 Run the test suite in parallel, using n simultaneous processes. If None
388 Run the test suite in parallel, using n simultaneous processes. If None
389 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
389 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
390
390
391 inc_slow : bool
391 inc_slow : bool
392 Include slow tests, like IPython.parallel. By default, these tests aren't
392 Include slow tests, like IPython.parallel. By default, these tests aren't
393 run.
393 run.
394
394
395 xunit : bool
395 xunit : bool
396 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
396 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
397
397
398 coverage : bool or str
398 coverage : bool or str
399 Measure code coverage from tests. True will store the raw coverage data,
399 Measure code coverage from tests. True will store the raw coverage data,
400 or pass 'html' or 'xml' to get reports.
400 or pass 'html' or 'xml' to get reports.
401
401
402 extra_args : list
402 extra_args : list
403 Extra arguments to pass to the test subprocesses, e.g. '-v'
403 Extra arguments to pass to the test subprocesses, e.g. '-v'
404 """
404 """
405 to_run, not_run = prepare_controllers(options)
405 to_run, not_run = prepare_controllers(options)
406
406
407 def justify(ltext, rtext, width=70, fill='-'):
407 def justify(ltext, rtext, width=70, fill='-'):
408 ltext += ' '
408 ltext += ' '
409 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
409 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
410 return ltext + rtext
410 return ltext + rtext
411
411
412 # Run all test runners, tracking execution time
412 # Run all test runners, tracking execution time
413 failed = []
413 failed = []
414 t_start = time.time()
414 t_start = time.time()
415
415
416 print()
416 print()
417 if options.fast == 1:
417 if options.fast == 1:
418 # This actually means sequential, i.e. with 1 job
418 # This actually means sequential, i.e. with 1 job
419 for controller in to_run:
419 for controller in to_run:
420 print('Test group:', controller.section)
420 print('Test group:', controller.section)
421 sys.stdout.flush() # Show in correct order when output is piped
421 sys.stdout.flush() # Show in correct order when output is piped
422 controller, res = do_run(controller, buffer_output=False)
422 controller, res = do_run(controller, buffer_output=False)
423 if res:
423 if res:
424 failed.append(controller)
424 failed.append(controller)
425 if res == -signal.SIGINT:
425 if res == -signal.SIGINT:
426 print("Interrupted")
426 print("Interrupted")
427 break
427 break
428 print()
428 print()
429
429
430 else:
430 else:
431 # Run tests concurrently
431 # Run tests concurrently
432 try:
432 try:
433 pool = multiprocessing.pool.ThreadPool(options.fast)
433 pool = multiprocessing.pool.ThreadPool(options.fast)
434 for (controller, res) in pool.imap_unordered(do_run, to_run):
434 for (controller, res) in pool.imap_unordered(do_run, to_run):
435 res_string = 'OK' if res == 0 else 'FAILED'
435 res_string = 'OK' if res == 0 else 'FAILED'
436 print(justify('Test group: ' + controller.section, res_string))
436 print(justify('Test group: ' + controller.section, res_string))
437 if res:
437 if res:
438 controller.print_extra_info()
438 controller.print_extra_info()
439 print(bytes_to_str(controller.stdout))
439 print(bytes_to_str(controller.stdout))
440 failed.append(controller)
440 failed.append(controller)
441 if res == -signal.SIGINT:
441 if res == -signal.SIGINT:
442 print("Interrupted")
442 print("Interrupted")
443 break
443 break
444 except KeyboardInterrupt:
444 except KeyboardInterrupt:
445 return
445 return
446
446
447 for controller in not_run:
447 for controller in not_run:
448 print(justify('Test group: ' + controller.section, 'NOT RUN'))
448 print(justify('Test group: ' + controller.section, 'NOT RUN'))
449
449
450 t_end = time.time()
450 t_end = time.time()
451 t_tests = t_end - t_start
451 t_tests = t_end - t_start
452 nrunners = len(to_run)
452 nrunners = len(to_run)
453 nfail = len(failed)
453 nfail = len(failed)
454 # summarize results
454 # summarize results
455 print('_'*70)
455 print('_'*70)
456 print('Test suite completed for system with the following information:')
456 print('Test suite completed for system with the following information:')
457 print(report())
457 print(report())
458 took = "Took %.3fs." % t_tests
458 took = "Took %.3fs." % t_tests
459 print('Status: ', end='')
459 print('Status: ', end='')
460 if not failed:
460 if not failed:
461 print('OK (%d test groups).' % nrunners, took)
461 print('OK (%d test groups).' % nrunners, took)
462 else:
462 else:
463 # If anything went wrong, point out what command to rerun manually to
463 # If anything went wrong, point out what command to rerun manually to
464 # see the actual errors and individual summary
464 # see the actual errors and individual summary
465 failed_sections = [c.section for c in failed]
465 failed_sections = [c.section for c in failed]
466 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
466 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
467 nrunners, ', '.join(failed_sections)), took)
467 nrunners, ', '.join(failed_sections)), took)
468 print()
468 print()
469 print('You may wish to rerun these, with:')
469 print('You may wish to rerun these, with:')
470 print(' iptest', *failed_sections)
470 print(' iptest', *failed_sections)
471 print()
471 print()
472
472
473 if options.coverage:
473 if options.coverage:
474 from coverage import coverage
474 from coverage import coverage
475 cov = coverage(data_file='.coverage')
475 cov = coverage(data_file='.coverage')
476 cov.combine()
476 cov.combine()
477 cov.save()
477 cov.save()
478
478
479 # Coverage HTML report
479 # Coverage HTML report
480 if options.coverage == 'html':
480 if options.coverage == 'html':
481 html_dir = 'ipy_htmlcov'
481 html_dir = 'ipy_htmlcov'
482 shutil.rmtree(html_dir, ignore_errors=True)
482 shutil.rmtree(html_dir, ignore_errors=True)
483 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
483 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
484 sys.stdout.flush()
484 sys.stdout.flush()
485
485
486 # Custom HTML reporter to clean up module names.
486 # Custom HTML reporter to clean up module names.
487 from coverage.html import HtmlReporter
487 from coverage.html import HtmlReporter
488 class CustomHtmlReporter(HtmlReporter):
488 class CustomHtmlReporter(HtmlReporter):
489 def find_code_units(self, morfs):
489 def find_code_units(self, morfs):
490 super(CustomHtmlReporter, self).find_code_units(morfs)
490 super(CustomHtmlReporter, self).find_code_units(morfs)
491 for cu in self.code_units:
491 for cu in self.code_units:
492 nameparts = cu.name.split(os.sep)
492 nameparts = cu.name.split(os.sep)
493 if 'IPython' not in nameparts:
493 if 'IPython' not in nameparts:
494 continue
494 continue
495 ix = nameparts.index('IPython')
495 ix = nameparts.index('IPython')
496 cu.name = '.'.join(nameparts[ix:])
496 cu.name = '.'.join(nameparts[ix:])
497
497
498 # Reimplement the html_report method with our custom reporter
498 # Reimplement the html_report method with our custom reporter
499 cov._harvest_data()
499 cov._harvest_data()
500 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
500 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
501 html_title='IPython test coverage',
501 html_title='IPython test coverage',
502 )
502 )
503 reporter = CustomHtmlReporter(cov, cov.config)
503 reporter = CustomHtmlReporter(cov, cov.config)
504 reporter.report(None)
504 reporter.report(None)
505 print('done.')
505 print('done.')
506
506
507 # Coverage XML report
507 # Coverage XML report
508 elif options.coverage == 'xml':
508 elif options.coverage == 'xml':
509 cov.xml_report(outfile='ipy_coverage.xml')
509 cov.xml_report(outfile='ipy_coverage.xml')
510
510
511 if failed:
511 if failed:
512 # Ensure that our exit code indicates failure
512 # Ensure that our exit code indicates failure
513 sys.exit(1)
513 sys.exit(1)
514
514
515 argparser = argparse.ArgumentParser(description='Run IPython test suite')
515 argparser = argparse.ArgumentParser(description='Run IPython test suite')
516 argparser.add_argument('testgroups', nargs='*',
516 argparser.add_argument('testgroups', nargs='*',
517 help='Run specified groups of tests. If omitted, run '
517 help='Run specified groups of tests. If omitted, run '
518 'all tests.')
518 'all tests.')
519 argparser.add_argument('--all', action='store_true',
519 argparser.add_argument('--all', action='store_true',
520 help='Include slow tests not run by default.')
520 help='Include slow tests not run by default.')
521 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
521 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
522 help='Run test sections in parallel.')
522 help='Run test sections in parallel. This starts as many '
523 'processes as you have cores, or you can specify a number.')
523 argparser.add_argument('--xunit', action='store_true',
524 argparser.add_argument('--xunit', action='store_true',
524 help='Produce Xunit XML results')
525 help='Produce Xunit XML results')
525 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
526 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
526 help="Measure test coverage. Specify 'html' or "
527 help="Measure test coverage. Specify 'html' or "
527 "'xml' to get reports.")
528 "'xml' to get reports.")
528 argparser.add_argument('--subproc-streams', default='capture',
529 argparser.add_argument('--subproc-streams', default='capture',
529 help="What to do with stdout/stderr from subprocesses. "
530 help="What to do with stdout/stderr from subprocesses. "
530 "'capture' (default), 'show' and 'discard' are the options.")
531 "'capture' (default), 'show' and 'discard' are the options.")
531
532
532 def default_options():
533 def default_options():
533 """Get an argparse Namespace object with the default arguments, to pass to
534 """Get an argparse Namespace object with the default arguments, to pass to
534 :func:`run_iptestall`.
535 :func:`run_iptestall`.
535 """
536 """
536 options = argparser.parse_args([])
537 options = argparser.parse_args([])
537 options.extra_args = []
538 options.extra_args = []
538 return options
539 return options
539
540
540 def main():
541 def main():
541 # iptest doesn't work correctly if the working directory is the
542 # iptest doesn't work correctly if the working directory is the
542 # root of the IPython source tree. Tell the user to avoid
543 # root of the IPython source tree. Tell the user to avoid
543 # frustration.
544 # frustration.
544 if os.path.exists(os.path.join(os.getcwd(),
545 if os.path.exists(os.path.join(os.getcwd(),
545 'IPython', 'testing', '__main__.py')):
546 'IPython', 'testing', '__main__.py')):
546 print("Don't run iptest from the IPython source directory",
547 print("Don't run iptest from the IPython source directory",
547 file=sys.stderr)
548 file=sys.stderr)
548 sys.exit(1)
549 sys.exit(1)
549 # Arguments after -- should be passed through to nose. Argparse treats
550 # Arguments after -- should be passed through to nose. Argparse treats
550 # everything after -- as regular positional arguments, so we separate them
551 # everything after -- as regular positional arguments, so we separate them
551 # first.
552 # first.
552 try:
553 try:
553 ix = sys.argv.index('--')
554 ix = sys.argv.index('--')
554 except ValueError:
555 except ValueError:
555 to_parse = sys.argv[1:]
556 to_parse = sys.argv[1:]
556 extra_args = []
557 extra_args = []
557 else:
558 else:
558 to_parse = sys.argv[1:ix]
559 to_parse = sys.argv[1:ix]
559 extra_args = sys.argv[ix+1:]
560 extra_args = sys.argv[ix+1:]
560
561
561 options = argparser.parse_args(to_parse)
562 options = argparser.parse_args(to_parse)
562 options.extra_args = extra_args
563 options.extra_args = extra_args
563
564
564 run_iptestall(options)
565 run_iptestall(options)
565
566
566
567
567 if __name__ == '__main__':
568 if __name__ == '__main__':
568 main()
569 main()
General Comments 0
You need to be logged in to leave comments. Login now