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 | - fix issue43: improve doctests with better traceback reporting on | ||||||
|   unexpected exceptions |   unexpected exceptions | ||||||
| - fix issue47: timing output in junitxml for test cases is now correct | - 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 @@ | ||||||
| """ | """Utilities for assertion debugging""" | ||||||
| support for presented detailed information in failing assertions. | 
 | ||||||
| """ |  | ||||||
| import py | 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 util module is used by the new assertion | ||||||
|     # The _reprcompare attribute on the py.code module is used by | # interpretation code and assertion rewriter to detect this plugin was | ||||||
|     # py._code._assertionnew to detect this plugin was loaded and in | # loaded and in turn call the hooks defined here as part of the | ||||||
|     # turn call the hooks defined here as part of the | # DebugInterpreter. | ||||||
|     # DebugInterpreter. | _reprcompare = None | ||||||
|     m = monkeypatch() | 
 | ||||||
|     config._cleanup.append(m.undo) | def format_explanation(explanation): | ||||||
|     warn_about_missing_assertion() |     """This formats an explanation | ||||||
|     if not config.getvalue("noassert") and not config.getvalue("nomagic"): | 
 | ||||||
|         def callbinrepr(op, left, right): |     Normally all embedded newlines are escaped, however there are | ||||||
|             hook_result = config.hook.pytest_assertrepr_compare( |     three exceptions: \n{, \n} and \n~.  The first two are intended | ||||||
|                 config=config, op=op, left=left, right=right) |     cover nested explanations, see function and attribute explanations | ||||||
|             for new_expl in hook_result: |     for examples (.visit_Call(), visit_Attribute()).  The last one is | ||||||
|                 if new_expl: |     for when one explanation needs to span multiple lines, e.g. when | ||||||
|                     return '\n~'.join(new_expl) |     displaying diffs. | ||||||
|         m.setattr(py.builtin.builtins, |     """ | ||||||
|                   'AssertionError', py.code._AssertionError) |     raw_lines = (explanation or '').split('\n') | ||||||
|         m.setattr(py.code, '_reprcompare', callbinrepr) |     # escape newlines not followed by {, } and ~ | ||||||
|  |     lines = [raw_lines[0]] | ||||||
|  |     for l in raw_lines[1:]: | ||||||
|  |         if l.startswith('{') or l.startswith('}') or l.startswith('~'): | ||||||
|  |             lines.append(l) | ||||||
|  |         else: | ||||||
|  |             lines[-1] += '\\n' + l | ||||||
|  | 
 | ||||||
|  |     result = lines[:1] | ||||||
|  |     stack = [0] | ||||||
|  |     stackcnt = [0] | ||||||
|  |     for line in lines[1:]: | ||||||
|  |         if line.startswith('{'): | ||||||
|  |             if stackcnt[-1]: | ||||||
|  |                 s = 'and   ' | ||||||
|  |             else: | ||||||
|  |                 s = 'where ' | ||||||
|  |             stack.append(len(result)) | ||||||
|  |             stackcnt[-1] += 1 | ||||||
|  |             stackcnt.append(0) | ||||||
|  |             result.append(' +' + '  '*(len(stack)-1) + s + line[1:]) | ||||||
|  |         elif line.startswith('}'): | ||||||
|  |             assert line.startswith('}') | ||||||
|  |             stack.pop() | ||||||
|  |             stackcnt.pop() | ||||||
|  |             result[stack[-1]] += line[1:] | ||||||
|  |         else: | ||||||
|  |             assert line.startswith('~') | ||||||
|  |             result.append('  '*len(stack) + line[1:]) | ||||||
|  |     assert len(stack) == 1 | ||||||
|  |     return '\n'.join(result) | ||||||
| 
 | 
 | ||||||
| def warn_about_missing_assertion(): |  | ||||||
|     try: |  | ||||||
|         assert False |  | ||||||
|     except AssertionError: |  | ||||||
|         pass |  | ||||||
|     else: |  | ||||||
|         sys.stderr.write("WARNING: failing tests may report as passing because " |  | ||||||
|         "assertions are turned off!  (are you using python -O?)\n") |  | ||||||
| 
 | 
 | ||||||
| # Provide basestring in python3 | # Provide basestring in python3 | ||||||
| try: | try: | ||||||
|  | @ -46,7 +60,7 @@ except NameError: | ||||||
|     basestring = str |     basestring = str | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_assertrepr_compare(op, left, right): | def assertrepr_compare(op, left, right): | ||||||
|     """return specialised explanations for some operators/operands""" |     """return specialised explanations for some operators/operands""" | ||||||
|     width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op |     width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op | ||||||
|     left_repr = py.io.saferepr(left, maxsize=int(width/2)) |     left_repr = py.io.saferepr(left, maxsize=int(width/2)) | ||||||
|  | @ -16,9 +16,6 @@ def pytest_addoption(parser): | ||||||
|     group.addoption('--traceconfig', |     group.addoption('--traceconfig', | ||||||
|                action="store_true", dest="traceconfig", default=False, |                action="store_true", dest="traceconfig", default=False, | ||||||
|                help="trace considerations of conftest.py files."), |                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', |     group.addoption('--debug', | ||||||
|                action="store_true", dest="debug", default=False, |                action="store_true", dest="debug", default=False, | ||||||
|                help="generate and show internal debugging information.") |                help="generate and show internal debugging information.") | ||||||
|  |  | ||||||
|  | @ -46,23 +46,22 @@ def pytest_addoption(parser): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pytest_namespace(): | 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): | def pytest_configure(config): | ||||||
|     py.test.config = config # compatibiltiy |     py.test.config = config # compatibiltiy | ||||||
|     if config.option.exitfirst: |     if config.option.exitfirst: | ||||||
|         config.option.maxfail = 1 |         config.option.maxfail = 1 | ||||||
| 
 | 
 | ||||||
| def pytest_cmdline_main(config): | def wrap_session(config, doit): | ||||||
|     """ default command line protocol for initialization, session, |     """Skeleton command line program""" | ||||||
|     running tests and reporting. """ |  | ||||||
|     session = Session(config) |     session = Session(config) | ||||||
|     session.exitstatus = EXIT_OK |     session.exitstatus = EXIT_OK | ||||||
|     try: |     try: | ||||||
|         config.pluginmanager.do_configure(config) |         config.pluginmanager.do_configure(config) | ||||||
|         config.hook.pytest_sessionstart(session=session) |         config.hook.pytest_sessionstart(session=session) | ||||||
|         config.hook.pytest_collection(session=session) |         doit(config, session) | ||||||
|         config.hook.pytest_runtestloop(session=session) |  | ||||||
|     except pytest.UsageError: |     except pytest.UsageError: | ||||||
|         raise |         raise | ||||||
|     except KeyboardInterrupt: |     except KeyboardInterrupt: | ||||||
|  | @ -82,13 +81,17 @@ def pytest_cmdline_main(config): | ||||||
|     config.pluginmanager.do_unconfigure(config) |     config.pluginmanager.do_unconfigure(config) | ||||||
|     return session.exitstatus |     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): | def pytest_collection(session): | ||||||
|     session.perform_collect() |     return session.perform_collect() | ||||||
|     hook = session.config.hook |  | ||||||
|     hook.pytest_collection_modifyitems(session=session, |  | ||||||
|         config=session.config, items=session.items) |  | ||||||
|     hook.pytest_collection_finish(session=session) |  | ||||||
|     return True |  | ||||||
| 
 | 
 | ||||||
| def pytest_runtestloop(session): | def pytest_runtestloop(session): | ||||||
|     if session.config.option.collectonly: |     if session.config.option.collectonly: | ||||||
|  | @ -374,6 +377,16 @@ class Session(FSCollector): | ||||||
|         return HookProxy(fspath, self.config) |         return HookProxy(fspath, self.config) | ||||||
| 
 | 
 | ||||||
|     def perform_collect(self, args=None, genitems=True): |     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: |         if args is None: | ||||||
|             args = self.config.args |             args = self.config.args | ||||||
|         self.trace("perform_collect", self, args) |         self.trace("perform_collect", self, args) | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import re | ||||||
| import inspect | import inspect | ||||||
| import time | import time | ||||||
| from fnmatch import fnmatch | from fnmatch import fnmatch | ||||||
| from _pytest.main import Session | from _pytest.main import Session, EXIT_OK | ||||||
| from py.builtin import print_ | from py.builtin import print_ | ||||||
| from _pytest.core import HookRelay | from _pytest.core import HookRelay | ||||||
| 
 | 
 | ||||||
|  | @ -292,13 +292,19 @@ class TmpTestdir: | ||||||
|         assert '::' not in str(arg) |         assert '::' not in str(arg) | ||||||
|         p = py.path.local(arg) |         p = py.path.local(arg) | ||||||
|         x = session.fspath.bestrelpath(p) |         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): |     def getpathnode(self, path): | ||||||
|         config = self.parseconfig(path) |         config = self.parseconfigure(path) | ||||||
|         session = Session(config) |         session = Session(config) | ||||||
|         x = session.fspath.bestrelpath(path) |         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): |     def genitems(self, colitems): | ||||||
|         session = colitems[0].session |         session = colitems[0].session | ||||||
|  | @ -312,7 +318,9 @@ class TmpTestdir: | ||||||
|         config = self.parseconfigure(*args) |         config = self.parseconfigure(*args) | ||||||
|         rec = self.getreportrecorder(config) |         rec = self.getreportrecorder(config) | ||||||
|         session = Session(config) |         session = Session(config) | ||||||
|  |         config.hook.pytest_sessionstart(session=session) | ||||||
|         session.perform_collect() |         session.perform_collect() | ||||||
|  |         config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) | ||||||
|         return session.items, rec |         return session.items, rec | ||||||
| 
 | 
 | ||||||
|     def runitem(self, source): |     def runitem(self, source): | ||||||
|  | @ -382,6 +390,8 @@ class TmpTestdir: | ||||||
|             c.basetemp = py.path.local.make_numbered_dir(prefix="reparse", |             c.basetemp = py.path.local.make_numbered_dir(prefix="reparse", | ||||||
|                 keep=0, rootdir=self.tmpdir, lock_timeout=None) |                 keep=0, rootdir=self.tmpdir, lock_timeout=None) | ||||||
|             c.parse(args) |             c.parse(args) | ||||||
|  |             c.pluginmanager.do_configure(c) | ||||||
|  |             self.request.addfinalizer(lambda: c.pluginmanager.do_unconfigure(c)) | ||||||
|             return c |             return c | ||||||
|         finally: |         finally: | ||||||
|             py.test.config = oldconfig |             py.test.config = oldconfig | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import inspect | ||||||
| import sys | import sys | ||||||
| import pytest | import pytest | ||||||
| from py._code.code import TerminalRepr | from py._code.code import TerminalRepr | ||||||
|  | from _pytest import assertion | ||||||
| 
 | 
 | ||||||
| import _pytest | import _pytest | ||||||
| cutdir = py.path.local(_pytest.__file__).dirpath() | cutdir = py.path.local(_pytest.__file__).dirpath() | ||||||
|  | @ -226,8 +227,12 @@ class Module(pytest.File, PyCollectorMixin): | ||||||
| 
 | 
 | ||||||
|     def _importtestmodule(self): |     def _importtestmodule(self): | ||||||
|         # we assume we are only called once per module |         # we assume we are only called once per module | ||||||
|  |         assertion.before_module_import(self) | ||||||
|         try: |         try: | ||||||
|             mod = self.fspath.pyimport(ensuresyspath=True) |             try: | ||||||
|  |                 mod = self.fspath.pyimport(ensuresyspath=True) | ||||||
|  |             finally: | ||||||
|  |                 assertion.after_module_import(self) | ||||||
|         except SyntaxError: |         except SyntaxError: | ||||||
|             excinfo = py.code.ExceptionInfo() |             excinfo = py.code.ExceptionInfo() | ||||||
|             raise self.CollectError(excinfo.getrepr(style="short")) |             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 |         # test generators are seen as collectors but they also | ||||||
|         # invoke setup/teardown on popular request |         # invoke setup/teardown on popular request | ||||||
|         # (induced by the common "test_*" naming shared with normal tests) |         # (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 |         # see FunctionMixin.setup and test_setupstate_is_preserved_134 | ||||||
|         self._preservedparent = self.parent.obj |         self._preservedparent = self.parent.obj | ||||||
|         l = [] |         l = [] | ||||||
|  | @ -726,7 +731,7 @@ class FuncargRequest: | ||||||
| 
 | 
 | ||||||
|     def _addfinalizer(self, finalizer, scope): |     def _addfinalizer(self, finalizer, scope): | ||||||
|         colitem = self._getscopeitem(scope) |         colitem = self._getscopeitem(scope) | ||||||
|         self.config._setupstate.addfinalizer( |         self._pyfuncitem.session._setupstate.addfinalizer( | ||||||
|             finalizer=finalizer, colitem=colitem) |             finalizer=finalizer, colitem=colitem) | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|  | @ -747,8 +752,10 @@ class FuncargRequest: | ||||||
|         raise self.LookupError(msg) |         raise self.LookupError(msg) | ||||||
| 
 | 
 | ||||||
| def showfuncargs(config): | def showfuncargs(config): | ||||||
|     from _pytest.main import Session |     from _pytest.main import wrap_session | ||||||
|     session = Session(config) |     return wrap_session(config, _showfuncargs_main) | ||||||
|  | 
 | ||||||
|  | def _showfuncargs_main(config, session): | ||||||
|     session.perform_collect() |     session.perform_collect() | ||||||
|     if session.items: |     if session.items: | ||||||
|         plugins = session.items[0].getplugins() |         plugins = session.items[0].getplugins() | ||||||
|  |  | ||||||
|  | @ -14,17 +14,15 @@ def pytest_namespace(): | ||||||
| # | # | ||||||
| # pytest plugin hooks | # pytest plugin hooks | ||||||
| 
 | 
 | ||||||
| # XXX move to pytest_sessionstart and fix py.test owns tests | def pytest_sessionstart(session): | ||||||
| def pytest_configure(config): |     session._setupstate = SetupState() | ||||||
|     config._setupstate = SetupState() |  | ||||||
| 
 | 
 | ||||||
| def pytest_sessionfinish(session, exitstatus): | def pytest_sessionfinish(session, exitstatus): | ||||||
|     if hasattr(session.config, '_setupstate'): |     hook = session.config.hook | ||||||
|         hook = session.config.hook |     rep = hook.pytest__teardown_final(session=session) | ||||||
|         rep = hook.pytest__teardown_final(session=session) |     if rep: | ||||||
|         if rep: |         hook.pytest__teardown_final_logerror(session=session, report=rep) | ||||||
|             hook.pytest__teardown_final_logerror(session=session, report=rep) |         session.exitstatus = 1 | ||||||
|             session.exitstatus = 1 |  | ||||||
| 
 | 
 | ||||||
| class NodeInfo: | class NodeInfo: | ||||||
|     def __init__(self, location): |     def __init__(self, location): | ||||||
|  | @ -46,16 +44,16 @@ def runtestprotocol(item, log=True): | ||||||
|     return reports |     return reports | ||||||
| 
 | 
 | ||||||
| def pytest_runtest_setup(item): | def pytest_runtest_setup(item): | ||||||
|     item.config._setupstate.prepare(item) |     item.session._setupstate.prepare(item) | ||||||
| 
 | 
 | ||||||
| def pytest_runtest_call(item): | def pytest_runtest_call(item): | ||||||
|     item.runtest() |     item.runtest() | ||||||
| 
 | 
 | ||||||
| def pytest_runtest_teardown(item): | def pytest_runtest_teardown(item): | ||||||
|     item.config._setupstate.teardown_exact(item) |     item.session._setupstate.teardown_exact(item) | ||||||
| 
 | 
 | ||||||
| def pytest__teardown_final(session): | 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: |     if call.excinfo: | ||||||
|         ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) |         ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) | ||||||
|         call.excinfo.traceback = ntraceback.filter() |         call.excinfo.traceback = ntraceback.filter() | ||||||
|  |  | ||||||
|  | @ -18,8 +18,8 @@ following:: | ||||||
|     def test_function(): |     def test_function(): | ||||||
|         assert f() == 4 |         assert f() == 4 | ||||||
| 
 | 
 | ||||||
| to assert that your object returns a certain value. If this | to assert that your function returns a certain value. If this assertion fails | ||||||
| assertion fails you will see the value of ``x``:: | you will see the value of ``x``:: | ||||||
| 
 | 
 | ||||||
|     $ py.test test_assert1.py |     $ py.test test_assert1.py | ||||||
|     =========================== test session starts ============================ |     =========================== test session starts ============================ | ||||||
|  | @ -39,22 +39,12 @@ assertion fails you will see the value of ``x``:: | ||||||
|     test_assert1.py:5: AssertionError |     test_assert1.py:5: AssertionError | ||||||
|     ========================= 1 failed in 0.02 seconds ========================= |     ========================= 1 failed in 0.02 seconds ========================= | ||||||
| 
 | 
 | ||||||
| Reporting details about the failing assertion is achieved by re-evaluating | py.test has support for showing the values of the most common subexpressions | ||||||
| the assert expression and recording the intermediate values. | 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 | See :ref:`assert-details` for more information on assertion introspection. | ||||||
| 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 != '...' |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| assertions about expected exceptions | 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. | 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 |     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 | ``py.test`` still uses many metaprogramming techniques and | ||||||
| reading its source is thus likely not something for Python beginners. | reading its source is thus likely not something for Python beginners. | ||||||
| 
 | 
 | ||||||
| A second "magic" issue arguably the assert statement re-intepreation: | A second "magic" issue arguably the assert statement debugging feature. When | ||||||
| When an ``assert`` statement fails, py.test re-interprets the expression | loading test modules py.test rewrites the source code of assert statements. When | ||||||
| to show intermediate values if a test fails.  If your expression | a rewritten assert statement fails, its error message has more information than | ||||||
| has side effects (better to avoid them anyway!) the intermediate values | the original. py.test also has a second assert debugging technique. When an | ||||||
| may not be the same, obfuscating the initial error (this is also | ``assert`` statement that was missed by the rewriter fails, py.test | ||||||
| explained at the command line if it happens). | re-interprets the expression to show intermediate values if a test fails. This | ||||||
| ``py.test --no-assert`` turns off assert re-interpretation. | 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 namespaces`: index.html | ||||||
| .. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py | .. _`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', |         name='pytest', | ||||||
|         description='py.test: simple powerful testing with Python', |         description='py.test: simple powerful testing with Python', | ||||||
|         long_description = long_description, |         long_description = long_description, | ||||||
|         version='2.1.0.dev1', |         version='2.1.0.dev2', | ||||||
|         url='http://pytest.org', |         url='http://pytest.org', | ||||||
|         license='MIT license', |         license='MIT license', | ||||||
|         platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], |         platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], | ||||||
|         author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', |         author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', | ||||||
|         author_email='holger at merlinux.eu', |         author_email='holger at merlinux.eu', | ||||||
|         entry_points= make_entry_points(), |         entry_points= make_entry_points(), | ||||||
|         install_requires=['py>1.4.1'], |         install_requires=['py>1.4.3'], | ||||||
|         classifiers=['Development Status :: 5 - Production/Stable', |         classifiers=['Development Status :: 5 - Production/Stable', | ||||||
|                      'Intended Audience :: Developers', |                      'Intended Audience :: Developers', | ||||||
|                      'License :: OSI Approved :: MIT License', |                      '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 py, pytest | ||||||
| import _pytest.assertion as plugin | import _pytest.assertion as plugin | ||||||
|  | from _pytest.assertion import reinterpret, util | ||||||
| 
 | 
 | ||||||
| needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") | needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") | ||||||
| 
 | 
 | ||||||
| def interpret(expr): | 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: | class TestBinReprIntegration: | ||||||
|     pytestmark = needsnewassert |     pytestmark = needsnewassert | ||||||
|  | @ -25,7 +26,7 @@ class TestBinReprIntegration: | ||||||
|                 self.right = right |                 self.right = right | ||||||
|         mockhook = MockHook() |         mockhook = MockHook() | ||||||
|         monkeypatch = request.getfuncargvalue("monkeypatch") |         monkeypatch = request.getfuncargvalue("monkeypatch") | ||||||
|         monkeypatch.setattr(py.code, '_reprcompare', mockhook) |         monkeypatch.setattr(util, '_reprcompare', mockhook) | ||||||
|         return mockhook |         return mockhook | ||||||
| 
 | 
 | ||||||
|     def test_pytest_assertrepr_compare_called(self, hook): |     def test_pytest_assertrepr_compare_called(self, hook): | ||||||
|  | @ -40,13 +41,13 @@ class TestBinReprIntegration: | ||||||
|         assert hook.right == [0, 2] |         assert hook.right == [0, 2] | ||||||
| 
 | 
 | ||||||
|     def test_configure_unconfigure(self, testdir, hook): |     def test_configure_unconfigure(self, testdir, hook): | ||||||
|         assert hook == py.code._reprcompare |         assert hook == util._reprcompare | ||||||
|         config = testdir.parseconfig() |         config = testdir.parseconfig() | ||||||
|         plugin.pytest_configure(config) |         plugin.pytest_configure(config) | ||||||
|         assert hook != py.code._reprcompare |         assert hook != util._reprcompare | ||||||
|         from _pytest.config import pytest_unconfigure |         from _pytest.config import pytest_unconfigure | ||||||
|         pytest_unconfigure(config) |         pytest_unconfigure(config) | ||||||
|         assert hook == py.code._reprcompare |         assert hook == util._reprcompare | ||||||
| 
 | 
 | ||||||
| def callequal(left, right): | def callequal(left, right): | ||||||
|     return plugin.pytest_assertrepr_compare('==', left, right) |     return plugin.pytest_assertrepr_compare('==', left, right) | ||||||
|  | @ -119,6 +120,10 @@ class TestAssert_reprcompare: | ||||||
|         expl = ' '.join(callequal('foo', 'bar')) |         expl = ' '.join(callequal('foo', 'bar')) | ||||||
|         assert 'raised in repr()' not in expl |         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(): | def test_reprcompare_notin(): | ||||||
|     detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:] |     detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:] | ||||||
|     assert detail == ["'foo' is contained here:", '  aaafoobbb', '?    +++'] |     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(""" |     testdir.makepyfile(""" | ||||||
|         def test_hello(): |         def test_hello(): | ||||||
|             x = 3 |             x = 3 | ||||||
|  | @ -167,8 +172,30 @@ def test_functional(testdir): | ||||||
|     """) |     """) | ||||||
|     result = testdir.runpytest() |     result = testdir.runpytest() | ||||||
|     assert "3 == 4" in result.stdout.str() |     assert "3 == 4" in result.stdout.str() | ||||||
|     result = testdir.runpytest("--no-assert") |     off_options = (("--no-assert",), | ||||||
|     assert "3 == 4" not in result.stdout.str() |                    ("--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): | def test_triple_quoted_string_issue113(testdir): | ||||||
|     testdir.makepyfile(""" |     testdir.makepyfile(""" | ||||||
|  | @ -221,3 +248,10 @@ def test_warn_missing(testdir): | ||||||
|     result.stderr.fnmatch_lines([ |     result.stderr.fnmatch_lines([ | ||||||
|         "*WARNING*assertion*", |         "*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): |     def test_collect_topdir(self, testdir): | ||||||
|         p = testdir.makepyfile("def test_func(): pass") |         p = testdir.makepyfile("def test_func(): pass") | ||||||
|         id = "::".join([p.basename, "test_func"]) |         id = "::".join([p.basename, "test_func"]) | ||||||
|         config = testdir.parseconfig(id) |         config = testdir.parseconfigure(id) | ||||||
|         topdir = testdir.tmpdir |         topdir = testdir.tmpdir | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         assert topdir == rcol.fspath |         assert topdir == rcol.fspath | ||||||
|  | @ -328,7 +328,7 @@ class TestSession: | ||||||
|     def test_collect_protocol_single_function(self, testdir): |     def test_collect_protocol_single_function(self, testdir): | ||||||
|         p = testdir.makepyfile("def test_func(): pass") |         p = testdir.makepyfile("def test_func(): pass") | ||||||
|         id = "::".join([p.basename, "test_func"]) |         id = "::".join([p.basename, "test_func"]) | ||||||
|         config = testdir.parseconfig(id) |         config = testdir.parseconfigure(id) | ||||||
|         topdir = testdir.tmpdir |         topdir = testdir.tmpdir | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         assert topdir == rcol.fspath |         assert topdir == rcol.fspath | ||||||
|  | @ -363,7 +363,7 @@ class TestSession: | ||||||
|                    p.basename + "::TestClass::()", |                    p.basename + "::TestClass::()", | ||||||
|                    normid, |                    normid, | ||||||
|                    ]: |                    ]: | ||||||
|             config = testdir.parseconfig(id) |             config = testdir.parseconfigure(id) | ||||||
|             rcol = Session(config=config) |             rcol = Session(config=config) | ||||||
|             rcol.perform_collect() |             rcol.perform_collect() | ||||||
|             items = rcol.items |             items = rcol.items | ||||||
|  | @ -388,7 +388,7 @@ class TestSession: | ||||||
|         """ % p.basename) |         """ % p.basename) | ||||||
|         id = p.basename |         id = p.basename | ||||||
| 
 | 
 | ||||||
|         config = testdir.parseconfig(id) |         config = testdir.parseconfigure(id) | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         hookrec = testdir.getreportrecorder(config) |         hookrec = testdir.getreportrecorder(config) | ||||||
|         rcol.perform_collect() |         rcol.perform_collect() | ||||||
|  | @ -413,7 +413,7 @@ class TestSession: | ||||||
|         aaa = testdir.mkpydir("aaa") |         aaa = testdir.mkpydir("aaa") | ||||||
|         test_aaa = aaa.join("test_aaa.py") |         test_aaa = aaa.join("test_aaa.py") | ||||||
|         p.move(test_aaa) |         p.move(test_aaa) | ||||||
|         config = testdir.parseconfig() |         config = testdir.parseconfigure() | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         hookrec = testdir.getreportrecorder(config) |         hookrec = testdir.getreportrecorder(config) | ||||||
|         rcol.perform_collect() |         rcol.perform_collect() | ||||||
|  | @ -437,7 +437,7 @@ class TestSession: | ||||||
|         p.move(test_bbb) |         p.move(test_bbb) | ||||||
| 
 | 
 | ||||||
|         id = "." |         id = "." | ||||||
|         config = testdir.parseconfig(id) |         config = testdir.parseconfigure(id) | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         hookrec = testdir.getreportrecorder(config) |         hookrec = testdir.getreportrecorder(config) | ||||||
|         rcol.perform_collect() |         rcol.perform_collect() | ||||||
|  | @ -455,7 +455,7 @@ class TestSession: | ||||||
| 
 | 
 | ||||||
|     def test_serialization_byid(self, testdir): |     def test_serialization_byid(self, testdir): | ||||||
|         p = testdir.makepyfile("def test_func(): pass") |         p = testdir.makepyfile("def test_func(): pass") | ||||||
|         config = testdir.parseconfig() |         config = testdir.parseconfigure() | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         rcol.perform_collect() |         rcol.perform_collect() | ||||||
|         items = rcol.items |         items = rcol.items | ||||||
|  | @ -476,7 +476,7 @@ class TestSession: | ||||||
|                     pass |                     pass | ||||||
|         """) |         """) | ||||||
|         arg = p.basename + ("::TestClass::test_method") |         arg = p.basename + ("::TestClass::test_method") | ||||||
|         config = testdir.parseconfig(arg) |         config = testdir.parseconfigure(arg) | ||||||
|         rcol = Session(config) |         rcol = Session(config) | ||||||
|         rcol.perform_collect() |         rcol.perform_collect() | ||||||
|         items = rcol.items |         items = rcol.items | ||||||
|  |  | ||||||
|  | @ -705,11 +705,11 @@ class TestRequest: | ||||||
|             def test_func(something): pass |             def test_func(something): pass | ||||||
|         """) |         """) | ||||||
|         req = funcargs.FuncargRequest(item) |         req = funcargs.FuncargRequest(item) | ||||||
|         req.config._setupstate.prepare(item) # XXX |         req._pyfuncitem.session._setupstate.prepare(item) # XXX | ||||||
|         req._fillfuncargs() |         req._fillfuncargs() | ||||||
|         # successively check finalization calls |         # successively check finalization calls | ||||||
|         teardownlist = item.getparent(pytest.Module).obj.teardownlist |         teardownlist = item.getparent(pytest.Module).obj.teardownlist | ||||||
|         ss = item.config._setupstate |         ss = item.session._setupstate | ||||||
|         assert not teardownlist |         assert not teardownlist | ||||||
|         ss.teardown_exact(item) |         ss.teardown_exact(item) | ||||||
|         print(ss.stack) |         print(ss.stack) | ||||||
|  | @ -834,11 +834,11 @@ class TestRequestCachedSetup: | ||||||
|         ret1 = req1.cached_setup(setup, teardown, scope="function") |         ret1 = req1.cached_setup(setup, teardown, scope="function") | ||||||
|         assert l == ['setup'] |         assert l == ['setup'] | ||||||
|         # artificial call of finalizer |         # artificial call of finalizer | ||||||
|         req1.config._setupstate._callfinalizers(item1) |         req1._pyfuncitem.session._setupstate._callfinalizers(item1) | ||||||
|         assert l == ["setup", "teardown"] |         assert l == ["setup", "teardown"] | ||||||
|         ret2 = req1.cached_setup(setup, teardown, scope="function") |         ret2 = req1.cached_setup(setup, teardown, scope="function") | ||||||
|         assert l == ["setup", "teardown", "setup"] |         assert l == ["setup", "teardown", "setup"] | ||||||
|         req1.config._setupstate._callfinalizers(item1) |         req1._pyfuncitem.session._setupstate._callfinalizers(item1) | ||||||
|         assert l == ["setup", "teardown", "setup", "teardown"] |         assert l == ["setup", "teardown", "setup", "teardown"] | ||||||
| 
 | 
 | ||||||
|     def test_request_cached_setup_two_args(self, testdir): |     def test_request_cached_setup_two_args(self, testdir): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue