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