##// END OF EJS Templates
remove redundant js_enabled variable...
MinRK -
Show More
@@ -1,634 +1,632 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 # 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 try:
277 try:
278 self._load_server_info()
278 self._load_server_info()
279 except ValueError:
279 except ValueError:
280 # If the server is halfway through writing the file, we may
280 # If the server is halfway through writing the file, we may
281 # get invalid JSON; it should be ready next iteration.
281 # get invalid JSON; it should be ready next iteration.
282 pass
282 pass
283 else:
283 else:
284 return
284 return
285 time.sleep(0.1)
285 time.sleep(0.1)
286 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,
287 file=sys.stderr
287 file=sys.stderr
288 )
288 )
289
289
290 def _failed_to_start(self):
290 def _failed_to_start(self):
291 """Notebook server exited prematurely"""
291 """Notebook server exited prematurely"""
292 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
292 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
293 print("Notebook failed to start: ", file=sys.stderr)
293 print("Notebook failed to start: ", file=sys.stderr)
294 print(self.server_command)
294 print(self.server_command)
295 print(captured, file=sys.stderr)
295 print(captured, file=sys.stderr)
296
296
297 def _load_server_info(self):
297 def _load_server_info(self):
298 """Notebook server started, load connection info from JSON"""
298 """Notebook server started, load connection info from JSON"""
299 with open(self.server_info_file) as f:
299 with open(self.server_info_file) as f:
300 info = json.load(f)
300 info = json.load(f)
301 self.server_port = info['port']
301 self.server_port = info['port']
302
302
303 def cleanup(self):
303 def cleanup(self):
304 try:
304 try:
305 self.server.terminate()
305 self.server.terminate()
306 except OSError:
306 except OSError:
307 # already dead
307 # already dead
308 pass
308 pass
309 # wait 10s for the server to shutdown
309 # wait 10s for the server to shutdown
310 try:
310 try:
311 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
311 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
312 except TimeoutExpired:
312 except TimeoutExpired:
313 # server didn't terminate, kill it
313 # server didn't terminate, kill it
314 try:
314 try:
315 print("Failed to terminate notebook server, killing it.",
315 print("Failed to terminate notebook server, killing it.",
316 file=sys.stderr
316 file=sys.stderr
317 )
317 )
318 self.server.kill()
318 self.server.kill()
319 except OSError:
319 except OSError:
320 # already dead
320 # already dead
321 pass
321 pass
322 # wait another 10s
322 # wait another 10s
323 try:
323 try:
324 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
324 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
325 except TimeoutExpired:
325 except TimeoutExpired:
326 print("Notebook server still running (%s)" % self.server_info_file,
326 print("Notebook server still running (%s)" % self.server_info_file,
327 file=sys.stderr
327 file=sys.stderr
328 )
328 )
329
329
330 self.stream_capturer.halt()
330 self.stream_capturer.halt()
331 TestController.cleanup(self)
331 TestController.cleanup(self)
332
332
333
333
334 def prepare_controllers(options):
334 def prepare_controllers(options):
335 """Returns two lists of TestController instances, those to run, and those
335 """Returns two lists of TestController instances, those to run, and those
336 not to run."""
336 not to run."""
337 testgroups = options.testgroups
337 testgroups = options.testgroups
338 js_enabled = True
339 if testgroups:
338 if testgroups:
340 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
341 or g.startswith('IPython.')]
342 if 'js' in testgroups:
339 if 'js' in testgroups:
343 js_testgroups = all_js_groups()
340 js_testgroups = all_js_groups()
344 else:
341 else:
345 js_testgroups = [g for g in testgroups if g not in py_testgroups]
342 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
346 js_enabled = len(js_testgroups) > 0
343
344 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
347 else:
345 else:
348 py_testgroups = py_test_group_names
346 py_testgroups = py_test_group_names
349 if not options.all:
347 if not options.all:
350 js_testgroups = []
348 js_testgroups = []
351 test_sections['parallel'].enabled = False
349 test_sections['parallel'].enabled = False
352 else:
350 else:
353 js_testgroups = all_js_groups()
351 js_testgroups = all_js_groups()
354
352
355 c_js = [JSController(name, js_enabled) for name in js_testgroups]
353 c_js = [JSController(name) for name in js_testgroups]
356 c_py = [PyTestController(name, options) for name in py_testgroups]
354 c_py = [PyTestController(name, options) for name in py_testgroups]
357
355
358 controllers = c_py + c_js
356 controllers = c_py + c_js
359 to_run = [c for c in controllers if c.will_run]
357 to_run = [c for c in controllers if c.will_run]
360 not_run = [c for c in controllers if not c.will_run]
358 not_run = [c for c in controllers if not c.will_run]
361 return to_run, not_run
359 return to_run, not_run
362
360
363 def do_run(controller, buffer_output=True):
361 def do_run(controller, buffer_output=True):
364 """Setup and run a test controller.
362 """Setup and run a test controller.
365
363
366 If buffer_output is True, no output is displayed, to avoid it appearing
364 If buffer_output is True, no output is displayed, to avoid it appearing
367 interleaved. In this case, the caller is responsible for displaying test
365 interleaved. In this case, the caller is responsible for displaying test
368 output on failure.
366 output on failure.
369
367
370 Returns
368 Returns
371 -------
369 -------
372 controller : TestController
370 controller : TestController
373 The same controller as passed in, as a convenience for using map() type
371 The same controller as passed in, as a convenience for using map() type
374 APIs.
372 APIs.
375 exitcode : int
373 exitcode : int
376 The exit code of the test subprocess. Non-zero indicates failure.
374 The exit code of the test subprocess. Non-zero indicates failure.
377 """
375 """
378 try:
376 try:
379 try:
377 try:
380 controller.setup()
378 controller.setup()
381 if not buffer_output:
379 if not buffer_output:
382 controller.print_extra_info()
380 controller.print_extra_info()
383 controller.launch(buffer_output=buffer_output)
381 controller.launch(buffer_output=buffer_output)
384 except Exception:
382 except Exception:
385 import traceback
383 import traceback
386 traceback.print_exc()
384 traceback.print_exc()
387 return controller, 1 # signal failure
385 return controller, 1 # signal failure
388
386
389 exitcode = controller.wait()
387 exitcode = controller.wait()
390 return controller, exitcode
388 return controller, exitcode
391
389
392 except KeyboardInterrupt:
390 except KeyboardInterrupt:
393 return controller, -signal.SIGINT
391 return controller, -signal.SIGINT
394 finally:
392 finally:
395 controller.cleanup()
393 controller.cleanup()
396
394
397 def report():
395 def report():
398 """Return a string with a summary report of test-related variables."""
396 """Return a string with a summary report of test-related variables."""
399 inf = get_sys_info()
397 inf = get_sys_info()
400 out = []
398 out = []
401 def _add(name, value):
399 def _add(name, value):
402 out.append((name, value))
400 out.append((name, value))
403
401
404 _add('IPython version', inf['ipython_version'])
402 _add('IPython version', inf['ipython_version'])
405 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
403 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
406 _add('IPython package', compress_user(inf['ipython_path']))
404 _add('IPython package', compress_user(inf['ipython_path']))
407 _add('Python version', inf['sys_version'].replace('\n',''))
405 _add('Python version', inf['sys_version'].replace('\n',''))
408 _add('sys.executable', compress_user(inf['sys_executable']))
406 _add('sys.executable', compress_user(inf['sys_executable']))
409 _add('Platform', inf['platform'])
407 _add('Platform', inf['platform'])
410
408
411 width = max(len(n) for (n,v) in out)
409 width = max(len(n) for (n,v) in out)
412 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
410 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
413
411
414 avail = []
412 avail = []
415 not_avail = []
413 not_avail = []
416
414
417 for k, is_avail in have.items():
415 for k, is_avail in have.items():
418 if is_avail:
416 if is_avail:
419 avail.append(k)
417 avail.append(k)
420 else:
418 else:
421 not_avail.append(k)
419 not_avail.append(k)
422
420
423 if avail:
421 if avail:
424 out.append('\nTools and libraries available at test time:\n')
422 out.append('\nTools and libraries available at test time:\n')
425 avail.sort()
423 avail.sort()
426 out.append(' ' + ' '.join(avail)+'\n')
424 out.append(' ' + ' '.join(avail)+'\n')
427
425
428 if not_avail:
426 if not_avail:
429 out.append('\nTools and libraries NOT available at test time:\n')
427 out.append('\nTools and libraries NOT available at test time:\n')
430 not_avail.sort()
428 not_avail.sort()
431 out.append(' ' + ' '.join(not_avail)+'\n')
429 out.append(' ' + ' '.join(not_avail)+'\n')
432
430
433 return ''.join(out)
431 return ''.join(out)
434
432
435 def run_iptestall(options):
433 def run_iptestall(options):
436 """Run the entire IPython test suite by calling nose and trial.
434 """Run the entire IPython test suite by calling nose and trial.
437
435
438 This function constructs :class:`IPTester` instances for all IPython
436 This function constructs :class:`IPTester` instances for all IPython
439 modules and package and then runs each of them. This causes the modules
437 modules and package and then runs each of them. This causes the modules
440 and packages of IPython to be tested each in their own subprocess using
438 and packages of IPython to be tested each in their own subprocess using
441 nose.
439 nose.
442
440
443 Parameters
441 Parameters
444 ----------
442 ----------
445
443
446 All parameters are passed as attributes of the options object.
444 All parameters are passed as attributes of the options object.
447
445
448 testgroups : list of str
446 testgroups : list of str
449 Run only these sections of the test suite. If empty, run all the available
447 Run only these sections of the test suite. If empty, run all the available
450 sections.
448 sections.
451
449
452 fast : int or None
450 fast : int or None
453 Run the test suite in parallel, using n simultaneous processes. If None
451 Run the test suite in parallel, using n simultaneous processes. If None
454 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
452 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
455
453
456 inc_slow : bool
454 inc_slow : bool
457 Include slow tests, like IPython.parallel. By default, these tests aren't
455 Include slow tests, like IPython.parallel. By default, these tests aren't
458 run.
456 run.
459
457
460 xunit : bool
458 xunit : bool
461 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
459 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
462
460
463 coverage : bool or str
461 coverage : bool or str
464 Measure code coverage from tests. True will store the raw coverage data,
462 Measure code coverage from tests. True will store the raw coverage data,
465 or pass 'html' or 'xml' to get reports.
463 or pass 'html' or 'xml' to get reports.
466
464
467 extra_args : list
465 extra_args : list
468 Extra arguments to pass to the test subprocesses, e.g. '-v'
466 Extra arguments to pass to the test subprocesses, e.g. '-v'
469 """
467 """
470 to_run, not_run = prepare_controllers(options)
468 to_run, not_run = prepare_controllers(options)
471
469
472 def justify(ltext, rtext, width=70, fill='-'):
470 def justify(ltext, rtext, width=70, fill='-'):
473 ltext += ' '
471 ltext += ' '
474 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
472 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
475 return ltext + rtext
473 return ltext + rtext
476
474
477 # Run all test runners, tracking execution time
475 # Run all test runners, tracking execution time
478 failed = []
476 failed = []
479 t_start = time.time()
477 t_start = time.time()
480
478
481 print()
479 print()
482 if options.fast == 1:
480 if options.fast == 1:
483 # This actually means sequential, i.e. with 1 job
481 # This actually means sequential, i.e. with 1 job
484 for controller in to_run:
482 for controller in to_run:
485 print('Test group:', controller.section)
483 print('Test group:', controller.section)
486 sys.stdout.flush() # Show in correct order when output is piped
484 sys.stdout.flush() # Show in correct order when output is piped
487 controller, res = do_run(controller, buffer_output=False)
485 controller, res = do_run(controller, buffer_output=False)
488 if res:
486 if res:
489 failed.append(controller)
487 failed.append(controller)
490 if res == -signal.SIGINT:
488 if res == -signal.SIGINT:
491 print("Interrupted")
489 print("Interrupted")
492 break
490 break
493 print()
491 print()
494
492
495 else:
493 else:
496 # Run tests concurrently
494 # Run tests concurrently
497 try:
495 try:
498 pool = multiprocessing.pool.ThreadPool(options.fast)
496 pool = multiprocessing.pool.ThreadPool(options.fast)
499 for (controller, res) in pool.imap_unordered(do_run, to_run):
497 for (controller, res) in pool.imap_unordered(do_run, to_run):
500 res_string = 'OK' if res == 0 else 'FAILED'
498 res_string = 'OK' if res == 0 else 'FAILED'
501 print(justify('Test group: ' + controller.section, res_string))
499 print(justify('Test group: ' + controller.section, res_string))
502 if res:
500 if res:
503 controller.print_extra_info()
501 controller.print_extra_info()
504 print(bytes_to_str(controller.stdout))
502 print(bytes_to_str(controller.stdout))
505 failed.append(controller)
503 failed.append(controller)
506 if res == -signal.SIGINT:
504 if res == -signal.SIGINT:
507 print("Interrupted")
505 print("Interrupted")
508 break
506 break
509 except KeyboardInterrupt:
507 except KeyboardInterrupt:
510 return
508 return
511
509
512 for controller in not_run:
510 for controller in not_run:
513 print(justify('Test group: ' + controller.section, 'NOT RUN'))
511 print(justify('Test group: ' + controller.section, 'NOT RUN'))
514
512
515 t_end = time.time()
513 t_end = time.time()
516 t_tests = t_end - t_start
514 t_tests = t_end - t_start
517 nrunners = len(to_run)
515 nrunners = len(to_run)
518 nfail = len(failed)
516 nfail = len(failed)
519 # summarize results
517 # summarize results
520 print('_'*70)
518 print('_'*70)
521 print('Test suite completed for system with the following information:')
519 print('Test suite completed for system with the following information:')
522 print(report())
520 print(report())
523 took = "Took %.3fs." % t_tests
521 took = "Took %.3fs." % t_tests
524 print('Status: ', end='')
522 print('Status: ', end='')
525 if not failed:
523 if not failed:
526 print('OK (%d test groups).' % nrunners, took)
524 print('OK (%d test groups).' % nrunners, took)
527 else:
525 else:
528 # If anything went wrong, point out what command to rerun manually to
526 # If anything went wrong, point out what command to rerun manually to
529 # see the actual errors and individual summary
527 # see the actual errors and individual summary
530 failed_sections = [c.section for c in failed]
528 failed_sections = [c.section for c in failed]
531 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
529 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
532 nrunners, ', '.join(failed_sections)), took)
530 nrunners, ', '.join(failed_sections)), took)
533 print()
531 print()
534 print('You may wish to rerun these, with:')
532 print('You may wish to rerun these, with:')
535 print(' iptest', *failed_sections)
533 print(' iptest', *failed_sections)
536 print()
534 print()
537
535
538 if options.coverage:
536 if options.coverage:
539 from coverage import coverage
537 from coverage import coverage
540 cov = coverage(data_file='.coverage')
538 cov = coverage(data_file='.coverage')
541 cov.combine()
539 cov.combine()
542 cov.save()
540 cov.save()
543
541
544 # Coverage HTML report
542 # Coverage HTML report
545 if options.coverage == 'html':
543 if options.coverage == 'html':
546 html_dir = 'ipy_htmlcov'
544 html_dir = 'ipy_htmlcov'
547 shutil.rmtree(html_dir, ignore_errors=True)
545 shutil.rmtree(html_dir, ignore_errors=True)
548 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
546 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
549 sys.stdout.flush()
547 sys.stdout.flush()
550
548
551 # Custom HTML reporter to clean up module names.
549 # Custom HTML reporter to clean up module names.
552 from coverage.html import HtmlReporter
550 from coverage.html import HtmlReporter
553 class CustomHtmlReporter(HtmlReporter):
551 class CustomHtmlReporter(HtmlReporter):
554 def find_code_units(self, morfs):
552 def find_code_units(self, morfs):
555 super(CustomHtmlReporter, self).find_code_units(morfs)
553 super(CustomHtmlReporter, self).find_code_units(morfs)
556 for cu in self.code_units:
554 for cu in self.code_units:
557 nameparts = cu.name.split(os.sep)
555 nameparts = cu.name.split(os.sep)
558 if 'IPython' not in nameparts:
556 if 'IPython' not in nameparts:
559 continue
557 continue
560 ix = nameparts.index('IPython')
558 ix = nameparts.index('IPython')
561 cu.name = '.'.join(nameparts[ix:])
559 cu.name = '.'.join(nameparts[ix:])
562
560
563 # Reimplement the html_report method with our custom reporter
561 # Reimplement the html_report method with our custom reporter
564 cov._harvest_data()
562 cov._harvest_data()
565 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
563 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
566 html_title='IPython test coverage',
564 html_title='IPython test coverage',
567 )
565 )
568 reporter = CustomHtmlReporter(cov, cov.config)
566 reporter = CustomHtmlReporter(cov, cov.config)
569 reporter.report(None)
567 reporter.report(None)
570 print('done.')
568 print('done.')
571
569
572 # Coverage XML report
570 # Coverage XML report
573 elif options.coverage == 'xml':
571 elif options.coverage == 'xml':
574 cov.xml_report(outfile='ipy_coverage.xml')
572 cov.xml_report(outfile='ipy_coverage.xml')
575
573
576 if failed:
574 if failed:
577 # Ensure that our exit code indicates failure
575 # Ensure that our exit code indicates failure
578 sys.exit(1)
576 sys.exit(1)
579
577
580 argparser = argparse.ArgumentParser(description='Run IPython test suite')
578 argparser = argparse.ArgumentParser(description='Run IPython test suite')
581 argparser.add_argument('testgroups', nargs='*',
579 argparser.add_argument('testgroups', nargs='*',
582 help='Run specified groups of tests. If omitted, run '
580 help='Run specified groups of tests. If omitted, run '
583 'all tests.')
581 'all tests.')
584 argparser.add_argument('--all', action='store_true',
582 argparser.add_argument('--all', action='store_true',
585 help='Include slow tests not run by default.')
583 help='Include slow tests not run by default.')
586 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
584 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
587 help='Run test sections in parallel. This starts as many '
585 help='Run test sections in parallel. This starts as many '
588 'processes as you have cores, or you can specify a number.')
586 'processes as you have cores, or you can specify a number.')
589 argparser.add_argument('--xunit', action='store_true',
587 argparser.add_argument('--xunit', action='store_true',
590 help='Produce Xunit XML results')
588 help='Produce Xunit XML results')
591 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
589 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
592 help="Measure test coverage. Specify 'html' or "
590 help="Measure test coverage. Specify 'html' or "
593 "'xml' to get reports.")
591 "'xml' to get reports.")
594 argparser.add_argument('--subproc-streams', default='capture',
592 argparser.add_argument('--subproc-streams', default='capture',
595 help="What to do with stdout/stderr from subprocesses. "
593 help="What to do with stdout/stderr from subprocesses. "
596 "'capture' (default), 'show' and 'discard' are the options.")
594 "'capture' (default), 'show' and 'discard' are the options.")
597
595
598 def default_options():
596 def default_options():
599 """Get an argparse Namespace object with the default arguments, to pass to
597 """Get an argparse Namespace object with the default arguments, to pass to
600 :func:`run_iptestall`.
598 :func:`run_iptestall`.
601 """
599 """
602 options = argparser.parse_args([])
600 options = argparser.parse_args([])
603 options.extra_args = []
601 options.extra_args = []
604 return options
602 return options
605
603
606 def main():
604 def main():
607 # iptest doesn't work correctly if the working directory is the
605 # iptest doesn't work correctly if the working directory is the
608 # root of the IPython source tree. Tell the user to avoid
606 # root of the IPython source tree. Tell the user to avoid
609 # frustration.
607 # frustration.
610 if os.path.exists(os.path.join(os.getcwd(),
608 if os.path.exists(os.path.join(os.getcwd(),
611 'IPython', 'testing', '__main__.py')):
609 'IPython', 'testing', '__main__.py')):
612 print("Don't run iptest from the IPython source directory",
610 print("Don't run iptest from the IPython source directory",
613 file=sys.stderr)
611 file=sys.stderr)
614 sys.exit(1)
612 sys.exit(1)
615 # Arguments after -- should be passed through to nose. Argparse treats
613 # Arguments after -- should be passed through to nose. Argparse treats
616 # everything after -- as regular positional arguments, so we separate them
614 # everything after -- as regular positional arguments, so we separate them
617 # first.
615 # first.
618 try:
616 try:
619 ix = sys.argv.index('--')
617 ix = sys.argv.index('--')
620 except ValueError:
618 except ValueError:
621 to_parse = sys.argv[1:]
619 to_parse = sys.argv[1:]
622 extra_args = []
620 extra_args = []
623 else:
621 else:
624 to_parse = sys.argv[1:ix]
622 to_parse = sys.argv[1:ix]
625 extra_args = sys.argv[ix+1:]
623 extra_args = sys.argv[ix+1:]
626
624
627 options = argparser.parse_args(to_parse)
625 options = argparser.parse_args(to_parse)
628 options.extra_args = extra_args
626 options.extra_args = extra_args
629
627
630 run_iptestall(options)
628 run_iptestall(options)
631
629
632
630
633 if __name__ == '__main__':
631 if __name__ == '__main__':
634 main()
632 main()
General Comments 0
You need to be logged in to leave comments. Login now