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