Commit dd0e8d64 authored by Armin Rigo's avatar Armin Rigo

Backout changes 3c0ae7fce1c7 to cfbda2605f9d

We should discuss on irc whether it's a good idea or not.
Most importantly, this doesn't seem to work on Python 2.5,
which I find to be still an important platform.
parent e2f131eb
#
__version__ = '2.1.0.dev4'
__version__ = '2.0.3'
"""Utilities for assertion debugging"""
"""
support for presented detailed information in failing assertions.
"""
import py
# 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)
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")
# Provide basestring in python3
try:
......@@ -82,7 +46,7 @@ except NameError:
basestring = str
def assertrepr_compare(op, left, right):
def pytest_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))
......
"""
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
This diff is collapsed.
......@@ -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,6 +16,9 @@ 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,8 +65,7 @@ def pytest_unconfigure(config):
class LogXML(object):
def __init__(self, logfile, prefix):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(logfile)
self.logfile = logfile
self.prefix = prefix
self.test_logs = []
self.passed = self.skipped = 0
......@@ -77,7 +76,7 @@ class LogXML(object):
names = report.nodeid.split("::")
names[0] = names[0].replace("/", '.')
names = tuple(names)
d = {'time': self._durations.pop(report.nodeid, "0")}
d = {'time': self._durations.pop(names, "0")}
names = [x.replace(".py", "") for x in names if x != "()"]
classnames = names[:-1]
if self.prefix:
......@@ -171,11 +170,12 @@ 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[item.nodeid] = time.time() - start
self._durations[names] = time.time() - start
def pytest_collectreport(self, report):
if not report.passed:
......
......@@ -46,25 +46,23 @@ def pytest_addoption(parser):
def pytest_namespace():
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
return dict(collect=collect)
return dict(collect=dict(Item=Item, Collector=Collector, File=File))
def pytest_configure(config):
py.test.config = config # compatibiltiy
if config.option.exitfirst:
config.option.maxfail = 1
def wrap_session(config, doit):
"""Skeleton command line program"""
def pytest_cmdline_main(config):
""" default command line protocol for initialization, session,
running tests and reporting. """
session = Session(config)
session.exitstatus = EXIT_OK
initstate = 0
try:
config.pluginmanager.do_configure(config)
initstate = 1
config.hook.pytest_sessionstart(session=session)
initstate = 2
doit(config, session)
config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)
except pytest.UsageError:
raise
except KeyboardInterrupt:
......@@ -79,24 +77,18 @@ def wrap_session(config, doit):
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
if not session.exitstatus and session._testsfailed:
session.exitstatus = EXIT_TESTSFAILED
if initstate >= 2:
config.hook.pytest_sessionfinish(session=session,
exitstatus=session.exitstatus)
if initstate >= 1:
config.pluginmanager.do_unconfigure(config)
config.hook.pytest_sessionfinish(session=session,
exitstatus=session.exitstatus)
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):
return session.perform_collect()
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
def pytest_runtestloop(session):
if session.config.option.collectonly:
......@@ -382,16 +374,6 @@ 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, EXIT_OK
from _pytest.main import Session
from py.builtin import print_
from _pytest.core import HookRelay
......@@ -292,19 +292,13 @@ class TmpTestdir:
assert '::' not in str(arg)
p = py.path.local(arg)
x = session.fspath.bestrelpath(p)
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
return session.perform_collect([x], genitems=False)[0]
def getpathnode(self, path):
config = self.parseconfigure(path)
config = self.parseconfig(path)
session = Session(config)
x = session.fspath.bestrelpath(path)
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
return session.perform_collect([x], genitems=False)[0]
def genitems(self, colitems):
session = colitems[0].session
......@@ -318,9 +312,7 @@ 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):
......@@ -390,8 +382,6 @@ 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,13 +226,8 @@ 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:
try:
mod = self.fspath.pyimport(ensuresyspath=True)
finally:
assertion.after_module_import(self)
mod = self.fspath.pyimport(ensuresyspath=True)
except SyntaxError:
excinfo = py.code.ExceptionInfo()
raise self.CollectError(excinfo.getrepr(style="short"))
......@@ -379,7 +374,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.session._setupstate.prepare(self)
self.config._setupstate.prepare(self)
# see FunctionMixin.setup and test_setupstate_is_preserved_134
self._preservedparent = self.parent.obj
l = []
......@@ -726,7 +721,7 @@ class FuncargRequest:
def _addfinalizer(self, finalizer, scope):
colitem = self._getscopeitem(scope)
self._pyfuncitem.session._setupstate.addfinalizer(
self.config._setupstate.addfinalizer(
finalizer=finalizer, colitem=colitem)
def __repr__(self):
......@@ -747,10 +742,8 @@ class FuncargRequest:
raise self.LookupError(msg)
def showfuncargs(config):
from _pytest.main import wrap_session
return wrap_session(config, _showfuncargs_main)
def _showfuncargs_main(config, session):
from _pytest.main import Session
session = Session(config)
session.perform_collect()
if session.items:
plugins = session.items[0].getplugins()
......
......@@ -14,15 +14,17 @@ def pytest_namespace():
#
# pytest plugin hooks
def pytest_sessionstart(session):
session._setupstate = SetupState()
# XXX move to pytest_sessionstart and fix py.test owns tests
def pytest_configure(config):
config._setupstate = SetupState()
def pytest_sessionfinish(session, exitstatus):
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
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, location):
......@@ -44,16 +46,16 @@ def runtestprotocol(item, log=True):
return reports
def pytest_runtest_setup(item):
item.session._setupstate.prepare(item)
item.config._setupstate.prepare(item)
def pytest_runtest_call(item):
item.runtest()
def pytest_runtest_teardown(item):
item.session._setupstate.teardown_exact(item)
item.config._setupstate.teardown_exact(item)
def pytest__teardown_final(session):
call = CallInfo(session._setupstate.teardown_all, when="teardown")
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()
......
......@@ -8,7 +8,7 @@ dictionary or an import path.
(c) Holger Krekel and others, 2004-2010
"""
__version__ = '1.4.4.dev1'
__version__ = '1.4.3'
from py import _apipkg
......@@ -70,6 +70,10 @@ _apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs={
'getrawcode' : '._code.code:getrawcode',
'patch_builtins' : '._code.code:patch_builtins',
'unpatch_builtins' : '._code.code:unpatch_builtins',
'_AssertionError' : '._code.assertion:AssertionError',
'_reinterpret_old' : '._code.assertion:reinterpret_old',
'_reinterpret' : '._code.assertion:reinterpret',
'_reprcompare' : '._code.assertion:_reprcompare',
},
# backports and additions of builtins
......
"""
Find intermediate evalutation results in assert statements through builtin AST.
This should replace oldinterpret.py eventually.
This should replace _assertionold.py eventually.
"""
import sys
import ast
import py
from _pytest.assertion import util
from _pytest.assertion.reinterpret import BuiltinAssertionError
from py._code.assertion import _format_explanation, BuiltinAssertionError
if sys.platform.startswith("java") and sys.version_info < (2, 5, 2):
......@@ -60,18 +59,21 @@ def run(offending_line, frame=None):
frame = py.code.Frame(sys._getframe(1))
return interpret(offending_line, frame)
def getfailure(e):
explanation = util.format_explanation(e.explanation)
value = e.cause[1]
def getfailure(failure):
explanation = _format_explanation(failure.explanation)
value = failure.cause[1]
if str(value):
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 '):
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 "):
text = text[16:]
return text
operator_map = {
ast.BitOr : "|",
ast.BitXor : "^",
......@@ -152,8 +154,8 @@ class DebugInterpreter(ast.NodeVisitor):
local = self.frame.eval(co)
except Exception:
# have to assume it isn't
local = None
if local is None or not self.frame.is_true(local):
local = False
if not local:
return name.id, result
return explanation, result
......@@ -173,7 +175,7 @@ class DebugInterpreter(ast.NodeVisitor):
except Exception:
raise Failure(explanation)
try:
if not self.frame.is_true(result):
if not result:
break
except KeyboardInterrupt:
raise
......@@ -181,8 +183,9 @@ class DebugInterpreter(ast.NodeVisitor):
break
left_explanation, left_result = next_explanation, next_result
if util._reprcompare is not None:
res = util._reprcompare(op_symbol, left_result, next_result)
rcomp = py.code._reprcompare
if rcomp:
res = rcomp(op_symbol, left_result, next_result)
if res:
explanation = res
return explanation, result
......@@ -299,8 +302,8 @@ class DebugInterpreter(ast.NodeVisitor):
try:
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
except Exception:
from_instance = None
if from_instance is None or self.frame.is_true(from_instance):
from_instance = True
if from_instance:
rep = self.frame.repr(result)
pattern = "%s\n{%s = %s\n}"
explanation = pattern % (rep, rep, explanation)
......@@ -308,8 +311,11 @@ 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 self.frame.is_true(test_result):
if not test_result:
try:
raise BuiltinAssertionError
except Exception:
......
import py
import sys, inspect
from compiler import parse, ast, pycodegen
from _pytest.assertion.util import format_explanation
from _pytest.assertion.reinterpret import BuiltinAssertionError
from py._code.assertion import BuiltinAssertionError, _format_explanation
passthroughex = py.builtin._sysex
......@@ -132,7 +131,7 @@ class Interpretable(View):
raise Failure(self)
def nice_explanation(self):
return format_explanation(self.explanation)
return _format_explanation(self.explanation)
class Name(Interpretable):
......@@ -384,6 +383,10 @@ 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,6 +3,52 @@ 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.