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