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