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