remove pylib things and move things to new pytest namespace
--HG-- branch : trunk
This commit is contained in:
1
pytest/plugin/__init__.py
Normal file
1
pytest/plugin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
211
pytest/plugin/hookspec.py
Normal file
211
pytest/plugin/hookspec.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
hook specifications for py.test plugins
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Command line and configuration
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_namespace():
|
||||
"return dict of name->object which will get stored at py.test. namespace"
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"add optparse-style options via parser.addoption."
|
||||
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"add hooks via pluginmanager.registerhooks(module)"
|
||||
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed.
|
||||
and all plugins and initial conftest files been loaded.
|
||||
"""
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
""" called for performing the main (cmdline) action. """
|
||||
pytest_cmdline_main.firstresult = True
|
||||
|
||||
def pytest_runtest_mainloop(session):
|
||||
""" called for performing the main runtest loop (after collection. """
|
||||
pytest_runtest_mainloop.firstresult = True
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# collection hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_perform_collection(session):
|
||||
""" perform the collection protocol for the given session. """
|
||||
pytest_perform_collection.firstresult = True
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
""" called to allow filtering and selecting of test items (inplace). """
|
||||
|
||||
def pytest_log_finishcollection(collection):
|
||||
""" called after collection has finished. """
|
||||
|
||||
def pytest_ignore_collect(path, config):
|
||||
""" return true value to prevent considering this path for collection.
|
||||
This hook is consulted for all files and directories prior to considering
|
||||
collection hooks.
|
||||
"""
|
||||
pytest_ignore_collect.firstresult = True
|
||||
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" return Collection node or None for the given path. """
|
||||
pytest_collect_directory.firstresult = True
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
""" return Collection node or None for the given path. """
|
||||
|
||||
# logging hooks for collection
|
||||
def pytest_collectstart(collector):
|
||||
""" collector starts collecting. """
|
||||
|
||||
def pytest_log_itemcollect(item):
|
||||
""" we just collected a test item. """
|
||||
|
||||
def pytest_collectreport(report):
|
||||
""" collector finished collecting. """
|
||||
|
||||
def pytest_deselected(items):
|
||||
""" called for test items deselected by keyword. """
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
""" perform a collection and return a collection. """
|
||||
pytest_make_collect_report.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Python test function related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
""" return a Module collector or None for the given path.
|
||||
This hook will be called for each matching test module path.
|
||||
The pytest_collect_file hook needs to be used if you want to
|
||||
create test modules for files that do not match as a test module.
|
||||
"""
|
||||
pytest_pycollect_makemodule.firstresult = True
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
""" return custom item/collector for a python object in a module, or None. """
|
||||
pytest_pycollect_makeitem.firstresult = True
|
||||
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
""" call underlying test function. """
|
||||
pytest_pyfunc_call.firstresult = True
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
""" generate (multiple) parametrized calls to a test function."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
def pytest_itemstart(item, node=None):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
|
||||
def pytest_runtest_protocol(item):
|
||||
""" implement fixture, run and report about the given test item. """
|
||||
pytest_runtest_protocol.firstresult = True
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location, fspath):
|
||||
""" signal the start of a test run. """
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
""" called before pytest_runtest_call(). """
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
""" execute test item. """
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
""" called after pytest_runtest_call(). """
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
""" make a test report for the given item and call outcome. """
|
||||
pytest_runtest_makereport.firstresult = True
|
||||
|
||||
def pytest_runtest_logreport(report):
|
||||
""" process item test report. """
|
||||
|
||||
# special handling for final teardown - somewhat internal for now
|
||||
def pytest__teardown_final(session):
|
||||
""" called before test session finishes. """
|
||||
pytest__teardown_final.firstresult = True
|
||||
|
||||
def pytest__teardown_final_logerror(report, session):
|
||||
""" called if runtest_teardown_final failed. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# test session related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for customising the assert methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_assertrepr_compare(config, op, left, right):
|
||||
"""return explanation for comparisons in failing assert expressions.
|
||||
|
||||
Return None for no custom explanation, otherwise return a list
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented sligthly, the intention is for the first line to be a summary.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for influencing reporting (invoked from pytest_terminal)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_report_header(config):
|
||||
""" return a string to be displayed as header info for terminal reporting."""
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
""" return result-category, shortletter and verbose word for reporting."""
|
||||
pytest_report_teststatus.firstresult = True
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
""" add additional section in terminal summary reporting. """
|
||||
|
||||
def pytest_report_iteminfo(item):
|
||||
""" return (fspath, lineno, domainpath) location info for the item.
|
||||
the information is used for result display and to sort tests.
|
||||
fspath,lineno: file and linenumber of source of item definition.
|
||||
domainpath: custom id - e.g. for python: dotted import address
|
||||
"""
|
||||
pytest_report_iteminfo.firstresult = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_doctest_prepare_content(content):
|
||||
""" return processed content for a given doctest"""
|
||||
pytest_doctest_prepare_content.firstresult = True
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# error handling and internal debugging hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new py lib plugin got registered. """
|
||||
|
||||
def pytest_plugin_unregistered(plugin):
|
||||
""" a py lib plugin got unregistered. """
|
||||
|
||||
def pytest_internalerror(excrepr):
|
||||
""" called for internal errors. """
|
||||
|
||||
def pytest_keyboard_interrupt(excinfo):
|
||||
""" called for keyboard interrupt. """
|
||||
|
||||
def pytest_trace(category, msg):
|
||||
""" called for debug info. """
|
||||
123
pytest/plugin/pytest__pytest.py
Normal file
123
pytest/plugin/pytest__pytest.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import py
|
||||
|
||||
from pytest.pluginmanager import HookRelay
|
||||
|
||||
def pytest_funcarg___pytest(request):
|
||||
return PytestArg(request)
|
||||
|
||||
class PytestArg:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def gethookrecorder(self, hook):
|
||||
hookrecorder = HookRecorder(hook._registry)
|
||||
hookrecorder.start_recording(hook._hookspecs)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
class ParsedCall:
|
||||
def __init__(self, name, locals):
|
||||
assert '_name' not in locals
|
||||
self.__dict__.update(locals)
|
||||
self.__dict__.pop('self')
|
||||
self._name = name
|
||||
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
del d['_name']
|
||||
return "<ParsedCall %r(**%r)>" %(self._name, d)
|
||||
|
||||
class HookRecorder:
|
||||
def __init__(self, registry):
|
||||
self._registry = registry
|
||||
self.calls = []
|
||||
self._recorders = {}
|
||||
|
||||
def start_recording(self, hookspecs):
|
||||
if not isinstance(hookspecs, (list, tuple)):
|
||||
hookspecs = [hookspecs]
|
||||
for hookspec in hookspecs:
|
||||
assert hookspec not in self._recorders
|
||||
class RecordCalls:
|
||||
_recorder = self
|
||||
for name, method in vars(hookspec).items():
|
||||
if name[0] != "_":
|
||||
setattr(RecordCalls, name, self._makecallparser(method))
|
||||
recorder = RecordCalls()
|
||||
self._recorders[hookspec] = recorder
|
||||
self._registry.register(recorder)
|
||||
self.hook = HookRelay(hookspecs, registry=self._registry,
|
||||
prefix="pytest_")
|
||||
|
||||
def finish_recording(self):
|
||||
for recorder in self._recorders.values():
|
||||
self._registry.unregister(recorder)
|
||||
self._recorders.clear()
|
||||
|
||||
def _makecallparser(self, method):
|
||||
name = method.__name__
|
||||
args, varargs, varkw, default = py.std.inspect.getargspec(method)
|
||||
if not args or args[0] != "self":
|
||||
args.insert(0, 'self')
|
||||
fspec = py.std.inspect.formatargspec(args, varargs, varkw, default)
|
||||
# we use exec because we want to have early type
|
||||
# errors on wrong input arguments, using
|
||||
# *args/**kwargs delays this and gives errors
|
||||
# elsewhere
|
||||
exec (py.code.compile("""
|
||||
def %(name)s%(fspec)s:
|
||||
self._recorder.calls.append(
|
||||
ParsedCall(%(name)r, locals()))
|
||||
""" % locals()))
|
||||
return locals()[name]
|
||||
|
||||
def getcalls(self, names):
|
||||
if isinstance(names, str):
|
||||
names = names.split()
|
||||
for name in names:
|
||||
for cls in self._recorders:
|
||||
if name in vars(cls):
|
||||
break
|
||||
else:
|
||||
raise ValueError("callname %r not found in %r" %(
|
||||
name, self._recorders.keys()))
|
||||
l = []
|
||||
for call in self.calls:
|
||||
if call._name in names:
|
||||
l.append(call)
|
||||
return l
|
||||
|
||||
def contains(self, entries):
|
||||
from py.builtin import print_
|
||||
i = 0
|
||||
entries = list(entries)
|
||||
backlocals = py.std.sys._getframe(1).f_locals
|
||||
while entries:
|
||||
name, check = entries.pop(0)
|
||||
for ind, call in enumerate(self.calls[i:]):
|
||||
if call._name == name:
|
||||
print_("NAMEMATCH", name, call)
|
||||
if eval(check, backlocals, call.__dict__):
|
||||
print_("CHECKERMATCH", repr(check), "->", call)
|
||||
else:
|
||||
print_("NOCHECKERMATCH", repr(check), "-", call)
|
||||
continue
|
||||
i += ind + 1
|
||||
break
|
||||
print_("NONAMEMATCH", name, "with", call)
|
||||
else:
|
||||
raise AssertionError("could not find %r in %r" %(
|
||||
name, self.calls[i:]))
|
||||
|
||||
def popcall(self, name):
|
||||
for i, call in enumerate(self.calls):
|
||||
if call._name == name:
|
||||
del self.calls[i]
|
||||
return call
|
||||
raise ValueError("could not find call %r" %(name, ))
|
||||
|
||||
def getcall(self, name):
|
||||
l = self.getcalls(name)
|
||||
assert len(l) == 1, (name, l)
|
||||
return l[0]
|
||||
|
||||
157
pytest/plugin/pytest_assertion.py
Normal file
157
pytest/plugin/pytest_assertion.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import py
|
||||
import sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group._addoption('--no-assert', action="store_true", default=False,
|
||||
dest="noassert",
|
||||
help="disable python assert expression reinterpretation."),
|
||||
|
||||
def pytest_configure(config):
|
||||
# The _pytesthook attribute on the AssertionError is used by
|
||||
# py._code._assertionnew to detect this plugin was loaded and in
|
||||
# turn call the hooks defined here as part of the
|
||||
# DebugInterpreter.
|
||||
if not config.getvalue("noassert") and not config.getvalue("nomagic"):
|
||||
warn_about_missing_assertion()
|
||||
config._oldassertion = py.builtin.builtins.AssertionError
|
||||
config._oldbinrepr = py.code._reprcompare
|
||||
py.builtin.builtins.AssertionError = py.code._AssertionError
|
||||
def callbinrepr(op, left, right):
|
||||
hook_result = config.hook.pytest_assertrepr_compare(
|
||||
config=config, op=op, left=left, right=right)
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
return '\n~'.join(new_expl)
|
||||
py.code._reprcompare = callbinrepr
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_oldassertion'):
|
||||
py.builtin.builtins.AssertionError = config._oldassertion
|
||||
py.code._reprcompare = config._oldbinrepr
|
||||
del config._oldassertion
|
||||
del config._oldbinrepr
|
||||
|
||||
def warn_about_missing_assertion():
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
py.std.warnings.warn("Assertions are turned off!"
|
||||
" (are you using python -O?)")
|
||||
|
||||
|
||||
# Provide basestring in python3
|
||||
try:
|
||||
basestring = basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
def pytest_assertrepr_compare(op, left, right):
|
||||
"""return specialised explanations for some operators/operands"""
|
||||
left_repr = py.io.saferepr(left, maxsize=30)
|
||||
right_repr = py.io.saferepr(right, maxsize=30)
|
||||
summary = '%s %s %s' % (left_repr, op, right_repr)
|
||||
|
||||
issequence = lambda x: isinstance(x, (list, tuple))
|
||||
istext = lambda x: isinstance(x, basestring)
|
||||
isdict = lambda x: isinstance(x, dict)
|
||||
isset = lambda x: isinstance(x, set)
|
||||
|
||||
explanation = None
|
||||
try:
|
||||
if op == '==':
|
||||
if istext(left) and istext(right):
|
||||
explanation = _diff_text(left, right)
|
||||
elif issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _diff_text(py.std.pprint.pformat(left),
|
||||
py.std.pprint.pformat(right))
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
explanation = ['(pytest_assertion plugin: representation of '
|
||||
'details failed. Probably an object has a faulty __repr__.)',
|
||||
str(excinfo)
|
||||
]
|
||||
|
||||
|
||||
if not explanation:
|
||||
return None
|
||||
|
||||
# Don't include pageloads of data, should be configurable
|
||||
if len(''.join(explanation)) > 80*8:
|
||||
explanation = ['Detailed information too verbose, truncated']
|
||||
|
||||
return [summary] + explanation
|
||||
|
||||
|
||||
def _diff_text(left, right):
|
||||
"""Return the explanation for the diff between text
|
||||
|
||||
This will skip leading and trailing characters which are
|
||||
identical to keep the diff minimal.
|
||||
"""
|
||||
explanation = []
|
||||
i = 0 # just in case left or right has zero length
|
||||
for i in range(min(len(left), len(right))):
|
||||
if left[i] != right[i]:
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation = ['Skipping %s identical '
|
||||
'leading characters in diff' % i]
|
||||
left = left[i:]
|
||||
right = right[i:]
|
||||
if len(left) == len(right):
|
||||
for i in range(len(left)):
|
||||
if left[-i] != right[-i]:
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += ['Skipping %s identical '
|
||||
'trailing characters in diff' % i]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
explanation += [line.strip('\n')
|
||||
for line in py.std.difflib.ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_sequence(left, right):
|
||||
explanation = []
|
||||
for i in range(min(len(left), len(right))):
|
||||
if left[i] != right[i]:
|
||||
explanation += ['At index %s diff: %r != %r' %
|
||||
(i, left[i], right[i])]
|
||||
break
|
||||
if len(left) > len(right):
|
||||
explanation += ['Left contains more items, '
|
||||
'first extra item: %s' % py.io.saferepr(left[len(right)],)]
|
||||
elif len(left) < len(right):
|
||||
explanation += ['Right contains more items, '
|
||||
'first extra item: %s' % py.io.saferepr(right[len(left)],)]
|
||||
return explanation # + _diff_text(py.std.pprint.pformat(left),
|
||||
# py.std.pprint.pformat(right))
|
||||
|
||||
|
||||
def _compare_eq_set(left, right):
|
||||
explanation = []
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append('Extra items in the left set:')
|
||||
for item in diff_left:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
if diff_right:
|
||||
explanation.append('Extra items in the right set:')
|
||||
for item in diff_right:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
return explanation
|
||||
287
pytest/plugin/pytest_capture.py
Normal file
287
pytest/plugin/pytest_capture.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
configurable per-test stdout/stderr capturing mechanisms.
|
||||
|
||||
This plugin captures stdout/stderr output for each test separately.
|
||||
In case of test failures this captured output is shown grouped
|
||||
togtther with the test.
|
||||
|
||||
The plugin also provides test function arguments that help to
|
||||
assert stdout/stderr output from within your tests, see the
|
||||
`funcarg example`_.
|
||||
|
||||
|
||||
Capturing of input/output streams during tests
|
||||
---------------------------------------------------
|
||||
|
||||
By default ``sys.stdout`` and ``sys.stderr`` are substituted with
|
||||
temporary streams during the execution of tests and setup/teardown code.
|
||||
During the whole testing process it will re-use the same temporary
|
||||
streams allowing to play well with the logging module which easily
|
||||
takes ownership on these streams.
|
||||
|
||||
Also, 'sys.stdin' is substituted with a file-like "null" object that
|
||||
does not return any values. This is to immediately error out
|
||||
on tests that wait on reading something from stdin.
|
||||
|
||||
You can influence output capturing mechanisms from the command line::
|
||||
|
||||
py.test -s # disable all capturing
|
||||
py.test --capture=sys # replace sys.stdout/stderr with in-mem files
|
||||
py.test --capture=fd # point filedescriptors 1 and 2 to temp file
|
||||
|
||||
If you set capturing values in a conftest file like this::
|
||||
|
||||
# conftest.py
|
||||
option_capture = 'fd'
|
||||
|
||||
then all tests in that directory will execute with "fd" style capturing.
|
||||
|
||||
sys-level capturing
|
||||
------------------------------------------
|
||||
|
||||
Capturing on 'sys' level means that ``sys.stdout`` and ``sys.stderr``
|
||||
will be replaced with in-memory files (``py.io.TextIO`` to be precise)
|
||||
that capture writes and decode non-unicode strings to a unicode object
|
||||
(using a default, usually, UTF-8, encoding).
|
||||
|
||||
FD-level capturing and subprocesses
|
||||
------------------------------------------
|
||||
|
||||
The ``fd`` based method means that writes going to system level files
|
||||
based on the standard file descriptors will be captured, for example
|
||||
writes such as ``os.write(1, 'hello')`` will be captured properly.
|
||||
Capturing on fd-level will include output generated from
|
||||
any subprocesses created during a test.
|
||||
|
||||
.. _`funcarg example`:
|
||||
|
||||
Example Usage of the capturing Function arguments
|
||||
---------------------------------------------------
|
||||
|
||||
You can use the `capsys funcarg`_ and `capfd funcarg`_ to
|
||||
capture writes to stdout and stderr streams. Using the
|
||||
funcargs frees your test from having to care about setting/resetting
|
||||
the old streams and also interacts well with py.test's own
|
||||
per-test capturing. Here is an example test function:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def test_myoutput(capsys):
|
||||
print ("hello")
|
||||
sys.stderr.write("world\\n")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "hello\\n"
|
||||
assert err == "world\\n"
|
||||
print "next"
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "next\\n"
|
||||
|
||||
The ``readouterr()`` call snapshots the output so far -
|
||||
and capturing will be continued. After the test
|
||||
function finishes the original streams will
|
||||
be restored. If you want to capture on
|
||||
the filedescriptor level you can use the ``capfd`` function
|
||||
argument which offers the same interface.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--capture', action="store", default=None,
|
||||
metavar="method", type="choice", choices=['fd', 'sys', 'no'],
|
||||
help="per-test capturing method: one of fd (default)|sys|no.")
|
||||
group._addoption('-s', action="store_const", const="no", dest="capture",
|
||||
help="shortcut for --capture=no.")
|
||||
|
||||
def addouterr(rep, outerr):
|
||||
repr = getattr(rep, 'longrepr', None)
|
||||
if not hasattr(repr, 'addsection'):
|
||||
return
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
repr.addsection("Captured std%s" % secname, content.rstrip())
|
||||
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(CaptureManager(), 'capturemanager')
|
||||
|
||||
class NoCapture:
|
||||
def startall(self):
|
||||
pass
|
||||
def resume(self):
|
||||
pass
|
||||
def suspend(self):
|
||||
return "", ""
|
||||
|
||||
class CaptureManager:
|
||||
def __init__(self):
|
||||
self._method2capture = {}
|
||||
|
||||
def _maketempfile(self):
|
||||
f = py.std.tempfile.TemporaryFile()
|
||||
newf = py.io.dupfile(f, encoding="UTF-8")
|
||||
return newf
|
||||
|
||||
def _makestringio(self):
|
||||
return py.io.TextIO()
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
return py.io.StdCaptureFD(now=False,
|
||||
out=self._maketempfile(), err=self._maketempfile()
|
||||
)
|
||||
elif method == "sys":
|
||||
return py.io.StdCapture(now=False,
|
||||
out=self._makestringio(), err=self._makestringio()
|
||||
)
|
||||
elif method == "no":
|
||||
return NoCapture()
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def _getmethod(self, config, fspath):
|
||||
if config.option.capture:
|
||||
method = config.option.capture
|
||||
else:
|
||||
try:
|
||||
method = config._conftest.rget("option_capture", path=fspath)
|
||||
except KeyError:
|
||||
method = "fd"
|
||||
if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
|
||||
method = "sys"
|
||||
return method
|
||||
|
||||
def resumecapture_item(self, item):
|
||||
method = self._getmethod(item.config, item.fspath)
|
||||
if not hasattr(item, 'outerr'):
|
||||
item.outerr = ('', '') # we accumulate outerr on the item
|
||||
return self.resumecapture(method)
|
||||
|
||||
def resumecapture(self, method):
|
||||
if hasattr(self, '_capturing'):
|
||||
raise ValueError("cannot resume, already capturing with %r" %
|
||||
(self._capturing,))
|
||||
cap = self._method2capture.get(method)
|
||||
self._capturing = method
|
||||
if cap is None:
|
||||
self._method2capture[method] = cap = self._getcapture(method)
|
||||
cap.startall()
|
||||
else:
|
||||
cap.resume()
|
||||
|
||||
def suspendcapture(self, item=None):
|
||||
self.deactivate_funcargs()
|
||||
if hasattr(self, '_capturing'):
|
||||
method = self._capturing
|
||||
cap = self._method2capture.get(method)
|
||||
if cap is not None:
|
||||
outerr = cap.suspend()
|
||||
del self._capturing
|
||||
if item:
|
||||
outerr = (item.outerr[0] + outerr[0],
|
||||
item.outerr[1] + outerr[1])
|
||||
return outerr
|
||||
if hasattr(item, 'outerr'):
|
||||
return item.outerr
|
||||
return "", ""
|
||||
|
||||
def activate_funcargs(self, pyfuncitem):
|
||||
if not hasattr(pyfuncitem, 'funcargs'):
|
||||
return
|
||||
assert not hasattr(self, '_capturing_funcargs')
|
||||
self._capturing_funcargs = capturing_funcargs = []
|
||||
for name, capfuncarg in pyfuncitem.funcargs.items():
|
||||
if name in ('capsys', 'capfd'):
|
||||
capturing_funcargs.append(capfuncarg)
|
||||
capfuncarg._start()
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capturing_funcargs = getattr(self, '_capturing_funcargs', None)
|
||||
if capturing_funcargs is not None:
|
||||
while capturing_funcargs:
|
||||
capfuncarg = capturing_funcargs.pop()
|
||||
capfuncarg._finalize()
|
||||
del self._capturing_funcargs
|
||||
|
||||
def pytest_make_collect_report(self, __multicall__, collector):
|
||||
method = self._getmethod(collector.config, collector.fspath)
|
||||
self.resumecapture(method)
|
||||
try:
|
||||
rep = __multicall__.execute()
|
||||
finally:
|
||||
outerr = self.suspendcapture()
|
||||
addouterr(rep, outerr)
|
||||
return rep
|
||||
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.activate_funcargs(item)
|
||||
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
def pytest__teardown_final(self, __multicall__, session):
|
||||
method = self._getmethod(session.config, None)
|
||||
self.resumecapture(method)
|
||||
try:
|
||||
rep = __multicall__.execute()
|
||||
finally:
|
||||
outerr = self.suspendcapture()
|
||||
if rep:
|
||||
addouterr(rep, outerr)
|
||||
return rep
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
if hasattr(self, '_capturing'):
|
||||
self.suspendcapture()
|
||||
|
||||
def pytest_runtest_makereport(self, __multicall__, item, call):
|
||||
self.deactivate_funcargs()
|
||||
rep = __multicall__.execute()
|
||||
outerr = self.suspendcapture(item)
|
||||
if not rep.passed:
|
||||
addouterr(rep, outerr)
|
||||
if not rep.passed or rep.when == "teardown":
|
||||
outerr = ('', '')
|
||||
item.outerr = outerr
|
||||
return rep
|
||||
|
||||
def pytest_funcarg__capsys(request):
|
||||
"""captures writes to sys.stdout/sys.stderr and makes
|
||||
them available successively via a ``capsys.readouterr()`` method
|
||||
which returns a ``(out, err)`` tuple of captured snapshot strings.
|
||||
"""
|
||||
return CaptureFuncarg(py.io.StdCapture)
|
||||
|
||||
def pytest_funcarg__capfd(request):
|
||||
"""captures writes to file descriptors 1 and 2 and makes
|
||||
snapshotted ``(out, err)`` string tuples available
|
||||
via the ``capsys.readouterr()`` method. If the underlying
|
||||
platform does not have ``os.dup`` (e.g. Jython) tests using
|
||||
this funcarg will automatically skip.
|
||||
"""
|
||||
if not hasattr(os, 'dup'):
|
||||
py.test.skip("capfd funcarg needs os.dup")
|
||||
return CaptureFuncarg(py.io.StdCaptureFD)
|
||||
|
||||
class CaptureFuncarg:
|
||||
def __init__(self, captureclass):
|
||||
self.capture = captureclass(now=False)
|
||||
|
||||
def _start(self):
|
||||
self.capture.startall()
|
||||
|
||||
def _finalize(self):
|
||||
if hasattr(self, 'capture'):
|
||||
self.capture.reset()
|
||||
del self.capture
|
||||
|
||||
def readouterr(self):
|
||||
return self.capture.readouterr()
|
||||
|
||||
def close(self):
|
||||
self._finalize()
|
||||
78
pytest/plugin/pytest_default.py
Normal file
78
pytest/plugin/pytest_default.py
Normal file
@@ -0,0 +1,78 @@
|
||||
""" default hooks and general py.test options. """
|
||||
|
||||
import sys
|
||||
import py
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
from pytest.session import Session
|
||||
return Session(config).main()
|
||||
|
||||
def pytest_perform_collection(session):
|
||||
collection = session.collection
|
||||
assert not hasattr(collection, 'items')
|
||||
hook = session.config.hook
|
||||
collection.items = items = collection.perform_collect()
|
||||
hook.pytest_collection_modifyitems(config=session.config, items=items)
|
||||
hook.pytest_log_finishcollection(collection=collection)
|
||||
return True
|
||||
|
||||
def pytest_runtest_mainloop(session):
|
||||
if session.config.option.collectonly:
|
||||
return True
|
||||
for item in session.collection.items:
|
||||
item.config.hook.pytest_runtest_protocol(item=item)
|
||||
if session.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
return True
|
||||
|
||||
def pytest_ignore_collect(path, config):
|
||||
p = path.dirpath()
|
||||
ignore_paths = config.getconftest_pathlist("collect_ignore", path=p)
|
||||
ignore_paths = ignore_paths or []
|
||||
excludeopt = config.getvalue("ignore")
|
||||
if excludeopt:
|
||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||
return path in ignore_paths
|
||||
|
||||
def pytest_collect_directory(path, parent):
|
||||
if not parent.recfilter(path): # by default special ".cvs", ...
|
||||
# check if cmdline specified this dir or a subdir directly
|
||||
for arg in parent.collection._argfspaths:
|
||||
if path == arg or arg.relto(path):
|
||||
break
|
||||
else:
|
||||
return
|
||||
return parent.Directory(path, parent=parent)
|
||||
|
||||
def pytest_report_iteminfo(item):
|
||||
return item.reportinfo()
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general", "running and selection options")
|
||||
group._addoption('-x', '--exitfirst', action="store_true", default=False,
|
||||
dest="exitfirst",
|
||||
help="exit instantly on first error or failed test."),
|
||||
group._addoption('--maxfail', metavar="num",
|
||||
action="store", type="int", dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly',
|
||||
action="store_true", dest="collectonly",
|
||||
help="only collect tests, don't execute them."),
|
||||
group.addoption("--ignore", action="append", metavar="path",
|
||||
help="ignore path during collection (multi-allowed).")
|
||||
group.addoption('--confcutdir', dest="confcutdir", default=None,
|
||||
metavar="dir",
|
||||
help="only load conftest.py's relative to specified dir.")
|
||||
|
||||
group = parser.getgroup("debugconfig",
|
||||
"test process debugging and configuration")
|
||||
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
def pytest_configure(config):
|
||||
# compat
|
||||
if config.getvalue("exitfirst"):
|
||||
config.option.maxfail = 1
|
||||
|
||||
103
pytest/plugin/pytest_doctest.py
Normal file
103
pytest/plugin/pytest_doctest.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
collect and execute doctests from modules and test files.
|
||||
|
||||
Usage
|
||||
-------------
|
||||
|
||||
By default all files matching the ``test*.txt`` pattern will
|
||||
be run through the python standard ``doctest`` module. Issue::
|
||||
|
||||
py.test --doctest-glob='*.rst'
|
||||
|
||||
to change the pattern. Additionally you can trigger running of
|
||||
tests in all python modules (including regular python test modules)::
|
||||
|
||||
py.test --doctest-modules
|
||||
|
||||
You can also make these changes permanent in your project by
|
||||
putting them into a conftest.py file like this::
|
||||
|
||||
# content of conftest.py
|
||||
option_doctestmodules = True
|
||||
option_doctestglob = "*.rst"
|
||||
"""
|
||||
|
||||
import py
|
||||
from py._code.code import TerminalRepr, ReprFileLocation
|
||||
import doctest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption("--doctest-modules",
|
||||
action="store_true", default=False,
|
||||
help="run doctests in all .py modules",
|
||||
dest="doctestmodules")
|
||||
group.addoption("--doctest-glob",
|
||||
action="store", default="test*.txt", metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.getvalue("doctestmodules"):
|
||||
return DoctestModule(path, parent)
|
||||
elif path.check(fnmatch=config.getvalue("doctestglob")):
|
||||
return DoctestTextfile(path, parent)
|
||||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
def __init__(self, reprlocation, lines):
|
||||
self.reprlocation = reprlocation
|
||||
self.lines = lines
|
||||
def toterminal(self, tw):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
self.reprlocation.toterminal(tw)
|
||||
|
||||
class DoctestItem(py.test.collect.Item):
|
||||
def __init__(self, path, parent):
|
||||
name = self.__class__.__name__ + ":" + path.basename
|
||||
super(DoctestItem, self).__init__(name=name, parent=parent)
|
||||
self.fspath = path
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
if excinfo.errisinstance(doctest.DocTestFailure):
|
||||
doctestfailure = excinfo.value
|
||||
example = doctestfailure.example
|
||||
test = doctestfailure.test
|
||||
filename = test.filename
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = excinfo.type.__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = doctest.OutputChecker()
|
||||
REPORT_UDIFF = doctest.REPORT_UDIFF
|
||||
filelines = py.path.local(filename).readlines(cr=0)
|
||||
i = max(test.lineno, max(0, lineno - 10)) # XXX?
|
||||
lines = []
|
||||
for line in filelines[i:lineno]:
|
||||
lines.append("%03d %s" % (i+1, line))
|
||||
i += 1
|
||||
lines += checker.output_difference(example,
|
||||
doctestfailure.got, REPORT_UDIFF).split("\n")
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
elif excinfo.errisinstance(doctest.UnexpectedException):
|
||||
excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "[doctest]"
|
||||
|
||||
class DoctestTextfile(DoctestItem):
|
||||
def runtest(self):
|
||||
if not self._deprecated_testexecution():
|
||||
failed, tot = doctest.testfile(
|
||||
str(self.fspath), module_relative=False,
|
||||
raise_on_error=True, verbose=0)
|
||||
|
||||
class DoctestModule(DoctestItem):
|
||||
def runtest(self):
|
||||
module = self.fspath.pyimport()
|
||||
failed, tot = doctest.testmod(
|
||||
module, raise_on_error=True, verbose=0)
|
||||
69
pytest/plugin/pytest_genscript.py
Executable file
69
pytest/plugin/pytest_genscript.py
Executable file
@@ -0,0 +1,69 @@
|
||||
#! /usr/bin/env python
|
||||
"""
|
||||
generate standalone test script to be distributed along with an application.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption("--genscript", action="store", default=None,
|
||||
dest="genscript", metavar="path",
|
||||
help="create standalone py.test script at given target path.")
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
genscript = config.getvalue("genscript")
|
||||
if genscript:
|
||||
import py
|
||||
mydir = py.path.local(__file__).dirpath()
|
||||
infile = mydir.join("standalonetemplate.py")
|
||||
pybasedir = py.path.local(py.__file__).dirpath().dirpath()
|
||||
genscript = py.path.local(genscript)
|
||||
main(pybasedir, outfile=genscript, infile=infile)
|
||||
return 0
|
||||
|
||||
def main(pybasedir, outfile, infile):
|
||||
import base64
|
||||
import zlib
|
||||
try:
|
||||
import pickle
|
||||
except Importerror:
|
||||
import cPickle as pickle
|
||||
|
||||
outfile = str(outfile)
|
||||
infile = str(infile)
|
||||
assert os.path.isabs(outfile)
|
||||
os.chdir(str(pybasedir))
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk("py"):
|
||||
for f in filenames:
|
||||
if not f.endswith(".py"):
|
||||
continue
|
||||
|
||||
fn = os.path.join(dirpath, f)
|
||||
files.append(fn)
|
||||
|
||||
name2src = {}
|
||||
for f in files:
|
||||
k = f.replace(os.sep, ".")[:-3]
|
||||
name2src[k] = open(f, "r").read()
|
||||
|
||||
data = pickle.dumps(name2src, 2)
|
||||
data = zlib.compress(data, 9)
|
||||
data = base64.encodestring(data)
|
||||
data = data.decode("ascii")
|
||||
|
||||
exe = open(infile, "r").read()
|
||||
exe = exe.replace("@SOURCES@", data)
|
||||
|
||||
open(outfile, "w").write(exe)
|
||||
os.chmod(outfile, 493) # 0755
|
||||
sys.stdout.write("generated standalone py.test at %r, have fun!\n" % outfile)
|
||||
|
||||
if __name__=="__main__":
|
||||
dn = os.path.dirname
|
||||
here = os.path.abspath(dn(__file__)) # py/plugin/
|
||||
pybasedir = dn(dn(here))
|
||||
outfile = os.path.join(os.getcwd(), "py.test-standalone")
|
||||
infile = os.path.join(here, 'standalonetemplate.py')
|
||||
main(pybasedir, outfile, infile)
|
||||
165
pytest/plugin/pytest_helpconfig.py
Normal file
165
pytest/plugin/pytest_helpconfig.py
Normal file
@@ -0,0 +1,165 @@
|
||||
""" provide version info, conftest/environment config names.
|
||||
"""
|
||||
import py
|
||||
import inspect, sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('debugconfig')
|
||||
group.addoption('--version', action="store_true",
|
||||
help="display py lib version and import information.")
|
||||
group._addoption('-p', action="append", dest="plugins", default = [],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed).")
|
||||
group.addoption('--traceconfig',
|
||||
action="store_true", dest="traceconfig", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
group._addoption('--nomagic',
|
||||
action="store_true", dest="nomagic", default=False,
|
||||
help="don't reinterpret asserts, no traceback cutting. ")
|
||||
group.addoption('--debug',
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="generate and show internal debugging information.")
|
||||
group.addoption("--help-config", action="store_true", dest="helpconfig",
|
||||
help="show available conftest.py and ENV-variable names.")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.version:
|
||||
p = py.path.local(py.__file__).dirpath()
|
||||
sys.stderr.write("This is py.test version %s, imported from %s\n" %
|
||||
(py.__version__, p))
|
||||
return 0
|
||||
elif config.option.helpconfig:
|
||||
config.pluginmanager.do_configure(config)
|
||||
showpluginhelp(config)
|
||||
return 0
|
||||
|
||||
def showpluginhelp(config):
|
||||
options = []
|
||||
for group in config._parser._groups:
|
||||
options.extend(group.options)
|
||||
widths = [0] * 10
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.sep("-")
|
||||
tw.line("%-13s | %-18s | %-25s | %s" %(
|
||||
"cmdline name", "conftest.py name", "ENV-variable name", "help"))
|
||||
tw.sep("-")
|
||||
|
||||
options = [opt for opt in options if opt._long_opts]
|
||||
options.sort(key=lambda x: x._long_opts)
|
||||
for opt in options:
|
||||
if not opt._long_opts or not opt.dest:
|
||||
continue
|
||||
optstrings = list(opt._long_opts) # + list(opt._short_opts)
|
||||
optstrings = filter(None, optstrings)
|
||||
optstring = "|".join(optstrings)
|
||||
line = "%-13s | %-18s | %-25s | %s" %(
|
||||
optstring,
|
||||
"option_%s" % opt.dest,
|
||||
"PYTEST_OPTION_%s" % opt.dest.upper(),
|
||||
opt.help and opt.help or "",
|
||||
)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
for name, help in conftest_options:
|
||||
line = "%-13s | %-18s | %-25s | %s" %(
|
||||
"",
|
||||
name,
|
||||
"",
|
||||
help,
|
||||
)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
tw.sep("-")
|
||||
|
||||
conftest_options = (
|
||||
('pytest_plugins', 'list of plugin names to load'),
|
||||
('collect_ignore', '(relative) paths ignored during collection'),
|
||||
('rsyncdirs', 'to-be-rsynced directories for dist-testing'),
|
||||
)
|
||||
|
||||
def pytest_report_header(config):
|
||||
lines = []
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
lines.append("using py lib: %s" % (py.path.local(py.__file__).dirpath()))
|
||||
if config.option.traceconfig:
|
||||
lines.append("active plugins:")
|
||||
plugins = []
|
||||
items = config.pluginmanager._name2plugin.items()
|
||||
for name, plugin in items:
|
||||
lines.append(" %-20s: %s" %(name, repr(plugin)))
|
||||
return lines
|
||||
|
||||
|
||||
# =====================================================
|
||||
# validate plugin syntax and hooks
|
||||
# =====================================================
|
||||
|
||||
def pytest_plugin_registered(manager, plugin):
|
||||
methods = collectattr(plugin)
|
||||
hooks = {}
|
||||
for hookspec in manager.hook._hookspecs:
|
||||
hooks.update(collectattr(hookspec))
|
||||
|
||||
stringio = py.io.TextIO()
|
||||
def Print(*args):
|
||||
if args:
|
||||
stringio.write(" ".join(map(str, args)))
|
||||
stringio.write("\n")
|
||||
|
||||
fail = False
|
||||
while methods:
|
||||
name, method = methods.popitem()
|
||||
#print "checking", name
|
||||
if isgenerichook(name):
|
||||
continue
|
||||
if name not in hooks:
|
||||
if not getattr(method, 'optionalhook', False):
|
||||
Print("found unknown hook:", name)
|
||||
fail = True
|
||||
else:
|
||||
#print "checking", method
|
||||
method_args = getargs(method)
|
||||
#print "method_args", method_args
|
||||
if '__multicall__' in method_args:
|
||||
method_args.remove('__multicall__')
|
||||
hook = hooks[name]
|
||||
hookargs = getargs(hook)
|
||||
for arg in method_args:
|
||||
if arg not in hookargs:
|
||||
Print("argument %r not available" %(arg, ))
|
||||
Print("actual definition: %s" %(formatdef(method)))
|
||||
Print("available hook arguments: %s" %
|
||||
", ".join(hookargs))
|
||||
fail = True
|
||||
break
|
||||
#if not fail:
|
||||
# print "matching hook:", formatdef(method)
|
||||
if fail:
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue()))
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def getargs(func):
|
||||
args = inspect.getargs(py.code.getrawcode(func))[0]
|
||||
startindex = inspect.ismethod(func) and 1 or 0
|
||||
return args[startindex:]
|
||||
|
||||
def collectattr(obj, prefixes=("pytest_",)):
|
||||
methods = {}
|
||||
for apiname in dir(obj):
|
||||
for prefix in prefixes:
|
||||
if apiname.startswith(prefix):
|
||||
methods[apiname] = getattr(obj, apiname)
|
||||
return methods
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" %(
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
33
pytest/plugin/pytest_hooklog.py
Normal file
33
pytest/plugin/pytest_hooklog.py
Normal file
@@ -0,0 +1,33 @@
|
||||
""" log invocations of extension hooks to a file. """
|
||||
import py
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--hooklog", dest="hooklog", default=None,
|
||||
help="write hook calls to the given file.")
|
||||
|
||||
def pytest_configure(config):
|
||||
hooklog = config.getvalue("hooklog")
|
||||
if hooklog:
|
||||
config._hooklogfile = open(hooklog, 'w')
|
||||
config._hooklog_oldperformcall = config.hook._performcall
|
||||
config.hook._performcall = (lambda name, multicall:
|
||||
logged_call(name=name, multicall=multicall, config=config))
|
||||
|
||||
def logged_call(name, multicall, config):
|
||||
f = config._hooklogfile
|
||||
f.write("%s(**%s)\n" % (name, multicall.kwargs))
|
||||
try:
|
||||
res = config._hooklog_oldperformcall(name=name, multicall=multicall)
|
||||
except:
|
||||
f.write("-> exception")
|
||||
raise
|
||||
f.write("-> %r" % (res,))
|
||||
return res
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
try:
|
||||
del config.hook.__dict__['_performcall']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
config._hooklogfile.close()
|
||||
171
pytest/plugin/pytest_junitxml.py
Normal file
171
pytest/plugin/pytest_junitxml.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
logging of test results in JUnit-XML format, for use with Hudson
|
||||
and build integration servers. Based on initial code from Ross Lawley.
|
||||
"""
|
||||
|
||||
import py
|
||||
import time
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption('--junitxml', action="store", dest="xmlpath",
|
||||
metavar="path", default=None,
|
||||
help="create junit-xml style report file at given path.")
|
||||
group.addoption('--junitprefix', action="store", dest="junitprefix",
|
||||
metavar="str", default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
|
||||
def pytest_configure(config):
|
||||
xmlpath = config.option.xmlpath
|
||||
if xmlpath:
|
||||
config._xml = LogXML(xmlpath, config.option.junitprefix)
|
||||
config.pluginmanager.register(config._xml)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
xml = getattr(config, '_xml', None)
|
||||
if xml:
|
||||
del config._xml
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
self.logfile = logfile
|
||||
self.prefix = prefix
|
||||
self.test_logs = []
|
||||
self.passed = self.skipped = 0
|
||||
self.failed = self.errors = 0
|
||||
self._durations = {}
|
||||
|
||||
def _opentestcase(self, report):
|
||||
names = report.nodenames
|
||||
d = {'time': self._durations.pop(names, "0")}
|
||||
names = [x.replace(".py", "") for x in names if x != "()"]
|
||||
classnames = names[:-1]
|
||||
if self.prefix:
|
||||
classnames.insert(0, self.prefix)
|
||||
d['classname'] = ".".join(classnames)
|
||||
d['name'] = py.xml.escape(names[-1])
|
||||
attrs = ['%s="%s"' % item for item in sorted(d.items())]
|
||||
self.test_logs.append("\n<testcase %s>" % " ".join(attrs))
|
||||
|
||||
def _closetestcase(self):
|
||||
self.test_logs.append("</testcase>")
|
||||
|
||||
def appendlog(self, fmt, *args):
|
||||
args = tuple([py.xml.escape(arg) for arg in args])
|
||||
self.test_logs.append(fmt % args)
|
||||
|
||||
def append_pass(self, report):
|
||||
self.passed += 1
|
||||
self._opentestcase(report)
|
||||
self._closetestcase()
|
||||
|
||||
def append_failure(self, report):
|
||||
self._opentestcase(report)
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
if "xfail" in report.keywords:
|
||||
self.appendlog(
|
||||
'<skipped message="xfail-marked test passes unexpectedly"/>')
|
||||
self.skipped += 1
|
||||
else:
|
||||
self.appendlog('<failure message="test failure">%s</failure>',
|
||||
report.longrepr)
|
||||
self.failed += 1
|
||||
self._closetestcase()
|
||||
|
||||
def append_collect_failure(self, report):
|
||||
self._opentestcase(report)
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.appendlog('<failure message="collection failure">%s</failure>',
|
||||
report.longrepr)
|
||||
self._closetestcase()
|
||||
self.errors += 1
|
||||
|
||||
def append_collect_skipped(self, report):
|
||||
self._opentestcase(report)
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.appendlog('<skipped message="collection skipped">%s</skipped>',
|
||||
report.longrepr)
|
||||
self._closetestcase()
|
||||
self.skipped += 1
|
||||
|
||||
def append_error(self, report):
|
||||
self._opentestcase(report)
|
||||
self.appendlog('<error message="test setup failure">%s</error>',
|
||||
report.longrepr)
|
||||
self._closetestcase()
|
||||
self.errors += 1
|
||||
|
||||
def append_skipped(self, report):
|
||||
self._opentestcase(report)
|
||||
if "xfail" in report.keywords:
|
||||
self.appendlog(
|
||||
'<skipped message="expected test failure">%s</skipped>',
|
||||
report.keywords['xfail'])
|
||||
else:
|
||||
self.appendlog("<skipped/>")
|
||||
self._closetestcase()
|
||||
self.skipped += 1
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.passed:
|
||||
self.append_pass(report)
|
||||
elif report.failed:
|
||||
if report.when != "call":
|
||||
self.append_error(report)
|
||||
else:
|
||||
self.append_failure(report)
|
||||
elif report.skipped:
|
||||
self.append_skipped(report)
|
||||
|
||||
def pytest_runtest_call(self, item, __multicall__):
|
||||
names = tuple(item.listnames())
|
||||
start = time.time()
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
self._durations[names] = time.time() - start
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
self.append_collect_failure(report)
|
||||
else:
|
||||
self.append_collect_skipped(report)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
self.errors += 1
|
||||
data = py.xml.escape(excrepr)
|
||||
self.test_logs.append(
|
||||
'\n<testcase classname="pytest" name="internal">'
|
||||
' <error message="internal error">'
|
||||
'%s</error></testcase>' % data)
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
self.suite_start_time = time.time()
|
||||
|
||||
def pytest_sessionfinish(self, session, exitstatus, __multicall__):
|
||||
if py.std.sys.version_info[0] < 3:
|
||||
logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
|
||||
else:
|
||||
logfile = open(self.logfile, 'w', encoding='utf-8')
|
||||
|
||||
suite_stop_time = time.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
numtests = self.passed + self.failed
|
||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||
logfile.write('<testsuite ')
|
||||
logfile.write('name="" ')
|
||||
logfile.write('errors="%i" ' % self.errors)
|
||||
logfile.write('failures="%i" ' % self.failed)
|
||||
logfile.write('skips="%i" ' % self.skipped)
|
||||
logfile.write('tests="%i" ' % numtests)
|
||||
logfile.write('time="%.3f"' % suite_time_delta)
|
||||
logfile.write(' >')
|
||||
logfile.writelines(self.test_logs)
|
||||
logfile.write('</testsuite>')
|
||||
logfile.close()
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
tw = terminalreporter._tw
|
||||
terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile))
|
||||
65
pytest/plugin/pytest_keyword.py
Normal file
65
pytest/plugin/pytest_keyword.py
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('-k',
|
||||
action="store", dest="keyword", default='',
|
||||
help="only run test items matching the given "
|
||||
"space separated keywords. precede a keyword with '-' to negate. "
|
||||
"Terminate the expression with ':' to treat a match as a signal "
|
||||
"to run all subsequent tests. ")
|
||||
|
||||
def pytest_collection_modifyitems(items, config):
|
||||
keywordexpr = config.option.keyword
|
||||
if not keywordexpr:
|
||||
return
|
||||
selectuntil = False
|
||||
if keywordexpr[-1] == ":":
|
||||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for colitem in items:
|
||||
if keywordexpr and skipbykeyword(colitem, keywordexpr):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
remaining.append(colitem)
|
||||
if selectuntil:
|
||||
keywordexpr = None
|
||||
|
||||
if deselected:
|
||||
config.hook.pytest_deselected(items=deselected)
|
||||
items[:] = remaining
|
||||
|
||||
def skipbykeyword(colitem, keywordexpr):
|
||||
""" return True if they given keyword expression means to
|
||||
skip this collector/item.
|
||||
"""
|
||||
if not keywordexpr:
|
||||
return
|
||||
chain = colitem.listchain()
|
||||
for key in filter(None, keywordexpr.split()):
|
||||
eor = key[:1] == '-'
|
||||
if eor:
|
||||
key = key[1:]
|
||||
if not (eor ^ matchonekeyword(key, chain)):
|
||||
return True
|
||||
|
||||
def matchonekeyword(key, chain):
|
||||
elems = key.split(".")
|
||||
# XXX O(n^2), anyone cares?
|
||||
chain = [item.keywords for item in chain if item.keywords]
|
||||
for start, _ in enumerate(chain):
|
||||
if start + len(elems) > len(chain):
|
||||
return False
|
||||
for num, elem in enumerate(elems):
|
||||
for keyword in chain[num + start]:
|
||||
ok = False
|
||||
if elem in keyword:
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
break
|
||||
if num == len(elems) - 1 and ok:
|
||||
return True
|
||||
return False
|
||||
176
pytest/plugin/pytest_mark.py
Normal file
176
pytest/plugin/pytest_mark.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
generic mechanism for marking python functions.
|
||||
|
||||
By using the ``py.test.mark`` helper you can instantiate
|
||||
decorators that will set named meta data on test functions.
|
||||
|
||||
Marking a single function
|
||||
----------------------------------------------------
|
||||
|
||||
You can "mark" a test function with meta data like this::
|
||||
|
||||
@py.test.mark.webtest
|
||||
def test_send_http():
|
||||
...
|
||||
|
||||
This will set a "Marker" instance as a function attribute named "webtest".
|
||||
You can also specify parametrized meta data like this::
|
||||
|
||||
@py.test.mark.webtest(firefox=30)
|
||||
def test_receive():
|
||||
...
|
||||
|
||||
The named marker can be accessed like this later::
|
||||
|
||||
test_receive.webtest.kwargs['firefox'] == 30
|
||||
|
||||
In addition to set key-value pairs you can also use positional arguments::
|
||||
|
||||
@py.test.mark.webtest("triangular")
|
||||
def test_receive():
|
||||
...
|
||||
|
||||
and later access it with ``test_receive.webtest.args[0] == 'triangular``.
|
||||
|
||||
.. _`scoped-marking`:
|
||||
|
||||
Marking whole classes or modules
|
||||
----------------------------------------------------
|
||||
|
||||
If you are programming with Python2.6 you may use ``py.test.mark`` decorators
|
||||
with classes to apply markers to all its test methods::
|
||||
|
||||
@py.test.mark.webtest
|
||||
class TestClass:
|
||||
def test_startup(self):
|
||||
...
|
||||
def test_startup_and_more(self):
|
||||
...
|
||||
|
||||
This is equivalent to directly applying the decorator to the
|
||||
two test functions.
|
||||
|
||||
To remain compatible with Python2.5 you can also set a
|
||||
``pytestmark`` attribute on a TestClass like this::
|
||||
|
||||
import py
|
||||
|
||||
class TestClass:
|
||||
pytestmark = py.test.mark.webtest
|
||||
|
||||
or if you need to use multiple markers you can use a list::
|
||||
|
||||
import py
|
||||
|
||||
class TestClass:
|
||||
pytestmark = [py.test.mark.webtest, pytest.mark.slowtest]
|
||||
|
||||
You can also set a module level marker::
|
||||
|
||||
import py
|
||||
pytestmark = py.test.mark.webtest
|
||||
|
||||
in which case it will be applied to all functions and
|
||||
methods defined in the module.
|
||||
|
||||
Using "-k MARKNAME" to select tests
|
||||
----------------------------------------------------
|
||||
|
||||
You can use the ``-k`` command line option to select
|
||||
tests::
|
||||
|
||||
py.test -k webtest # will only run tests marked as webtest
|
||||
|
||||
"""
|
||||
import py
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': MarkGenerator()}
|
||||
|
||||
class MarkGenerator:
|
||||
""" non-underscore attributes of this object can be used as decorators for
|
||||
marking test functions. Example: @py.test.mark.slowtest in front of a
|
||||
function will set the 'slowtest' marker object on it. """
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
return MarkDecorator(name)
|
||||
|
||||
class MarkDecorator:
|
||||
""" decorator for setting function attributes. """
|
||||
def __init__(self, name):
|
||||
self.markname = name
|
||||
self.kwargs = {}
|
||||
self.args = []
|
||||
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
name = d.pop('markname')
|
||||
return "<MarkDecorator %r %r>" %(name, d)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
""" if passed a single callable argument: decorate it with mark info.
|
||||
otherwise add *args/**kwargs in-place to mark information. """
|
||||
if args:
|
||||
func = args[0]
|
||||
if len(args) == 1 and hasattr(func, '__call__') or \
|
||||
hasattr(func, '__bases__'):
|
||||
if hasattr(func, '__bases__'):
|
||||
if hasattr(func, 'pytestmark'):
|
||||
l = func.pytestmark
|
||||
if not isinstance(l, list):
|
||||
func.pytestmark = [l, self]
|
||||
else:
|
||||
l.append(self)
|
||||
else:
|
||||
func.pytestmark = [self]
|
||||
else:
|
||||
holder = getattr(func, self.markname, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(self.markname, self.args, self.kwargs)
|
||||
setattr(func, self.markname, holder)
|
||||
else:
|
||||
holder.kwargs.update(self.kwargs)
|
||||
holder.args.extend(self.args)
|
||||
return func
|
||||
else:
|
||||
self.args.extend(args)
|
||||
self.kwargs.update(kwargs)
|
||||
return self
|
||||
|
||||
class MarkInfo:
|
||||
def __init__(self, name, args, kwargs):
|
||||
self._name = name
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] != '_' and name in self.kwargs:
|
||||
py.log._apiwarn("1.1", "use .kwargs attribute to access key-values")
|
||||
return self.kwargs[name]
|
||||
raise AttributeError(name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self._name, self.args, self.kwargs)
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
|
||||
item = __multicall__.execute()
|
||||
if isinstance(item, py.test.collect.Function):
|
||||
cls = collector.getparent(py.test.collect.Class)
|
||||
mod = collector.getparent(py.test.collect.Module)
|
||||
func = item.obj
|
||||
func = getattr(func, '__func__', func) # py3
|
||||
func = getattr(func, 'im_func', func) # py2
|
||||
for parent in [x for x in (mod, cls) if x]:
|
||||
marker = getattr(parent.obj, 'pytestmark', None)
|
||||
if marker is not None:
|
||||
if not isinstance(marker, list):
|
||||
marker = [marker]
|
||||
for mark in marker:
|
||||
if isinstance(mark, MarkDecorator):
|
||||
mark(func)
|
||||
item.keywords.update(py.builtin._getfuncdict(func) or {})
|
||||
|
||||
return item
|
||||
141
pytest/plugin/pytest_monkeypatch.py
Normal file
141
pytest/plugin/pytest_monkeypatch.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
safely patch object attributes, dicts and environment variables.
|
||||
|
||||
Usage
|
||||
----------------
|
||||
|
||||
Use the `monkeypatch funcarg`_ to tweak your global test environment
|
||||
for running a particular test. You can safely set/del an attribute,
|
||||
dictionary item or environment variable by respective methods
|
||||
on the monkeypatch funcarg. If you want e.g. to set an ENV1 variable
|
||||
and have os.path.expanduser return a particular directory, you can
|
||||
write it down like this:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def test_mytest(monkeypatch):
|
||||
monkeypatch.setenv('ENV1', 'myval')
|
||||
monkeypatch.setattr(os.path, 'expanduser', lambda x: '/tmp/xyz')
|
||||
... # your test code that uses those patched values implicitely
|
||||
|
||||
After the test function finished all modifications will be undone,
|
||||
because the ``monkeypatch.undo()`` method is registered as a finalizer.
|
||||
|
||||
``monkeypatch.setattr/delattr/delitem/delenv()`` all
|
||||
by default raise an Exception if the target does not exist.
|
||||
Pass ``raising=False`` if you want to skip this check.
|
||||
|
||||
prepending to PATH or other environment variables
|
||||
---------------------------------------------------------
|
||||
|
||||
To prepend a value to an already existing environment parameter:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def test_mypath_finding(monkeypatch):
|
||||
monkeypatch.setenv('PATH', 'x/y', prepend=":")
|
||||
# in bash language: export PATH=x/y:$PATH
|
||||
|
||||
calling "undo" finalization explicitely
|
||||
-----------------------------------------
|
||||
|
||||
At the end of function execution py.test invokes
|
||||
a teardown hook which undoes all monkeypatch changes.
|
||||
If you do not want to wait that long you can call
|
||||
finalization explicitely::
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
This will undo previous changes. This call consumes the
|
||||
undo stack. Calling it a second time has no effect unless
|
||||
you start monkeypatching after the undo call.
|
||||
|
||||
.. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/
|
||||
"""
|
||||
|
||||
import os, sys
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
|
||||
All modifications will be undone when the requesting
|
||||
test function finished its execution. The ``raising``
|
||||
parameter determines if a KeyError or AttributeError
|
||||
will be raised if the set/deletion operation has no target.
|
||||
"""
|
||||
monkeypatch = MonkeyPatch()
|
||||
request.addfinalizer(monkeypatch.undo)
|
||||
return monkeypatch
|
||||
|
||||
notset = object()
|
||||
|
||||
class MonkeyPatch:
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
|
||||
def setattr(self, obj, name, value, raising=True):
|
||||
oldval = getattr(obj, name, notset)
|
||||
if raising and oldval is notset:
|
||||
raise AttributeError("%r has no attribute %r" %(obj, name))
|
||||
self._setattr.insert(0, (obj, name, oldval))
|
||||
setattr(obj, name, value)
|
||||
|
||||
def delattr(self, obj, name, raising=True):
|
||||
if not hasattr(obj, name):
|
||||
if raising:
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
self._setattr.insert(0, (obj, name, getattr(obj, name, notset)))
|
||||
delattr(obj, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
if name not in dic:
|
||||
if raising:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
value = str(value)
|
||||
if prepend and name in os.environ:
|
||||
value = value + prepend + os.environ[name]
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
if not hasattr(self, '_savesyspath'):
|
||||
self._savesyspath = sys.path[:]
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
def undo(self):
|
||||
for obj, name, value in self._setattr:
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
else:
|
||||
delattr(obj, name)
|
||||
self._setattr[:] = []
|
||||
for dictionary, name, value in self._setitem:
|
||||
if value is notset:
|
||||
del dictionary[name]
|
||||
else:
|
||||
dictionary[name] = value
|
||||
self._setitem[:] = []
|
||||
if hasattr(self, '_savesyspath'):
|
||||
sys.path[:] = self._savesyspath
|
||||
97
pytest/plugin/pytest_nose.py
Normal file
97
pytest/plugin/pytest_nose.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""nose-compatibility plugin: allow to run nose test suites natively.
|
||||
|
||||
This is an experimental plugin for allowing to run tests written
|
||||
in 'nosetests style with py.test.
|
||||
|
||||
Usage
|
||||
-------------
|
||||
|
||||
type::
|
||||
|
||||
py.test # instead of 'nosetests'
|
||||
|
||||
and you should be able to run nose style tests and at the same
|
||||
time can make full use of py.test's capabilities.
|
||||
|
||||
Supported nose Idioms
|
||||
----------------------
|
||||
|
||||
* setup and teardown at module/class/method level
|
||||
* SkipTest exceptions and markers
|
||||
* setup/teardown decorators
|
||||
* yield-based tests and their setup
|
||||
* general usage of nose utilities
|
||||
|
||||
Unsupported idioms / issues
|
||||
----------------------------------
|
||||
|
||||
- nose-style doctests are not collected and executed correctly,
|
||||
also fixtures don't work.
|
||||
|
||||
- no nose-configuration is recognized
|
||||
|
||||
If you find other issues or have suggestions please run::
|
||||
|
||||
py.test --pastebin=all
|
||||
|
||||
and send the resulting URL to a py.test contact channel,
|
||||
at best to the mailing list.
|
||||
"""
|
||||
import py
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
|
||||
if SkipTest:
|
||||
if call.excinfo and call.excinfo.errisinstance(SkipTest):
|
||||
# let's substitute the excinfo with a py.test.skip one
|
||||
call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
def pytest_report_iteminfo(item):
|
||||
# nose 0.11.1 uses decorators for "raises" and other helpers.
|
||||
# for reporting progress by filename we fish for the filename
|
||||
if isinstance(item, py.test.collect.Function):
|
||||
obj = item.obj
|
||||
if hasattr(obj, 'compat_co_firstlineno'):
|
||||
fn = sys.modules[obj.__module__].__file__
|
||||
if fn.endswith(".pyc"):
|
||||
fn = fn[:-1]
|
||||
#assert 0
|
||||
#fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
|
||||
lineno = obj.compat_co_firstlineno
|
||||
return py.path.local(fn), lineno, obj.__module__
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if isinstance(item, (py.test.collect.Function)):
|
||||
if isinstance(item.parent, py.test.collect.Generator):
|
||||
gen = item.parent
|
||||
if not hasattr(gen, '_nosegensetup'):
|
||||
call_optional(gen.obj, 'setup')
|
||||
if isinstance(gen.parent, py.test.collect.Instance):
|
||||
call_optional(gen.parent.obj, 'setup')
|
||||
gen._nosegensetup = True
|
||||
if not call_optional(item.obj, 'setup'):
|
||||
# call module level setup if there is no object level one
|
||||
call_optional(item.parent.obj, 'setup')
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
if isinstance(item, py.test.collect.Function):
|
||||
if not call_optional(item.obj, 'teardown'):
|
||||
call_optional(item.parent.obj, 'teardown')
|
||||
#if hasattr(item.parent, '_nosegensetup'):
|
||||
# #call_optional(item._nosegensetup, 'teardown')
|
||||
# del item.parent._nosegensetup
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
if isinstance(collector, py.test.collect.Generator):
|
||||
call_optional(collector.obj, 'setup')
|
||||
|
||||
def call_optional(obj, name):
|
||||
method = getattr(obj, name, None)
|
||||
if method:
|
||||
# If there's any problems allow the exception to raise rather than
|
||||
# silently ignoring them
|
||||
method()
|
||||
return True
|
||||
83
pytest/plugin/pytest_pastebin.py
Normal file
83
pytest/plugin/pytest_pastebin.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
submit failure or test session information to a pastebin service.
|
||||
|
||||
Usage
|
||||
----------
|
||||
|
||||
**Creating a URL for each test failure**::
|
||||
|
||||
py.test --pastebin=failed
|
||||
|
||||
This will submit test run information to a remote Paste service and
|
||||
provide a URL for each failure. You may select tests as usual or add
|
||||
for example ``-x`` if you only want to send one particular failure.
|
||||
|
||||
**Creating a URL for a whole test session log**::
|
||||
|
||||
py.test --pastebin=all
|
||||
|
||||
Currently only pasting to the http://paste.pocoo.org service is implemented.
|
||||
|
||||
"""
|
||||
import py, sys
|
||||
|
||||
class url:
|
||||
base = "http://paste.pocoo.org"
|
||||
xmlrpc = base + "/xmlrpc/"
|
||||
show = base + "/show/"
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group._addoption('--pastebin', metavar="mode",
|
||||
action='store', dest="pastebin", default=None,
|
||||
type="choice", choices=['failed', 'all'],
|
||||
help="send failed|all info to Pocoo pastebin service.")
|
||||
|
||||
def pytest_configure(__multicall__, config):
|
||||
import tempfile
|
||||
__multicall__.execute()
|
||||
if config.option.pastebin == "all":
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+')
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
oldwrite = tr._tw.write
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
tr._tw.write = tee_write
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_pastebinfile'):
|
||||
config._pastebinfile.seek(0)
|
||||
sessionlog = config._pastebinfile.read()
|
||||
config._pastebinfile.close()
|
||||
del config._pastebinfile
|
||||
proxyid = getproxy().newPaste("python", sessionlog)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
sys.stderr.write("pastebin session-log: %s\n" % pastebinurl)
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
del tr._tw.__dict__['write']
|
||||
|
||||
def getproxy():
|
||||
return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
if terminalreporter.config.option.pastebin != "failed":
|
||||
return
|
||||
tr = terminalreporter
|
||||
if 'failed' in tr.stats:
|
||||
terminalreporter.write_sep("=", "Sending information to Paste Service")
|
||||
if tr.config.option.debug:
|
||||
terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,))
|
||||
serverproxy = getproxy()
|
||||
for rep in terminalreporter.stats.get('failed'):
|
||||
try:
|
||||
msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
|
||||
except AttributeError:
|
||||
msg = tr._getfailureheadline(rep)
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
rep.toterminal(tw)
|
||||
s = tw.stringio.getvalue()
|
||||
assert len(s)
|
||||
proxyid = serverproxy.newPaste("python", s)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
tr.write_line("%s --> %s" %(msg, pastebinurl))
|
||||
78
pytest/plugin/pytest_pdb.py
Normal file
78
pytest/plugin/pytest_pdb.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
interactive debugging with the Python Debugger.
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--pdb',
|
||||
action="store_true", dest="usepdb", default=False,
|
||||
help="start the interactive Python debugger on errors.")
|
||||
|
||||
def pytest_namespace():
|
||||
return {'set_trace': pytestPDB().set_trace}
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
|
||||
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
item = None
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
frame = sys._getframe().f_back
|
||||
item = getattr(self, 'item', None)
|
||||
if item is not None:
|
||||
capman = item.config.pluginmanager.getplugin("capturemanager")
|
||||
out, err = capman.suspendcapture()
|
||||
if hasattr(item, 'outerr'):
|
||||
item.outerr = (item.outerr[0] + out, item.outerr[1] + err)
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
py.std.pdb.Pdb().set_trace(frame)
|
||||
|
||||
def pdbitem(item):
|
||||
pytestPDB.item = item
|
||||
pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem
|
||||
|
||||
def pytest_runtest_makereport():
|
||||
pytestPDB.item = None
|
||||
|
||||
class PdbInvoke:
|
||||
def pytest_sessionfinish(self, session):
|
||||
# don't display failures again at the end
|
||||
session.config.option.tbstyle = "no"
|
||||
def pytest_runtest_makereport(self, item, call, __multicall__):
|
||||
if not call.excinfo or \
|
||||
call.excinfo.errisinstance(py.test.skip.Exception) or \
|
||||
call.excinfo.errisinstance(py.std.bdb.BdbQuit):
|
||||
return
|
||||
rep = __multicall__.execute()
|
||||
if "xfail" in rep.keywords:
|
||||
return rep
|
||||
# we assume that the above execute() suspended capturing
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line()
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
post_mortem(call.excinfo._excinfo[2])
|
||||
return rep
|
||||
|
||||
def post_mortem(t):
|
||||
pdb = py.std.pdb
|
||||
class Pdb(pdb.Pdb):
|
||||
def get_stack(self, f, t):
|
||||
stack, i = pdb.Pdb.get_stack(self, f, t)
|
||||
if f is None:
|
||||
i = max(0, len(stack) - 1)
|
||||
while i and stack[i][0].f_locals.get("__tracebackhide__", False):
|
||||
i-=1
|
||||
return stack, i
|
||||
p = Pdb()
|
||||
p.reset()
|
||||
p.interaction(None, t)
|
||||
36
pytest/plugin/pytest_pylint.py
Normal file
36
pytest/plugin/pytest_pylint.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""pylint plugin
|
||||
|
||||
XXX: Currently in progress, NOT IN WORKING STATE.
|
||||
"""
|
||||
import py
|
||||
|
||||
pylint = py.test.importorskip("pylint.lint")
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('pylint options')
|
||||
group.addoption('--pylint', action='store_true',
|
||||
default=False, dest='pylint',
|
||||
help='run pylint on python files.')
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
if path.ext == ".py":
|
||||
if parent.config.getvalue('pylint'):
|
||||
return PylintItem(path, parent)
|
||||
|
||||
#def pytest_terminal_summary(terminalreporter):
|
||||
# print 'placeholder for pylint output'
|
||||
|
||||
class PylintItem(py.test.collect.Item):
|
||||
def runtest(self):
|
||||
capture = py.io.StdCaptureFD()
|
||||
try:
|
||||
linter = pylint.lint.PyLinter()
|
||||
linter.check(str(self.fspath))
|
||||
finally:
|
||||
out, err = capture.reset()
|
||||
rating = out.strip().split('\n')[-1]
|
||||
sys.stdout.write(">>>")
|
||||
print(rating)
|
||||
assert 0
|
||||
|
||||
|
||||
513
pytest/plugin/pytest_pytester.py
Normal file
513
pytest/plugin/pytest_pytester.py
Normal file
@@ -0,0 +1,513 @@
|
||||
"""
|
||||
funcargs and support code for testing py.test's own functionality.
|
||||
"""
|
||||
|
||||
import py
|
||||
import sys, os
|
||||
import re
|
||||
import inspect
|
||||
import time
|
||||
from fnmatch import fnmatch
|
||||
from pytest.config import Config as pytestConfig
|
||||
from py.builtin import print_
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("pylib")
|
||||
group.addoption('--no-tools-on-path',
|
||||
action="store_true", dest="notoolsonpath", default=False,
|
||||
help=("discover tools on PATH instead of going through py.cmdline.")
|
||||
)
|
||||
|
||||
pytest_plugins = '_pytest'
|
||||
|
||||
def pytest_funcarg__linecomp(request):
|
||||
return LineComp()
|
||||
|
||||
def pytest_funcarg__LineMatcher(request):
|
||||
return LineMatcher
|
||||
|
||||
def pytest_funcarg__testdir(request):
|
||||
tmptestdir = TmpTestdir(request)
|
||||
return tmptestdir
|
||||
|
||||
rex_outcome = re.compile("(\d+) (\w+)")
|
||||
class RunResult:
|
||||
def __init__(self, ret, outlines, errlines, duration):
|
||||
self.ret = ret
|
||||
self.outlines = outlines
|
||||
self.errlines = errlines
|
||||
self.stdout = LineMatcher(outlines)
|
||||
self.stderr = LineMatcher(errlines)
|
||||
self.duration = duration
|
||||
|
||||
def parseoutcomes(self):
|
||||
for line in reversed(self.outlines):
|
||||
if 'seconds' in line:
|
||||
outcomes = rex_outcome.findall(line)
|
||||
if outcomes:
|
||||
d = {}
|
||||
for num, cat in outcomes:
|
||||
d[cat] = int(num)
|
||||
return d
|
||||
|
||||
class TmpTestdir:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self._pytest = request.getfuncargvalue("_pytest")
|
||||
# XXX remove duplication with tmpdir plugin
|
||||
basetmp = request.config.ensuretemp("testdir")
|
||||
name = request.function.__name__
|
||||
for i in range(100):
|
||||
try:
|
||||
tmpdir = basetmp.mkdir(name + str(i))
|
||||
except py.error.EEXIST:
|
||||
continue
|
||||
break
|
||||
# we need to create another subdir
|
||||
# because Directory.collect() currently loads
|
||||
# conftest.py from sibling directories
|
||||
self.tmpdir = tmpdir.mkdir(name)
|
||||
self.plugins = []
|
||||
self._syspathremove = []
|
||||
self.chdir() # always chdir
|
||||
self.request.addfinalizer(self.finalize)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TmpTestdir %r>" % (self.tmpdir,)
|
||||
|
||||
def Config(self):
|
||||
return pytestConfig()
|
||||
|
||||
def finalize(self):
|
||||
for p in self._syspathremove:
|
||||
py.std.sys.path.remove(p)
|
||||
if hasattr(self, '_olddir'):
|
||||
self._olddir.chdir()
|
||||
# delete modules that have been loaded from tmpdir
|
||||
for name, mod in list(sys.modules.items()):
|
||||
if mod:
|
||||
fn = getattr(mod, '__file__', None)
|
||||
if fn and fn.startswith(str(self.tmpdir)):
|
||||
del sys.modules[name]
|
||||
|
||||
def getreportrecorder(self, obj):
|
||||
if hasattr(obj, 'config'):
|
||||
obj = obj.config
|
||||
if hasattr(obj, 'hook'):
|
||||
obj = obj.hook
|
||||
assert hasattr(obj, '_hookspecs'), obj
|
||||
reprec = ReportRecorder(obj)
|
||||
reprec.hookrecorder = self._pytest.gethookrecorder(obj)
|
||||
reprec.hook = reprec.hookrecorder.hook
|
||||
return reprec
|
||||
|
||||
def chdir(self):
|
||||
old = self.tmpdir.chdir()
|
||||
if not hasattr(self, '_olddir'):
|
||||
self._olddir = old
|
||||
|
||||
def _makefile(self, ext, args, kwargs):
|
||||
items = list(kwargs.items())
|
||||
if args:
|
||||
source = "\n".join(map(str, args)) + "\n"
|
||||
basename = self.request.function.__name__
|
||||
items.insert(0, (basename, source))
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
source = str(py.code.Source(value)).lstrip()
|
||||
p.write(source.encode("utf-8"), "wb")
|
||||
if ret is None:
|
||||
ret = p
|
||||
return ret
|
||||
|
||||
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
return self._makefile(ext, args, kwargs)
|
||||
|
||||
def makeconftest(self, source):
|
||||
return self.makepyfile(conftest=source)
|
||||
|
||||
def makepyfile(self, *args, **kwargs):
|
||||
return self._makefile('.py', args, kwargs)
|
||||
|
||||
def maketxtfile(self, *args, **kwargs):
|
||||
return self._makefile('.txt', args, kwargs)
|
||||
|
||||
def syspathinsert(self, path=None):
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
py.std.sys.path.insert(0, str(path))
|
||||
self._syspathremove.append(str(path))
|
||||
|
||||
def mkdir(self, name):
|
||||
return self.tmpdir.mkdir(name)
|
||||
|
||||
def mkpydir(self, name):
|
||||
p = self.mkdir(name)
|
||||
p.ensure("__init__.py")
|
||||
return p
|
||||
|
||||
def getnode(self, config, arg):
|
||||
from pytest.session import Collection
|
||||
collection = Collection(config)
|
||||
return collection.getbyid(collection._normalizearg(arg))[0]
|
||||
|
||||
def genitems(self, colitems):
|
||||
collection = colitems[0].collection
|
||||
result = []
|
||||
collection.genitems(colitems, (), result)
|
||||
return result
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
#config = self.parseconfig(*args)
|
||||
from pytest.session import Collection
|
||||
config = self.parseconfigure(*args)
|
||||
rec = self.getreportrecorder(config)
|
||||
items = Collection(config).perform_collect()
|
||||
return items, rec
|
||||
|
||||
def runitem(self, source):
|
||||
# used from runner functional tests
|
||||
item = self.getitem(source)
|
||||
# the test class where we are called from wants to provide the runner
|
||||
testclassinstance = py.builtin._getimself(self.request.function)
|
||||
runner = testclassinstance.getrunner()
|
||||
return runner(item)
|
||||
|
||||
def inline_runsource(self, source, *cmdlineargs):
|
||||
p = self.makepyfile(source)
|
||||
l = list(cmdlineargs) + [p]
|
||||
return self.inline_run(*l)
|
||||
|
||||
def inline_runsource1(self, *args):
|
||||
args = list(args)
|
||||
source = args.pop()
|
||||
p = self.makepyfile(source)
|
||||
l = list(args) + [p]
|
||||
reprec = self.inline_run(*l)
|
||||
reports = reprec.getreports("pytest_runtest_logreport")
|
||||
assert len(reports) == 1, reports
|
||||
return reports[0]
|
||||
|
||||
def inline_run(self, *args):
|
||||
args = ("-s", ) + args # otherwise FD leakage
|
||||
config = self.parseconfig(*args)
|
||||
reprec = self.getreportrecorder(config)
|
||||
#config.pluginmanager.do_configure(config)
|
||||
config.hook.pytest_cmdline_main(config=config)
|
||||
#config.pluginmanager.do_unconfigure(config)
|
||||
return reprec
|
||||
|
||||
def config_preparse(self):
|
||||
config = self.Config()
|
||||
for plugin in self.plugins:
|
||||
if isinstance(plugin, str):
|
||||
config.pluginmanager.import_plugin(plugin)
|
||||
else:
|
||||
if isinstance(plugin, dict):
|
||||
plugin = PseudoPlugin(plugin)
|
||||
if not config.pluginmanager.isregistered(plugin):
|
||||
config.pluginmanager.register(plugin)
|
||||
return config
|
||||
|
||||
def parseconfig(self, *args):
|
||||
if not args:
|
||||
args = (self.tmpdir,)
|
||||
config = self.config_preparse()
|
||||
args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')]
|
||||
config.parse(args)
|
||||
return config
|
||||
|
||||
def reparseconfig(self, args=None):
|
||||
""" this is used from tests that want to re-invoke parse(). """
|
||||
if not args:
|
||||
args = [self.tmpdir]
|
||||
from pytest import config
|
||||
oldconfig = config.config_per_process # py.test.config
|
||||
try:
|
||||
c = config.config_per_process = py.test.config = pytestConfig()
|
||||
c.basetemp = oldconfig.mktemp("reparse", numbered=True)
|
||||
c.parse(args)
|
||||
return c
|
||||
finally:
|
||||
config.config_per_process = py.test.config = oldconfig
|
||||
|
||||
def parseconfigure(self, *args):
|
||||
config = self.parseconfig(*args)
|
||||
config.pluginmanager.do_configure(config)
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
modcol = self.getmodulecol(source)
|
||||
moditems = modcol.collect()
|
||||
for item in modcol.collect():
|
||||
if item.name == funcname:
|
||||
return item
|
||||
else:
|
||||
assert 0, "%r item not found in module:\n%s" %(funcname, source)
|
||||
|
||||
def getitems(self, source):
|
||||
modcol = self.getmodulecol(source)
|
||||
return self.genitems([modcol])
|
||||
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
kw = {self.request.function.__name__: py.code.Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__ = "#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
#config.pluginmanager.do_unconfigure(config)
|
||||
return node
|
||||
|
||||
def popen(self, cmdargs, stdout, stderr, **kw):
|
||||
if not hasattr(py.std, 'subprocess'):
|
||||
py.test.skip("no subprocess module")
|
||||
env = os.environ.copy()
|
||||
env['PYTHONPATH'] = ":".join(filter(None, [
|
||||
str(os.getcwd()), env.get('PYTHONPATH', '')]))
|
||||
kw['env'] = env
|
||||
#print "env", env
|
||||
return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
|
||||
|
||||
def run(self, *cmdargs):
|
||||
return self._run(*cmdargs)
|
||||
|
||||
def _run(self, *cmdargs):
|
||||
cmdargs = [str(x) for x in cmdargs]
|
||||
p1 = self.tmpdir.join("stdout")
|
||||
p2 = self.tmpdir.join("stderr")
|
||||
print_("running", cmdargs, "curdir=", py.path.local())
|
||||
f1 = p1.open("wb")
|
||||
f2 = p2.open("wb")
|
||||
now = time.time()
|
||||
popen = self.popen(cmdargs, stdout=f1, stderr=f2,
|
||||
close_fds=(sys.platform != "win32"))
|
||||
ret = popen.wait()
|
||||
f1.close()
|
||||
f2.close()
|
||||
out = p1.read("rb")
|
||||
out = getdecoded(out).splitlines()
|
||||
err = p2.read("rb")
|
||||
err = getdecoded(err).splitlines()
|
||||
def dump_lines(lines, fp):
|
||||
try:
|
||||
for line in lines:
|
||||
py.builtin.print_(line, file=fp)
|
||||
except UnicodeEncodeError:
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
dump_lines(out, sys.stdout)
|
||||
dump_lines(err, sys.stderr)
|
||||
return RunResult(ret, out, err, time.time()-now)
|
||||
|
||||
def runpybin(self, scriptname, *args):
|
||||
fullargs = self._getpybinargs(scriptname) + args
|
||||
return self.run(*fullargs)
|
||||
|
||||
def _getpybinargs(self, scriptname):
|
||||
if not self.request.config.getvalue("notoolsonpath"):
|
||||
script = py.path.local.sysfind(scriptname)
|
||||
assert script, "script %r not found" % scriptname
|
||||
return (script,)
|
||||
else:
|
||||
cmdlinename = scriptname.replace(".", "")
|
||||
assert hasattr(py.cmdline, cmdlinename), cmdlinename
|
||||
source = ("import sys;sys.path.insert(0,%r);"
|
||||
"import py;py.cmdline.%s()" %
|
||||
(str(py._pydir.dirpath()), cmdlinename))
|
||||
return (sys.executable, "-c", source,)
|
||||
|
||||
def runpython(self, script, prepend=True):
|
||||
if prepend:
|
||||
s = self._getsysprepend()
|
||||
if s:
|
||||
script.write(s + "\n" + script.read())
|
||||
return self.run(sys.executable, script)
|
||||
|
||||
def _getsysprepend(self):
|
||||
if self.request.config.getvalue("notoolsonpath"):
|
||||
s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath())
|
||||
else:
|
||||
s = ""
|
||||
return s
|
||||
|
||||
def runpython_c(self, command):
|
||||
command = self._getsysprepend() + command
|
||||
return self.run(py.std.sys.executable, "-c", command)
|
||||
|
||||
def runpytest(self, *args):
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-",
|
||||
keep=None, rootdir=self.tmpdir)
|
||||
args = ('--basetemp=%s' % p, ) + args
|
||||
for x in args:
|
||||
if '--confcutdir' in str(x):
|
||||
break
|
||||
else:
|
||||
args = ('--confcutdir=.',) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
args = ('-p', plugins[0]) + args
|
||||
return self.runpybin("py.test", *args)
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
if self.request.config.getvalue("notoolsonpath"):
|
||||
py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests")
|
||||
basetemp = self.tmpdir.mkdir("pexpect")
|
||||
invoke = self._getpybinargs("py.test")[0]
|
||||
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
||||
def spawn(self, cmd, expect_timeout=10.0):
|
||||
pexpect = py.test.importorskip("pexpect", "2.4")
|
||||
logfile = self.tmpdir.join("spawn.out")
|
||||
child = pexpect.spawn(cmd, logfile=logfile.open("w"))
|
||||
child.timeout = expect_timeout
|
||||
return child
|
||||
|
||||
def getdecoded(out):
|
||||
try:
|
||||
return out.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
|
||||
class PseudoPlugin:
|
||||
def __init__(self, vars):
|
||||
self.__dict__.update(vars)
|
||||
|
||||
class ReportRecorder(object):
|
||||
def __init__(self, hook):
|
||||
self.hook = hook
|
||||
self.registry = hook._registry
|
||||
self.registry.register(self)
|
||||
|
||||
def getcall(self, name):
|
||||
return self.hookrecorder.getcall(name)
|
||||
|
||||
def popcall(self, name):
|
||||
return self.hookrecorder.popcall(name)
|
||||
|
||||
def getcalls(self, names):
|
||||
""" return list of ParsedCall instances matching the given eventname. """
|
||||
return self.hookrecorder.getcalls(names)
|
||||
|
||||
# functionality for test reports
|
||||
|
||||
def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
for rep in self.getreports(names=names):
|
||||
if not inamepart or inamepart in rep.nodenames:
|
||||
l.append(rep)
|
||||
if not l:
|
||||
raise ValueError("could not find test report matching %r: no test reports at all!" %
|
||||
(inamepart,))
|
||||
if len(l) > 1:
|
||||
raise ValueError("found more than one testreport matching %r: %s" %(
|
||||
inamepart, l))
|
||||
return l[0]
|
||||
|
||||
def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'):
|
||||
return [rep for rep in self.getreports(names) if rep.failed]
|
||||
|
||||
def getfailedcollections(self):
|
||||
return self.getfailures('pytest_collectreport')
|
||||
|
||||
def listoutcomes(self):
|
||||
passed = []
|
||||
skipped = []
|
||||
failed = []
|
||||
for rep in self.getreports("pytest_runtest_logreport"):
|
||||
if rep.passed:
|
||||
if rep.when == "call":
|
||||
passed.append(rep)
|
||||
elif rep.skipped:
|
||||
skipped.append(rep)
|
||||
elif rep.failed:
|
||||
failed.append(rep)
|
||||
return passed, skipped, failed
|
||||
|
||||
def countoutcomes(self):
|
||||
return [len(x) for x in self.listoutcomes()]
|
||||
|
||||
def assertoutcome(self, passed=0, skipped=0, failed=0):
|
||||
realpassed, realskipped, realfailed = self.listoutcomes()
|
||||
assert passed == len(realpassed)
|
||||
assert skipped == len(realskipped)
|
||||
assert failed == len(realfailed)
|
||||
|
||||
def clear(self):
|
||||
self.hookrecorder.calls[:] = []
|
||||
|
||||
def unregister(self):
|
||||
self.registry.unregister(self)
|
||||
self.hookrecorder.finish_recording()
|
||||
|
||||
class LineComp:
|
||||
def __init__(self):
|
||||
self.stringio = py.io.TextIO()
|
||||
|
||||
def assert_contains_lines(self, lines2):
|
||||
""" assert that lines2 are contained (linearly) in lines1.
|
||||
return a list of extralines found.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
val = self.stringio.getvalue()
|
||||
self.stringio.truncate(0)
|
||||
self.stringio.seek(0)
|
||||
lines1 = val.split("\n")
|
||||
return LineMatcher(lines1).fnmatch_lines(lines2)
|
||||
|
||||
class LineMatcher:
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
|
||||
def str(self):
|
||||
return "\n".join(self.lines)
|
||||
|
||||
def _getlines(self, lines2):
|
||||
if isinstance(lines2, str):
|
||||
lines2 = py.code.Source(lines2)
|
||||
if isinstance(lines2, py.code.Source):
|
||||
lines2 = lines2.strip().lines
|
||||
return lines2
|
||||
|
||||
def fnmatch_lines_random(self, lines2):
|
||||
lines2 = self._getlines(lines2)
|
||||
for line in lines2:
|
||||
for x in self.lines:
|
||||
if line == x or fnmatch(x, line):
|
||||
print_("matched: ", repr(line))
|
||||
break
|
||||
else:
|
||||
raise ValueError("line %r not found in output" % line)
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
nextline = None
|
||||
extralines = []
|
||||
__tracebackhide__ = True
|
||||
for line in lines2:
|
||||
nomatchprinted = False
|
||||
while lines1:
|
||||
nextline = lines1.pop(0)
|
||||
if line == nextline:
|
||||
print_("exact match:", repr(line))
|
||||
break
|
||||
elif fnmatch(nextline, line):
|
||||
print_("fnmatch:", repr(line))
|
||||
print_(" with:", repr(nextline))
|
||||
break
|
||||
else:
|
||||
if not nomatchprinted:
|
||||
print_("nomatch:", repr(line))
|
||||
nomatchprinted = True
|
||||
print_(" and:", repr(nextline))
|
||||
extralines.append(nextline)
|
||||
else:
|
||||
assert line == nextline
|
||||
701
pytest/plugin/pytest_python.py
Normal file
701
pytest/plugin/pytest_python.py
Normal file
@@ -0,0 +1,701 @@
|
||||
"""
|
||||
Python related collection nodes.
|
||||
"""
|
||||
import py
|
||||
import inspect
|
||||
import sys
|
||||
import pytest
|
||||
from pytest.collect import configproperty, warnoldcollect
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group._addoption('--funcargs',
|
||||
action="store_true", dest="showfuncargs", default=False,
|
||||
help="show available function arguments, sorted by plugin")
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.showfuncargs:
|
||||
showfuncargs(config)
|
||||
return 0
|
||||
|
||||
def pytest_namespace():
|
||||
return {'collect': {
|
||||
'Module': Module, 'Class': Class, 'Instance': Instance,
|
||||
'Function': Function, 'Generator': Generator,
|
||||
'_fillfuncargs': fillfuncargs}}
|
||||
|
||||
def pytest_funcarg__pytestconfig(request):
|
||||
""" the pytest config object with access to command line opts."""
|
||||
return request.config
|
||||
|
||||
def pytest_pyfunc_call(__multicall__, pyfuncitem):
|
||||
if not __multicall__.execute():
|
||||
testfunction = pyfuncitem.obj
|
||||
if pyfuncitem._isyieldedfunction():
|
||||
testfunction(*pyfuncitem._args)
|
||||
else:
|
||||
funcargs = pyfuncitem.funcargs
|
||||
testfunction(**funcargs)
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
ext = path.ext
|
||||
pb = path.purebasename
|
||||
if pb.startswith("test_") or pb.endswith("_test") or \
|
||||
path in parent.collection._argfspaths:
|
||||
if ext == ".py":
|
||||
return parent.ihook.pytest_pycollect_makemodule(
|
||||
path=path, parent=parent)
|
||||
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
return parent.Module(path, parent)
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
|
||||
res = __multicall__.execute()
|
||||
if res is not None:
|
||||
return res
|
||||
if collector._istestclasscandidate(name, obj):
|
||||
res = collector._deprecated_join(name)
|
||||
if res is not None:
|
||||
return res
|
||||
return collector.Class(name, parent=collector)
|
||||
elif collector.funcnamefilter(name) and hasattr(obj, '__call__'):
|
||||
res = collector._deprecated_join(name)
|
||||
if res is not None:
|
||||
return res
|
||||
if is_generator(obj):
|
||||
# XXX deprecation warning
|
||||
return collector.Generator(name, parent=collector)
|
||||
else:
|
||||
return collector._genfunctions(name, obj)
|
||||
|
||||
def is_generator(func):
|
||||
try:
|
||||
return py.code.getrawcode(func).co_flags & 32 # generator function
|
||||
except AttributeError: # builtin functions have no bytecode
|
||||
# assume them to not be generators
|
||||
return False
|
||||
|
||||
class PyobjMixin(object):
|
||||
def obj():
|
||||
def fget(self):
|
||||
try:
|
||||
return self._obj
|
||||
except AttributeError:
|
||||
self._obj = obj = self._getobj()
|
||||
return obj
|
||||
def fset(self, value):
|
||||
self._obj = value
|
||||
return property(fget, fset, None, "underlying python object")
|
||||
obj = obj()
|
||||
|
||||
def _getobj(self):
|
||||
return getattr(self.parent.obj, self.name)
|
||||
|
||||
def getmodpath(self, stopatmodule=True, includemodule=False):
|
||||
""" return python path relative to the containing module. """
|
||||
chain = self.listchain()
|
||||
chain.reverse()
|
||||
parts = []
|
||||
for node in chain:
|
||||
if isinstance(node, Instance):
|
||||
continue
|
||||
name = node.name
|
||||
if isinstance(node, Module):
|
||||
assert name.endswith(".py")
|
||||
name = name[:-3]
|
||||
if stopatmodule:
|
||||
if includemodule:
|
||||
parts.append(name)
|
||||
break
|
||||
parts.append(name)
|
||||
parts.reverse()
|
||||
s = ".".join(parts)
|
||||
return s.replace(".[", "[")
|
||||
|
||||
def _getfslineno(self):
|
||||
try:
|
||||
return self._fslineno
|
||||
except AttributeError:
|
||||
pass
|
||||
obj = self.obj
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
if hasattr(obj, 'place_as'):
|
||||
obj = obj.place_as
|
||||
|
||||
self._fslineno = py.code.getfslineno(obj)
|
||||
return self._fslineno
|
||||
|
||||
def reportinfo(self):
|
||||
fspath, lineno = self._getfslineno()
|
||||
modpath = self.getmodpath()
|
||||
return fspath, lineno, modpath
|
||||
|
||||
class PyCollectorMixin(PyobjMixin, pytest.collect.Collector):
|
||||
Class = configproperty('Class')
|
||||
Instance = configproperty('Instance')
|
||||
Function = configproperty('Function')
|
||||
Generator = configproperty('Generator')
|
||||
|
||||
def funcnamefilter(self, name):
|
||||
return name.startswith('test')
|
||||
def classnamefilter(self, name):
|
||||
return name.startswith('Test')
|
||||
|
||||
def collect(self):
|
||||
l = self._deprecated_collect()
|
||||
if l is not None:
|
||||
return l
|
||||
# NB. we avoid random getattrs and peek in the __dict__ instead
|
||||
dicts = [getattr(self.obj, '__dict__', {})]
|
||||
for basecls in inspect.getmro(self.obj.__class__):
|
||||
dicts.append(basecls.__dict__)
|
||||
seen = {}
|
||||
l = []
|
||||
for dic in dicts:
|
||||
for name, obj in dic.items():
|
||||
if name in seen:
|
||||
continue
|
||||
seen[name] = True
|
||||
if name[0] != "_":
|
||||
res = self.makeitem(name, obj)
|
||||
if res is None:
|
||||
continue
|
||||
if not isinstance(res, list):
|
||||
res = [res]
|
||||
l.extend(res)
|
||||
l.sort(key=lambda item: item.reportinfo()[:2])
|
||||
return l
|
||||
|
||||
def _deprecated_join(self, name):
|
||||
if self.__class__.join != pytest.collect.Collector.join:
|
||||
warnoldcollect()
|
||||
return self.join(name)
|
||||
|
||||
def makeitem(self, name, obj):
|
||||
return self.ihook.pytest_pycollect_makeitem(
|
||||
collector=self, name=name, obj=obj)
|
||||
|
||||
def _istestclasscandidate(self, name, obj):
|
||||
if self.classnamefilter(name) and \
|
||||
inspect.isclass(obj):
|
||||
if hasinit(obj):
|
||||
# XXX WARN
|
||||
return False
|
||||
return True
|
||||
|
||||
def _genfunctions(self, name, funcobj):
|
||||
module = self.getparent(Module).obj
|
||||
clscol = self.getparent(Class)
|
||||
cls = clscol and clscol.obj or None
|
||||
metafunc = Metafunc(funcobj, config=self.config,
|
||||
cls=cls, module=module)
|
||||
gentesthook = self.config.hook.pytest_generate_tests
|
||||
plugins = getplugins(self, withpy=True)
|
||||
gentesthook.pcall(plugins, metafunc=metafunc)
|
||||
if not metafunc._calls:
|
||||
return self.Function(name, parent=self)
|
||||
l = []
|
||||
for callspec in metafunc._calls:
|
||||
subname = "%s[%s]" %(name, callspec.id)
|
||||
function = self.Function(name=subname, parent=self,
|
||||
callspec=callspec, callobj=funcobj)
|
||||
l.append(function)
|
||||
return l
|
||||
|
||||
class Module(pytest.collect.File, PyCollectorMixin):
|
||||
def _getobj(self):
|
||||
return self._memoizedcall('_obj', self._importtestmodule)
|
||||
|
||||
def _importtestmodule(self):
|
||||
# we assume we are only called once per module
|
||||
try:
|
||||
mod = self.fspath.pyimport(ensuresyspath=True)
|
||||
except SyntaxError:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
raise self.CollectError(excinfo.getrepr(style="short"))
|
||||
except self.fspath.ImportMismatchError:
|
||||
e = sys.exc_info()[1]
|
||||
raise self.CollectError(
|
||||
"import file mismatch:\n"
|
||||
"imported module %r has this __file__ attribute:\n"
|
||||
" %s\n"
|
||||
"which is not the same as the test file we want to collect:\n"
|
||||
" %s\n"
|
||||
"HINT: use a unique basename for your test file modules"
|
||||
% e.args
|
||||
)
|
||||
#print "imported test module", mod
|
||||
self.config.pluginmanager.consider_module(mod)
|
||||
return mod
|
||||
|
||||
def setup(self):
|
||||
if getattr(self.obj, 'disabled', 0):
|
||||
py.log._apiwarn(">1.1.1", "%r uses 'disabled' which is deprecated, "
|
||||
"use pytestmark=..., see pytest_skipping plugin" % (self.obj,))
|
||||
pytest.skip("%r is disabled" %(self.obj,))
|
||||
if hasattr(self.obj, 'setup_module'):
|
||||
#XXX: nose compat hack, move to nose plugin
|
||||
# if it takes a positional arg, its probably a pytest style one
|
||||
# so we pass the current module object
|
||||
if inspect.getargspec(self.obj.setup_module)[0]:
|
||||
self.obj.setup_module(self.obj)
|
||||
else:
|
||||
self.obj.setup_module()
|
||||
|
||||
def teardown(self):
|
||||
if hasattr(self.obj, 'teardown_module'):
|
||||
#XXX: nose compat hack, move to nose plugin
|
||||
# if it takes a positional arg, its probably a py.test style one
|
||||
# so we pass the current module object
|
||||
if inspect.getargspec(self.obj.teardown_module)[0]:
|
||||
self.obj.teardown_module(self.obj)
|
||||
else:
|
||||
self.obj.teardown_module()
|
||||
|
||||
class Class(PyCollectorMixin, pytest.collect.Collector):
|
||||
|
||||
def collect(self):
|
||||
l = self._deprecated_collect()
|
||||
if l is not None:
|
||||
return l
|
||||
return [self.Instance(name="()", parent=self)]
|
||||
|
||||
def setup(self):
|
||||
if getattr(self.obj, 'disabled', 0):
|
||||
py.log._apiwarn(">1.1.1", "%r uses 'disabled' which is deprecated, "
|
||||
"use pytestmark=..., see pytest_skipping plugin" % (self.obj,))
|
||||
pytest.skip("%r is disabled" %(self.obj,))
|
||||
setup_class = getattr(self.obj, 'setup_class', None)
|
||||
if setup_class is not None:
|
||||
setup_class = getattr(setup_class, 'im_func', setup_class)
|
||||
setup_class(self.obj)
|
||||
|
||||
def teardown(self):
|
||||
teardown_class = getattr(self.obj, 'teardown_class', None)
|
||||
if teardown_class is not None:
|
||||
teardown_class = getattr(teardown_class, 'im_func', teardown_class)
|
||||
teardown_class(self.obj)
|
||||
|
||||
class Instance(PyCollectorMixin, pytest.collect.Collector):
|
||||
def _getobj(self):
|
||||
return self.parent.obj()
|
||||
|
||||
def _keywords(self):
|
||||
return []
|
||||
|
||||
def newinstance(self):
|
||||
self.obj = self._getobj()
|
||||
return self.obj
|
||||
|
||||
class FunctionMixin(PyobjMixin):
|
||||
""" mixin for the code common to Function and Generator.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
""" perform setup for this test function. """
|
||||
if inspect.ismethod(self.obj):
|
||||
name = 'setup_method'
|
||||
else:
|
||||
name = 'setup_function'
|
||||
if isinstance(self.parent, Instance):
|
||||
obj = self.parent.newinstance()
|
||||
self.obj = self._getobj()
|
||||
else:
|
||||
obj = self.parent.obj
|
||||
setup_func_or_method = getattr(obj, name, None)
|
||||
if setup_func_or_method is not None:
|
||||
setup_func_or_method(self.obj)
|
||||
|
||||
def teardown(self):
|
||||
""" perform teardown for this test function. """
|
||||
if inspect.ismethod(self.obj):
|
||||
name = 'teardown_method'
|
||||
else:
|
||||
name = 'teardown_function'
|
||||
obj = self.parent.obj
|
||||
teardown_func_or_meth = getattr(obj, name, None)
|
||||
if teardown_func_or_meth is not None:
|
||||
teardown_func_or_meth(self.obj)
|
||||
|
||||
def _prunetraceback(self, traceback):
|
||||
if hasattr(self, '_obj') and not self.config.option.fulltrace:
|
||||
code = py.code.Code(self.obj)
|
||||
path, firstlineno = code.path, code.firstlineno
|
||||
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(path=path)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=py._pydir)
|
||||
traceback = ntraceback.filter()
|
||||
return traceback
|
||||
|
||||
def _repr_failure_py(self, excinfo, style="long"):
|
||||
if excinfo.errisinstance(FuncargRequest.LookupError):
|
||||
fspath, lineno, msg = self.reportinfo()
|
||||
lines, _ = inspect.getsourcelines(self.obj)
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('def'):
|
||||
return FuncargLookupErrorRepr(fspath, lineno,
|
||||
lines[:i+1], str(excinfo.value))
|
||||
return super(FunctionMixin, self)._repr_failure_py(excinfo,
|
||||
style=style)
|
||||
|
||||
def repr_failure(self, excinfo, outerr=None):
|
||||
assert outerr is None, "XXX outerr usage is deprecated"
|
||||
return self._repr_failure_py(excinfo,
|
||||
style=self.config.getvalue("tbstyle"))
|
||||
|
||||
shortfailurerepr = "F"
|
||||
|
||||
class FuncargLookupErrorRepr(TerminalRepr):
|
||||
def __init__(self, filename, firstlineno, deflines, errorstring):
|
||||
self.deflines = deflines
|
||||
self.errorstring = errorstring
|
||||
self.filename = filename
|
||||
self.firstlineno = firstlineno
|
||||
|
||||
def toterminal(self, tw):
|
||||
tw.line()
|
||||
for line in self.deflines:
|
||||
tw.line(" " + line.strip())
|
||||
for line in self.errorstring.split("\n"):
|
||||
tw.line(" " + line.strip(), red=True)
|
||||
tw.line()
|
||||
tw.line("%s:%d" % (self.filename, self.firstlineno+1))
|
||||
|
||||
class Generator(FunctionMixin, PyCollectorMixin, pytest.collect.Collector):
|
||||
def collect(self):
|
||||
# test generators are seen as collectors but they also
|
||||
# invoke setup/teardown on popular request
|
||||
# (induced by the common "test_*" naming shared with normal tests)
|
||||
self.config._setupstate.prepare(self)
|
||||
l = []
|
||||
seen = {}
|
||||
for i, x in enumerate(self.obj()):
|
||||
name, call, args = self.getcallargs(x)
|
||||
if not py.builtin.callable(call):
|
||||
raise TypeError("%r yielded non callable test %r" %(self.obj, call,))
|
||||
if name is None:
|
||||
name = "[%d]" % i
|
||||
else:
|
||||
name = "['%s']" % name
|
||||
if name in seen:
|
||||
raise ValueError("%r generated tests with non-unique name %r" %(self, name))
|
||||
seen[name] = True
|
||||
l.append(self.Function(name, self, args=args, callobj=call))
|
||||
return l
|
||||
|
||||
def getcallargs(self, obj):
|
||||
if not isinstance(obj, (tuple, list)):
|
||||
obj = (obj,)
|
||||
# explict naming
|
||||
if isinstance(obj[0], py.builtin._basestring):
|
||||
name = obj[0]
|
||||
obj = obj[1:]
|
||||
else:
|
||||
name = None
|
||||
call, args = obj[0], obj[1:]
|
||||
return name, call, args
|
||||
|
||||
|
||||
#
|
||||
# Test Items
|
||||
#
|
||||
_dummy = object()
|
||||
class Function(FunctionMixin, pytest.collect.Item):
|
||||
""" a Function Item is responsible for setting up
|
||||
and executing a Python callable test object.
|
||||
"""
|
||||
_genid = None
|
||||
def __init__(self, name, parent=None, args=None, config=None,
|
||||
callspec=None, callobj=_dummy, collection=None):
|
||||
super(Function, self).__init__(name, parent,
|
||||
config=config, collection=collection)
|
||||
self._args = args
|
||||
if self._isyieldedfunction():
|
||||
assert not callspec, "yielded functions (deprecated) cannot have funcargs"
|
||||
else:
|
||||
if callspec is not None:
|
||||
self.funcargs = callspec.funcargs or {}
|
||||
self._genid = callspec.id
|
||||
if hasattr(callspec, "param"):
|
||||
self._requestparam = callspec.param
|
||||
else:
|
||||
self.funcargs = {}
|
||||
if callobj is not _dummy:
|
||||
self._obj = callobj
|
||||
self.function = getattr(self.obj, 'im_func', self.obj)
|
||||
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
|
||||
|
||||
def _getobj(self):
|
||||
name = self.name
|
||||
i = name.find("[") # parametrization
|
||||
if i != -1:
|
||||
name = name[:i]
|
||||
return getattr(self.parent.obj, name)
|
||||
|
||||
def _isyieldedfunction(self):
|
||||
return self._args is not None
|
||||
|
||||
def runtest(self):
|
||||
""" execute the underlying test function. """
|
||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||
|
||||
def setup(self):
|
||||
super(Function, self).setup()
|
||||
if hasattr(self, 'funcargs'):
|
||||
fillfuncargs(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
return (self.name == other.name and
|
||||
self._args == other._args and
|
||||
self.parent == other.parent and
|
||||
self.obj == other.obj and
|
||||
getattr(self, '_genid', None) ==
|
||||
getattr(other, '_genid', None)
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.parent, self.name))
|
||||
|
||||
def hasinit(obj):
|
||||
init = getattr(obj, '__init__', None)
|
||||
if init:
|
||||
if init != object.__init__:
|
||||
return True
|
||||
|
||||
|
||||
def getfuncargnames(function):
|
||||
argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0]
|
||||
startindex = py.std.inspect.ismethod(function) and 1 or 0
|
||||
defaults = getattr(function, 'func_defaults',
|
||||
getattr(function, '__defaults__', None)) or ()
|
||||
numdefaults = len(defaults)
|
||||
if numdefaults:
|
||||
return argnames[startindex:-numdefaults]
|
||||
return argnames[startindex:]
|
||||
|
||||
def fillfuncargs(function):
|
||||
""" fill missing funcargs. """
|
||||
request = FuncargRequest(pyfuncitem=function)
|
||||
request._fillfuncargs()
|
||||
|
||||
def getplugins(node, withpy=False): # might by any node
|
||||
plugins = node.config._getmatchingplugins(node.fspath)
|
||||
if withpy:
|
||||
mod = node.getparent(pytest.collect.Module)
|
||||
if mod is not None:
|
||||
plugins.append(mod.obj)
|
||||
inst = node.getparent(pytest.collect.Instance)
|
||||
if inst is not None:
|
||||
plugins.append(inst.obj)
|
||||
return plugins
|
||||
|
||||
_notexists = object()
|
||||
class CallSpec:
|
||||
def __init__(self, funcargs, id, param):
|
||||
self.funcargs = funcargs
|
||||
self.id = id
|
||||
if param is not _notexists:
|
||||
self.param = param
|
||||
def __repr__(self):
|
||||
return "<CallSpec id=%r param=%r funcargs=%r>" %(
|
||||
self.id, getattr(self, 'param', '?'), self.funcargs)
|
||||
|
||||
class Metafunc:
|
||||
def __init__(self, function, config=None, cls=None, module=None):
|
||||
self.config = config
|
||||
self.module = module
|
||||
self.function = function
|
||||
self.funcargnames = getfuncargnames(function)
|
||||
self.cls = cls
|
||||
self.module = module
|
||||
self._calls = []
|
||||
self._ids = py.builtin.set()
|
||||
|
||||
def addcall(self, funcargs=None, id=_notexists, param=_notexists):
|
||||
assert funcargs is None or isinstance(funcargs, dict)
|
||||
if id is None:
|
||||
raise ValueError("id=None not allowed")
|
||||
if id is _notexists:
|
||||
id = len(self._calls)
|
||||
id = str(id)
|
||||
if id in self._ids:
|
||||
raise ValueError("duplicate id %r" % id)
|
||||
self._ids.add(id)
|
||||
self._calls.append(CallSpec(funcargs, id, param))
|
||||
|
||||
class FuncargRequest:
|
||||
_argprefix = "pytest_funcarg__"
|
||||
_argname = None
|
||||
|
||||
class LookupError(LookupError):
|
||||
""" error on performing funcarg request. """
|
||||
|
||||
def __init__(self, pyfuncitem):
|
||||
self._pyfuncitem = pyfuncitem
|
||||
self.function = pyfuncitem.obj
|
||||
self.module = pyfuncitem.getparent(pytest.collect.Module).obj
|
||||
clscol = pyfuncitem.getparent(pytest.collect.Class)
|
||||
self.cls = clscol and clscol.obj or None
|
||||
self.instance = py.builtin._getimself(self.function)
|
||||
self.config = pyfuncitem.config
|
||||
self.fspath = pyfuncitem.fspath
|
||||
if hasattr(pyfuncitem, '_requestparam'):
|
||||
self.param = pyfuncitem._requestparam
|
||||
self._plugins = getplugins(pyfuncitem, withpy=True)
|
||||
self._funcargs = self._pyfuncitem.funcargs.copy()
|
||||
self._name2factory = {}
|
||||
self._currentarg = None
|
||||
|
||||
def _fillfuncargs(self):
|
||||
argnames = getfuncargnames(self.function)
|
||||
if argnames:
|
||||
assert not getattr(self._pyfuncitem, '_args', None), (
|
||||
"yielded functions cannot have funcargs")
|
||||
for argname in argnames:
|
||||
if argname not in self._pyfuncitem.funcargs:
|
||||
self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname)
|
||||
|
||||
|
||||
def applymarker(self, marker):
|
||||
""" apply a marker to a test function invocation.
|
||||
|
||||
The 'marker' must be created with pytest.mark.* XYZ.
|
||||
"""
|
||||
if not isinstance(marker, py.test.mark.XYZ.__class__):
|
||||
raise ValueError("%r is not a py.test.mark.* object")
|
||||
self._pyfuncitem.keywords[marker.markname] = marker
|
||||
|
||||
def cached_setup(self, setup, teardown=None, scope="module", extrakey=None):
|
||||
""" cache and return result of calling setup().
|
||||
|
||||
The requested argument name, the scope and the ``extrakey``
|
||||
determine the cache key. The scope also determines when
|
||||
teardown(result) will be called. valid scopes are:
|
||||
scope == 'function': when the single test function run finishes.
|
||||
scope == 'module': when tests in a different module are run
|
||||
scope == 'session': when tests of the session have run.
|
||||
"""
|
||||
if not hasattr(self.config, '_setupcache'):
|
||||
self.config._setupcache = {} # XXX weakref?
|
||||
cachekey = (self._currentarg, self._getscopeitem(scope), extrakey)
|
||||
cache = self.config._setupcache
|
||||
try:
|
||||
val = cache[cachekey]
|
||||
except KeyError:
|
||||
val = setup()
|
||||
cache[cachekey] = val
|
||||
if teardown is not None:
|
||||
def finalizer():
|
||||
del cache[cachekey]
|
||||
teardown(val)
|
||||
self._addfinalizer(finalizer, scope=scope)
|
||||
return val
|
||||
|
||||
def getfuncargvalue(self, argname):
|
||||
try:
|
||||
return self._funcargs[argname]
|
||||
except KeyError:
|
||||
pass
|
||||
if argname not in self._name2factory:
|
||||
self._name2factory[argname] = self.config.pluginmanager.listattr(
|
||||
plugins=self._plugins,
|
||||
attrname=self._argprefix + str(argname)
|
||||
)
|
||||
#else: we are called recursively
|
||||
if not self._name2factory[argname]:
|
||||
self._raiselookupfailed(argname)
|
||||
funcargfactory = self._name2factory[argname].pop()
|
||||
oldarg = self._currentarg
|
||||
self._currentarg = argname
|
||||
try:
|
||||
self._funcargs[argname] = res = funcargfactory(request=self)
|
||||
finally:
|
||||
self._currentarg = oldarg
|
||||
return res
|
||||
|
||||
def _getscopeitem(self, scope):
|
||||
if scope == "function":
|
||||
return self._pyfuncitem
|
||||
elif scope == "module":
|
||||
return self._pyfuncitem.getparent(py.test.collect.Module)
|
||||
elif scope == "session":
|
||||
return None
|
||||
raise ValueError("unknown finalization scope %r" %(scope,))
|
||||
|
||||
def _addfinalizer(self, finalizer, scope):
|
||||
colitem = self._getscopeitem(scope)
|
||||
self.config._setupstate.addfinalizer(
|
||||
finalizer=finalizer, colitem=colitem)
|
||||
|
||||
def addfinalizer(self, finalizer):
|
||||
""" call the given finalizer after test function finished execution. """
|
||||
self._addfinalizer(finalizer, scope="function")
|
||||
|
||||
def __repr__(self):
|
||||
return "<FuncargRequest for %r>" %(self._pyfuncitem)
|
||||
|
||||
def _raiselookupfailed(self, argname):
|
||||
available = []
|
||||
for plugin in self._plugins:
|
||||
for name in vars(plugin):
|
||||
if name.startswith(self._argprefix):
|
||||
name = name[len(self._argprefix):]
|
||||
if name not in available:
|
||||
available.append(name)
|
||||
fspath, lineno, msg = self._pyfuncitem.reportinfo()
|
||||
msg = "LookupError: no factory found for function argument %r" % (argname,)
|
||||
msg += "\n available funcargs: %s" %(", ".join(available),)
|
||||
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
|
||||
raise self.LookupError(msg)
|
||||
|
||||
def showfuncargs(config):
|
||||
from pytest.session import Collection
|
||||
collection = Collection(config)
|
||||
firstid = collection._normalizearg(config.args[0])
|
||||
colitem = collection.getbyid(firstid)[0]
|
||||
curdir = py.path.local()
|
||||
tw = py.io.TerminalWriter()
|
||||
plugins = getplugins(colitem, withpy=True)
|
||||
verbose = config.getvalue("verbose")
|
||||
for plugin in plugins:
|
||||
available = []
|
||||
for name, factory in vars(plugin).items():
|
||||
if name.startswith(FuncargRequest._argprefix):
|
||||
name = name[len(FuncargRequest._argprefix):]
|
||||
if name not in available:
|
||||
available.append([name, factory])
|
||||
if available:
|
||||
pluginname = plugin.__name__
|
||||
for name, factory in available:
|
||||
loc = getlocation(factory, curdir)
|
||||
if verbose:
|
||||
funcargspec = "%s -- %s" %(name, loc,)
|
||||
else:
|
||||
funcargspec = name
|
||||
tw.line(funcargspec, green=True)
|
||||
doc = factory.__doc__ or ""
|
||||
if doc:
|
||||
for line in doc.split("\n"):
|
||||
tw.line(" " + line.strip())
|
||||
else:
|
||||
tw.line(" %s: no docstring available" %(loc,),
|
||||
red=True)
|
||||
|
||||
def getlocation(function, curdir):
|
||||
import inspect
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = py.builtin._getcode(function).co_firstlineno
|
||||
if fn.relto(curdir):
|
||||
fn = fn.relto(curdir)
|
||||
return "%s:%d" %(fn, lineno+1)
|
||||
128
pytest/plugin/pytest_recwarn.py
Normal file
128
pytest/plugin/pytest_recwarn.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
helpers for asserting deprecation and other warnings.
|
||||
|
||||
Example usage
|
||||
---------------------
|
||||
|
||||
You can use the ``recwarn`` funcarg to track
|
||||
warnings within a test function:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def test_hello(recwarn):
|
||||
from warnings import warn
|
||||
warn("hello", DeprecationWarning)
|
||||
w = recwarn.pop(DeprecationWarning)
|
||||
assert issubclass(w.category, DeprecationWarning)
|
||||
assert 'hello' in str(w.message)
|
||||
assert w.filename
|
||||
assert w.lineno
|
||||
|
||||
You can also call a global helper for checking
|
||||
taht a certain function call yields a Deprecation
|
||||
warning:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
import py
|
||||
|
||||
def test_global():
|
||||
py.test.deprecated_call(myfunction, 17)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import py
|
||||
import sys, os
|
||||
|
||||
def pytest_funcarg__recwarn(request):
|
||||
"""Return a WarningsRecorder instance that provides these methods:
|
||||
|
||||
* ``pop(category=None)``: return last warning matching the category.
|
||||
* ``clear()``: clear list of warnings
|
||||
"""
|
||||
if sys.version_info >= (2,7):
|
||||
import warnings
|
||||
oldfilters = warnings.filters[:]
|
||||
warnings.simplefilter('default')
|
||||
def reset_filters():
|
||||
warnings.filters[:] = oldfilters
|
||||
request.addfinalizer(reset_filters)
|
||||
wrec = WarningsRecorder()
|
||||
request.addfinalizer(wrec.finalize)
|
||||
return wrec
|
||||
|
||||
def pytest_namespace():
|
||||
return {'deprecated_call': deprecated_call}
|
||||
|
||||
def deprecated_call(func, *args, **kwargs):
|
||||
""" assert that calling func(*args, **kwargs)
|
||||
triggers a DeprecationWarning.
|
||||
"""
|
||||
warningmodule = py.std.warnings
|
||||
l = []
|
||||
oldwarn_explicit = getattr(warningmodule, 'warn_explicit')
|
||||
def warn_explicit(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn_explicit(*args, **kwargs)
|
||||
oldwarn = getattr(warningmodule, 'warn')
|
||||
def warn(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn(*args, **kwargs)
|
||||
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
if not l:
|
||||
#print warningmodule
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
|
||||
return ret
|
||||
|
||||
|
||||
class RecordedWarning:
|
||||
def __init__(self, message, category, filename, lineno, line):
|
||||
self.message = message
|
||||
self.category = category
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.line = line
|
||||
|
||||
class WarningsRecorder:
|
||||
def __init__(self):
|
||||
warningmodule = py.std.warnings
|
||||
self.list = []
|
||||
def showwarning(message, category, filename, lineno, line=0):
|
||||
self.list.append(RecordedWarning(
|
||||
message, category, filename, lineno, line))
|
||||
try:
|
||||
self.old_showwarning(message, category,
|
||||
filename, lineno, line=line)
|
||||
except TypeError:
|
||||
# < python2.6
|
||||
self.old_showwarning(message, category, filename, lineno)
|
||||
self.old_showwarning = warningmodule.showwarning
|
||||
warningmodule.showwarning = showwarning
|
||||
|
||||
def pop(self, cls=Warning):
|
||||
""" pop the first recorded warning, raise exception if not exists."""
|
||||
for i, w in enumerate(self.list):
|
||||
if issubclass(w.category, cls):
|
||||
return self.list.pop(i)
|
||||
__tracebackhide__ = True
|
||||
assert 0, "%r not found in %r" %(cls, self.list)
|
||||
|
||||
#def resetregistry(self):
|
||||
# import warnings
|
||||
# warnings.onceregistry.clear()
|
||||
# warnings.__warningregistry__.clear()
|
||||
|
||||
def clear(self):
|
||||
self.list[:] = []
|
||||
|
||||
def finalize(self):
|
||||
py.std.warnings.showwarning = self.old_showwarning
|
||||
429
pytest/plugin/pytest_restdoc.py
Normal file
429
pytest/plugin/pytest_restdoc.py
Normal file
@@ -0,0 +1,429 @@
|
||||
"""
|
||||
perform ReST syntax, local and remote reference tests on .rst/.txt files.
|
||||
"""
|
||||
import py
|
||||
import sys, os, re
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("ReST", "ReST documentation check options")
|
||||
group.addoption('-R', '--urlcheck',
|
||||
action="store_true", dest="urlcheck", default=False,
|
||||
help="urlopen() remote links found in ReST text files.")
|
||||
group.addoption('--urltimeout', action="store", metavar="secs",
|
||||
type="int", dest="urlcheck_timeout", default=5,
|
||||
help="timeout in seconds for remote urlchecks")
|
||||
group.addoption('--forcegen',
|
||||
action="store_true", dest="forcegen", default=False,
|
||||
help="force generation of html files.")
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
if path.ext in (".txt", ".rst"):
|
||||
project = getproject(path)
|
||||
if project is not None:
|
||||
return ReSTFile(path, parent=parent, project=project)
|
||||
|
||||
def getproject(path):
|
||||
for parent in path.parts(reverse=True):
|
||||
confrest = parent.join("confrest.py")
|
||||
if confrest.check():
|
||||
Project = confrest.pyimport().Project
|
||||
return Project(parent)
|
||||
|
||||
class ReSTFile(py.test.collect.File):
|
||||
def __init__(self, fspath, parent, project):
|
||||
super(ReSTFile, self).__init__(fspath=fspath, parent=parent)
|
||||
self.project = project
|
||||
|
||||
def collect(self):
|
||||
return [
|
||||
ReSTSyntaxTest("ReSTSyntax", parent=self, project=self.project),
|
||||
LinkCheckerMaker("checklinks", parent=self),
|
||||
DoctestText("doctest", parent=self),
|
||||
]
|
||||
|
||||
def deindent(s, sep='\n'):
|
||||
leastspaces = -1
|
||||
lines = s.split(sep)
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
spaces = len(line) - len(line.lstrip())
|
||||
if leastspaces == -1 or spaces < leastspaces:
|
||||
leastspaces = spaces
|
||||
if leastspaces == -1:
|
||||
return s
|
||||
for i, line in enumerate(lines):
|
||||
if not line.strip():
|
||||
lines[i] = ''
|
||||
else:
|
||||
lines[i] = line[leastspaces:]
|
||||
return sep.join(lines)
|
||||
|
||||
class ReSTSyntaxTest(py.test.collect.Item):
|
||||
def __init__(self, name, parent, project):
|
||||
super(ReSTSyntaxTest, self).__init__(name=name, parent=parent)
|
||||
self.project = project
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "syntax check"
|
||||
|
||||
def runtest(self):
|
||||
self.restcheck(py.path.svnwc(self.fspath))
|
||||
|
||||
def restcheck(self, path):
|
||||
py.test.importorskip("docutils")
|
||||
self.register_linkrole()
|
||||
from docutils.utils import SystemMessage
|
||||
try:
|
||||
self._checkskip(path, self.project.get_htmloutputpath(path))
|
||||
self.project.process(path)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except SystemMessage:
|
||||
# we assume docutils printed info on stdout
|
||||
py.test.fail("docutils processing failed, see captured stderr")
|
||||
|
||||
def register_linkrole(self):
|
||||
#directive.register_linkrole('api', self.resolve_linkrole)
|
||||
#directive.register_linkrole('source', self.resolve_linkrole)
|
||||
#
|
||||
# # XXX fake sphinx' "toctree" and refs
|
||||
# directive.register_linkrole('ref', self.resolve_linkrole)
|
||||
|
||||
from docutils.parsers.rst import directives
|
||||
def toctree_directive(name, arguments, options, content, lineno,
|
||||
content_offset, block_text, state, state_machine):
|
||||
return []
|
||||
toctree_directive.content = 1
|
||||
toctree_directive.options = {'maxdepth': int, 'glob': directives.flag,
|
||||
'hidden': directives.flag}
|
||||
directives.register_directive('toctree', toctree_directive)
|
||||
self.register_pygments()
|
||||
|
||||
def register_pygments(self):
|
||||
# taken from pygments-main/external/rst-directive.py
|
||||
from docutils.parsers.rst import directives
|
||||
try:
|
||||
from pygments.formatters import HtmlFormatter
|
||||
except ImportError:
|
||||
def pygments_directive(name, arguments, options, content, lineno,
|
||||
content_offset, block_text, state, state_machine):
|
||||
return []
|
||||
pygments_directive.options = {}
|
||||
else:
|
||||
# The default formatter
|
||||
DEFAULT = HtmlFormatter(noclasses=True)
|
||||
# Add name -> formatter pairs for every variant you want to use
|
||||
VARIANTS = {
|
||||
# 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
|
||||
}
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name, TextLexer
|
||||
|
||||
def pygments_directive(name, arguments, options, content, lineno,
|
||||
content_offset, block_text, state, state_machine):
|
||||
try:
|
||||
lexer = get_lexer_by_name(arguments[0])
|
||||
except ValueError:
|
||||
# no lexer found - use the text one instead of an exception
|
||||
lexer = TextLexer()
|
||||
# take an arbitrary option if more than one is given
|
||||
formatter = options and VARIANTS[options.keys()[0]] or DEFAULT
|
||||
parsed = highlight('\n'.join(content), lexer, formatter)
|
||||
return [nodes.raw('', parsed, format='html')]
|
||||
|
||||
pygments_directive.options = dict([(key, directives.flag) for key in VARIANTS])
|
||||
|
||||
pygments_directive.arguments = (1, 0, 1)
|
||||
pygments_directive.content = 1
|
||||
directives.register_directive('sourcecode', pygments_directive)
|
||||
|
||||
def resolve_linkrole(self, name, text, check=True):
|
||||
apigen_relpath = self.project.apigen_relpath
|
||||
|
||||
if name == 'api':
|
||||
if text == 'py':
|
||||
return ('py', apigen_relpath + 'api/index.html')
|
||||
else:
|
||||
assert text.startswith('py.'), (
|
||||
'api link "%s" does not point to the py package') % (text,)
|
||||
dotted_name = text
|
||||
if dotted_name.find('(') > -1:
|
||||
dotted_name = dotted_name[:text.find('(')]
|
||||
# remove pkg root
|
||||
path = dotted_name.split('.')[1:]
|
||||
dotted_name = '.'.join(path)
|
||||
obj = py
|
||||
if check:
|
||||
for chunk in path:
|
||||
try:
|
||||
obj = getattr(obj, chunk)
|
||||
except AttributeError:
|
||||
raise AssertionError(
|
||||
'problem with linkrole :api:`%s`: can not resolve '
|
||||
'dotted name %s' % (text, dotted_name,))
|
||||
return (text, apigen_relpath + 'api/%s.html' % (dotted_name,))
|
||||
elif name == 'source':
|
||||
assert text.startswith('py/'), ('source link "%s" does not point '
|
||||
'to the py package') % (text,)
|
||||
relpath = '/'.join(text.split('/')[1:])
|
||||
if check:
|
||||
pkgroot = py._pydir
|
||||
abspath = pkgroot.join(relpath)
|
||||
assert pkgroot.join(relpath).check(), (
|
||||
'problem with linkrole :source:`%s`: '
|
||||
'path %s does not exist' % (text, relpath))
|
||||
if relpath.endswith('/') or not relpath:
|
||||
relpath += 'index.html'
|
||||
else:
|
||||
relpath += '.html'
|
||||
return (text, apigen_relpath + 'source/%s' % (relpath,))
|
||||
elif name == 'ref':
|
||||
return ("", "")
|
||||
|
||||
def _checkskip(self, lpath, htmlpath=None):
|
||||
if not self.config.getvalue("forcegen"):
|
||||
lpath = py.path.local(lpath)
|
||||
if htmlpath is not None:
|
||||
htmlpath = py.path.local(htmlpath)
|
||||
if lpath.ext == '.txt':
|
||||
htmlpath = htmlpath or lpath.new(ext='.html')
|
||||
if htmlpath.check(file=1) and htmlpath.mtime() >= lpath.mtime():
|
||||
py.test.skip("html file is up to date, use --forcegen to regenerate")
|
||||
#return [] # no need to rebuild
|
||||
|
||||
class DoctestText(py.test.collect.Item):
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "doctest"
|
||||
|
||||
def runtest(self):
|
||||
content = self._normalize_linesep()
|
||||
newcontent = self.config.hook.pytest_doctest_prepare_content(content=content)
|
||||
if newcontent is not None:
|
||||
content = newcontent
|
||||
s = content
|
||||
l = []
|
||||
prefix = '.. >>> '
|
||||
mod = py.std.types.ModuleType(self.fspath.purebasename)
|
||||
skipchunk = False
|
||||
for line in deindent(s).split('\n'):
|
||||
stripped = line.strip()
|
||||
if skipchunk and line.startswith(skipchunk):
|
||||
py.builtin.print_("skipping", line)
|
||||
continue
|
||||
skipchunk = False
|
||||
if stripped.startswith(prefix):
|
||||
try:
|
||||
py.builtin.exec_(py.code.Source(
|
||||
stripped[len(prefix):]).compile(), mod.__dict__)
|
||||
except ValueError:
|
||||
e = sys.exc_info()[1]
|
||||
if e.args and e.args[0] == "skipchunk":
|
||||
skipchunk = " " * (len(line) - len(line.lstrip()))
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
l.append(line)
|
||||
docstring = "\n".join(l)
|
||||
mod.__doc__ = docstring
|
||||
failed, tot = py.std.doctest.testmod(mod, verbose=1)
|
||||
if failed:
|
||||
py.test.fail("doctest %s: %s failed out of %s" %(
|
||||
self.fspath, failed, tot))
|
||||
|
||||
def _normalize_linesep(self):
|
||||
# XXX quite nasty... but it works (fixes win32 issues)
|
||||
s = self.fspath.read()
|
||||
linesep = '\n'
|
||||
if '\r' in s:
|
||||
if '\n' not in s:
|
||||
linesep = '\r'
|
||||
else:
|
||||
linesep = '\r\n'
|
||||
s = s.replace(linesep, '\n')
|
||||
return s
|
||||
|
||||
class LinkCheckerMaker(py.test.collect.Collector):
|
||||
def collect(self):
|
||||
return list(self.genlinkchecks())
|
||||
|
||||
def genlinkchecks(self):
|
||||
path = self.fspath
|
||||
# generating functions + args as single tests
|
||||
timeout = self.config.getvalue("urlcheck_timeout")
|
||||
for lineno, line in enumerate(path.readlines()):
|
||||
line = line.strip()
|
||||
if line.startswith('.. _'):
|
||||
if line.startswith('.. _`'):
|
||||
delim = '`:'
|
||||
else:
|
||||
delim = ':'
|
||||
l = line.split(delim, 1)
|
||||
if len(l) != 2:
|
||||
continue
|
||||
tryfn = l[1].strip()
|
||||
name = "%s:%d" %(tryfn, lineno)
|
||||
if tryfn.startswith('http:') or tryfn.startswith('https'):
|
||||
if self.config.getvalue("urlcheck"):
|
||||
yield CheckLink(name, parent=self,
|
||||
args=(tryfn, path, lineno, timeout), checkfunc=urlcheck)
|
||||
elif tryfn.startswith('webcal:'):
|
||||
continue
|
||||
else:
|
||||
i = tryfn.find('#')
|
||||
if i != -1:
|
||||
checkfn = tryfn[:i]
|
||||
else:
|
||||
checkfn = tryfn
|
||||
if checkfn.strip() and (1 or checkfn.endswith('.html')):
|
||||
yield CheckLink(name, parent=self,
|
||||
args=(tryfn, path, lineno), checkfunc=localrefcheck)
|
||||
|
||||
class CheckLink(py.test.collect.Item):
|
||||
def __init__(self, name, parent, args, checkfunc):
|
||||
super(CheckLink, self).__init__(name, parent)
|
||||
self.args = args
|
||||
self.checkfunc = checkfunc
|
||||
|
||||
def runtest(self):
|
||||
return self.checkfunc(*self.args)
|
||||
|
||||
def reportinfo(self, basedir=None):
|
||||
return (self.fspath, self.args[2], "checklink: %s" % self.args[0])
|
||||
|
||||
def urlcheck(tryfn, path, lineno, TIMEOUT_URLOPEN):
|
||||
old = py.std.socket.getdefaulttimeout()
|
||||
py.std.socket.setdefaulttimeout(TIMEOUT_URLOPEN)
|
||||
try:
|
||||
try:
|
||||
py.builtin.print_("trying remote", tryfn)
|
||||
py.std.urllib2.urlopen(tryfn)
|
||||
finally:
|
||||
py.std.socket.setdefaulttimeout(old)
|
||||
except (py.std.urllib2.URLError, py.std.urllib2.HTTPError):
|
||||
e = sys.exc_info()[1]
|
||||
if getattr(e, 'code', None) in (401, 403): # authorization required, forbidden
|
||||
py.test.skip("%s: %s" %(tryfn, str(e)))
|
||||
else:
|
||||
py.test.fail("remote reference error %r in %s:%d\n%s" %(
|
||||
tryfn, path.basename, lineno+1, e))
|
||||
|
||||
def localrefcheck(tryfn, path, lineno):
|
||||
# assume it should be a file
|
||||
i = tryfn.find('#')
|
||||
if tryfn.startswith('javascript:'):
|
||||
return # don't check JS refs
|
||||
if i != -1:
|
||||
anchor = tryfn[i+1:]
|
||||
tryfn = tryfn[:i]
|
||||
else:
|
||||
anchor = ''
|
||||
fn = path.dirpath(tryfn)
|
||||
ishtml = fn.ext == '.html'
|
||||
fn = ishtml and fn.new(ext='.txt') or fn
|
||||
py.builtin.print_("filename is", fn)
|
||||
if not fn.check(): # not ishtml or not fn.check():
|
||||
if not py.path.local(tryfn).check(): # the html could be there
|
||||
py.test.fail("reference error %r in %s:%d" %(
|
||||
tryfn, path.basename, lineno+1))
|
||||
if anchor:
|
||||
source = unicode(fn.read(), 'latin1')
|
||||
source = source.lower().replace('-', ' ') # aehem
|
||||
|
||||
anchor = anchor.replace('-', ' ')
|
||||
match2 = ".. _`%s`:" % anchor
|
||||
match3 = ".. _%s:" % anchor
|
||||
candidates = (anchor, match2, match3)
|
||||
py.builtin.print_("candidates", repr(candidates))
|
||||
for line in source.split('\n'):
|
||||
line = line.strip()
|
||||
if line in candidates:
|
||||
break
|
||||
else:
|
||||
py.test.fail("anchor reference error %s#%s in %s:%d" %(
|
||||
tryfn, anchor, path.basename, lineno+1))
|
||||
|
||||
if hasattr(sys.stdout, 'fileno') and os.isatty(sys.stdout.fileno()):
|
||||
def log(msg):
|
||||
print(msg)
|
||||
else:
|
||||
def log(msg):
|
||||
pass
|
||||
|
||||
def convert_rest_html(source, source_path, stylesheet=None, encoding='latin1'):
|
||||
""" return html latin1-encoded document for the given input.
|
||||
source a ReST-string
|
||||
sourcepath where to look for includes (basically)
|
||||
stylesheet path (to be used if any)
|
||||
"""
|
||||
from docutils.core import publish_string
|
||||
kwargs = {
|
||||
'stylesheet' : stylesheet,
|
||||
'stylesheet_path': None,
|
||||
'traceback' : 1,
|
||||
'embed_stylesheet': 0,
|
||||
'output_encoding' : encoding,
|
||||
#'halt' : 0, # 'info',
|
||||
'halt_level' : 2,
|
||||
}
|
||||
# docutils uses os.getcwd() :-(
|
||||
source_path = os.path.abspath(str(source_path))
|
||||
prevdir = os.getcwd()
|
||||
try:
|
||||
#os.chdir(os.path.dirname(source_path))
|
||||
return publish_string(source, source_path, writer_name='html',
|
||||
settings_overrides=kwargs)
|
||||
finally:
|
||||
os.chdir(prevdir)
|
||||
|
||||
def process(txtpath, encoding='latin1'):
|
||||
""" process a textfile """
|
||||
log("processing %s" % txtpath)
|
||||
assert txtpath.check(ext='.txt')
|
||||
if isinstance(txtpath, py.path.svnwc):
|
||||
txtpath = txtpath.localpath
|
||||
htmlpath = txtpath.new(ext='.html')
|
||||
#svninfopath = txtpath.localpath.new(ext='.svninfo')
|
||||
|
||||
style = txtpath.dirpath('style.css')
|
||||
if style.check():
|
||||
stylesheet = style.basename
|
||||
else:
|
||||
stylesheet = None
|
||||
content = unicode(txtpath.read(), encoding)
|
||||
doc = convert_rest_html(content, txtpath, stylesheet=stylesheet, encoding=encoding)
|
||||
htmlpath.open('wb').write(doc)
|
||||
#log("wrote %r" % htmlpath)
|
||||
#if txtpath.check(svnwc=1, versioned=1):
|
||||
# info = txtpath.info()
|
||||
# svninfopath.dump(info)
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
def _uni(s): return s
|
||||
else:
|
||||
def _uni(s):
|
||||
return unicode(s)
|
||||
|
||||
rex1 = re.compile(r'.*<body>(.*)</body>.*', re.MULTILINE | re.DOTALL)
|
||||
rex2 = re.compile(r'.*<div class="document">(.*)</div>.*', re.MULTILINE | re.DOTALL)
|
||||
|
||||
def strip_html_header(string, encoding='utf8'):
|
||||
""" return the content of the body-tag """
|
||||
uni = unicode(string, encoding)
|
||||
for rex in rex1,rex2:
|
||||
match = rex.search(uni)
|
||||
if not match:
|
||||
break
|
||||
uni = match.group(1)
|
||||
return uni
|
||||
|
||||
class Project: # used for confrest.py files
|
||||
def __init__(self, sourcepath):
|
||||
self.sourcepath = sourcepath
|
||||
def process(self, path):
|
||||
return process(path)
|
||||
def get_htmloutputpath(self, path):
|
||||
return path.new(ext='html')
|
||||
98
pytest/plugin/pytest_resultlog.py
Normal file
98
pytest/plugin/pytest_resultlog.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""non-xml machine-readable logging of test results.
|
||||
Useful for buildbot integration code. See the `PyPy-test`_
|
||||
web page for post-processing.
|
||||
|
||||
.. _`PyPy-test`: http://codespeak.net:8099/summary
|
||||
|
||||
"""
|
||||
|
||||
import py
|
||||
from py.builtin import print_
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("resultlog", "resultlog plugin options")
|
||||
group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None,
|
||||
help="path for machine-readable result log.")
|
||||
|
||||
def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
# prevent opening resultlog on slave nodes (xdist)
|
||||
if resultlog and not hasattr(config, 'slaveinput'):
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
config._resultlog = ResultLog(config, logfile)
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
resultlog = getattr(config, '_resultlog', None)
|
||||
if resultlog:
|
||||
resultlog.logfile.close()
|
||||
del config._resultlog
|
||||
config.pluginmanager.unregister(resultlog)
|
||||
|
||||
def generic_path(item):
|
||||
chain = item.listchain()
|
||||
gpath = [chain[0].name]
|
||||
fspath = chain[0].fspath
|
||||
fspart = False
|
||||
for node in chain[1:]:
|
||||
newfspath = node.fspath
|
||||
if newfspath == fspath:
|
||||
if fspart:
|
||||
gpath.append(':')
|
||||
fspart = False
|
||||
else:
|
||||
gpath.append('.')
|
||||
else:
|
||||
gpath.append('/')
|
||||
fspart = True
|
||||
name = node.name
|
||||
if name[0] in '([':
|
||||
gpath.pop()
|
||||
gpath.append(name)
|
||||
fspath = newfspath
|
||||
return ''.join(gpath)
|
||||
|
||||
class ResultLog(object):
|
||||
def __init__(self, config, logfile):
|
||||
self.config = config
|
||||
self.logfile = logfile # preferably line buffered
|
||||
|
||||
def write_log_entry(self, testpath, lettercode, longrepr):
|
||||
print_("%s %s" % (lettercode, testpath), file=self.logfile)
|
||||
for line in longrepr.splitlines():
|
||||
print_(" %s" % line, file=self.logfile)
|
||||
|
||||
def log_outcome(self, report, lettercode, longrepr):
|
||||
testpath = getattr(report, 'nodeid', None)
|
||||
if testpath is None:
|
||||
testpath = report.fspath
|
||||
self.write_log_entry(testpath, lettercode, longrepr)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
res = self.config.hook.pytest_report_teststatus(report=report)
|
||||
code = res[1]
|
||||
if code == 'x':
|
||||
longrepr = str(report.longrepr)
|
||||
elif code == 'X':
|
||||
longrepr = ''
|
||||
elif report.passed:
|
||||
longrepr = ""
|
||||
elif report.failed:
|
||||
longrepr = str(report.longrepr)
|
||||
elif report.skipped:
|
||||
longrepr = str(report.longrepr.reprcrash.message)
|
||||
self.log_outcome(report, code, longrepr)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
code = "F"
|
||||
else:
|
||||
assert report.skipped
|
||||
code = "S"
|
||||
longrepr = str(report.longrepr.reprcrash)
|
||||
self.log_outcome(report, code, longrepr)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
path = excrepr.reprcrash.path
|
||||
self.write_log_entry(path, '!', str(excrepr))
|
||||
484
pytest/plugin/pytest_runner.py
Normal file
484
pytest/plugin/pytest_runner.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
collect and run test items and create reports.
|
||||
"""
|
||||
|
||||
import py, sys
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_namespace():
|
||||
return {
|
||||
'raises' : raises,
|
||||
'skip' : skip,
|
||||
'importorskip' : importorskip,
|
||||
'fail' : fail,
|
||||
'xfail' : xfail,
|
||||
'exit' : exit,
|
||||
}
|
||||
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
# XXX move to pytest_sessionstart and fix py.test owns tests
|
||||
def pytest_configure(config):
|
||||
config._setupstate = SetupState()
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
if hasattr(session.config, '_setupstate'):
|
||||
hook = session.config.hook
|
||||
rep = hook.pytest__teardown_final(session=session)
|
||||
if rep:
|
||||
hook.pytest__teardown_final_logerror(session=session, report=rep)
|
||||
session.exitstatus = 1
|
||||
|
||||
class NodeInfo:
|
||||
def __init__(self, nodeid, nodenames, fspath, location):
|
||||
self.nodeid = nodeid
|
||||
self.nodenames = nodenames
|
||||
self.fspath = fspath
|
||||
self.location = location
|
||||
|
||||
def getitemnodeinfo(item):
|
||||
try:
|
||||
return item._nodeinfo
|
||||
except AttributeError:
|
||||
location = item.ihook.pytest_report_iteminfo(item=item)
|
||||
location = (str(location[0]), location[1], str(location[2]))
|
||||
nodenames = tuple(item.listnames())
|
||||
nodeid = item.collection.getid(item)
|
||||
fspath = item.fspath
|
||||
item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location)
|
||||
return n
|
||||
|
||||
def pytest_runtest_protocol(item):
|
||||
nodeinfo = getitemnodeinfo(item)
|
||||
item.ihook.pytest_runtest_logstart(
|
||||
nodeid=nodeinfo.nodeid,
|
||||
location=nodeinfo.location,
|
||||
fspath=str(item.fspath),
|
||||
)
|
||||
runtestprotocol(item)
|
||||
return True
|
||||
|
||||
def runtestprotocol(item, log=True):
|
||||
rep = call_and_report(item, "setup", log)
|
||||
reports = [rep]
|
||||
if rep.passed:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
reports.append(call_and_report(item, "teardown", log))
|
||||
return reports
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item.config._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
if not item._deprecated_testexecution():
|
||||
item.runtest()
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
item.config._setupstate.teardown_exact(item)
|
||||
|
||||
def pytest__teardown_final(session):
|
||||
call = CallInfo(session.config._setupstate.teardown_all, when="teardown")
|
||||
if call.excinfo:
|
||||
ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
|
||||
call.excinfo.traceback = ntraceback.filter()
|
||||
longrepr = call.excinfo.getrepr(funcargs=True)
|
||||
return TeardownErrorReport(longrepr)
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.when in ("setup", "teardown"):
|
||||
if report.failed:
|
||||
# category, shortletter, verbose-word
|
||||
return "error", "E", "ERROR"
|
||||
elif report.skipped:
|
||||
return "skipped", "s", "SKIPPED"
|
||||
else:
|
||||
return "", "", ""
|
||||
|
||||
|
||||
#
|
||||
# Implementation
|
||||
|
||||
def call_and_report(item, when, log=True):
|
||||
call = call_runtest_hook(item, when)
|
||||
hook = item.ihook
|
||||
report = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log and (when == "call" or not report.passed):
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
return report
|
||||
|
||||
def call_runtest_hook(item, when):
|
||||
hookname = "pytest_runtest_" + when
|
||||
ihook = getattr(item.ihook, hookname)
|
||||
return CallInfo(lambda: ihook(item=item), when=when)
|
||||
|
||||
class CallInfo:
|
||||
excinfo = None
|
||||
def __init__(self, func, when):
|
||||
self.when = when
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
|
||||
def __repr__(self):
|
||||
if self.excinfo:
|
||||
status = "exception: %s" % str(self.excinfo.value)
|
||||
else:
|
||||
status = "result: %r" % (self.result,)
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
class BaseReport(object):
|
||||
def toterminal(self, out):
|
||||
longrepr = self.longrepr
|
||||
if hasattr(longrepr, 'toterminal'):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
out.line(str(longrepr))
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
nodeinfo = getitemnodeinfo(item)
|
||||
when = call.when
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
longrepr = None
|
||||
else:
|
||||
if not isinstance(excinfo, py.code.ExceptionInfo):
|
||||
outcome = "failed"
|
||||
longrepr = excinfo
|
||||
elif excinfo.errisinstance(py.test.skip.Exception):
|
||||
outcome = "skipped"
|
||||
longrepr = item._repr_failure_py(excinfo)
|
||||
else:
|
||||
outcome = "failed"
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo)
|
||||
return TestReport(nodeinfo.nodeid, nodeinfo.nodenames,
|
||||
nodeinfo.fspath, nodeinfo.location,
|
||||
keywords, outcome, longrepr, when)
|
||||
|
||||
class TestReport(BaseReport):
|
||||
def __init__(self, nodeid, nodenames, fspath, location,
|
||||
keywords, outcome, longrepr, when):
|
||||
self.nodeid = nodeid
|
||||
self.nodenames = nodenames
|
||||
self.fspath = fspath # where the test was collected
|
||||
self.location = location
|
||||
self.keywords = keywords
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.when = when
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid, self.when, self.outcome)
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
def __init__(self, longrepr):
|
||||
self.longrepr = longrepr
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
result = excinfo = None
|
||||
try:
|
||||
result = collector._memocollect()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
nodenames = tuple(collector.listnames())
|
||||
nodeid = collector.collection.getid(collector)
|
||||
fspath = str(collector.fspath)
|
||||
reason = longrepr = None
|
||||
if not excinfo:
|
||||
outcome = "passed"
|
||||
else:
|
||||
if excinfo.errisinstance(py.test.skip.Exception):
|
||||
outcome = "skipped"
|
||||
reason = str(excinfo.value)
|
||||
longrepr = collector._repr_failure_py(excinfo, "line")
|
||||
else:
|
||||
outcome = "failed"
|
||||
errorinfo = collector.repr_failure(excinfo)
|
||||
if not hasattr(errorinfo, "toterminal"):
|
||||
errorinfo = CollectErrorRepr(errorinfo)
|
||||
longrepr = errorinfo
|
||||
return CollectReport(nodenames, nodeid, fspath,
|
||||
outcome, longrepr, result, reason)
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodenames, nodeid, fspath, outcome,
|
||||
longrepr, result, reason):
|
||||
self.nodenames = nodenames
|
||||
self.nodeid = nodeid
|
||||
self.fspath = fspath
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r outcome=%r>" % (self.nodeid, self.outcome)
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
def toterminal(self, out):
|
||||
out.line(str(self.longrepr), red=True)
|
||||
|
||||
class SetupState(object):
|
||||
""" shared state for setting up/tearing down test items or collectors. """
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
self._finalizers = {}
|
||||
|
||||
def addfinalizer(self, finalizer, colitem):
|
||||
""" attach a finalizer to the given colitem.
|
||||
if colitem is None, this will add a finalizer that
|
||||
is called at the end of teardown_all().
|
||||
"""
|
||||
assert hasattr(finalizer, '__call__')
|
||||
#assert colitem in self.stack
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
def _pop_and_teardown(self):
|
||||
colitem = self.stack.pop()
|
||||
self._teardown_with_finalization(colitem)
|
||||
|
||||
def _callfinalizers(self, colitem):
|
||||
finalizers = self._finalizers.pop(colitem, None)
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
fin()
|
||||
|
||||
def _teardown_with_finalization(self, colitem):
|
||||
self._callfinalizers(colitem)
|
||||
if colitem:
|
||||
colitem.teardown()
|
||||
for colitem in self._finalizers:
|
||||
assert colitem is None or colitem in self.stack
|
||||
|
||||
def teardown_all(self):
|
||||
while self.stack:
|
||||
self._pop_and_teardown()
|
||||
self._teardown_with_finalization(None)
|
||||
assert not self._finalizers
|
||||
|
||||
def teardown_exact(self, item):
|
||||
if self.stack and item == self.stack[-1]:
|
||||
self._pop_and_teardown()
|
||||
else:
|
||||
self._callfinalizers(item)
|
||||
|
||||
def prepare(self, colitem):
|
||||
""" setup objects along the collector chain to the test-method
|
||||
and teardown previously setup objects."""
|
||||
needed_collectors = colitem.listchain()
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
break
|
||||
self._pop_and_teardown()
|
||||
# check if the last collection node has raised an error
|
||||
for col in self.stack:
|
||||
if hasattr(col, '_prepare_exc'):
|
||||
py.builtin._reraise(*col._prepare_exc)
|
||||
for col in needed_collectors[len(self.stack):]:
|
||||
self.stack.append(col)
|
||||
try:
|
||||
col.setup()
|
||||
except Exception:
|
||||
col._prepare_exc = sys.exc_info()
|
||||
raise
|
||||
|
||||
# =============================================================
|
||||
# Test OutcomeExceptions and helpers for creating them.
|
||||
|
||||
|
||||
class OutcomeException(Exception):
|
||||
""" OutcomeException and its subclass instances indicate and
|
||||
contain info about test and collection outcomes.
|
||||
"""
|
||||
def __init__(self, msg=None, excinfo=None):
|
||||
self.msg = msg
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __repr__(self):
|
||||
if self.msg:
|
||||
return repr(self.msg)
|
||||
return "<%s instance>" %(self.__class__.__name__,)
|
||||
__str__ = __repr__
|
||||
|
||||
class Skipped(OutcomeException):
|
||||
# XXX hackish: on 3k we fake to live in the builtins
|
||||
# in order to have Skipped exception printing shorter/nicer
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to py.test.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
class XFailed(OutcomeException):
|
||||
""" raised from an explicit call to py.test.xfail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
class ExceptionFailure(Failed):
|
||||
""" raised by py.test.raises on an exception-assertion mismatch. """
|
||||
def __init__(self, expr, expected, msg=None, excinfo=None):
|
||||
Failed.__init__(self, msg=msg, excinfo=excinfo)
|
||||
self.expr = expr
|
||||
self.expected = expected
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
""" raised by py.test.exit for immediate program exits without tracebacks and reporter/summary. """
|
||||
def __init__(self, msg="unknown reason"):
|
||||
self.msg = msg
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
|
||||
exit.Exception = Exit
|
||||
|
||||
def skip(msg=""):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better use the py.test.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Skipped(msg=msg)
|
||||
|
||||
skip.Exception = Skipped
|
||||
|
||||
def fail(msg=""):
|
||||
""" explicitely fail an currently-executing test with the given Message. """
|
||||
__tracebackhide__ = True
|
||||
raise Failed(msg=msg)
|
||||
|
||||
fail.Exception = Failed
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions, taking an optional
|
||||
reason string.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
xfail.Exception = XFailed
|
||||
|
||||
def raises(ExpectedException, *args, **kwargs):
|
||||
""" assert that a code block/function call raises an exception.
|
||||
|
||||
If using Python 2.5 or above, you may use this function as a
|
||||
context manager::
|
||||
|
||||
>>> with raises(ZeroDivisionError):
|
||||
... 1/0
|
||||
|
||||
Or you can one of two forms:
|
||||
|
||||
if args[0] is callable: raise AssertionError if calling it with
|
||||
the remaining arguments does not raise the expected exception.
|
||||
if args[0] is a string: raise AssertionError if executing the
|
||||
the string in the calling scope does not raise expected exception.
|
||||
examples:
|
||||
>>> x = 5
|
||||
>>> raises(TypeError, lambda x: x + 'hello', x=x)
|
||||
>>> raises(TypeError, "x + 'hello'")
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
if not args:
|
||||
return RaisesContext(ExpectedException)
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
assert isinstance(code, str)
|
||||
frame = sys._getframe(1)
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(kwargs)
|
||||
#print "raises frame scope: %r" % frame.f_locals
|
||||
try:
|
||||
code = py.code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
# XXX didn'T mean f_globals == f_locals something special?
|
||||
# this is destroyed here ...
|
||||
except ExpectedException:
|
||||
return py.code.ExceptionInfo()
|
||||
else:
|
||||
func = args[0]
|
||||
try:
|
||||
func(*args[1:], **kwargs)
|
||||
except ExpectedException:
|
||||
return py.code.ExceptionInfo()
|
||||
k = ", ".join(["%s=%r" % x for x in kwargs.items()])
|
||||
if k:
|
||||
k = ', ' + k
|
||||
expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k)
|
||||
raise ExceptionFailure(msg="DID NOT RAISE",
|
||||
expr=args, expected=ExpectedException)
|
||||
|
||||
|
||||
class RaisesContext(object):
|
||||
|
||||
def __init__(self, ExpectedException):
|
||||
self.ExpectedException = ExpectedException
|
||||
self.excinfo = None
|
||||
|
||||
def __enter__(self):
|
||||
self.excinfo = object.__new__(py.code.ExceptionInfo)
|
||||
return self.excinfo
|
||||
|
||||
def __exit__(self, *tp):
|
||||
__tracebackhide__ = True
|
||||
if tp[0] is None:
|
||||
raise ExceptionFailure(msg="DID NOT RAISE",
|
||||
expr=(),
|
||||
expected=self.ExpectedException)
|
||||
self.excinfo.__init__(tp)
|
||||
return issubclass(self.excinfo.type, self.ExpectedException)
|
||||
|
||||
|
||||
raises.Exception = ExceptionFailure
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has a higher __version__ than the
|
||||
optionally specified 'minversion' - otherwise call py.test.skip()
|
||||
with a message detailing the mismatch.
|
||||
"""
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
try:
|
||||
mod = __import__(modname, None, None, ['__doc__'])
|
||||
except ImportError:
|
||||
py.test.skip("could not import %r" %(modname,))
|
||||
if minversion is None:
|
||||
return mod
|
||||
verattr = getattr(mod, '__version__', None)
|
||||
if isinstance(minversion, str):
|
||||
minver = minversion.split(".")
|
||||
else:
|
||||
minver = list(minversion)
|
||||
if verattr is None or verattr.split(".") < minver:
|
||||
py.test.skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
return mod
|
||||
|
||||
|
||||
349
pytest/plugin/pytest_skipping.py
Normal file
349
pytest/plugin/pytest_skipping.py
Normal file
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
advanced skipping for python test functions, classes or modules.
|
||||
|
||||
With this plugin you can mark test functions for conditional skipping
|
||||
or as "xfail", expected-to-fail. Skipping a test will avoid running it
|
||||
while xfail-marked tests will run and result in an inverted outcome:
|
||||
a pass becomes a failure and a fail becomes a semi-passing one.
|
||||
|
||||
The need for skipping a test is usually connected to a condition.
|
||||
If a test fails under all conditions then it's probably better
|
||||
to mark your test as 'xfail'.
|
||||
|
||||
By passing ``-rxs`` to the terminal reporter you will see extra
|
||||
summary information on skips and xfail-run tests at the end of a test run.
|
||||
|
||||
.. _skipif:
|
||||
|
||||
Skipping a single function
|
||||
-------------------------------------------
|
||||
|
||||
Here is an example for marking a test function to be skipped
|
||||
when run on a Python3 interpreter::
|
||||
|
||||
@py.test.mark.skipif("sys.version_info >= (3,0)")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
During test function setup the skipif condition is
|
||||
evaluated by calling ``eval(expr, namespace)``. The namespace
|
||||
contains the ``sys`` and ``os`` modules and the test
|
||||
``config`` object. The latter allows you to skip based
|
||||
on a test configuration value e.g. like this::
|
||||
|
||||
@py.test.mark.skipif("not config.getvalue('db')")
|
||||
def test_function(...):
|
||||
...
|
||||
|
||||
Create a shortcut for your conditional skip decorator
|
||||
at module level like this::
|
||||
|
||||
win32only = py.test.mark.skipif("sys.platform != 'win32'")
|
||||
|
||||
@win32only
|
||||
def test_function():
|
||||
...
|
||||
|
||||
|
||||
skip groups of test functions
|
||||
--------------------------------------
|
||||
|
||||
As with all metadata function marking you can do it at
|
||||
`whole class- or module level`_. Here is an example
|
||||
for skipping all methods of a test class based on platform::
|
||||
|
||||
class TestPosixCalls:
|
||||
pytestmark = py.test.mark.skipif("sys.platform == 'win32'")
|
||||
|
||||
def test_function(self):
|
||||
# will not be setup or run under 'win32' platform
|
||||
#
|
||||
|
||||
The ``pytestmark`` decorator will be applied to each test function.
|
||||
If your code targets python2.6 or above you can equivalently use
|
||||
the skipif decorator on classes::
|
||||
|
||||
@py.test.mark.skipif("sys.platform == 'win32'")
|
||||
class TestPosixCalls:
|
||||
|
||||
def test_function(self):
|
||||
# will not be setup or run under 'win32' platform
|
||||
#
|
||||
|
||||
It is fine in general to apply multiple "skipif" decorators
|
||||
on a single function - this means that if any of the conditions
|
||||
apply the function will be skipped.
|
||||
|
||||
.. _`whole class- or module level`: mark.html#scoped-marking
|
||||
|
||||
.. _xfail:
|
||||
|
||||
mark a test function as **expected to fail**
|
||||
-------------------------------------------------------
|
||||
|
||||
You can use the ``xfail`` marker to indicate that you
|
||||
expect the test to fail::
|
||||
|
||||
@py.test.mark.xfail
|
||||
def test_function():
|
||||
...
|
||||
|
||||
This test will be run but no traceback will be reported
|
||||
when it fails. Instead terminal reporting will list it in the
|
||||
"expected to fail" or "unexpectedly passing" sections.
|
||||
|
||||
Same as with skipif_ you can also selectively expect a failure
|
||||
depending on platform::
|
||||
|
||||
@py.test.mark.xfail("sys.version_info >= (3,0)")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
To not run a test and still regard it as "xfailed"::
|
||||
|
||||
@py.test.mark.xfail(..., run=False)
|
||||
|
||||
To specify an explicit reason to be shown with xfailure detail::
|
||||
|
||||
@py.test.mark.xfail(..., reason="my reason")
|
||||
|
||||
imperative xfail from within a test or setup function
|
||||
------------------------------------------------------
|
||||
|
||||
If you cannot declare xfail-conditions at import time
|
||||
you can also imperatively produce an XFail-outcome from
|
||||
within test or setup code. Example::
|
||||
|
||||
def test_function():
|
||||
if not valid_config():
|
||||
py.test.xfail("unsuppored configuration")
|
||||
|
||||
|
||||
skipping on a missing import dependency
|
||||
--------------------------------------------------
|
||||
|
||||
You can use the following import helper at module level
|
||||
or within a test or test setup function::
|
||||
|
||||
docutils = py.test.importorskip("docutils")
|
||||
|
||||
If ``docutils`` cannot be imported here, this will lead to a
|
||||
skip outcome of the test. You can also skip dependeing if
|
||||
if a library does not come with a high enough version::
|
||||
|
||||
docutils = py.test.importorskip("docutils", minversion="0.3")
|
||||
|
||||
The version will be read from the specified module's ``__version__`` attribute.
|
||||
|
||||
imperative skip from within a test or setup function
|
||||
------------------------------------------------------
|
||||
|
||||
If for some reason you cannot declare skip-conditions
|
||||
you can also imperatively produce a Skip-outcome from
|
||||
within test or setup code. Example::
|
||||
|
||||
def test_function():
|
||||
if not valid_config():
|
||||
py.test.skip("unsuppored configuration")
|
||||
|
||||
"""
|
||||
|
||||
import py
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption('--runxfail',
|
||||
action="store_true", dest="runxfail", default=False,
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
class MarkEvaluator:
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name, None)
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def istrue(self):
|
||||
if self.holder:
|
||||
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
||||
if self.holder.args:
|
||||
self.result = False
|
||||
for expr in self.holder.args:
|
||||
self.expr = expr
|
||||
if isinstance(expr, str):
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
result = expr
|
||||
if result:
|
||||
self.result = True
|
||||
self.expr = expr
|
||||
break
|
||||
else:
|
||||
self.result = True
|
||||
return getattr(self, 'result', False)
|
||||
|
||||
def get(self, attr, default=None):
|
||||
return self.holder.kwargs.get(attr, default)
|
||||
|
||||
def getexplanation(self):
|
||||
expl = self.get('reason', None)
|
||||
if not expl:
|
||||
if not hasattr(self, 'expr'):
|
||||
return ""
|
||||
else:
|
||||
return "condition: " + self.expr
|
||||
return expl
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if not isinstance(item, py.test.collect.Function):
|
||||
return
|
||||
evalskip = MarkEvaluator(item, 'skipif')
|
||||
if evalskip.istrue():
|
||||
py.test.skip(evalskip.getexplanation())
|
||||
item._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
check_xfail_no_run(pyfuncitem)
|
||||
|
||||
def check_xfail_no_run(item):
|
||||
if not item.config.getvalue("runxfail"):
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
py.test.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
if not isinstance(item, py.test.collect.Function):
|
||||
return
|
||||
if not (call.excinfo and
|
||||
call.excinfo.errisinstance(py.test.xfail.Exception)):
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
if not evalxfail:
|
||||
return
|
||||
if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception):
|
||||
if not item.config.getvalue("runxfail"):
|
||||
rep = __multicall__.execute()
|
||||
rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
return rep
|
||||
if call.when == "call":
|
||||
rep = __multicall__.execute()
|
||||
evalxfail = getattr(item, '_evalxfail')
|
||||
if not item.config.getvalue("runxfail") and evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
rep.outcome = "skipped"
|
||||
else:
|
||||
rep.outcome = "failed"
|
||||
rep.keywords['xfail'] = evalxfail.getexplanation()
|
||||
else:
|
||||
if 'xfail' in rep.keywords:
|
||||
del rep.keywords['xfail']
|
||||
return rep
|
||||
|
||||
# called by terminalreporter progress reporting
|
||||
def pytest_report_teststatus(report):
|
||||
if 'xfail' in report.keywords:
|
||||
if report.skipped:
|
||||
return "xfailed", "x", "xfail"
|
||||
elif report.failed:
|
||||
return "xpassed", "X", "XPASS"
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
tr = terminalreporter
|
||||
if not tr.reportchars:
|
||||
#for name in "xfailed skipped failed xpassed":
|
||||
# if not tr.stats.get(name, 0):
|
||||
# tr.write_line("HINT: use '-r' option to see extra "
|
||||
# "summary info about tests")
|
||||
# break
|
||||
return
|
||||
|
||||
lines = []
|
||||
for char in tr.reportchars:
|
||||
if char == "x":
|
||||
show_xfailed(terminalreporter, lines)
|
||||
elif char == "X":
|
||||
show_xpassed(terminalreporter, lines)
|
||||
elif char in "fF":
|
||||
show_failed(terminalreporter, lines)
|
||||
elif char in "sS":
|
||||
show_skipped(terminalreporter, lines)
|
||||
if lines:
|
||||
tr._tw.sep("=", "short test summary info")
|
||||
for line in lines:
|
||||
tr._tw.line(line)
|
||||
|
||||
def show_failed(terminalreporter, lines):
|
||||
tw = terminalreporter._tw
|
||||
failed = terminalreporter.stats.get("failed")
|
||||
if failed:
|
||||
for rep in failed:
|
||||
pos = rep.nodeid
|
||||
lines.append("FAIL %s" %(pos, ))
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
if xfailed:
|
||||
for rep in xfailed:
|
||||
pos = rep.nodeid
|
||||
reason = rep.keywords['xfail']
|
||||
lines.append("XFAIL %s" % (pos,))
|
||||
if reason:
|
||||
lines.append(" " + str(reason))
|
||||
|
||||
def show_xpassed(terminalreporter, lines):
|
||||
xpassed = terminalreporter.stats.get("xpassed")
|
||||
if xpassed:
|
||||
for rep in xpassed:
|
||||
pos = rep.nodeid
|
||||
reason = rep.keywords['xfail']
|
||||
lines.append("XPASS %s %s" %(pos, reason))
|
||||
|
||||
def cached_eval(config, expr, d):
|
||||
if not hasattr(config, '_evalcache'):
|
||||
config._evalcache = {}
|
||||
try:
|
||||
return config._evalcache[expr]
|
||||
except KeyError:
|
||||
#import sys
|
||||
#print >>sys.stderr, ("cache-miss: %r" % expr)
|
||||
config._evalcache[expr] = x = eval(expr, d)
|
||||
return x
|
||||
|
||||
|
||||
def folded_skips(skipped):
|
||||
d = {}
|
||||
for event in skipped:
|
||||
entry = event.longrepr.reprcrash
|
||||
key = entry.path, entry.lineno, entry.message
|
||||
d.setdefault(key, []).append(event)
|
||||
l = []
|
||||
for key, events in d.items():
|
||||
l.append((len(events),) + key)
|
||||
return l
|
||||
|
||||
def show_skipped(terminalreporter, lines):
|
||||
tr = terminalreporter
|
||||
skipped = tr.stats.get('skipped', [])
|
||||
if skipped:
|
||||
#if not tr.hasopt('skipped'):
|
||||
# tr.write_line(
|
||||
# "%d skipped tests, specify -rs for more info" %
|
||||
# len(skipped))
|
||||
# return
|
||||
fskips = folded_skips(skipped)
|
||||
if fskips:
|
||||
#tr.write_sep("_", "skipped test summary")
|
||||
for num, fspath, lineno, reason in fskips:
|
||||
if reason.startswith("Skipped: "):
|
||||
reason = reason[9:]
|
||||
lines.append("SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno, reason))
|
||||
395
pytest/plugin/pytest_terminal.py
Normal file
395
pytest/plugin/pytest_terminal.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Implements terminal reporting of the full testing process.
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group._addoption('-v', '--verbose', action="count",
|
||||
dest="verbose", default=0, help="increase verbosity."),
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default=None, metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(s)skipped, (x)failed, (X)passed.")
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
group._addoption('--report',
|
||||
action="store", dest="report", default=None, metavar="opts",
|
||||
help="(deprecated, use -r)")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='long',
|
||||
type="choice", choices=['long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (long/short/line/no).")
|
||||
group._addoption('--fulltrace',
|
||||
action="store_true", dest="fulltrace", default=False,
|
||||
help="don't cut any tracebacks (default is to cut).")
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.option.collectonly:
|
||||
reporter = CollectonlyReporter(config)
|
||||
else:
|
||||
reporter = TerminalReporter(config)
|
||||
config.pluginmanager.register(reporter, 'terminalreporter')
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
optvalue = config.getvalue("report")
|
||||
if optvalue:
|
||||
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
|
||||
file=py.std.sys.stderr)
|
||||
if optvalue:
|
||||
for setting in optvalue.split(","):
|
||||
setting = setting.strip()
|
||||
if setting == "skipped":
|
||||
reportopts += "s"
|
||||
elif setting == "xfailed":
|
||||
reportopts += "x"
|
||||
reportchars = config.getvalue("reportchars")
|
||||
if reportchars:
|
||||
for char in reportchars:
|
||||
if char not in reportopts:
|
||||
reportopts += char
|
||||
return reportopts
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.passed:
|
||||
letter = "."
|
||||
elif report.skipped:
|
||||
letter = "s"
|
||||
elif report.failed:
|
||||
letter = "F"
|
||||
if report.when != "call":
|
||||
letter = "f"
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
class TerminalReporter:
|
||||
def __init__(self, config, file=None):
|
||||
self.config = config
|
||||
self.stats = {}
|
||||
self.curdir = py.path.local()
|
||||
if file is None:
|
||||
file = py.std.sys.stdout
|
||||
# we try hard to make printing resilient against
|
||||
# later changes on FD level.
|
||||
if hasattr(os, 'dup') and hasattr(file, 'fileno'):
|
||||
try:
|
||||
newfd = os.dup(file.fileno())
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
file = os.fdopen(newfd, file.mode, 1)
|
||||
self._tw = py.io.TerminalWriter(file)
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char,char)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, fspath, res):
|
||||
if fspath != self.currentfspath:
|
||||
self.currentfspath = fspath
|
||||
fspath = self.curdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
relpath = self.curdir.bestrelpath(fspath)
|
||||
self._tw.write(relpath + " ")
|
||||
self._tw.write(res)
|
||||
|
||||
def write_ensure_prefix(self, prefix, extra="", **kwargs):
|
||||
if self.currentfspath != prefix:
|
||||
self._tw.line()
|
||||
self.currentfspath = prefix
|
||||
self._tw.write(prefix)
|
||||
if extra:
|
||||
self._tw.write(extra, **kwargs)
|
||||
self.currentfspath = -2
|
||||
|
||||
def ensure_newline(self):
|
||||
if self.currentfspath:
|
||||
self._tw.line()
|
||||
self.currentfspath = None
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
line = str(line)
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
def write_sep(self, sep, title=None, **markup):
|
||||
self.ensure_newline()
|
||||
self._tw.sep(sep, title, **markup)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in str(excrepr).split("\n"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: %s" %(plugin,)
|
||||
# XXX this event may happen during setup/teardown time
|
||||
# which unfortunately captures our output here
|
||||
# which garbles our output if we use self.write_line
|
||||
self.write_line(msg)
|
||||
|
||||
def pytest_trace(self, category, msg):
|
||||
if self.config.option.debug or \
|
||||
self.config.option.traceconfig and category.find("config") != -1:
|
||||
self.write_line("[%s] %s" %(category, msg))
|
||||
|
||||
def pytest_deselected(self, items):
|
||||
self.stats.setdefault('deselected', []).extend(items)
|
||||
|
||||
def pytest__teardown_final_logerror(self, report):
|
||||
self.stats.setdefault("error", []).append(report)
|
||||
|
||||
def pytest_runtest_logstart(self, nodeid, location, fspath):
|
||||
# ensure that the path is printed before the
|
||||
# 1st test of a module starts running
|
||||
if self.config.option.verbose:
|
||||
line = self._locationline(fspath, *location)
|
||||
self.write_ensure_prefix(line, "")
|
||||
else:
|
||||
self.write_fspath_result(py.path.local(fspath), "")
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep)
|
||||
cat, letter, word = res
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
if not self.config.option.verbose:
|
||||
if not hasattr(rep, 'node'):
|
||||
self.write_fspath_result(rep.fspath, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
if rep.passed:
|
||||
markup = {'green':True}
|
||||
elif rep.failed:
|
||||
markup = {'red':True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow':True}
|
||||
line = self._locationline(str(rep.fspath), *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
#self._tw.write(word, **markup)
|
||||
else:
|
||||
self.ensure_newline()
|
||||
if hasattr(rep, 'node'):
|
||||
self._tw.write("[%s] " % rep.node.gateway.id)
|
||||
self._tw.write(word, **markup)
|
||||
self._tw.write(" " + line)
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
self.stats.setdefault("error", []).append(report)
|
||||
self.write_fspath_result(report.fspath, "E")
|
||||
elif report.skipped:
|
||||
self.stats.setdefault("skipped", []).append(report)
|
||||
self.write_fspath_result(report.fspath, "S")
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
self._sessionstarttime = py.std.time.time()
|
||||
verinfo = ".".join(map(str, sys.version_info[:3]))
|
||||
msg = "platform %s -- Python %s" % (sys.platform, verinfo)
|
||||
msg += " -- pytest-%s" % (py.__version__)
|
||||
if self.config.option.verbose or self.config.option.debug or \
|
||||
getattr(self.config.option, 'pastebin', None):
|
||||
msg += " -- " + str(sys.executable)
|
||||
self.write_line(msg)
|
||||
lines = self.config.hook.pytest_report_header(config=self.config)
|
||||
lines.reverse()
|
||||
for line in flatten(lines):
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_log_finishcollection(self):
|
||||
for i, testarg in enumerate(self.config.args):
|
||||
self.write_line("test path %d: %s" %(i+1, testarg))
|
||||
|
||||
def pytest_sessionfinish(self, exitstatus, __multicall__):
|
||||
__multicall__.execute()
|
||||
self._tw.line("")
|
||||
if exitstatus in (0, 1, 2):
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
if exitstatus == 2:
|
||||
self._report_keyboardinterrupt()
|
||||
self.summary_deselected()
|
||||
self.summary_stats()
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
|
||||
|
||||
def _report_keyboardinterrupt(self):
|
||||
excrepr = self._keyboardinterrupt_memo
|
||||
msg = excrepr.reprcrash.message
|
||||
self.write_sep("!", msg)
|
||||
if "KeyboardInterrupt" in msg:
|
||||
if self.config.getvalue("fulltrace"):
|
||||
excrepr.toterminal(self._tw)
|
||||
else:
|
||||
excrepr.reprcrash.toterminal(self._tw)
|
||||
|
||||
def _locationline(self, collect_fspath, fspath, lineno, domain):
|
||||
if fspath and fspath != collect_fspath:
|
||||
fspath = "%s <- %s" % (
|
||||
self.curdir.bestrelpath(py.path.local(collect_fspath)),
|
||||
self.curdir.bestrelpath(py.path.local(fspath)))
|
||||
elif fspath:
|
||||
fspath = self.curdir.bestrelpath(py.path.local(fspath))
|
||||
if lineno is not None:
|
||||
lineno += 1
|
||||
if fspath and lineno and domain:
|
||||
line = "%(fspath)s:%(lineno)s: %(domain)s"
|
||||
elif fspath and domain:
|
||||
line = "%(fspath)s: %(domain)s"
|
||||
elif fspath and lineno:
|
||||
line = "%(fspath)s:%(lineno)s %(extrapath)s"
|
||||
else:
|
||||
line = "[nolocation]"
|
||||
return line % locals() + " "
|
||||
|
||||
def _getfailureheadline(self, rep):
|
||||
if hasattr(rep, 'location'):
|
||||
fspath, lineno, domain = rep.location
|
||||
return domain
|
||||
else:
|
||||
return "test session" # XXX?
|
||||
|
||||
def _getcrashline(self, rep):
|
||||
try:
|
||||
return str(rep.longrepr.reprcrash)
|
||||
except AttributeError:
|
||||
try:
|
||||
return str(rep.longrepr)[:50]
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
#
|
||||
# summaries for sessionfinish
|
||||
#
|
||||
|
||||
def summary_failures(self):
|
||||
tbstyle = self.config.getvalue("tbstyle")
|
||||
if 'failed' in self.stats and tbstyle != "no":
|
||||
self.write_sep("=", "FAILURES")
|
||||
for rep in self.stats['failed']:
|
||||
if tbstyle == "line":
|
||||
line = self._getcrashline(rep)
|
||||
self.write_line(line)
|
||||
else:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
rep.toterminal(self._tw)
|
||||
|
||||
def summary_errors(self):
|
||||
if 'error' in self.stats and self.config.option.tbstyle != "no":
|
||||
self.write_sep("=", "ERRORS")
|
||||
for rep in self.stats['error']:
|
||||
msg = self._getfailureheadline(rep)
|
||||
if not hasattr(rep, 'when'):
|
||||
# collect
|
||||
msg = "ERROR collecting " + msg
|
||||
elif rep.when == "setup":
|
||||
msg = "ERROR at setup of " + msg
|
||||
elif rep.when == "teardown":
|
||||
msg = "ERROR at teardown of " + msg
|
||||
self.write_sep("_", msg)
|
||||
rep.toterminal(self._tw)
|
||||
|
||||
def summary_stats(self):
|
||||
session_duration = py.std.time.time() - self._sessionstarttime
|
||||
|
||||
keys = "failed passed skipped deselected".split()
|
||||
for key in self.stats.keys():
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
parts = []
|
||||
for key in keys:
|
||||
val = self.stats.get(key, None)
|
||||
if val:
|
||||
parts.append("%d %s" %(len(val), key))
|
||||
line = ", ".join(parts)
|
||||
# XXX coloring
|
||||
self.write_sep("=", "%s in %.2f seconds" %(line, session_duration), bold=True)
|
||||
|
||||
def summary_deselected(self):
|
||||
if 'deselected' in self.stats:
|
||||
self.write_sep("=", "%d tests deselected by %r" %(
|
||||
len(self.stats['deselected']), self.config.option.keyword), bold=True)
|
||||
|
||||
|
||||
class CollectonlyReporter:
|
||||
INDENT = " "
|
||||
|
||||
def __init__(self, config, out=None):
|
||||
self.config = config
|
||||
if out is None:
|
||||
out = py.std.sys.stdout
|
||||
self._tw = py.io.TerminalWriter(out)
|
||||
self.indent = ""
|
||||
self._failed = []
|
||||
|
||||
def outindent(self, line):
|
||||
self._tw.line(self.indent + str(line))
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in str(excrepr).split("\n"):
|
||||
self._tw.line("INTERNALERROR> " + line)
|
||||
|
||||
def pytest_collectstart(self, collector):
|
||||
self.outindent(collector)
|
||||
self.indent += self.INDENT
|
||||
|
||||
def pytest_log_itemcollect(self, item):
|
||||
self.outindent(item)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if hasattr(report.longrepr, 'reprcrash'):
|
||||
msg = report.longrepr.reprcrash.message
|
||||
else:
|
||||
# XXX unify (we have CollectErrorRepr here)
|
||||
msg = str(report.longrepr.longrepr)
|
||||
self.outindent("!!! %s !!!" % msg)
|
||||
#self.outindent("!!! error !!!")
|
||||
self._failed.append(report)
|
||||
self.indent = self.indent[:-len(self.INDENT)]
|
||||
|
||||
def pytest_log_finishcollection(self):
|
||||
if self._failed:
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self._failed:
|
||||
rep.toterminal(self._tw)
|
||||
return self._failed and 1 or 0
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
if v is None:
|
||||
v = sys.version_info
|
||||
try:
|
||||
return "%s.%s.%s-%s-%s" % v
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
def flatten(l):
|
||||
for x in l:
|
||||
if isinstance(x, (list, tuple)):
|
||||
for y in flatten(x):
|
||||
yield y
|
||||
else:
|
||||
yield x
|
||||
|
||||
22
pytest/plugin/pytest_tmpdir.py
Normal file
22
pytest/plugin/pytest_tmpdir.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""provide temporary directories to test functions.
|
||||
|
||||
usage example::
|
||||
|
||||
def test_plugin(tmpdir):
|
||||
tmpdir.join("hello").write("hello")
|
||||
|
||||
.. _`py.path.local`: ../../path.html
|
||||
|
||||
"""
|
||||
import py
|
||||
|
||||
def pytest_funcarg__tmpdir(request):
|
||||
"""return a temporary directory path object
|
||||
unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a `py.path.local`_
|
||||
path object.
|
||||
"""
|
||||
name = request.function.__name__
|
||||
x = request.config.mktemp(name, numbered=True)
|
||||
return x.realpath()
|
||||
81
pytest/plugin/pytest_unittest.py
Normal file
81
pytest/plugin/pytest_unittest.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
automatically discover and run traditional "unittest.py" style tests.
|
||||
|
||||
Usage
|
||||
----------------
|
||||
|
||||
This plugin collects and runs Python `unittest.py style`_ tests.
|
||||
It will automatically collect ``unittest.TestCase`` subclasses
|
||||
and their ``test`` methods from the test modules of a project
|
||||
(usually following the ``test_*.py`` pattern).
|
||||
|
||||
This plugin is enabled by default.
|
||||
|
||||
.. _`unittest.py style`: http://docs.python.org/library/unittest.html
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if 'unittest' not in sys.modules:
|
||||
return # nobody derived unittest.TestCase
|
||||
try:
|
||||
isunit = issubclass(obj, py.std.unittest.TestCase)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if isunit:
|
||||
return UnitTestCase(name, parent=collector)
|
||||
|
||||
class UnitTestCase(py.test.collect.Class):
|
||||
def collect(self):
|
||||
return [UnitTestCaseInstance("()", self)]
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
_dummy = object()
|
||||
class UnitTestCaseInstance(py.test.collect.Instance):
|
||||
def collect(self):
|
||||
loader = py.std.unittest.TestLoader()
|
||||
names = loader.getTestCaseNames(self.obj.__class__)
|
||||
l = []
|
||||
for name in names:
|
||||
callobj = getattr(self.obj, name)
|
||||
if py.builtin.callable(callobj):
|
||||
l.append(UnitTestFunction(name, parent=self))
|
||||
return l
|
||||
|
||||
def _getobj(self):
|
||||
x = self.parent.obj
|
||||
return self.parent.obj(methodName='run')
|
||||
|
||||
class UnitTestFunction(py.test.collect.Function):
|
||||
def __init__(self, name, parent, args=(), obj=_dummy, sort_value=None):
|
||||
super(UnitTestFunction, self).__init__(name, parent)
|
||||
self._args = args
|
||||
if obj is not _dummy:
|
||||
self._obj = obj
|
||||
self._sort_value = sort_value
|
||||
if hasattr(self.parent, 'newinstance'):
|
||||
self.parent.newinstance()
|
||||
self.obj = self._getobj()
|
||||
|
||||
def runtest(self):
|
||||
target = self.obj
|
||||
args = self._args
|
||||
target(*args)
|
||||
|
||||
def setup(self):
|
||||
instance = py.builtin._getimself(self.obj)
|
||||
instance.setUp()
|
||||
|
||||
def teardown(self):
|
||||
instance = py.builtin._getimself(self.obj)
|
||||
instance.tearDown()
|
||||
|
||||
63
pytest/plugin/standalonetemplate.py
Executable file
63
pytest/plugin/standalonetemplate.py
Executable file
@@ -0,0 +1,63 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
sources = """
|
||||
@SOURCES@"""
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import zlib
|
||||
import imp
|
||||
|
||||
class DictImporter(object):
|
||||
def __init__(self, sources):
|
||||
self.sources = sources
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname in self.sources:
|
||||
return self
|
||||
if fullname+'.__init__' in self.sources:
|
||||
return self
|
||||
return None
|
||||
|
||||
def load_module(self, fullname):
|
||||
# print "load_module:", fullname
|
||||
from types import ModuleType
|
||||
try:
|
||||
s = self.sources[fullname]
|
||||
is_pkg = False
|
||||
except KeyError:
|
||||
s = self.sources[fullname+'.__init__']
|
||||
is_pkg = True
|
||||
|
||||
co = compile(s, fullname, 'exec')
|
||||
module = sys.modules.setdefault(fullname, ModuleType(fullname))
|
||||
module.__file__ = "%s/%s" % (__file__, fullname)
|
||||
module.__loader__ = self
|
||||
if is_pkg:
|
||||
module.__path__ = [fullname]
|
||||
|
||||
do_exec(co, module.__dict__)
|
||||
return sys.modules[fullname]
|
||||
|
||||
def get_source(self, name):
|
||||
res = self.sources.get(name)
|
||||
if res is None:
|
||||
res = self.sources.get(name+'.__init__')
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.version_info >= (3,0):
|
||||
exec("def do_exec(co, loc): exec(co, loc)\n")
|
||||
import pickle
|
||||
sources = sources.encode("ascii") # ensure bytes
|
||||
sources = pickle.loads(zlib.decompress(base64.decodebytes(sources)))
|
||||
else:
|
||||
import cPickle as pickle
|
||||
exec("def do_exec(co, loc): exec co in loc\n")
|
||||
sources = pickle.loads(zlib.decompress(base64.decodestring(sources)))
|
||||
|
||||
importer = DictImporter(sources)
|
||||
sys.meta_path.append(importer)
|
||||
|
||||
import py
|
||||
py.cmdline.pytest()
|
||||
Reference in New Issue
Block a user