##// END OF EJS Templates
html and js tests require sqlite3 (session manager)
MinRK -
Show More
@@ -1,524 +1,524 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 31 from io import BytesIO
32 32 import os
33 33 import os.path as path
34 34 import sys
35 35 from threading import Thread, Lock, Event
36 36 import warnings
37 37
38 38 # Now, proceed to import nose itself
39 39 import nose.plugins.builtin
40 40 from nose.plugins.xunit import Xunit
41 41 from nose import SkipTest
42 42 from nose.core import TestProgram
43 43 from nose.plugins import Plugin
44 44 from nose.util import safe_str
45 45
46 46 # Our own imports
47 47 from IPython.utils.process import is_cmd_found
48 48 from IPython.utils.importstring import import_item
49 49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 50 from IPython.external.decorators import KnownFailure, knownfailureif
51 51
52 52 pjoin = path.join
53 53
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Globals
57 57 #-----------------------------------------------------------------------------
58 58
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Warnings control
62 62 #-----------------------------------------------------------------------------
63 63
64 64 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 65 # that imports 'sets' as of today
66 66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 67 DeprecationWarning )
68 68
69 69 # This one also comes from Twisted
70 70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 71 DeprecationWarning)
72 72
73 73 # Wx on Fedora11 spits these out
74 74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 75 UserWarning)
76 76
77 77 # ------------------------------------------------------------------------------
78 78 # Monkeypatch Xunit to count known failures as skipped.
79 79 # ------------------------------------------------------------------------------
80 80 def monkeypatch_xunit():
81 81 try:
82 82 knownfailureif(True)(lambda: None)()
83 83 except Exception as e:
84 84 KnownFailureTest = type(e)
85 85
86 86 def addError(self, test, err, capt=None):
87 87 if issubclass(err[0], KnownFailureTest):
88 88 err = (SkipTest,) + err[1:]
89 89 return self.orig_addError(test, err, capt)
90 90
91 91 Xunit.orig_addError = Xunit.addError
92 92 Xunit.addError = addError
93 93
94 94 #-----------------------------------------------------------------------------
95 95 # Check which dependencies are installed and greater than minimum version.
96 96 #-----------------------------------------------------------------------------
97 97 def extract_version(mod):
98 98 return mod.__version__
99 99
100 100 def test_for(item, min_version=None, callback=extract_version):
101 101 """Test to see if item is importable, and optionally check against a minimum
102 102 version.
103 103
104 104 If min_version is given, the default behavior is to check against the
105 105 `__version__` attribute of the item, but specifying `callback` allows you to
106 106 extract the value you are interested in. e.g::
107 107
108 108 In [1]: import sys
109 109
110 110 In [2]: from IPython.testing.iptest import test_for
111 111
112 112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 113 Out[3]: True
114 114
115 115 """
116 116 try:
117 117 check = import_item(item)
118 118 except (ImportError, RuntimeError):
119 119 # GTK reports Runtime error if it can't be initialized even if it's
120 120 # importable.
121 121 return False
122 122 else:
123 123 if min_version:
124 124 if callback:
125 125 # extra processing step to get version to compare
126 126 check = callback(check)
127 127
128 128 return check >= min_version
129 129 else:
130 130 return True
131 131
132 132 # Global dict where we can store information on what we have and what we don't
133 133 # have available at test run time
134 134 have = {}
135 135
136 136 have['curses'] = test_for('_curses')
137 137 have['matplotlib'] = test_for('matplotlib')
138 138 have['numpy'] = test_for('numpy')
139 139 have['pexpect'] = test_for('IPython.external.pexpect')
140 140 have['pymongo'] = test_for('pymongo')
141 141 have['pygments'] = test_for('pygments')
142 142 have['qt'] = test_for('IPython.external.qt')
143 143 have['rpy2'] = test_for('rpy2')
144 144 have['sqlite3'] = test_for('sqlite3')
145 145 have['cython'] = test_for('Cython')
146 146 have['oct2py'] = test_for('oct2py')
147 147 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
148 148 have['jinja2'] = test_for('jinja2')
149 149 have['requests'] = test_for('requests')
150 150 have['sphinx'] = test_for('sphinx')
151 151 have['casperjs'] = is_cmd_found('casperjs')
152 152
153 153 min_zmq = (2,1,11)
154 154
155 155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
156 156
157 157 #-----------------------------------------------------------------------------
158 158 # Test suite definitions
159 159 #-----------------------------------------------------------------------------
160 160
161 161 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
162 162 'extensions', 'lib', 'terminal', 'testing', 'utils',
163 163 'nbformat', 'qt', 'html', 'nbconvert'
164 164 ]
165 165
166 166 class TestSection(object):
167 167 def __init__(self, name, includes):
168 168 self.name = name
169 169 self.includes = includes
170 170 self.excludes = []
171 171 self.dependencies = []
172 172 self.enabled = True
173 173
174 174 def exclude(self, module):
175 175 if not module.startswith('IPython'):
176 176 module = self.includes[0] + "." + module
177 177 self.excludes.append(module.replace('.', os.sep))
178 178
179 179 def requires(self, *packages):
180 180 self.dependencies.extend(packages)
181 181
182 182 @property
183 183 def will_run(self):
184 184 return self.enabled and all(have[p] for p in self.dependencies)
185 185
186 186 # Name -> (include, exclude, dependencies_met)
187 187 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
188 188
189 189 # Exclusions and dependencies
190 190 # ---------------------------
191 191
192 192 # core:
193 193 sec = test_sections['core']
194 194 if not have['sqlite3']:
195 195 sec.exclude('tests.test_history')
196 196 sec.exclude('history')
197 197 if not have['matplotlib']:
198 198 sec.exclude('pylabtools'),
199 199 sec.exclude('tests.test_pylabtools')
200 200
201 201 # lib:
202 202 sec = test_sections['lib']
203 203 if not have['zmq']:
204 204 sec.exclude('kernel')
205 205 # We do this unconditionally, so that the test suite doesn't import
206 206 # gtk, changing the default encoding and masking some unicode bugs.
207 207 sec.exclude('inputhookgtk')
208 208 # We also do this unconditionally, because wx can interfere with Unix signals.
209 209 # There are currently no tests for it anyway.
210 210 sec.exclude('inputhookwx')
211 211 # Testing inputhook will need a lot of thought, to figure out
212 212 # how to have tests that don't lock up with the gui event
213 213 # loops in the picture
214 214 sec.exclude('inputhook')
215 215
216 216 # testing:
217 217 sec = test_sections['testing']
218 218 # These have to be skipped on win32 because they use echo, rm, cd, etc.
219 219 # See ticket https://github.com/ipython/ipython/issues/87
220 220 if sys.platform == 'win32':
221 221 sec.exclude('plugin.test_exampleip')
222 222 sec.exclude('plugin.dtexample')
223 223
224 224 # terminal:
225 225 if (not have['pexpect']) or (not have['zmq']):
226 226 test_sections['terminal'].exclude('console')
227 227
228 228 # parallel
229 229 sec = test_sections['parallel']
230 230 sec.requires('zmq')
231 231 if not have['pymongo']:
232 232 sec.exclude('controller.mongodb')
233 233 sec.exclude('tests.test_mongodb')
234 234
235 235 # kernel:
236 236 sec = test_sections['kernel']
237 237 sec.requires('zmq')
238 238 # The in-process kernel tests are done in a separate section
239 239 sec.exclude('inprocess')
240 240 # importing gtk sets the default encoding, which we want to avoid
241 241 sec.exclude('zmq.gui.gtkembed')
242 242 if not have['matplotlib']:
243 243 sec.exclude('zmq.pylab')
244 244
245 245 # kernel.inprocess:
246 246 test_sections['kernel.inprocess'].requires('zmq')
247 247
248 248 # extensions:
249 249 sec = test_sections['extensions']
250 250 if not have['cython']:
251 251 sec.exclude('cythonmagic')
252 252 sec.exclude('tests.test_cythonmagic')
253 253 if not have['oct2py']:
254 254 sec.exclude('octavemagic')
255 255 sec.exclude('tests.test_octavemagic')
256 256 if not have['rpy2'] or not have['numpy']:
257 257 sec.exclude('rmagic')
258 258 sec.exclude('tests.test_rmagic')
259 259 # autoreload does some strange stuff, so move it to its own test section
260 260 sec.exclude('autoreload')
261 261 sec.exclude('tests.test_autoreload')
262 262 test_sections['autoreload'] = TestSection('autoreload',
263 263 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
264 264 test_group_names.append('autoreload')
265 265
266 266 # qt:
267 267 test_sections['qt'].requires('zmq', 'qt', 'pygments')
268 268
269 269 # html:
270 270 sec = test_sections['html']
271 sec.requires('zmq', 'tornado', 'requests')
271 sec.requires('zmq', 'tornado', 'requests', 'sqlite3')
272 272 # The notebook 'static' directory contains JS, css and other
273 273 # files for web serving. Occasionally projects may put a .py
274 274 # file in there (MathJax ships a conf.py), so we might as
275 275 # well play it safe and skip the whole thing.
276 276 sec.exclude('static')
277 277 sec.exclude('fabfile')
278 278 if not have['jinja2']:
279 279 sec.exclude('notebookapp')
280 280 if not have['pygments'] or not have['jinja2']:
281 281 sec.exclude('nbconvert')
282 282
283 283 # config:
284 284 # Config files aren't really importable stand-alone
285 285 test_sections['config'].exclude('profile')
286 286
287 287 # nbconvert:
288 288 sec = test_sections['nbconvert']
289 289 sec.requires('pygments', 'jinja2')
290 290 # Exclude nbconvert directories containing config files used to test.
291 291 # Executing the config files with iptest would cause an exception.
292 292 sec.exclude('tests.files')
293 293 sec.exclude('exporters.tests.files')
294 294 if not have['tornado']:
295 295 sec.exclude('nbconvert.post_processors.serve')
296 296 sec.exclude('nbconvert.post_processors.tests.test_serve')
297 297
298 298 #-----------------------------------------------------------------------------
299 299 # Functions and classes
300 300 #-----------------------------------------------------------------------------
301 301
302 302 def check_exclusions_exist():
303 303 from IPython.utils.path import get_ipython_package_dir
304 304 from IPython.utils.warn import warn
305 305 parent = os.path.dirname(get_ipython_package_dir())
306 306 for sec in test_sections:
307 307 for pattern in sec.exclusions:
308 308 fullpath = pjoin(parent, pattern)
309 309 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
310 310 warn("Excluding nonexistent file: %r" % pattern)
311 311
312 312
313 313 class ExclusionPlugin(Plugin):
314 314 """A nose plugin to effect our exclusions of files and directories.
315 315 """
316 316 name = 'exclusions'
317 317 score = 3000 # Should come before any other plugins
318 318
319 319 def __init__(self, exclude_patterns=None):
320 320 """
321 321 Parameters
322 322 ----------
323 323
324 324 exclude_patterns : sequence of strings, optional
325 325 Filenames containing these patterns (as raw strings, not as regular
326 326 expressions) are excluded from the tests.
327 327 """
328 328 self.exclude_patterns = exclude_patterns or []
329 329 super(ExclusionPlugin, self).__init__()
330 330
331 331 def options(self, parser, env=os.environ):
332 332 Plugin.options(self, parser, env)
333 333
334 334 def configure(self, options, config):
335 335 Plugin.configure(self, options, config)
336 336 # Override nose trying to disable plugin.
337 337 self.enabled = True
338 338
339 339 def wantFile(self, filename):
340 340 """Return whether the given filename should be scanned for tests.
341 341 """
342 342 if any(pat in filename for pat in self.exclude_patterns):
343 343 return False
344 344 return None
345 345
346 346 def wantDirectory(self, directory):
347 347 """Return whether the given directory should be scanned for tests.
348 348 """
349 349 if any(pat in directory for pat in self.exclude_patterns):
350 350 return False
351 351 return None
352 352
353 353
354 354 class StreamCapturer(Thread):
355 355 daemon = True # Don't hang if main thread crashes
356 356 started = False
357 357 def __init__(self):
358 358 super(StreamCapturer, self).__init__()
359 359 self.streams = []
360 360 self.buffer = BytesIO()
361 361 self.readfd, self.writefd = os.pipe()
362 362 self.buffer_lock = Lock()
363 363 self.stop = Event()
364 364
365 365 def run(self):
366 366 self.started = True
367 367
368 368 while not self.stop.is_set():
369 369 chunk = os.read(self.readfd, 1024)
370 370
371 371 with self.buffer_lock:
372 372 self.buffer.write(chunk)
373 373
374 374 os.close(self.readfd)
375 375 os.close(self.writefd)
376 376
377 377 def reset_buffer(self):
378 378 with self.buffer_lock:
379 379 self.buffer.truncate(0)
380 380 self.buffer.seek(0)
381 381
382 382 def get_buffer(self):
383 383 with self.buffer_lock:
384 384 return self.buffer.getvalue()
385 385
386 386 def ensure_started(self):
387 387 if not self.started:
388 388 self.start()
389 389
390 390 def halt(self):
391 391 """Safely stop the thread."""
392 392 if not self.started:
393 393 return
394 394
395 395 self.stop.set()
396 396 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
397 397 self.join()
398 398
399 399 class SubprocessStreamCapturePlugin(Plugin):
400 400 name='subprocstreams'
401 401 def __init__(self):
402 402 Plugin.__init__(self)
403 403 self.stream_capturer = StreamCapturer()
404 404 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
405 405 # This is ugly, but distant parts of the test machinery need to be able
406 406 # to redirect streams, so we make the object globally accessible.
407 407 nose.iptest_stdstreams_fileno = self.get_write_fileno
408 408
409 409 def get_write_fileno(self):
410 410 if self.destination == 'capture':
411 411 self.stream_capturer.ensure_started()
412 412 return self.stream_capturer.writefd
413 413 elif self.destination == 'discard':
414 414 return os.open(os.devnull, os.O_WRONLY)
415 415 else:
416 416 return sys.__stdout__.fileno()
417 417
418 418 def configure(self, options, config):
419 419 Plugin.configure(self, options, config)
420 420 # Override nose trying to disable plugin.
421 421 if self.destination == 'capture':
422 422 self.enabled = True
423 423
424 424 def startTest(self, test):
425 425 # Reset log capture
426 426 self.stream_capturer.reset_buffer()
427 427
428 428 def formatFailure(self, test, err):
429 429 # Show output
430 430 ec, ev, tb = err
431 431 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
432 432 if captured.strip():
433 433 ev = safe_str(ev)
434 434 out = [ev, '>> begin captured subprocess output <<',
435 435 captured,
436 436 '>> end captured subprocess output <<']
437 437 return ec, '\n'.join(out), tb
438 438
439 439 return err
440 440
441 441 formatError = formatFailure
442 442
443 443 def finalize(self, result):
444 444 self.stream_capturer.halt()
445 445
446 446
447 447 def run_iptest():
448 448 """Run the IPython test suite using nose.
449 449
450 450 This function is called when this script is **not** called with the form
451 451 `iptest all`. It simply calls nose with appropriate command line flags
452 452 and accepts all of the standard nose arguments.
453 453 """
454 454 # Apply our monkeypatch to Xunit
455 455 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
456 456 monkeypatch_xunit()
457 457
458 458 warnings.filterwarnings('ignore',
459 459 'This will be removed soon. Use IPython.testing.util instead')
460 460
461 461 arg1 = sys.argv[1]
462 462 if arg1 in test_sections:
463 463 section = test_sections[arg1]
464 464 sys.argv[1:2] = section.includes
465 465 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
466 466 section = test_sections[arg1[8:]]
467 467 sys.argv[1:2] = section.includes
468 468 else:
469 469 section = TestSection(arg1, includes=[arg1])
470 470
471 471
472 472 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
473 473
474 474 '--with-ipdoctest',
475 475 '--ipdoctest-tests','--ipdoctest-extension=txt',
476 476
477 477 # We add --exe because of setuptools' imbecility (it
478 478 # blindly does chmod +x on ALL files). Nose does the
479 479 # right thing and it tries to avoid executables,
480 480 # setuptools unfortunately forces our hand here. This
481 481 # has been discussed on the distutils list and the
482 482 # setuptools devs refuse to fix this problem!
483 483 '--exe',
484 484 ]
485 485 if '-a' not in argv and '-A' not in argv:
486 486 argv = argv + ['-a', '!crash']
487 487
488 488 if nose.__version__ >= '0.11':
489 489 # I don't fully understand why we need this one, but depending on what
490 490 # directory the test suite is run from, if we don't give it, 0 tests
491 491 # get run. Specifically, if the test suite is run from the source dir
492 492 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
493 493 # even if the same call done in this directory works fine). It appears
494 494 # that if the requested package is in the current dir, nose bails early
495 495 # by default. Since it's otherwise harmless, leave it in by default
496 496 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
497 497 argv.append('--traverse-namespace')
498 498
499 499 # use our plugin for doctesting. It will remove the standard doctest plugin
500 500 # if it finds it enabled
501 501 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
502 502 SubprocessStreamCapturePlugin() ]
503 503
504 504 # Use working directory set by parent process (see iptestcontroller)
505 505 if 'IPTEST_WORKING_DIR' in os.environ:
506 506 os.chdir(os.environ['IPTEST_WORKING_DIR'])
507 507
508 508 # We need a global ipython running in this process, but the special
509 509 # in-process group spawns its own IPython kernels, so for *that* group we
510 510 # must avoid also opening the global one (otherwise there's a conflict of
511 511 # singletons). Ultimately the solution to this problem is to refactor our
512 512 # assumptions about what needs to be a singleton and what doesn't (app
513 513 # objects should, individual shells shouldn't). But for now, this
514 514 # workaround allows the test suite for the inprocess module to complete.
515 515 if 'kernel.inprocess' not in section.name:
516 516 from IPython.testing import globalipapp
517 517 globalipapp.start_ipython()
518 518
519 519 # Now nose can run
520 520 TestProgram(argv=argv, addplugins=plugins)
521 521
522 522 if __name__ == '__main__':
523 523 run_iptest()
524 524
@@ -1,595 +1,595 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2009-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19 from __future__ import print_function
20 20
21 21 import argparse
22 22 import json
23 23 import multiprocessing.pool
24 24 import os
25 25 import shutil
26 26 import signal
27 27 import sys
28 28 import subprocess
29 29 import time
30 30
31 31 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
32 32 from IPython.utils.path import compress_user
33 33 from IPython.utils.py3compat import bytes_to_str
34 34 from IPython.utils.sysinfo import get_sys_info
35 35 from IPython.utils.tempdir import TemporaryDirectory
36 36
37 37
38 38 class TestController(object):
39 39 """Run tests in a subprocess
40 40 """
41 41 #: str, IPython test suite to be executed.
42 42 section = None
43 43 #: list, command line arguments to be executed
44 44 cmd = None
45 45 #: dict, extra environment variables to set for the subprocess
46 46 env = None
47 47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 48 dirs = None
49 49 #: subprocess.Popen instance
50 50 process = None
51 51 #: str, process stdout+stderr
52 52 stdout = None
53 53
54 54 def __init__(self):
55 55 self.cmd = []
56 56 self.env = {}
57 57 self.dirs = []
58 58
59 59 def setup(self):
60 60 """Create temporary directories etc.
61 61
62 62 This is only called when we know the test group will be run. Things
63 63 created here may be cleaned up by self.cleanup().
64 64 """
65 65 pass
66 66
67 67 def launch(self, buffer_output=False):
68 68 # print('*** ENV:', self.env) # dbg
69 69 # print('*** CMD:', self.cmd) # dbg
70 70 env = os.environ.copy()
71 71 env.update(self.env)
72 72 output = subprocess.PIPE if buffer_output else None
73 73 stdout = subprocess.STDOUT if buffer_output else None
74 74 self.process = subprocess.Popen(self.cmd, stdout=output,
75 75 stderr=stdout, env=env)
76 76
77 77 def wait(self):
78 78 self.stdout, _ = self.process.communicate()
79 79 return self.process.returncode
80 80
81 81 def print_extra_info(self):
82 82 """Print extra information about this test run.
83 83
84 84 If we're running in parallel and showing the concise view, this is only
85 85 called if the test group fails. Otherwise, it's called before the test
86 86 group is started.
87 87
88 88 The base implementation does nothing, but it can be overridden by
89 89 subclasses.
90 90 """
91 91 return
92 92
93 93 def cleanup_process(self):
94 94 """Cleanup on exit by killing any leftover processes."""
95 95 subp = self.process
96 96 if subp is None or (subp.poll() is not None):
97 97 return # Process doesn't exist, or is already dead.
98 98
99 99 try:
100 100 print('Cleaning up stale PID: %d' % subp.pid)
101 101 subp.kill()
102 102 except: # (OSError, WindowsError) ?
103 103 # This is just a best effort, if we fail or the process was
104 104 # really gone, ignore it.
105 105 pass
106 106 else:
107 107 for i in range(10):
108 108 if subp.poll() is None:
109 109 time.sleep(0.1)
110 110 else:
111 111 break
112 112
113 113 if subp.poll() is None:
114 114 # The process did not die...
115 115 print('... failed. Manual cleanup may be required.')
116 116
117 117 def cleanup(self):
118 118 "Kill process if it's still alive, and clean up temporary directories"
119 119 self.cleanup_process()
120 120 for td in self.dirs:
121 121 td.cleanup()
122 122
123 123 __del__ = cleanup
124 124
125 125 class PyTestController(TestController):
126 126 """Run Python tests using IPython.testing.iptest"""
127 127 #: str, Python command to execute in subprocess
128 128 pycmd = None
129 129
130 130 def __init__(self, section):
131 131 """Create new test runner."""
132 132 TestController.__init__(self)
133 133 self.section = section
134 134 # pycmd is put into cmd[2] in PyTestController.launch()
135 135 self.cmd = [sys.executable, '-c', None, section]
136 136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
137 137
138 138 def setup(self):
139 139 ipydir = TemporaryDirectory()
140 140 self.dirs.append(ipydir)
141 141 self.env['IPYTHONDIR'] = ipydir.name
142 142 self.workingdir = workingdir = TemporaryDirectory()
143 143 self.dirs.append(workingdir)
144 144 self.env['IPTEST_WORKING_DIR'] = workingdir.name
145 145 # This means we won't get odd effects from our own matplotlib config
146 146 self.env['MPLCONFIGDIR'] = workingdir.name
147 147
148 148 @property
149 149 def will_run(self):
150 150 try:
151 151 return test_sections[self.section].will_run
152 152 except KeyError:
153 153 return True
154 154
155 155 def add_xunit(self):
156 156 xunit_file = os.path.abspath(self.section + '.xunit.xml')
157 157 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
158 158
159 159 def add_coverage(self):
160 160 try:
161 161 sources = test_sections[self.section].includes
162 162 except KeyError:
163 163 sources = ['IPython']
164 164
165 165 coverage_rc = ("[run]\n"
166 166 "data_file = {data_file}\n"
167 167 "source =\n"
168 168 " {source}\n"
169 169 ).format(data_file=os.path.abspath('.coverage.'+self.section),
170 170 source="\n ".join(sources))
171 171 config_file = os.path.join(self.workingdir.name, '.coveragerc')
172 172 with open(config_file, 'w') as f:
173 173 f.write(coverage_rc)
174 174
175 175 self.env['COVERAGE_PROCESS_START'] = config_file
176 176 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
177 177
178 178 def launch(self, buffer_output=False):
179 179 self.cmd[2] = self.pycmd
180 180 super(PyTestController, self).launch(buffer_output=buffer_output)
181 181
182 182 js_prefix = 'js/'
183 183
184 184 def get_js_test_dir():
185 185 import IPython.html.tests as t
186 186 return os.path.join(os.path.dirname(t.__file__), '')
187 187
188 188 def all_js_groups():
189 189 import glob
190 190 test_dir = get_js_test_dir()
191 191 all_subdirs = glob.glob(test_dir + '*/')
192 192 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
193 193
194 194 class JSController(TestController):
195 195 """Run CasperJS tests """
196 196 def __init__(self, section):
197 197 """Create new test runner."""
198 198 TestController.__init__(self)
199 199 self.section = section
200 200 js_test_dir = get_js_test_dir()
201 201 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
202 202 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
203 203 self.cmd = ['casperjs', 'test', includes, test_cases]
204 204
205 205 def setup(self):
206 206 self.ipydir = TemporaryDirectory()
207 207 self.nbdir = TemporaryDirectory()
208 208 self.dirs.append(self.ipydir)
209 209 self.dirs.append(self.nbdir)
210 210 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
211 211 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
212 212
213 213 # start the ipython notebook, so we get the port number
214 214 self.server_port = 0
215 215 self._init_server()
216 216 if self.server_port:
217 217 self.cmd.append("--port=%i" % self.server_port)
218 218
219 219 def print_extra_info(self):
220 220 print("Running tests with notebook directory %r" % self.nbdir.name)
221 221
222 222 @property
223 223 def will_run(self):
224 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
224 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3'])
225 225
226 226 def _init_server(self):
227 227 "Start the notebook server in a separate process"
228 228 self.server_command = command = [sys.executable,
229 229 '-m', 'IPython.html',
230 230 '--no-browser',
231 231 '--ipython-dir', self.ipydir.name,
232 232 '--notebook-dir', self.nbdir.name,
233 233 ]
234 234 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
235 235 # which run afoul of ipc's maximum path length.
236 236 if sys.platform.startswith('linux'):
237 237 command.append('--KernelManager.transport=ipc')
238 238 self.stream_capturer = c = StreamCapturer()
239 239 c.start()
240 240 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
241 241 self.server_info_file = os.path.join(self.ipydir.name,
242 242 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
243 243 )
244 244 self._wait_for_server()
245 245
246 246 def _wait_for_server(self):
247 247 """Wait 30 seconds for the notebook server to start"""
248 248 for i in range(300):
249 249 if self.server.poll() is not None:
250 250 return self._failed_to_start()
251 251 if os.path.exists(self.server_info_file):
252 252 self._load_server_info()
253 253 return
254 254 time.sleep(0.1)
255 255 print("Notebook server-info file never arrived: %s" % self.server_info_file,
256 256 file=sys.stderr
257 257 )
258 258
259 259 def _failed_to_start(self):
260 260 """Notebook server exited prematurely"""
261 261 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
262 262 print("Notebook failed to start: ", file=sys.stderr)
263 263 print(self.server_command)
264 264 print(captured, file=sys.stderr)
265 265
266 266 def _load_server_info(self):
267 267 """Notebook server started, load connection info from JSON"""
268 268 with open(self.server_info_file) as f:
269 269 info = json.load(f)
270 270 self.server_port = info['port']
271 271
272 272 def cleanup(self):
273 273 self.stream_capturer.halt()
274 274 try:
275 275 self.server.terminate()
276 276 except OSError:
277 277 # already dead
278 278 pass
279 279 self.server.wait()
280 280 TestController.cleanup(self)
281 281
282 282
283 283 def prepare_controllers(options):
284 284 """Returns two lists of TestController instances, those to run, and those
285 285 not to run."""
286 286 testgroups = options.testgroups
287 287
288 288 if testgroups:
289 289 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
290 290 or g.startswith('IPython.')]
291 291 if 'js' in testgroups:
292 292 js_testgroups = all_js_groups()
293 293 else:
294 294 js_testgroups = [g for g in testgroups if g not in py_testgroups]
295 295 else:
296 296 py_testgroups = py_test_group_names
297 297 js_testgroups = all_js_groups()
298 298 if not options.all:
299 299 test_sections['parallel'].enabled = False
300 300
301 301 c_js = [JSController(name) for name in js_testgroups]
302 302 c_py = [PyTestController(name) for name in py_testgroups]
303 303
304 304 configure_py_controllers(c_py, xunit=options.xunit,
305 305 coverage=options.coverage, subproc_streams=options.subproc_streams,
306 306 extra_args=options.extra_args)
307 307
308 308 controllers = c_py + c_js
309 309 to_run = [c for c in controllers if c.will_run]
310 310 not_run = [c for c in controllers if not c.will_run]
311 311 return to_run, not_run
312 312
313 313 def configure_py_controllers(controllers, xunit=False, coverage=False,
314 314 subproc_streams='capture', extra_args=()):
315 315 """Apply options for a collection of TestController objects."""
316 316 for controller in controllers:
317 317 if xunit:
318 318 controller.add_xunit()
319 319 if coverage:
320 320 controller.add_coverage()
321 321 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
322 322 controller.cmd.extend(extra_args)
323 323
324 324 def do_run(controller, buffer_output=True):
325 325 """Setup and run a test controller.
326 326
327 327 If buffer_output is True, no output is displayed, to avoid it appearing
328 328 interleaved. In this case, the caller is responsible for displaying test
329 329 output on failure.
330 330
331 331 Returns
332 332 -------
333 333 controller : TestController
334 334 The same controller as passed in, as a convenience for using map() type
335 335 APIs.
336 336 exitcode : int
337 337 The exit code of the test subprocess. Non-zero indicates failure.
338 338 """
339 339 try:
340 340 try:
341 341 controller.setup()
342 342 if not buffer_output:
343 343 controller.print_extra_info()
344 344 controller.launch(buffer_output=buffer_output)
345 345 except Exception:
346 346 import traceback
347 347 traceback.print_exc()
348 348 return controller, 1 # signal failure
349 349
350 350 exitcode = controller.wait()
351 351 return controller, exitcode
352 352
353 353 except KeyboardInterrupt:
354 354 return controller, -signal.SIGINT
355 355 finally:
356 356 controller.cleanup()
357 357
358 358 def report():
359 359 """Return a string with a summary report of test-related variables."""
360 360 inf = get_sys_info()
361 361 out = []
362 362 def _add(name, value):
363 363 out.append((name, value))
364 364
365 365 _add('IPython version', inf['ipython_version'])
366 366 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
367 367 _add('IPython package', compress_user(inf['ipython_path']))
368 368 _add('Python version', inf['sys_version'].replace('\n',''))
369 369 _add('sys.executable', compress_user(inf['sys_executable']))
370 370 _add('Platform', inf['platform'])
371 371
372 372 width = max(len(n) for (n,v) in out)
373 373 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
374 374
375 375 avail = []
376 376 not_avail = []
377 377
378 378 for k, is_avail in have.items():
379 379 if is_avail:
380 380 avail.append(k)
381 381 else:
382 382 not_avail.append(k)
383 383
384 384 if avail:
385 385 out.append('\nTools and libraries available at test time:\n')
386 386 avail.sort()
387 387 out.append(' ' + ' '.join(avail)+'\n')
388 388
389 389 if not_avail:
390 390 out.append('\nTools and libraries NOT available at test time:\n')
391 391 not_avail.sort()
392 392 out.append(' ' + ' '.join(not_avail)+'\n')
393 393
394 394 return ''.join(out)
395 395
396 396 def run_iptestall(options):
397 397 """Run the entire IPython test suite by calling nose and trial.
398 398
399 399 This function constructs :class:`IPTester` instances for all IPython
400 400 modules and package and then runs each of them. This causes the modules
401 401 and packages of IPython to be tested each in their own subprocess using
402 402 nose.
403 403
404 404 Parameters
405 405 ----------
406 406
407 407 All parameters are passed as attributes of the options object.
408 408
409 409 testgroups : list of str
410 410 Run only these sections of the test suite. If empty, run all the available
411 411 sections.
412 412
413 413 fast : int or None
414 414 Run the test suite in parallel, using n simultaneous processes. If None
415 415 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
416 416
417 417 inc_slow : bool
418 418 Include slow tests, like IPython.parallel. By default, these tests aren't
419 419 run.
420 420
421 421 xunit : bool
422 422 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
423 423
424 424 coverage : bool or str
425 425 Measure code coverage from tests. True will store the raw coverage data,
426 426 or pass 'html' or 'xml' to get reports.
427 427
428 428 extra_args : list
429 429 Extra arguments to pass to the test subprocesses, e.g. '-v'
430 430 """
431 431 to_run, not_run = prepare_controllers(options)
432 432
433 433 def justify(ltext, rtext, width=70, fill='-'):
434 434 ltext += ' '
435 435 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
436 436 return ltext + rtext
437 437
438 438 # Run all test runners, tracking execution time
439 439 failed = []
440 440 t_start = time.time()
441 441
442 442 print()
443 443 if options.fast == 1:
444 444 # This actually means sequential, i.e. with 1 job
445 445 for controller in to_run:
446 446 print('Test group:', controller.section)
447 447 sys.stdout.flush() # Show in correct order when output is piped
448 448 controller, res = do_run(controller, buffer_output=False)
449 449 if res:
450 450 failed.append(controller)
451 451 if res == -signal.SIGINT:
452 452 print("Interrupted")
453 453 break
454 454 print()
455 455
456 456 else:
457 457 # Run tests concurrently
458 458 try:
459 459 pool = multiprocessing.pool.ThreadPool(options.fast)
460 460 for (controller, res) in pool.imap_unordered(do_run, to_run):
461 461 res_string = 'OK' if res == 0 else 'FAILED'
462 462 print(justify('Test group: ' + controller.section, res_string))
463 463 if res:
464 464 controller.print_extra_info()
465 465 print(bytes_to_str(controller.stdout))
466 466 failed.append(controller)
467 467 if res == -signal.SIGINT:
468 468 print("Interrupted")
469 469 break
470 470 except KeyboardInterrupt:
471 471 return
472 472
473 473 for controller in not_run:
474 474 print(justify('Test group: ' + controller.section, 'NOT RUN'))
475 475
476 476 t_end = time.time()
477 477 t_tests = t_end - t_start
478 478 nrunners = len(to_run)
479 479 nfail = len(failed)
480 480 # summarize results
481 481 print('_'*70)
482 482 print('Test suite completed for system with the following information:')
483 483 print(report())
484 484 took = "Took %.3fs." % t_tests
485 485 print('Status: ', end='')
486 486 if not failed:
487 487 print('OK (%d test groups).' % nrunners, took)
488 488 else:
489 489 # If anything went wrong, point out what command to rerun manually to
490 490 # see the actual errors and individual summary
491 491 failed_sections = [c.section for c in failed]
492 492 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
493 493 nrunners, ', '.join(failed_sections)), took)
494 494 print()
495 495 print('You may wish to rerun these, with:')
496 496 print(' iptest', *failed_sections)
497 497 print()
498 498
499 499 if options.coverage:
500 500 from coverage import coverage
501 501 cov = coverage(data_file='.coverage')
502 502 cov.combine()
503 503 cov.save()
504 504
505 505 # Coverage HTML report
506 506 if options.coverage == 'html':
507 507 html_dir = 'ipy_htmlcov'
508 508 shutil.rmtree(html_dir, ignore_errors=True)
509 509 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
510 510 sys.stdout.flush()
511 511
512 512 # Custom HTML reporter to clean up module names.
513 513 from coverage.html import HtmlReporter
514 514 class CustomHtmlReporter(HtmlReporter):
515 515 def find_code_units(self, morfs):
516 516 super(CustomHtmlReporter, self).find_code_units(morfs)
517 517 for cu in self.code_units:
518 518 nameparts = cu.name.split(os.sep)
519 519 if 'IPython' not in nameparts:
520 520 continue
521 521 ix = nameparts.index('IPython')
522 522 cu.name = '.'.join(nameparts[ix:])
523 523
524 524 # Reimplement the html_report method with our custom reporter
525 525 cov._harvest_data()
526 526 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
527 527 html_title='IPython test coverage',
528 528 )
529 529 reporter = CustomHtmlReporter(cov, cov.config)
530 530 reporter.report(None)
531 531 print('done.')
532 532
533 533 # Coverage XML report
534 534 elif options.coverage == 'xml':
535 535 cov.xml_report(outfile='ipy_coverage.xml')
536 536
537 537 if failed:
538 538 # Ensure that our exit code indicates failure
539 539 sys.exit(1)
540 540
541 541 argparser = argparse.ArgumentParser(description='Run IPython test suite')
542 542 argparser.add_argument('testgroups', nargs='*',
543 543 help='Run specified groups of tests. If omitted, run '
544 544 'all tests.')
545 545 argparser.add_argument('--all', action='store_true',
546 546 help='Include slow tests not run by default.')
547 547 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
548 548 help='Run test sections in parallel. This starts as many '
549 549 'processes as you have cores, or you can specify a number.')
550 550 argparser.add_argument('--xunit', action='store_true',
551 551 help='Produce Xunit XML results')
552 552 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
553 553 help="Measure test coverage. Specify 'html' or "
554 554 "'xml' to get reports.")
555 555 argparser.add_argument('--subproc-streams', default='capture',
556 556 help="What to do with stdout/stderr from subprocesses. "
557 557 "'capture' (default), 'show' and 'discard' are the options.")
558 558
559 559 def default_options():
560 560 """Get an argparse Namespace object with the default arguments, to pass to
561 561 :func:`run_iptestall`.
562 562 """
563 563 options = argparser.parse_args([])
564 564 options.extra_args = []
565 565 return options
566 566
567 567 def main():
568 568 # iptest doesn't work correctly if the working directory is the
569 569 # root of the IPython source tree. Tell the user to avoid
570 570 # frustration.
571 571 if os.path.exists(os.path.join(os.getcwd(),
572 572 'IPython', 'testing', '__main__.py')):
573 573 print("Don't run iptest from the IPython source directory",
574 574 file=sys.stderr)
575 575 sys.exit(1)
576 576 # Arguments after -- should be passed through to nose. Argparse treats
577 577 # everything after -- as regular positional arguments, so we separate them
578 578 # first.
579 579 try:
580 580 ix = sys.argv.index('--')
581 581 except ValueError:
582 582 to_parse = sys.argv[1:]
583 583 extra_args = []
584 584 else:
585 585 to_parse = sys.argv[1:ix]
586 586 extra_args = sys.argv[ix+1:]
587 587
588 588 options = argparser.parse_args(to_parse)
589 589 options.extra_args = extra_args
590 590
591 591 run_iptestall(options)
592 592
593 593
594 594 if __name__ == '__main__':
595 595 main()
General Comments 0
You need to be logged in to leave comments. Login now