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