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