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