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