|
@@
-1,492
+1,479
b''
|
|
1
|
# -*- coding: utf-8 -*-
|
|
1
|
# -*- coding: utf-8 -*-
|
|
2
|
"""IPython Test Suite Runner.
|
|
2
|
"""IPython Test Suite Runner.
|
|
3
|
|
|
3
|
|
|
4
|
This module provides a main entry point to a user script to test IPython
|
|
4
|
This module provides a main entry point to a user script to test IPython
|
|
5
|
itself from the command line. There are two ways of running this script:
|
|
5
|
itself from the command line. There are two ways of running this script:
|
|
6
|
|
|
6
|
|
|
7
|
1. With the syntax `iptest all`. This runs our entire test suite by
|
|
7
|
1. With the syntax `iptest all`. This runs our entire test suite by
|
|
8
|
calling this script (with different arguments) recursively. This
|
|
8
|
calling this script (with different arguments) recursively. This
|
|
9
|
causes modules and package to be tested in different processes, using nose
|
|
9
|
causes modules and package to be tested in different processes, using nose
|
|
10
|
or trial where appropriate.
|
|
10
|
or trial where appropriate.
|
|
11
|
2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
|
|
11
|
2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
|
|
12
|
the script simply calls nose, but with special command line flags and
|
|
12
|
the script simply calls nose, but with special command line flags and
|
|
13
|
plugins loaded.
|
|
13
|
plugins loaded.
|
|
14
|
|
|
14
|
|
|
15
|
"""
|
|
15
|
"""
|
|
16
|
|
|
16
|
|
|
17
|
# Copyright (c) IPython Development Team.
|
|
17
|
# Copyright (c) IPython Development Team.
|
|
18
|
# Distributed under the terms of the Modified BSD License.
|
|
18
|
# Distributed under the terms of the Modified BSD License.
|
|
19
|
|
|
19
|
|
|
20
|
from __future__ import print_function
|
|
20
|
from __future__ import print_function
|
|
21
|
|
|
21
|
|
|
22
|
import glob
|
|
22
|
import glob
|
|
23
|
from io import BytesIO
|
|
23
|
from io import BytesIO
|
|
24
|
import os
|
|
24
|
import os
|
|
25
|
import os.path as path
|
|
25
|
import os.path as path
|
|
26
|
import sys
|
|
26
|
import sys
|
|
27
|
from threading import Thread, Lock, Event
|
|
27
|
from threading import Thread, Lock, Event
|
|
28
|
import warnings
|
|
28
|
import warnings
|
|
29
|
|
|
29
|
|
|
30
|
import nose.plugins.builtin
|
|
30
|
import nose.plugins.builtin
|
|
31
|
from nose.plugins.xunit import Xunit
|
|
31
|
from nose.plugins.xunit import Xunit
|
|
32
|
from nose import SkipTest
|
|
32
|
from nose import SkipTest
|
|
33
|
from nose.core import TestProgram
|
|
33
|
from nose.core import TestProgram
|
|
34
|
from nose.plugins import Plugin
|
|
34
|
from nose.plugins import Plugin
|
|
35
|
from nose.util import safe_str
|
|
35
|
from nose.util import safe_str
|
|
36
|
|
|
36
|
|
|
37
|
from IPython.utils.process import is_cmd_found
|
|
37
|
from IPython.utils.process import is_cmd_found
|
|
38
|
from IPython.utils.py3compat import bytes_to_str
|
|
38
|
from IPython.utils.py3compat import bytes_to_str
|
|
39
|
from IPython.utils.importstring import import_item
|
|
39
|
from IPython.utils.importstring import import_item
|
|
40
|
from IPython.testing.plugin.ipdoctest import IPythonDoctest
|
|
40
|
from IPython.testing.plugin.ipdoctest import IPythonDoctest
|
|
41
|
from IPython.external.decorators import KnownFailure, knownfailureif
|
|
41
|
from IPython.external.decorators import KnownFailure, knownfailureif
|
|
42
|
|
|
42
|
|
|
43
|
pjoin = path.join
|
|
43
|
pjoin = path.join
|
|
44
|
|
|
44
|
|
|
45
|
#-----------------------------------------------------------------------------
|
|
45
|
#-----------------------------------------------------------------------------
|
|
46
|
# Warnings control
|
|
46
|
# Warnings control
|
|
47
|
#-----------------------------------------------------------------------------
|
|
47
|
#-----------------------------------------------------------------------------
|
|
48
|
|
|
48
|
|
|
49
|
# Twisted generates annoying warnings with Python 2.6, as will do other code
|
|
49
|
# Twisted generates annoying warnings with Python 2.6, as will do other code
|
|
50
|
# that imports 'sets' as of today
|
|
50
|
# that imports 'sets' as of today
|
|
51
|
warnings.filterwarnings('ignore', 'the sets module is deprecated',
|
|
51
|
warnings.filterwarnings('ignore', 'the sets module is deprecated',
|
|
52
|
DeprecationWarning )
|
|
52
|
DeprecationWarning )
|
|
53
|
|
|
53
|
|
|
54
|
# This one also comes from Twisted
|
|
54
|
# This one also comes from Twisted
|
|
55
|
warnings.filterwarnings('ignore', 'the sha module is deprecated',
|
|
55
|
warnings.filterwarnings('ignore', 'the sha module is deprecated',
|
|
56
|
DeprecationWarning)
|
|
56
|
DeprecationWarning)
|
|
57
|
|
|
57
|
|
|
58
|
# Wx on Fedora11 spits these out
|
|
58
|
# Wx on Fedora11 spits these out
|
|
59
|
warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
|
|
59
|
warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
|
|
60
|
UserWarning)
|
|
60
|
UserWarning)
|
|
61
|
|
|
61
|
|
|
62
|
# ------------------------------------------------------------------------------
|
|
62
|
# ------------------------------------------------------------------------------
|
|
63
|
# Monkeypatch Xunit to count known failures as skipped.
|
|
63
|
# Monkeypatch Xunit to count known failures as skipped.
|
|
64
|
# ------------------------------------------------------------------------------
|
|
64
|
# ------------------------------------------------------------------------------
|
|
65
|
def monkeypatch_xunit():
|
|
65
|
def monkeypatch_xunit():
|
|
66
|
try:
|
|
66
|
try:
|
|
67
|
knownfailureif(True)(lambda: None)()
|
|
67
|
knownfailureif(True)(lambda: None)()
|
|
68
|
except Exception as e:
|
|
68
|
except Exception as e:
|
|
69
|
KnownFailureTest = type(e)
|
|
69
|
KnownFailureTest = type(e)
|
|
70
|
|
|
70
|
|
|
71
|
def addError(self, test, err, capt=None):
|
|
71
|
def addError(self, test, err, capt=None):
|
|
72
|
if issubclass(err[0], KnownFailureTest):
|
|
72
|
if issubclass(err[0], KnownFailureTest):
|
|
73
|
err = (SkipTest,) + err[1:]
|
|
73
|
err = (SkipTest,) + err[1:]
|
|
74
|
return self.orig_addError(test, err, capt)
|
|
74
|
return self.orig_addError(test, err, capt)
|
|
75
|
|
|
75
|
|
|
76
|
Xunit.orig_addError = Xunit.addError
|
|
76
|
Xunit.orig_addError = Xunit.addError
|
|
77
|
Xunit.addError = addError
|
|
77
|
Xunit.addError = addError
|
|
78
|
|
|
78
|
|
|
79
|
#-----------------------------------------------------------------------------
|
|
79
|
#-----------------------------------------------------------------------------
|
|
80
|
# Check which dependencies are installed and greater than minimum version.
|
|
80
|
# Check which dependencies are installed and greater than minimum version.
|
|
81
|
#-----------------------------------------------------------------------------
|
|
81
|
#-----------------------------------------------------------------------------
|
|
82
|
def extract_version(mod):
|
|
82
|
def extract_version(mod):
|
|
83
|
return mod.__version__
|
|
83
|
return mod.__version__
|
|
84
|
|
|
84
|
|
|
85
|
def test_for(item, min_version=None, callback=extract_version):
|
|
85
|
def test_for(item, min_version=None, callback=extract_version):
|
|
86
|
"""Test to see if item is importable, and optionally check against a minimum
|
|
86
|
"""Test to see if item is importable, and optionally check against a minimum
|
|
87
|
version.
|
|
87
|
version.
|
|
88
|
|
|
88
|
|
|
89
|
If min_version is given, the default behavior is to check against the
|
|
89
|
If min_version is given, the default behavior is to check against the
|
|
90
|
`__version__` attribute of the item, but specifying `callback` allows you to
|
|
90
|
`__version__` attribute of the item, but specifying `callback` allows you to
|
|
91
|
extract the value you are interested in. e.g::
|
|
91
|
extract the value you are interested in. e.g::
|
|
92
|
|
|
92
|
|
|
93
|
In [1]: import sys
|
|
93
|
In [1]: import sys
|
|
94
|
|
|
94
|
|
|
95
|
In [2]: from IPython.testing.iptest import test_for
|
|
95
|
In [2]: from IPython.testing.iptest import test_for
|
|
96
|
|
|
96
|
|
|
97
|
In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
|
|
97
|
In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
|
|
98
|
Out[3]: True
|
|
98
|
Out[3]: True
|
|
99
|
|
|
99
|
|
|
100
|
"""
|
|
100
|
"""
|
|
101
|
try:
|
|
101
|
try:
|
|
102
|
check = import_item(item)
|
|
102
|
check = import_item(item)
|
|
103
|
except (ImportError, RuntimeError):
|
|
103
|
except (ImportError, RuntimeError):
|
|
104
|
# GTK reports Runtime error if it can't be initialized even if it's
|
|
104
|
# GTK reports Runtime error if it can't be initialized even if it's
|
|
105
|
# importable.
|
|
105
|
# importable.
|
|
106
|
return False
|
|
106
|
return False
|
|
107
|
else:
|
|
107
|
else:
|
|
108
|
if min_version:
|
|
108
|
if min_version:
|
|
109
|
if callback:
|
|
109
|
if callback:
|
|
110
|
# extra processing step to get version to compare
|
|
110
|
# extra processing step to get version to compare
|
|
111
|
check = callback(check)
|
|
111
|
check = callback(check)
|
|
112
|
|
|
112
|
|
|
113
|
return check >= min_version
|
|
113
|
return check >= min_version
|
|
114
|
else:
|
|
114
|
else:
|
|
115
|
return True
|
|
115
|
return True
|
|
116
|
|
|
116
|
|
|
117
|
# Global dict where we can store information on what we have and what we don't
|
|
117
|
# Global dict where we can store information on what we have and what we don't
|
|
118
|
# have available at test run time
|
|
118
|
# have available at test run time
|
|
119
|
have = {}
|
|
119
|
have = {}
|
|
120
|
|
|
120
|
|
|
121
|
have['curses'] = test_for('_curses')
|
|
121
|
have['curses'] = test_for('_curses')
|
|
122
|
have['matplotlib'] = test_for('matplotlib')
|
|
122
|
have['matplotlib'] = test_for('matplotlib')
|
|
123
|
have['numpy'] = test_for('numpy')
|
|
123
|
have['numpy'] = test_for('numpy')
|
|
124
|
have['pexpect'] = test_for('pexpect')
|
|
124
|
have['pexpect'] = test_for('pexpect')
|
|
125
|
have['pymongo'] = test_for('pymongo')
|
|
125
|
have['pymongo'] = test_for('pymongo')
|
|
126
|
have['pygments'] = test_for('pygments')
|
|
126
|
have['pygments'] = test_for('pygments')
|
|
127
|
have['sqlite3'] = test_for('sqlite3')
|
|
127
|
have['sqlite3'] = test_for('sqlite3')
|
|
128
|
have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
|
|
128
|
have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
|
|
129
|
have['jinja2'] = test_for('jinja2')
|
|
129
|
have['jinja2'] = test_for('jinja2')
|
|
130
|
have['mistune'] = test_for('mistune')
|
|
130
|
have['mistune'] = test_for('mistune')
|
|
131
|
have['requests'] = test_for('requests')
|
|
131
|
have['requests'] = test_for('requests')
|
|
132
|
have['sphinx'] = test_for('sphinx')
|
|
132
|
have['sphinx'] = test_for('sphinx')
|
|
133
|
have['jsonschema'] = test_for('jsonschema')
|
|
133
|
have['jsonschema'] = test_for('jsonschema')
|
|
134
|
have['terminado'] = test_for('terminado')
|
|
134
|
have['terminado'] = test_for('terminado')
|
|
135
|
have['casperjs'] = is_cmd_found('casperjs')
|
|
135
|
have['casperjs'] = is_cmd_found('casperjs')
|
|
136
|
have['phantomjs'] = is_cmd_found('phantomjs')
|
|
136
|
have['phantomjs'] = is_cmd_found('phantomjs')
|
|
137
|
have['slimerjs'] = is_cmd_found('slimerjs')
|
|
137
|
have['slimerjs'] = is_cmd_found('slimerjs')
|
|
138
|
|
|
138
|
|
|
139
|
min_zmq = (13,)
|
|
139
|
min_zmq = (13,)
|
|
140
|
|
|
140
|
|
|
141
|
have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
|
|
141
|
have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
|
|
142
|
|
|
142
|
|
|
143
|
#-----------------------------------------------------------------------------
|
|
143
|
#-----------------------------------------------------------------------------
|
|
144
|
# Test suite definitions
|
|
144
|
# Test suite definitions
|
|
145
|
#-----------------------------------------------------------------------------
|
|
145
|
#-----------------------------------------------------------------------------
|
|
146
|
|
|
146
|
|
|
147
|
test_group_names = ['core',
|
|
147
|
test_group_names = ['core',
|
|
148
|
'extensions', 'lib', 'terminal', 'testing', 'utils',
|
|
148
|
'extensions', 'lib', 'terminal', 'testing', 'utils',
|
|
149
|
'html', 'nbconvert'
|
|
149
|
'html',
|
|
150
|
]
|
|
150
|
]
|
|
151
|
|
|
151
|
|
|
152
|
class TestSection(object):
|
|
152
|
class TestSection(object):
|
|
153
|
def __init__(self, name, includes):
|
|
153
|
def __init__(self, name, includes):
|
|
154
|
self.name = name
|
|
154
|
self.name = name
|
|
155
|
self.includes = includes
|
|
155
|
self.includes = includes
|
|
156
|
self.excludes = []
|
|
156
|
self.excludes = []
|
|
157
|
self.dependencies = []
|
|
157
|
self.dependencies = []
|
|
158
|
self.enabled = True
|
|
158
|
self.enabled = True
|
|
159
|
|
|
159
|
|
|
160
|
def exclude(self, module):
|
|
160
|
def exclude(self, module):
|
|
161
|
if not module.startswith('IPython'):
|
|
161
|
if not module.startswith('IPython'):
|
|
162
|
module = self.includes[0] + "." + module
|
|
162
|
module = self.includes[0] + "." + module
|
|
163
|
self.excludes.append(module.replace('.', os.sep))
|
|
163
|
self.excludes.append(module.replace('.', os.sep))
|
|
164
|
|
|
164
|
|
|
165
|
def requires(self, *packages):
|
|
165
|
def requires(self, *packages):
|
|
166
|
self.dependencies.extend(packages)
|
|
166
|
self.dependencies.extend(packages)
|
|
167
|
|
|
167
|
|
|
168
|
@property
|
|
168
|
@property
|
|
169
|
def will_run(self):
|
|
169
|
def will_run(self):
|
|
170
|
return self.enabled and all(have[p] for p in self.dependencies)
|
|
170
|
return self.enabled and all(have[p] for p in self.dependencies)
|
|
171
|
|
|
171
|
|
|
172
|
shims = {
|
|
172
|
shims = {
|
|
173
|
'html': 'jupyter_notebook',
|
|
173
|
'html': 'jupyter_notebook',
|
|
174
|
}
|
|
174
|
}
|
|
175
|
|
|
175
|
|
|
176
|
# Name -> (include, exclude, dependencies_met)
|
|
176
|
# Name -> (include, exclude, dependencies_met)
|
|
177
|
test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
|
|
177
|
test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
|
|
178
|
|
|
178
|
|
|
179
|
|
|
179
|
|
|
180
|
# Exclusions and dependencies
|
|
180
|
# Exclusions and dependencies
|
|
181
|
# ---------------------------
|
|
181
|
# ---------------------------
|
|
182
|
|
|
182
|
|
|
183
|
# core:
|
|
183
|
# core:
|
|
184
|
sec = test_sections['core']
|
|
184
|
sec = test_sections['core']
|
|
185
|
if not have['sqlite3']:
|
|
185
|
if not have['sqlite3']:
|
|
186
|
sec.exclude('tests.test_history')
|
|
186
|
sec.exclude('tests.test_history')
|
|
187
|
sec.exclude('history')
|
|
187
|
sec.exclude('history')
|
|
188
|
if not have['matplotlib']:
|
|
188
|
if not have['matplotlib']:
|
|
189
|
sec.exclude('pylabtools'),
|
|
189
|
sec.exclude('pylabtools'),
|
|
190
|
sec.exclude('tests.test_pylabtools')
|
|
190
|
sec.exclude('tests.test_pylabtools')
|
|
191
|
|
|
191
|
|
|
192
|
# lib:
|
|
192
|
# lib:
|
|
193
|
sec = test_sections['lib']
|
|
193
|
sec = test_sections['lib']
|
|
194
|
if not have['zmq']:
|
|
194
|
if not have['zmq']:
|
|
195
|
sec.exclude('kernel')
|
|
195
|
sec.exclude('kernel')
|
|
196
|
if not have['pygments']:
|
|
196
|
if not have['pygments']:
|
|
197
|
sec.exclude('tests.test_lexers')
|
|
197
|
sec.exclude('tests.test_lexers')
|
|
198
|
# We do this unconditionally, so that the test suite doesn't import
|
|
198
|
# We do this unconditionally, so that the test suite doesn't import
|
|
199
|
# gtk, changing the default encoding and masking some unicode bugs.
|
|
199
|
# gtk, changing the default encoding and masking some unicode bugs.
|
|
200
|
sec.exclude('inputhookgtk')
|
|
200
|
sec.exclude('inputhookgtk')
|
|
201
|
# We also do this unconditionally, because wx can interfere with Unix signals.
|
|
201
|
# We also do this unconditionally, because wx can interfere with Unix signals.
|
|
202
|
# There are currently no tests for it anyway.
|
|
202
|
# There are currently no tests for it anyway.
|
|
203
|
sec.exclude('inputhookwx')
|
|
203
|
sec.exclude('inputhookwx')
|
|
204
|
# Testing inputhook will need a lot of thought, to figure out
|
|
204
|
# Testing inputhook will need a lot of thought, to figure out
|
|
205
|
# how to have tests that don't lock up with the gui event
|
|
205
|
# how to have tests that don't lock up with the gui event
|
|
206
|
# loops in the picture
|
|
206
|
# loops in the picture
|
|
207
|
sec.exclude('inputhook')
|
|
207
|
sec.exclude('inputhook')
|
|
208
|
|
|
208
|
|
|
209
|
# testing:
|
|
209
|
# testing:
|
|
210
|
sec = test_sections['testing']
|
|
210
|
sec = test_sections['testing']
|
|
211
|
# These have to be skipped on win32 because they use echo, rm, cd, etc.
|
|
211
|
# These have to be skipped on win32 because they use echo, rm, cd, etc.
|
|
212
|
# See ticket https://github.com/ipython/ipython/issues/87
|
|
212
|
# See ticket https://github.com/ipython/ipython/issues/87
|
|
213
|
if sys.platform == 'win32':
|
|
213
|
if sys.platform == 'win32':
|
|
214
|
sec.exclude('plugin.test_exampleip')
|
|
214
|
sec.exclude('plugin.test_exampleip')
|
|
215
|
sec.exclude('plugin.dtexample')
|
|
215
|
sec.exclude('plugin.dtexample')
|
|
216
|
|
|
216
|
|
|
217
|
# don't run jupyter_console tests found via shim
|
|
217
|
# don't run jupyter_console tests found via shim
|
|
218
|
test_sections['terminal'].exclude('console')
|
|
218
|
test_sections['terminal'].exclude('console')
|
|
219
|
|
|
219
|
|
|
220
|
# extensions:
|
|
220
|
# extensions:
|
|
221
|
sec = test_sections['extensions']
|
|
221
|
sec = test_sections['extensions']
|
|
222
|
# This is deprecated in favour of rpy2
|
|
222
|
# This is deprecated in favour of rpy2
|
|
223
|
sec.exclude('rmagic')
|
|
223
|
sec.exclude('rmagic')
|
|
224
|
# autoreload does some strange stuff, so move it to its own test section
|
|
224
|
# autoreload does some strange stuff, so move it to its own test section
|
|
225
|
sec.exclude('autoreload')
|
|
225
|
sec.exclude('autoreload')
|
|
226
|
sec.exclude('tests.test_autoreload')
|
|
226
|
sec.exclude('tests.test_autoreload')
|
|
227
|
test_sections['autoreload'] = TestSection('autoreload',
|
|
227
|
test_sections['autoreload'] = TestSection('autoreload',
|
|
228
|
['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
|
|
228
|
['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
|
|
229
|
test_group_names.append('autoreload')
|
|
229
|
test_group_names.append('autoreload')
|
|
230
|
|
|
230
|
|
|
231
|
# html:
|
|
231
|
# html:
|
|
232
|
sec = test_sections['html']
|
|
232
|
sec = test_sections['html']
|
|
233
|
sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
|
|
233
|
sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
|
|
234
|
# The notebook 'static' directory contains JS, css and other
|
|
234
|
# The notebook 'static' directory contains JS, css and other
|
|
235
|
# files for web serving. Occasionally projects may put a .py
|
|
235
|
# files for web serving. Occasionally projects may put a .py
|
|
236
|
# file in there (MathJax ships a conf.py), so we might as
|
|
236
|
# file in there (MathJax ships a conf.py), so we might as
|
|
237
|
# well play it safe and skip the whole thing.
|
|
237
|
# well play it safe and skip the whole thing.
|
|
238
|
sec.exclude('static')
|
|
238
|
sec.exclude('static')
|
|
239
|
sec.exclude('tasks')
|
|
239
|
sec.exclude('tasks')
|
|
240
|
if not have['jinja2']:
|
|
240
|
if not have['jinja2']:
|
|
241
|
sec.exclude('notebookapp')
|
|
241
|
sec.exclude('notebookapp')
|
|
242
|
if not have['pygments'] or not have['jinja2']:
|
|
|
|
|
243
|
sec.exclude('nbconvert')
|
|
|
|
|
244
|
if not have['terminado']:
|
|
242
|
if not have['terminado']:
|
|
245
|
sec.exclude('terminal')
|
|
243
|
sec.exclude('terminal')
|
|
246
|
|
|
244
|
|
|
247
|
# nbconvert:
|
|
|
|
|
248
|
sec = test_sections['nbconvert']
|
|
|
|
|
249
|
sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
|
|
|
|
|
250
|
# Exclude nbconvert directories containing config files used to test.
|
|
|
|
|
251
|
# Executing the config files with iptest would cause an exception.
|
|
|
|
|
252
|
sec.exclude('tests.files')
|
|
|
|
|
253
|
sec.exclude('exporters.tests.files')
|
|
|
|
|
254
|
if not have['tornado']:
|
|
|
|
|
255
|
sec.exclude('nbconvert.post_processors.serve')
|
|
|
|
|
256
|
sec.exclude('nbconvert.post_processors.tests.test_serve')
|
|
|
|
|
257
|
|
|
|
|
|
258
|
|
|
245
|
|
|
259
|
#-----------------------------------------------------------------------------
|
|
246
|
#-----------------------------------------------------------------------------
|
|
260
|
# Functions and classes
|
|
247
|
# Functions and classes
|
|
261
|
#-----------------------------------------------------------------------------
|
|
248
|
#-----------------------------------------------------------------------------
|
|
262
|
|
|
249
|
|
|
263
|
def check_exclusions_exist():
|
|
250
|
def check_exclusions_exist():
|
|
264
|
from IPython.utils.path import get_ipython_package_dir
|
|
251
|
from IPython.utils.path import get_ipython_package_dir
|
|
265
|
from IPython.utils.warn import warn
|
|
252
|
from IPython.utils.warn import warn
|
|
266
|
parent = os.path.dirname(get_ipython_package_dir())
|
|
253
|
parent = os.path.dirname(get_ipython_package_dir())
|
|
267
|
for sec in test_sections:
|
|
254
|
for sec in test_sections:
|
|
268
|
for pattern in sec.exclusions:
|
|
255
|
for pattern in sec.exclusions:
|
|
269
|
fullpath = pjoin(parent, pattern)
|
|
256
|
fullpath = pjoin(parent, pattern)
|
|
270
|
if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
|
|
257
|
if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
|
|
271
|
warn("Excluding nonexistent file: %r" % pattern)
|
|
258
|
warn("Excluding nonexistent file: %r" % pattern)
|
|
272
|
|
|
259
|
|
|
273
|
|
|
260
|
|
|
274
|
class ExclusionPlugin(Plugin):
|
|
261
|
class ExclusionPlugin(Plugin):
|
|
275
|
"""A nose plugin to effect our exclusions of files and directories.
|
|
262
|
"""A nose plugin to effect our exclusions of files and directories.
|
|
276
|
"""
|
|
263
|
"""
|
|
277
|
name = 'exclusions'
|
|
264
|
name = 'exclusions'
|
|
278
|
score = 3000 # Should come before any other plugins
|
|
265
|
score = 3000 # Should come before any other plugins
|
|
279
|
|
|
266
|
|
|
280
|
def __init__(self, exclude_patterns=None):
|
|
267
|
def __init__(self, exclude_patterns=None):
|
|
281
|
"""
|
|
268
|
"""
|
|
282
|
Parameters
|
|
269
|
Parameters
|
|
283
|
----------
|
|
270
|
----------
|
|
284
|
|
|
271
|
|
|
285
|
exclude_patterns : sequence of strings, optional
|
|
272
|
exclude_patterns : sequence of strings, optional
|
|
286
|
Filenames containing these patterns (as raw strings, not as regular
|
|
273
|
Filenames containing these patterns (as raw strings, not as regular
|
|
287
|
expressions) are excluded from the tests.
|
|
274
|
expressions) are excluded from the tests.
|
|
288
|
"""
|
|
275
|
"""
|
|
289
|
self.exclude_patterns = exclude_patterns or []
|
|
276
|
self.exclude_patterns = exclude_patterns or []
|
|
290
|
super(ExclusionPlugin, self).__init__()
|
|
277
|
super(ExclusionPlugin, self).__init__()
|
|
291
|
|
|
278
|
|
|
292
|
def options(self, parser, env=os.environ):
|
|
279
|
def options(self, parser, env=os.environ):
|
|
293
|
Plugin.options(self, parser, env)
|
|
280
|
Plugin.options(self, parser, env)
|
|
294
|
|
|
281
|
|
|
295
|
def configure(self, options, config):
|
|
282
|
def configure(self, options, config):
|
|
296
|
Plugin.configure(self, options, config)
|
|
283
|
Plugin.configure(self, options, config)
|
|
297
|
# Override nose trying to disable plugin.
|
|
284
|
# Override nose trying to disable plugin.
|
|
298
|
self.enabled = True
|
|
285
|
self.enabled = True
|
|
299
|
|
|
286
|
|
|
300
|
def wantFile(self, filename):
|
|
287
|
def wantFile(self, filename):
|
|
301
|
"""Return whether the given filename should be scanned for tests.
|
|
288
|
"""Return whether the given filename should be scanned for tests.
|
|
302
|
"""
|
|
289
|
"""
|
|
303
|
if any(pat in filename for pat in self.exclude_patterns):
|
|
290
|
if any(pat in filename for pat in self.exclude_patterns):
|
|
304
|
return False
|
|
291
|
return False
|
|
305
|
return None
|
|
292
|
return None
|
|
306
|
|
|
293
|
|
|
307
|
def wantDirectory(self, directory):
|
|
294
|
def wantDirectory(self, directory):
|
|
308
|
"""Return whether the given directory should be scanned for tests.
|
|
295
|
"""Return whether the given directory should be scanned for tests.
|
|
309
|
"""
|
|
296
|
"""
|
|
310
|
if any(pat in directory for pat in self.exclude_patterns):
|
|
297
|
if any(pat in directory for pat in self.exclude_patterns):
|
|
311
|
return False
|
|
298
|
return False
|
|
312
|
return None
|
|
299
|
return None
|
|
313
|
|
|
300
|
|
|
314
|
|
|
301
|
|
|
315
|
class StreamCapturer(Thread):
|
|
302
|
class StreamCapturer(Thread):
|
|
316
|
daemon = True # Don't hang if main thread crashes
|
|
303
|
daemon = True # Don't hang if main thread crashes
|
|
317
|
started = False
|
|
304
|
started = False
|
|
318
|
def __init__(self, echo=False):
|
|
305
|
def __init__(self, echo=False):
|
|
319
|
super(StreamCapturer, self).__init__()
|
|
306
|
super(StreamCapturer, self).__init__()
|
|
320
|
self.echo = echo
|
|
307
|
self.echo = echo
|
|
321
|
self.streams = []
|
|
308
|
self.streams = []
|
|
322
|
self.buffer = BytesIO()
|
|
309
|
self.buffer = BytesIO()
|
|
323
|
self.readfd, self.writefd = os.pipe()
|
|
310
|
self.readfd, self.writefd = os.pipe()
|
|
324
|
self.buffer_lock = Lock()
|
|
311
|
self.buffer_lock = Lock()
|
|
325
|
self.stop = Event()
|
|
312
|
self.stop = Event()
|
|
326
|
|
|
313
|
|
|
327
|
def run(self):
|
|
314
|
def run(self):
|
|
328
|
self.started = True
|
|
315
|
self.started = True
|
|
329
|
|
|
316
|
|
|
330
|
while not self.stop.is_set():
|
|
317
|
while not self.stop.is_set():
|
|
331
|
chunk = os.read(self.readfd, 1024)
|
|
318
|
chunk = os.read(self.readfd, 1024)
|
|
332
|
|
|
319
|
|
|
333
|
with self.buffer_lock:
|
|
320
|
with self.buffer_lock:
|
|
334
|
self.buffer.write(chunk)
|
|
321
|
self.buffer.write(chunk)
|
|
335
|
if self.echo:
|
|
322
|
if self.echo:
|
|
336
|
sys.stdout.write(bytes_to_str(chunk))
|
|
323
|
sys.stdout.write(bytes_to_str(chunk))
|
|
337
|
|
|
324
|
|
|
338
|
os.close(self.readfd)
|
|
325
|
os.close(self.readfd)
|
|
339
|
os.close(self.writefd)
|
|
326
|
os.close(self.writefd)
|
|
340
|
|
|
327
|
|
|
341
|
def reset_buffer(self):
|
|
328
|
def reset_buffer(self):
|
|
342
|
with self.buffer_lock:
|
|
329
|
with self.buffer_lock:
|
|
343
|
self.buffer.truncate(0)
|
|
330
|
self.buffer.truncate(0)
|
|
344
|
self.buffer.seek(0)
|
|
331
|
self.buffer.seek(0)
|
|
345
|
|
|
332
|
|
|
346
|
def get_buffer(self):
|
|
333
|
def get_buffer(self):
|
|
347
|
with self.buffer_lock:
|
|
334
|
with self.buffer_lock:
|
|
348
|
return self.buffer.getvalue()
|
|
335
|
return self.buffer.getvalue()
|
|
349
|
|
|
336
|
|
|
350
|
def ensure_started(self):
|
|
337
|
def ensure_started(self):
|
|
351
|
if not self.started:
|
|
338
|
if not self.started:
|
|
352
|
self.start()
|
|
339
|
self.start()
|
|
353
|
|
|
340
|
|
|
354
|
def halt(self):
|
|
341
|
def halt(self):
|
|
355
|
"""Safely stop the thread."""
|
|
342
|
"""Safely stop the thread."""
|
|
356
|
if not self.started:
|
|
343
|
if not self.started:
|
|
357
|
return
|
|
344
|
return
|
|
358
|
|
|
345
|
|
|
359
|
self.stop.set()
|
|
346
|
self.stop.set()
|
|
360
|
os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
|
|
347
|
os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
|
|
361
|
self.join()
|
|
348
|
self.join()
|
|
362
|
|
|
349
|
|
|
363
|
class SubprocessStreamCapturePlugin(Plugin):
|
|
350
|
class SubprocessStreamCapturePlugin(Plugin):
|
|
364
|
name='subprocstreams'
|
|
351
|
name='subprocstreams'
|
|
365
|
def __init__(self):
|
|
352
|
def __init__(self):
|
|
366
|
Plugin.__init__(self)
|
|
353
|
Plugin.__init__(self)
|
|
367
|
self.stream_capturer = StreamCapturer()
|
|
354
|
self.stream_capturer = StreamCapturer()
|
|
368
|
self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
|
|
355
|
self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
|
|
369
|
# This is ugly, but distant parts of the test machinery need to be able
|
|
356
|
# This is ugly, but distant parts of the test machinery need to be able
|
|
370
|
# to redirect streams, so we make the object globally accessible.
|
|
357
|
# to redirect streams, so we make the object globally accessible.
|
|
371
|
nose.iptest_stdstreams_fileno = self.get_write_fileno
|
|
358
|
nose.iptest_stdstreams_fileno = self.get_write_fileno
|
|
372
|
|
|
359
|
|
|
373
|
def get_write_fileno(self):
|
|
360
|
def get_write_fileno(self):
|
|
374
|
if self.destination == 'capture':
|
|
361
|
if self.destination == 'capture':
|
|
375
|
self.stream_capturer.ensure_started()
|
|
362
|
self.stream_capturer.ensure_started()
|
|
376
|
return self.stream_capturer.writefd
|
|
363
|
return self.stream_capturer.writefd
|
|
377
|
elif self.destination == 'discard':
|
|
364
|
elif self.destination == 'discard':
|
|
378
|
return os.open(os.devnull, os.O_WRONLY)
|
|
365
|
return os.open(os.devnull, os.O_WRONLY)
|
|
379
|
else:
|
|
366
|
else:
|
|
380
|
return sys.__stdout__.fileno()
|
|
367
|
return sys.__stdout__.fileno()
|
|
381
|
|
|
368
|
|
|
382
|
def configure(self, options, config):
|
|
369
|
def configure(self, options, config):
|
|
383
|
Plugin.configure(self, options, config)
|
|
370
|
Plugin.configure(self, options, config)
|
|
384
|
# Override nose trying to disable plugin.
|
|
371
|
# Override nose trying to disable plugin.
|
|
385
|
if self.destination == 'capture':
|
|
372
|
if self.destination == 'capture':
|
|
386
|
self.enabled = True
|
|
373
|
self.enabled = True
|
|
387
|
|
|
374
|
|
|
388
|
def startTest(self, test):
|
|
375
|
def startTest(self, test):
|
|
389
|
# Reset log capture
|
|
376
|
# Reset log capture
|
|
390
|
self.stream_capturer.reset_buffer()
|
|
377
|
self.stream_capturer.reset_buffer()
|
|
391
|
|
|
378
|
|
|
392
|
def formatFailure(self, test, err):
|
|
379
|
def formatFailure(self, test, err):
|
|
393
|
# Show output
|
|
380
|
# Show output
|
|
394
|
ec, ev, tb = err
|
|
381
|
ec, ev, tb = err
|
|
395
|
captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
|
|
382
|
captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
|
|
396
|
if captured.strip():
|
|
383
|
if captured.strip():
|
|
397
|
ev = safe_str(ev)
|
|
384
|
ev = safe_str(ev)
|
|
398
|
out = [ev, '>> begin captured subprocess output <<',
|
|
385
|
out = [ev, '>> begin captured subprocess output <<',
|
|
399
|
captured,
|
|
386
|
captured,
|
|
400
|
'>> end captured subprocess output <<']
|
|
387
|
'>> end captured subprocess output <<']
|
|
401
|
return ec, '\n'.join(out), tb
|
|
388
|
return ec, '\n'.join(out), tb
|
|
402
|
|
|
389
|
|
|
403
|
return err
|
|
390
|
return err
|
|
404
|
|
|
391
|
|
|
405
|
formatError = formatFailure
|
|
392
|
formatError = formatFailure
|
|
406
|
|
|
393
|
|
|
407
|
def finalize(self, result):
|
|
394
|
def finalize(self, result):
|
|
408
|
self.stream_capturer.halt()
|
|
395
|
self.stream_capturer.halt()
|
|
409
|
|
|
396
|
|
|
410
|
|
|
397
|
|
|
411
|
def run_iptest():
|
|
398
|
def run_iptest():
|
|
412
|
"""Run the IPython test suite using nose.
|
|
399
|
"""Run the IPython test suite using nose.
|
|
413
|
|
|
400
|
|
|
414
|
This function is called when this script is **not** called with the form
|
|
401
|
This function is called when this script is **not** called with the form
|
|
415
|
`iptest all`. It simply calls nose with appropriate command line flags
|
|
402
|
`iptest all`. It simply calls nose with appropriate command line flags
|
|
416
|
and accepts all of the standard nose arguments.
|
|
403
|
and accepts all of the standard nose arguments.
|
|
417
|
"""
|
|
404
|
"""
|
|
418
|
# Apply our monkeypatch to Xunit
|
|
405
|
# Apply our monkeypatch to Xunit
|
|
419
|
if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
|
|
406
|
if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
|
|
420
|
monkeypatch_xunit()
|
|
407
|
monkeypatch_xunit()
|
|
421
|
|
|
408
|
|
|
422
|
warnings.filterwarnings('ignore',
|
|
409
|
warnings.filterwarnings('ignore',
|
|
423
|
'This will be removed soon. Use IPython.testing.util instead')
|
|
410
|
'This will be removed soon. Use IPython.testing.util instead')
|
|
424
|
|
|
411
|
|
|
425
|
arg1 = sys.argv[1]
|
|
412
|
arg1 = sys.argv[1]
|
|
426
|
if arg1 in test_sections:
|
|
413
|
if arg1 in test_sections:
|
|
427
|
section = test_sections[arg1]
|
|
414
|
section = test_sections[arg1]
|
|
428
|
sys.argv[1:2] = section.includes
|
|
415
|
sys.argv[1:2] = section.includes
|
|
429
|
elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
|
|
416
|
elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
|
|
430
|
section = test_sections[arg1[8:]]
|
|
417
|
section = test_sections[arg1[8:]]
|
|
431
|
sys.argv[1:2] = section.includes
|
|
418
|
sys.argv[1:2] = section.includes
|
|
432
|
else:
|
|
419
|
else:
|
|
433
|
section = TestSection(arg1, includes=[arg1])
|
|
420
|
section = TestSection(arg1, includes=[arg1])
|
|
434
|
|
|
421
|
|
|
435
|
|
|
422
|
|
|
436
|
argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
|
|
423
|
argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
|
|
437
|
# We add --exe because of setuptools' imbecility (it
|
|
424
|
# We add --exe because of setuptools' imbecility (it
|
|
438
|
# blindly does chmod +x on ALL files). Nose does the
|
|
425
|
# blindly does chmod +x on ALL files). Nose does the
|
|
439
|
# right thing and it tries to avoid executables,
|
|
426
|
# right thing and it tries to avoid executables,
|
|
440
|
# setuptools unfortunately forces our hand here. This
|
|
427
|
# setuptools unfortunately forces our hand here. This
|
|
441
|
# has been discussed on the distutils list and the
|
|
428
|
# has been discussed on the distutils list and the
|
|
442
|
# setuptools devs refuse to fix this problem!
|
|
429
|
# setuptools devs refuse to fix this problem!
|
|
443
|
'--exe',
|
|
430
|
'--exe',
|
|
444
|
]
|
|
431
|
]
|
|
445
|
if '-a' not in argv and '-A' not in argv:
|
|
432
|
if '-a' not in argv and '-A' not in argv:
|
|
446
|
argv = argv + ['-a', '!crash']
|
|
433
|
argv = argv + ['-a', '!crash']
|
|
447
|
|
|
434
|
|
|
448
|
if nose.__version__ >= '0.11':
|
|
435
|
if nose.__version__ >= '0.11':
|
|
449
|
# I don't fully understand why we need this one, but depending on what
|
|
436
|
# I don't fully understand why we need this one, but depending on what
|
|
450
|
# directory the test suite is run from, if we don't give it, 0 tests
|
|
437
|
# directory the test suite is run from, if we don't give it, 0 tests
|
|
451
|
# get run. Specifically, if the test suite is run from the source dir
|
|
438
|
# get run. Specifically, if the test suite is run from the source dir
|
|
452
|
# with an argument (like 'iptest.py IPython.core', 0 tests are run,
|
|
439
|
# with an argument (like 'iptest.py IPython.core', 0 tests are run,
|
|
453
|
# even if the same call done in this directory works fine). It appears
|
|
440
|
# even if the same call done in this directory works fine). It appears
|
|
454
|
# that if the requested package is in the current dir, nose bails early
|
|
441
|
# that if the requested package is in the current dir, nose bails early
|
|
455
|
# by default. Since it's otherwise harmless, leave it in by default
|
|
442
|
# by default. Since it's otherwise harmless, leave it in by default
|
|
456
|
# for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
|
|
443
|
# for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
|
|
457
|
argv.append('--traverse-namespace')
|
|
444
|
argv.append('--traverse-namespace')
|
|
458
|
|
|
445
|
|
|
459
|
plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
|
|
446
|
plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
|
|
460
|
SubprocessStreamCapturePlugin() ]
|
|
447
|
SubprocessStreamCapturePlugin() ]
|
|
461
|
|
|
448
|
|
|
462
|
# we still have some vestigial doctests in core
|
|
449
|
# we still have some vestigial doctests in core
|
|
463
|
if (section.name.startswith(('core', 'IPython.core'))):
|
|
450
|
if (section.name.startswith(('core', 'IPython.core'))):
|
|
464
|
plugins.append(IPythonDoctest())
|
|
451
|
plugins.append(IPythonDoctest())
|
|
465
|
argv.extend([
|
|
452
|
argv.extend([
|
|
466
|
'--with-ipdoctest',
|
|
453
|
'--with-ipdoctest',
|
|
467
|
'--ipdoctest-tests',
|
|
454
|
'--ipdoctest-tests',
|
|
468
|
'--ipdoctest-extension=txt',
|
|
455
|
'--ipdoctest-extension=txt',
|
|
469
|
])
|
|
456
|
])
|
|
470
|
|
|
457
|
|
|
471
|
|
|
458
|
|
|
472
|
# Use working directory set by parent process (see iptestcontroller)
|
|
459
|
# Use working directory set by parent process (see iptestcontroller)
|
|
473
|
if 'IPTEST_WORKING_DIR' in os.environ:
|
|
460
|
if 'IPTEST_WORKING_DIR' in os.environ:
|
|
474
|
os.chdir(os.environ['IPTEST_WORKING_DIR'])
|
|
461
|
os.chdir(os.environ['IPTEST_WORKING_DIR'])
|
|
475
|
|
|
462
|
|
|
476
|
# We need a global ipython running in this process, but the special
|
|
463
|
# We need a global ipython running in this process, but the special
|
|
477
|
# in-process group spawns its own IPython kernels, so for *that* group we
|
|
464
|
# in-process group spawns its own IPython kernels, so for *that* group we
|
|
478
|
# must avoid also opening the global one (otherwise there's a conflict of
|
|
465
|
# must avoid also opening the global one (otherwise there's a conflict of
|
|
479
|
# singletons). Ultimately the solution to this problem is to refactor our
|
|
466
|
# singletons). Ultimately the solution to this problem is to refactor our
|
|
480
|
# assumptions about what needs to be a singleton and what doesn't (app
|
|
467
|
# assumptions about what needs to be a singleton and what doesn't (app
|
|
481
|
# objects should, individual shells shouldn't). But for now, this
|
|
468
|
# objects should, individual shells shouldn't). But for now, this
|
|
482
|
# workaround allows the test suite for the inprocess module to complete.
|
|
469
|
# workaround allows the test suite for the inprocess module to complete.
|
|
483
|
if 'kernel.inprocess' not in section.name:
|
|
470
|
if 'kernel.inprocess' not in section.name:
|
|
484
|
from IPython.testing import globalipapp
|
|
471
|
from IPython.testing import globalipapp
|
|
485
|
globalipapp.start_ipython()
|
|
472
|
globalipapp.start_ipython()
|
|
486
|
|
|
473
|
|
|
487
|
# Now nose can run
|
|
474
|
# Now nose can run
|
|
488
|
TestProgram(argv=argv, addplugins=plugins)
|
|
475
|
TestProgram(argv=argv, addplugins=plugins)
|
|
489
|
|
|
476
|
|
|
490
|
if __name__ == '__main__':
|
|
477
|
if __name__ == '__main__':
|
|
491
|
run_iptest()
|
|
478
|
run_iptest()
|
|
492
|
|
|
479
|
|