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