##// END OF EJS Templates
remove ipython_parallel
Min RK -
Show More

The requested changes are too big and content was truncated. Show full diff

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