Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pytest_before_assert hook. #4047

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ Raphael Pierzina
Raquel Alegre
Ravi Chandra
Roberto Polli
Robert Schweizer
Roland Puntaier
Romain Dorgueil
Roman Bolshakov
Expand Down
1 change: 1 addition & 0 deletions changelog/4047.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add pytest_before_assert hook to run before any assertion.
27 changes: 27 additions & 0 deletions doc/en/assert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,33 @@ the conftest file:
.. _assert-details:
.. _`assert introspection`:

Collecting information about passing assertions
-----------------------------------------------

The ``pytest_assertrepr_compare`` hook only runs for failing assertions. Information
about passing assertions can be collected with the ``pytest_before_assert`` hook.

.. autofunction:: _pytest.hookspec.pytest_before_assert
:noindex:

For example, to report every encountered assertion, the following hook
needs to be added to :ref:`conftest.py <conftest.py>`::

# content of conftest.py
def pytest_before_assert():
print("Before-assert hook is executed.")

now, given this test module::

# content of test_sample.py
def test_answer():
assert 1 == 1

the following stdout is captured, e.g. in an HTML report::

----------------------------- Captured stdout call -----------------------------
Before-assert hook is executed.

Assertion introspection details
-------------------------------

Expand Down
5 changes: 3 additions & 2 deletions doc/en/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,10 @@ test execution:

.. autofunction:: pytest_runtest_logreport

You can also use this hook to customize assertion representation for some
types:
You can also use these hooks to output assertion information or customize
assertion representation for some types:

.. autofunction:: pytest_before_assert
.. autofunction:: pytest_assertrepr_compare


Expand Down
17 changes: 12 additions & 5 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,14 @@ def pytest_collection(session):


def pytest_runtest_setup(item):
"""Setup the pytest_assertrepr_compare hook
"""Setup the pytest_assertrepr_compare and pytest_before_assert hooks.

The newinterpret and rewrite modules will use util._reprcompare if
it exists to use custom reporting via the
pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
The rewrite module will use util._reprcompare and util._before_assert
if they exist to enable custom reporting. Comparison representation is
customized via the pytest_assertrepr_compare hook. Before every assert,
the pytest_before_assert hook is run.

This sets up the custom assert hooks for the test.
"""

def callbinrepr(op, left, right):
Expand Down Expand Up @@ -137,11 +139,16 @@ def callbinrepr(op, left, right):
res = res.replace("%", "%%")
return res

def call_before_assert():
item.ihook.pytest_before_assert(config=item.config)

util._reprcompare = callbinrepr
util._before_assert = call_before_assert


def pytest_runtest_teardown(item):
util._reprcompare = None
util._before_assert = None


def pytest_sessionfinish(session):
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ def _call_reprcompare(ops, results, expls, each_obj):
return expl


def _call_before_assert():
if util._before_assert is not None:
util._before_assert()


unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}

binop_map = {
Expand Down Expand Up @@ -833,6 +838,8 @@ def visit_Assert(self, assert_):
self.stack = []
self.on_failure = []
self.push_format_context()
# Run before assert hook.
self.statements.append(ast.Expr(self.helper("call_before_assert")))
# Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test)
# If in a test module, check if directly asserting None, in order to warn [Issue #3191]
Expand Down
1 change: 1 addition & 0 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare = None
_before_assert = None


# the re-encoding is needed for python2 repr
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,13 @@ def pytest_assertrepr_compare(config, op, left, right):
"""


def pytest_before_assert(config):
""" called before every assertion is evaluated.

:param _pytest.config.Config config: pytest config object
"""


# -------------------------------------------------------------------------
# hooks for influencing reporting (invoked from _pytest_terminal)
# -------------------------------------------------------------------------
Expand Down
42 changes: 38 additions & 4 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def test_register_assert_rewrite_checks_types(self):
)


class TestBinReprIntegration(object):
class TestAssertHooksIntegration(object):
def test_pytest_assertrepr_compare_called(self, testdir):
testdir.makeconftest(
"""
Expand All @@ -315,6 +315,30 @@ def test_check(list):
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])

def test_pytest_before_assert_called(self, testdir):
testdir.makeconftest(
"""
import pytest
values = []
def pytest_before_assert():
values.append(True)

@pytest.fixture
def list(request):
return values
"""
)
testdir.makepyfile(
"""
def test_hello():
assert 0 == 1
def test_check(list):
assert list == [True, True] # The hook is run before the assert
"""
)
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])


def callequal(left, right, verbose=False):
config = mock_config()
Expand Down Expand Up @@ -962,27 +986,37 @@ def test_hello():
)


def test_assertrepr_loaded_per_dir(testdir):
def test_assert_hooks_loaded_per_dir(testdir):
testdir.makepyfile(test_base=["def test_base(): assert 1 == 2"])
a = testdir.mkdir("a")
a_test = a.join("test_a.py")
a_test.write("def test_a(): assert 1 == 2")
a_conftest = a.join("conftest.py")
a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]')
a_conftest.write(
'def pytest_before_assert(): print("assert hook a")\n'
+ 'def pytest_assertrepr_compare(): return ["summary a"]'
)
b = testdir.mkdir("b")
b_test = b.join("test_b.py")
b_test.write("def test_b(): assert 1 == 2")
b_conftest = b.join("conftest.py")
b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]')
b_conftest.write(
'def pytest_before_assert(): print("assert hook b")\n'
+ 'def pytest_assertrepr_compare(): return ["summary b"]'
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(
[
"*def test_base():*",
"*E*assert 1 == 2*",
"*def test_a():*",
"*E*assert summary a*",
"*Captured stdout call*",
"*assert hook a*",
"*def test_b():*",
"*E*assert summary b*",
"*Captured stdout call*",
"*assert hook b*",
]
)

Expand Down