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