##// END OF EJS Templates
Capture output from subprocs during test, and display on failure...
Thomas Kluyver -
Show More
@@ -1,125 +1,132 b''
1 1 """toplevel setup/teardown for parallel tests."""
2 2
3 3 #-------------------------------------------------------------------------------
4 4 # Copyright (C) 2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-------------------------------------------------------------------------------
9 9
10 10 #-------------------------------------------------------------------------------
11 11 # Imports
12 12 #-------------------------------------------------------------------------------
13 13
14 14 import os
15 15 import tempfile
16 16 import time
17 from subprocess import Popen
17 from subprocess import Popen, PIPE, STDOUT
18
19 import nose
18 20
19 21 from IPython.utils.path import get_ipython_dir
20 22 from IPython.parallel import Client
21 23 from IPython.parallel.apps.launcher import (LocalProcessLauncher,
22 24 ipengine_cmd_argv,
23 25 ipcontroller_cmd_argv,
24 26 SIGKILL,
25 27 ProcessStateError,
26 28 )
27 29
28 30 # globals
29 31 launchers = []
30 32 blackhole = open(os.devnull, 'w')
31 33
32 34 # Launcher class
33 35 class TestProcessLauncher(LocalProcessLauncher):
34 36 """subclass LocalProcessLauncher, to prevent extra sockets and threads being created on Windows"""
35 37 def start(self):
36 38 if self.state == 'before':
37 39 self.process = Popen(self.args,
38 stdout=blackhole, stderr=blackhole,
40 stdout=PIPE, stderr=STDOUT,
39 41 env=os.environ,
40 42 cwd=self.work_dir
41 43 )
42 44 self.notify_start(self.process.pid)
43 45 self.poll = self.process.poll
46 # Store stdout & stderr to show with failing tests.
47 # This is defined in IPython.testing.iptest
48 nose.ipy_stream_capturer.add_stream(self.process.stdout.fileno())
49 nose.ipy_stream_capturer.ensure_started()
44 50 else:
45 51 s = 'The process was already started and has state: %r' % self.state
46 52 raise ProcessStateError(s)
47 53
48 54 # nose setup/teardown
49 55
50 56 def setup():
51 57 cluster_dir = os.path.join(get_ipython_dir(), 'profile_iptest')
52 58 engine_json = os.path.join(cluster_dir, 'security', 'ipcontroller-engine.json')
53 59 client_json = os.path.join(cluster_dir, 'security', 'ipcontroller-client.json')
54 60 for json in (engine_json, client_json):
55 61 if os.path.exists(json):
56 62 os.remove(json)
57 63
58 64 cp = TestProcessLauncher()
59 65 cp.cmd_and_args = ipcontroller_cmd_argv + \
60 ['--profile=iptest', '--log-level=50', '--ping=250', '--dictdb']
66 ['--profile=iptest', '--log-level=20', '--ping=250', '--dictdb']
61 67 cp.start()
62 68 launchers.append(cp)
63 69 tic = time.time()
64 70 while not os.path.exists(engine_json) or not os.path.exists(client_json):
65 71 if cp.poll() is not None:
66 72 raise RuntimeError("The test controller exited with status %s" % cp.poll())
67 73 elif time.time()-tic > 15:
68 74 raise RuntimeError("Timeout waiting for the test controller to start.")
69 75 time.sleep(0.1)
70 76 add_engines(1)
71 77
72 78 def add_engines(n=1, profile='iptest', total=False):
73 79 """add a number of engines to a given profile.
74 80
75 81 If total is True, then already running engines are counted, and only
76 82 the additional engines necessary (if any) are started.
77 83 """
78 84 rc = Client(profile=profile)
79 85 base = len(rc)
80 86
81 87 if total:
82 88 n = max(n - base, 0)
83 89
84 90 eps = []
85 91 for i in range(n):
86 92 ep = TestProcessLauncher()
87 93 ep.cmd_and_args = ipengine_cmd_argv + [
88 94 '--profile=%s' % profile,
89 95 '--log-level=50',
90 96 '--InteractiveShell.colors=nocolor'
91 97 ]
92 98 ep.start()
93 99 launchers.append(ep)
94 100 eps.append(ep)
95 101 tic = time.time()
96 102 while len(rc) < base+n:
97 103 if any([ ep.poll() is not None for ep in eps ]):
98 104 raise RuntimeError("A test engine failed to start.")
99 105 elif time.time()-tic > 15:
100 106 raise RuntimeError("Timeout waiting for engines to connect.")
101 107 time.sleep(.1)
102 108 rc.spin()
103 109 rc.close()
104 110 return eps
105 111
106 112 def teardown():
107 113 time.sleep(1)
108 114 while launchers:
109 115 p = launchers.pop()
116 nose.ipy_stream_capturer.remove_stream(p.process.stdout.fileno())
110 117 if p.poll() is None:
111 118 try:
112 119 p.stop()
113 120 except Exception as e:
114 121 print e
115 122 pass
116 123 if p.poll() is None:
117 124 time.sleep(.25)
118 125 if p.poll() is None:
119 126 try:
120 127 print 'cleaning up test process...'
121 128 p.signal(SIGKILL)
122 129 except:
123 130 print "couldn't shutdown process: ", p
124 131 blackhole.close()
125 132
@@ -1,432 +1,520 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 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2009-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27 from __future__ import print_function
28 28
29 29 # Stdlib
30 30 import glob
31 from io import BytesIO
31 32 import os
32 33 import os.path as path
33 34 import re
35 from select import select
34 36 import sys
37 from threading import Thread, Lock, Event
35 38 import warnings
36 39
37 40 # Now, proceed to import nose itself
38 41 import nose.plugins.builtin
39 42 from nose.plugins.xunit import Xunit
40 43 from nose import SkipTest
41 44 from nose.core import TestProgram
42 45 from nose.plugins import Plugin
46 from nose.util import safe_str
43 47
44 48 # Our own imports
45 49 from IPython.utils.importstring import import_item
46 50 from IPython.testing.plugin.ipdoctest import IPythonDoctest
47 51 from IPython.external.decorators import KnownFailure, knownfailureif
48 52
49 53 pjoin = path.join
50 54
51 55
52 56 #-----------------------------------------------------------------------------
53 57 # Globals
54 58 #-----------------------------------------------------------------------------
55 59
56 60
57 61 #-----------------------------------------------------------------------------
58 62 # Warnings control
59 63 #-----------------------------------------------------------------------------
60 64
61 65 # Twisted generates annoying warnings with Python 2.6, as will do other code
62 66 # that imports 'sets' as of today
63 67 warnings.filterwarnings('ignore', 'the sets module is deprecated',
64 68 DeprecationWarning )
65 69
66 70 # This one also comes from Twisted
67 71 warnings.filterwarnings('ignore', 'the sha module is deprecated',
68 72 DeprecationWarning)
69 73
70 74 # Wx on Fedora11 spits these out
71 75 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
72 76 UserWarning)
73 77
74 78 # ------------------------------------------------------------------------------
75 79 # Monkeypatch Xunit to count known failures as skipped.
76 80 # ------------------------------------------------------------------------------
77 81 def monkeypatch_xunit():
78 82 try:
79 83 knownfailureif(True)(lambda: None)()
80 84 except Exception as e:
81 85 KnownFailureTest = type(e)
82 86
83 87 def addError(self, test, err, capt=None):
84 88 if issubclass(err[0], KnownFailureTest):
85 89 err = (SkipTest,) + err[1:]
86 90 return self.orig_addError(test, err, capt)
87 91
88 92 Xunit.orig_addError = Xunit.addError
89 93 Xunit.addError = addError
90 94
91 95 #-----------------------------------------------------------------------------
92 96 # Check which dependencies are installed and greater than minimum version.
93 97 #-----------------------------------------------------------------------------
94 98 def extract_version(mod):
95 99 return mod.__version__
96 100
97 101 def test_for(item, min_version=None, callback=extract_version):
98 102 """Test to see if item is importable, and optionally check against a minimum
99 103 version.
100 104
101 105 If min_version is given, the default behavior is to check against the
102 106 `__version__` attribute of the item, but specifying `callback` allows you to
103 107 extract the value you are interested in. e.g::
104 108
105 109 In [1]: import sys
106 110
107 111 In [2]: from IPython.testing.iptest import test_for
108 112
109 113 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
110 114 Out[3]: True
111 115
112 116 """
113 117 try:
114 118 check = import_item(item)
115 119 except (ImportError, RuntimeError):
116 120 # GTK reports Runtime error if it can't be initialized even if it's
117 121 # importable.
118 122 return False
119 123 else:
120 124 if min_version:
121 125 if callback:
122 126 # extra processing step to get version to compare
123 127 check = callback(check)
124 128
125 129 return check >= min_version
126 130 else:
127 131 return True
128 132
129 133 # Global dict where we can store information on what we have and what we don't
130 134 # have available at test run time
131 135 have = {}
132 136
133 137 have['curses'] = test_for('_curses')
134 138 have['matplotlib'] = test_for('matplotlib')
135 139 have['numpy'] = test_for('numpy')
136 140 have['pexpect'] = test_for('IPython.external.pexpect')
137 141 have['pymongo'] = test_for('pymongo')
138 142 have['pygments'] = test_for('pygments')
139 143 have['qt'] = test_for('IPython.external.qt')
140 144 have['rpy2'] = test_for('rpy2')
141 145 have['sqlite3'] = test_for('sqlite3')
142 146 have['cython'] = test_for('Cython')
143 147 have['oct2py'] = test_for('oct2py')
144 148 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
145 149 have['jinja2'] = test_for('jinja2')
146 150 have['wx'] = test_for('wx')
147 151 have['wx.aui'] = test_for('wx.aui')
148 152 have['azure'] = test_for('azure')
149 153 have['sphinx'] = test_for('sphinx')
150 154
151 155 min_zmq = (2,1,11)
152 156
153 157 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
154 158
155 159 #-----------------------------------------------------------------------------
156 160 # Test suite definitions
157 161 #-----------------------------------------------------------------------------
158 162
159 163 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
160 164 'extensions', 'lib', 'terminal', 'testing', 'utils',
161 165 'nbformat', 'qt', 'html', 'nbconvert'
162 166 ]
163 167
164 168 class TestSection(object):
165 169 def __init__(self, name, includes):
166 170 self.name = name
167 171 self.includes = includes
168 172 self.excludes = []
169 173 self.dependencies = []
170 174 self.enabled = True
171 175
172 176 def exclude(self, module):
173 177 if not module.startswith('IPython'):
174 178 module = self.includes[0] + "." + module
175 179 self.excludes.append(module.replace('.', os.sep))
176 180
177 181 def requires(self, *packages):
178 182 self.dependencies.extend(packages)
179 183
180 184 @property
181 185 def will_run(self):
182 186 return self.enabled and all(have[p] for p in self.dependencies)
183 187
184 188 # Name -> (include, exclude, dependencies_met)
185 189 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
186 190
187 191 # Exclusions and dependencies
188 192 # ---------------------------
189 193
190 194 # core:
191 195 sec = test_sections['core']
192 196 if not have['sqlite3']:
193 197 sec.exclude('tests.test_history')
194 198 sec.exclude('history')
195 199 if not have['matplotlib']:
196 200 sec.exclude('pylabtools'),
197 201 sec.exclude('tests.test_pylabtools')
198 202
199 203 # lib:
200 204 sec = test_sections['lib']
201 205 if not have['wx']:
202 206 sec.exclude('inputhookwx')
203 207 if not have['pexpect']:
204 208 sec.exclude('irunner')
205 209 sec.exclude('tests.test_irunner')
206 210 if not have['zmq']:
207 211 sec.exclude('kernel')
208 212 # We do this unconditionally, so that the test suite doesn't import
209 213 # gtk, changing the default encoding and masking some unicode bugs.
210 214 sec.exclude('inputhookgtk')
211 215 # Testing inputhook will need a lot of thought, to figure out
212 216 # how to have tests that don't lock up with the gui event
213 217 # loops in the picture
214 218 sec.exclude('inputhook')
215 219
216 220 # testing:
217 221 sec = test_sections['testing']
218 222 # This guy is probably attic material
219 223 sec.exclude('mkdoctests')
220 224 # These have to be skipped on win32 because they use echo, rm, cd, etc.
221 225 # See ticket https://github.com/ipython/ipython/issues/87
222 226 if sys.platform == 'win32':
223 227 sec.exclude('plugin.test_exampleip')
224 228 sec.exclude('plugin.dtexample')
225 229
226 230 # terminal:
227 231 if (not have['pexpect']) or (not have['zmq']):
228 232 test_sections['terminal'].exclude('console')
229 233
230 234 # parallel
231 235 sec = test_sections['parallel']
232 236 sec.requires('zmq')
233 237 if not have['pymongo']:
234 238 sec.exclude('controller.mongodb')
235 239 sec.exclude('tests.test_mongodb')
236 240
237 241 # kernel:
238 242 sec = test_sections['kernel']
239 243 sec.requires('zmq')
240 244 # The in-process kernel tests are done in a separate section
241 245 sec.exclude('inprocess')
242 246 # importing gtk sets the default encoding, which we want to avoid
243 247 sec.exclude('zmq.gui.gtkembed')
244 248 if not have['matplotlib']:
245 249 sec.exclude('zmq.pylab')
246 250
247 251 # kernel.inprocess:
248 252 test_sections['kernel.inprocess'].requires('zmq')
249 253
250 254 # extensions:
251 255 sec = test_sections['extensions']
252 256 if not have['cython']:
253 257 sec.exclude('cythonmagic')
254 258 sec.exclude('tests.test_cythonmagic')
255 259 if not have['oct2py']:
256 260 sec.exclude('octavemagic')
257 261 sec.exclude('tests.test_octavemagic')
258 262 if not have['rpy2'] or not have['numpy']:
259 263 sec.exclude('rmagic')
260 264 sec.exclude('tests.test_rmagic')
261 265 # autoreload does some strange stuff, so move it to its own test section
262 266 sec.exclude('autoreload')
263 267 sec.exclude('tests.test_autoreload')
264 268 test_sections['autoreload'] = TestSection('autoreload',
265 269 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
266 270 test_group_names.append('autoreload')
267 271
268 272 # qt:
269 273 test_sections['qt'].requires('zmq', 'qt', 'pygments')
270 274
271 275 # html:
272 276 sec = test_sections['html']
273 277 sec.requires('zmq', 'tornado')
274 278 # The notebook 'static' directory contains JS, css and other
275 279 # files for web serving. Occasionally projects may put a .py
276 280 # file in there (MathJax ships a conf.py), so we might as
277 281 # well play it safe and skip the whole thing.
278 282 sec.exclude('static')
279 283 sec.exclude('fabfile')
280 284 if not have['jinja2']:
281 285 sec.exclude('notebookapp')
282 286 if not have['azure']:
283 287 sec.exclude('services.notebooks.azurenbmanager')
284 288
285 289 # config:
286 290 # Config files aren't really importable stand-alone
287 291 test_sections['config'].exclude('profile')
288 292
289 293 # nbconvert:
290 294 sec = test_sections['nbconvert']
291 295 sec.requires('pygments', 'jinja2', 'sphinx')
292 296 # Exclude nbconvert directories containing config files used to test.
293 297 # Executing the config files with iptest would cause an exception.
294 298 sec.exclude('tests.files')
295 299 sec.exclude('exporters.tests.files')
296 300 if not have['tornado']:
297 301 sec.exclude('nbconvert.post_processors.serve')
298 302 sec.exclude('nbconvert.post_processors.tests.test_serve')
299 303
300 304 #-----------------------------------------------------------------------------
301 305 # Functions and classes
302 306 #-----------------------------------------------------------------------------
303 307
304 308 def check_exclusions_exist():
305 309 from IPython.utils.path import get_ipython_package_dir
306 310 from IPython.utils.warn import warn
307 311 parent = os.path.dirname(get_ipython_package_dir())
308 312 for sec in test_sections:
309 313 for pattern in sec.exclusions:
310 314 fullpath = pjoin(parent, pattern)
311 315 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
312 316 warn("Excluding nonexistent file: %r" % pattern)
313 317
314 318
315 319 class ExclusionPlugin(Plugin):
316 320 """A nose plugin to effect our exclusions of files and directories.
317 321 """
318 322 name = 'exclusions'
319 323 score = 3000 # Should come before any other plugins
320 324
321 325 def __init__(self, exclude_patterns=None):
322 326 """
323 327 Parameters
324 328 ----------
325 329
326 330 exclude_patterns : sequence of strings, optional
327 331 Filenames containing these patterns (as raw strings, not as regular
328 332 expressions) are excluded from the tests.
329 333 """
330 334 self.exclude_patterns = exclude_patterns or []
331 335 super(ExclusionPlugin, self).__init__()
332 336
333 337 def options(self, parser, env=os.environ):
334 338 Plugin.options(self, parser, env)
335 339
336 340 def configure(self, options, config):
337 341 Plugin.configure(self, options, config)
338 342 # Override nose trying to disable plugin.
339 343 self.enabled = True
340 344
341 345 def wantFile(self, filename):
342 346 """Return whether the given filename should be scanned for tests.
343 347 """
344 348 if any(pat in filename for pat in self.exclude_patterns):
345 349 return False
346 350 return None
347 351
348 352 def wantDirectory(self, directory):
349 353 """Return whether the given directory should be scanned for tests.
350 354 """
351 355 if any(pat in directory for pat in self.exclude_patterns):
352 356 return False
353 357 return None
354 358
355 359
360 class StreamCapturer(Thread):
361 started = False
362 def __init__(self):
363 super(StreamCapturer, self).__init__()
364 self.streams = []
365 self.buffer = BytesIO()
366 self.streams_lock = Lock()
367 self.buffer_lock = Lock()
368 self.stream_added = Event()
369 self.stop = Event()
370
371 def run(self):
372 self.started = True
373 while not self.stop.is_set():
374 with self.streams_lock:
375 streams = self.streams
376
377 if not streams:
378 self.stream_added.wait(timeout=1)
379 self.stream_added.clear()
380 continue
381
382 ready = select(streams, [], [], 0.5)[0]
383 with self.buffer_lock:
384 for fd in ready:
385 self.buffer.write(os.read(fd, 1024))
386
387 def add_stream(self, fd):
388 with self.streams_lock:
389 self.streams.append(fd)
390 self.stream_added.set()
391
392 def remove_stream(self, fd):
393 with self.streams_lock:
394 self.streams.remove(fd)
395
396 def reset_buffer(self):
397 with self.buffer_lock:
398 self.buffer.truncate(0)
399 self.buffer.seek(0)
400
401 def get_buffer(self):
402 with self.buffer_lock:
403 return self.buffer.getvalue()
404
405 def ensure_started(self):
406 if not self.started:
407 self.start()
408
409 class SubprocessStreamCapturePlugin(Plugin):
410 name='subprocstreams'
411 def __init__(self):
412 Plugin.__init__(self)
413 self.stream_capturer = StreamCapturer()
414 # This is ugly, but distant parts of the test machinery need to be able
415 # to add streams, so we make the object globally accessible.
416 nose.ipy_stream_capturer = self.stream_capturer
417
418 def configure(self, options, config):
419 Plugin.configure(self, options, config)
420 # Override nose trying to disable plugin.
421 self.enabled = True
422
423 def startTest(self, test):
424 # Reset log capture
425 self.stream_capturer.reset_buffer()
426
427 def formatFailure(self, test, err):
428 # Show output
429 ec, ev, tb = err
430 ev = safe_str(ev)
431 out = [ev, '>> begin captured subprocess output <<',
432 self.stream_capturer.get_buffer().decode('utf-8', 'replace'),
433 '>> end captured subprocess output <<']
434 return ec, '\n'.join(out), tb
435
436 formatError = formatFailure
437
438 def finalize(self, result):
439 self.stream_capturer.stop.set()
440 self.stream_capturer.join()
441
442
356 443 def run_iptest():
357 444 """Run the IPython test suite using nose.
358 445
359 446 This function is called when this script is **not** called with the form
360 447 `iptest all`. It simply calls nose with appropriate command line flags
361 448 and accepts all of the standard nose arguments.
362 449 """
363 450 # Apply our monkeypatch to Xunit
364 451 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
365 452 monkeypatch_xunit()
366 453
367 454 warnings.filterwarnings('ignore',
368 455 'This will be removed soon. Use IPython.testing.util instead')
369 456
370 457 arg1 = sys.argv[1]
371 458 if arg1 in test_sections:
372 459 section = test_sections[arg1]
373 460 sys.argv[1:2] = section.includes
374 461 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
375 462 section = test_sections[arg1[8:]]
376 463 sys.argv[1:2] = section.includes
377 464 else:
378 465 section = TestSection(arg1, includes=[arg1])
379 466
380 467
381 468 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
382 469
383 470 '--with-ipdoctest',
384 471 '--ipdoctest-tests','--ipdoctest-extension=txt',
385 472
386 473 # We add --exe because of setuptools' imbecility (it
387 474 # blindly does chmod +x on ALL files). Nose does the
388 475 # right thing and it tries to avoid executables,
389 476 # setuptools unfortunately forces our hand here. This
390 477 # has been discussed on the distutils list and the
391 478 # setuptools devs refuse to fix this problem!
392 479 '--exe',
393 480 ]
394 481 if '-a' not in argv and '-A' not in argv:
395 482 argv = argv + ['-a', '!crash']
396 483
397 484 if nose.__version__ >= '0.11':
398 485 # I don't fully understand why we need this one, but depending on what
399 486 # directory the test suite is run from, if we don't give it, 0 tests
400 487 # get run. Specifically, if the test suite is run from the source dir
401 488 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
402 489 # even if the same call done in this directory works fine). It appears
403 490 # that if the requested package is in the current dir, nose bails early
404 491 # by default. Since it's otherwise harmless, leave it in by default
405 492 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
406 493 argv.append('--traverse-namespace')
407 494
408 495 # use our plugin for doctesting. It will remove the standard doctest plugin
409 496 # if it finds it enabled
410 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
497 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
498 SubprocessStreamCapturePlugin() ]
411 499
412 500 # Use working directory set by parent process (see iptestcontroller)
413 501 if 'IPTEST_WORKING_DIR' in os.environ:
414 502 os.chdir(os.environ['IPTEST_WORKING_DIR'])
415 503
416 504 # We need a global ipython running in this process, but the special
417 505 # in-process group spawns its own IPython kernels, so for *that* group we
418 506 # must avoid also opening the global one (otherwise there's a conflict of
419 507 # singletons). Ultimately the solution to this problem is to refactor our
420 508 # assumptions about what needs to be a singleton and what doesn't (app
421 509 # objects should, individual shells shouldn't). But for now, this
422 510 # workaround allows the test suite for the inprocess module to complete.
423 511 if 'kernel.inprocess' not in section.name:
424 512 from IPython.testing import globalipapp
425 513 globalipapp.start_ipython()
426 514
427 515 # Now nose can run
428 516 TestProgram(argv=argv, addplugins=plugins)
429 517
430 518 if __name__ == '__main__':
431 519 run_iptest()
432 520
General Comments 0
You need to be logged in to leave comments. Login now