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