tools.py
490 lines
| 14.8 KiB
| text/x-python
|
PythonLexer
MinRK
|
r6421 | """Generic testing tools. | ||
Fernando Perez
|
r1955 | |||
Authors | ||||
------- | ||||
- Fernando Perez <Fernando.Perez@berkeley.edu> | ||||
""" | ||||
Brian Granger
|
r2498 | from __future__ import absolute_import | ||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r12354 | # Copyright (C) 2009 The IPython Development Team | ||
Fernando Perez
|
r1955 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
Brian Granger
|
r2498 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r1955 | |||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2498 | # Imports | ||
Fernando Perez
|
r1955 | #----------------------------------------------------------------------------- | ||
import os | ||||
Fernando Perez
|
r2353 | import re | ||
Fernando Perez
|
r1955 | import sys | ||
MinRK
|
r4486 | import tempfile | ||
Fernando Perez
|
r1955 | |||
MinRK
|
r4214 | from contextlib import contextmanager | ||
Thomas Kluyver
|
r4901 | from io import StringIO | ||
MinRK
|
r11475 | from subprocess import Popen, PIPE | ||
MinRK
|
r4214 | |||
Fernando Perez
|
r2442 | try: | ||
# These tools are used by parts of the runtime, so we make the nose | ||||
# dependency optional at this point. Nose is a hard dependency to run the | ||||
# test suite, but NOT to use ipython itself. | ||||
import nose.tools as nt | ||||
has_nose = True | ||||
except ImportError: | ||||
has_nose = False | ||||
Fernando Perez
|
r1955 | |||
Brian Granger
|
r2499 | from IPython.config.loader import Config | ||
MinRK
|
r12354 | from IPython.utils.process import get_output_error_code | ||
Brandon Parsons
|
r6653 | from IPython.utils.text import list_strings | ||
Thomas Kluyver
|
r4901 | from IPython.utils.io import temp_pyfile, Tee | ||
from IPython.utils import py3compat | ||||
Brandon Parsons
|
r6716 | from IPython.utils.encoding import DEFAULT_ENCODING | ||
Fernando Perez
|
r1955 | |||
Fernando Perez
|
r2461 | from . import decorators as dec | ||
MinRK
|
r3905 | from . import skipdoctest | ||
Fernando Perez
|
r2461 | |||
Fernando Perez
|
r1955 | #----------------------------------------------------------------------------- | ||
# Functions and classes | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2461 | # The docstring for full_path doctests differently on win32 (different path | ||
# separator) so just skip the doctest there. The example remains informative. | ||||
MinRK
|
r3905 | doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco | ||
Brian Granger
|
r1982 | |||
Fernando Perez
|
r2461 | @doctest_deco | ||
Fernando Perez
|
r1955 | def full_path(startPath,files): | ||
"""Make full paths for all the listed files, based on startPath. | ||||
Only the base part of startPath is kept, since this routine is typically | ||||
Thomas Kluyver
|
r13592 | used with a script's ``__file__`` variable as startPath. The base of startPath | ||
Fernando Perez
|
r1955 | is then prepended to all the listed files, forming the output list. | ||
Brian Granger
|
r1973 | Parameters | ||
---------- | ||||
Thomas Kluyver
|
r13592 | startPath : string | ||
Initial path to use as the base for the results. This path is split | ||||
Fernando Perez
|
r1955 | using os.path.split() and only its first component is kept. | ||
Thomas Kluyver
|
r13592 | files : string or list | ||
One or more files. | ||||
Fernando Perez
|
r1955 | |||
Brian Granger
|
r1973 | Examples | ||
-------- | ||||
Fernando Perez
|
r1955 | |||
>>> full_path('/foo/bar.py',['a.txt','b.txt']) | ||||
['/foo/a.txt', '/foo/b.txt'] | ||||
>>> full_path('/foo',['a.txt','b.txt']) | ||||
['/a.txt', '/b.txt'] | ||||
Thomas Kluyver
|
r13592 | If a single file is given, the output is still a list:: | ||
>>> full_path('/foo','a.txt') | ||||
['/a.txt'] | ||||
Fernando Perez
|
r1955 | """ | ||
Brian Granger
|
r2498 | files = list_strings(files) | ||
Fernando Perez
|
r1955 | base = os.path.split(startPath)[0] | ||
return [ os.path.join(base,f) for f in files ] | ||||
Fernando Perez
|
r2353 | |||
def parse_test_output(txt): | ||||
"""Parse the output of a test run and return errors, failures. | ||||
Parameters | ||||
---------- | ||||
txt : str | ||||
Text output of a test run, assumed to contain a line of one of the | ||||
following forms:: | ||||
Thomas Kluyver
|
r9244 | |||
Fernando Perez
|
r2353 | 'FAILED (errors=1)' | ||
'FAILED (failures=1)' | ||||
'FAILED (errors=1, failures=1)' | ||||
Returns | ||||
------- | ||||
Thomas Kluyver
|
r13592 | nerr, nfail | ||
number of errors and failures. | ||||
Fernando Perez
|
r2353 | """ | ||
err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE) | ||||
if err_m: | ||||
nerr = int(err_m.group(1)) | ||||
nfail = 0 | ||||
return nerr, nfail | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2353 | fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE) | ||
if fail_m: | ||||
nerr = 0 | ||||
nfail = int(fail_m.group(1)) | ||||
return nerr, nfail | ||||
both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt, | ||||
re.MULTILINE) | ||||
if both_m: | ||||
nerr = int(both_m.group(1)) | ||||
nfail = int(both_m.group(2)) | ||||
return nerr, nfail | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2353 | # If the input didn't match any of these forms, assume no error/failures | ||
return 0, 0 | ||||
Fernando Perez
|
r2414 | |||
Fernando Perez
|
r2353 | # So nose doesn't think this is a test | ||
parse_test_output.__test__ = False | ||||
Fernando Perez
|
r2414 | |||
def default_argv(): | ||||
"""Return a valid default argv for creating testing instances of ipython""" | ||||
Fernando Perez
|
r2477 | return ['--quick', # so no config file is loaded | ||
# Other defaults to minimize side effects on stdout | ||||
MinRK
|
r4197 | '--colors=NoColor', '--no-term-title','--no-banner', | ||
'--autocall=0'] | ||||
Brian Granger
|
r2499 | |||
def default_config(): | ||||
"""Return a config object with good defaults for testing.""" | ||||
config = Config() | ||||
Brian Granger
|
r2761 | config.TerminalInteractiveShell.colors = 'NoColor' | ||
config.TerminalTerminalInteractiveShell.term_title = False, | ||||
config.TerminalInteractiveShell.autocall = 0 | ||||
Julian Taylor
|
r15372 | f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False) | ||
config.HistoryManager.hist_file = f.name | ||||
f.close() | ||||
Thomas Kluyver
|
r3712 | config.HistoryManager.db_cache_size = 10000 | ||
Brian Granger
|
r2499 | return config | ||
Fernando Perez
|
r2414 | |||
Paul Ivanov
|
r11861 | def get_ipython_cmd(as_string=False): | ||
""" | ||||
Return appropriate IPython command line name. By default, this will return | ||||
a list that can be used with subprocess.Popen, for example, but passing | ||||
`as_string=True` allows for returning the IPython command as a string. | ||||
Parameters | ||||
---------- | ||||
as_string: bool | ||||
Flag to allow to return the command as a string. | ||||
""" | ||||
Thomas Kluyver
|
r12151 | ipython_cmd = [sys.executable, "-m", "IPython"] | ||
Paul Ivanov
|
r11861 | |||
if as_string: | ||||
ipython_cmd = " ".join(ipython_cmd) | ||||
return ipython_cmd | ||||
Thomas Ballinger
|
r18526 | def ipexec(fname, options=None, commands=()): | ||
Fernando Perez
|
r2414 | """Utility to call 'ipython filename'. | ||
MinRK
|
r11353 | Starts IPython with a minimal and safe configuration to make startup as fast | ||
Fernando Perez
|
r2414 | as possible. | ||
Note that this starts IPython in a subprocess! | ||||
Parameters | ||||
---------- | ||||
fname : str | ||||
Name of file to be executed (should have .py or .ipy extension). | ||||
Fernando Perez
|
r2415 | options : optional, list | ||
Extra command-line flags to be passed to IPython. | ||||
Thomas Ballinger
|
r18526 | commands : optional, list | ||
Commands to send in on stdin | ||||
Fernando Perez
|
r2414 | Returns | ||
------- | ||||
(stdout, stderr) of ipython subprocess. | ||||
Fernando Perez
|
r2415 | """ | ||
if options is None: options = [] | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2477 | # For these subprocess calls, eliminate all prompt printing so we only see | ||
# output from script execution | ||||
MinRK
|
r5548 | prompt_opts = [ '--PromptManager.in_template=""', | ||
'--PromptManager.in2_template=""', | ||||
'--PromptManager.out_template=""' | ||||
MinRK
|
r4029 | ] | ||
MinRK
|
r11475 | cmdargs = default_argv() + prompt_opts + options | ||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2414 | test_dir = os.path.dirname(__file__) | ||
Paul Ivanov
|
r11861 | |||
ipython_cmd = get_ipython_cmd() | ||||
Fernando Perez
|
r2446 | # Absolute path for filename | ||
Brian Granger
|
r2507 | full_fname = os.path.join(test_dir, fname) | ||
MinRK
|
r11475 | full_cmd = ipython_cmd + cmdargs + [full_fname] | ||
Thomas Kluyver
|
r15466 | env = os.environ.copy() | ||
Thomas Kluyver
|
r15469 | env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr | ||
Thomas Ballinger
|
r18526 | p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env) | ||
out, err = p.communicate(input=py3compat.str_to_bytes('\n'.join(commands)) or None) | ||||
MinRK
|
r11475 | out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err) | ||
MinRK
|
r6190 | # `import readline` causes 'ESC[?1034h' to be output sometimes, | ||
# so strip that out before doing comparisons | ||||
MinRK
|
r4470 | if out: | ||
MinRK
|
r6190 | out = re.sub(r'\x1b\[[^h]+h', '', out) | ||
MinRK
|
r6221 | return out, err | ||
Fernando Perez
|
r2414 | |||
Fernando Perez
|
r2494 | def ipexec_validate(fname, expected_out, expected_err='', | ||
Thomas Ballinger
|
r18526 | options=None, commands=()): | ||
Fernando Perez
|
r2414 | """Utility to call 'ipython filename' and validate output/error. | ||
This function raises an AssertionError if the validation fails. | ||||
Note that this starts IPython in a subprocess! | ||||
Parameters | ||||
---------- | ||||
fname : str | ||||
Name of the file to be executed (should have .py or .ipy extension). | ||||
expected_out : str | ||||
Expected stdout of the process. | ||||
Fernando Perez
|
r2415 | expected_err : optional, str | ||
Expected stderr of the process. | ||||
options : optional, list | ||||
Extra command-line flags to be passed to IPython. | ||||
Fernando Perez
|
r2414 | Returns | ||
------- | ||||
None | ||||
""" | ||||
Fernando Perez
|
r2442 | import nose.tools as nt | ||
Bernardo B. Marques
|
r4872 | |||
Thomas Ballinger
|
r18526 | out, err = ipexec(fname, options, commands) | ||
Fernando Perez
|
r2494 | #print 'OUT', out # dbg | ||
#print 'ERR', err # dbg | ||||
# If there are any errors, we must check those befor stdout, as they may be | ||||
# more informative than simply having an empty stdout. | ||||
if err: | ||||
if expected_err: | ||||
Jörgen Stenarson
|
r8291 | nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines())) | ||
Fernando Perez
|
r2494 | else: | ||
raise ValueError('Running file %r produced error: %r' % | ||||
(fname, err)) | ||||
# If no errors or output on stderr was expected, match stdout | ||||
Jörgen Stenarson
|
r8288 | nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines())) | ||
Fernando Perez
|
r2415 | |||
class TempFileMixin(object): | ||||
"""Utility class to create temporary Python/IPython files. | ||||
Meant as a mixin class for test cases.""" | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2415 | def mktmp(self, src, ext='.py'): | ||
"""Make a valid python temp file.""" | ||||
fname, f = temp_pyfile(src, ext) | ||||
self.tmpfile = f | ||||
self.fname = fname | ||||
Fernando Perez
|
r2907 | def tearDown(self): | ||
Fernando Perez
|
r2446 | if hasattr(self, 'tmpfile'): | ||
# If the tmpfile wasn't made because of skipped tests, like in | ||||
# win32, there's nothing to cleanup. | ||||
self.tmpfile.close() | ||||
try: | ||||
os.unlink(self.fname) | ||||
except: | ||||
# On Windows, even though we close the file, we still can't | ||||
# delete it. I have no clue why | ||||
pass | ||||
Fernando Perez
|
r2415 | |||
Thomas Kluyver
|
r4746 | pair_fail_msg = ("Testing {0}\n\n" | ||
Thomas Kluyver
|
r4079 | "In:\n" | ||
" {1!r}\n" | ||||
"Expected:\n" | ||||
" {2!r}\n" | ||||
"Got:\n" | ||||
" {3!r}\n") | ||||
def check_pairs(func, pairs): | ||||
Bernardo B. Marques
|
r4872 | """Utility function for the common case of checking a function with a | ||
Thomas Kluyver
|
r4079 | sequence of input/output pairs. | ||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r4079 | Parameters | ||
---------- | ||||
func : callable | ||||
The function to be tested. Should accept a single argument. | ||||
pairs : iterable | ||||
A list of (input, expected_output) tuples. | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r4079 | Returns | ||
------- | ||||
None. Raises an AssertionError if any output does not match the expected | ||||
value. | ||||
""" | ||||
Thomas Kluyver
|
r4746 | name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>")) | ||
Thomas Kluyver
|
r4079 | for inp, expected in pairs: | ||
out = func(inp) | ||||
Thomas Kluyver
|
r4746 | assert out == expected, pair_fail_msg.format(name, inp, expected, out) | ||
MinRK
|
r4214 | |||
Thomas Kluyver
|
r4920 | |||
Thomas Kluyver
|
r4901 | if py3compat.PY3: | ||
MyStringIO = StringIO | ||||
else: | ||||
# In Python 2, stdout/stderr can have either bytes or unicode written to them, | ||||
# so we need a class that can handle both. | ||||
class MyStringIO(StringIO): | ||||
def write(self, s): | ||||
Brandon Parsons
|
r6716 | s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING) | ||
Thomas Kluyver
|
r4901 | super(MyStringIO, self).write(s) | ||
MinRK
|
r15219 | _re_type = type(re.compile(r'')) | ||
Thomas Kluyver
|
r4901 | notprinted_msg = """Did not find {0!r} in printed output (on {1}): | ||
Thomas Kluyver
|
r8099 | ------- | ||
{2!s} | ||||
------- | ||||
""" | ||||
Thomas Kluyver
|
r4920 | |||
Thomas Kluyver
|
r4901 | class AssertPrints(object): | ||
"""Context manager for testing that code prints certain text. | ||||
Examples | ||||
-------- | ||||
Thomas Kluyver
|
r4904 | >>> with AssertPrints("abc", suppress=False): | ||
Thomas Kluyver
|
r13393 | ... print("abcd") | ||
... print("def") | ||||
Thomas Kluyver
|
r4901 | ... | ||
abcd | ||||
def | ||||
""" | ||||
Thomas Kluyver
|
r4904 | def __init__(self, s, channel='stdout', suppress=True): | ||
Thomas Kluyver
|
r4901 | self.s = s | ||
MinRK
|
r15219 | if isinstance(self.s, (py3compat.string_types, _re_type)): | ||
Thomas Kluyver
|
r12543 | self.s = [self.s] | ||
Thomas Kluyver
|
r4901 | self.channel = channel | ||
Thomas Kluyver
|
r4904 | self.suppress = suppress | ||
Thomas Kluyver
|
r4901 | |||
def __enter__(self): | ||||
self.orig_stream = getattr(sys, self.channel) | ||||
self.buffer = MyStringIO() | ||||
self.tee = Tee(self.buffer, channel=self.channel) | ||||
Thomas Kluyver
|
r4904 | setattr(sys, self.channel, self.buffer if self.suppress else self.tee) | ||
Thomas Kluyver
|
r4901 | |||
def __exit__(self, etype, value, traceback): | ||||
Scott Sanderson
|
r17801 | try: | ||
if value is not None: | ||||
# If an error was raised, don't check anything else | ||||
return False | ||||
self.tee.flush() | ||||
setattr(sys, self.channel, self.orig_stream) | ||||
printed = self.buffer.getvalue() | ||||
for s in self.s: | ||||
if isinstance(s, _re_type): | ||||
assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed) | ||||
else: | ||||
assert s in printed, notprinted_msg.format(s, self.channel, printed) | ||||
Thomas Kluyver
|
r12950 | return False | ||
Scott Sanderson
|
r17801 | finally: | ||
self.tee.close() | ||||
Thomas Kluyver
|
r8099 | |||
printed_msg = """Found {0!r} in printed output (on {1}): | ||||
------- | ||||
{2!s} | ||||
------- | ||||
""" | ||||
Thomas Kluyver
|
r4904 | class AssertNotPrints(AssertPrints): | ||
"""Context manager for checking that certain output *isn't* produced. | ||||
Counterpart of AssertPrints""" | ||||
def __exit__(self, etype, value, traceback): | ||||
Scott Sanderson
|
r17801 | try: | ||
if value is not None: | ||||
# If an error was raised, don't check anything else | ||||
self.tee.close() | ||||
return False | ||||
self.tee.flush() | ||||
setattr(sys, self.channel, self.orig_stream) | ||||
printed = self.buffer.getvalue() | ||||
for s in self.s: | ||||
if isinstance(s, _re_type): | ||||
assert not s.search(printed),printed_msg.format( | ||||
s.pattern, self.channel, printed) | ||||
else: | ||||
assert s not in printed, printed_msg.format( | ||||
s, self.channel, printed) | ||||
Thomas Kluyver
|
r12950 | return False | ||
Scott Sanderson
|
r17801 | finally: | ||
self.tee.close() | ||||
Thomas Kluyver
|
r4901 | |||
MinRK
|
r4214 | @contextmanager | ||
def mute_warn(): | ||||
from IPython.utils import warn | ||||
save_warn = warn.warn | ||||
warn.warn = lambda *a, **kw: None | ||||
try: | ||||
yield | ||||
finally: | ||||
Robert Kern
|
r4688 | warn.warn = save_warn | ||
@contextmanager | ||||
def make_tempfile(name): | ||||
""" Create an empty, named, temporary file for the duration of the context. | ||||
""" | ||||
f = open(name, 'w') | ||||
f.close() | ||||
try: | ||||
yield | ||||
finally: | ||||
os.unlink(name) | ||||
Takafumi Arakaki
|
r7858 | |||
@contextmanager | ||||
def monkeypatch(obj, name, attr): | ||||
""" | ||||
Context manager to replace attribute named `name` in `obj` with `attr`. | ||||
""" | ||||
orig = getattr(obj, name) | ||||
setattr(obj, name, attr) | ||||
yield | ||||
setattr(obj, name, orig) | ||||
MinRK
|
r12354 | |||
def help_output_test(subcommand=''): | ||||
"""test that `ipython [subcommand] -h` works""" | ||||
Thomas Kluyver
|
r13739 | cmd = get_ipython_cmd() + [subcommand, '-h'] | ||
MinRK
|
r12354 | out, err, rc = get_output_error_code(cmd) | ||
nt.assert_equal(rc, 0, err) | ||||
nt.assert_not_in("Traceback", err) | ||||
nt.assert_in("Options", out) | ||||
nt.assert_in("--help-all", out) | ||||
return out, err | ||||
def help_all_output_test(subcommand=''): | ||||
"""test that `ipython [subcommand] --help-all` works""" | ||||
Thomas Kluyver
|
r13739 | cmd = get_ipython_cmd() + [subcommand, '--help-all'] | ||
MinRK
|
r12354 | out, err, rc = get_output_error_code(cmd) | ||
nt.assert_equal(rc, 0, err) | ||||
nt.assert_not_in("Traceback", err) | ||||
nt.assert_in("Options", out) | ||||
nt.assert_in("Class parameters", out) | ||||
return out, err | ||||
MinRK
|
r18248 | def assert_big_text_equal(a, b, chunk_size=80): | ||
"""assert that large strings are equal | ||||
Zooms in on first chunk that differs, | ||||
to give better info than vanilla assertEqual for large text blobs. | ||||
""" | ||||
for i in range(0, len(a), chunk_size): | ||||
chunk_a = a[i:i + chunk_size] | ||||
chunk_b = b[i:i + chunk_size] | ||||
nt.assert_equal(chunk_a, chunk_b, "[offset: %i]\n%r != \n%r" % ( | ||||
i, chunk_a, chunk_b)) | ||||
if len(a) > len(b): | ||||
nt.fail("Length doesn't match (%i > %i). Extra text:\n%r" % ( | ||||
len(a), len(b), a[len(b):] | ||||
)) | ||||
elif len(a) < len(b): | ||||
nt.fail("Length doesn't match (%i < %i). Extra text:\n%r" % ( | ||||
len(a), len(b), b[len(a):] | ||||
)) | ||||