##// END OF EJS Templates
Merge pull request #2238 from takluyver/fasttest...
Bussonnier Matthias -
r8131:ecae5d1b merge
parent child Browse files
Show More
@@ -1,29 +1,29 b''
1 """Testing support (tools to test IPython itself).
1 """Testing support (tools to test IPython itself).
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2009-2011 The IPython Development Team
5 # Copyright (C) 2009-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Functions
12 # Functions
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 # User-level entry point for testing
15 # User-level entry point for testing
16 def test():
16 def test(all=False):
17 """Run the entire IPython test suite.
17 """Run the entire IPython test suite.
18
18
19 For fine-grained control, you should use the :file:`iptest` script supplied
19 For fine-grained control, you should use the :file:`iptest` script supplied
20 with the IPython installation."""
20 with the IPython installation."""
21
21
22 # Do the import internally, so that this function doesn't increase total
22 # Do the import internally, so that this function doesn't increase total
23 # import time
23 # import time
24 from iptest import run_iptestall
24 from iptest import run_iptestall
25 run_iptestall()
25 run_iptestall(inc_slow=all)
26
26
27 # So nose doesn't try to run this as a test itself and we end up with an
27 # So nose doesn't try to run this as a test itself and we end up with an
28 # infinite test loop
28 # infinite test loop
29 test.__test__ = False
29 test.__test__ = False
@@ -1,566 +1,579 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
30 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import signal
33 import signal
34 import sys
34 import sys
35 import subprocess
35 import subprocess
36 import tempfile
36 import tempfile
37 import time
37 import time
38 import warnings
38 import warnings
39
39
40 # Note: monkeypatch!
40 # Note: monkeypatch!
41 # We need to monkeypatch a small problem in nose itself first, before importing
41 # We need to monkeypatch a small problem in nose itself first, before importing
42 # it for actual use. This should get into nose upstream, but its release cycle
42 # it for actual use. This should get into nose upstream, but its release cycle
43 # is slow and we need it for our parametric tests to work correctly.
43 # is slow and we need it for our parametric tests to work correctly.
44 from IPython.testing import nosepatch
44 from IPython.testing import nosepatch
45
45
46 # Monkeypatch extra assert methods into nose.tools if they're not already there.
46 # Monkeypatch extra assert methods into nose.tools if they're not already there.
47 # This can be dropped once we no longer test on Python 2.6
47 # This can be dropped once we no longer test on Python 2.6
48 from IPython.testing import nose_assert_methods
48 from IPython.testing import nose_assert_methods
49
49
50 # Now, proceed to import nose itself
50 # Now, proceed to import nose itself
51 import nose.plugins.builtin
51 import nose.plugins.builtin
52 from nose.plugins.xunit import Xunit
52 from nose.plugins.xunit import Xunit
53 from nose import SkipTest
53 from nose import SkipTest
54 from nose.core import TestProgram
54 from nose.core import TestProgram
55
55
56 # Our own imports
56 # Our own imports
57 from IPython.utils import py3compat
57 from IPython.utils import py3compat
58 from IPython.utils.importstring import import_item
58 from IPython.utils.importstring import import_item
59 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
59 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
60 from IPython.utils.process import find_cmd, pycmd2argv
60 from IPython.utils.process import find_cmd, pycmd2argv
61 from IPython.utils.sysinfo import sys_info
61 from IPython.utils.sysinfo import sys_info
62 from IPython.utils.tempdir import TemporaryDirectory
62 from IPython.utils.tempdir import TemporaryDirectory
63 from IPython.utils.warn import warn
63 from IPython.utils.warn import warn
64
64
65 from IPython.testing import globalipapp
65 from IPython.testing import globalipapp
66 from IPython.testing.plugin.ipdoctest import IPythonDoctest
66 from IPython.testing.plugin.ipdoctest import IPythonDoctest
67 from IPython.external.decorators import KnownFailure, knownfailureif
67 from IPython.external.decorators import KnownFailure, knownfailureif
68
68
69 pjoin = path.join
69 pjoin = path.join
70
70
71
71
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 # Globals
73 # Globals
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76
76
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78 # Warnings control
78 # Warnings control
79 #-----------------------------------------------------------------------------
79 #-----------------------------------------------------------------------------
80
80
81 # Twisted generates annoying warnings with Python 2.6, as will do other code
81 # Twisted generates annoying warnings with Python 2.6, as will do other code
82 # that imports 'sets' as of today
82 # that imports 'sets' as of today
83 warnings.filterwarnings('ignore', 'the sets module is deprecated',
83 warnings.filterwarnings('ignore', 'the sets module is deprecated',
84 DeprecationWarning )
84 DeprecationWarning )
85
85
86 # This one also comes from Twisted
86 # This one also comes from Twisted
87 warnings.filterwarnings('ignore', 'the sha module is deprecated',
87 warnings.filterwarnings('ignore', 'the sha module is deprecated',
88 DeprecationWarning)
88 DeprecationWarning)
89
89
90 # Wx on Fedora11 spits these out
90 # Wx on Fedora11 spits these out
91 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
91 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
92 UserWarning)
92 UserWarning)
93
93
94 # ------------------------------------------------------------------------------
94 # ------------------------------------------------------------------------------
95 # Monkeypatch Xunit to count known failures as skipped.
95 # Monkeypatch Xunit to count known failures as skipped.
96 # ------------------------------------------------------------------------------
96 # ------------------------------------------------------------------------------
97 def monkeypatch_xunit():
97 def monkeypatch_xunit():
98 try:
98 try:
99 knownfailureif(True)(lambda: None)()
99 knownfailureif(True)(lambda: None)()
100 except Exception as e:
100 except Exception as e:
101 KnownFailureTest = type(e)
101 KnownFailureTest = type(e)
102
102
103 def addError(self, test, err, capt=None):
103 def addError(self, test, err, capt=None):
104 if issubclass(err[0], KnownFailureTest):
104 if issubclass(err[0], KnownFailureTest):
105 err = (SkipTest,) + err[1:]
105 err = (SkipTest,) + err[1:]
106 return self.orig_addError(test, err, capt)
106 return self.orig_addError(test, err, capt)
107
107
108 Xunit.orig_addError = Xunit.addError
108 Xunit.orig_addError = Xunit.addError
109 Xunit.addError = addError
109 Xunit.addError = addError
110
110
111 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
112 # Logic for skipping doctests
112 # Logic for skipping doctests
113 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
114 def extract_version(mod):
114 def extract_version(mod):
115 return mod.__version__
115 return mod.__version__
116
116
117 def test_for(item, min_version=None, callback=extract_version):
117 def test_for(item, min_version=None, callback=extract_version):
118 """Test to see if item is importable, and optionally check against a minimum
118 """Test to see if item is importable, and optionally check against a minimum
119 version.
119 version.
120
120
121 If min_version is given, the default behavior is to check against the
121 If min_version is given, the default behavior is to check against the
122 `__version__` attribute of the item, but specifying `callback` allows you to
122 `__version__` attribute of the item, but specifying `callback` allows you to
123 extract the value you are interested in. e.g::
123 extract the value you are interested in. e.g::
124
124
125 In [1]: import sys
125 In [1]: import sys
126
126
127 In [2]: from IPython.testing.iptest import test_for
127 In [2]: from IPython.testing.iptest import test_for
128
128
129 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
129 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
130 Out[3]: True
130 Out[3]: True
131
131
132 """
132 """
133 try:
133 try:
134 check = import_item(item)
134 check = import_item(item)
135 except (ImportError, RuntimeError):
135 except (ImportError, RuntimeError):
136 # GTK reports Runtime error if it can't be initialized even if it's
136 # GTK reports Runtime error if it can't be initialized even if it's
137 # importable.
137 # importable.
138 return False
138 return False
139 else:
139 else:
140 if min_version:
140 if min_version:
141 if callback:
141 if callback:
142 # extra processing step to get version to compare
142 # extra processing step to get version to compare
143 check = callback(check)
143 check = callback(check)
144
144
145 return check >= min_version
145 return check >= min_version
146 else:
146 else:
147 return True
147 return True
148
148
149 # Global dict where we can store information on what we have and what we don't
149 # Global dict where we can store information on what we have and what we don't
150 # have available at test run time
150 # have available at test run time
151 have = {}
151 have = {}
152
152
153 have['curses'] = test_for('_curses')
153 have['curses'] = test_for('_curses')
154 have['matplotlib'] = test_for('matplotlib')
154 have['matplotlib'] = test_for('matplotlib')
155 have['numpy'] = test_for('numpy')
155 have['numpy'] = test_for('numpy')
156 have['pexpect'] = test_for('IPython.external.pexpect')
156 have['pexpect'] = test_for('IPython.external.pexpect')
157 have['pymongo'] = test_for('pymongo')
157 have['pymongo'] = test_for('pymongo')
158 have['pygments'] = test_for('pygments')
158 have['pygments'] = test_for('pygments')
159 have['qt'] = test_for('IPython.external.qt')
159 have['qt'] = test_for('IPython.external.qt')
160 have['rpy2'] = test_for('rpy2')
160 have['rpy2'] = test_for('rpy2')
161 have['sqlite3'] = test_for('sqlite3')
161 have['sqlite3'] = test_for('sqlite3')
162 have['cython'] = test_for('Cython')
162 have['cython'] = test_for('Cython')
163 have['oct2py'] = test_for('oct2py')
163 have['oct2py'] = test_for('oct2py')
164 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
164 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
165 have['wx'] = test_for('wx')
165 have['wx'] = test_for('wx')
166 have['wx.aui'] = test_for('wx.aui')
166 have['wx.aui'] = test_for('wx.aui')
167
167
168 if os.name == 'nt':
168 if os.name == 'nt':
169 min_zmq = (2,1,7)
169 min_zmq = (2,1,7)
170 else:
170 else:
171 min_zmq = (2,1,4)
171 min_zmq = (2,1,4)
172
172
173 def version_tuple(mod):
173 def version_tuple(mod):
174 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
174 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
175 # turn 'dev' into 999, because Python3 rejects str-int comparisons
175 # turn 'dev' into 999, because Python3 rejects str-int comparisons
176 vs = mod.__version__.replace('dev', '.999')
176 vs = mod.__version__.replace('dev', '.999')
177 tup = tuple([int(v) for v in vs.split('.') ])
177 tup = tuple([int(v) for v in vs.split('.') ])
178 return tup
178 return tup
179
179
180 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
180 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
181
181
182 #-----------------------------------------------------------------------------
182 #-----------------------------------------------------------------------------
183 # Functions and classes
183 # Functions and classes
184 #-----------------------------------------------------------------------------
184 #-----------------------------------------------------------------------------
185
185
186 def report():
186 def report():
187 """Return a string with a summary report of test-related variables."""
187 """Return a string with a summary report of test-related variables."""
188
188
189 out = [ sys_info(), '\n']
189 out = [ sys_info(), '\n']
190
190
191 avail = []
191 avail = []
192 not_avail = []
192 not_avail = []
193
193
194 for k, is_avail in have.items():
194 for k, is_avail in have.items():
195 if is_avail:
195 if is_avail:
196 avail.append(k)
196 avail.append(k)
197 else:
197 else:
198 not_avail.append(k)
198 not_avail.append(k)
199
199
200 if avail:
200 if avail:
201 out.append('\nTools and libraries available at test time:\n')
201 out.append('\nTools and libraries available at test time:\n')
202 avail.sort()
202 avail.sort()
203 out.append(' ' + ' '.join(avail)+'\n')
203 out.append(' ' + ' '.join(avail)+'\n')
204
204
205 if not_avail:
205 if not_avail:
206 out.append('\nTools and libraries NOT available at test time:\n')
206 out.append('\nTools and libraries NOT available at test time:\n')
207 not_avail.sort()
207 not_avail.sort()
208 out.append(' ' + ' '.join(not_avail)+'\n')
208 out.append(' ' + ' '.join(not_avail)+'\n')
209
209
210 return ''.join(out)
210 return ''.join(out)
211
211
212
212
213 def make_exclude():
213 def make_exclude():
214 """Make patterns of modules and packages to exclude from testing.
214 """Make patterns of modules and packages to exclude from testing.
215
215
216 For the IPythonDoctest plugin, we need to exclude certain patterns that
216 For the IPythonDoctest plugin, we need to exclude certain patterns that
217 cause testing problems. We should strive to minimize the number of
217 cause testing problems. We should strive to minimize the number of
218 skipped modules, since this means untested code.
218 skipped modules, since this means untested code.
219
219
220 These modules and packages will NOT get scanned by nose at all for tests.
220 These modules and packages will NOT get scanned by nose at all for tests.
221 """
221 """
222 # Simple utility to make IPython paths more readably, we need a lot of
222 # Simple utility to make IPython paths more readably, we need a lot of
223 # these below
223 # these below
224 ipjoin = lambda *paths: pjoin('IPython', *paths)
224 ipjoin = lambda *paths: pjoin('IPython', *paths)
225
225
226 exclusions = [ipjoin('external'),
226 exclusions = [ipjoin('external'),
227 ipjoin('quarantine'),
227 ipjoin('quarantine'),
228 ipjoin('deathrow'),
228 ipjoin('deathrow'),
229 # This guy is probably attic material
229 # This guy is probably attic material
230 ipjoin('testing', 'mkdoctests'),
230 ipjoin('testing', 'mkdoctests'),
231 # Testing inputhook will need a lot of thought, to figure out
231 # Testing inputhook will need a lot of thought, to figure out
232 # how to have tests that don't lock up with the gui event
232 # how to have tests that don't lock up with the gui event
233 # loops in the picture
233 # loops in the picture
234 ipjoin('lib', 'inputhook'),
234 ipjoin('lib', 'inputhook'),
235 # Config files aren't really importable stand-alone
235 # Config files aren't really importable stand-alone
236 ipjoin('config', 'profile'),
236 ipjoin('config', 'profile'),
237 # The notebook 'static' directory contains JS, css and other
237 # The notebook 'static' directory contains JS, css and other
238 # files for web serving. Occasionally projects may put a .py
238 # files for web serving. Occasionally projects may put a .py
239 # file in there (MathJax ships a conf.py), so we might as
239 # file in there (MathJax ships a conf.py), so we might as
240 # well play it safe and skip the whole thing.
240 # well play it safe and skip the whole thing.
241 ipjoin('frontend', 'html', 'notebook', 'static')
241 ipjoin('frontend', 'html', 'notebook', 'static')
242 ]
242 ]
243 if not have['sqlite3']:
243 if not have['sqlite3']:
244 exclusions.append(ipjoin('core', 'tests', 'test_history'))
244 exclusions.append(ipjoin('core', 'tests', 'test_history'))
245 exclusions.append(ipjoin('core', 'history'))
245 exclusions.append(ipjoin('core', 'history'))
246 if not have['wx']:
246 if not have['wx']:
247 exclusions.append(ipjoin('lib', 'inputhookwx'))
247 exclusions.append(ipjoin('lib', 'inputhookwx'))
248
248
249 # FIXME: temporarily disable autoreload tests, as they can produce
249 # FIXME: temporarily disable autoreload tests, as they can produce
250 # spurious failures in subsequent tests (cythonmagic).
250 # spurious failures in subsequent tests (cythonmagic).
251 exclusions.append(ipjoin('extensions', 'autoreload'))
251 exclusions.append(ipjoin('extensions', 'autoreload'))
252 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
252 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
253
253
254 # We do this unconditionally, so that the test suite doesn't import
254 # We do this unconditionally, so that the test suite doesn't import
255 # gtk, changing the default encoding and masking some unicode bugs.
255 # gtk, changing the default encoding and masking some unicode bugs.
256 exclusions.append(ipjoin('lib', 'inputhookgtk'))
256 exclusions.append(ipjoin('lib', 'inputhookgtk'))
257 exclusions.append(ipjoin('zmq', 'gui', 'gtkembed'))
257 exclusions.append(ipjoin('zmq', 'gui', 'gtkembed'))
258
258
259 # These have to be skipped on win32 because the use echo, rm, cd, etc.
259 # These have to be skipped on win32 because the use echo, rm, cd, etc.
260 # See ticket https://github.com/ipython/ipython/issues/87
260 # See ticket https://github.com/ipython/ipython/issues/87
261 if sys.platform == 'win32':
261 if sys.platform == 'win32':
262 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
262 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
263 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
263 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
264
264
265 if not have['pexpect']:
265 if not have['pexpect']:
266 exclusions.extend([ipjoin('lib', 'irunner'),
266 exclusions.extend([ipjoin('lib', 'irunner'),
267 ipjoin('lib', 'tests', 'test_irunner'),
267 ipjoin('lib', 'tests', 'test_irunner'),
268 ipjoin('frontend', 'terminal', 'console'),
268 ipjoin('frontend', 'terminal', 'console'),
269 ])
269 ])
270
270
271 if not have['zmq']:
271 if not have['zmq']:
272 exclusions.append(ipjoin('zmq'))
272 exclusions.append(ipjoin('zmq'))
273 exclusions.append(ipjoin('frontend', 'qt'))
273 exclusions.append(ipjoin('frontend', 'qt'))
274 exclusions.append(ipjoin('frontend', 'html'))
274 exclusions.append(ipjoin('frontend', 'html'))
275 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
275 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
276 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
276 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
277 exclusions.append(ipjoin('parallel'))
277 exclusions.append(ipjoin('parallel'))
278 elif not have['qt'] or not have['pygments']:
278 elif not have['qt'] or not have['pygments']:
279 exclusions.append(ipjoin('frontend', 'qt'))
279 exclusions.append(ipjoin('frontend', 'qt'))
280
280
281 if not have['pymongo']:
281 if not have['pymongo']:
282 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
282 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
283 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
283 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
284
284
285 if not have['matplotlib']:
285 if not have['matplotlib']:
286 exclusions.extend([ipjoin('core', 'pylabtools'),
286 exclusions.extend([ipjoin('core', 'pylabtools'),
287 ipjoin('core', 'tests', 'test_pylabtools'),
287 ipjoin('core', 'tests', 'test_pylabtools'),
288 ipjoin('zmq', 'pylab'),
288 ipjoin('zmq', 'pylab'),
289 ])
289 ])
290
290
291 if not have['cython']:
291 if not have['cython']:
292 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
292 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
293 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
293 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
294
294
295 if not have['oct2py']:
295 if not have['oct2py']:
296 exclusions.extend([ipjoin('extensions', 'octavemagic')])
296 exclusions.extend([ipjoin('extensions', 'octavemagic')])
297 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
297 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
298
298
299 if not have['tornado']:
299 if not have['tornado']:
300 exclusions.append(ipjoin('frontend', 'html'))
300 exclusions.append(ipjoin('frontend', 'html'))
301
301
302 if not have['rpy2'] or not have['numpy']:
302 if not have['rpy2'] or not have['numpy']:
303 exclusions.append(ipjoin('extensions', 'rmagic'))
303 exclusions.append(ipjoin('extensions', 'rmagic'))
304 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
304 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
305
305
306 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
306 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
307 if sys.platform == 'win32':
307 if sys.platform == 'win32':
308 exclusions = [s.replace('\\','\\\\') for s in exclusions]
308 exclusions = [s.replace('\\','\\\\') for s in exclusions]
309
309
310 # check for any exclusions that don't seem to exist:
310 # check for any exclusions that don't seem to exist:
311 parent, _ = os.path.split(get_ipython_package_dir())
311 parent, _ = os.path.split(get_ipython_package_dir())
312 for exclusion in exclusions:
312 for exclusion in exclusions:
313 if exclusion.endswith(('deathrow', 'quarantine')):
313 if exclusion.endswith(('deathrow', 'quarantine')):
314 # ignore deathrow/quarantine, which exist in dev, but not install
314 # ignore deathrow/quarantine, which exist in dev, but not install
315 continue
315 continue
316 fullpath = pjoin(parent, exclusion)
316 fullpath = pjoin(parent, exclusion)
317 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
317 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
318 warn("Excluding nonexistent file: %r\n" % exclusion)
318 warn("Excluding nonexistent file: %r\n" % exclusion)
319
319
320 return exclusions
320 return exclusions
321
321
322
322
323 class IPTester(object):
323 class IPTester(object):
324 """Call that calls iptest or trial in a subprocess.
324 """Call that calls iptest or trial in a subprocess.
325 """
325 """
326 #: string, name of test runner that will be called
326 #: string, name of test runner that will be called
327 runner = None
327 runner = None
328 #: list, parameters for test runner
328 #: list, parameters for test runner
329 params = None
329 params = None
330 #: list, arguments of system call to be made to call test runner
330 #: list, arguments of system call to be made to call test runner
331 call_args = None
331 call_args = None
332 #: list, subprocesses we start (for cleanup)
332 #: list, subprocesses we start (for cleanup)
333 processes = None
333 processes = None
334 #: str, coverage xml output file
334 #: str, coverage xml output file
335 coverage_xml = None
335 coverage_xml = None
336
336
337 def __init__(self, runner='iptest', params=None):
337 def __init__(self, runner='iptest', params=None):
338 """Create new test runner."""
338 """Create new test runner."""
339 p = os.path
339 p = os.path
340 if runner == 'iptest':
340 if runner == 'iptest':
341 iptest_app = get_ipython_module_path('IPython.testing.iptest')
341 iptest_app = get_ipython_module_path('IPython.testing.iptest')
342 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
342 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
343 else:
343 else:
344 raise Exception('Not a valid test runner: %s' % repr(runner))
344 raise Exception('Not a valid test runner: %s' % repr(runner))
345 if params is None:
345 if params is None:
346 params = []
346 params = []
347 if isinstance(params, str):
347 if isinstance(params, str):
348 params = [params]
348 params = [params]
349 self.params = params
349 self.params = params
350
350
351 # Assemble call
351 # Assemble call
352 self.call_args = self.runner+self.params
352 self.call_args = self.runner+self.params
353
353
354 # Find the section we're testing (IPython.foo)
354 # Find the section we're testing (IPython.foo)
355 for sect in self.params:
355 for sect in self.params:
356 if sect.startswith('IPython'): break
356 if sect.startswith('IPython'): break
357 else:
357 else:
358 raise ValueError("Section not found", self.params)
358 raise ValueError("Section not found", self.params)
359
359
360 if '--with-xunit' in self.call_args:
360 if '--with-xunit' in self.call_args:
361
361
362 self.call_args.append('--xunit-file')
362 self.call_args.append('--xunit-file')
363 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
363 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
364 xunit_file = path.abspath(sect+'.xunit.xml')
364 xunit_file = path.abspath(sect+'.xunit.xml')
365 if sys.platform == 'win32':
365 if sys.platform == 'win32':
366 xunit_file = '"%s"' % xunit_file
366 xunit_file = '"%s"' % xunit_file
367 self.call_args.append(xunit_file)
367 self.call_args.append(xunit_file)
368
368
369 if '--with-xml-coverage' in self.call_args:
369 if '--with-xml-coverage' in self.call_args:
370 self.coverage_xml = path.abspath(sect+".coverage.xml")
370 self.coverage_xml = path.abspath(sect+".coverage.xml")
371 self.call_args.remove('--with-xml-coverage')
371 self.call_args.remove('--with-xml-coverage')
372 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
372 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
373
373
374 # Store anything we start to clean up on deletion
374 # Store anything we start to clean up on deletion
375 self.processes = []
375 self.processes = []
376
376
377 def _run_cmd(self):
377 def _run_cmd(self):
378 with TemporaryDirectory() as IPYTHONDIR:
378 with TemporaryDirectory() as IPYTHONDIR:
379 env = os.environ.copy()
379 env = os.environ.copy()
380 env['IPYTHONDIR'] = IPYTHONDIR
380 env['IPYTHONDIR'] = IPYTHONDIR
381 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
381 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
382 subp = subprocess.Popen(self.call_args, env=env)
382 subp = subprocess.Popen(self.call_args, env=env)
383 self.processes.append(subp)
383 self.processes.append(subp)
384 # If this fails, the process will be left in self.processes and
384 # If this fails, the process will be left in self.processes and
385 # cleaned up later, but if the wait call succeeds, then we can
385 # cleaned up later, but if the wait call succeeds, then we can
386 # clear the stored process.
386 # clear the stored process.
387 retcode = subp.wait()
387 retcode = subp.wait()
388 self.processes.pop()
388 self.processes.pop()
389 return retcode
389 return retcode
390
390
391 def run(self):
391 def run(self):
392 """Run the stored commands"""
392 """Run the stored commands"""
393 try:
393 try:
394 retcode = self._run_cmd()
394 retcode = self._run_cmd()
395 except:
395 except:
396 import traceback
396 import traceback
397 traceback.print_exc()
397 traceback.print_exc()
398 return 1 # signal failure
398 return 1 # signal failure
399
399
400 if self.coverage_xml:
400 if self.coverage_xml:
401 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
401 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
402 return retcode
402 return retcode
403
403
404 def __del__(self):
404 def __del__(self):
405 """Cleanup on exit by killing any leftover processes."""
405 """Cleanup on exit by killing any leftover processes."""
406 for subp in self.processes:
406 for subp in self.processes:
407 if subp.poll() is not None:
407 if subp.poll() is not None:
408 continue # process is already dead
408 continue # process is already dead
409
409
410 try:
410 try:
411 print('Cleaning stale PID: %d' % subp.pid)
411 print('Cleaning stale PID: %d' % subp.pid)
412 subp.kill()
412 subp.kill()
413 except: # (OSError, WindowsError) ?
413 except: # (OSError, WindowsError) ?
414 # This is just a best effort, if we fail or the process was
414 # This is just a best effort, if we fail or the process was
415 # really gone, ignore it.
415 # really gone, ignore it.
416 pass
416 pass
417
417
418 if subp.poll() is None:
418 if subp.poll() is None:
419 # The process did not die...
419 # The process did not die...
420 print('... failed. Manual cleanup may be required.'
420 print('... failed. Manual cleanup may be required.'
421 % subp.pid)
421 % subp.pid)
422
422
423 def make_runners():
423 def make_runners(inc_slow=False):
424 """Define the top-level packages that need to be tested.
424 """Define the top-level packages that need to be tested.
425 """
425 """
426
426
427 # Packages to be tested via nose, that only depend on the stdlib
427 # Packages to be tested via nose, that only depend on the stdlib
428 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
428 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
429 'testing', 'utils', 'nbformat' ]
429 'testing', 'utils', 'nbformat' ]
430
430
431 if have['zmq']:
431 if have['zmq']:
432 nose_pkg_names.append('zmq')
432 nose_pkg_names.append('zmq')
433 nose_pkg_names.append('parallel')
433 if inc_slow:
434 nose_pkg_names.append('parallel')
434
435
435 # For debugging this code, only load quick stuff
436 # For debugging this code, only load quick stuff
436 #nose_pkg_names = ['core', 'extensions'] # dbg
437 #nose_pkg_names = ['core', 'extensions'] # dbg
437
438
438 # Make fully qualified package names prepending 'IPython.' to our name lists
439 # Make fully qualified package names prepending 'IPython.' to our name lists
439 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
440 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
440
441
441 # Make runners
442 # Make runners
442 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
443 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
443
444
444 return runners
445 return runners
445
446
446
447
447 def run_iptest():
448 def run_iptest():
448 """Run the IPython test suite using nose.
449 """Run the IPython test suite using nose.
449
450
450 This function is called when this script is **not** called with the form
451 This function is called when this script is **not** called with the form
451 `iptest all`. It simply calls nose with appropriate command line flags
452 `iptest all`. It simply calls nose with appropriate command line flags
452 and accepts all of the standard nose arguments.
453 and accepts all of the standard nose arguments.
453 """
454 """
454 # Apply our monkeypatch to Xunit
455 # Apply our monkeypatch to Xunit
455 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
456 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
456 monkeypatch_xunit()
457 monkeypatch_xunit()
457
458
458 warnings.filterwarnings('ignore',
459 warnings.filterwarnings('ignore',
459 'This will be removed soon. Use IPython.testing.util instead')
460 'This will be removed soon. Use IPython.testing.util instead')
460
461
461 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
462 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
462
463
463 '--with-ipdoctest',
464 '--with-ipdoctest',
464 '--ipdoctest-tests','--ipdoctest-extension=txt',
465 '--ipdoctest-tests','--ipdoctest-extension=txt',
465
466
466 # We add --exe because of setuptools' imbecility (it
467 # We add --exe because of setuptools' imbecility (it
467 # blindly does chmod +x on ALL files). Nose does the
468 # blindly does chmod +x on ALL files). Nose does the
468 # right thing and it tries to avoid executables,
469 # right thing and it tries to avoid executables,
469 # setuptools unfortunately forces our hand here. This
470 # setuptools unfortunately forces our hand here. This
470 # has been discussed on the distutils list and the
471 # has been discussed on the distutils list and the
471 # setuptools devs refuse to fix this problem!
472 # setuptools devs refuse to fix this problem!
472 '--exe',
473 '--exe',
473 ]
474 ]
474
475
475 if nose.__version__ >= '0.11':
476 if nose.__version__ >= '0.11':
476 # I don't fully understand why we need this one, but depending on what
477 # I don't fully understand why we need this one, but depending on what
477 # directory the test suite is run from, if we don't give it, 0 tests
478 # directory the test suite is run from, if we don't give it, 0 tests
478 # get run. Specifically, if the test suite is run from the source dir
479 # get run. Specifically, if the test suite is run from the source dir
479 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
480 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
480 # even if the same call done in this directory works fine). It appears
481 # even if the same call done in this directory works fine). It appears
481 # that if the requested package is in the current dir, nose bails early
482 # that if the requested package is in the current dir, nose bails early
482 # by default. Since it's otherwise harmless, leave it in by default
483 # by default. Since it's otherwise harmless, leave it in by default
483 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
484 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
484 argv.append('--traverse-namespace')
485 argv.append('--traverse-namespace')
485
486
486 # use our plugin for doctesting. It will remove the standard doctest plugin
487 # use our plugin for doctesting. It will remove the standard doctest plugin
487 # if it finds it enabled
488 # if it finds it enabled
488 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
489 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
489 # We need a global ipython running in this process
490 # We need a global ipython running in this process
490 globalipapp.start_ipython()
491 globalipapp.start_ipython()
491 # Now nose can run
492 # Now nose can run
492 TestProgram(argv=argv, addplugins=plugins)
493 TestProgram(argv=argv, addplugins=plugins)
493
494
494
495
495 def run_iptestall():
496 def run_iptestall(inc_slow=False):
496 """Run the entire IPython test suite by calling nose and trial.
497 """Run the entire IPython test suite by calling nose and trial.
497
498
498 This function constructs :class:`IPTester` instances for all IPython
499 This function constructs :class:`IPTester` instances for all IPython
499 modules and package and then runs each of them. This causes the modules
500 modules and package and then runs each of them. This causes the modules
500 and packages of IPython to be tested each in their own subprocess using
501 and packages of IPython to be tested each in their own subprocess using
501 nose.
502 nose.
503
504 Parameters
505 ----------
506
507 inc_slow : bool, optional
508 Include slow tests, like IPython.parallel. By default, these tests aren't
509 run.
502 """
510 """
503
511
504 runners = make_runners()
512 runners = make_runners(inc_slow=inc_slow)
505
513
506 # Run the test runners in a temporary dir so we can nuke it when finished
514 # Run the test runners in a temporary dir so we can nuke it when finished
507 # to clean up any junk files left over by accident. This also makes it
515 # to clean up any junk files left over by accident. This also makes it
508 # robust against being run in non-writeable directories by mistake, as the
516 # robust against being run in non-writeable directories by mistake, as the
509 # temp dir will always be user-writeable.
517 # temp dir will always be user-writeable.
510 curdir = os.getcwdu()
518 curdir = os.getcwdu()
511 testdir = tempfile.gettempdir()
519 testdir = tempfile.gettempdir()
512 os.chdir(testdir)
520 os.chdir(testdir)
513
521
514 # Run all test runners, tracking execution time
522 # Run all test runners, tracking execution time
515 failed = []
523 failed = []
516 t_start = time.time()
524 t_start = time.time()
517 try:
525 try:
518 for (name, runner) in runners:
526 for (name, runner) in runners:
519 print('*'*70)
527 print('*'*70)
520 print('IPython test group:',name)
528 print('IPython test group:',name)
521 res = runner.run()
529 res = runner.run()
522 if res:
530 if res:
523 failed.append( (name, runner) )
531 failed.append( (name, runner) )
524 finally:
532 finally:
525 os.chdir(curdir)
533 os.chdir(curdir)
526 t_end = time.time()
534 t_end = time.time()
527 t_tests = t_end - t_start
535 t_tests = t_end - t_start
528 nrunners = len(runners)
536 nrunners = len(runners)
529 nfail = len(failed)
537 nfail = len(failed)
530 # summarize results
538 # summarize results
531 print()
539 print()
532 print('*'*70)
540 print('*'*70)
533 print('Test suite completed for system with the following information:')
541 print('Test suite completed for system with the following information:')
534 print(report())
542 print(report())
535 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
543 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
536 print()
544 print()
537 print('Status:')
545 print('Status:')
538 if not failed:
546 if not failed:
539 print('OK')
547 print('OK')
540 else:
548 else:
541 # If anything went wrong, point out what command to rerun manually to
549 # If anything went wrong, point out what command to rerun manually to
542 # see the actual errors and individual summary
550 # see the actual errors and individual summary
543 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
551 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
544 for name, failed_runner in failed:
552 for name, failed_runner in failed:
545 print('-'*40)
553 print('-'*40)
546 print('Runner failed:',name)
554 print('Runner failed:',name)
547 print('You may wish to rerun this one individually, with:')
555 print('You may wish to rerun this one individually, with:')
548 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
556 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
549 print(u' '.join(failed_call_args))
557 print(u' '.join(failed_call_args))
550 print()
558 print()
551 # Ensure that our exit code indicates failure
559 # Ensure that our exit code indicates failure
552 sys.exit(1)
560 sys.exit(1)
553
561
554
562
555 def main():
563 def main():
556 for arg in sys.argv[1:]:
564 for arg in sys.argv[1:]:
557 if arg.startswith('IPython'):
565 if arg.startswith('IPython'):
558 # This is in-process
566 # This is in-process
559 run_iptest()
567 run_iptest()
560 else:
568 else:
569 if "--all" in sys.argv:
570 sys.argv.remove("--all")
571 inc_slow = True
572 else:
573 inc_slow = False
561 # This starts subprocesses
574 # This starts subprocesses
562 run_iptestall()
575 run_iptestall(inc_slow=inc_slow)
563
576
564
577
565 if __name__ == '__main__':
578 if __name__ == '__main__':
566 main()
579 main()
@@ -1,282 +1,288 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """
2 """
3 This is a script for testing pull requests for IPython. It merges the pull
3 This is a script for testing pull requests for IPython. It merges the pull
4 request with current master, installs and tests on all available versions of
4 request with current master, installs and tests on all available versions of
5 Python, and posts the results to Gist if any tests fail.
5 Python, and posts the results to Gist if any tests fail.
6
6
7 Usage:
7 Usage:
8 python test_pr.py 1657
8 python test_pr.py 1657
9 """
9 """
10 from __future__ import print_function
10 from __future__ import print_function
11
11
12 import errno
12 import errno
13 from glob import glob
13 from glob import glob
14 import io
14 import io
15 import json
15 import json
16 import os
16 import os
17 import pickle
17 import pickle
18 import re
18 import re
19 import requests
19 import requests
20 import shutil
20 import shutil
21 import time
21 import time
22 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
22 from subprocess import call, check_call, check_output, PIPE, STDOUT, CalledProcessError
23 import sys
23 import sys
24
24
25 import gh_api
25 import gh_api
26 from gh_api import Obj
26 from gh_api import Obj
27
27
28 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
28 basedir = os.path.join(os.path.expanduser("~"), ".ipy_pr_tests")
29 repodir = os.path.join(basedir, "ipython")
29 repodir = os.path.join(basedir, "ipython")
30 ipy_repository = 'git://github.com/ipython/ipython.git'
30 ipy_repository = 'git://github.com/ipython/ipython.git'
31 ipy_http_repository = 'http://github.com/ipython/ipython.git'
31 ipy_http_repository = 'http://github.com/ipython/ipython.git'
32 gh_project="ipython/ipython"
32 gh_project="ipython/ipython"
33
33
34 supported_pythons = ['python2.6', 'python2.7', 'python3.2']
34 supported_pythons = ['python2.6', 'python2.7', 'python3.2']
35
35
36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
36 missing_libs_re = re.compile(r"Tools and libraries NOT available at test time:\n"
37 r"\s*(.*?)\n")
37 r"\s*(.*?)\n")
38 def get_missing_libraries(log):
38 def get_missing_libraries(log):
39 m = missing_libs_re.search(log)
39 m = missing_libs_re.search(log)
40 if m:
40 if m:
41 return m.group(1)
41 return m.group(1)
42
42
43 class TestRun(object):
43 class TestRun(object):
44 def __init__(self, pr_num):
44 def __init__(self, pr_num, extra_args):
45 self.unavailable_pythons = []
45 self.unavailable_pythons = []
46 self.venvs = []
46 self.venvs = []
47 self.pr_num = pr_num
47 self.pr_num = pr_num
48 self.extra_args = extra_args
48
49
49 self.pr = gh_api.get_pull_request(gh_project, pr_num)
50 self.pr = gh_api.get_pull_request(gh_project, pr_num)
50
51
51 self.setup()
52 self.setup()
52
53
53 self.results = []
54 self.results = []
54
55
55 def available_python_versions(self):
56 def available_python_versions(self):
56 """Get the executable names of available versions of Python on the system.
57 """Get the executable names of available versions of Python on the system.
57 """
58 """
58 for py in supported_pythons:
59 for py in supported_pythons:
59 try:
60 try:
60 check_call([py, '-c', 'import nose'], stdout=PIPE)
61 check_call([py, '-c', 'import nose'], stdout=PIPE)
61 yield py
62 yield py
62 except (OSError, CalledProcessError):
63 except (OSError, CalledProcessError):
63 self.unavailable_pythons.append(py)
64 self.unavailable_pythons.append(py)
64
65
65 def setup(self):
66 def setup(self):
66 """Prepare the repository and virtualenvs."""
67 """Prepare the repository and virtualenvs."""
67 try:
68 try:
68 os.mkdir(basedir)
69 os.mkdir(basedir)
69 except OSError as e:
70 except OSError as e:
70 if e.errno != errno.EEXIST:
71 if e.errno != errno.EEXIST:
71 raise
72 raise
72 os.chdir(basedir)
73 os.chdir(basedir)
73
74
74 # Delete virtualenvs and recreate
75 # Delete virtualenvs and recreate
75 for venv in glob('venv-*'):
76 for venv in glob('venv-*'):
76 shutil.rmtree(venv)
77 shutil.rmtree(venv)
77 for py in self.available_python_versions():
78 for py in self.available_python_versions():
78 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
79 check_call(['virtualenv', '-p', py, '--system-site-packages', 'venv-%s' % py])
79 self.venvs.append((py, 'venv-%s' % py))
80 self.venvs.append((py, 'venv-%s' % py))
80
81
81 # Check out and update the repository
82 # Check out and update the repository
82 if not os.path.exists('ipython'):
83 if not os.path.exists('ipython'):
83 try :
84 try :
84 check_call(['git', 'clone', ipy_repository])
85 check_call(['git', 'clone', ipy_repository])
85 except CalledProcessError :
86 except CalledProcessError :
86 check_call(['git', 'clone', ipy_http_repository])
87 check_call(['git', 'clone', ipy_http_repository])
87 os.chdir(repodir)
88 os.chdir(repodir)
88 check_call(['git', 'checkout', 'master'])
89 check_call(['git', 'checkout', 'master'])
89 try :
90 try :
90 check_call(['git', 'pull', 'origin', 'master'])
91 check_call(['git', 'pull', 'origin', 'master'])
91 except CalledProcessError :
92 except CalledProcessError :
92 check_call(['git', 'pull', ipy_http_repository, 'master'])
93 check_call(['git', 'pull', ipy_http_repository, 'master'])
93 os.chdir(basedir)
94 os.chdir(basedir)
94
95
95 def get_branch(self):
96 def get_branch(self):
96 repo = self.pr['head']['repo']['clone_url']
97 repo = self.pr['head']['repo']['clone_url']
97 branch = self.pr['head']['ref']
98 branch = self.pr['head']['ref']
98 owner = self.pr['head']['repo']['owner']['login']
99 owner = self.pr['head']['repo']['owner']['login']
99 mergeable = self.pr['mergeable']
100 mergeable = self.pr['mergeable']
100
101
101 os.chdir(repodir)
102 os.chdir(repodir)
102 if mergeable:
103 if mergeable:
103 merged_branch = "%s-%s" % (owner, branch)
104 merged_branch = "%s-%s" % (owner, branch)
104 # Delete the branch first
105 # Delete the branch first
105 call(['git', 'branch', '-D', merged_branch])
106 call(['git', 'branch', '-D', merged_branch])
106 check_call(['git', 'checkout', '-b', merged_branch])
107 check_call(['git', 'checkout', '-b', merged_branch])
107 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
108 check_call(['git', 'pull', '--no-ff', '--no-commit', repo, branch])
108 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
109 check_call(['git', 'commit', '-m', "merge %s/%s" % (repo, branch)])
109 else:
110 else:
110 # Fetch the branch without merging it.
111 # Fetch the branch without merging it.
111 check_call(['git', 'fetch', repo, branch])
112 check_call(['git', 'fetch', repo, branch])
112 check_call(['git', 'checkout', 'FETCH_HEAD'])
113 check_call(['git', 'checkout', 'FETCH_HEAD'])
113 os.chdir(basedir)
114 os.chdir(basedir)
114
115
115 def markdown_format(self):
116 def markdown_format(self):
116 def format_result(result):
117 def format_result(result):
117 s = "* %s: " % result.py
118 s = "* %s: " % result.py
118 if result.passed:
119 if result.passed:
119 s += "OK"
120 s += "OK"
120 else:
121 else:
121 s += "Failed, log at %s" % result.log_url
122 s += "Failed, log at %s" % result.log_url
122 if result.missing_libraries:
123 if result.missing_libraries:
123 s += " (libraries not available: " + result.missing_libraries + ")"
124 s += " (libraries not available: " + result.missing_libraries + ")"
124 return s
125 return s
125
126
126 if self.pr['mergeable']:
127 if self.pr['mergeable']:
127 com = self.pr['head']['sha'][:7] + " merged into master"
128 com = self.pr['head']['sha'][:7] + " merged into master"
128 else:
129 else:
129 com = self.pr['head']['sha'][:7] + " (can't merge cleanly)"
130 com = self.pr['head']['sha'][:7] + " (can't merge cleanly)"
130 lines = ["**Test results for commit %s**" % com,
131 lines = ["**Test results for commit %s**" % com,
131 "Platform: " + sys.platform,
132 "Platform: " + sys.platform,
132 ""] + \
133 ""] + \
133 [format_result(r) for r in self.results] + \
134 [format_result(r) for r in self.results] + \
134 ["",
135 [""]
135 "Not available for testing: " + ", ".join(self.unavailable_pythons)]
136 if self.extra_args:
137 lines.append("Extra args: %r" % self.extra_args),
138 lines.append("Not available for testing: " + ", ".join(self.unavailable_pythons))
136 return "\n".join(lines)
139 return "\n".join(lines)
137
140
138 def post_results_comment(self):
141 def post_results_comment(self):
139 body = self.markdown_format()
142 body = self.markdown_format()
140 gh_api.post_issue_comment(gh_project, self.pr_num, body)
143 gh_api.post_issue_comment(gh_project, self.pr_num, body)
141
144
142 def print_results(self):
145 def print_results(self):
143 pr = self.pr
146 pr = self.pr
144
147
145 print("\n")
148 print("\n")
146 if pr['mergeable']:
149 if pr['mergeable']:
147 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
150 print("**Test results for commit %s merged into master**" % pr['head']['sha'][:7])
148 else:
151 else:
149 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
152 print("**Test results for commit %s (can't merge cleanly)**" % pr['head']['sha'][:7])
150 print("Platform:", sys.platform)
153 print("Platform:", sys.platform)
151 for result in self.results:
154 for result in self.results:
152 if result.passed:
155 if result.passed:
153 print(result.py, ":", "OK")
156 print(result.py, ":", "OK")
154 else:
157 else:
155 print(result.py, ":", "Failed")
158 print(result.py, ":", "Failed")
156 print(" Test log:", result.get('log_url') or result.log_file)
159 print(" Test log:", result.get('log_url') or result.log_file)
157 if result.missing_libraries:
160 if result.missing_libraries:
158 print(" Libraries not available:", result.missing_libraries)
161 print(" Libraries not available:", result.missing_libraries)
162
163 if self.extra_args:
164 print("Extra args:", self.extra_args)
159 print("Not available for testing:", ", ".join(self.unavailable_pythons))
165 print("Not available for testing:", ", ".join(self.unavailable_pythons))
160
166
161 def dump_results(self):
167 def dump_results(self):
162 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
168 with open(os.path.join(basedir, 'lastresults.pkl'), 'wb') as f:
163 pickle.dump(self, f)
169 pickle.dump(self, f)
164
170
165 @staticmethod
171 @staticmethod
166 def load_results():
172 def load_results():
167 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
173 with open(os.path.join(basedir, 'lastresults.pkl'), 'rb') as f:
168 return pickle.load(f)
174 return pickle.load(f)
169
175
170 def save_logs(self):
176 def save_logs(self):
171 for result in self.results:
177 for result in self.results:
172 if not result.passed:
178 if not result.passed:
173 result_locn = os.path.abspath(os.path.join('venv-%s' % result.py,
179 result_locn = os.path.abspath(os.path.join('venv-%s' % result.py,
174 self.pr['head']['sha'][:7]+".log"))
180 self.pr['head']['sha'][:7]+".log"))
175 with io.open(result_locn, 'w', encoding='utf-8') as f:
181 with io.open(result_locn, 'w', encoding='utf-8') as f:
176 f.write(result.log)
182 f.write(result.log)
177
183
178 result.log_file = result_locn
184 result.log_file = result_locn
179
185
180 def post_logs(self):
186 def post_logs(self):
181 for result in self.results:
187 for result in self.results:
182 if not result.passed:
188 if not result.passed:
183 result.log_url = gh_api.post_gist(result.log,
189 result.log_url = gh_api.post_gist(result.log,
184 description='IPython test log',
190 description='IPython test log',
185 filename="results.log", auth=True)
191 filename="results.log", auth=True)
186
192
187 def run(self):
193 def run(self):
188 for py, venv in self.venvs:
194 for py, venv in self.venvs:
189 tic = time.time()
195 tic = time.time()
190 passed, log = run_tests(venv)
196 passed, log = run_tests(venv, self.extra_args)
191 elapsed = int(time.time() - tic)
197 elapsed = int(time.time() - tic)
192 print("Ran tests with %s in %is" % (py, elapsed))
198 print("Ran tests with %s in %is" % (py, elapsed))
193 missing_libraries = get_missing_libraries(log)
199 missing_libraries = get_missing_libraries(log)
194
200
195 self.results.append(Obj(py=py,
201 self.results.append(Obj(py=py,
196 passed=passed,
202 passed=passed,
197 log=log,
203 log=log,
198 missing_libraries=missing_libraries
204 missing_libraries=missing_libraries
199 )
205 )
200 )
206 )
201
207
202
208
203 def run_tests(venv):
209 def run_tests(venv, extra_args):
204 py = os.path.join(basedir, venv, 'bin', 'python')
210 py = os.path.join(basedir, venv, 'bin', 'python')
205 print(py)
211 print(py)
206 os.chdir(repodir)
212 os.chdir(repodir)
207 # cleanup build-dir
213 # cleanup build-dir
208 if os.path.exists('build'):
214 if os.path.exists('build'):
209 shutil.rmtree('build')
215 shutil.rmtree('build')
210 tic = time.time()
216 tic = time.time()
211 print ("\nInstalling IPython with %s" % py)
217 print ("\nInstalling IPython with %s" % py)
212 logfile = os.path.join(basedir, venv, 'install.log')
218 logfile = os.path.join(basedir, venv, 'install.log')
213 print ("Install log at %s" % logfile)
219 print ("Install log at %s" % logfile)
214 with open(logfile, 'wb') as f:
220 with open(logfile, 'wb') as f:
215 check_call([py, 'setup.py', 'install'], stdout=f)
221 check_call([py, 'setup.py', 'install'], stdout=f)
216 toc = time.time()
222 toc = time.time()
217 print ("Installed IPython in %.1fs" % (toc-tic))
223 print ("Installed IPython in %.1fs" % (toc-tic))
218 os.chdir(basedir)
224 os.chdir(basedir)
219
225
220 # Environment variables:
226 # Environment variables:
221 orig_path = os.environ["PATH"]
227 orig_path = os.environ["PATH"]
222 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
228 os.environ["PATH"] = os.path.join(basedir, venv, 'bin') + ':' + os.environ["PATH"]
223 os.environ.pop("PYTHONPATH", None)
229 os.environ.pop("PYTHONPATH", None)
224
230
225 # check that the right IPython is imported
231 # check that the right IPython is imported
226 ipython_file = check_output([py, '-c', 'import IPython; print (IPython.__file__)'])
232 ipython_file = check_output([py, '-c', 'import IPython; print (IPython.__file__)'])
227 ipython_file = ipython_file.strip().decode('utf-8')
233 ipython_file = ipython_file.strip().decode('utf-8')
228 if not ipython_file.startswith(os.path.join(basedir, venv)):
234 if not ipython_file.startswith(os.path.join(basedir, venv)):
229 msg = u"IPython does not appear to be in the venv: %s" % ipython_file
235 msg = "IPython does not appear to be in the venv: %s" % ipython_file
230 msg += u"\nDo you use setupegg.py develop?"
236 msg += "\nDo you use setupegg.py develop?"
231 print(msg, file=sys.stderr)
237 print(msg, file=sys.stderr)
232 return False, msg
238 return False, msg
233
239
234 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
240 iptest = os.path.join(basedir, venv, 'bin', 'iptest')
235 if not os.path.exists(iptest):
241 if not os.path.exists(iptest):
236 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
242 iptest = os.path.join(basedir, venv, 'bin', 'iptest3')
237
243
238 print("\nRunning tests, this typically takes a few minutes...")
244 print("\nRunning tests, this typically takes a few minutes...")
239 try:
245 try:
240 return True, check_output([iptest], stderr=STDOUT).decode('utf-8')
246 return True, check_output([iptest] + extra_args, stderr=STDOUT).decode('utf-8')
241 except CalledProcessError as e:
247 except CalledProcessError as e:
242 return False, e.output.decode('utf-8')
248 return False, e.output.decode('utf-8')
243 finally:
249 finally:
244 # Restore $PATH
250 # Restore $PATH
245 os.environ["PATH"] = orig_path
251 os.environ["PATH"] = orig_path
246
252
247
253
248 def test_pr(num, post_results=True):
254 def test_pr(num, post_results=True, extra_args=None):
249 # Get Github authorisation first, so that the user is prompted straight away
255 # Get Github authorisation first, so that the user is prompted straight away
250 # if their login is needed.
256 # if their login is needed.
251 if post_results:
257 if post_results:
252 gh_api.get_auth_token()
258 gh_api.get_auth_token()
253
259
254 testrun = TestRun(num)
260 testrun = TestRun(num, extra_args or [])
255
261
256 testrun.get_branch()
262 testrun.get_branch()
257
263
258 testrun.run()
264 testrun.run()
259
265
260 testrun.dump_results()
266 testrun.dump_results()
261
267
262 testrun.save_logs()
268 testrun.save_logs()
263 testrun.print_results()
269 testrun.print_results()
264
270
265 if post_results:
271 if post_results:
266 testrun.post_logs()
272 testrun.post_logs()
267 testrun.post_results_comment()
273 testrun.post_results_comment()
268 print("(Posted to Github)")
274 print("(Posted to Github)")
269 else:
275 else:
270 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
276 post_script = os.path.join(os.path.dirname(sys.argv[0]), "post_pr_test.py")
271 print("To post the results to Github, run", post_script)
277 print("To post the results to Github, run", post_script)
272
278
273
279
274 if __name__ == '__main__':
280 if __name__ == '__main__':
275 import argparse
281 import argparse
276 parser = argparse.ArgumentParser(description="Test an IPython pull request")
282 parser = argparse.ArgumentParser(description="Test an IPython pull request")
277 parser.add_argument('-p', '--publish', action='store_true',
283 parser.add_argument('-p', '--publish', action='store_true',
278 help="Publish the results to Github")
284 help="Publish the results to Github")
279 parser.add_argument('number', type=int, help="The pull request number")
285 parser.add_argument('number', type=int, help="The pull request number")
280
286
281 args = parser.parse_args()
287 args, extra_args = parser.parse_known_args()
282 test_pr(args.number, post_results=args.publish)
288 test_pr(args.number, post_results=args.publish, extra_args=extra_args)
General Comments 0
You need to be logged in to leave comments. Login now