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