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