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