##// END OF EJS Templates
ipexec: prevent output coloring under PyCharm...
Nikita Kniazev -
Show More
@@ -1,472 +1,474 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 180 fname : str
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 203 env = os.environ.copy()
204 204 # FIXME: ignore all warnings in ipexec while we have shims
205 205 # should we keep suppressing warnings here, even after removing shims?
206 206 env['PYTHONWARNINGS'] = 'ignore'
207 207 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
208 # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
209 env.pop("PYCHARM_HOSTED", None)
208 210 for k, v in env.items():
209 211 # Debug a bizarre failure we've seen on Windows:
210 212 # TypeError: environment can only contain strings
211 213 if not isinstance(v, str):
212 214 print(k, v)
213 215 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
214 216 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
215 217 out, err = py3compat.decode(out), py3compat.decode(err)
216 218 # `import readline` causes 'ESC[?1034h' to be output sometimes,
217 219 # so strip that out before doing comparisons
218 220 if out:
219 221 out = re.sub(r'\x1b\[[^h]+h', '', out)
220 222 return out, err
221 223
222 224
223 225 def ipexec_validate(fname, expected_out, expected_err='',
224 226 options=None, commands=()):
225 227 """Utility to call 'ipython filename' and validate output/error.
226 228
227 229 This function raises an AssertionError if the validation fails.
228 230
229 231 Note that this starts IPython in a subprocess!
230 232
231 233 Parameters
232 234 ----------
233 235 fname : str
234 236 Name of the file to be executed (should have .py or .ipy extension).
235 237
236 238 expected_out : str
237 239 Expected stdout of the process.
238 240
239 241 expected_err : optional, str
240 242 Expected stderr of the process.
241 243
242 244 options : optional, list
243 245 Extra command-line flags to be passed to IPython.
244 246
245 247 Returns
246 248 -------
247 249 None
248 250 """
249 251
250 252 import nose.tools as nt
251 253
252 254 out, err = ipexec(fname, options, commands)
253 255 #print 'OUT', out # dbg
254 256 #print 'ERR', err # dbg
255 257 # If there are any errors, we must check those before stdout, as they may be
256 258 # more informative than simply having an empty stdout.
257 259 if err:
258 260 if expected_err:
259 261 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
260 262 else:
261 263 raise ValueError('Running file %r produced error: %r' %
262 264 (fname, err))
263 265 # If no errors or output on stderr was expected, match stdout
264 266 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
265 267
266 268
267 269 class TempFileMixin(unittest.TestCase):
268 270 """Utility class to create temporary Python/IPython files.
269 271
270 272 Meant as a mixin class for test cases."""
271 273
272 274 def mktmp(self, src, ext='.py'):
273 275 """Make a valid python temp file."""
274 276 fname = temp_pyfile(src, ext)
275 277 if not hasattr(self, 'tmps'):
276 278 self.tmps=[]
277 279 self.tmps.append(fname)
278 280 self.fname = fname
279 281
280 282 def tearDown(self):
281 283 # If the tmpfile wasn't made because of skipped tests, like in
282 284 # win32, there's nothing to cleanup.
283 285 if hasattr(self, 'tmps'):
284 286 for fname in self.tmps:
285 287 # If the tmpfile wasn't made because of skipped tests, like in
286 288 # win32, there's nothing to cleanup.
287 289 try:
288 290 os.unlink(fname)
289 291 except:
290 292 # On Windows, even though we close the file, we still can't
291 293 # delete it. I have no clue why
292 294 pass
293 295
294 296 def __enter__(self):
295 297 return self
296 298
297 299 def __exit__(self, exc_type, exc_value, traceback):
298 300 self.tearDown()
299 301
300 302
301 303 pair_fail_msg = ("Testing {0}\n\n"
302 304 "In:\n"
303 305 " {1!r}\n"
304 306 "Expected:\n"
305 307 " {2!r}\n"
306 308 "Got:\n"
307 309 " {3!r}\n")
308 310 def check_pairs(func, pairs):
309 311 """Utility function for the common case of checking a function with a
310 312 sequence of input/output pairs.
311 313
312 314 Parameters
313 315 ----------
314 316 func : callable
315 317 The function to be tested. Should accept a single argument.
316 318 pairs : iterable
317 319 A list of (input, expected_output) tuples.
318 320
319 321 Returns
320 322 -------
321 323 None. Raises an AssertionError if any output does not match the expected
322 324 value.
323 325 """
324 326 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
325 327 for inp, expected in pairs:
326 328 out = func(inp)
327 329 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
328 330
329 331
330 332 MyStringIO = StringIO
331 333
332 334 _re_type = type(re.compile(r''))
333 335
334 336 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
335 337 -------
336 338 {2!s}
337 339 -------
338 340 """
339 341
340 342 class AssertPrints(object):
341 343 """Context manager for testing that code prints certain text.
342 344
343 345 Examples
344 346 --------
345 347 >>> with AssertPrints("abc", suppress=False):
346 348 ... print("abcd")
347 349 ... print("def")
348 350 ...
349 351 abcd
350 352 def
351 353 """
352 354 def __init__(self, s, channel='stdout', suppress=True):
353 355 self.s = s
354 356 if isinstance(self.s, (str, _re_type)):
355 357 self.s = [self.s]
356 358 self.channel = channel
357 359 self.suppress = suppress
358 360
359 361 def __enter__(self):
360 362 self.orig_stream = getattr(sys, self.channel)
361 363 self.buffer = MyStringIO()
362 364 self.tee = Tee(self.buffer, channel=self.channel)
363 365 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
364 366
365 367 def __exit__(self, etype, value, traceback):
366 368 try:
367 369 if value is not None:
368 370 # If an error was raised, don't check anything else
369 371 return False
370 372 self.tee.flush()
371 373 setattr(sys, self.channel, self.orig_stream)
372 374 printed = self.buffer.getvalue()
373 375 for s in self.s:
374 376 if isinstance(s, _re_type):
375 377 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
376 378 else:
377 379 assert s in printed, notprinted_msg.format(s, self.channel, printed)
378 380 return False
379 381 finally:
380 382 self.tee.close()
381 383
382 384 printed_msg = """Found {0!r} in printed output (on {1}):
383 385 -------
384 386 {2!s}
385 387 -------
386 388 """
387 389
388 390 class AssertNotPrints(AssertPrints):
389 391 """Context manager for checking that certain output *isn't* produced.
390 392
391 393 Counterpart of AssertPrints"""
392 394 def __exit__(self, etype, value, traceback):
393 395 try:
394 396 if value is not None:
395 397 # If an error was raised, don't check anything else
396 398 self.tee.close()
397 399 return False
398 400 self.tee.flush()
399 401 setattr(sys, self.channel, self.orig_stream)
400 402 printed = self.buffer.getvalue()
401 403 for s in self.s:
402 404 if isinstance(s, _re_type):
403 405 assert not s.search(printed),printed_msg.format(
404 406 s.pattern, self.channel, printed)
405 407 else:
406 408 assert s not in printed, printed_msg.format(
407 409 s, self.channel, printed)
408 410 return False
409 411 finally:
410 412 self.tee.close()
411 413
412 414 @contextmanager
413 415 def mute_warn():
414 416 from IPython.utils import warn
415 417 save_warn = warn.warn
416 418 warn.warn = lambda *a, **kw: None
417 419 try:
418 420 yield
419 421 finally:
420 422 warn.warn = save_warn
421 423
422 424 @contextmanager
423 425 def make_tempfile(name):
424 426 """ Create an empty, named, temporary file for the duration of the context.
425 427 """
426 428 open(name, 'w').close()
427 429 try:
428 430 yield
429 431 finally:
430 432 os.unlink(name)
431 433
432 434 def fake_input(inputs):
433 435 """Temporarily replace the input() function to return the given values
434 436
435 437 Use as a context manager:
436 438
437 439 with fake_input(['result1', 'result2']):
438 440 ...
439 441
440 442 Values are returned in order. If input() is called again after the last value
441 443 was used, EOFError is raised.
442 444 """
443 445 it = iter(inputs)
444 446 def mock_input(prompt=''):
445 447 try:
446 448 return next(it)
447 449 except StopIteration as e:
448 450 raise EOFError('No more inputs given') from e
449 451
450 452 return patch('builtins.input', mock_input)
451 453
452 454 def help_output_test(subcommand=''):
453 455 """test that `ipython [subcommand] -h` works"""
454 456 cmd = get_ipython_cmd() + [subcommand, '-h']
455 457 out, err, rc = get_output_error_code(cmd)
456 458 nt.assert_equal(rc, 0, err)
457 459 nt.assert_not_in("Traceback", err)
458 460 nt.assert_in("Options", out)
459 461 nt.assert_in("--help-all", out)
460 462 return out, err
461 463
462 464
463 465 def help_all_output_test(subcommand=''):
464 466 """test that `ipython [subcommand] --help-all` works"""
465 467 cmd = get_ipython_cmd() + [subcommand, '--help-all']
466 468 out, err, rc = get_output_error_code(cmd)
467 469 nt.assert_equal(rc, 0, err)
468 470 nt.assert_not_in("Traceback", err)
469 471 nt.assert_in("Options", out)
470 472 nt.assert_in("Class", out)
471 473 return out, err
472 474
General Comments 0
You need to be logged in to leave comments. Login now