##// END OF EJS Templates
Merge pull request #6237 from jasongrout/iptest-profile-dir...
Thomas Kluyver -
r17471:aaabb878 merge
parent child Browse files
Show More
@@ -1,680 +1,680 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.utils.text import strip_ansi
30 from IPython.utils.text 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 'jsonschema', 'jsonpointer']
221 'jsonschema', 'jsonpointer']
222 display_slimer_output = False
222 display_slimer_output = False
223
223
224 def __init__(self, section, xunit=True, engine='phantomjs'):
224 def __init__(self, section, xunit=True, engine='phantomjs'):
225 """Create new test runner."""
225 """Create new test runner."""
226 TestController.__init__(self)
226 TestController.__init__(self)
227 self.engine = engine
227 self.engine = engine
228 self.section = section
228 self.section = section
229 self.xunit = xunit
229 self.xunit = xunit
230 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
230 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
231 js_test_dir = get_js_test_dir()
231 js_test_dir = get_js_test_dir()
232 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
232 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
233 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
233 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]
234 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
235
235
236 def setup(self):
236 def setup(self):
237 self.ipydir = TemporaryDirectory()
237 self.ipydir = TemporaryDirectory()
238 self.nbdir = TemporaryDirectory()
238 self.nbdir = TemporaryDirectory()
239 self.dirs.append(self.ipydir)
239 self.dirs.append(self.ipydir)
240 self.dirs.append(self.nbdir)
240 self.dirs.append(self.nbdir)
241 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
241 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')))
242 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
243
243
244 if self.xunit:
244 if self.xunit:
245 self.add_xunit()
245 self.add_xunit()
246
246
247 # start the ipython notebook, so we get the port number
247 # start the ipython notebook, so we get the port number
248 self.server_port = 0
248 self.server_port = 0
249 self._init_server()
249 self._init_server()
250 if self.server_port:
250 if self.server_port:
251 self.cmd.append("--port=%i" % self.server_port)
251 self.cmd.append("--port=%i" % self.server_port)
252 else:
252 else:
253 # don't launch tests if the server didn't start
253 # don't launch tests if the server didn't start
254 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
254 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
255
255
256 def add_xunit(self):
256 def add_xunit(self):
257 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
257 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
258 self.cmd.append('--xunit=%s' % xunit_file)
258 self.cmd.append('--xunit=%s' % xunit_file)
259
259
260 def launch(self, buffer_output):
260 def launch(self, buffer_output):
261 # If the engine is SlimerJS, we need to buffer the output because
261 # If the engine is SlimerJS, we need to buffer the output because
262 # SlimerJS does not support exit codes, so CasperJS always returns 0.
262 # SlimerJS does not support exit codes, so CasperJS always returns 0.
263 if self.engine == 'slimerjs' and not buffer_output:
263 if self.engine == 'slimerjs' and not buffer_output:
264 self.display_slimer_output = True
264 self.display_slimer_output = True
265 return super(JSController, self).launch(buffer_output=True)
265 return super(JSController, self).launch(buffer_output=True)
266
266
267 else:
267 else:
268 return super(JSController, self).launch(buffer_output=buffer_output)
268 return super(JSController, self).launch(buffer_output=buffer_output)
269
269
270 def wait(self, *pargs, **kwargs):
270 def wait(self, *pargs, **kwargs):
271 """Wait for the JSController to finish"""
271 """Wait for the JSController to finish"""
272 ret = super(JSController, self).wait(*pargs, **kwargs)
272 ret = super(JSController, self).wait(*pargs, **kwargs)
273 # If this is a SlimerJS controller, check the captured stdout for
273 # If this is a SlimerJS controller, check the captured stdout for
274 # errors. Otherwise, just return the return code.
274 # errors. Otherwise, just return the return code.
275 if self.engine == 'slimerjs':
275 if self.engine == 'slimerjs':
276 stdout = bytes_to_str(self.stdout)
276 stdout = bytes_to_str(self.stdout)
277 if self.display_slimer_output:
277 if self.display_slimer_output:
278 print(stdout)
278 print(stdout)
279 if ret != 0:
279 if ret != 0:
280 # This could still happen e.g. if it's stopped by SIGINT
280 # This could still happen e.g. if it's stopped by SIGINT
281 return ret
281 return ret
282 return bool(self.slimer_failure.search(strip_ansi(stdout)))
282 return bool(self.slimer_failure.search(strip_ansi(stdout)))
283 else:
283 else:
284 return ret
284 return ret
285
285
286 def print_extra_info(self):
286 def print_extra_info(self):
287 print("Running tests with notebook directory %r" % self.nbdir.name)
287 print("Running tests with notebook directory %r" % self.nbdir.name)
288
288
289 @property
289 @property
290 def will_run(self):
290 def will_run(self):
291 return all(have[a] for a in self.requirements + [self.engine])
291 return all(have[a] for a in self.requirements + [self.engine])
292
292
293 def _init_server(self):
293 def _init_server(self):
294 "Start the notebook server in a separate process"
294 "Start the notebook server in a separate process"
295 self.server_command = command = [sys.executable,
295 self.server_command = command = [sys.executable,
296 '-m', 'IPython.html',
296 '-m', 'IPython.html',
297 '--no-browser',
297 '--no-browser',
298 '--ipython-dir', self.ipydir.name,
298 '--ipython-dir', self.ipydir.name,
299 '--notebook-dir', self.nbdir.name,
299 '--notebook-dir', self.nbdir.name,
300 ]
300 ]
301 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
301 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
302 # which run afoul of ipc's maximum path length.
302 # which run afoul of ipc's maximum path length.
303 if sys.platform.startswith('linux'):
303 if sys.platform.startswith('linux'):
304 command.append('--KernelManager.transport=ipc')
304 command.append('--KernelManager.transport=ipc')
305 self.stream_capturer = c = StreamCapturer()
305 self.stream_capturer = c = StreamCapturer()
306 c.start()
306 c.start()
307 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
307 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,
308 self.server_info_file = os.path.join(self.ipydir.name,
309 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
309 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
310 )
310 )
311 self._wait_for_server()
311 self._wait_for_server()
312
312
313 def _wait_for_server(self):
313 def _wait_for_server(self):
314 """Wait 30 seconds for the notebook server to start"""
314 """Wait 30 seconds for the notebook server to start"""
315 for i in range(300):
315 for i in range(300):
316 if self.server.poll() is not None:
316 if self.server.poll() is not None:
317 return self._failed_to_start()
317 return self._failed_to_start()
318 if os.path.exists(self.server_info_file):
318 if os.path.exists(self.server_info_file):
319 try:
319 try:
320 self._load_server_info()
320 self._load_server_info()
321 except ValueError:
321 except ValueError:
322 # If the server is halfway through writing the file, we may
322 # If the server is halfway through writing the file, we may
323 # get invalid JSON; it should be ready next iteration.
323 # get invalid JSON; it should be ready next iteration.
324 pass
324 pass
325 else:
325 else:
326 return
326 return
327 time.sleep(0.1)
327 time.sleep(0.1)
328 print("Notebook server-info file never arrived: %s" % self.server_info_file,
328 print("Notebook server-info file never arrived: %s" % self.server_info_file,
329 file=sys.stderr
329 file=sys.stderr
330 )
330 )
331
331
332 def _failed_to_start(self):
332 def _failed_to_start(self):
333 """Notebook server exited prematurely"""
333 """Notebook server exited prematurely"""
334 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
334 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
335 print("Notebook failed to start: ", file=sys.stderr)
335 print("Notebook failed to start: ", file=sys.stderr)
336 print(self.server_command)
336 print(self.server_command)
337 print(captured, file=sys.stderr)
337 print(captured, file=sys.stderr)
338
338
339 def _load_server_info(self):
339 def _load_server_info(self):
340 """Notebook server started, load connection info from JSON"""
340 """Notebook server started, load connection info from JSON"""
341 with open(self.server_info_file) as f:
341 with open(self.server_info_file) as f:
342 info = json.load(f)
342 info = json.load(f)
343 self.server_port = info['port']
343 self.server_port = info['port']
344
344
345 def cleanup(self):
345 def cleanup(self):
346 try:
346 try:
347 self.server.terminate()
347 self.server.terminate()
348 except OSError:
348 except OSError:
349 # already dead
349 # already dead
350 pass
350 pass
351 # wait 10s for the server to shutdown
351 # wait 10s for the server to shutdown
352 try:
352 try:
353 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
353 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
354 except TimeoutExpired:
354 except TimeoutExpired:
355 # server didn't terminate, kill it
355 # server didn't terminate, kill it
356 try:
356 try:
357 print("Failed to terminate notebook server, killing it.",
357 print("Failed to terminate notebook server, killing it.",
358 file=sys.stderr
358 file=sys.stderr
359 )
359 )
360 self.server.kill()
360 self.server.kill()
361 except OSError:
361 except OSError:
362 # already dead
362 # already dead
363 pass
363 pass
364 # wait another 10s
364 # wait another 10s
365 try:
365 try:
366 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
366 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
367 except TimeoutExpired:
367 except TimeoutExpired:
368 print("Notebook server still running (%s)" % self.server_info_file,
368 print("Notebook server still running (%s)" % self.server_info_file,
369 file=sys.stderr
369 file=sys.stderr
370 )
370 )
371
371
372 self.stream_capturer.halt()
372 self.stream_capturer.halt()
373 TestController.cleanup(self)
373 TestController.cleanup(self)
374
374
375
375
376 def prepare_controllers(options):
376 def prepare_controllers(options):
377 """Returns two lists of TestController instances, those to run, and those
377 """Returns two lists of TestController instances, those to run, and those
378 not to run."""
378 not to run."""
379 testgroups = options.testgroups
379 testgroups = options.testgroups
380 if testgroups:
380 if testgroups:
381 if 'js' in testgroups:
381 if 'js' in testgroups:
382 js_testgroups = all_js_groups()
382 js_testgroups = all_js_groups()
383 else:
383 else:
384 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
384 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
385
385
386 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
386 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
387 else:
387 else:
388 py_testgroups = py_test_group_names
388 py_testgroups = py_test_group_names
389 if not options.all:
389 if not options.all:
390 js_testgroups = []
390 js_testgroups = []
391 test_sections['parallel'].enabled = False
391 test_sections['parallel'].enabled = False
392 else:
392 else:
393 js_testgroups = all_js_groups()
393 js_testgroups = all_js_groups()
394
394
395 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
395 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
396 c_js = [JSController(name, xunit=options.xunit, engine=engine) for name in js_testgroups]
396 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]
397 c_py = [PyTestController(name, options) for name in py_testgroups]
398
398
399 controllers = c_py + c_js
399 controllers = c_py + c_js
400 to_run = [c for c in controllers if c.will_run]
400 to_run = [c for c in controllers if c.will_run]
401 not_run = [c for c in controllers if not c.will_run]
401 not_run = [c for c in controllers if not c.will_run]
402 return to_run, not_run
402 return to_run, not_run
403
403
404 def do_run(controller, buffer_output=True):
404 def do_run(controller, buffer_output=True):
405 """Setup and run a test controller.
405 """Setup and run a test controller.
406
406
407 If buffer_output is True, no output is displayed, to avoid it appearing
407 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
408 interleaved. In this case, the caller is responsible for displaying test
409 output on failure.
409 output on failure.
410
410
411 Returns
411 Returns
412 -------
412 -------
413 controller : TestController
413 controller : TestController
414 The same controller as passed in, as a convenience for using map() type
414 The same controller as passed in, as a convenience for using map() type
415 APIs.
415 APIs.
416 exitcode : int
416 exitcode : int
417 The exit code of the test subprocess. Non-zero indicates failure.
417 The exit code of the test subprocess. Non-zero indicates failure.
418 """
418 """
419 try:
419 try:
420 try:
420 try:
421 controller.setup()
421 controller.setup()
422 if not buffer_output:
422 if not buffer_output:
423 controller.print_extra_info()
423 controller.print_extra_info()
424 controller.launch(buffer_output=buffer_output)
424 controller.launch(buffer_output=buffer_output)
425 except Exception:
425 except Exception:
426 import traceback
426 import traceback
427 traceback.print_exc()
427 traceback.print_exc()
428 return controller, 1 # signal failure
428 return controller, 1 # signal failure
429
429
430 exitcode = controller.wait()
430 exitcode = controller.wait()
431 return controller, exitcode
431 return controller, exitcode
432
432
433 except KeyboardInterrupt:
433 except KeyboardInterrupt:
434 return controller, -signal.SIGINT
434 return controller, -signal.SIGINT
435 finally:
435 finally:
436 controller.cleanup()
436 controller.cleanup()
437
437
438 def report():
438 def report():
439 """Return a string with a summary report of test-related variables."""
439 """Return a string with a summary report of test-related variables."""
440 inf = get_sys_info()
440 inf = get_sys_info()
441 out = []
441 out = []
442 def _add(name, value):
442 def _add(name, value):
443 out.append((name, value))
443 out.append((name, value))
444
444
445 _add('IPython version', inf['ipython_version'])
445 _add('IPython version', inf['ipython_version'])
446 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
446 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
447 _add('IPython package', compress_user(inf['ipython_path']))
447 _add('IPython package', compress_user(inf['ipython_path']))
448 _add('Python version', inf['sys_version'].replace('\n',''))
448 _add('Python version', inf['sys_version'].replace('\n',''))
449 _add('sys.executable', compress_user(inf['sys_executable']))
449 _add('sys.executable', compress_user(inf['sys_executable']))
450 _add('Platform', inf['platform'])
450 _add('Platform', inf['platform'])
451
451
452 width = max(len(n) for (n,v) in out)
452 width = max(len(n) for (n,v) in out)
453 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
453 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
454
454
455 avail = []
455 avail = []
456 not_avail = []
456 not_avail = []
457
457
458 for k, is_avail in have.items():
458 for k, is_avail in have.items():
459 if is_avail:
459 if is_avail:
460 avail.append(k)
460 avail.append(k)
461 else:
461 else:
462 not_avail.append(k)
462 not_avail.append(k)
463
463
464 if avail:
464 if avail:
465 out.append('\nTools and libraries available at test time:\n')
465 out.append('\nTools and libraries available at test time:\n')
466 avail.sort()
466 avail.sort()
467 out.append(' ' + ' '.join(avail)+'\n')
467 out.append(' ' + ' '.join(avail)+'\n')
468
468
469 if not_avail:
469 if not_avail:
470 out.append('\nTools and libraries NOT available at test time:\n')
470 out.append('\nTools and libraries NOT available at test time:\n')
471 not_avail.sort()
471 not_avail.sort()
472 out.append(' ' + ' '.join(not_avail)+'\n')
472 out.append(' ' + ' '.join(not_avail)+'\n')
473
473
474 return ''.join(out)
474 return ''.join(out)
475
475
476 def run_iptestall(options):
476 def run_iptestall(options):
477 """Run the entire IPython test suite by calling nose and trial.
477 """Run the entire IPython test suite by calling nose and trial.
478
478
479 This function constructs :class:`IPTester` instances for all IPython
479 This function constructs :class:`IPTester` instances for all IPython
480 modules and package and then runs each of them. This causes the modules
480 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
481 and packages of IPython to be tested each in their own subprocess using
482 nose.
482 nose.
483
483
484 Parameters
484 Parameters
485 ----------
485 ----------
486
486
487 All parameters are passed as attributes of the options object.
487 All parameters are passed as attributes of the options object.
488
488
489 testgroups : list of str
489 testgroups : list of str
490 Run only these sections of the test suite. If empty, run all the available
490 Run only these sections of the test suite. If empty, run all the available
491 sections.
491 sections.
492
492
493 fast : int or None
493 fast : int or None
494 Run the test suite in parallel, using n simultaneous processes. If None
494 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)
495 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
496
496
497 inc_slow : bool
497 inc_slow : bool
498 Include slow tests, like IPython.parallel. By default, these tests aren't
498 Include slow tests, like IPython.parallel. By default, these tests aren't
499 run.
499 run.
500
500
501 slimerjs : bool
501 slimerjs : bool
502 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
502 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
503
503
504 xunit : bool
504 xunit : bool
505 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
505 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
506
506
507 coverage : bool or str
507 coverage : bool or str
508 Measure code coverage from tests. True will store the raw coverage data,
508 Measure code coverage from tests. True will store the raw coverage data,
509 or pass 'html' or 'xml' to get reports.
509 or pass 'html' or 'xml' to get reports.
510
510
511 extra_args : list
511 extra_args : list
512 Extra arguments to pass to the test subprocesses, e.g. '-v'
512 Extra arguments to pass to the test subprocesses, e.g. '-v'
513 """
513 """
514 to_run, not_run = prepare_controllers(options)
514 to_run, not_run = prepare_controllers(options)
515
515
516 def justify(ltext, rtext, width=70, fill='-'):
516 def justify(ltext, rtext, width=70, fill='-'):
517 ltext += ' '
517 ltext += ' '
518 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
518 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
519 return ltext + rtext
519 return ltext + rtext
520
520
521 # Run all test runners, tracking execution time
521 # Run all test runners, tracking execution time
522 failed = []
522 failed = []
523 t_start = time.time()
523 t_start = time.time()
524
524
525 print()
525 print()
526 if options.fast == 1:
526 if options.fast == 1:
527 # This actually means sequential, i.e. with 1 job
527 # This actually means sequential, i.e. with 1 job
528 for controller in to_run:
528 for controller in to_run:
529 print('Test group:', controller.section)
529 print('Test group:', controller.section)
530 sys.stdout.flush() # Show in correct order when output is piped
530 sys.stdout.flush() # Show in correct order when output is piped
531 controller, res = do_run(controller, buffer_output=False)
531 controller, res = do_run(controller, buffer_output=False)
532 if res:
532 if res:
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 print()
537 print()
538
538
539 else:
539 else:
540 # Run tests concurrently
540 # Run tests concurrently
541 try:
541 try:
542 pool = multiprocessing.pool.ThreadPool(options.fast)
542 pool = multiprocessing.pool.ThreadPool(options.fast)
543 for (controller, res) in pool.imap_unordered(do_run, to_run):
543 for (controller, res) in pool.imap_unordered(do_run, to_run):
544 res_string = 'OK' if res == 0 else 'FAILED'
544 res_string = 'OK' if res == 0 else 'FAILED'
545 print(justify('Test group: ' + controller.section, res_string))
545 print(justify('Test group: ' + controller.section, res_string))
546 if res:
546 if res:
547 controller.print_extra_info()
547 controller.print_extra_info()
548 print(bytes_to_str(controller.stdout))
548 print(bytes_to_str(controller.stdout))
549 failed.append(controller)
549 failed.append(controller)
550 if res == -signal.SIGINT:
550 if res == -signal.SIGINT:
551 print("Interrupted")
551 print("Interrupted")
552 break
552 break
553 except KeyboardInterrupt:
553 except KeyboardInterrupt:
554 return
554 return
555
555
556 for controller in not_run:
556 for controller in not_run:
557 print(justify('Test group: ' + controller.section, 'NOT RUN'))
557 print(justify('Test group: ' + controller.section, 'NOT RUN'))
558
558
559 t_end = time.time()
559 t_end = time.time()
560 t_tests = t_end - t_start
560 t_tests = t_end - t_start
561 nrunners = len(to_run)
561 nrunners = len(to_run)
562 nfail = len(failed)
562 nfail = len(failed)
563 # summarize results
563 # summarize results
564 print('_'*70)
564 print('_'*70)
565 print('Test suite completed for system with the following information:')
565 print('Test suite completed for system with the following information:')
566 print(report())
566 print(report())
567 took = "Took %.3fs." % t_tests
567 took = "Took %.3fs." % t_tests
568 print('Status: ', end='')
568 print('Status: ', end='')
569 if not failed:
569 if not failed:
570 print('OK (%d test groups).' % nrunners, took)
570 print('OK (%d test groups).' % nrunners, took)
571 else:
571 else:
572 # If anything went wrong, point out what command to rerun manually to
572 # If anything went wrong, point out what command to rerun manually to
573 # see the actual errors and individual summary
573 # see the actual errors and individual summary
574 failed_sections = [c.section for c in failed]
574 failed_sections = [c.section for c in failed]
575 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
575 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
576 nrunners, ', '.join(failed_sections)), took)
576 nrunners, ', '.join(failed_sections)), took)
577 print()
577 print()
578 print('You may wish to rerun these, with:')
578 print('You may wish to rerun these, with:')
579 print(' iptest', *failed_sections)
579 print(' iptest', *failed_sections)
580 print()
580 print()
581
581
582 if options.coverage:
582 if options.coverage:
583 from coverage import coverage
583 from coverage import coverage
584 cov = coverage(data_file='.coverage')
584 cov = coverage(data_file='.coverage')
585 cov.combine()
585 cov.combine()
586 cov.save()
586 cov.save()
587
587
588 # Coverage HTML report
588 # Coverage HTML report
589 if options.coverage == 'html':
589 if options.coverage == 'html':
590 html_dir = 'ipy_htmlcov'
590 html_dir = 'ipy_htmlcov'
591 shutil.rmtree(html_dir, ignore_errors=True)
591 shutil.rmtree(html_dir, ignore_errors=True)
592 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
592 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
593 sys.stdout.flush()
593 sys.stdout.flush()
594
594
595 # Custom HTML reporter to clean up module names.
595 # Custom HTML reporter to clean up module names.
596 from coverage.html import HtmlReporter
596 from coverage.html import HtmlReporter
597 class CustomHtmlReporter(HtmlReporter):
597 class CustomHtmlReporter(HtmlReporter):
598 def find_code_units(self, morfs):
598 def find_code_units(self, morfs):
599 super(CustomHtmlReporter, self).find_code_units(morfs)
599 super(CustomHtmlReporter, self).find_code_units(morfs)
600 for cu in self.code_units:
600 for cu in self.code_units:
601 nameparts = cu.name.split(os.sep)
601 nameparts = cu.name.split(os.sep)
602 if 'IPython' not in nameparts:
602 if 'IPython' not in nameparts:
603 continue
603 continue
604 ix = nameparts.index('IPython')
604 ix = nameparts.index('IPython')
605 cu.name = '.'.join(nameparts[ix:])
605 cu.name = '.'.join(nameparts[ix:])
606
606
607 # Reimplement the html_report method with our custom reporter
607 # Reimplement the html_report method with our custom reporter
608 cov._harvest_data()
608 cov._harvest_data()
609 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
609 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
610 html_title='IPython test coverage',
610 html_title='IPython test coverage',
611 )
611 )
612 reporter = CustomHtmlReporter(cov, cov.config)
612 reporter = CustomHtmlReporter(cov, cov.config)
613 reporter.report(None)
613 reporter.report(None)
614 print('done.')
614 print('done.')
615
615
616 # Coverage XML report
616 # Coverage XML report
617 elif options.coverage == 'xml':
617 elif options.coverage == 'xml':
618 cov.xml_report(outfile='ipy_coverage.xml')
618 cov.xml_report(outfile='ipy_coverage.xml')
619
619
620 if failed:
620 if failed:
621 # Ensure that our exit code indicates failure
621 # Ensure that our exit code indicates failure
622 sys.exit(1)
622 sys.exit(1)
623
623
624 argparser = argparse.ArgumentParser(description='Run IPython test suite')
624 argparser = argparse.ArgumentParser(description='Run IPython test suite')
625 argparser.add_argument('testgroups', nargs='*',
625 argparser.add_argument('testgroups', nargs='*',
626 help='Run specified groups of tests. If omitted, run '
626 help='Run specified groups of tests. If omitted, run '
627 'all tests.')
627 'all tests.')
628 argparser.add_argument('--all', action='store_true',
628 argparser.add_argument('--all', action='store_true',
629 help='Include slow tests not run by default.')
629 help='Include slow tests not run by default.')
630 argparser.add_argument('--slimerjs', action='store_true',
630 argparser.add_argument('--slimerjs', action='store_true',
631 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
631 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,
632 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
633 help='Run test sections in parallel. This starts as many '
633 help='Run test sections in parallel. This starts as many '
634 'processes as you have cores, or you can specify a number.')
634 'processes as you have cores, or you can specify a number.')
635 argparser.add_argument('--xunit', action='store_true',
635 argparser.add_argument('--xunit', action='store_true',
636 help='Produce Xunit XML results')
636 help='Produce Xunit XML results')
637 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
637 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
638 help="Measure test coverage. Specify 'html' or "
638 help="Measure test coverage. Specify 'html' or "
639 "'xml' to get reports.")
639 "'xml' to get reports.")
640 argparser.add_argument('--subproc-streams', default='capture',
640 argparser.add_argument('--subproc-streams', default='capture',
641 help="What to do with stdout/stderr from subprocesses. "
641 help="What to do with stdout/stderr from subprocesses. "
642 "'capture' (default), 'show' and 'discard' are the options.")
642 "'capture' (default), 'show' and 'discard' are the options.")
643
643
644 def default_options():
644 def default_options():
645 """Get an argparse Namespace object with the default arguments, to pass to
645 """Get an argparse Namespace object with the default arguments, to pass to
646 :func:`run_iptestall`.
646 :func:`run_iptestall`.
647 """
647 """
648 options = argparser.parse_args([])
648 options = argparser.parse_args([])
649 options.extra_args = []
649 options.extra_args = []
650 return options
650 return options
651
651
652 def main():
652 def main():
653 # iptest doesn't work correctly if the working directory is the
653 # iptest doesn't work correctly if the working directory is the
654 # root of the IPython source tree. Tell the user to avoid
654 # root of the IPython source tree. Tell the user to avoid
655 # frustration.
655 # frustration.
656 if os.path.exists(os.path.join(os.getcwd(),
656 if os.path.exists(os.path.join(os.getcwd(),
657 'IPython', 'testing', '__main__.py')):
657 'IPython', 'testing', '__main__.py')):
658 print("Don't run iptest from the IPython source directory",
658 print("Don't run iptest from the IPython source directory",
659 file=sys.stderr)
659 file=sys.stderr)
660 sys.exit(1)
660 sys.exit(1)
661 # Arguments after -- should be passed through to nose. Argparse treats
661 # Arguments after -- should be passed through to nose. Argparse treats
662 # everything after -- as regular positional arguments, so we separate them
662 # everything after -- as regular positional arguments, so we separate them
663 # first.
663 # first.
664 try:
664 try:
665 ix = sys.argv.index('--')
665 ix = sys.argv.index('--')
666 except ValueError:
666 except ValueError:
667 to_parse = sys.argv[1:]
667 to_parse = sys.argv[1:]
668 extra_args = []
668 extra_args = []
669 else:
669 else:
670 to_parse = sys.argv[1:ix]
670 to_parse = sys.argv[1:ix]
671 extra_args = sys.argv[ix+1:]
671 extra_args = sys.argv[ix+1:]
672
672
673 options = argparser.parse_args(to_parse)
673 options = argparser.parse_args(to_parse)
674 options.extra_args = extra_args
674 options.extra_args = extra_args
675
675
676 run_iptestall(options)
676 run_iptestall(options)
677
677
678
678
679 if __name__ == '__main__':
679 if __name__ == '__main__':
680 main()
680 main()
General Comments 0
You need to be logged in to leave comments. Login now