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