##// END OF EJS Templates
add tests
Matthias Bussonnier -
Show More
@@ -1,342 +1,399 b''
1 1 """Tests for autoreload extension.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2012 IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import os
16 16 import sys
17 17 import tempfile
18 18 import textwrap
19 19 import shutil
20 20 import random
21 21 import time
22 22 from io import StringIO
23 23
24 24 import nose.tools as nt
25 25 import IPython.testing.tools as tt
26 26
27 27 from IPython.testing.decorators import skipif
28 28
29 29 from IPython.extensions.autoreload import AutoreloadMagics
30 30 from IPython.core.events import EventManager, pre_run_cell
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Test fixture
34 34 #-----------------------------------------------------------------------------
35 35
36 36 noop = lambda *a, **kw: None
37 37
38 38 class FakeShell(object):
39 39
40 40 def __init__(self):
41 41 self.ns = {}
42 42 self.events = EventManager(self, {'pre_run_cell', pre_run_cell})
43 43 self.auto_magics = AutoreloadMagics(shell=self)
44 44 self.events.register('pre_run_cell', self.auto_magics.pre_run_cell)
45 45
46 46 register_magics = set_hook = noop
47 47
48 48 def run_code(self, code):
49 49 self.events.trigger('pre_run_cell')
50 50 exec(code, self.ns)
51 51 self.auto_magics.post_execute_hook()
52 52
53 53 def push(self, items):
54 54 self.ns.update(items)
55 55
56 56 def magic_autoreload(self, parameter):
57 57 self.auto_magics.autoreload(parameter)
58 58
59 59 def magic_aimport(self, parameter, stream=None):
60 60 self.auto_magics.aimport(parameter, stream=stream)
61 61 self.auto_magics.post_execute_hook()
62 62
63 63
64 64 class Fixture(object):
65 65 """Fixture for creating test module files"""
66 66
67 67 test_dir = None
68 68 old_sys_path = None
69 69 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
70 70
71 71 def setUp(self):
72 72 self.test_dir = tempfile.mkdtemp()
73 73 self.old_sys_path = list(sys.path)
74 74 sys.path.insert(0, self.test_dir)
75 75 self.shell = FakeShell()
76 76
77 77 def tearDown(self):
78 78 shutil.rmtree(self.test_dir)
79 79 sys.path = self.old_sys_path
80 80
81 81 self.test_dir = None
82 82 self.old_sys_path = None
83 83 self.shell = None
84 84
85 85 def get_module(self):
86 86 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20))
87 87 if module_name in sys.modules:
88 88 del sys.modules[module_name]
89 89 file_name = os.path.join(self.test_dir, module_name + ".py")
90 90 return module_name, file_name
91 91
92 92 def write_file(self, filename, content):
93 93 """
94 94 Write a file, and force a timestamp difference of at least one second
95 95
96 96 Notes
97 97 -----
98 98 Python's .pyc files record the timestamp of their compilation
99 99 with a time resolution of one second.
100 100
101 101 Therefore, we need to force a timestamp difference between .py
102 102 and .pyc, without having the .py file be timestamped in the
103 103 future, and without changing the timestamp of the .pyc file
104 104 (because that is stored in the file). The only reliable way
105 105 to achieve this seems to be to sleep.
106 106 """
107 107
108 108 # Sleep one second + eps
109 109 time.sleep(1.05)
110 110
111 111 # Write
112 112 f = open(filename, 'w')
113 113 try:
114 114 f.write(content)
115 115 finally:
116 116 f.close()
117 117
118 118 def new_module(self, code):
119 119 mod_name, mod_fn = self.get_module()
120 120 f = open(mod_fn, 'w')
121 121 try:
122 122 f.write(code)
123 123 finally:
124 124 f.close()
125 125 return mod_name, mod_fn
126 126
127 127 #-----------------------------------------------------------------------------
128 128 # Test automatic reloading
129 129 #-----------------------------------------------------------------------------
130 130
131 131 class TestAutoreload(Fixture):
132 132
133 133 @skipif(sys.version_info < (3, 6))
134 134 def test_reload_enums(self):
135 135 import enum
136 136 mod_name, mod_fn = self.new_module(textwrap.dedent("""
137 137 from enum import Enum
138 138 class MyEnum(Enum):
139 139 A = 'A'
140 140 B = 'B'
141 141 """))
142 142 self.shell.magic_autoreload("2")
143 143 self.shell.magic_aimport(mod_name)
144 144 self.write_file(mod_fn, textwrap.dedent("""
145 145 from enum import Enum
146 146 class MyEnum(Enum):
147 147 A = 'A'
148 148 B = 'B'
149 149 C = 'C'
150 150 """))
151 151 with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
152 152 self.shell.run_code("pass") # trigger another reload
153 153
154 def test_reload_class_attributes(self):
155 self.shell.magic_autoreload("2")
156 mod_name, mod_fn = self.new_module(textwrap.dedent("""
157 class MyClass:
158
159 def __init__(self, a=10):
160 self.a = a
161 self.b = 22
162 # self.toto = 33
163
164 def square(self):
165 print('compute square')
166 return self.a*self.a
167 """))
168 self.shell.run_code("from %s import MyClass" % mod_name)
169 self.shell.run_code("c = MyClass(5)")
170 self.shell.run_code("c.square()")
171 with nt.assert_raises(AttributeError):
172 self.shell.run_code("c.cube()")
173 with nt.assert_raises(AttributeError):
174 self.shell.run_code("c.power(5)")
175 self.shell.run_code("c.b")
176 with nt.assert_raises(AttributeError):
177 self.shell.run_code("c.toto")
178
179
180 self.write_file(mod_fn, textwrap.dedent("""
181 class MyClass:
182
183 def __init__(self, a=10):
184 self.a = a
185 self.b = 11
186
187 def power(self, p):
188 print('compute power '+str(p))
189 return self.a**p
190 """))
191
192 self.shell.run_code("d = MyClass(5)")
193 self.shell.run_code("d.power(5)")
194 with nt.assert_raises(AttributeError):
195 self.shell.run_code("c.cube()")
196 with nt.assert_raises(AttributeError):
197 self.shell.run_code("c.square(5)")
198 self.shell.run_code("c.b")
199 self.shell.run_code("c.a")
200 with nt.assert_raises(AttributeError):
201 self.shell.run_code("c.toto")
202
203
204
205
206
154 207
155 208 def _check_smoketest(self, use_aimport=True):
156 209 """
157 210 Functional test for the automatic reloader using either
158 211 '%autoreload 1' or '%autoreload 2'
159 212 """
160 213
161 214 mod_name, mod_fn = self.new_module("""
162 215 x = 9
163 216
164 217 z = 123 # this item will be deleted
165 218
166 219 def foo(y):
167 220 return y + 3
168 221
169 222 class Baz(object):
170 223 def __init__(self, x):
171 224 self.x = x
172 225 def bar(self, y):
173 226 return self.x + y
174 227 @property
175 228 def quux(self):
176 229 return 42
177 230 def zzz(self):
178 231 '''This method will be deleted below'''
179 232 return 99
180 233
181 234 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
182 235 def foo(self):
183 236 return 1
184 237 """)
185 238
186 239 #
187 240 # Import module, and mark for reloading
188 241 #
189 242 if use_aimport:
190 243 self.shell.magic_autoreload("1")
191 244 self.shell.magic_aimport(mod_name)
192 245 stream = StringIO()
193 246 self.shell.magic_aimport("", stream=stream)
194 247 nt.assert_in(("Modules to reload:\n%s" % mod_name), stream.getvalue())
195 248
196 249 with nt.assert_raises(ImportError):
197 250 self.shell.magic_aimport("tmpmod_as318989e89ds")
198 251 else:
199 252 self.shell.magic_autoreload("2")
200 253 self.shell.run_code("import %s" % mod_name)
201 254 stream = StringIO()
202 255 self.shell.magic_aimport("", stream=stream)
203 256 nt.assert_true("Modules to reload:\nall-except-skipped" in
204 257 stream.getvalue())
205 258 nt.assert_in(mod_name, self.shell.ns)
206 259
207 260 mod = sys.modules[mod_name]
208 261
209 262 #
210 263 # Test module contents
211 264 #
212 265 old_foo = mod.foo
213 266 old_obj = mod.Baz(9)
214 267 old_obj2 = mod.Bar()
215 268
216 269 def check_module_contents():
217 270 nt.assert_equal(mod.x, 9)
218 271 nt.assert_equal(mod.z, 123)
219 272
220 273 nt.assert_equal(old_foo(0), 3)
221 274 nt.assert_equal(mod.foo(0), 3)
222 275
223 276 obj = mod.Baz(9)
224 277 nt.assert_equal(old_obj.bar(1), 10)
225 278 nt.assert_equal(obj.bar(1), 10)
226 279 nt.assert_equal(obj.quux, 42)
227 280 nt.assert_equal(obj.zzz(), 99)
228 281
229 282 obj2 = mod.Bar()
230 283 nt.assert_equal(old_obj2.foo(), 1)
231 284 nt.assert_equal(obj2.foo(), 1)
232 285
233 286 check_module_contents()
234 287
235 288 #
236 289 # Simulate a failed reload: no reload should occur and exactly
237 290 # one error message should be printed
238 291 #
239 292 self.write_file(mod_fn, """
240 293 a syntax error
241 294 """)
242 295
243 296 with tt.AssertPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
244 297 self.shell.run_code("pass") # trigger reload
245 298 with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
246 299 self.shell.run_code("pass") # trigger another reload
247 300 check_module_contents()
248 301
249 302 #
250 303 # Rewrite module (this time reload should succeed)
251 304 #
252 305 self.write_file(mod_fn, """
253 306 x = 10
254 307
255 308 def foo(y):
256 309 return y + 4
257 310
258 311 class Baz(object):
259 312 def __init__(self, x):
260 313 self.x = x
261 314 def bar(self, y):
262 315 return self.x + y + 1
263 316 @property
264 317 def quux(self):
265 318 return 43
266 319
267 320 class Bar: # old-style class
268 321 def foo(self):
269 322 return 2
270 323 """)
271 324
272 325 def check_module_contents():
273 326 nt.assert_equal(mod.x, 10)
274 327 nt.assert_false(hasattr(mod, 'z'))
275 328
276 329 nt.assert_equal(old_foo(0), 4) # superreload magic!
277 330 nt.assert_equal(mod.foo(0), 4)
278 331
279 332 obj = mod.Baz(9)
280 333 nt.assert_equal(old_obj.bar(1), 11) # superreload magic!
281 334 nt.assert_equal(obj.bar(1), 11)
282 335
283 336 nt.assert_equal(old_obj.quux, 43)
284 337 nt.assert_equal(obj.quux, 43)
285 338
286 339 nt.assert_false(hasattr(old_obj, 'zzz'))
287 340 nt.assert_false(hasattr(obj, 'zzz'))
288 341
289 342 obj2 = mod.Bar()
290 343 nt.assert_equal(old_obj2.foo(), 2)
291 344 nt.assert_equal(obj2.foo(), 2)
292 345
293 346 self.shell.run_code("pass") # trigger reload
294 347 check_module_contents()
295 348
296 349 #
297 350 # Another failure case: deleted file (shouldn't reload)
298 351 #
299 352 os.unlink(mod_fn)
300 353
301 354 self.shell.run_code("pass") # trigger reload
302 355 check_module_contents()
303 356
304 357 #
305 358 # Disable autoreload and rewrite module: no reload should occur
306 359 #
307 360 if use_aimport:
308 361 self.shell.magic_aimport("-" + mod_name)
309 362 stream = StringIO()
310 363 self.shell.magic_aimport("", stream=stream)
311 364 nt.assert_true(("Modules to skip:\n%s" % mod_name) in
312 365 stream.getvalue())
313 366
314 367 # This should succeed, although no such module exists
315 368 self.shell.magic_aimport("-tmpmod_as318989e89ds")
316 369 else:
317 370 self.shell.magic_autoreload("0")
318 371
319 372 self.write_file(mod_fn, """
320 373 x = -99
321 374 """)
322 375
323 376 self.shell.run_code("pass") # trigger reload
324 377 self.shell.run_code("pass")
325 378 check_module_contents()
326 379
327 380 #
328 381 # Re-enable autoreload: reload should now occur
329 382 #
330 383 if use_aimport:
331 384 self.shell.magic_aimport(mod_name)
332 385 else:
333 386 self.shell.magic_autoreload("")
334 387
335 388 self.shell.run_code("pass") # trigger reload
336 389 nt.assert_equal(mod.x, -99)
337 390
338 391 def test_smoketest_aimport(self):
339 392 self._check_smoketest(use_aimport=True)
340 393
341 394 def test_smoketest_autoreload(self):
342 395 self._check_smoketest(use_aimport=False)
396
397
398
399
@@ -1,454 +1,460 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 # Copyright (c) IPython Development Team.
18 18 # Distributed under the terms of the Modified BSD License.
19 19
20 20
21 21 import glob
22 22 from io import BytesIO
23 23 import os
24 24 import os.path as path
25 25 import sys
26 26 from threading import Thread, Lock, Event
27 27 import warnings
28 28
29 29 import nose.plugins.builtin
30 30 from nose.plugins.xunit import Xunit
31 31 from nose import SkipTest
32 32 from nose.core import TestProgram
33 33 from nose.plugins import Plugin
34 34 from nose.util import safe_str
35 35
36 36 from IPython import version_info
37 37 from IPython.utils.py3compat import decode
38 38 from IPython.utils.importstring import import_item
39 39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 40 from IPython.external.decorators import KnownFailure, knownfailureif
41 41
42 42 pjoin = path.join
43 43
44 44
45 45 # Enable printing all warnings raise by IPython's modules
46 46 warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
47 47 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
48 48 warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
49 49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
50 50
51 51 warnings.filterwarnings('error', message='.*apply_wrapper.*', category=DeprecationWarning, module='.*')
52 52 warnings.filterwarnings('error', message='.*make_label_dec', category=DeprecationWarning, module='.*')
53 53 warnings.filterwarnings('error', message='.*decorated_dummy.*', category=DeprecationWarning, module='.*')
54 54 warnings.filterwarnings('error', message='.*skip_file_no_x11.*', category=DeprecationWarning, module='.*')
55 55 warnings.filterwarnings('error', message='.*onlyif_any_cmd_exists.*', category=DeprecationWarning, module='.*')
56 56
57 57 warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*')
58 58
59 59 warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*')
60 60
61 61 # Jedi older versions
62 62 warnings.filterwarnings(
63 63 'error', message='.*elementwise != comparison failed and.*', category=FutureWarning, module='.*')
64 64
65 65 if version_info < (6,):
66 66 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
67 67 # warning with the runner they also import from standard import library. (as of Dec 2015)
68 68 # Ignore, let's revisit that in a couple of years for IPython 6.
69 69 warnings.filterwarnings(
70 70 'ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
71 71
72 72 if version_info < (8,):
73 73 warnings.filterwarnings('ignore', message='.*Completer.complete.*',
74 74 category=PendingDeprecationWarning, module='.*')
75 75 else:
76 76 warnings.warn(
77 77 'Completer.complete was pending deprecation and should be changed to Deprecated', FutureWarning)
78 78
79 79
80 80
81 81 # ------------------------------------------------------------------------------
82 82 # Monkeypatch Xunit to count known failures as skipped.
83 83 # ------------------------------------------------------------------------------
84 84 def monkeypatch_xunit():
85 85 try:
86 86 knownfailureif(True)(lambda: None)()
87 87 except Exception as e:
88 88 KnownFailureTest = type(e)
89 89
90 90 def addError(self, test, err, capt=None):
91 91 if issubclass(err[0], KnownFailureTest):
92 92 err = (SkipTest,) + err[1:]
93 93 return self.orig_addError(test, err, capt)
94 94
95 95 Xunit.orig_addError = Xunit.addError
96 96 Xunit.addError = addError
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Check which dependencies are installed and greater than minimum version.
100 100 #-----------------------------------------------------------------------------
101 101 def extract_version(mod):
102 102 return mod.__version__
103 103
104 104 def test_for(item, min_version=None, callback=extract_version):
105 105 """Test to see if item is importable, and optionally check against a minimum
106 106 version.
107 107
108 108 If min_version is given, the default behavior is to check against the
109 109 `__version__` attribute of the item, but specifying `callback` allows you to
110 110 extract the value you are interested in. e.g::
111 111
112 112 In [1]: import sys
113 113
114 114 In [2]: from IPython.testing.iptest import test_for
115 115
116 116 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
117 117 Out[3]: True
118 118
119 119 """
120 120 try:
121 121 check = import_item(item)
122 122 except (ImportError, RuntimeError):
123 123 # GTK reports Runtime error if it can't be initialized even if it's
124 124 # importable.
125 125 return False
126 126 else:
127 127 if min_version:
128 128 if callback:
129 129 # extra processing step to get version to compare
130 130 check = callback(check)
131 131
132 132 return check >= min_version
133 133 else:
134 134 return True
135 135
136 136 # Global dict where we can store information on what we have and what we don't
137 137 # have available at test run time
138 138 have = {'matplotlib': test_for('matplotlib'),
139 139 'pygments': test_for('pygments'),
140 140 'sqlite3': test_for('sqlite3')}
141 141
142 142 #-----------------------------------------------------------------------------
143 143 # Test suite definitions
144 144 #-----------------------------------------------------------------------------
145 145
146 146 test_group_names = ['core',
147 147 'extensions', 'lib', 'terminal', 'testing', 'utils',
148 148 ]
149 149
150 150 class TestSection(object):
151 151 def __init__(self, name, includes):
152 152 self.name = name
153 153 self.includes = includes
154 154 self.excludes = []
155 155 self.dependencies = []
156 156 self.enabled = True
157 157
158 158 def exclude(self, module):
159 159 if not module.startswith('IPython'):
160 160 module = self.includes[0] + "." + module
161 161 self.excludes.append(module.replace('.', os.sep))
162 162
163 163 def requires(self, *packages):
164 164 self.dependencies.extend(packages)
165 165
166 166 @property
167 167 def will_run(self):
168 168 return self.enabled and all(have[p] for p in self.dependencies)
169 169
170 170 # Name -> (include, exclude, dependencies_met)
171 171 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
172 172
173 173
174 174 # Exclusions and dependencies
175 175 # ---------------------------
176 176
177 177 # core:
178 178 sec = test_sections['core']
179 179 if not have['sqlite3']:
180 180 sec.exclude('tests.test_history')
181 181 sec.exclude('history')
182 182 if not have['matplotlib']:
183 183 sec.exclude('pylabtools'),
184 184 sec.exclude('tests.test_pylabtools')
185 185
186 186 # lib:
187 187 sec = test_sections['lib']
188 188 sec.exclude('kernel')
189 189 if not have['pygments']:
190 190 sec.exclude('tests.test_lexers')
191 191 # We do this unconditionally, so that the test suite doesn't import
192 192 # gtk, changing the default encoding and masking some unicode bugs.
193 193 sec.exclude('inputhookgtk')
194 194 # We also do this unconditionally, because wx can interfere with Unix signals.
195 195 # There are currently no tests for it anyway.
196 196 sec.exclude('inputhookwx')
197 197 # Testing inputhook will need a lot of thought, to figure out
198 198 # how to have tests that don't lock up with the gui event
199 199 # loops in the picture
200 200 sec.exclude('inputhook')
201 201
202 202 # testing:
203 203 sec = test_sections['testing']
204 204 # These have to be skipped on win32 because they use echo, rm, cd, etc.
205 205 # See ticket https://github.com/ipython/ipython/issues/87
206 206 if sys.platform == 'win32':
207 207 sec.exclude('plugin.test_exampleip')
208 208 sec.exclude('plugin.dtexample')
209 209
210 210 # don't run jupyter_console tests found via shim
211 211 test_sections['terminal'].exclude('console')
212 212
213 213 # extensions:
214 214 sec = test_sections['extensions']
215 215 # This is deprecated in favour of rpy2
216 216 sec.exclude('rmagic')
217 217 # autoreload does some strange stuff, so move it to its own test section
218 218 sec.exclude('autoreload')
219 219 sec.exclude('tests.test_autoreload')
220 220 test_sections['autoreload'] = TestSection('autoreload',
221 221 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
222 222 test_group_names.append('autoreload')
223 223
224 224
225 225 #-----------------------------------------------------------------------------
226 226 # Functions and classes
227 227 #-----------------------------------------------------------------------------
228 228
229 229 def check_exclusions_exist():
230 230 from IPython.paths import get_ipython_package_dir
231 231 from warnings import warn
232 232 parent = os.path.dirname(get_ipython_package_dir())
233 233 for sec in test_sections:
234 234 for pattern in sec.exclusions:
235 235 fullpath = pjoin(parent, pattern)
236 236 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
237 237 warn("Excluding nonexistent file: %r" % pattern)
238 238
239 239
240 240 class ExclusionPlugin(Plugin):
241 241 """A nose plugin to effect our exclusions of files and directories.
242 242 """
243 243 name = 'exclusions'
244 244 score = 3000 # Should come before any other plugins
245 245
246 246 def __init__(self, exclude_patterns=None):
247 247 """
248 248 Parameters
249 249 ----------
250 250
251 251 exclude_patterns : sequence of strings, optional
252 252 Filenames containing these patterns (as raw strings, not as regular
253 253 expressions) are excluded from the tests.
254 254 """
255 255 self.exclude_patterns = exclude_patterns or []
256 256 super(ExclusionPlugin, self).__init__()
257 257
258 258 def options(self, parser, env=os.environ):
259 259 Plugin.options(self, parser, env)
260 260
261 261 def configure(self, options, config):
262 262 Plugin.configure(self, options, config)
263 263 # Override nose trying to disable plugin.
264 264 self.enabled = True
265 265
266 266 def wantFile(self, filename):
267 267 """Return whether the given filename should be scanned for tests.
268 268 """
269 269 if any(pat in filename for pat in self.exclude_patterns):
270 270 return False
271 271 return None
272 272
273 273 def wantDirectory(self, directory):
274 274 """Return whether the given directory should be scanned for tests.
275 275 """
276 276 if any(pat in directory for pat in self.exclude_patterns):
277 277 return False
278 278 return None
279 279
280 280
281 281 class StreamCapturer(Thread):
282 282 daemon = True # Don't hang if main thread crashes
283 283 started = False
284 284 def __init__(self, echo=False):
285 285 super(StreamCapturer, self).__init__()
286 286 self.echo = echo
287 287 self.streams = []
288 288 self.buffer = BytesIO()
289 289 self.readfd, self.writefd = os.pipe()
290 290 self.buffer_lock = Lock()
291 291 self.stop = Event()
292 292
293 293 def run(self):
294 294 self.started = True
295 295
296 296 while not self.stop.is_set():
297 297 chunk = os.read(self.readfd, 1024)
298 298
299 299 with self.buffer_lock:
300 300 self.buffer.write(chunk)
301 301 if self.echo:
302 302 sys.stdout.write(decode(chunk))
303 303
304 304 os.close(self.readfd)
305 305 os.close(self.writefd)
306 306
307 307 def reset_buffer(self):
308 308 with self.buffer_lock:
309 309 self.buffer.truncate(0)
310 310 self.buffer.seek(0)
311 311
312 312 def get_buffer(self):
313 313 with self.buffer_lock:
314 314 return self.buffer.getvalue()
315 315
316 316 def ensure_started(self):
317 317 if not self.started:
318 318 self.start()
319 319
320 320 def halt(self):
321 321 """Safely stop the thread."""
322 322 if not self.started:
323 323 return
324 324
325 325 self.stop.set()
326 326 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
327 327 self.join()
328 328
329 329 class SubprocessStreamCapturePlugin(Plugin):
330 330 name='subprocstreams'
331 331 def __init__(self):
332 332 Plugin.__init__(self)
333 333 self.stream_capturer = StreamCapturer()
334 334 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
335 335 # This is ugly, but distant parts of the test machinery need to be able
336 336 # to redirect streams, so we make the object globally accessible.
337 337 nose.iptest_stdstreams_fileno = self.get_write_fileno
338 338
339 339 def get_write_fileno(self):
340 340 if self.destination == 'capture':
341 341 self.stream_capturer.ensure_started()
342 342 return self.stream_capturer.writefd
343 343 elif self.destination == 'discard':
344 344 return os.open(os.devnull, os.O_WRONLY)
345 345 else:
346 346 return sys.__stdout__.fileno()
347 347
348 348 def configure(self, options, config):
349 349 Plugin.configure(self, options, config)
350 350 # Override nose trying to disable plugin.
351 351 if self.destination == 'capture':
352 352 self.enabled = True
353 353
354 354 def startTest(self, test):
355 355 # Reset log capture
356 356 self.stream_capturer.reset_buffer()
357 357
358 358 def formatFailure(self, test, err):
359 359 # Show output
360 360 ec, ev, tb = err
361 361 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
362 362 if captured.strip():
363 363 ev = safe_str(ev)
364 364 out = [ev, '>> begin captured subprocess output <<',
365 365 captured,
366 366 '>> end captured subprocess output <<']
367 367 return ec, '\n'.join(out), tb
368 368
369 369 return err
370 370
371 371 formatError = formatFailure
372 372
373 373 def finalize(self, result):
374 374 self.stream_capturer.halt()
375 375
376 376
377 377 def run_iptest():
378 378 """Run the IPython test suite using nose.
379 379
380 380 This function is called when this script is **not** called with the form
381 381 `iptest all`. It simply calls nose with appropriate command line flags
382 382 and accepts all of the standard nose arguments.
383 383 """
384 384 # Apply our monkeypatch to Xunit
385 385 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
386 386 monkeypatch_xunit()
387 387
388 388 arg1 = sys.argv[1]
389 if arg1.startswith('IPython/'):
390 if arg1.endswith('.py'):
391 arg1 = arg1[:-3]
392 sys.argv[1] = arg1.replace('/', '.')
393
394 arg1 = sys.argv[1]
389 395 if arg1 in test_sections:
390 396 section = test_sections[arg1]
391 397 sys.argv[1:2] = section.includes
392 398 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
393 399 section = test_sections[arg1[8:]]
394 400 sys.argv[1:2] = section.includes
395 401 else:
396 402 section = TestSection(arg1, includes=[arg1])
397 403
398 404
399 405 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
400 406 # We add --exe because of setuptools' imbecility (it
401 407 # blindly does chmod +x on ALL files). Nose does the
402 408 # right thing and it tries to avoid executables,
403 409 # setuptools unfortunately forces our hand here. This
404 410 # has been discussed on the distutils list and the
405 411 # setuptools devs refuse to fix this problem!
406 412 '--exe',
407 413 ]
408 414 if '-a' not in argv and '-A' not in argv:
409 415 argv = argv + ['-a', '!crash']
410 416
411 417 if nose.__version__ >= '0.11':
412 418 # I don't fully understand why we need this one, but depending on what
413 419 # directory the test suite is run from, if we don't give it, 0 tests
414 420 # get run. Specifically, if the test suite is run from the source dir
415 421 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
416 422 # even if the same call done in this directory works fine). It appears
417 423 # that if the requested package is in the current dir, nose bails early
418 424 # by default. Since it's otherwise harmless, leave it in by default
419 425 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
420 426 argv.append('--traverse-namespace')
421 427
422 428 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
423 429 SubprocessStreamCapturePlugin() ]
424 430
425 431 # we still have some vestigial doctests in core
426 432 if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))):
427 433 plugins.append(IPythonDoctest())
428 434 argv.extend([
429 435 '--with-ipdoctest',
430 436 '--ipdoctest-tests',
431 437 '--ipdoctest-extension=txt',
432 438 ])
433 439
434 440
435 441 # Use working directory set by parent process (see iptestcontroller)
436 442 if 'IPTEST_WORKING_DIR' in os.environ:
437 443 os.chdir(os.environ['IPTEST_WORKING_DIR'])
438 444
439 445 # We need a global ipython running in this process, but the special
440 446 # in-process group spawns its own IPython kernels, so for *that* group we
441 447 # must avoid also opening the global one (otherwise there's a conflict of
442 448 # singletons). Ultimately the solution to this problem is to refactor our
443 449 # assumptions about what needs to be a singleton and what doesn't (app
444 450 # objects should, individual shells shouldn't). But for now, this
445 451 # workaround allows the test suite for the inprocess module to complete.
446 452 if 'kernel.inprocess' not in section.name:
447 453 from IPython.testing import globalipapp
448 454 globalipapp.start_ipython()
449 455
450 456 # Now nose can run
451 457 TestProgram(argv=argv, addplugins=plugins)
452 458
453 459 if __name__ == '__main__':
454 460 run_iptest()
General Comments 0
You need to be logged in to leave comments. Login now