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