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