commit
37fb50a3ed
|
@ -0,0 +1,4 @@
|
||||||
|
New `pytest_assertion_pass <https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_assertion_pass>`__
|
||||||
|
hook, called with context information when an assertion *passes*.
|
||||||
|
|
||||||
|
This hook is still **experimental** so use it with caution.
|
|
@ -0,0 +1 @@
|
||||||
|
pytest now also depends on the `astor <https://pypi.org/project/astor/>`__ package.
|
|
@ -665,15 +665,14 @@ Session related reporting hooks:
|
||||||
.. autofunction:: pytest_fixture_post_finalizer
|
.. autofunction:: pytest_fixture_post_finalizer
|
||||||
.. autofunction:: pytest_warning_captured
|
.. autofunction:: pytest_warning_captured
|
||||||
|
|
||||||
And here is the central hook for reporting about
|
Central hook for reporting about test execution:
|
||||||
test execution:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_runtest_logreport
|
.. autofunction:: pytest_runtest_logreport
|
||||||
|
|
||||||
You can also use this hook to customize assertion representation for some
|
Assertion related hooks:
|
||||||
types:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_assertrepr_compare
|
.. autofunction:: pytest_assertrepr_compare
|
||||||
|
.. autofunction:: pytest_assertion_pass
|
||||||
|
|
||||||
|
|
||||||
Debugging/Interaction hooks
|
Debugging/Interaction hooks
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -13,6 +13,7 @@ INSTALL_REQUIRES = [
|
||||||
"pluggy>=0.12,<1.0",
|
"pluggy>=0.12,<1.0",
|
||||||
"importlib-metadata>=0.12",
|
"importlib-metadata>=0.12",
|
||||||
"wcwidth",
|
"wcwidth",
|
||||||
|
"astor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,13 @@ def pytest_addoption(parser):
|
||||||
test modules on import to provide assert
|
test modules on import to provide assert
|
||||||
expression information.""",
|
expression information.""",
|
||||||
)
|
)
|
||||||
|
parser.addini(
|
||||||
|
"enable_assertion_pass_hook",
|
||||||
|
type="bool",
|
||||||
|
default=False,
|
||||||
|
help="Enables the pytest_assertion_pass hook."
|
||||||
|
"Make sure to delete any previously generated pyc cache files.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_assert_rewrite(*names):
|
def register_assert_rewrite(*names):
|
||||||
|
@ -92,7 +99,7 @@ def pytest_collection(session):
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
"""Setup the pytest_assertrepr_compare hook
|
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
|
||||||
|
|
||||||
The newinterpret and rewrite modules will use util._reprcompare if
|
The newinterpret and rewrite modules will use util._reprcompare if
|
||||||
it exists to use custom reporting via the
|
it exists to use custom reporting via the
|
||||||
|
@ -129,9 +136,19 @@ def pytest_runtest_setup(item):
|
||||||
|
|
||||||
util._reprcompare = callbinrepr
|
util._reprcompare = callbinrepr
|
||||||
|
|
||||||
|
if item.ihook.pytest_assertion_pass.get_hookimpls():
|
||||||
|
|
||||||
|
def call_assertion_pass_hook(lineno, expl, orig):
|
||||||
|
item.ihook.pytest_assertion_pass(
|
||||||
|
item=item, lineno=lineno, orig=orig, expl=expl
|
||||||
|
)
|
||||||
|
|
||||||
|
util._assertion_pass = call_assertion_pass_hook
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_teardown(item):
|
def pytest_runtest_teardown(item):
|
||||||
util._reprcompare = None
|
util._reprcompare = None
|
||||||
|
util._assertion_pass = None
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session):
|
def pytest_sessionfinish(session):
|
||||||
|
|
|
@ -10,6 +10,7 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
import astor
|
||||||
import atomicwrites
|
import atomicwrites
|
||||||
|
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
@ -134,7 +135,7 @@ class AssertionRewritingHook:
|
||||||
co = _read_pyc(fn, pyc, state.trace)
|
co = _read_pyc(fn, pyc, state.trace)
|
||||||
if co is None:
|
if co is None:
|
||||||
state.trace("rewriting {!r}".format(fn))
|
state.trace("rewriting {!r}".format(fn))
|
||||||
source_stat, co = _rewrite_test(fn)
|
source_stat, co = _rewrite_test(fn, self.config)
|
||||||
if write:
|
if write:
|
||||||
self._writing_pyc = True
|
self._writing_pyc = True
|
||||||
try:
|
try:
|
||||||
|
@ -278,13 +279,13 @@ def _write_pyc(state, co, source_stat, pyc):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _rewrite_test(fn):
|
def _rewrite_test(fn, config):
|
||||||
"""read and rewrite *fn* and return the code object."""
|
"""read and rewrite *fn* and return the code object."""
|
||||||
stat = os.stat(fn)
|
stat = os.stat(fn)
|
||||||
with open(fn, "rb") as f:
|
with open(fn, "rb") as f:
|
||||||
source = f.read()
|
source = f.read()
|
||||||
tree = ast.parse(source, filename=fn)
|
tree = ast.parse(source, filename=fn)
|
||||||
rewrite_asserts(tree, fn)
|
rewrite_asserts(tree, fn, config)
|
||||||
co = compile(tree, fn, "exec", dont_inherit=True)
|
co = compile(tree, fn, "exec", dont_inherit=True)
|
||||||
return stat, co
|
return stat, co
|
||||||
|
|
||||||
|
@ -326,9 +327,9 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
return co
|
return co
|
||||||
|
|
||||||
|
|
||||||
def rewrite_asserts(mod, module_path=None):
|
def rewrite_asserts(mod, module_path=None, config=None):
|
||||||
"""Rewrite the assert statements in mod."""
|
"""Rewrite the assert statements in mod."""
|
||||||
AssertionRewriter(module_path).run(mod)
|
AssertionRewriter(module_path, config).run(mod)
|
||||||
|
|
||||||
|
|
||||||
def _saferepr(obj):
|
def _saferepr(obj):
|
||||||
|
@ -401,6 +402,17 @@ def _call_reprcompare(ops, results, expls, each_obj):
|
||||||
return expl
|
return expl
|
||||||
|
|
||||||
|
|
||||||
|
def _call_assertion_pass(lineno, orig, expl):
|
||||||
|
if util._assertion_pass is not None:
|
||||||
|
util._assertion_pass(lineno=lineno, orig=orig, expl=expl)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_if_assertion_pass_impl():
|
||||||
|
"""Checks if any plugins implement the pytest_assertion_pass hook
|
||||||
|
in order not to generate explanation unecessarily (might be expensive)"""
|
||||||
|
return True if util._assertion_pass else False
|
||||||
|
|
||||||
|
|
||||||
unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
|
unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
|
||||||
|
|
||||||
binop_map = {
|
binop_map = {
|
||||||
|
@ -473,7 +485,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
original assert statement: it rewrites the test of an assertion
|
original assert statement: it rewrites the test of an assertion
|
||||||
to provide intermediate values and replace it with an if statement
|
to provide intermediate values and replace it with an if statement
|
||||||
which raises an assertion error with a detailed explanation in
|
which raises an assertion error with a detailed explanation in
|
||||||
case the expression is false.
|
case the expression is false and calls pytest_assertion_pass hook
|
||||||
|
if expression is true.
|
||||||
|
|
||||||
For this .visit_Assert() uses the visitor pattern to visit all the
|
For this .visit_Assert() uses the visitor pattern to visit all the
|
||||||
AST nodes of the ast.Assert.test field, each visit call returning
|
AST nodes of the ast.Assert.test field, each visit call returning
|
||||||
|
@ -491,9 +504,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
by statements. Variables are created using .variable() and
|
by statements. Variables are created using .variable() and
|
||||||
have the form of "@py_assert0".
|
have the form of "@py_assert0".
|
||||||
|
|
||||||
:on_failure: The AST statements which will be executed if the
|
:expl_stmts: The AST statements which will be executed to get
|
||||||
assertion test fails. This is the code which will construct
|
data from the assertion. This is the code which will construct
|
||||||
the failure message and raises the AssertionError.
|
the detailed assertion message that is used in the AssertionError
|
||||||
|
or for the pytest_assertion_pass hook.
|
||||||
|
|
||||||
:explanation_specifiers: A dict filled by .explanation_param()
|
:explanation_specifiers: A dict filled by .explanation_param()
|
||||||
with %-formatting placeholders and their corresponding
|
with %-formatting placeholders and their corresponding
|
||||||
|
@ -509,9 +523,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, module_path):
|
def __init__(self, module_path, config):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.module_path = module_path
|
self.module_path = module_path
|
||||||
|
self.config = config
|
||||||
|
if config is not None:
|
||||||
|
self.enable_assertion_pass_hook = config.getini(
|
||||||
|
"enable_assertion_pass_hook"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.enable_assertion_pass_hook = False
|
||||||
|
|
||||||
def run(self, mod):
|
def run(self, mod):
|
||||||
"""Find all assert statements in *mod* and rewrite them."""
|
"""Find all assert statements in *mod* and rewrite them."""
|
||||||
|
@ -642,7 +663,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
|
|
||||||
The expl_expr should be an ast.Str instance constructed from
|
The expl_expr should be an ast.Str instance constructed from
|
||||||
the %-placeholders created by .explanation_param(). This will
|
the %-placeholders created by .explanation_param(). This will
|
||||||
add the required code to format said string to .on_failure and
|
add the required code to format said string to .expl_stmts and
|
||||||
return the ast.Name instance of the formatted string.
|
return the ast.Name instance of the formatted string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -653,7 +674,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
format_dict = ast.Dict(keys, list(current.values()))
|
format_dict = ast.Dict(keys, list(current.values()))
|
||||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||||
name = "@py_format" + str(next(self.variable_counter))
|
name = "@py_format" + str(next(self.variable_counter))
|
||||||
self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form))
|
if self.enable_assertion_pass_hook:
|
||||||
|
self.format_variables.append(name)
|
||||||
|
self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
|
||||||
return ast.Name(name, ast.Load())
|
return ast.Name(name, ast.Load())
|
||||||
|
|
||||||
def generic_visit(self, node):
|
def generic_visit(self, node):
|
||||||
|
@ -687,8 +710,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.statements = []
|
self.statements = []
|
||||||
self.variables = []
|
self.variables = []
|
||||||
self.variable_counter = itertools.count()
|
self.variable_counter = itertools.count()
|
||||||
|
|
||||||
|
if self.enable_assertion_pass_hook:
|
||||||
|
self.format_variables = []
|
||||||
|
|
||||||
self.stack = []
|
self.stack = []
|
||||||
self.on_failure = []
|
self.expl_stmts = []
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# Rewrite assert into a bunch of statements.
|
# Rewrite assert into a bunch of statements.
|
||||||
top_condition, explanation = self.visit(assert_.test)
|
top_condition, explanation = self.visit(assert_.test)
|
||||||
|
@ -699,24 +726,77 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
top_condition, module_path=self.module_path, lineno=assert_.lineno
|
top_condition, module_path=self.module_path, lineno=assert_.lineno
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Create failure message.
|
|
||||||
body = self.on_failure
|
|
||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
|
||||||
self.statements.append(ast.If(negation, body, []))
|
|
||||||
if assert_.msg:
|
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
|
||||||
explanation = "\n>assert " + explanation
|
|
||||||
else:
|
|
||||||
assertmsg = ast.Str("")
|
|
||||||
explanation = "assert " + explanation
|
|
||||||
template = ast.BinOp(assertmsg, ast.Add(), 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], [])
|
|
||||||
raise_ = ast.Raise(exc, None)
|
|
||||||
|
|
||||||
body.append(raise_)
|
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||||
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
msg = self.pop_format_context(ast.Str(explanation))
|
||||||
|
|
||||||
|
# Failed
|
||||||
|
if assert_.msg:
|
||||||
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
|
gluestr = "\n>assert "
|
||||||
|
else:
|
||||||
|
assertmsg = ast.Str("")
|
||||||
|
gluestr = "assert "
|
||||||
|
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
||||||
|
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||||
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
|
fmt = self.helper("_format_explanation", err_msg)
|
||||||
|
exc = ast.Call(err_name, [fmt], [])
|
||||||
|
raise_ = ast.Raise(exc, None)
|
||||||
|
statements_fail = []
|
||||||
|
statements_fail.extend(self.expl_stmts)
|
||||||
|
statements_fail.append(raise_)
|
||||||
|
|
||||||
|
# Passed
|
||||||
|
fmt_pass = self.helper("_format_explanation", msg)
|
||||||
|
orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")")
|
||||||
|
hook_call_pass = ast.Expr(
|
||||||
|
self.helper(
|
||||||
|
"_call_assertion_pass",
|
||||||
|
ast.Num(assert_.lineno),
|
||||||
|
ast.Str(orig),
|
||||||
|
fmt_pass,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# If any hooks implement assert_pass hook
|
||||||
|
hook_impl_test = ast.If(
|
||||||
|
self.helper("_check_if_assertion_pass_impl"),
|
||||||
|
self.expl_stmts + [hook_call_pass],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
statements_pass = [hook_impl_test]
|
||||||
|
|
||||||
|
# Test for assertion condition
|
||||||
|
main_test = ast.If(negation, statements_fail, statements_pass)
|
||||||
|
self.statements.append(main_test)
|
||||||
|
if self.format_variables:
|
||||||
|
variables = [
|
||||||
|
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||||
|
]
|
||||||
|
clear_format = ast.Assign(variables, _NameConstant(None))
|
||||||
|
self.statements.append(clear_format)
|
||||||
|
|
||||||
|
else: # Original assertion rewriting
|
||||||
|
# Create failure message.
|
||||||
|
body = self.expl_stmts
|
||||||
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
self.statements.append(ast.If(negation, body, []))
|
||||||
|
if assert_.msg:
|
||||||
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
|
explanation = "\n>assert " + explanation
|
||||||
|
else:
|
||||||
|
assertmsg = ast.Str("")
|
||||||
|
explanation = "assert " + explanation
|
||||||
|
template = ast.BinOp(assertmsg, ast.Add(), 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], [])
|
||||||
|
raise_ = ast.Raise(exc, None)
|
||||||
|
|
||||||
|
body.append(raise_)
|
||||||
|
|
||||||
# Clear temporary variables by setting them to None.
|
# Clear temporary variables by setting them to None.
|
||||||
if self.variables:
|
if self.variables:
|
||||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||||
|
@ -770,7 +850,7 @@ warn_explicit(
|
||||||
app = ast.Attribute(expl_list, "append", ast.Load())
|
app = ast.Attribute(expl_list, "append", ast.Load())
|
||||||
is_or = int(isinstance(boolop.op, ast.Or))
|
is_or = int(isinstance(boolop.op, ast.Or))
|
||||||
body = save = self.statements
|
body = save = self.statements
|
||||||
fail_save = self.on_failure
|
fail_save = self.expl_stmts
|
||||||
levels = len(boolop.values) - 1
|
levels = len(boolop.values) - 1
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# Process each operand, short-circuiting if needed.
|
# Process each operand, short-circuiting if needed.
|
||||||
|
@ -778,14 +858,14 @@ warn_explicit(
|
||||||
if i:
|
if i:
|
||||||
fail_inner = []
|
fail_inner = []
|
||||||
# cond is set in a prior loop iteration below
|
# cond is set in a prior loop iteration below
|
||||||
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
|
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||||
self.on_failure = fail_inner
|
self.expl_stmts = fail_inner
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
res, expl = self.visit(v)
|
res, expl = self.visit(v)
|
||||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||||
expl_format = self.pop_format_context(ast.Str(expl))
|
expl_format = self.pop_format_context(ast.Str(expl))
|
||||||
call = ast.Call(app, [expl_format], [])
|
call = ast.Call(app, [expl_format], [])
|
||||||
self.on_failure.append(ast.Expr(call))
|
self.expl_stmts.append(ast.Expr(call))
|
||||||
if i < levels:
|
if i < levels:
|
||||||
cond = res
|
cond = res
|
||||||
if is_or:
|
if is_or:
|
||||||
|
@ -794,7 +874,7 @@ warn_explicit(
|
||||||
self.statements.append(ast.If(cond, inner, []))
|
self.statements.append(ast.If(cond, inner, []))
|
||||||
self.statements = body = inner
|
self.statements = body = inner
|
||||||
self.statements = save
|
self.statements = save
|
||||||
self.on_failure = fail_save
|
self.expl_stmts = fail_save
|
||||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
||||||
expl = self.pop_format_context(expl_template)
|
expl = self.pop_format_context(expl_template)
|
||||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||||
|
|
|
@ -12,6 +12,10 @@ from _pytest._io.saferepr import saferepr
|
||||||
# DebugInterpreter.
|
# DebugInterpreter.
|
||||||
_reprcompare = None
|
_reprcompare = None
|
||||||
|
|
||||||
|
# Works similarly as _reprcompare attribute. Is populated with the hook call
|
||||||
|
# when pytest_runtest_setup is called.
|
||||||
|
_assertion_pass = None
|
||||||
|
|
||||||
|
|
||||||
def format_explanation(explanation):
|
def format_explanation(explanation):
|
||||||
"""This formats an explanation
|
"""This formats an explanation
|
||||||
|
|
|
@ -485,6 +485,42 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_assertion_pass(item, lineno, orig, expl):
|
||||||
|
"""
|
||||||
|
**(Experimental)**
|
||||||
|
|
||||||
|
Hook called whenever an assertion *passes*.
|
||||||
|
|
||||||
|
Use this hook to do some processing after a passing assertion.
|
||||||
|
The original assertion information is available in the `orig` string
|
||||||
|
and the pytest introspected assertion information is available in the
|
||||||
|
`expl` string.
|
||||||
|
|
||||||
|
This hook must be explicitly enabled by the ``enable_assertion_pass_hook``
|
||||||
|
ini-file option:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
enable_assertion_pass_hook=true
|
||||||
|
|
||||||
|
You need to **clean the .pyc** files in your project directory and interpreter libraries
|
||||||
|
when enabling this option, as assertions will require to be re-written.
|
||||||
|
|
||||||
|
:param _pytest.nodes.Item item: pytest item object of current test
|
||||||
|
:param int lineno: line number of the assert statement
|
||||||
|
:param string orig: string with original assertion
|
||||||
|
:param string expl: string with assert explanation
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This hook is **experimental**, so its parameters or even the hook itself might
|
||||||
|
be changed/removed without warning in any future pytest release.
|
||||||
|
|
||||||
|
If you find this hook useful, please share your feedback opening an issue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# hooks for influencing reporting (invoked from _pytest_terminal)
|
# hooks for influencing reporting (invoked from _pytest_terminal)
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
|
@ -1101,7 +1101,10 @@ def test_fixture_values_leak(testdir):
|
||||||
assert fix_of_test1_ref() is None
|
assert fix_of_test1_ref() is None
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
# Running on subprocess does not activate the HookRecorder
|
||||||
|
# which holds itself a reference to objects in case of the
|
||||||
|
# pytest_assert_reprcompare hook
|
||||||
|
result = testdir.runpytest_subprocess()
|
||||||
result.stdout.fnmatch_lines(["* 2 passed *"])
|
result.stdout.fnmatch_lines(["* 2 passed *"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,9 @@ class TestRaises:
|
||||||
assert sys.exc_info() == (None, None, None)
|
assert sys.exc_info() == (None, None, None)
|
||||||
|
|
||||||
del t
|
del t
|
||||||
|
# Make sure this does get updated in locals dict
|
||||||
|
# otherwise it could keep a reference
|
||||||
|
locals()
|
||||||
|
|
||||||
# ensure the t instance is not stuck in a cyclic reference
|
# ensure the t instance is not stuck in a cyclic reference
|
||||||
for o in gc.get_objects():
|
for o in gc.get_objects():
|
||||||
|
|
|
@ -1332,3 +1332,115 @@ class TestEarlyRewriteBailout:
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["* 1 passed in *"])
|
result.stdout.fnmatch_lines(["* 1 passed in *"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssertionPass:
|
||||||
|
def test_option_default(self, testdir):
|
||||||
|
config = testdir.parseconfig()
|
||||||
|
assert config.getini("enable_assertion_pass_hook") is False
|
||||||
|
|
||||||
|
def test_hook_call(self, testdir):
|
||||||
|
testdir.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_assertion_pass(item, lineno, orig, expl):
|
||||||
|
raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
enable_assertion_pass_hook = True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_simple():
|
||||||
|
a=1
|
||||||
|
b=2
|
||||||
|
c=3
|
||||||
|
d=0
|
||||||
|
|
||||||
|
assert a+b == c+d
|
||||||
|
|
||||||
|
# cover failing assertions with a message
|
||||||
|
def test_fails():
|
||||||
|
assert False, "assert with message"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
"*Assertion Passed: a + b == c + d (1 + 2) == (3 + 0) at line 7*"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch):
|
||||||
|
"""Assertion pass should not be called (and hence formatting should
|
||||||
|
not occur) if there is no hook declared for pytest_assertion_pass"""
|
||||||
|
|
||||||
|
def raise_on_assertionpass(*_, **__):
|
||||||
|
raise Exception("Assertion passed called when it shouldn't!")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
enable_assertion_pass_hook = True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_simple():
|
||||||
|
a=1
|
||||||
|
b=2
|
||||||
|
c=3
|
||||||
|
d=0
|
||||||
|
|
||||||
|
assert a+b == c+d
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
def test_hook_not_called_without_cmd_option(self, testdir, monkeypatch):
|
||||||
|
"""Assertion pass should not be called (and hence formatting should
|
||||||
|
not occur) if there is no hook declared for pytest_assertion_pass"""
|
||||||
|
|
||||||
|
def raise_on_assertionpass(*_, **__):
|
||||||
|
raise Exception("Assertion passed called when it shouldn't!")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_assertion_pass(item, lineno, orig, expl):
|
||||||
|
raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
enable_assertion_pass_hook = False
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_simple():
|
||||||
|
a=1
|
||||||
|
b=2
|
||||||
|
c=3
|
||||||
|
d=0
|
||||||
|
|
||||||
|
assert a+b == c+d
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
Loading…
Reference in New Issue