##// END OF EJS Templates
Add Path support to ipexec
Nikita Kniazev -
Show More
@@ -1,156 +1,156 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for profile-related functions.
2 """Tests for profile-related functions.
3
3
4 Currently only the startup-dir functionality is tested, but more tests should
4 Currently only the startup-dir functionality is tested, but more tests should
5 be added for:
5 be added for:
6
6
7 * ipython profile create
7 * ipython profile create
8 * ipython profile list
8 * ipython profile list
9 * ipython profile create --parallel
9 * ipython profile create --parallel
10 * security dir permissions
10 * security dir permissions
11
11
12 Authors
12 Authors
13 -------
13 -------
14
14
15 * MinRK
15 * MinRK
16
16
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 import shutil
23 import shutil
24 import sys
24 import sys
25 import tempfile
25 import tempfile
26
26
27 from pathlib import Path
27 from pathlib import Path
28 from unittest import TestCase
28 from unittest import TestCase
29
29
30 from IPython.core.profileapp import list_profiles_in, list_bundled_profiles
30 from IPython.core.profileapp import list_profiles_in, list_bundled_profiles
31 from IPython.core.profiledir import ProfileDir
31 from IPython.core.profiledir import ProfileDir
32
32
33 from IPython.testing import decorators as dec
33 from IPython.testing import decorators as dec
34 from IPython.testing import tools as tt
34 from IPython.testing import tools as tt
35 from IPython.utils.process import getoutput
35 from IPython.utils.process import getoutput
36 from IPython.utils.tempdir import TemporaryDirectory
36 from IPython.utils.tempdir import TemporaryDirectory
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Globals
39 # Globals
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 TMP_TEST_DIR = Path(tempfile.mkdtemp())
41 TMP_TEST_DIR = Path(tempfile.mkdtemp())
42 HOME_TEST_DIR = TMP_TEST_DIR / "home_test_dir"
42 HOME_TEST_DIR = TMP_TEST_DIR / "home_test_dir"
43 IP_TEST_DIR = HOME_TEST_DIR / ".ipython"
43 IP_TEST_DIR = HOME_TEST_DIR / ".ipython"
44
44
45 #
45 #
46 # Setup/teardown functions/decorators
46 # Setup/teardown functions/decorators
47 #
47 #
48
48
49 def setup_module():
49 def setup_module():
50 """Setup test environment for the module:
50 """Setup test environment for the module:
51
51
52 - Adds dummy home dir tree
52 - Adds dummy home dir tree
53 """
53 """
54 # Do not mask exceptions here. In particular, catching WindowsError is a
54 # Do not mask exceptions here. In particular, catching WindowsError is a
55 # problem because that exception is only defined on Windows...
55 # problem because that exception is only defined on Windows...
56 (Path.cwd() / IP_TEST_DIR).mkdir(parents=True)
56 (Path.cwd() / IP_TEST_DIR).mkdir(parents=True)
57
57
58
58
59 def teardown_module():
59 def teardown_module():
60 """Teardown test environment for the module:
60 """Teardown test environment for the module:
61
61
62 - Remove dummy home dir tree
62 - Remove dummy home dir tree
63 """
63 """
64 # Note: we remove the parent test dir, which is the root of all test
64 # Note: we remove the parent test dir, which is the root of all test
65 # subdirs we may have created. Use shutil instead of os.removedirs, so
65 # subdirs we may have created. Use shutil instead of os.removedirs, so
66 # that non-empty directories are all recursively removed.
66 # that non-empty directories are all recursively removed.
67 shutil.rmtree(TMP_TEST_DIR)
67 shutil.rmtree(TMP_TEST_DIR)
68
68
69
69
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71 # Test functions
71 # Test functions
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 class ProfileStartupTest(TestCase):
73 class ProfileStartupTest(TestCase):
74 def setUp(self):
74 def setUp(self):
75 # create profile dir
75 # create profile dir
76 self.pd = ProfileDir.create_profile_dir_by_name(IP_TEST_DIR, "test")
76 self.pd = ProfileDir.create_profile_dir_by_name(IP_TEST_DIR, "test")
77 self.options = ["--ipython-dir", IP_TEST_DIR, "--profile", "test"]
77 self.options = ["--ipython-dir", IP_TEST_DIR, "--profile", "test"]
78 self.fname = TMP_TEST_DIR / "test.py"
78 self.fname = TMP_TEST_DIR / "test.py"
79
79
80 def tearDown(self):
80 def tearDown(self):
81 # We must remove this profile right away so its presence doesn't
81 # We must remove this profile right away so its presence doesn't
82 # confuse other tests.
82 # confuse other tests.
83 shutil.rmtree(self.pd.location)
83 shutil.rmtree(self.pd.location)
84
84
85 def init(self, startup_file, startup, test):
85 def init(self, startup_file, startup, test):
86 # write startup python file
86 # write startup python file
87 with open(Path(self.pd.startup_dir) / startup_file, "w") as f:
87 with open(Path(self.pd.startup_dir) / startup_file, "w") as f:
88 f.write(startup)
88 f.write(startup)
89 # write simple test file, to check that the startup file was run
89 # write simple test file, to check that the startup file was run
90 with open(self.fname, 'w') as f:
90 with open(self.fname, 'w') as f:
91 f.write(test)
91 f.write(test)
92
92
93 def validate(self, output):
93 def validate(self, output):
94 tt.ipexec_validate(self.fname, output, '', options=self.options)
94 tt.ipexec_validate(self.fname, output, "", options=self.options)
95
95
96 def test_startup_py(self):
96 def test_startup_py(self):
97 self.init('00-start.py', 'zzz=123\n', 'print(zzz)\n')
97 self.init('00-start.py', 'zzz=123\n', 'print(zzz)\n')
98 self.validate('123')
98 self.validate('123')
99
99
100 def test_startup_ipy(self):
100 def test_startup_ipy(self):
101 self.init('00-start.ipy', '%xmode plain\n', '')
101 self.init('00-start.ipy', '%xmode plain\n', '')
102 self.validate('Exception reporting mode: Plain')
102 self.validate('Exception reporting mode: Plain')
103
103
104
104
105 def test_list_profiles_in():
105 def test_list_profiles_in():
106 # No need to remove these directories and files, as they will get nuked in
106 # No need to remove these directories and files, as they will get nuked in
107 # the module-level teardown.
107 # the module-level teardown.
108 td = Path(tempfile.mkdtemp(dir=TMP_TEST_DIR))
108 td = Path(tempfile.mkdtemp(dir=TMP_TEST_DIR))
109 for name in ("profile_foo", "profile_hello", "not_a_profile"):
109 for name in ("profile_foo", "profile_hello", "not_a_profile"):
110 Path(td / name).mkdir(parents=True)
110 Path(td / name).mkdir(parents=True)
111 if dec.unicode_paths:
111 if dec.unicode_paths:
112 Path(td / u"profile_ΓΌnicode").mkdir(parents=True)
112 Path(td / u"profile_ΓΌnicode").mkdir(parents=True)
113
113
114 with open(td / "profile_file", "w") as f:
114 with open(td / "profile_file", "w") as f:
115 f.write("I am not a profile directory")
115 f.write("I am not a profile directory")
116 profiles = list_profiles_in(td)
116 profiles = list_profiles_in(td)
117
117
118 # unicode normalization can turn u'ΓΌnicode' into u'u\0308nicode',
118 # unicode normalization can turn u'ΓΌnicode' into u'u\0308nicode',
119 # so only check for *nicode, and that creating a ProfileDir from the
119 # so only check for *nicode, and that creating a ProfileDir from the
120 # name remains valid
120 # name remains valid
121 found_unicode = False
121 found_unicode = False
122 for p in list(profiles):
122 for p in list(profiles):
123 if p.endswith('nicode'):
123 if p.endswith('nicode'):
124 pd = ProfileDir.find_profile_dir_by_name(td, p)
124 pd = ProfileDir.find_profile_dir_by_name(td, p)
125 profiles.remove(p)
125 profiles.remove(p)
126 found_unicode = True
126 found_unicode = True
127 break
127 break
128 if dec.unicode_paths:
128 if dec.unicode_paths:
129 assert found_unicode is True
129 assert found_unicode is True
130 assert set(profiles) == {"foo", "hello"}
130 assert set(profiles) == {"foo", "hello"}
131
131
132
132
133 def test_list_bundled_profiles():
133 def test_list_bundled_profiles():
134 # This variable will need to be updated when a new profile gets bundled
134 # This variable will need to be updated when a new profile gets bundled
135 bundled = sorted(list_bundled_profiles())
135 bundled = sorted(list_bundled_profiles())
136 assert bundled == []
136 assert bundled == []
137
137
138
138
139 def test_profile_create_ipython_dir():
139 def test_profile_create_ipython_dir():
140 """ipython profile create respects --ipython-dir"""
140 """ipython profile create respects --ipython-dir"""
141 with TemporaryDirectory() as td:
141 with TemporaryDirectory() as td:
142 getoutput(
142 getoutput(
143 [
143 [
144 sys.executable,
144 sys.executable,
145 "-m",
145 "-m",
146 "IPython",
146 "IPython",
147 "profile",
147 "profile",
148 "create",
148 "create",
149 "foo",
149 "foo",
150 "--ipython-dir=%s" % td,
150 "--ipython-dir=%s" % td,
151 ]
151 ]
152 )
152 )
153 profile_dir = Path(td) / "profile_foo"
153 profile_dir = Path(td) / "profile_foo"
154 assert Path(profile_dir).exists()
154 assert Path(profile_dir).exists()
155 ipython_config = profile_dir / "ipython_config.py"
155 ipython_config = profile_dir / "ipython_config.py"
156 assert Path(ipython_config).exists()
156 assert Path(ipython_config).exists()
@@ -1,474 +1,477 b''
1 """Generic testing tools.
1 """Generic testing tools.
2
2
3 Authors
3 Authors
4 -------
4 -------
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 """
6 """
7
7
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 import os
12 import os
13 from pathlib import Path
13 from pathlib import Path
14 import re
14 import re
15 import sys
15 import sys
16 import tempfile
16 import tempfile
17 import unittest
17 import unittest
18
18
19 from contextlib import contextmanager
19 from contextlib import contextmanager
20 from io import StringIO
20 from io import StringIO
21 from subprocess import Popen, PIPE
21 from subprocess import Popen, PIPE
22 from unittest.mock import patch
22 from unittest.mock import patch
23
23
24 try:
24 try:
25 # These tools are used by parts of the runtime, so we make the nose
25 # These tools are used by parts of the runtime, so we make the nose
26 # dependency optional at this point. Nose is a hard dependency to run the
26 # dependency optional at this point. Nose is a hard dependency to run the
27 # test suite, but NOT to use ipython itself.
27 # test suite, but NOT to use ipython itself.
28 import nose.tools as nt
28 import nose.tools as nt
29 has_nose = True
29 has_nose = True
30 except ImportError:
30 except ImportError:
31 has_nose = False
31 has_nose = False
32
32
33 from traitlets.config.loader import Config
33 from traitlets.config.loader import Config
34 from IPython.utils.process import get_output_error_code
34 from IPython.utils.process import get_output_error_code
35 from IPython.utils.text import list_strings
35 from IPython.utils.text import list_strings
36 from IPython.utils.io import temp_pyfile, Tee
36 from IPython.utils.io import temp_pyfile, Tee
37 from IPython.utils import py3compat
37 from IPython.utils import py3compat
38
38
39 from . import decorators as dec
39 from . import decorators as dec
40 from . import skipdoctest
40 from . import skipdoctest
41
41
42
42
43 # The docstring for full_path doctests differently on win32 (different path
43 # The docstring for full_path doctests differently on win32 (different path
44 # separator) so just skip the doctest there. The example remains informative.
44 # separator) so just skip the doctest there. The example remains informative.
45 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
45 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
46
46
47 @doctest_deco
47 @doctest_deco
48 def full_path(startPath,files):
48 def full_path(startPath,files):
49 """Make full paths for all the listed files, based on startPath.
49 """Make full paths for all the listed files, based on startPath.
50
50
51 Only the base part of startPath is kept, since this routine is typically
51 Only the base part of startPath is kept, since this routine is typically
52 used with a script's ``__file__`` variable as startPath. The base of startPath
52 used with a script's ``__file__`` variable as startPath. The base of startPath
53 is then prepended to all the listed files, forming the output list.
53 is then prepended to all the listed files, forming the output list.
54
54
55 Parameters
55 Parameters
56 ----------
56 ----------
57 startPath : string
57 startPath : string
58 Initial path to use as the base for the results. This path is split
58 Initial path to use as the base for the results. This path is split
59 using os.path.split() and only its first component is kept.
59 using os.path.split() and only its first component is kept.
60
60
61 files : string or list
61 files : string or list
62 One or more files.
62 One or more files.
63
63
64 Examples
64 Examples
65 --------
65 --------
66
66
67 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
67 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
68 ['/foo/a.txt', '/foo/b.txt']
68 ['/foo/a.txt', '/foo/b.txt']
69
69
70 >>> full_path('/foo',['a.txt','b.txt'])
70 >>> full_path('/foo',['a.txt','b.txt'])
71 ['/a.txt', '/b.txt']
71 ['/a.txt', '/b.txt']
72
72
73 If a single file is given, the output is still a list::
73 If a single file is given, the output is still a list::
74
74
75 >>> full_path('/foo','a.txt')
75 >>> full_path('/foo','a.txt')
76 ['/a.txt']
76 ['/a.txt']
77 """
77 """
78
78
79 files = list_strings(files)
79 files = list_strings(files)
80 base = os.path.split(startPath)[0]
80 base = os.path.split(startPath)[0]
81 return [ os.path.join(base,f) for f in files ]
81 return [ os.path.join(base,f) for f in files ]
82
82
83
83
84 def parse_test_output(txt):
84 def parse_test_output(txt):
85 """Parse the output of a test run and return errors, failures.
85 """Parse the output of a test run and return errors, failures.
86
86
87 Parameters
87 Parameters
88 ----------
88 ----------
89 txt : str
89 txt : str
90 Text output of a test run, assumed to contain a line of one of the
90 Text output of a test run, assumed to contain a line of one of the
91 following forms::
91 following forms::
92
92
93 'FAILED (errors=1)'
93 'FAILED (errors=1)'
94 'FAILED (failures=1)'
94 'FAILED (failures=1)'
95 'FAILED (errors=1, failures=1)'
95 'FAILED (errors=1, failures=1)'
96
96
97 Returns
97 Returns
98 -------
98 -------
99 nerr, nfail
99 nerr, nfail
100 number of errors and failures.
100 number of errors and failures.
101 """
101 """
102
102
103 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
103 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
104 if err_m:
104 if err_m:
105 nerr = int(err_m.group(1))
105 nerr = int(err_m.group(1))
106 nfail = 0
106 nfail = 0
107 return nerr, nfail
107 return nerr, nfail
108
108
109 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
109 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
110 if fail_m:
110 if fail_m:
111 nerr = 0
111 nerr = 0
112 nfail = int(fail_m.group(1))
112 nfail = int(fail_m.group(1))
113 return nerr, nfail
113 return nerr, nfail
114
114
115 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
115 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
116 re.MULTILINE)
116 re.MULTILINE)
117 if both_m:
117 if both_m:
118 nerr = int(both_m.group(1))
118 nerr = int(both_m.group(1))
119 nfail = int(both_m.group(2))
119 nfail = int(both_m.group(2))
120 return nerr, nfail
120 return nerr, nfail
121
121
122 # If the input didn't match any of these forms, assume no error/failures
122 # If the input didn't match any of these forms, assume no error/failures
123 return 0, 0
123 return 0, 0
124
124
125
125
126 # So nose doesn't think this is a test
126 # So nose doesn't think this is a test
127 parse_test_output.__test__ = False
127 parse_test_output.__test__ = False
128
128
129
129
130 def default_argv():
130 def default_argv():
131 """Return a valid default argv for creating testing instances of ipython"""
131 """Return a valid default argv for creating testing instances of ipython"""
132
132
133 return ['--quick', # so no config file is loaded
133 return ['--quick', # so no config file is loaded
134 # Other defaults to minimize side effects on stdout
134 # Other defaults to minimize side effects on stdout
135 '--colors=NoColor', '--no-term-title','--no-banner',
135 '--colors=NoColor', '--no-term-title','--no-banner',
136 '--autocall=0']
136 '--autocall=0']
137
137
138
138
139 def default_config():
139 def default_config():
140 """Return a config object with good defaults for testing."""
140 """Return a config object with good defaults for testing."""
141 config = Config()
141 config = Config()
142 config.TerminalInteractiveShell.colors = 'NoColor'
142 config.TerminalInteractiveShell.colors = 'NoColor'
143 config.TerminalTerminalInteractiveShell.term_title = False,
143 config.TerminalTerminalInteractiveShell.term_title = False,
144 config.TerminalInteractiveShell.autocall = 0
144 config.TerminalInteractiveShell.autocall = 0
145 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
145 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
146 config.HistoryManager.hist_file = Path(f.name)
146 config.HistoryManager.hist_file = Path(f.name)
147 f.close()
147 f.close()
148 config.HistoryManager.db_cache_size = 10000
148 config.HistoryManager.db_cache_size = 10000
149 return config
149 return config
150
150
151
151
152 def get_ipython_cmd(as_string=False):
152 def get_ipython_cmd(as_string=False):
153 """
153 """
154 Return appropriate IPython command line name. By default, this will return
154 Return appropriate IPython command line name. By default, this will return
155 a list that can be used with subprocess.Popen, for example, but passing
155 a list that can be used with subprocess.Popen, for example, but passing
156 `as_string=True` allows for returning the IPython command as a string.
156 `as_string=True` allows for returning the IPython command as a string.
157
157
158 Parameters
158 Parameters
159 ----------
159 ----------
160 as_string: bool
160 as_string: bool
161 Flag to allow to return the command as a string.
161 Flag to allow to return the command as a string.
162 """
162 """
163 ipython_cmd = [sys.executable, "-m", "IPython"]
163 ipython_cmd = [sys.executable, "-m", "IPython"]
164
164
165 if as_string:
165 if as_string:
166 ipython_cmd = " ".join(ipython_cmd)
166 ipython_cmd = " ".join(ipython_cmd)
167
167
168 return ipython_cmd
168 return ipython_cmd
169
169
170 def ipexec(fname, options=None, commands=()):
170 def ipexec(fname, options=None, commands=()):
171 """Utility to call 'ipython filename'.
171 """Utility to call 'ipython filename'.
172
172
173 Starts IPython with a minimal and safe configuration to make startup as fast
173 Starts IPython with a minimal and safe configuration to make startup as fast
174 as possible.
174 as possible.
175
175
176 Note that this starts IPython in a subprocess!
176 Note that this starts IPython in a subprocess!
177
177
178 Parameters
178 Parameters
179 ----------
179 ----------
180 fname : str
180 fname : str, Path
181 Name of file to be executed (should have .py or .ipy extension).
181 Name of file to be executed (should have .py or .ipy extension).
182
182
183 options : optional, list
183 options : optional, list
184 Extra command-line flags to be passed to IPython.
184 Extra command-line flags to be passed to IPython.
185
185
186 commands : optional, list
186 commands : optional, list
187 Commands to send in on stdin
187 Commands to send in on stdin
188
188
189 Returns
189 Returns
190 -------
190 -------
191 ``(stdout, stderr)`` of ipython subprocess.
191 ``(stdout, stderr)`` of ipython subprocess.
192 """
192 """
193 if options is None: options = []
193 if options is None: options = []
194
194
195 cmdargs = default_argv() + options
195 cmdargs = default_argv() + options
196
196
197 test_dir = os.path.dirname(__file__)
197 test_dir = os.path.dirname(__file__)
198
198
199 ipython_cmd = get_ipython_cmd()
199 ipython_cmd = get_ipython_cmd()
200 # Absolute path for filename
200 # Absolute path for filename
201 full_fname = os.path.join(test_dir, fname)
201 full_fname = os.path.join(test_dir, fname)
202 full_cmd = ipython_cmd + cmdargs + ['--', full_fname]
202 full_cmd = ipython_cmd + cmdargs + ['--', full_fname]
203 if sys.platform == "win32" and sys.version_info < (3, 8):
204 # subprocess.Popen does not support Path objects yet
205 full_cmd = list(map(str, full_cmd))
203 env = os.environ.copy()
206 env = os.environ.copy()
204 # FIXME: ignore all warnings in ipexec while we have shims
207 # FIXME: ignore all warnings in ipexec while we have shims
205 # should we keep suppressing warnings here, even after removing shims?
208 # should we keep suppressing warnings here, even after removing shims?
206 env['PYTHONWARNINGS'] = 'ignore'
209 env['PYTHONWARNINGS'] = 'ignore'
207 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
210 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
208 # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
211 # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
209 env.pop("PYCHARM_HOSTED", None)
212 env.pop("PYCHARM_HOSTED", None)
210 for k, v in env.items():
213 for k, v in env.items():
211 # Debug a bizarre failure we've seen on Windows:
214 # Debug a bizarre failure we've seen on Windows:
212 # TypeError: environment can only contain strings
215 # TypeError: environment can only contain strings
213 if not isinstance(v, str):
216 if not isinstance(v, str):
214 print(k, v)
217 print(k, v)
215 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
218 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
216 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
219 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
217 out, err = py3compat.decode(out), py3compat.decode(err)
220 out, err = py3compat.decode(out), py3compat.decode(err)
218 # `import readline` causes 'ESC[?1034h' to be output sometimes,
221 # `import readline` causes 'ESC[?1034h' to be output sometimes,
219 # so strip that out before doing comparisons
222 # so strip that out before doing comparisons
220 if out:
223 if out:
221 out = re.sub(r'\x1b\[[^h]+h', '', out)
224 out = re.sub(r'\x1b\[[^h]+h', '', out)
222 return out, err
225 return out, err
223
226
224
227
225 def ipexec_validate(fname, expected_out, expected_err='',
228 def ipexec_validate(fname, expected_out, expected_err='',
226 options=None, commands=()):
229 options=None, commands=()):
227 """Utility to call 'ipython filename' and validate output/error.
230 """Utility to call 'ipython filename' and validate output/error.
228
231
229 This function raises an AssertionError if the validation fails.
232 This function raises an AssertionError if the validation fails.
230
233
231 Note that this starts IPython in a subprocess!
234 Note that this starts IPython in a subprocess!
232
235
233 Parameters
236 Parameters
234 ----------
237 ----------
235 fname : str
238 fname : str, Path
236 Name of the file to be executed (should have .py or .ipy extension).
239 Name of the file to be executed (should have .py or .ipy extension).
237
240
238 expected_out : str
241 expected_out : str
239 Expected stdout of the process.
242 Expected stdout of the process.
240
243
241 expected_err : optional, str
244 expected_err : optional, str
242 Expected stderr of the process.
245 Expected stderr of the process.
243
246
244 options : optional, list
247 options : optional, list
245 Extra command-line flags to be passed to IPython.
248 Extra command-line flags to be passed to IPython.
246
249
247 Returns
250 Returns
248 -------
251 -------
249 None
252 None
250 """
253 """
251
254
252 import nose.tools as nt
255 import nose.tools as nt
253
256
254 out, err = ipexec(fname, options, commands)
257 out, err = ipexec(fname, options, commands)
255 #print 'OUT', out # dbg
258 #print 'OUT', out # dbg
256 #print 'ERR', err # dbg
259 #print 'ERR', err # dbg
257 # If there are any errors, we must check those before stdout, as they may be
260 # If there are any errors, we must check those before stdout, as they may be
258 # more informative than simply having an empty stdout.
261 # more informative than simply having an empty stdout.
259 if err:
262 if err:
260 if expected_err:
263 if expected_err:
261 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
264 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
262 else:
265 else:
263 raise ValueError('Running file %r produced error: %r' %
266 raise ValueError('Running file %r produced error: %r' %
264 (fname, err))
267 (fname, err))
265 # If no errors or output on stderr was expected, match stdout
268 # If no errors or output on stderr was expected, match stdout
266 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
269 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
267
270
268
271
269 class TempFileMixin(unittest.TestCase):
272 class TempFileMixin(unittest.TestCase):
270 """Utility class to create temporary Python/IPython files.
273 """Utility class to create temporary Python/IPython files.
271
274
272 Meant as a mixin class for test cases."""
275 Meant as a mixin class for test cases."""
273
276
274 def mktmp(self, src, ext='.py'):
277 def mktmp(self, src, ext='.py'):
275 """Make a valid python temp file."""
278 """Make a valid python temp file."""
276 fname = temp_pyfile(src, ext)
279 fname = temp_pyfile(src, ext)
277 if not hasattr(self, 'tmps'):
280 if not hasattr(self, 'tmps'):
278 self.tmps=[]
281 self.tmps=[]
279 self.tmps.append(fname)
282 self.tmps.append(fname)
280 self.fname = fname
283 self.fname = fname
281
284
282 def tearDown(self):
285 def tearDown(self):
283 # If the tmpfile wasn't made because of skipped tests, like in
286 # If the tmpfile wasn't made because of skipped tests, like in
284 # win32, there's nothing to cleanup.
287 # win32, there's nothing to cleanup.
285 if hasattr(self, 'tmps'):
288 if hasattr(self, 'tmps'):
286 for fname in self.tmps:
289 for fname in self.tmps:
287 # If the tmpfile wasn't made because of skipped tests, like in
290 # If the tmpfile wasn't made because of skipped tests, like in
288 # win32, there's nothing to cleanup.
291 # win32, there's nothing to cleanup.
289 try:
292 try:
290 os.unlink(fname)
293 os.unlink(fname)
291 except:
294 except:
292 # On Windows, even though we close the file, we still can't
295 # On Windows, even though we close the file, we still can't
293 # delete it. I have no clue why
296 # delete it. I have no clue why
294 pass
297 pass
295
298
296 def __enter__(self):
299 def __enter__(self):
297 return self
300 return self
298
301
299 def __exit__(self, exc_type, exc_value, traceback):
302 def __exit__(self, exc_type, exc_value, traceback):
300 self.tearDown()
303 self.tearDown()
301
304
302
305
303 pair_fail_msg = ("Testing {0}\n\n"
306 pair_fail_msg = ("Testing {0}\n\n"
304 "In:\n"
307 "In:\n"
305 " {1!r}\n"
308 " {1!r}\n"
306 "Expected:\n"
309 "Expected:\n"
307 " {2!r}\n"
310 " {2!r}\n"
308 "Got:\n"
311 "Got:\n"
309 " {3!r}\n")
312 " {3!r}\n")
310 def check_pairs(func, pairs):
313 def check_pairs(func, pairs):
311 """Utility function for the common case of checking a function with a
314 """Utility function for the common case of checking a function with a
312 sequence of input/output pairs.
315 sequence of input/output pairs.
313
316
314 Parameters
317 Parameters
315 ----------
318 ----------
316 func : callable
319 func : callable
317 The function to be tested. Should accept a single argument.
320 The function to be tested. Should accept a single argument.
318 pairs : iterable
321 pairs : iterable
319 A list of (input, expected_output) tuples.
322 A list of (input, expected_output) tuples.
320
323
321 Returns
324 Returns
322 -------
325 -------
323 None. Raises an AssertionError if any output does not match the expected
326 None. Raises an AssertionError if any output does not match the expected
324 value.
327 value.
325 """
328 """
326 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
329 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
327 for inp, expected in pairs:
330 for inp, expected in pairs:
328 out = func(inp)
331 out = func(inp)
329 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
332 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
330
333
331
334
332 MyStringIO = StringIO
335 MyStringIO = StringIO
333
336
334 _re_type = type(re.compile(r''))
337 _re_type = type(re.compile(r''))
335
338
336 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
339 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
337 -------
340 -------
338 {2!s}
341 {2!s}
339 -------
342 -------
340 """
343 """
341
344
342 class AssertPrints(object):
345 class AssertPrints(object):
343 """Context manager for testing that code prints certain text.
346 """Context manager for testing that code prints certain text.
344
347
345 Examples
348 Examples
346 --------
349 --------
347 >>> with AssertPrints("abc", suppress=False):
350 >>> with AssertPrints("abc", suppress=False):
348 ... print("abcd")
351 ... print("abcd")
349 ... print("def")
352 ... print("def")
350 ...
353 ...
351 abcd
354 abcd
352 def
355 def
353 """
356 """
354 def __init__(self, s, channel='stdout', suppress=True):
357 def __init__(self, s, channel='stdout', suppress=True):
355 self.s = s
358 self.s = s
356 if isinstance(self.s, (str, _re_type)):
359 if isinstance(self.s, (str, _re_type)):
357 self.s = [self.s]
360 self.s = [self.s]
358 self.channel = channel
361 self.channel = channel
359 self.suppress = suppress
362 self.suppress = suppress
360
363
361 def __enter__(self):
364 def __enter__(self):
362 self.orig_stream = getattr(sys, self.channel)
365 self.orig_stream = getattr(sys, self.channel)
363 self.buffer = MyStringIO()
366 self.buffer = MyStringIO()
364 self.tee = Tee(self.buffer, channel=self.channel)
367 self.tee = Tee(self.buffer, channel=self.channel)
365 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
368 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
366
369
367 def __exit__(self, etype, value, traceback):
370 def __exit__(self, etype, value, traceback):
368 try:
371 try:
369 if value is not None:
372 if value is not None:
370 # If an error was raised, don't check anything else
373 # If an error was raised, don't check anything else
371 return False
374 return False
372 self.tee.flush()
375 self.tee.flush()
373 setattr(sys, self.channel, self.orig_stream)
376 setattr(sys, self.channel, self.orig_stream)
374 printed = self.buffer.getvalue()
377 printed = self.buffer.getvalue()
375 for s in self.s:
378 for s in self.s:
376 if isinstance(s, _re_type):
379 if isinstance(s, _re_type):
377 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
380 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
378 else:
381 else:
379 assert s in printed, notprinted_msg.format(s, self.channel, printed)
382 assert s in printed, notprinted_msg.format(s, self.channel, printed)
380 return False
383 return False
381 finally:
384 finally:
382 self.tee.close()
385 self.tee.close()
383
386
384 printed_msg = """Found {0!r} in printed output (on {1}):
387 printed_msg = """Found {0!r} in printed output (on {1}):
385 -------
388 -------
386 {2!s}
389 {2!s}
387 -------
390 -------
388 """
391 """
389
392
390 class AssertNotPrints(AssertPrints):
393 class AssertNotPrints(AssertPrints):
391 """Context manager for checking that certain output *isn't* produced.
394 """Context manager for checking that certain output *isn't* produced.
392
395
393 Counterpart of AssertPrints"""
396 Counterpart of AssertPrints"""
394 def __exit__(self, etype, value, traceback):
397 def __exit__(self, etype, value, traceback):
395 try:
398 try:
396 if value is not None:
399 if value is not None:
397 # If an error was raised, don't check anything else
400 # If an error was raised, don't check anything else
398 self.tee.close()
401 self.tee.close()
399 return False
402 return False
400 self.tee.flush()
403 self.tee.flush()
401 setattr(sys, self.channel, self.orig_stream)
404 setattr(sys, self.channel, self.orig_stream)
402 printed = self.buffer.getvalue()
405 printed = self.buffer.getvalue()
403 for s in self.s:
406 for s in self.s:
404 if isinstance(s, _re_type):
407 if isinstance(s, _re_type):
405 assert not s.search(printed),printed_msg.format(
408 assert not s.search(printed),printed_msg.format(
406 s.pattern, self.channel, printed)
409 s.pattern, self.channel, printed)
407 else:
410 else:
408 assert s not in printed, printed_msg.format(
411 assert s not in printed, printed_msg.format(
409 s, self.channel, printed)
412 s, self.channel, printed)
410 return False
413 return False
411 finally:
414 finally:
412 self.tee.close()
415 self.tee.close()
413
416
414 @contextmanager
417 @contextmanager
415 def mute_warn():
418 def mute_warn():
416 from IPython.utils import warn
419 from IPython.utils import warn
417 save_warn = warn.warn
420 save_warn = warn.warn
418 warn.warn = lambda *a, **kw: None
421 warn.warn = lambda *a, **kw: None
419 try:
422 try:
420 yield
423 yield
421 finally:
424 finally:
422 warn.warn = save_warn
425 warn.warn = save_warn
423
426
424 @contextmanager
427 @contextmanager
425 def make_tempfile(name):
428 def make_tempfile(name):
426 """ Create an empty, named, temporary file for the duration of the context.
429 """ Create an empty, named, temporary file for the duration of the context.
427 """
430 """
428 open(name, 'w').close()
431 open(name, 'w').close()
429 try:
432 try:
430 yield
433 yield
431 finally:
434 finally:
432 os.unlink(name)
435 os.unlink(name)
433
436
434 def fake_input(inputs):
437 def fake_input(inputs):
435 """Temporarily replace the input() function to return the given values
438 """Temporarily replace the input() function to return the given values
436
439
437 Use as a context manager:
440 Use as a context manager:
438
441
439 with fake_input(['result1', 'result2']):
442 with fake_input(['result1', 'result2']):
440 ...
443 ...
441
444
442 Values are returned in order. If input() is called again after the last value
445 Values are returned in order. If input() is called again after the last value
443 was used, EOFError is raised.
446 was used, EOFError is raised.
444 """
447 """
445 it = iter(inputs)
448 it = iter(inputs)
446 def mock_input(prompt=''):
449 def mock_input(prompt=''):
447 try:
450 try:
448 return next(it)
451 return next(it)
449 except StopIteration as e:
452 except StopIteration as e:
450 raise EOFError('No more inputs given') from e
453 raise EOFError('No more inputs given') from e
451
454
452 return patch('builtins.input', mock_input)
455 return patch('builtins.input', mock_input)
453
456
454 def help_output_test(subcommand=''):
457 def help_output_test(subcommand=''):
455 """test that `ipython [subcommand] -h` works"""
458 """test that `ipython [subcommand] -h` works"""
456 cmd = get_ipython_cmd() + [subcommand, '-h']
459 cmd = get_ipython_cmd() + [subcommand, '-h']
457 out, err, rc = get_output_error_code(cmd)
460 out, err, rc = get_output_error_code(cmd)
458 nt.assert_equal(rc, 0, err)
461 nt.assert_equal(rc, 0, err)
459 nt.assert_not_in("Traceback", err)
462 nt.assert_not_in("Traceback", err)
460 nt.assert_in("Options", out)
463 nt.assert_in("Options", out)
461 nt.assert_in("--help-all", out)
464 nt.assert_in("--help-all", out)
462 return out, err
465 return out, err
463
466
464
467
465 def help_all_output_test(subcommand=''):
468 def help_all_output_test(subcommand=''):
466 """test that `ipython [subcommand] --help-all` works"""
469 """test that `ipython [subcommand] --help-all` works"""
467 cmd = get_ipython_cmd() + [subcommand, '--help-all']
470 cmd = get_ipython_cmd() + [subcommand, '--help-all']
468 out, err, rc = get_output_error_code(cmd)
471 out, err, rc = get_output_error_code(cmd)
469 nt.assert_equal(rc, 0, err)
472 nt.assert_equal(rc, 0, err)
470 nt.assert_not_in("Traceback", err)
473 nt.assert_not_in("Traceback", err)
471 nt.assert_in("Options", out)
474 nt.assert_in("Options", out)
472 nt.assert_in("Class", out)
475 nt.assert_in("Class", out)
473 return out, err
476 return out, err
474
477
General Comments 0
You need to be logged in to leave comments. Login now