##// END OF EJS Templates
Merge pull request #12362 from Carreau/pytest-ref...
Matthias Bussonnier -
r25806:94b7db20 merge
parent child Browse files
Show More
@@ -1,181 +1,183 b''
1 # encoding: utf-8
2 """Tests for IPython.utils.path.py"""
1 """Tests for IPython.utils.path.py"""
3
4 # Copyright (c) IPython Development Team.
2 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
3 # Distributed under the terms of the Modified BSD License.
4
5 from contextlib import contextmanager
6 from unittest.mock import patch
6 from unittest.mock import patch
7
7 import nose.tools as nt
8 import nose.tools as nt
9 import pytest
8
10
9 from IPython.lib import latextools
11 from IPython.lib import latextools
10 from IPython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib
12 from IPython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib
11 from IPython.utils.process import FindCmdError
13 from IPython.utils.process import FindCmdError
12
14
13
15
14 def test_latex_to_png_dvipng_fails_when_no_cmd():
16 @pytest.mark.parametrize('command', ['latex', 'dvipng'])
15 """
17 def test_check_latex_to_png_dvipng_fails_when_no_cmd(command):
16 `latex_to_png_dvipng` should return None when there is no required command
17 """
18 for command in ['latex', 'dvipng']:
19 yield (check_latex_to_png_dvipng_fails_when_no_cmd, command)
20
21
22 def check_latex_to_png_dvipng_fails_when_no_cmd(command):
23 def mock_find_cmd(arg):
18 def mock_find_cmd(arg):
24 if arg == command:
19 if arg == command:
25 raise FindCmdError
20 raise FindCmdError
26
21
27 with patch.object(latextools, "find_cmd", mock_find_cmd):
22 with patch.object(latextools, "find_cmd", mock_find_cmd):
28 nt.assert_equal(latextools.latex_to_png_dvipng("whatever", True),
23 assert latextools.latex_to_png_dvipng("whatever", True) == None
29 None)
30
24
31
25
32 @onlyif_cmds_exist('latex', 'dvipng')
26 @onlyif_cmds_exist('latex', 'dvipng')
33 def test_latex_to_png_dvipng_runs():
27 def test_latex_to_png_dvipng_runs():
34 """
28 """
35 Test that latex_to_png_dvipng just runs without error.
29 Test that latex_to_png_dvipng just runs without error.
36 """
30 """
37 def mock_kpsewhich(filename):
31 def mock_kpsewhich(filename):
38 nt.assert_equal(filename, "breqn.sty")
32 assert filename == "breqn.sty"
39 return None
33 return None
40
34
41 for (s, wrap) in [(u"$$x^2$$", False), (u"x^2", True)]:
35 for (s, wrap) in [(u"$$x^2$$", False), (u"x^2", True)]:
42 yield (latextools.latex_to_png_dvipng, s, wrap)
36 yield (latextools.latex_to_png_dvipng, s, wrap)
43
37
44 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
38 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
45 yield (latextools.latex_to_png_dvipng, s, wrap)
39 yield (latextools.latex_to_png_dvipng, s, wrap)
46
40
47 @skipif_not_matplotlib
41
48 def test_latex_to_png_mpl_runs():
42 @contextmanager
49 """
43 def no_op(*args, **kwargs):
50 Test that latex_to_png_mpl just runs without error.
44 yield
51 """
45
52 def mock_kpsewhich(filename):
46 def mock_kpsewhich(filename):
53 nt.assert_equal(filename, "breqn.sty")
47 assert filename == "breqn.sty"
54 return None
48 return None
55
49
56 for (s, wrap) in [("$x^2$", False), ("x^2", True)]:
50 @contextmanager
57 yield (latextools.latex_to_png_mpl, s, wrap)
51 def patch_latextool():
52 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
53 yield
58
54
59 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
55 @pytest.mark.parametrize('context', [no_op, patch_latextool])
60 yield (latextools.latex_to_png_mpl, s, wrap)
56 @pytest.mark.parametrize('s_wrap', [("$x^2$", False), ("x^2", True)])
57 def test_latex_to_png_mpl_runs(s_wrap, context):
58 """
59 Test that latex_to_png_mpl just runs without error.
60 """
61 try:
62 import matplotbli
63 except ImportError:
64 pytest.skip("This needs matplotlib to be availlable")
65 return
66 s, wrap = s_wrap
67 with context():
68 latextools.latex_to_png_mpl(s, wrap)
61
69
62 @skipif_not_matplotlib
70 @skipif_not_matplotlib
63 def test_latex_to_html():
71 def test_latex_to_html():
64 img = latextools.latex_to_html("$x^2$")
72 img = latextools.latex_to_html("$x^2$")
65 nt.assert_in("data:image/png;base64,iVBOR", img)
73 assert "data:image/png;base64,iVBOR" in img
66
74
67
75
68 def test_genelatex_no_wrap():
76 def test_genelatex_no_wrap():
69 """
77 """
70 Test genelatex with wrap=False.
78 Test genelatex with wrap=False.
71 """
79 """
72 def mock_kpsewhich(filename):
80 def mock_kpsewhich(filename):
73 assert False, ("kpsewhich should not be called "
81 assert False, ("kpsewhich should not be called "
74 "(called with {0})".format(filename))
82 "(called with {0})".format(filename))
75
83
76 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
84 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
77 nt.assert_equal(
85 assert '\n'.join(latextools.genelatex("body text", False)) == r'''\documentclass{article}
78 '\n'.join(latextools.genelatex("body text", False)),
79 r'''\documentclass{article}
80 \usepackage{amsmath}
86 \usepackage{amsmath}
81 \usepackage{amsthm}
87 \usepackage{amsthm}
82 \usepackage{amssymb}
88 \usepackage{amssymb}
83 \usepackage{bm}
89 \usepackage{bm}
84 \pagestyle{empty}
90 \pagestyle{empty}
85 \begin{document}
91 \begin{document}
86 body text
92 body text
87 \end{document}''')
93 \end{document}'''
88
94
89
95
90 def test_genelatex_wrap_with_breqn():
96 def test_genelatex_wrap_with_breqn():
91 """
97 """
92 Test genelatex with wrap=True for the case breqn.sty is installed.
98 Test genelatex with wrap=True for the case breqn.sty is installed.
93 """
99 """
94 def mock_kpsewhich(filename):
100 def mock_kpsewhich(filename):
95 nt.assert_equal(filename, "breqn.sty")
101 assert filename == "breqn.sty"
96 return "path/to/breqn.sty"
102 return "path/to/breqn.sty"
97
103
98 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
104 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
99 nt.assert_equal(
105 assert '\n'.join(latextools.genelatex("x^2", True)) == r'''\documentclass{article}
100 '\n'.join(latextools.genelatex("x^2", True)),
101 r'''\documentclass{article}
102 \usepackage{amsmath}
106 \usepackage{amsmath}
103 \usepackage{amsthm}
107 \usepackage{amsthm}
104 \usepackage{amssymb}
108 \usepackage{amssymb}
105 \usepackage{bm}
109 \usepackage{bm}
106 \usepackage{breqn}
110 \usepackage{breqn}
107 \pagestyle{empty}
111 \pagestyle{empty}
108 \begin{document}
112 \begin{document}
109 \begin{dmath*}
113 \begin{dmath*}
110 x^2
114 x^2
111 \end{dmath*}
115 \end{dmath*}
112 \end{document}''')
116 \end{document}'''
113
117
114
118
115 def test_genelatex_wrap_without_breqn():
119 def test_genelatex_wrap_without_breqn():
116 """
120 """
117 Test genelatex with wrap=True for the case breqn.sty is not installed.
121 Test genelatex with wrap=True for the case breqn.sty is not installed.
118 """
122 """
119 def mock_kpsewhich(filename):
123 def mock_kpsewhich(filename):
120 nt.assert_equal(filename, "breqn.sty")
124 assert filename == "breqn.sty"
121 return None
125 return None
122
126
123 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
127 with patch.object(latextools, "kpsewhich", mock_kpsewhich):
124 nt.assert_equal(
128 assert '\n'.join(latextools.genelatex("x^2", True)) == r'''\documentclass{article}
125 '\n'.join(latextools.genelatex("x^2", True)),
126 r'''\documentclass{article}
127 \usepackage{amsmath}
129 \usepackage{amsmath}
128 \usepackage{amsthm}
130 \usepackage{amsthm}
129 \usepackage{amssymb}
131 \usepackage{amssymb}
130 \usepackage{bm}
132 \usepackage{bm}
131 \pagestyle{empty}
133 \pagestyle{empty}
132 \begin{document}
134 \begin{document}
133 $$x^2$$
135 $$x^2$$
134 \end{document}''')
136 \end{document}'''
135
137
136
138
137 @skipif_not_matplotlib
139 @skipif_not_matplotlib
138 @onlyif_cmds_exist('latex', 'dvipng')
140 @onlyif_cmds_exist('latex', 'dvipng')
139 def test_latex_to_png_color():
141 def test_latex_to_png_color():
140 """
142 """
141 Test color settings for latex_to_png.
143 Test color settings for latex_to_png.
142 """
144 """
143 latex_string = "$x^2$"
145 latex_string = "$x^2$"
144 default_value = latextools.latex_to_png(latex_string, wrap=False)
146 default_value = latextools.latex_to_png(latex_string, wrap=False)
145 default_hexblack = latextools.latex_to_png(latex_string, wrap=False,
147 default_hexblack = latextools.latex_to_png(latex_string, wrap=False,
146 color='#000000')
148 color='#000000')
147 dvipng_default = latextools.latex_to_png_dvipng(latex_string, False)
149 dvipng_default = latextools.latex_to_png_dvipng(latex_string, False)
148 dvipng_black = latextools.latex_to_png_dvipng(latex_string, False, 'Black')
150 dvipng_black = latextools.latex_to_png_dvipng(latex_string, False, 'Black')
149 nt.assert_equal(dvipng_default, dvipng_black)
151 assert dvipng_default == dvipng_black
150 mpl_default = latextools.latex_to_png_mpl(latex_string, False)
152 mpl_default = latextools.latex_to_png_mpl(latex_string, False)
151 mpl_black = latextools.latex_to_png_mpl(latex_string, False, 'Black')
153 mpl_black = latextools.latex_to_png_mpl(latex_string, False, 'Black')
152 nt.assert_equal(mpl_default, mpl_black)
154 assert mpl_default == mpl_black
153 nt.assert_in(default_value, [dvipng_black, mpl_black])
155 assert default_value in [dvipng_black, mpl_black]
154 nt.assert_in(default_hexblack, [dvipng_black, mpl_black])
156 assert default_hexblack in [dvipng_black, mpl_black]
155
157
156 # Test that dvips name colors can be used without error
158 # Test that dvips name colors can be used without error
157 dvipng_maroon = latextools.latex_to_png_dvipng(latex_string, False,
159 dvipng_maroon = latextools.latex_to_png_dvipng(latex_string, False,
158 'Maroon')
160 'Maroon')
159 # And that it doesn't return the black one
161 # And that it doesn't return the black one
160 nt.assert_not_equal(dvipng_black, dvipng_maroon)
162 assert dvipng_black != dvipng_maroon
161
163
162 mpl_maroon = latextools.latex_to_png_mpl(latex_string, False, 'Maroon')
164 mpl_maroon = latextools.latex_to_png_mpl(latex_string, False, 'Maroon')
163 nt.assert_not_equal(mpl_black, mpl_maroon)
165 assert mpl_black != mpl_maroon
164 mpl_white = latextools.latex_to_png_mpl(latex_string, False, 'White')
166 mpl_white = latextools.latex_to_png_mpl(latex_string, False, 'White')
165 mpl_hexwhite = latextools.latex_to_png_mpl(latex_string, False, '#FFFFFF')
167 mpl_hexwhite = latextools.latex_to_png_mpl(latex_string, False, '#FFFFFF')
166 nt.assert_equal(mpl_white, mpl_hexwhite)
168 assert mpl_white == mpl_hexwhite
167
169
168 mpl_white_scale = latextools.latex_to_png_mpl(latex_string, False,
170 mpl_white_scale = latextools.latex_to_png_mpl(latex_string, False,
169 'White', 1.2)
171 'White', 1.2)
170 nt.assert_not_equal(mpl_white, mpl_white_scale)
172 assert mpl_white != mpl_white_scale
171
173
172
174
173 def test_latex_to_png_invalid_hex_colors():
175 def test_latex_to_png_invalid_hex_colors():
174 """
176 """
175 Test that invalid hex colors provided to dvipng gives an exception.
177 Test that invalid hex colors provided to dvipng gives an exception.
176 """
178 """
177 latex_string = "$x^2$"
179 latex_string = "$x^2$"
178 nt.assert_raises(ValueError, lambda: latextools.latex_to_png(latex_string,
180 nt.assert_raises(ValueError, lambda: latextools.latex_to_png(latex_string,
179 backend='dvipng', color="#f00bar"))
181 backend='dvipng', color="#f00bar"))
180 nt.assert_raises(ValueError, lambda: latextools.latex_to_png(latex_string,
182 nt.assert_raises(ValueError, lambda: latextools.latex_to_png(latex_string,
181 backend='dvipng', color="#f00"))
183 backend='dvipng', color="#f00"))
@@ -1,458 +1,457 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 IPython -- -vvs`. In this form
11 2. With the regular nose syntax, like `iptest IPython -- -vvs`. 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. Options after `--` are passed to nose.
13 plugins loaded. Options after `--` are passed to nose.
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
20
21 import glob
21 import glob
22 from io import BytesIO
22 from io import BytesIO
23 import os
23 import os
24 import os.path as path
24 import os.path as path
25 import sys
25 import sys
26 from threading import Thread, Lock, Event
26 from threading import Thread, Lock, Event
27 import warnings
27 import warnings
28
28
29 import nose.plugins.builtin
29 import nose.plugins.builtin
30 from nose.plugins.xunit import Xunit
30 from nose.plugins.xunit import Xunit
31 from nose import SkipTest
31 from nose import SkipTest
32 from nose.core import TestProgram
32 from nose.core import TestProgram
33 from nose.plugins import Plugin
33 from nose.plugins import Plugin
34 from nose.util import safe_str
34 from nose.util import safe_str
35
35
36 from IPython import version_info
36 from IPython import version_info
37 from IPython.utils.py3compat import decode
37 from IPython.utils.py3compat import decode
38 from IPython.utils.importstring import import_item
38 from IPython.utils.importstring import import_item
39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 from IPython.external.decorators import KnownFailure, knownfailureif
40 from IPython.external.decorators import KnownFailure, knownfailureif
41
41
42 pjoin = path.join
42 pjoin = path.join
43
43
44
44
45 # Enable printing all warnings raise by IPython's modules
45 # Enable printing all warnings raise by IPython's modules
46 warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
46 warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
47 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
47 warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
48 warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
48 warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
49 warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
50
50
51 warnings.filterwarnings('error', message='.*apply_wrapper.*', category=DeprecationWarning, module='.*')
51 warnings.filterwarnings('error', message='.*apply_wrapper.*', category=DeprecationWarning, module='.*')
52 warnings.filterwarnings('error', message='.*make_label_dec', category=DeprecationWarning, module='.*')
52 warnings.filterwarnings('error', message='.*make_label_dec', category=DeprecationWarning, module='.*')
53 warnings.filterwarnings('error', message='.*decorated_dummy.*', category=DeprecationWarning, module='.*')
53 warnings.filterwarnings('error', message='.*decorated_dummy.*', category=DeprecationWarning, module='.*')
54 warnings.filterwarnings('error', message='.*skip_file_no_x11.*', category=DeprecationWarning, module='.*')
54 warnings.filterwarnings('error', message='.*skip_file_no_x11.*', category=DeprecationWarning, module='.*')
55 warnings.filterwarnings('error', message='.*onlyif_any_cmd_exists.*', category=DeprecationWarning, module='.*')
55 warnings.filterwarnings('error', message='.*onlyif_any_cmd_exists.*', category=DeprecationWarning, module='.*')
56
56
57 warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*')
57 warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*')
58
58
59 warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*')
59 warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*')
60 warnings.filterwarnings('error', message='.*IPython.core.display.*', category=DeprecationWarning, module='.*')
60 warnings.filterwarnings('error', message='.*IPython.core.display.*', category=DeprecationWarning, module='.*')
61
61
62 # Jedi older versions
62 # Jedi older versions
63 warnings.filterwarnings(
63 warnings.filterwarnings(
64 'error', message='.*elementwise != comparison failed and.*', category=FutureWarning, module='.*')
64 'error', message='.*elementwise != comparison failed and.*', category=FutureWarning, module='.*')
65
65
66 if version_info < (6,):
66 if version_info < (6,):
67 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
67 # nose.tools renames all things from `camelCase` to `snake_case` which raise an
68 # warning with the runner they also import from standard import library. (as of Dec 2015)
68 # warning with the runner they also import from standard import library. (as of Dec 2015)
69 # Ignore, let's revisit that in a couple of years for IPython 6.
69 # Ignore, let's revisit that in a couple of years for IPython 6.
70 warnings.filterwarnings(
70 warnings.filterwarnings(
71 'ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
71 'ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
72
72
73 if version_info < (8,):
73 if version_info < (8,):
74 warnings.filterwarnings('ignore', message='.*Completer.complete.*',
74 warnings.filterwarnings('ignore', message='.*Completer.complete.*',
75 category=PendingDeprecationWarning, module='.*')
75 category=PendingDeprecationWarning, module='.*')
76 else:
76 else:
77 warnings.warn(
77 warnings.warn(
78 'Completer.complete was pending deprecation and should be changed to Deprecated', FutureWarning)
78 'Completer.complete was pending deprecation and should be changed to Deprecated', FutureWarning)
79
79
80
80
81
81
82 # ------------------------------------------------------------------------------
82 # ------------------------------------------------------------------------------
83 # Monkeypatch Xunit to count known failures as skipped.
83 # Monkeypatch Xunit to count known failures as skipped.
84 # ------------------------------------------------------------------------------
84 # ------------------------------------------------------------------------------
85 def monkeypatch_xunit():
85 def monkeypatch_xunit():
86 try:
86 try:
87 dec.knownfailureif(True)(lambda: None)()
87 dec.knownfailureif(True)(lambda: None)()
88 except Exception as e:
88 except Exception as e:
89 KnownFailureTest = type(e)
89 KnownFailureTest = type(e)
90
90
91 def addError(self, test, err, capt=None):
91 def addError(self, test, err, capt=None):
92 if issubclass(err[0], KnownFailureTest):
92 if issubclass(err[0], KnownFailureTest):
93 err = (SkipTest,) + err[1:]
93 err = (SkipTest,) + err[1:]
94 return self.orig_addError(test, err, capt)
94 return self.orig_addError(test, err, capt)
95
95
96 Xunit.orig_addError = Xunit.addError
96 Xunit.orig_addError = Xunit.addError
97 Xunit.addError = addError
97 Xunit.addError = addError
98
98
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100 # Check which dependencies are installed and greater than minimum version.
100 # Check which dependencies are installed and greater than minimum version.
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 def extract_version(mod):
102 def extract_version(mod):
103 return mod.__version__
103 return mod.__version__
104
104
105 def test_for(item, min_version=None, callback=extract_version):
105 def test_for(item, min_version=None, callback=extract_version):
106 """Test to see if item is importable, and optionally check against a minimum
106 """Test to see if item is importable, and optionally check against a minimum
107 version.
107 version.
108
108
109 If min_version is given, the default behavior is to check against the
109 If min_version is given, the default behavior is to check against the
110 `__version__` attribute of the item, but specifying `callback` allows you to
110 `__version__` attribute of the item, but specifying `callback` allows you to
111 extract the value you are interested in. e.g::
111 extract the value you are interested in. e.g::
112
112
113 In [1]: import sys
113 In [1]: import sys
114
114
115 In [2]: from IPython.testing.iptest import test_for
115 In [2]: from IPython.testing.iptest import test_for
116
116
117 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
117 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
118 Out[3]: True
118 Out[3]: True
119
119
120 """
120 """
121 try:
121 try:
122 check = import_item(item)
122 check = import_item(item)
123 except (ImportError, RuntimeError):
123 except (ImportError, RuntimeError):
124 # GTK reports Runtime error if it can't be initialized even if it's
124 # GTK reports Runtime error if it can't be initialized even if it's
125 # importable.
125 # importable.
126 return False
126 return False
127 else:
127 else:
128 if min_version:
128 if min_version:
129 if callback:
129 if callback:
130 # extra processing step to get version to compare
130 # extra processing step to get version to compare
131 check = callback(check)
131 check = callback(check)
132
132
133 return check >= min_version
133 return check >= min_version
134 else:
134 else:
135 return True
135 return True
136
136
137 # Global dict where we can store information on what we have and what we don't
137 # Global dict where we can store information on what we have and what we don't
138 # have available at test run time
138 # have available at test run time
139 have = {'matplotlib': test_for('matplotlib'),
139 have = {'matplotlib': test_for('matplotlib'),
140 'pygments': test_for('pygments'),
140 'pygments': test_for('pygments'),
141 }
141 }
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 ]
149 ]
150
150
151 class TestSection(object):
151 class TestSection(object):
152 def __init__(self, name, includes):
152 def __init__(self, name, includes):
153 self.name = name
153 self.name = name
154 self.includes = includes
154 self.includes = includes
155 self.excludes = []
155 self.excludes = []
156 self.dependencies = []
156 self.dependencies = []
157 self.enabled = True
157 self.enabled = True
158
158
159 def exclude(self, module):
159 def exclude(self, module):
160 if not module.startswith('IPython'):
160 if not module.startswith('IPython'):
161 module = self.includes[0] + "." + module
161 module = self.includes[0] + "." + module
162 self.excludes.append(module.replace('.', os.sep))
162 self.excludes.append(module.replace('.', os.sep))
163
163
164 def requires(self, *packages):
164 def requires(self, *packages):
165 self.dependencies.extend(packages)
165 self.dependencies.extend(packages)
166
166
167 @property
167 @property
168 def will_run(self):
168 def will_run(self):
169 return self.enabled and all(have[p] for p in self.dependencies)
169 return self.enabled and all(have[p] for p in self.dependencies)
170
170
171 # Name -> (include, exclude, dependencies_met)
171 # Name -> (include, exclude, dependencies_met)
172 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
172 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
173
173
174
174
175 # Exclusions and dependencies
175 # Exclusions and dependencies
176 # ---------------------------
176 # ---------------------------
177
177
178 # core:
178 # core:
179 sec = test_sections['core']
179 sec = test_sections['core']
180 if not have['matplotlib']:
180 if not have['matplotlib']:
181 sec.exclude('pylabtools'),
181 sec.exclude('pylabtools'),
182 sec.exclude('tests.test_pylabtools')
182 sec.exclude('tests.test_pylabtools')
183
183
184 # lib:
184 # lib:
185 sec = test_sections['lib']
185 sec = test_sections['lib']
186 sec.exclude('kernel')
186 sec.exclude('tests.test_latextools')
187 #sec.exclude('kernel')
187 if not have['pygments']:
188 if not have['pygments']:
188 sec.exclude('tests.test_lexers')
189 sec.exclude('tests.test_lexers')
189 # We do this unconditionally, so that the test suite doesn't import
190 # We do this unconditionally, so that the test suite doesn't import
190 # gtk, changing the default encoding and masking some unicode bugs.
191 # gtk, changing the default encoding and masking some unicode bugs.
191 sec.exclude('inputhookgtk')
192 sec.exclude('inputhookgtk')
192 # We also do this unconditionally, because wx can interfere with Unix signals.
193 # We also do this unconditionally, because wx can interfere with Unix signals.
193 # There are currently no tests for it anyway.
194 # There are currently no tests for it anyway.
194 sec.exclude('inputhookwx')
195 sec.exclude('inputhookwx')
195 # Testing inputhook will need a lot of thought, to figure out
196 # Testing inputhook will need a lot of thought, to figure out
196 # how to have tests that don't lock up with the gui event
197 # how to have tests that don't lock up with the gui event
197 # loops in the picture
198 # loops in the picture
198 sec.exclude('inputhook')
199 sec.exclude('inputhook')
199
200
200 # testing:
201 # testing:
201 sec = test_sections['testing']
202 sec = test_sections['testing']
202 # These have to be skipped on win32 because they use echo, rm, cd, etc.
203 # These have to be skipped on win32 because they use echo, rm, cd, etc.
203 # See ticket https://github.com/ipython/ipython/issues/87
204 # See ticket https://github.com/ipython/ipython/issues/87
204 if sys.platform == 'win32':
205 if sys.platform == 'win32':
205 sec.exclude('plugin.test_exampleip')
206 sec.exclude('plugin.test_exampleip')
206 sec.exclude('plugin.dtexample')
207 sec.exclude('plugin.dtexample')
207
208
208 # don't run jupyter_console tests found via shim
209 # don't run jupyter_console tests found via shim
209 test_sections['terminal'].exclude('console')
210 test_sections['terminal'].exclude('console')
210
211
211 # extensions:
212 # extensions:
212 sec = test_sections['extensions']
213 sec = test_sections['extensions']
213 # This is deprecated in favour of rpy2
214 sec.exclude('rmagic')
215 # autoreload does some strange stuff, so move it to its own test section
214 # autoreload does some strange stuff, so move it to its own test section
216 sec.exclude('autoreload')
215 sec.exclude('autoreload')
217 sec.exclude('tests.test_autoreload')
216 sec.exclude('tests.test_autoreload')
218 test_sections['autoreload'] = TestSection('autoreload',
217 test_sections['autoreload'] = TestSection('autoreload',
219 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
218 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
220 test_group_names.append('autoreload')
219 test_group_names.append('autoreload')
221
220
222
221
223 #-----------------------------------------------------------------------------
222 #-----------------------------------------------------------------------------
224 # Functions and classes
223 # Functions and classes
225 #-----------------------------------------------------------------------------
224 #-----------------------------------------------------------------------------
226
225
227 def check_exclusions_exist():
226 def check_exclusions_exist():
228 from IPython.paths import get_ipython_package_dir
227 from IPython.paths import get_ipython_package_dir
229 from warnings import warn
228 from warnings import warn
230 parent = os.path.dirname(get_ipython_package_dir())
229 parent = os.path.dirname(get_ipython_package_dir())
231 for sec in test_sections:
230 for sec in test_sections:
232 for pattern in sec.exclusions:
231 for pattern in sec.exclusions:
233 fullpath = pjoin(parent, pattern)
232 fullpath = pjoin(parent, pattern)
234 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
233 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
235 warn("Excluding nonexistent file: %r" % pattern)
234 warn("Excluding nonexistent file: %r" % pattern)
236
235
237
236
238 class ExclusionPlugin(Plugin):
237 class ExclusionPlugin(Plugin):
239 """A nose plugin to effect our exclusions of files and directories.
238 """A nose plugin to effect our exclusions of files and directories.
240 """
239 """
241 name = 'exclusions'
240 name = 'exclusions'
242 score = 3000 # Should come before any other plugins
241 score = 3000 # Should come before any other plugins
243
242
244 def __init__(self, exclude_patterns=None):
243 def __init__(self, exclude_patterns=None):
245 """
244 """
246 Parameters
245 Parameters
247 ----------
246 ----------
248
247
249 exclude_patterns : sequence of strings, optional
248 exclude_patterns : sequence of strings, optional
250 Filenames containing these patterns (as raw strings, not as regular
249 Filenames containing these patterns (as raw strings, not as regular
251 expressions) are excluded from the tests.
250 expressions) are excluded from the tests.
252 """
251 """
253 self.exclude_patterns = exclude_patterns or []
252 self.exclude_patterns = exclude_patterns or []
254 super(ExclusionPlugin, self).__init__()
253 super(ExclusionPlugin, self).__init__()
255
254
256 def options(self, parser, env=os.environ):
255 def options(self, parser, env=os.environ):
257 Plugin.options(self, parser, env)
256 Plugin.options(self, parser, env)
258
257
259 def configure(self, options, config):
258 def configure(self, options, config):
260 Plugin.configure(self, options, config)
259 Plugin.configure(self, options, config)
261 # Override nose trying to disable plugin.
260 # Override nose trying to disable plugin.
262 self.enabled = True
261 self.enabled = True
263
262
264 def wantFile(self, filename):
263 def wantFile(self, filename):
265 """Return whether the given filename should be scanned for tests.
264 """Return whether the given filename should be scanned for tests.
266 """
265 """
267 if any(pat in filename for pat in self.exclude_patterns):
266 if any(pat in filename for pat in self.exclude_patterns):
268 return False
267 return False
269 return None
268 return None
270
269
271 def wantDirectory(self, directory):
270 def wantDirectory(self, directory):
272 """Return whether the given directory should be scanned for tests.
271 """Return whether the given directory should be scanned for tests.
273 """
272 """
274 if any(pat in directory for pat in self.exclude_patterns):
273 if any(pat in directory for pat in self.exclude_patterns):
275 return False
274 return False
276 return None
275 return None
277
276
278
277
279 class StreamCapturer(Thread):
278 class StreamCapturer(Thread):
280 daemon = True # Don't hang if main thread crashes
279 daemon = True # Don't hang if main thread crashes
281 started = False
280 started = False
282 def __init__(self, echo=False):
281 def __init__(self, echo=False):
283 super(StreamCapturer, self).__init__()
282 super(StreamCapturer, self).__init__()
284 self.echo = echo
283 self.echo = echo
285 self.streams = []
284 self.streams = []
286 self.buffer = BytesIO()
285 self.buffer = BytesIO()
287 self.readfd, self.writefd = os.pipe()
286 self.readfd, self.writefd = os.pipe()
288 self.buffer_lock = Lock()
287 self.buffer_lock = Lock()
289 self.stop = Event()
288 self.stop = Event()
290
289
291 def run(self):
290 def run(self):
292 self.started = True
291 self.started = True
293
292
294 while not self.stop.is_set():
293 while not self.stop.is_set():
295 chunk = os.read(self.readfd, 1024)
294 chunk = os.read(self.readfd, 1024)
296
295
297 with self.buffer_lock:
296 with self.buffer_lock:
298 self.buffer.write(chunk)
297 self.buffer.write(chunk)
299 if self.echo:
298 if self.echo:
300 sys.stdout.write(decode(chunk))
299 sys.stdout.write(decode(chunk))
301
300
302 os.close(self.readfd)
301 os.close(self.readfd)
303 os.close(self.writefd)
302 os.close(self.writefd)
304
303
305 def reset_buffer(self):
304 def reset_buffer(self):
306 with self.buffer_lock:
305 with self.buffer_lock:
307 self.buffer.truncate(0)
306 self.buffer.truncate(0)
308 self.buffer.seek(0)
307 self.buffer.seek(0)
309
308
310 def get_buffer(self):
309 def get_buffer(self):
311 with self.buffer_lock:
310 with self.buffer_lock:
312 return self.buffer.getvalue()
311 return self.buffer.getvalue()
313
312
314 def ensure_started(self):
313 def ensure_started(self):
315 if not self.started:
314 if not self.started:
316 self.start()
315 self.start()
317
316
318 def halt(self):
317 def halt(self):
319 """Safely stop the thread."""
318 """Safely stop the thread."""
320 if not self.started:
319 if not self.started:
321 return
320 return
322
321
323 self.stop.set()
322 self.stop.set()
324 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
323 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
325 self.join()
324 self.join()
326
325
327 class SubprocessStreamCapturePlugin(Plugin):
326 class SubprocessStreamCapturePlugin(Plugin):
328 name='subprocstreams'
327 name='subprocstreams'
329 def __init__(self):
328 def __init__(self):
330 Plugin.__init__(self)
329 Plugin.__init__(self)
331 self.stream_capturer = StreamCapturer()
330 self.stream_capturer = StreamCapturer()
332 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
331 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
333 # This is ugly, but distant parts of the test machinery need to be able
332 # This is ugly, but distant parts of the test machinery need to be able
334 # to redirect streams, so we make the object globally accessible.
333 # to redirect streams, so we make the object globally accessible.
335 nose.iptest_stdstreams_fileno = self.get_write_fileno
334 nose.iptest_stdstreams_fileno = self.get_write_fileno
336
335
337 def get_write_fileno(self):
336 def get_write_fileno(self):
338 if self.destination == 'capture':
337 if self.destination == 'capture':
339 self.stream_capturer.ensure_started()
338 self.stream_capturer.ensure_started()
340 return self.stream_capturer.writefd
339 return self.stream_capturer.writefd
341 elif self.destination == 'discard':
340 elif self.destination == 'discard':
342 return os.open(os.devnull, os.O_WRONLY)
341 return os.open(os.devnull, os.O_WRONLY)
343 else:
342 else:
344 return sys.__stdout__.fileno()
343 return sys.__stdout__.fileno()
345
344
346 def configure(self, options, config):
345 def configure(self, options, config):
347 Plugin.configure(self, options, config)
346 Plugin.configure(self, options, config)
348 # Override nose trying to disable plugin.
347 # Override nose trying to disable plugin.
349 if self.destination == 'capture':
348 if self.destination == 'capture':
350 self.enabled = True
349 self.enabled = True
351
350
352 def startTest(self, test):
351 def startTest(self, test):
353 # Reset log capture
352 # Reset log capture
354 self.stream_capturer.reset_buffer()
353 self.stream_capturer.reset_buffer()
355
354
356 def formatFailure(self, test, err):
355 def formatFailure(self, test, err):
357 # Show output
356 # Show output
358 ec, ev, tb = err
357 ec, ev, tb = err
359 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
358 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
360 if captured.strip():
359 if captured.strip():
361 ev = safe_str(ev)
360 ev = safe_str(ev)
362 out = [ev, '>> begin captured subprocess output <<',
361 out = [ev, '>> begin captured subprocess output <<',
363 captured,
362 captured,
364 '>> end captured subprocess output <<']
363 '>> end captured subprocess output <<']
365 return ec, '\n'.join(out), tb
364 return ec, '\n'.join(out), tb
366
365
367 return err
366 return err
368
367
369 formatError = formatFailure
368 formatError = formatFailure
370
369
371 def finalize(self, result):
370 def finalize(self, result):
372 self.stream_capturer.halt()
371 self.stream_capturer.halt()
373
372
374
373
375 def run_iptest():
374 def run_iptest():
376 """Run the IPython test suite using nose.
375 """Run the IPython test suite using nose.
377
376
378 This function is called when this script is **not** called with the form
377 This function is called when this script is **not** called with the form
379 `iptest all`. It simply calls nose with appropriate command line flags
378 `iptest all`. It simply calls nose with appropriate command line flags
380 and accepts all of the standard nose arguments.
379 and accepts all of the standard nose arguments.
381 """
380 """
382 # Apply our monkeypatch to Xunit
381 # Apply our monkeypatch to Xunit
383 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
382 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
384 monkeypatch_xunit()
383 monkeypatch_xunit()
385
384
386 arg1 = sys.argv[1]
385 arg1 = sys.argv[1]
387 if arg1.startswith('IPython/'):
386 if arg1.startswith('IPython/'):
388 if arg1.endswith('.py'):
387 if arg1.endswith('.py'):
389 arg1 = arg1[:-3]
388 arg1 = arg1[:-3]
390 sys.argv[1] = arg1.replace('/', '.')
389 sys.argv[1] = arg1.replace('/', '.')
391
390
392 arg1 = sys.argv[1]
391 arg1 = sys.argv[1]
393 if arg1 in test_sections:
392 if arg1 in test_sections:
394 section = test_sections[arg1]
393 section = test_sections[arg1]
395 sys.argv[1:2] = section.includes
394 sys.argv[1:2] = section.includes
396 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
395 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
397 section = test_sections[arg1[8:]]
396 section = test_sections[arg1[8:]]
398 sys.argv[1:2] = section.includes
397 sys.argv[1:2] = section.includes
399 else:
398 else:
400 section = TestSection(arg1, includes=[arg1])
399 section = TestSection(arg1, includes=[arg1])
401
400
402
401
403 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
402 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
404 # We add --exe because of setuptools' imbecility (it
403 # We add --exe because of setuptools' imbecility (it
405 # blindly does chmod +x on ALL files). Nose does the
404 # blindly does chmod +x on ALL files). Nose does the
406 # right thing and it tries to avoid executables,
405 # right thing and it tries to avoid executables,
407 # setuptools unfortunately forces our hand here. This
406 # setuptools unfortunately forces our hand here. This
408 # has been discussed on the distutils list and the
407 # has been discussed on the distutils list and the
409 # setuptools devs refuse to fix this problem!
408 # setuptools devs refuse to fix this problem!
410 '--exe',
409 '--exe',
411 ]
410 ]
412 if '-a' not in argv and '-A' not in argv:
411 if '-a' not in argv and '-A' not in argv:
413 argv = argv + ['-a', '!crash']
412 argv = argv + ['-a', '!crash']
414
413
415 if nose.__version__ >= '0.11':
414 if nose.__version__ >= '0.11':
416 # I don't fully understand why we need this one, but depending on what
415 # I don't fully understand why we need this one, but depending on what
417 # directory the test suite is run from, if we don't give it, 0 tests
416 # directory the test suite is run from, if we don't give it, 0 tests
418 # get run. Specifically, if the test suite is run from the source dir
417 # get run. Specifically, if the test suite is run from the source dir
419 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
418 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
420 # even if the same call done in this directory works fine). It appears
419 # even if the same call done in this directory works fine). It appears
421 # that if the requested package is in the current dir, nose bails early
420 # that if the requested package is in the current dir, nose bails early
422 # by default. Since it's otherwise harmless, leave it in by default
421 # by default. Since it's otherwise harmless, leave it in by default
423 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
422 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
424 argv.append('--traverse-namespace')
423 argv.append('--traverse-namespace')
425
424
426 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
425 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
427 SubprocessStreamCapturePlugin() ]
426 SubprocessStreamCapturePlugin() ]
428
427
429 # we still have some vestigial doctests in core
428 # we still have some vestigial doctests in core
430 if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))):
429 if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))):
431 plugins.append(IPythonDoctest())
430 plugins.append(IPythonDoctest())
432 argv.extend([
431 argv.extend([
433 '--with-ipdoctest',
432 '--with-ipdoctest',
434 '--ipdoctest-tests',
433 '--ipdoctest-tests',
435 '--ipdoctest-extension=txt',
434 '--ipdoctest-extension=txt',
436 ])
435 ])
437
436
438
437
439 # Use working directory set by parent process (see iptestcontroller)
438 # Use working directory set by parent process (see iptestcontroller)
440 if 'IPTEST_WORKING_DIR' in os.environ:
439 if 'IPTEST_WORKING_DIR' in os.environ:
441 os.chdir(os.environ['IPTEST_WORKING_DIR'])
440 os.chdir(os.environ['IPTEST_WORKING_DIR'])
442
441
443 # We need a global ipython running in this process, but the special
442 # We need a global ipython running in this process, but the special
444 # in-process group spawns its own IPython kernels, so for *that* group we
443 # in-process group spawns its own IPython kernels, so for *that* group we
445 # must avoid also opening the global one (otherwise there's a conflict of
444 # must avoid also opening the global one (otherwise there's a conflict of
446 # singletons). Ultimately the solution to this problem is to refactor our
445 # singletons). Ultimately the solution to this problem is to refactor our
447 # assumptions about what needs to be a singleton and what doesn't (app
446 # assumptions about what needs to be a singleton and what doesn't (app
448 # objects should, individual shells shouldn't). But for now, this
447 # objects should, individual shells shouldn't). But for now, this
449 # workaround allows the test suite for the inprocess module to complete.
448 # workaround allows the test suite for the inprocess module to complete.
450 if 'kernel.inprocess' not in section.name:
449 if 'kernel.inprocess' not in section.name:
451 from IPython.testing import globalipapp
450 from IPython.testing import globalipapp
452 globalipapp.start_ipython()
451 globalipapp.start_ipython()
453
452
454 # Now nose can run
453 # Now nose can run
455 TestProgram(argv=argv, addplugins=plugins)
454 TestProgram(argv=argv, addplugins=plugins)
456
455
457 if __name__ == '__main__':
456 if __name__ == '__main__':
458 run_iptest()
457 run_iptest()
General Comments 0
You need to be logged in to leave comments. Login now