tools.py
320 lines
| 9.6 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r1955 | """Generic testing tools that do NOT depend on Twisted. | |
In particular, this module exposes a set of top-level assert* functions that | |||
can be used in place of nose.tools.assert* in method generators (the ones in | |||
nose can not, at least as of nose 0.10.4). | |||
Note: our testing package contains testing.util, which does depend on Twisted | |||
and provides utilities for tests that manage Deferreds. All testing support | |||
tools that only depend on nose, IPython or the standard library should go here | |||
instead. | |||
Authors | |||
------- | |||
- Fernando Perez <Fernando.Perez@berkeley.edu> | |||
""" | |||
Brian Granger
|
r2498 | from __future__ import absolute_import | |
#----------------------------------------------------------------------------- | |||
# 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
|
r4214 | from contextlib import contextmanager | |
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 | |
Brian Granger
|
r2498 | from IPython.utils.process import find_cmd, getoutputerror | |
from IPython.utils.text import list_strings | |||
from IPython.utils.io import temp_pyfile | |||
Fernando Perez
|
r1955 | ||
Fernando Perez
|
r2461 | from . import decorators as dec | |
MinRK
|
r3905 | from . import skipdoctest | |
Fernando Perez
|
r2461 | ||
Fernando Perez
|
r1955 | #----------------------------------------------------------------------------- | |
# Globals | |||
#----------------------------------------------------------------------------- | |||
# Make a bunch of nose.tools assert wrappers that can be used in test | |||
# generators. This will expose an assert* function for each one in nose.tools. | |||
_tpl = """ | |||
def %(name)s(*a,**kw): | |||
return nt.%(name)s(*a,**kw) | |||
""" | |||
Fernando Perez
|
r2442 | if has_nose: | |
for _x in [a for a in dir(nt) if a.startswith('assert')]: | |||
exec _tpl % dict(name=_x) | |||
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 | |||
used with a script's __file__ variable as startPath. The base of startPath | |||
is then prepended to all the listed files, forming the output list. | |||
Brian Granger
|
r1973 | Parameters | |
---------- | |||
Fernando Perez
|
r1955 | startPath : string | |
Initial path to use as the base for the results. This path is split | |||
using os.path.split() and only its first component is kept. | |||
files : string or list | |||
One or more files. | |||
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'] | |||
If a single file is given, the output is still a list: | |||
>>> full_path('/foo','a.txt') | |||
['/a.txt'] | |||
""" | |||
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:: | |||
'FAILED (errors=1)' | |||
'FAILED (failures=1)' | |||
'FAILED (errors=1, failures=1)' | |||
Returns | |||
------- | |||
nerr, nfail: number of errors and failures. | |||
""" | |||
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 | |||
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 | |||
# 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 | |||
Thomas Kluyver
|
r3712 | config.HistoryManager.hist_file = u'test_hist.sqlite' | |
config.HistoryManager.db_cache_size = 10000 | |||
Brian Granger
|
r2499 | return config | |
Fernando Perez
|
r2414 | ||
Fernando Perez
|
r2415 | def ipexec(fname, options=None): | |
Fernando Perez
|
r2414 | """Utility to call 'ipython filename'. | |
Starts IPython witha minimal and safe configuration to make startup as fast | |||
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. | |||
Fernando Perez
|
r2414 | Returns | |
------- | |||
(stdout, stderr) of ipython subprocess. | |||
Fernando Perez
|
r2415 | """ | |
if options is None: options = [] | |||
Fernando Perez
|
r2477 | ||
# For these subprocess calls, eliminate all prompt printing so we only see | |||
# output from script execution | |||
MinRK
|
r4197 | prompt_opts = [ '--InteractiveShell.prompt_in1=""', | |
'--InteractiveShell.prompt_in2=""', | |||
'--InteractiveShell.prompt_out=""' | |||
MinRK
|
r4029 | ] | |
Fernando Perez
|
r2477 | cmdargs = ' '.join(default_argv() + prompt_opts + options) | |
Fernando Perez
|
r2415 | ||
Fernando Perez
|
r2414 | _ip = get_ipython() | |
test_dir = os.path.dirname(__file__) | |||
Fernando Perez
|
r2481 | ||
Brian Granger
|
r2507 | ipython_cmd = find_cmd('ipython') | |
Fernando Perez
|
r2446 | # Absolute path for filename | |
Brian Granger
|
r2507 | full_fname = os.path.join(test_dir, fname) | |
Fernando Perez
|
r2494 | full_cmd = '%s %s %s' % (ipython_cmd, cmdargs, full_fname) | |
#print >> sys.stderr, 'FULL CMD:', full_cmd # dbg | |||
Brian Granger
|
r2498 | return getoutputerror(full_cmd) | |
Fernando Perez
|
r2414 | ||
Fernando Perez
|
r2494 | def ipexec_validate(fname, expected_out, expected_err='', | |
Fernando Perez
|
r2415 | options=None): | |
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 | |
Fernando Perez
|
r2414 | out, err = ipexec(fname) | |
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: | |||
nt.assert_equals(err.strip(), expected_err.strip()) | |||
else: | |||
raise ValueError('Running file %r produced error: %r' % | |||
(fname, err)) | |||
# If no errors or output on stderr was expected, match stdout | |||
Fernando Perez
|
r2414 | nt.assert_equals(out.strip(), expected_out.strip()) | |
Fernando Perez
|
r2415 | ||
class TempFileMixin(object): | |||
"""Utility class to create temporary Python/IPython files. | |||
Meant as a mixin class for test cases.""" | |||
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
|
r4079 | pair_fail_msg = ("Testing function {0}\n\n" | |
"In:\n" | |||
" {1!r}\n" | |||
"Expected:\n" | |||
" {2!r}\n" | |||
"Got:\n" | |||
" {3!r}\n") | |||
def check_pairs(func, pairs): | |||
"""Utility function for the common case of checking a function with a | |||
sequence of input/output pairs. | |||
Parameters | |||
---------- | |||
func : callable | |||
The function to be tested. Should accept a single argument. | |||
pairs : iterable | |||
A list of (input, expected_output) tuples. | |||
Returns | |||
------- | |||
None. Raises an AssertionError if any output does not match the expected | |||
value. | |||
""" | |||
for inp, expected in pairs: | |||
out = func(inp) | |||
assert out == expected, pair_fail_msg.format(func.func_name, inp, expected, out) | |||
MinRK
|
r4214 | ||
@contextmanager | |||
def mute_warn(): | |||
from IPython.utils import warn | |||
save_warn = warn.warn | |||
warn.warn = lambda *a, **kw: None | |||
try: | |||
yield | |||
finally: | |||
warn.warn = save_warn |