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