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