Commit 82fd0180 authored by Benjamin Peterson's avatar Benjamin Peterson

upgrade to latest development version of py.test

parent 17316782
#
__version__ = '2.0.3'
__version__ = '2.1.0.dev4'
"""
support for presenting detailed information in failing assertions.
"""
import py
import imp
import marshal
import struct
import sys
import pytest
from _pytest.monkeypatch import monkeypatch
from _pytest.assertion import reinterpret, util
try:
from _pytest.assertion.rewrite import rewrite_asserts
except ImportError:
rewrite_asserts = None
else:
import ast
def pytest_addoption(parser):
group = parser.getgroup("debugconfig")
group.addoption('--assertmode', action="store", dest="assertmode",
choices=("on", "old", "off", "default"), default="default",
metavar="on|old|off",
help="""control assertion debugging tools.
'off' performs no assertion debugging.
'old' reinterprets the expressions in asserts to glean information.
'on' (the default) rewrites the assert statements in test modules to provide
sub-expression results.""")
group.addoption('--no-assert', action="store_true", default=False,
dest="noassert", help="DEPRECATED equivalent to --assertmode=off")
group.addoption('--nomagic', action="store_true", default=False,
dest="nomagic", help="DEPRECATED equivalent to --assertmode=off")
class AssertionState:
"""State for the assertion plugin."""
def __init__(self, config, mode):
self.mode = mode
self.trace = config.trace.root.get("assertion")
def pytest_configure(config):
warn_about_missing_assertion()
mode = config.getvalue("assertmode")
if config.getvalue("noassert") or config.getvalue("nomagic"):
if mode not in ("off", "default"):
raise pytest.UsageError("assertion options conflict")
mode = "off"
elif mode == "default":
mode = "on"
if mode != "off":
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)
m = monkeypatch()
config._cleanup.append(m.undo)
m.setattr(py.builtin.builtins, 'AssertionError',
reinterpret.AssertionError)
m.setattr(util, '_reprcompare', callbinrepr)
if mode == "on" and rewrite_asserts is None:
mode = "old"
config._assertstate = AssertionState(config, mode)
config._assertstate.trace("configured with mode set to %r" % (mode,))
def _write_pyc(co, source_path):
if hasattr(imp, "cache_from_source"):
# Handle PEP 3147 pycs.
pyc = py.path.local(imp.cache_from_source(str(source_path)))
pyc.ensure()
else:
pyc = source_path + "c"
mtime = int(source_path.mtime())
fp = pyc.open("wb")
try:
fp.write(imp.get_magic())
fp.write(struct.pack("<l", mtime))
marshal.dump(co, fp)
finally:
fp.close()
return pyc
def before_module_import(mod):
if mod.config._assertstate.mode != "on":
return
# Some deep magic: load the source, rewrite the asserts, and write a
# fake pyc, so that it'll be loaded when the module is imported.
source = mod.fspath.read()
try:
tree = ast.parse(source)
except SyntaxError:
# Let this pop up again in the real import.
mod.config._assertstate.trace("failed to parse: %r" % (mod.fspath,))
return
rewrite_asserts(tree)
try:
co = compile(tree, str(mod.fspath), "exec")
except SyntaxError:
# It's possible that this error is from some bug in the assertion
# rewriting, but I don't know of a fast way to tell.
mod.config._assertstate.trace("failed to compile: %r" % (mod.fspath,))
return
mod._pyc = _write_pyc(co, mod.fspath)
mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,))
def after_module_import(mod):
if not hasattr(mod, "_pyc"):
return
state = mod.config._assertstate
try:
mod._pyc.remove()
except py.error.ENOENT:
state.trace("couldn't find pyc: %r" % (mod._pyc,))
else:
state.trace("removed pyc: %r" % (mod._pyc,))
def warn_about_missing_assertion():
try:
assert False
except AssertionError:
pass
else:
sys.stderr.write("WARNING: failing tests may report as passing because "
"assertions are turned off! (are you using python -O?)\n")
pytest_assertrepr_compare = util.assertrepr_compare
"""
Find intermediate evalutation results in assert statements through builtin AST.
This should replace _assertionold.py eventually.
This should replace oldinterpret.py eventually.
"""
import sys
import ast
import py
from py._code.assertion import _format_explanation, BuiltinAssertionError
from _pytest.assertion import util
from _pytest.assertion.reinterpret import BuiltinAssertionError
if sys.platform.startswith("java") and sys.version_info < (2, 5, 2):
......@@ -59,21 +60,18 @@ def run(offending_line, frame=None):
frame = py.code.Frame(sys._getframe(1))
return interpret(offending_line, frame)
def getfailure(failure):
explanation = _format_explanation(failure.explanation)
value = failure.cause[1]
def getfailure(e):
explanation = util.format_explanation(e.explanation)
value = e.cause[1]
if str(value):
lines = explanation.splitlines()
if not lines:
lines.append("")
lines[0] += " << %s" % (value,)
explanation = "\n".join(lines)
text = "%s: %s" % (failure.cause[0].__name__, explanation)
if text.startswith("AssertionError: assert "):
lines = explanation.split('\n')
lines[0] += " << %s" % (value,)
explanation = '\n'.join(lines)
text = "%s: %s" % (e.cause[0].__name__, explanation)
if text.startswith('AssertionError: assert '):
text = text[16:]
return text
operator_map = {
ast.BitOr : "|",
ast.BitXor : "^",
......@@ -154,8 +152,8 @@ class DebugInterpreter(ast.NodeVisitor):
local = self.frame.eval(co)
except Exception:
# have to assume it isn't
local = False
if not local:
local = None
if local is None or not self.frame.is_true(local):
return name.id, result
return explanation, result
......@@ -175,7 +173,7 @@ class DebugInterpreter(ast.NodeVisitor):
except Exception:
raise Failure(explanation)
try:
if not result:
if not self.frame.is_true(result):
break
except KeyboardInterrupt:
raise
......@@ -183,9 +181,8 @@ class DebugInterpreter(ast.NodeVisitor):
break
left_explanation, left_result = next_explanation, next_result
rcomp = py.code._reprcompare
if rcomp:
res = rcomp(op_symbol, left_result, next_result)
if util._reprcompare is not None:
res = util._reprcompare(op_symbol, left_result, next_result)
if res:
explanation = res
return explanation, result
......@@ -302,8 +299,8 @@ class DebugInterpreter(ast.NodeVisitor):
try:
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
except Exception:
from_instance = True
if from_instance:
from_instance = None
if from_instance is None or self.frame.is_true(from_instance):
rep = self.frame.repr(result)
pattern = "%s\n{%s = %s\n}"
explanation = pattern % (rep, rep, explanation)
......@@ -311,11 +308,8 @@ class DebugInterpreter(ast.NodeVisitor):
def visit_Assert(self, assrt):
test_explanation, test_result = self.visit(assrt.test)
if test_explanation.startswith("False\n{False =") and \
test_explanation.endswith("\n"):
test_explanation = test_explanation[15:-2]
explanation = "assert %s" % (test_explanation,)
if not test_result:
if not self.frame.is_true(test_result):
try:
raise BuiltinAssertionError
except Exception:
......
import py
import sys, inspect
from compiler import parse, ast, pycodegen
from py._code.assertion import BuiltinAssertionError, _format_explanation
from _pytest.assertion.util import format_explanation
from _pytest.assertion.reinterpret import BuiltinAssertionError
passthroughex = py.builtin._sysex
......@@ -131,7 +132,7 @@ class Interpretable(View):
raise Failure(self)
def nice_explanation(self):
return _format_explanation(self.explanation)
return format_explanation(self.explanation)
class Name(Interpretable):
......@@ -383,10 +384,6 @@ class Assert(Interpretable):
def run(self, frame):
test = Interpretable(self.test)
test.eval(frame)
# simplify 'assert False where False = ...'
if (test.explanation.startswith('False\n{False = ') and
test.explanation.endswith('\n}')):
test.explanation = test.explanation[15:-2]
# print the result as 'assert <explanation>'
self.result = test.result
self.explanation = 'assert ' + test.explanation
......
......@@ -3,52 +3,6 @@ import py
BuiltinAssertionError = py.builtin.builtins.AssertionError
_reprcompare = None # if set, will be called by assert reinterp for comparison ops
def _format_explanation(explanation):
"""This formats an explanation
Normally all embedded newlines are escaped, however there are
three exceptions: \n{, \n} and \n~. The first two are intended
cover nested explanations, see function and attribute explanations
for examples (.visit_Call(), visit_Attribute()). The last one is
for when one explanation needs to span multiple lines, e.g. when
displaying diffs.
"""
raw_lines = (explanation or '').split('\n')
# escape newlines not followed by {, } and ~
lines = [raw_lines[0]]
for l in raw_lines[1:]:
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
lines.append(l)
else:
lines[-1] += '\\n' + l
result = lines[:1]
stack = [0]
stackcnt = [0]
for line in lines[1:]:
if line.startswith('{'):
if stackcnt[-1]:
s = 'and '
else:
s = 'where '
stack.append(len(result))
stackcnt[-1] += 1
stackcnt.append(0)
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
elif line.startswith('}'):
assert line.startswith('}')
stack.pop()
stackcnt.pop()
result[stack[-1]] += line[1:]
else:
assert line.startswith('~')
result.append(' '*len(stack) + line[1:])
assert len(stack) == 1
return '\n'.join(result)
class AssertionError(BuiltinAssertionError):
def __init__(self, *args):
BuiltinAssertionError.__init__(self, *args)
......@@ -86,9 +40,9 @@ if sys.version_info > (3, 0):
AssertionError.__module__ = "builtins"
reinterpret_old = "old reinterpretation not available for py3"
else:
from py._code._assertionold import interpret as reinterpret_old
from _pytest.assertion.oldinterpret import interpret as reinterpret_old
if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
from py._code._assertionnew import interpret as reinterpret
from _pytest.assertion.newinterpret import interpret as reinterpret
else:
reinterpret = reinterpret_old
This diff is collapsed.
"""
support for presented detailed information in failing assertions.
"""
"""Utilities for assertion debugging"""
import py
import sys
from _pytest.monkeypatch import monkeypatch
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 _reprcompare attribute on the py.code module 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.
m = monkeypatch()
config._cleanup.append(m.undo)
warn_about_missing_assertion()
if not config.getvalue("noassert") and not config.getvalue("nomagic"):
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)
m.setattr(py.builtin.builtins,
'AssertionError', py.code._AssertionError)
m.setattr(py.code, '_reprcompare', callbinrepr)
def warn_about_missing_assertion():
try:
assert False
except AssertionError:
pass
else:
sys.stderr.write("WARNING: failing tests may report as passing because "
"assertions are turned off! (are you using python -O?)\n")
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare = None
def format_explanation(explanation):
"""This formats an explanation
Normally all embedded newlines are escaped, however there are
three exceptions: \n{, \n} and \n~. The first two are intended
cover nested explanations, see function and attribute explanations
for examples (.visit_Call(), visit_Attribute()). The last one is
for when one explanation needs to span multiple lines, e.g. when
displaying diffs.
"""
# simplify 'assert False where False = ...'
where = 0
while True:
start = where = explanation.find("False\n{False = ", where)
if where == -1:
break
level = 0
for i, c in enumerate(explanation[start:]):
if c == "{":
level += 1
elif c == "}":
level -= 1
if not level:
break
else:
raise AssertionError("unbalanced braces: %r" % (explanation,))
end = start + i
where = end
if explanation[end - 1] == '\n':
explanation = (explanation[:start] + explanation[start+15:end-1] +
explanation[end+1:])
where -= 17
raw_lines = (explanation or '').split('\n')
# escape newlines not followed by {, } and ~
lines = [raw_lines[0]]
for l in raw_lines[1:]:
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
lines.append(l)
else:
lines[-1] += '\\n' + l
result = lines[:1]
stack = [0]
stackcnt = [0]
for line in lines[1:]:
if line.startswith('{'):
if stackcnt[-1]:
s = 'and '
else:
s = 'where '
stack.append(len(result))
stackcnt[-1] += 1
stackcnt.append(0)
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
elif line.startswith('}'):
assert line.startswith('}')
stack.pop()
stackcnt.pop()
result[stack[-1]] += line[1:]
else:
assert line.startswith('~')
result.append(' '*len(stack) + line[1:])
assert len(stack) == 1
return '\n'.join(result)
# Provide basestring in python3
try:
......@@ -46,7 +82,7 @@ except NameError:
basestring = str
def pytest_assertrepr_compare(op, left, right):
def assertrepr_compare(op, left, right):
"""return specialised explanations for some operators/operands"""
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
left_repr = py.io.saferepr(left, maxsize=int(width/2))
......
......@@ -59,7 +59,7 @@ class DoctestItem(pytest.Item):
inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
lines += ["UNEXPECTED EXCEPTION: %s" %
repr(inner_excinfo.value)]
lines += py.std.traceback.format_exception(*excinfo.value.exc_info)
return ReprFailDoctest(reprlocation, lines)
else:
return super(DoctestItem, self).repr_failure(excinfo)
......
......@@ -16,9 +16,6 @@ def pytest_addoption(parser):
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.")
......
......@@ -65,7 +65,8 @@ def pytest_unconfigure(config):
class LogXML(object):
def __init__(self, logfile, prefix):
self.logfile = logfile
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(logfile)
self.prefix = prefix
self.test_logs = []
self.passed = self.skipped = 0
......@@ -76,7 +77,7 @@ class LogXML(object):
names = report.nodeid.split("::")
names[0] = names[0].replace("/", '.')
names = tuple(names)
d = {'time': self._durations.pop(names, "0")}
d = {'time': self._durations.pop(report.nodeid, "0")}
names = [x.replace(".py", "") for x in names if x != "()"]
classnames = names[:-1]
if self.prefix:
......@@ -170,12 +171,11 @@ class LogXML(object):
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
self._durations[item.nodeid] = time.time() - start
def pytest_collectreport(self, report):
if not report.passed:
......
......@@ -46,23 +46,25 @@ def pytest_addoption(parser):
def pytest_namespace():
return dict(collect=dict(Item=Item, Collector=Collector, File=File))
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
return dict(collect=collect)
def pytest_configure(config):
py.test.config = config # compatibiltiy
if config.option.exitfirst:
config.option.maxfail = 1
def pytest_cmdline_main(config):
""" default command line protocol for initialization, session,
running tests and reporting. """
def wrap_session(config, doit):
"""Skeleton command line program"""
session = Session(config)
session.exitstatus = EXIT_OK
initstate = 0
try:
config.pluginmanager.do_configure(config)
initstate = 1
config.hook.pytest_sessionstart(session=session)
config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)
initstate = 2
doit(config, session)
except pytest.UsageError:
raise
except KeyboardInterrupt:
......@@ -77,18 +79,24 @@ def pytest_cmdline_main(config):
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
if not session.exitstatus and session._testsfailed:
session.exitstatus = EXIT_TESTSFAILED
config.hook.pytest_sessionfinish(session=session,
exitstatus=session.exitstatus)
config.pluginmanager.do_unconfigure(config)
if initstate >= 2:
config.hook.pytest_sessionfinish(session=session,
exitstatus=session.exitstatus)
if initstate >= 1:
config.pluginmanager.do_unconfigure(config)
return session.exitstatus
def pytest_cmdline_main(config):
return wrap_session(config, _main)
def _main(config, session):
""" default command line protocol for initialization, session,
running tests and reporting. """
config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)
def pytest_collection(session):
session.perform_collect()
hook = session.config.hook
hook.pytest_collection_modifyitems(session=session,
config=session.config, items=session.items)
hook.pytest_collection_finish(session=session)
return True
return session.perform_collect()
def pytest_runtestloop(session):
if session.config.option.collectonly:
......@@ -374,6 +382,16 @@ class Session(FSCollector):
return HookProxy(fspath, self.config)
def perform_collect(self, args=None, genitems=True):
hook = self.config.hook
try:
items = self._perform_collect(args, genitems)
hook.pytest_collection_modifyitems(session=self,
config=self.config, items=items)
finally:
hook.pytest_collection_finish(session=self)
return items
def _perform_collect(self, args, genitems):
if args is None:
args = self.config.args
self.trace("perform_collect", self, args)
......
......@@ -153,7 +153,7 @@ class MarkInfo:
def __repr__(self):
return "<MarkInfo %r args=%r kwargs=%r>" % (
self._name, self.args, self.kwargs)
self.name, self.args, self.kwargs)
def pytest_itemcollected(item):
if not isinstance(item, pytest.Function):
......
......@@ -6,7 +6,7 @@ import re
import inspect
import time
from fnmatch import fnmatch
from _pytest.main import Session
from _pytest.main import Session, EXIT_OK
from py.builtin import print_
from _pytest.core import HookRelay
......@@ -292,13 +292,19 @@ class TmpTestdir:
assert '::' not in str(arg)
p = py.path.local(arg)
x = session.fspath.bestrelpath(p)
return session.perform_collect([x], genitems=False)[0]
config.hook.pytest_sessionstart(session=session)
res = session.perform_collect([x], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
return res
def getpathnode(self, path):
config = self.parseconfig(path)
config = self.parseconfigure(path)
session = Session(config)
x = session.fspath.bestrelpath(path)
return session.perform_collect([x], genitems=False)[0]
config.hook.pytest_sessionstart(session=session)
res = session.perform_collect([x], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
return res
def genitems(self, colitems):
session = colitems[0].session
......@@ -312,7 +318,9 @@ class TmpTestdir:
config = self.parseconfigure(*args)
rec = self.getreportrecorder(config)
session = Session(config)
config.hook.pytest_sessionstart(session=session)
session.perform_collect()
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
return session.items, rec
def runitem(self, source):
......@@ -382,6 +390,8 @@ class TmpTestdir:
c.basetemp = py.path.local.make_numbered_dir(prefix="reparse",
keep=0, rootdir=self.tmpdir, lock_timeout=None)
c.parse(args)
c.pluginmanager.do_configure(c)
self.request.addfinalizer(lambda: c.pluginmanager.do_unconfigure(c))
return c
finally:
py.test.config = oldconfig
......
......@@ -226,8 +226,13 @@ class Module(pytest.File, PyCollectorMixin):
def _importtestmodule(self):
# we assume we are only called once per module
from _pytest import assertion
assertion.before_module_import(self)
try:
mod = self.fspath.pyimport(ensuresyspath=True)
try:
mod = self.fspath.pyimport(ensuresyspath=True)
finally:
assertion.after_module_import(self)
except SyntaxError:
excinfo = py.code.ExceptionInfo()
raise self.CollectError(excinfo.getrepr(style="short"))
......@@ -374,7 +379,7 @@ class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
# 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)
self.session._setupstate.prepare(self)
# see FunctionMixin.setup and test_setupstate_is_preserved_134
self._preservedparent = self.parent.obj
l = []
......@@ -721,7 +726,7 @@ class FuncargRequest:
def _addfinalizer(self, finalizer, scope):
colitem = self._getscopeitem(scope)
self.config._setupstate.addfinalizer(
self._pyfuncitem.session._setupstate.addfinalizer(
finalizer=finalizer, colitem=colitem)
def __repr__(self):
......@@ -742,8 +747,10 @@ class FuncargRequest:
raise self.LookupError(msg)
def showfuncargs(config):
from _pytest.main import Session
session = Session(config)
from _pytest.main import wrap_session
return wrap_session(config, _showfuncargs_main)
def _showfuncargs_main(config, session):
session.perform_collect()
if session.items:
plugins = session.items[0].getplugins()
......
......@@ -14,17 +14,15 @@ def pytest_namespace():
#
# pytest plugin hooks
# XXX move to pytest_sessionstart and fix py.test owns tests
def pytest_configure(config):
config._setupstate = SetupState()
def pytest_sessionstart(session):
session._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: