merge Benjamin's assertion-rewrite branch: all assertion related code is now part of py.test core distribution - the builtin assertion plugin to be precise.
See doc/assert.txt for details on how what has been improved.
This commit is contained in:
commit
5690beab5a
|
@ -1,6 +1,10 @@
|
|||
Changes between 2.0.3 and DEV
|
||||
Changes between 2.0.3 and 2.1.0.DEV
|
||||
----------------------------------------------
|
||||
|
||||
- merge Benjamin's assertionrewrite branch: now assertions
|
||||
for test modules on python 2.6 and above are done by rewriting
|
||||
the AST and saving the pyc file before the test module is imported.
|
||||
see doc/assert.txt for more info.
|
||||
- fix issue43: improve doctests with better traceback reporting on
|
||||
unexpected exceptions
|
||||
- fix issue47: timing output in junitxml for test cases is now correct
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
#
|
||||
__version__ = '2.1.0.dev1'
|
||||
__version__ = '2.1.0.dev2'
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
"""
|
||||
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(imp.cache_from_source(source_math))
|
||||
pyc.dirname.ensure(dir=True)
|
||||
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
|
|
@ -0,0 +1,339 @@
|
|||
"""
|
||||
Find intermediate evalutation results in assert statements through builtin AST.
|
||||
This should replace oldinterpret.py eventually.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ast
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
|
||||
if sys.platform.startswith("java") and sys.version_info < (2, 5, 2):
|
||||
# See http://bugs.jython.org/issue1497
|
||||
_exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict",
|
||||
"ListComp", "GeneratorExp", "Yield", "Compare", "Call",
|
||||
"Repr", "Num", "Str", "Attribute", "Subscript", "Name",
|
||||
"List", "Tuple")
|
||||
_stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign",
|
||||
"AugAssign", "Print", "For", "While", "If", "With", "Raise",
|
||||
"TryExcept", "TryFinally", "Assert", "Import", "ImportFrom",
|
||||
"Exec", "Global", "Expr", "Pass", "Break", "Continue")
|
||||
_expr_nodes = set(getattr(ast, name) for name in _exprs)
|
||||
_stmt_nodes = set(getattr(ast, name) for name in _stmts)
|
||||
def _is_ast_expr(node):
|
||||
return node.__class__ in _expr_nodes
|
||||
def _is_ast_stmt(node):
|
||||
return node.__class__ in _stmt_nodes
|
||||
else:
|
||||
def _is_ast_expr(node):
|
||||
return isinstance(node, ast.expr)
|
||||
def _is_ast_stmt(node):
|
||||
return isinstance(node, ast.stmt)
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
"""Error found while interpreting AST."""
|
||||
|
||||
def __init__(self, explanation=""):
|
||||
self.cause = sys.exc_info()
|
||||
self.explanation = explanation
|
||||
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
mod = ast.parse(source)
|
||||
visitor = DebugInterpreter(frame)
|
||||
try:
|
||||
visitor.visit(mod)
|
||||
except Failure:
|
||||
failure = sys.exc_info()[1]
|
||||
return getfailure(failure)
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --no-assert)")
|
||||
|
||||
def run(offending_line, frame=None):
|
||||
if frame is None:
|
||||
frame = py.code.Frame(sys._getframe(1))
|
||||
return interpret(offending_line, frame)
|
||||
|
||||
def getfailure(failure):
|
||||
explanation = util.format_explanation(failure.explanation)
|
||||
value = failure.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 "):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
|
||||
operator_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
|
||||
class DebugInterpreter(ast.NodeVisitor):
|
||||
"""Interpret AST nodes to gleam useful debugging information. """
|
||||
|
||||
def __init__(self, frame):
|
||||
self.frame = frame
|
||||
|
||||
def generic_visit(self, node):
|
||||
# Fallback when we don't have a special implementation.
|
||||
if _is_ast_expr(node):
|
||||
mod = ast.Expression(node)
|
||||
co = self._compile(mod)
|
||||
try:
|
||||
result = self.frame.eval(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
explanation = self.frame.repr(result)
|
||||
return explanation, result
|
||||
elif _is_ast_stmt(node):
|
||||
mod = ast.Module([node])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
return None, None
|
||||
else:
|
||||
raise AssertionError("can't handle %s" %(node,))
|
||||
|
||||
def _compile(self, source, mode="eval"):
|
||||
return compile(source, "<assertion interpretation>", mode)
|
||||
|
||||
def visit_Expr(self, expr):
|
||||
return self.visit(expr.value)
|
||||
|
||||
def visit_Module(self, mod):
|
||||
for stmt in mod.body:
|
||||
self.visit(stmt)
|
||||
|
||||
def visit_Name(self, name):
|
||||
explanation, result = self.generic_visit(name)
|
||||
# See if the name is local.
|
||||
source = "%r in locals() is not globals()" % (name.id,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
local = self.frame.eval(co)
|
||||
except Exception:
|
||||
# have to assume it isn't
|
||||
local = False
|
||||
if not local:
|
||||
return name.id, result
|
||||
return explanation, result
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
left = comp.left
|
||||
left_explanation, left_result = self.visit(left)
|
||||
for op, next_op in zip(comp.ops, comp.comparators):
|
||||
next_explanation, next_result = self.visit(next_op)
|
||||
op_symbol = operator_map[op.__class__]
|
||||
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
||||
next_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=next_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
try:
|
||||
if not result:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
break
|
||||
left_explanation, left_result = next_explanation, next_result
|
||||
|
||||
if util._reprcompare is not None:
|
||||
res = util._reprcompare(op_symbol, left_result, next_result)
|
||||
if res:
|
||||
explanation = res
|
||||
return explanation, result
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
is_or = isinstance(boolop.op, ast.Or)
|
||||
explanations = []
|
||||
for operand in boolop.values:
|
||||
explanation, result = self.visit(operand)
|
||||
explanations.append(explanation)
|
||||
if result == is_or:
|
||||
break
|
||||
name = is_or and " or " or " and "
|
||||
explanation = "(" + name.join(explanations) + ")"
|
||||
return explanation, result
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_explanation, operand_result = self.visit(unary.operand)
|
||||
explanation = pattern % (operand_explanation,)
|
||||
co = self._compile(pattern % ("__exprinfo_expr",))
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=operand_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
left_explanation, left_result = self.visit(binop.left)
|
||||
right_explanation, right_result = self.visit(binop.right)
|
||||
symbol = operator_map[binop.op.__class__]
|
||||
explanation = "(%s %s %s)" % (left_explanation, symbol,
|
||||
right_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=right_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Call(self, call):
|
||||
func_explanation, func = self.visit(call.func)
|
||||
arg_explanations = []
|
||||
ns = {"__exprinfo_func" : func}
|
||||
arguments = []
|
||||
for arg in call.args:
|
||||
arg_explanation, arg_result = self.visit(arg)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append(arg_name)
|
||||
arg_explanations.append(arg_explanation)
|
||||
for keyword in call.keywords:
|
||||
arg_explanation, arg_result = self.visit(keyword.value)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
keyword_source = "%s=%%s" % (keyword.arg)
|
||||
arguments.append(keyword_source % (arg_name,))
|
||||
arg_explanations.append(keyword_source % (arg_explanation,))
|
||||
if call.starargs:
|
||||
arg_explanation, arg_result = self.visit(call.starargs)
|
||||
arg_name = "__exprinfo_star"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("*%s" % (arg_name,))
|
||||
arg_explanations.append("*%s" % (arg_explanation,))
|
||||
if call.kwargs:
|
||||
arg_explanation, arg_result = self.visit(call.kwargs)
|
||||
arg_name = "__exprinfo_kwds"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("**%s" % (arg_name,))
|
||||
arg_explanations.append("**%s" % (arg_explanation,))
|
||||
args_explained = ", ".join(arg_explanations)
|
||||
explanation = "%s(%s)" % (func_explanation, args_explained)
|
||||
args = ", ".join(arguments)
|
||||
source = "__exprinfo_func(%s)" % (args,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, **ns)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
rep = self.frame.repr(result)
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def _is_builtin_name(self, name):
|
||||
pattern = "%r not in globals() and %r not in locals()"
|
||||
source = pattern % (name.id, name.id)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
return self.frame.eval(co)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
source_explanation, source_result = self.visit(attr.value)
|
||||
explanation = "%s.%s" % (source_explanation, attr.attr)
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result),
|
||||
self.frame.repr(result),
|
||||
source_explanation, attr.attr)
|
||||
# Check if the attr is from an instance.
|
||||
source = "%r in getattr(__exprinfo_expr, '__dict__', {})"
|
||||
source = source % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
from_instance = True
|
||||
if from_instance:
|
||||
rep = self.frame.repr(result)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
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:
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, test_result
|
||||
|
||||
def visit_Assign(self, assign):
|
||||
value_explanation, value_result = self.visit(assign.value)
|
||||
explanation = "... = %s" % (value_explanation,)
|
||||
name = ast.Name("__exprinfo_expr", ast.Load(),
|
||||
lineno=assign.value.lineno,
|
||||
col_offset=assign.value.col_offset)
|
||||
new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno,
|
||||
col_offset=assign.col_offset)
|
||||
mod = ast.Module([new_assign])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co, __exprinfo_expr=value_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, value_result
|
|
@ -0,0 +1,556 @@
|
|||
import py
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
from _pytest.assertion.util import format_explanation
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
passthroughex = py.builtin._sysex
|
||||
|
||||
class Failure:
|
||||
def __init__(self, node):
|
||||
self.exc, self.value, self.tb = sys.exc_info()
|
||||
self.node = node
|
||||
|
||||
class View(object):
|
||||
"""View base class.
|
||||
|
||||
If C is a subclass of View, then C(x) creates a proxy object around
|
||||
the object x. The actual class of the proxy is not C in general,
|
||||
but a *subclass* of C determined by the rules below. To avoid confusion
|
||||
we call view class the class of the proxy (a subclass of C, so of View)
|
||||
and object class the class of x.
|
||||
|
||||
Attributes and methods not found in the proxy are automatically read on x.
|
||||
Other operations like setting attributes are performed on the proxy, as
|
||||
determined by its view class. The object x is available from the proxy
|
||||
as its __obj__ attribute.
|
||||
|
||||
The view class selection is determined by the __view__ tuples and the
|
||||
optional __viewkey__ method. By default, the selected view class is the
|
||||
most specific subclass of C whose __view__ mentions the class of x.
|
||||
If no such subclass is found, the search proceeds with the parent
|
||||
object classes. For example, C(True) will first look for a subclass
|
||||
of C with __view__ = (..., bool, ...) and only if it doesn't find any
|
||||
look for one with __view__ = (..., int, ...), and then ..., object,...
|
||||
If everything fails the class C itself is considered to be the default.
|
||||
|
||||
Alternatively, the view class selection can be driven by another aspect
|
||||
of the object x, instead of the class of x, by overriding __viewkey__.
|
||||
See last example at the end of this module.
|
||||
"""
|
||||
|
||||
_viewcache = {}
|
||||
__view__ = ()
|
||||
|
||||
def __new__(rootclass, obj, *args, **kwds):
|
||||
self = object.__new__(rootclass)
|
||||
self.__obj__ = obj
|
||||
self.__rootclass__ = rootclass
|
||||
key = self.__viewkey__()
|
||||
try:
|
||||
self.__class__ = self._viewcache[key]
|
||||
except KeyError:
|
||||
self.__class__ = self._selectsubclass(key)
|
||||
return self
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# attributes not found in the normal hierarchy rooted on View
|
||||
# are looked up in the object's real class
|
||||
return getattr(self.__obj__, attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
|
||||
def __matchkey__(self, key, subclasses):
|
||||
if inspect.isclass(key):
|
||||
keys = inspect.getmro(key)
|
||||
else:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
result = [C for C in subclasses if key in C.__view__]
|
||||
if result:
|
||||
return result
|
||||
return []
|
||||
|
||||
def _selectsubclass(self, key):
|
||||
subclasses = list(enumsubclasses(self.__rootclass__))
|
||||
for C in subclasses:
|
||||
if not isinstance(C.__view__, tuple):
|
||||
C.__view__ = (C.__view__,)
|
||||
choices = self.__matchkey__(key, subclasses)
|
||||
if not choices:
|
||||
return self.__rootclass__
|
||||
elif len(choices) == 1:
|
||||
return choices[0]
|
||||
else:
|
||||
# combine the multiple choices
|
||||
return type('?', tuple(choices), {})
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__)
|
||||
|
||||
|
||||
def enumsubclasses(cls):
|
||||
for subcls in cls.__subclasses__():
|
||||
for subsubclass in enumsubclasses(subcls):
|
||||
yield subsubclass
|
||||
yield cls
|
||||
|
||||
|
||||
class Interpretable(View):
|
||||
"""A parse tree node with a few extra methods."""
|
||||
explanation = None
|
||||
|
||||
def is_builtin(self, frame):
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
# fall-back for unknown expression nodes
|
||||
try:
|
||||
expr = ast.Expression(self.__obj__)
|
||||
expr.filename = '<eval>'
|
||||
self.__obj__.filename = '<eval>'
|
||||
co = pycodegen.ExpressionCodeGenerator(expr).getCode()
|
||||
result = frame.eval(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.result = result
|
||||
self.explanation = self.explanation or frame.repr(self.result)
|
||||
|
||||
def run(self, frame):
|
||||
# fall-back for unknown statement nodes
|
||||
try:
|
||||
expr = ast.Module(None, ast.Stmt([self.__obj__]))
|
||||
expr.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(expr).getCode()
|
||||
frame.exec_(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
def nice_explanation(self):
|
||||
return format_explanation(self.explanation)
|
||||
|
||||
|
||||
class Name(Interpretable):
|
||||
__view__ = ast.Name
|
||||
|
||||
def is_local(self, frame):
|
||||
source = '%r in locals() is not globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_global(self, frame):
|
||||
source = '%r in globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_builtin(self, frame):
|
||||
source = '%r not in locals() and %r not in globals()' % (
|
||||
self.name, self.name)
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
super(Name, self).eval(frame)
|
||||
if not self.is_local(frame):
|
||||
self.explanation = self.name
|
||||
|
||||
class Compare(Interpretable):
|
||||
__view__ = ast.Compare
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
for operation, expr2 in self.ops:
|
||||
if hasattr(self, 'result'):
|
||||
# shortcutting in chained expressions
|
||||
if not frame.is_true(self.result):
|
||||
break
|
||||
expr2 = Interpretable(expr2)
|
||||
expr2.eval(frame)
|
||||
self.explanation = "%s %s %s" % (
|
||||
expr.explanation, operation, expr2.explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % operation
|
||||
try:
|
||||
self.result = frame.eval(source,
|
||||
__exprinfo_left=expr.result,
|
||||
__exprinfo_right=expr2.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
expr = expr2
|
||||
|
||||
class And(Interpretable):
|
||||
__view__ = ast.And
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if not frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' and '.join(explanations) + ')'
|
||||
|
||||
class Or(Interpretable):
|
||||
__view__ = ast.Or
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' or '.join(explanations) + ')'
|
||||
|
||||
|
||||
# == Unary operations ==
|
||||
keepalive = []
|
||||
for astclass, astpattern in {
|
||||
ast.Not : 'not __exprinfo_expr',
|
||||
ast.Invert : '(~__exprinfo_expr)',
|
||||
}.items():
|
||||
|
||||
class UnaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.explanation = astpattern.replace('__exprinfo_expr',
|
||||
expr.explanation)
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(UnaryArith)
|
||||
|
||||
# == Binary operations ==
|
||||
for astclass, astpattern in {
|
||||
ast.Add : '(__exprinfo_left + __exprinfo_right)',
|
||||
ast.Sub : '(__exprinfo_left - __exprinfo_right)',
|
||||
ast.Mul : '(__exprinfo_left * __exprinfo_right)',
|
||||
ast.Div : '(__exprinfo_left / __exprinfo_right)',
|
||||
ast.Mod : '(__exprinfo_left % __exprinfo_right)',
|
||||
ast.Power : '(__exprinfo_left ** __exprinfo_right)',
|
||||
}.items():
|
||||
|
||||
class BinaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
left = Interpretable(self.left)
|
||||
left.eval(frame)
|
||||
right = Interpretable(self.right)
|
||||
right.eval(frame)
|
||||
self.explanation = (astpattern
|
||||
.replace('__exprinfo_left', left .explanation)
|
||||
.replace('__exprinfo_right', right.explanation))
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_left=left.result,
|
||||
__exprinfo_right=right.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(BinaryArith)
|
||||
|
||||
|
||||
class CallFunc(Interpretable):
|
||||
__view__ = ast.CallFunc
|
||||
|
||||
def is_bool(self, frame):
|
||||
source = 'isinstance(__exprinfo_value, bool)'
|
||||
try:
|
||||
return frame.is_true(frame.eval(source,
|
||||
__exprinfo_value=self.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
node = Interpretable(self.node)
|
||||
node.eval(frame)
|
||||
explanations = []
|
||||
vars = {'__exprinfo_fn': node.result}
|
||||
source = '__exprinfo_fn('
|
||||
for a in self.args:
|
||||
if isinstance(a, ast.Keyword):
|
||||
keyword = a.name
|
||||
a = a.expr
|
||||
else:
|
||||
keyword = None
|
||||
a = Interpretable(a)
|
||||
a.eval(frame)
|
||||
argname = '__exprinfo_%d' % len(vars)
|
||||
vars[argname] = a.result
|
||||
if keyword is None:
|
||||
source += argname + ','
|
||||
explanations.append(a.explanation)
|
||||
else:
|
||||
source += '%s=%s,' % (keyword, argname)
|
||||
explanations.append('%s=%s' % (keyword, a.explanation))
|
||||
if self.star_args:
|
||||
star_args = Interpretable(self.star_args)
|
||||
star_args.eval(frame)
|
||||
argname = '__exprinfo_star'
|
||||
vars[argname] = star_args.result
|
||||
source += '*' + argname + ','
|
||||
explanations.append('*' + star_args.explanation)
|
||||
if self.dstar_args:
|
||||
dstar_args = Interpretable(self.dstar_args)
|
||||
dstar_args.eval(frame)
|
||||
argname = '__exprinfo_kwds'
|
||||
vars[argname] = dstar_args.result
|
||||
source += '**' + argname + ','
|
||||
explanations.append('**' + dstar_args.explanation)
|
||||
self.explanation = "%s(%s)" % (
|
||||
node.explanation, ', '.join(explanations))
|
||||
if source.endswith(','):
|
||||
source = source[:-1]
|
||||
source += ')'
|
||||
try:
|
||||
self.result = frame.eval(source, **vars)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
if not node.is_builtin(frame) or not self.is_bool(frame):
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
class Getattr(Interpretable):
|
||||
__view__ = ast.Getattr
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.explanation = '%s.%s' % (expr.explanation, self.attrname)
|
||||
# if the attribute comes from the instance, its value is interesting
|
||||
source = ('hasattr(__exprinfo_expr, "__dict__") and '
|
||||
'%r in __exprinfo_expr.__dict__' % self.attrname)
|
||||
try:
|
||||
from_instance = frame.is_true(
|
||||
frame.eval(source, __exprinfo_expr=expr.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
from_instance = True
|
||||
if from_instance:
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
# == Re-interpretation of full statements ==
|
||||
|
||||
class Assert(Interpretable):
|
||||
__view__ = ast.Assert
|
||||
|
||||
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
|
||||
if not frame.is_true(test.result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Assign(Interpretable):
|
||||
__view__ = ast.Assign
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = '... = ' + expr.explanation
|
||||
# fall-back-run the rest of the assignment
|
||||
ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr'))
|
||||
mod = ast.Module(None, ast.Stmt([ass]))
|
||||
mod.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(mod).getCode()
|
||||
try:
|
||||
frame.exec_(co, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Discard(Interpretable):
|
||||
__view__ = ast.Discard
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = expr.explanation
|
||||
|
||||
class Stmt(Interpretable):
|
||||
__view__ = ast.Stmt
|
||||
|
||||
def run(self, frame):
|
||||
for stmt in self.nodes:
|
||||
stmt = Interpretable(stmt)
|
||||
stmt.run(frame)
|
||||
|
||||
|
||||
def report_failure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if explanation:
|
||||
explanation = ", in: " + explanation
|
||||
else:
|
||||
explanation = ""
|
||||
sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation))
|
||||
|
||||
def check(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
expr = parse(s, 'eval')
|
||||
assert isinstance(expr, ast.Expression)
|
||||
node = Interpretable(expr.node)
|
||||
try:
|
||||
node.eval(frame)
|
||||
except passthroughex:
|
||||
raise
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
else:
|
||||
if not frame.is_true(node.result):
|
||||
sys.stderr.write("assertion failed: %s\n" % node.nice_explanation())
|
||||
|
||||
|
||||
###########################################################
|
||||
# API / Entry points
|
||||
# #########################################################
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
module = Interpretable(parse(source, 'exec').node)
|
||||
#print "got module", module
|
||||
if isinstance(frame, py.std.types.FrameType):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
return getfailure(e)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --nomagic)")
|
||||
else:
|
||||
return None
|
||||
|
||||
def getmsg(excinfo):
|
||||
if isinstance(excinfo, tuple):
|
||||
excinfo = py.code.ExceptionInfo(excinfo)
|
||||
#frame, line = gettbline(tb)
|
||||
#frame = py.code.Frame(frame)
|
||||
#return interpret(line, frame)
|
||||
|
||||
tb = excinfo.traceback[-1]
|
||||
source = str(tb.statement).strip()
|
||||
x = interpret(source, tb.frame, should_fail=True)
|
||||
if not isinstance(x, str):
|
||||
raise TypeError("interpret returned non-string %r" % (x,))
|
||||
return x
|
||||
|
||||
def getfailure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if str(e.value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (e.value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.exc.__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
def run(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
module = Interpretable(parse(s, 'exec').node)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# example:
|
||||
def f():
|
||||
return 5
|
||||
def g():
|
||||
return 3
|
||||
def h(x):
|
||||
return 'never'
|
||||
check("f() * g() == 5")
|
||||
check("not f()")
|
||||
check("not (f() and g() or 0)")
|
||||
check("f() == g()")
|
||||
i = 4
|
||||
check("i == f()")
|
||||
check("len(f()) == 0")
|
||||
check("isinstance(2+3+4, float)")
|
||||
|
||||
run("x = i")
|
||||
check("x == 5")
|
||||
|
||||
run("assert not f(), 'oops'")
|
||||
run("a, b, c = 1, 2")
|
||||
run("a, b, c = f()")
|
||||
|
||||
check("max([f(),g()]) == 4")
|
||||
check("'hello'[g()] == 'h'")
|
||||
run("'guk%d' % h(f())")
|
|
@ -0,0 +1,48 @@
|
|||
import sys
|
||||
import py
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
|
||||
class AssertionError(BuiltinAssertionError):
|
||||
def __init__(self, *args):
|
||||
BuiltinAssertionError.__init__(self, *args)
|
||||
if args:
|
||||
try:
|
||||
self.msg = str(args[0])
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
self.msg = "<[broken __repr__] %s at %0xd>" %(
|
||||
args[0].__class__, id(args[0]))
|
||||
else:
|
||||
f = py.code.Frame(sys._getframe(1))
|
||||
try:
|
||||
source = f.code.fullsource
|
||||
if source is not None:
|
||||
try:
|
||||
source = source.getstatement(f.lineno, assertion=True)
|
||||
except IndexError:
|
||||
source = None
|
||||
else:
|
||||
source = str(source.deindent()).strip()
|
||||
except py.error.ENOENT:
|
||||
source = None
|
||||
# this can also occur during reinterpretation, when the
|
||||
# co_filename is set to "<run>".
|
||||
if source:
|
||||
self.msg = reinterpret(source, f, should_fail=True)
|
||||
else:
|
||||
self.msg = "<could not determine information>"
|
||||
if not self.args:
|
||||
self.args = (self.msg,)
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
reinterpret_old = "old reinterpretation not available for py3"
|
||||
else:
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret_old
|
||||
if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
reinterpret = reinterpret_old
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
|
||||
import ast
|
||||
import collections
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
"""Rewrite the assert statements in mod."""
|
||||
AssertionRewriter().run(mod)
|
||||
|
||||
|
||||
_saferepr = py.io.saferepr
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation
|
||||
|
||||
def _format_boolop(operands, explanations, is_or):
|
||||
show_explanations = []
|
||||
for operand, expl in zip(operands, explanations):
|
||||
show_explanations.append(expl)
|
||||
if operand == is_or:
|
||||
break
|
||||
return "(" + (is_or and " or " or " and ").join(show_explanations) + ")"
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
try:
|
||||
done = not res
|
||||
except Exception:
|
||||
done = True
|
||||
if done:
|
||||
break
|
||||
if util._reprcompare is not None:
|
||||
custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
|
||||
if custom is not None:
|
||||
return custom
|
||||
return expl
|
||||
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
binop_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
|
||||
def set_location(node, lineno, col_offset):
|
||||
"""Set node location information recursively."""
|
||||
def _fix(node, lineno, col_offset):
|
||||
if "lineno" in node._attributes:
|
||||
node.lineno = lineno
|
||||
if "col_offset" in node._attributes:
|
||||
node.col_offset = col_offset
|
||||
for child in ast.iter_child_nodes(node):
|
||||
_fix(child, lineno, col_offset)
|
||||
_fix(node, lineno, col_offset)
|
||||
return node
|
||||
|
||||
|
||||
class AssertionRewriter(ast.NodeVisitor):
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
if not mod.body:
|
||||
# Nothing to do.
|
||||
return
|
||||
# Insert some special imports at the top of the module but after any
|
||||
# docstrings and __future__ imports.
|
||||
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
|
||||
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
|
||||
expect_docstring = True
|
||||
pos = 0
|
||||
lineno = 0
|
||||
for item in mod.body:
|
||||
if (expect_docstring and isinstance(item, ast.Expr) and
|
||||
isinstance(item.value, ast.Str)):
|
||||
doc = item.value.s
|
||||
if "PYTEST_DONT_REWRITE" in doc:
|
||||
# The module has disabled assertion rewriting.
|
||||
return
|
||||
lineno += len(doc) - 1
|
||||
expect_docstring = False
|
||||
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and
|
||||
item.identifier != "__future__"):
|
||||
lineno = item.lineno
|
||||
break
|
||||
pos += 1
|
||||
imports = [ast.Import([alias], lineno=lineno, col_offset=0)
|
||||
for alias in aliases]
|
||||
mod.body[pos:pos] = imports
|
||||
# Collect asserts.
|
||||
nodes = collections.deque([mod])
|
||||
while nodes:
|
||||
node = nodes.popleft()
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new = []
|
||||
for i, child in enumerate(field):
|
||||
if isinstance(child, ast.Assert):
|
||||
# Transform assert.
|
||||
new.extend(self.visit(child))
|
||||
else:
|
||||
new.append(child)
|
||||
if isinstance(child, ast.AST):
|
||||
nodes.append(child)
|
||||
setattr(node, name, new)
|
||||
elif (isinstance(field, ast.AST) and
|
||||
# Don't recurse into expressions as they can't contain
|
||||
# asserts.
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def variable(self):
|
||||
"""Get a new variable."""
|
||||
# Use a character invalid in python identifiers to avoid clashing.
|
||||
name = "@py_assert" + str(next(self.variable_counter))
|
||||
self.variables.add(name)
|
||||
return name
|
||||
|
||||
def assign(self, expr):
|
||||
"""Give *expr* a name."""
|
||||
name = self.variable()
|
||||
self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def display(self, expr):
|
||||
"""Call py.io.saferepr on the expression."""
|
||||
return self.helper("saferepr", expr)
|
||||
|
||||
def helper(self, name, *args):
|
||||
"""Call a helper in this module."""
|
||||
py_name = ast.Name("@pytest_ar", ast.Load())
|
||||
attr = ast.Attribute(py_name, "_" + name, ast.Load())
|
||||
return ast.Call(attr, list(args), [], None, None)
|
||||
|
||||
def builtin(self, name):
|
||||
"""Return the builtin called *name*."""
|
||||
builtin_name = ast.Name("@py_builtins", ast.Load())
|
||||
return ast.Attribute(builtin_name, name, ast.Load())
|
||||
|
||||
def explanation_param(self, expr):
|
||||
specifier = "py" + str(next(self.variable_counter))
|
||||
self.explanation_specifiers[specifier] = expr
|
||||
return "%(" + specifier + ")s"
|
||||
|
||||
def push_format_context(self):
|
||||
self.explanation_specifiers = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def generic_visit(self, node):
|
||||
"""Handle expressions we don't have custom code for."""
|
||||
assert isinstance(node, ast.expr)
|
||||
res = self.assign(node)
|
||||
return res, self.explanation_param(self.display(res))
|
||||
|
||||
def visit_Assert(self, assert_):
|
||||
if assert_.msg:
|
||||
# There's already a message. Don't mess with it.
|
||||
return [assert_]
|
||||
self.statements = []
|
||||
self.variables = set()
|
||||
self.variable_counter = itertools.count()
|
||||
self.stack = []
|
||||
self.on_failure = []
|
||||
self.push_format_context()
|
||||
# Rewrite assert into a bunch of statements.
|
||||
top_condition, explanation = self.visit(assert_.test)
|
||||
# Create failure message.
|
||||
body = self.on_failure
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
self.statements.append(ast.If(negation, body, []))
|
||||
explanation = "assert " + explanation
|
||||
template = ast.Str(explanation)
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
exc = ast.Call(err_name, [fmt], [], None, None)
|
||||
if sys.version_info[0] >= 3:
|
||||
raise_ = ast.Raise(exc, None)
|
||||
else:
|
||||
raise_ = ast.Raise(exc, None, None)
|
||||
body.append(raise_)
|
||||
# Delete temporary variables.
|
||||
names = [ast.Name(name, ast.Del()) for name in self.variables]
|
||||
if names:
|
||||
delete = ast.Delete(names)
|
||||
self.statements.append(delete)
|
||||
# Fix line numbers.
|
||||
for stmt in self.statements:
|
||||
set_location(stmt, assert_.lineno, assert_.col_offset)
|
||||
return self.statements
|
||||
|
||||
def visit_Name(self, name):
|
||||
# Check if the name is local or not.
|
||||
locs = ast.Call(self.builtin("locals"), [], [], None, None)
|
||||
globs = ast.Call(self.builtin("globals"), [], [], None, None)
|
||||
ops = [ast.In(), ast.IsNot()]
|
||||
test = ast.Compare(ast.Str(name.id), ops, [locs, globs])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
operands = []
|
||||
explanations = []
|
||||
self.push_format_context()
|
||||
for operand in boolop.values:
|
||||
res, explanation = self.visit(operand)
|
||||
operands.append(res)
|
||||
explanations.append(explanation)
|
||||
expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load())
|
||||
is_or = ast.Num(isinstance(boolop.op, ast.Or))
|
||||
expl_template = self.helper("format_boolop",
|
||||
ast.Tuple(operands, ast.Load()), expls,
|
||||
is_or)
|
||||
expl = self.pop_format_context(expl_template)
|
||||
res = self.assign(ast.BoolOp(boolop.op, operands))
|
||||
return res, self.explanation_param(expl)
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_res, operand_expl = self.visit(unary.operand)
|
||||
res = self.assign(ast.UnaryOp(unary.op, operand_res))
|
||||
return res, pattern % (operand_expl,)
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
symbol = binop_map[binop.op.__class__]
|
||||
left_expr, left_expl = self.visit(binop.left)
|
||||
right_expr, right_expl = self.visit(binop.right)
|
||||
explanation = "(%s %s %s)" % (left_expl, symbol, right_expl)
|
||||
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
|
||||
return res, explanation
|
||||
|
||||
def visit_Call(self, call):
|
||||
new_func, func_expl = self.visit(call.func)
|
||||
arg_expls = []
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
new_star = new_kwarg = None
|
||||
for arg in call.args:
|
||||
res, expl = self.visit(arg)
|
||||
new_args.append(res)
|
||||
arg_expls.append(expl)
|
||||
for keyword in call.keywords:
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
arg_expls.append(keyword.arg + "=" + expl)
|
||||
if call.starargs:
|
||||
new_star, expl = self.visit(call.starargs)
|
||||
arg_expls.append("*" + expl)
|
||||
if call.kwargs:
|
||||
new_kwarg, expl = self.visit(call.kwarg)
|
||||
arg_expls.append("**" + expl)
|
||||
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
|
||||
new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg)
|
||||
res = self.assign(new_call)
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
||||
return res, outer_expl
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
value, value_expl = self.visit(attr.value)
|
||||
res = self.assign(ast.Attribute(value, attr.attr, ast.Load()))
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
pat = "%s\n{%s = %s.%s\n}"
|
||||
expl = pat % (res_expl, res_expl, value_expl, attr.attr)
|
||||
return res, expl
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
|
||||
expls = []
|
||||
syms = []
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
expl = "%s %s %s" % (left_expl, sym, next_expl)
|
||||
expls.append(ast.Str(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
# Use py.code._reprcompare if that's available.
|
||||
expl_call = self.helper("call_reprcompare",
|
||||
ast.Tuple(syms, ast.Load()),
|
||||
ast.Tuple(load_names, ast.Load()),
|
||||
ast.Tuple(expls, ast.Load()),
|
||||
ast.Tuple(results, ast.Load()))
|
||||
if len(comp.ops) > 1:
|
||||
res = ast.BoolOp(ast.And(), load_names)
|
||||
else:
|
||||
res = load_names[0]
|
||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
|
@ -1,43 +1,57 @@
|
|||
"""
|
||||
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)
|
||||
# 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 warn_about_missing_assertion():
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
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:
|
||||
sys.stderr.write("WARNING: failing tests may report as passing because "
|
||||
"assertions are turned off! (are you using python -O?)\n")
|
||||
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 +60,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))
|
|
@ -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.")
|
||||
|
|
|
@ -46,23 +46,22 @@ 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
|
||||
try:
|
||||
config.pluginmanager.do_configure(config)
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
config.hook.pytest_collection(session=session)
|
||||
config.hook.pytest_runtestloop(session=session)
|
||||
doit(config, session)
|
||||
except pytest.UsageError:
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
|
@ -82,13 +81,17 @@ def pytest_cmdline_main(config):
|
|||
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 +377,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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@ import inspect
|
|||
import sys
|
||||
import pytest
|
||||
from py._code.code import TerminalRepr
|
||||
from _pytest import assertion
|
||||
|
||||
import _pytest
|
||||
cutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
@ -226,8 +227,12 @@ class Module(pytest.File, PyCollectorMixin):
|
|||
|
||||
def _importtestmodule(self):
|
||||
# we assume we are only called once per module
|
||||
assertion.before_module_import(self)
|
||||
try:
|
||||
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 = []
|
||||
|
@ -726,7 +731,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):
|
||||
|
@ -747,8 +752,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,12 +14,10 @@ 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:
|
||||
|
@ -46,16 +44,16 @@ def runtestprotocol(item, log=True):
|
|||
return reports
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item.config._setupstate.prepare(item)
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
item.runtest()
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
item.config._setupstate.teardown_exact(item)
|
||||
item.session._setupstate.teardown_exact(item)
|
||||
|
||||
def pytest__teardown_final(session):
|
||||
call = CallInfo(session.config._setupstate.teardown_all, when="teardown")
|
||||
call = CallInfo(session._setupstate.teardown_all, when="teardown")
|
||||
if call.excinfo:
|
||||
ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
|
||||
call.excinfo.traceback = ntraceback.filter()
|
||||
|
|
|
@ -18,8 +18,8 @@ following::
|
|||
def test_function():
|
||||
assert f() == 4
|
||||
|
||||
to assert that your object returns a certain value. If this
|
||||
assertion fails you will see the value of ``x``::
|
||||
to assert that your function returns a certain value. If this assertion fails
|
||||
you will see the value of ``x``::
|
||||
|
||||
$ py.test test_assert1.py
|
||||
=========================== test session starts ============================
|
||||
|
@ -39,22 +39,12 @@ assertion fails you will see the value of ``x``::
|
|||
test_assert1.py:5: AssertionError
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
|
||||
Reporting details about the failing assertion is achieved by re-evaluating
|
||||
the assert expression and recording the intermediate values.
|
||||
py.test has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary operators. This
|
||||
allows you to use the idiomatic python constructs without boilerplate code while
|
||||
not losing introspection information.
|
||||
|
||||
Note: If evaluating the assert expression has side effects you may get a
|
||||
warning that the intermediate values could not be determined safely. A
|
||||
common example of this issue is an assertion which reads from a file::
|
||||
|
||||
assert f.read() != '...'
|
||||
|
||||
If this assertion fails then the re-evaluation will probably succeed!
|
||||
This is because ``f.read()`` will return an empty string when it is
|
||||
called the second time during the re-evaluation. However, it is
|
||||
easy to rewrite the assertion and avoid any trouble::
|
||||
|
||||
content = f.read()
|
||||
assert content != '...'
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
|
||||
assertions about expected exceptions
|
||||
|
@ -137,6 +127,66 @@ Special comparisons are done for a number of cases:
|
|||
|
||||
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
|
||||
|
||||
|
||||
.. _assert-details:
|
||||
|
||||
Assertion introspection details
|
||||
-------------------------------
|
||||
|
||||
Reporting details about the failing assertion is achieved either by rewriting
|
||||
assert statements before they are run or re-evaluating the assert expression and
|
||||
recording the intermediate values. Which technique is used depends on the
|
||||
location of the assert, py.test's configuration, and Python version being used
|
||||
to run py.test.
|
||||
|
||||
By default, if the Python version is greater than or equal to 2.6, py.test
|
||||
rewrites assert statements in test modules. Rewritten assert statements put
|
||||
introspection information into the assertion failure message. Note py.test only
|
||||
rewrites test modules directly discovered by its test collection process, so
|
||||
asserts in supporting modules will not be rewritten.
|
||||
|
||||
.. note::
|
||||
|
||||
py.test rewrites test modules as it collects tests from them. It does this by
|
||||
writing a new pyc file which Python loads when the test module is
|
||||
imported. If the module has already been loaded (it is in sys.modules),
|
||||
though, Python will not load the rewritten module. This means if a test
|
||||
module imports another test module which has not already been rewritten, then
|
||||
py.test will not be able to rewrite the second module.
|
||||
|
||||
If an assert statement has not been rewritten or the Python version is less than
|
||||
2.6, py.test falls back on assert reinterpretation. In assert reinterpretation,
|
||||
py.test walks the frame of the function containing the assert statement to
|
||||
discover sub-expression results of the failing assert statement. You can force
|
||||
py.test to always use assertion reinterpretation by passing the
|
||||
``--assertmode=old`` option.
|
||||
|
||||
Assert reinterpretation has a caveat not present with assert rewriting: If
|
||||
evaluating the assert expression has side effects you may get a warning that the
|
||||
intermediate values could not be determined safely. A common example of this
|
||||
issue is an assertion which reads from a file::
|
||||
|
||||
assert f.read() != '...'
|
||||
|
||||
If this assertion fails then the re-evaluation will probably succeed!
|
||||
This is because ``f.read()`` will return an empty string when it is
|
||||
called the second time during the re-evaluation. However, it is
|
||||
easy to rewrite the assertion and avoid any trouble::
|
||||
|
||||
content = f.read()
|
||||
assert content != '...'
|
||||
|
||||
All assert introspection can be turned off by passing ``--assertmode=off``.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Add assert rewriting as an alternate introspection technique.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
Introduce the ``--assertmode`` option. Deprecate ``--no-assert`` and
|
||||
``--nomagic``.
|
||||
|
||||
..
|
||||
Defining your own comparison
|
||||
----------------------------------------------
|
||||
|
|
18
doc/faq.txt
18
doc/faq.txt
|
@ -47,13 +47,17 @@ customizable testing frameworks for Python. However,
|
|||
``py.test`` still uses many metaprogramming techniques and
|
||||
reading its source is thus likely not something for Python beginners.
|
||||
|
||||
A second "magic" issue arguably the assert statement re-intepreation:
|
||||
When an ``assert`` statement fails, py.test re-interprets the expression
|
||||
to show intermediate values if a test fails. If your expression
|
||||
has side effects (better to avoid them anyway!) the intermediate values
|
||||
may not be the same, obfuscating the initial error (this is also
|
||||
explained at the command line if it happens).
|
||||
``py.test --no-assert`` turns off assert re-interpretation.
|
||||
A second "magic" issue arguably the assert statement debugging feature. When
|
||||
loading test modules py.test rewrites the source code of assert statements. When
|
||||
a rewritten assert statement fails, its error message has more information than
|
||||
the original. py.test also has a second assert debugging technique. When an
|
||||
``assert`` statement that was missed by the rewriter fails, py.test
|
||||
re-interprets the expression to show intermediate values if a test fails. This
|
||||
second technique suffers from caveat that the rewriting does not: If your
|
||||
expression has side effects (better to avoid them anyway!) the intermediate
|
||||
values may not be the same, confusing the reinterpreter and obfuscating the
|
||||
initial error (this is also explained at the command line if it happens).
|
||||
You can turn off all assertion debugging with ``py.test --assertmode=off``.
|
||||
|
||||
.. _`py namespaces`: index.html
|
||||
.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py
|
||||
|
|
4
setup.py
4
setup.py
|
@ -22,14 +22,14 @@ def main():
|
|||
name='pytest',
|
||||
description='py.test: simple powerful testing with Python',
|
||||
long_description = long_description,
|
||||
version='2.1.0.dev1',
|
||||
version='2.1.0.dev2',
|
||||
url='http://pytest.org',
|
||||
license='MIT license',
|
||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||
author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others',
|
||||
author_email='holger at merlinux.eu',
|
||||
entry_points= make_entry_points(),
|
||||
install_requires=['py>1.4.1'],
|
||||
install_requires=['py>1.4.3'],
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
"PYTEST_DONT_REWRITE"
|
||||
import pytest, py
|
||||
|
||||
from _pytest.assertion import util
|
||||
|
||||
def exvalue():
|
||||
return py.std.sys.exc_info()[1]
|
||||
|
||||
def f():
|
||||
return 2
|
||||
|
||||
def test_not_being_rewritten():
|
||||
assert "@py_builtins" not in globals()
|
||||
|
||||
def test_assert():
|
||||
try:
|
||||
assert f() == 3
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 == 3\n')
|
||||
|
||||
def test_assert_with_explicit_message():
|
||||
try:
|
||||
assert f() == 3, "hello"
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg == 'hello'
|
||||
|
||||
def test_assert_within_finally():
|
||||
class A:
|
||||
def f():
|
||||
pass
|
||||
excinfo = py.test.raises(TypeError, """
|
||||
try:
|
||||
A().f()
|
||||
finally:
|
||||
i = 42
|
||||
""")
|
||||
s = excinfo.exconly()
|
||||
assert s.find("takes no argument") != -1
|
||||
|
||||
#def g():
|
||||
# A.f()
|
||||
#excinfo = getexcinfo(TypeError, g)
|
||||
#msg = getmsg(excinfo)
|
||||
#assert msg.find("must be called with A") != -1
|
||||
|
||||
|
||||
def test_assert_multiline_1():
|
||||
try:
|
||||
assert (f() ==
|
||||
3)
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 == 3\n')
|
||||
|
||||
def test_assert_multiline_2():
|
||||
try:
|
||||
assert (f() == (4,
|
||||
3)[-1])
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 ==')
|
||||
|
||||
def test_in():
|
||||
try:
|
||||
assert "hi" in [1, 2]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 'hi' in")
|
||||
|
||||
def test_is():
|
||||
try:
|
||||
assert 1 is 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 is 2")
|
||||
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_attrib():
|
||||
class Foo(object):
|
||||
b = 1
|
||||
i = Foo()
|
||||
try:
|
||||
assert i.b == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 == 2")
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_attrib_inst():
|
||||
class Foo(object):
|
||||
b = 1
|
||||
try:
|
||||
assert Foo().b == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 == 2")
|
||||
|
||||
def test_len():
|
||||
l = list(range(42))
|
||||
try:
|
||||
assert len(l) == 100
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 42 == 100")
|
||||
assert "where 42 = len([" in s
|
||||
|
||||
def test_assert_non_string_message():
|
||||
class A:
|
||||
def __str__(self):
|
||||
return "hello"
|
||||
try:
|
||||
assert 0 == 1, A()
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg == "hello"
|
||||
|
||||
def test_assert_keyword_arg():
|
||||
def f(x=3):
|
||||
return False
|
||||
try:
|
||||
assert f(x=5)
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "x=5" in e.msg
|
||||
|
||||
# These tests should both fail, but should fail nicely...
|
||||
class WeirdRepr:
|
||||
def __repr__(self):
|
||||
return '<WeirdRepr\nsecond line>'
|
||||
|
||||
def bug_test_assert_repr():
|
||||
v = WeirdRepr()
|
||||
try:
|
||||
assert v == 1
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find('WeirdRepr') != -1
|
||||
assert e.msg.find('second line') != -1
|
||||
assert 0
|
||||
|
||||
def test_assert_non_string():
|
||||
try:
|
||||
assert 0, ['list']
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find("list") != -1
|
||||
|
||||
def test_assert_implicit_multiline():
|
||||
try:
|
||||
x = [1,2,3]
|
||||
assert x != [1,
|
||||
2, 3]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find('assert [1, 2, 3] !=') != -1
|
||||
|
||||
|
||||
def test_assert_with_brokenrepr_arg():
|
||||
class BrokenRepr:
|
||||
def __repr__(self): 0 / 0
|
||||
e = AssertionError(BrokenRepr())
|
||||
if e.msg.find("broken __repr__") == -1:
|
||||
py.test.fail("broken __repr__ not handle correctly")
|
||||
|
||||
def test_multiple_statements_per_line():
|
||||
try:
|
||||
a = 1; assert a == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "assert 1 == 2" in e.msg
|
||||
|
||||
def test_power():
|
||||
try:
|
||||
assert 2**3 == 7
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "assert (2 ** 3) == 7" in e.msg
|
||||
|
||||
|
||||
class TestView:
|
||||
|
||||
def setup_class(cls):
|
||||
cls.View = pytest.importorskip("_pytest.assertion.oldinterpret").View
|
||||
|
||||
def test_class_dispatch(self):
|
||||
### Use a custom class hierarchy with existing instances
|
||||
|
||||
class Picklable(self.View):
|
||||
pass
|
||||
|
||||
class Simple(Picklable):
|
||||
__view__ = object
|
||||
def pickle(self):
|
||||
return repr(self.__obj__)
|
||||
|
||||
class Seq(Picklable):
|
||||
__view__ = list, tuple, dict
|
||||
def pickle(self):
|
||||
return ';'.join(
|
||||
[Picklable(item).pickle() for item in self.__obj__])
|
||||
|
||||
class Dict(Seq):
|
||||
__view__ = dict
|
||||
def pickle(self):
|
||||
return Seq.pickle(self) + '!' + Seq(self.values()).pickle()
|
||||
|
||||
assert Picklable(123).pickle() == '123'
|
||||
assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4'
|
||||
assert Picklable({1:2}).pickle() == '1!2'
|
||||
|
||||
def test_viewtype_class_hierarchy(self):
|
||||
# Use a custom class hierarchy based on attributes of existing instances
|
||||
class Operation:
|
||||
"Existing class that I don't want to change."
|
||||
def __init__(self, opname, *args):
|
||||
self.opname = opname
|
||||
self.args = args
|
||||
|
||||
existing = [Operation('+', 4, 5),
|
||||
Operation('getitem', '', 'join'),
|
||||
Operation('setattr', 'x', 'y', 3),
|
||||
Operation('-', 12, 1)]
|
||||
|
||||
class PyOp(self.View):
|
||||
def __viewkey__(self):
|
||||
return self.opname
|
||||
def generate(self):
|
||||
return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args)))
|
||||
|
||||
class PyBinaryOp(PyOp):
|
||||
__view__ = ('+', '-', '*', '/')
|
||||
def generate(self):
|
||||
return '%s %s %s' % (self.args[0], self.opname, self.args[1])
|
||||
|
||||
codelines = [PyOp(op).generate() for op in existing]
|
||||
assert codelines == ["4 + 5", "getitem('', 'join')",
|
||||
"setattr('x', 'y', 3)", "12 - 1"]
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_assert_customizable_reprcompare(monkeypatch):
|
||||
monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello')
|
||||
try:
|
||||
assert 3 == 4
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert "hello" in s
|
||||
|
||||
def test_assert_long_source_1():
|
||||
try:
|
||||
assert len == [
|
||||
(None, ['somet text', 'more text']),
|
||||
]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert 're-run' not in s
|
||||
assert 'somet text' in s
|
||||
|
||||
def test_assert_long_source_2():
|
||||
try:
|
||||
assert(len == [
|
||||
(None, ['somet text', 'more text']),
|
||||
])
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert 're-run' not in s
|
||||
assert 'somet text' in s
|
||||
|
||||
def test_assert_raise_alias(testdir):
|
||||
testdir.makepyfile("""
|
||||
"PYTEST_DONT_REWRITE"
|
||||
import sys
|
||||
EX = AssertionError
|
||||
def test_hello():
|
||||
raise EX("hello"
|
||||
"multi"
|
||||
"line")
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*def test_hello*",
|
||||
"*raise EX*",
|
||||
"*1 failed*",
|
||||
])
|
||||
|
||||
|
||||
@pytest.mark.skipif("sys.version_info < (2,5)")
|
||||
def test_assert_raise_subclass():
|
||||
class SomeEx(AssertionError):
|
||||
def __init__(self, *args):
|
||||
super(SomeEx, self).__init__()
|
||||
try:
|
||||
raise SomeEx("hello")
|
||||
except AssertionError:
|
||||
s = str(exvalue())
|
||||
assert 're-run' not in s
|
||||
assert 'could not determine' in s
|
||||
|
||||
def test_assert_raises_in_nonzero_of_object_pytest_issue10():
|
||||
class A(object):
|
||||
def __nonzero__(self):
|
||||
raise ValueError(42)
|
||||
def __lt__(self, other):
|
||||
return A()
|
||||
def __repr__(self):
|
||||
return "<MY42 object>"
|
||||
def myany(x):
|
||||
return True
|
||||
try:
|
||||
assert not(myany(A() < 0))
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert "<MY42 object> < 0" in s
|
|
@ -2,11 +2,12 @@ import sys
|
|||
|
||||
import py, pytest
|
||||
import _pytest.assertion as plugin
|
||||
from _pytest.assertion import reinterpret, util
|
||||
|
||||
needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)")
|
||||
|
||||
def interpret(expr):
|
||||
return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1)))
|
||||
return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1)))
|
||||
|
||||
class TestBinReprIntegration:
|
||||
pytestmark = needsnewassert
|
||||
|
@ -25,7 +26,7 @@ class TestBinReprIntegration:
|
|||
self.right = right
|
||||
mockhook = MockHook()
|
||||
monkeypatch = request.getfuncargvalue("monkeypatch")
|
||||
monkeypatch.setattr(py.code, '_reprcompare', mockhook)
|
||||
monkeypatch.setattr(util, '_reprcompare', mockhook)
|
||||
return mockhook
|
||||
|
||||
def test_pytest_assertrepr_compare_called(self, hook):
|
||||
|
@ -40,13 +41,13 @@ class TestBinReprIntegration:
|
|||
assert hook.right == [0, 2]
|
||||
|
||||
def test_configure_unconfigure(self, testdir, hook):
|
||||
assert hook == py.code._reprcompare
|
||||
assert hook == util._reprcompare
|
||||
config = testdir.parseconfig()
|
||||
plugin.pytest_configure(config)
|
||||
assert hook != py.code._reprcompare
|
||||
assert hook != util._reprcompare
|
||||
from _pytest.config import pytest_unconfigure
|
||||
pytest_unconfigure(config)
|
||||
assert hook == py.code._reprcompare
|
||||
assert hook == util._reprcompare
|
||||
|
||||
def callequal(left, right):
|
||||
return plugin.pytest_assertrepr_compare('==', left, right)
|
||||
|
@ -119,6 +120,10 @@ class TestAssert_reprcompare:
|
|||
expl = ' '.join(callequal('foo', 'bar'))
|
||||
assert 'raised in repr()' not in expl
|
||||
|
||||
@pytest.mark.skipif("config._assertstate.mode != 'on'")
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()
|
||||
|
||||
def test_reprcompare_notin():
|
||||
detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:]
|
||||
assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++']
|
||||
|
@ -159,7 +164,7 @@ def test_sequence_comparison_uses_repr(testdir):
|
|||
])
|
||||
|
||||
|
||||
def test_functional(testdir):
|
||||
def test_assertion_options(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x = 3
|
||||
|
@ -167,8 +172,30 @@ def test_functional(testdir):
|
|||
""")
|
||||
result = testdir.runpytest()
|
||||
assert "3 == 4" in result.stdout.str()
|
||||
result = testdir.runpytest("--no-assert")
|
||||
off_options = (("--no-assert",),
|
||||
("--nomagic",),
|
||||
("--no-assert", "--nomagic"),
|
||||
("--assertmode=off",),
|
||||
("--assertmode=off", "--no-assert"),
|
||||
("--assertmode=off", "--nomagic"),
|
||||
("--assertmode=off," "--no-assert", "--nomagic"))
|
||||
for opt in off_options:
|
||||
result = testdir.runpytest(*opt)
|
||||
assert "3 == 4" not in result.stdout.str()
|
||||
for mode in "on", "old":
|
||||
for other_opt in off_options[:3]:
|
||||
opt = ("--assertmode=" + mode,) + other_opt
|
||||
result = testdir.runpytest(*opt)
|
||||
assert result.ret == 3
|
||||
assert "assertion options conflict" in result.stderr.str()
|
||||
|
||||
def test_old_assert_mode(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_in_old_mode():
|
||||
assert "@py_builtins" not in globals()
|
||||
""")
|
||||
result = testdir.runpytest("--assertmode=old")
|
||||
assert result.ret == 0
|
||||
|
||||
def test_triple_quoted_string_issue113(testdir):
|
||||
testdir.makepyfile("""
|
||||
|
@ -221,3 +248,10 @@ def test_warn_missing(testdir):
|
|||
result.stderr.fnmatch_lines([
|
||||
"*WARNING*assertion*",
|
||||
])
|
||||
|
||||
def test_load_fake_pyc(testdir):
|
||||
path = testdir.makepyfile("x = 'hello'")
|
||||
co = compile("x = 'bye'", str(path), "exec")
|
||||
plugin._write_pyc(co, path)
|
||||
mod = path.pyimport()
|
||||
assert mod.x == "bye"
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
import sys
|
||||
import py
|
||||
import pytest
|
||||
|
||||
ast = pytest.importorskip("ast")
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.rewrite import rewrite_asserts
|
||||
|
||||
|
||||
def setup_module(mod):
|
||||
mod._old_reprcompare = util._reprcompare
|
||||
py.code._reprcompare = None
|
||||
|
||||
def teardown_module(mod):
|
||||
util._reprcompare = mod._old_reprcompare
|
||||
del mod._old_reprcompare
|
||||
|
||||
|
||||
def rewrite(src):
|
||||
tree = ast.parse(src)
|
||||
rewrite_asserts(tree)
|
||||
return tree
|
||||
|
||||
def getmsg(f, extra_ns=None, must_pass=False):
|
||||
"""Rewrite the assertions in f, run it, and get the failure message."""
|
||||
src = '\n'.join(py.code.Code(f).source().lines)
|
||||
mod = rewrite(src)
|
||||
code = compile(mod, "<test>", "exec")
|
||||
ns = {}
|
||||
if extra_ns is not None:
|
||||
ns.update(extra_ns)
|
||||
py.builtin.exec_(code, ns)
|
||||
func = ns[f.__name__]
|
||||
try:
|
||||
func()
|
||||
except AssertionError:
|
||||
if must_pass:
|
||||
pytest.fail("shouldn't have raised")
|
||||
s = str(sys.exc_info()[1])
|
||||
if not s.startswith("assert"):
|
||||
return "AssertionError: " + s
|
||||
return s
|
||||
else:
|
||||
if not must_pass:
|
||||
pytest.fail("function didn't raise at all")
|
||||
|
||||
|
||||
class TestAssertionRewrite:
|
||||
|
||||
def test_place_initial_imports(self):
|
||||
s = """'Doc string'\nother = stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Assign)
|
||||
s = """from __future__ import with_statement\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
s = """'doc string'\nfrom __future__ import with_statement\nother"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 3
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[4], ast.Expr)
|
||||
|
||||
def test_dont_rewrite(self):
|
||||
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
||||
m = rewrite(s)
|
||||
assert len(m.body) == 2
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
assert isinstance(m.body[1], ast.Assert)
|
||||
assert m.body[1].msg is None
|
||||
|
||||
def test_name(self):
|
||||
def f():
|
||||
assert False
|
||||
assert getmsg(f) == "assert False"
|
||||
def f():
|
||||
f = False
|
||||
assert f
|
||||
assert getmsg(f) == "assert False"
|
||||
def f():
|
||||
assert a_global
|
||||
assert getmsg(f, {"a_global" : False}) == "assert a_global"
|
||||
|
||||
def test_assert_already_has_message(self):
|
||||
def f():
|
||||
assert False, "something bad!"
|
||||
assert getmsg(f) == "AssertionError: something bad!"
|
||||
|
||||
def test_boolop(self):
|
||||
def f():
|
||||
f = g = False
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (False)"
|
||||
def f():
|
||||
f = True
|
||||
g = False
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (True and False)"
|
||||
def f():
|
||||
f = False
|
||||
g = True
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (False)"
|
||||
def f():
|
||||
f = g = False
|
||||
assert f or g
|
||||
assert getmsg(f) == "assert (False or False)"
|
||||
def f():
|
||||
f = True
|
||||
g = False
|
||||
assert f or g
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_short_circut_evaluation(self):
|
||||
pytest.xfail("complicated fix; I'm not sure if it's important")
|
||||
def f():
|
||||
assert True or explode
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_unary_op(self):
|
||||
def f():
|
||||
x = True
|
||||
assert not x
|
||||
assert getmsg(f) == "assert not True"
|
||||
def f():
|
||||
x = 0
|
||||
assert ~x + 1
|
||||
assert getmsg(f) == "assert (~0 + 1)"
|
||||
def f():
|
||||
x = 3
|
||||
assert -x + x
|
||||
assert getmsg(f) == "assert (-3 + 3)"
|
||||
def f():
|
||||
x = 0
|
||||
assert +x + x
|
||||
assert getmsg(f) == "assert (+0 + 0)"
|
||||
|
||||
def test_binary_op(self):
|
||||
def f():
|
||||
x = 1
|
||||
y = -1
|
||||
assert x + y
|
||||
assert getmsg(f) == "assert (1 + -1)"
|
||||
|
||||
def test_call(self):
|
||||
def g(a=42, *args, **kwargs):
|
||||
return False
|
||||
ns = {"g" : g}
|
||||
def f():
|
||||
assert g()
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = g()"""
|
||||
def f():
|
||||
assert g(1)
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = g(1)"""
|
||||
def f():
|
||||
assert g(1, 2)
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = g(1, 2)"""
|
||||
def f():
|
||||
assert g(1, g=42)
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = g(1, g=42)"""
|
||||
def f():
|
||||
assert g(1, 3, g=23)
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = g(1, 3, g=23)"""
|
||||
|
||||
def test_attribute(self):
|
||||
class X(object):
|
||||
g = 3
|
||||
ns = {"X" : X, "x" : X()}
|
||||
def f():
|
||||
assert not x.g
|
||||
assert getmsg(f, ns) == """assert not 3
|
||||
+ where 3 = x.g"""
|
||||
def f():
|
||||
x.a = False
|
||||
assert x.a
|
||||
assert getmsg(f, ns) == """assert False
|
||||
+ where False = x.a"""
|
||||
|
||||
def test_comparisons(self):
|
||||
def f():
|
||||
a, b = range(2)
|
||||
assert b < a
|
||||
assert getmsg(f) == """assert 1 < 0"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a > b > c
|
||||
assert getmsg(f) == """assert 0 > 1"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b > c
|
||||
assert getmsg(f) == """assert 1 > 2"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b <= c
|
||||
getmsg(f, must_pass=True)
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b
|
||||
assert b < c
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_len(self):
|
||||
def f():
|
||||
l = list(range(10))
|
||||
assert len(l) == 11
|
||||
assert getmsg(f).startswith("""assert 10 == 11
|
||||
+ where 10 = len([""")
|
||||
|
||||
def test_custom_reprcompare(self, monkeypatch):
|
||||
def my_reprcompare(op, left, right):
|
||||
return "42"
|
||||
monkeypatch.setattr(util, "_reprcompare", my_reprcompare)
|
||||
def f():
|
||||
assert 42 < 3
|
||||
assert getmsg(f) == "assert 42"
|
||||
def my_reprcompare(op, left, right):
|
||||
return "%s %s %s" % (left, op, right)
|
||||
monkeypatch.setattr(util, "_reprcompare", my_reprcompare)
|
||||
def f():
|
||||
assert 1 < 3 < 5 <= 4 < 7
|
||||
assert getmsg(f) == "assert 5 <= 4"
|
||||
|
||||
def test_assert_raising_nonzero_in_comparison(self):
|
||||
def f():
|
||||
class A(object):
|
||||
def __nonzero__(self):
|
||||
raise ValueError(42)
|
||||
def __lt__(self, other):
|
||||
return A()
|
||||
def __repr__(self):
|
||||
return "<MY42 object>"
|
||||
def myany(x):
|
||||
return False
|
||||
assert myany(A() < 0)
|
||||
assert "<MY42 object> < 0" in getmsg(f)
|
|
@ -313,7 +313,7 @@ class TestSession:
|
|||
def test_collect_topdir(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
id = "::".join([p.basename, "test_func"])
|
||||
config = testdir.parseconfig(id)
|
||||
config = testdir.parseconfigure(id)
|
||||
topdir = testdir.tmpdir
|
||||
rcol = Session(config)
|
||||
assert topdir == rcol.fspath
|
||||
|
@ -328,7 +328,7 @@ class TestSession:
|
|||
def test_collect_protocol_single_function(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
id = "::".join([p.basename, "test_func"])
|
||||
config = testdir.parseconfig(id)
|
||||
config = testdir.parseconfigure(id)
|
||||
topdir = testdir.tmpdir
|
||||
rcol = Session(config)
|
||||
assert topdir == rcol.fspath
|
||||
|
@ -363,7 +363,7 @@ class TestSession:
|
|||
p.basename + "::TestClass::()",
|
||||
normid,
|
||||
]:
|
||||
config = testdir.parseconfig(id)
|
||||
config = testdir.parseconfigure(id)
|
||||
rcol = Session(config=config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
|
@ -388,7 +388,7 @@ class TestSession:
|
|||
""" % p.basename)
|
||||
id = p.basename
|
||||
|
||||
config = testdir.parseconfig(id)
|
||||
config = testdir.parseconfigure(id)
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
|
@ -413,7 +413,7 @@ class TestSession:
|
|||
aaa = testdir.mkpydir("aaa")
|
||||
test_aaa = aaa.join("test_aaa.py")
|
||||
p.move(test_aaa)
|
||||
config = testdir.parseconfig()
|
||||
config = testdir.parseconfigure()
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
|
@ -437,7 +437,7 @@ class TestSession:
|
|||
p.move(test_bbb)
|
||||
|
||||
id = "."
|
||||
config = testdir.parseconfig(id)
|
||||
config = testdir.parseconfigure(id)
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
|
@ -455,7 +455,7 @@ class TestSession:
|
|||
|
||||
def test_serialization_byid(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
config = testdir.parseconfig()
|
||||
config = testdir.parseconfigure()
|
||||
rcol = Session(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
|
@ -476,7 +476,7 @@ class TestSession:
|
|||
pass
|
||||
""")
|
||||
arg = p.basename + ("::TestClass::test_method")
|
||||
config = testdir.parseconfig(arg)
|
||||
config = testdir.parseconfigure(arg)
|
||||
rcol = Session(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
|
|
|
@ -705,11 +705,11 @@ class TestRequest:
|
|||
def test_func(something): pass
|
||||
""")
|
||||
req = funcargs.FuncargRequest(item)
|
||||
req.config._setupstate.prepare(item) # XXX
|
||||
req._pyfuncitem.session._setupstate.prepare(item) # XXX
|
||||
req._fillfuncargs()
|
||||
# successively check finalization calls
|
||||
teardownlist = item.getparent(pytest.Module).obj.teardownlist
|
||||
ss = item.config._setupstate
|
||||
ss = item.session._setupstate
|
||||
assert not teardownlist
|
||||
ss.teardown_exact(item)
|
||||
print(ss.stack)
|
||||
|
@ -834,11 +834,11 @@ class TestRequestCachedSetup:
|
|||
ret1 = req1.cached_setup(setup, teardown, scope="function")
|
||||
assert l == ['setup']
|
||||
# artificial call of finalizer
|
||||
req1.config._setupstate._callfinalizers(item1)
|
||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
||||
assert l == ["setup", "teardown"]
|
||||
ret2 = req1.cached_setup(setup, teardown, scope="function")
|
||||
assert l == ["setup", "teardown", "setup"]
|
||||
req1.config._setupstate._callfinalizers(item1)
|
||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
||||
assert l == ["setup", "teardown", "setup", "teardown"]
|
||||
|
||||
def test_request_cached_setup_two_args(self, testdir):
|
||||
|
|
Loading…
Reference in New Issue