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