diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc371720417..90fce83a989 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: hooks: - id: black args: [--safe, --quiet] + additional_dependencies: [click==7.1.2] - repo: https://github.com/asottile/blacken-docs rev: v1.7.0 hooks: @@ -20,7 +21,7 @@ repos: - id: debug-statements exclude: _pytest/(debugging|hookspec).py language_version: python3 -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/pycqa/flake8 rev: 3.8.2 hooks: - id: flake8 @@ -52,7 +53,7 @@ repos: hooks: - id: rst name: rst - entry: rst-lint --encoding utf-8 + entry: rst-lint files: ^(RELEASING.rst|README.rst|TIDELIFT.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint] diff --git a/extra/setup-py.test/setup.py b/extra/setup-py.test/setup.py index d0560ce1f5f..97883852e2e 100644 --- a/extra/setup-py.test/setup.py +++ b/extra/setup-py.test/setup.py @@ -1,4 +1,5 @@ import sys + from distutils.core import setup if __name__ == "__main__": diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 335e10996a2..ad7b736fc92 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -239,20 +239,24 @@ def pytest_runtest_setup(item: Item) -> None: skip(skipped.reason) if not item.config.option.runxfail: - item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) - if xfailed and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + xfailed = evaluate_xfail_marks(item) + if xfailed: + item._store[xfailed_key] = xfailed + if not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + else: + try: + del item._store[xfailed_key] + except KeyError: + pass @hookimpl(hookwrapper=True) def pytest_runtest_call(item: Item) -> Generator[None, None, None]: xfailed = item._store.get(xfailed_key, None) - if xfailed is None: - item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) - if not item.config.option.runxfail: - if xfailed and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + if not item.config.option.runxfail and xfailed and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) yield @@ -262,6 +266,10 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): outcome = yield rep = outcome.get_result() xfailed = item._store.get(xfailed_key, None) + if not item.config.option.runxfail and call.when == "call" and xfailed is None: + xfailed = evaluate_xfail_marks(item) + if xfailed: + item._store[xfailed_key] = xfailed # unittest special case, see setting of unexpectedsuccess_key if unexpectedsuccess_key in item._store and rep.when == "call": reason = item._store[unexpectedsuccess_key] @@ -292,6 +300,20 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): rep.outcome = "passed" rep.wasxfail = xfailed.reason + if ( + not item.config.option.runxfail + and call.when == "call" + and xfailed + and not xfailed.run + and not getattr(rep, "wasxfail", None) + ): + notrun_reason = "[NOTRUN]" + if xfailed.reason: + notrun_reason += " " + xfailed.reason + rep.wasxfail = "reason: " + notrun_reason + if not rep.skipped: + rep.outcome = "skipped" + if ( item._store.get(skipped_by_mark_key, True) and rep.skipped diff --git a/testing/test_dynamic_xfail.py b/testing/test_dynamic_xfail.py new file mode 100644 index 00000000000..882da8998c7 --- /dev/null +++ b/testing/test_dynamic_xfail.py @@ -0,0 +1,53 @@ +import pytest + + +pytest_plugins = "pytester" + + +@pytest.fixture +def pytester(testdir): + return testdir + + +def _disable_plugin_autoload(pytester): + pytester.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + + +def test_dynamic_xfail_during_call(pytester): + _disable_plugin_autoload(pytester) + pytester.makepyfile( + """ + import pytest + + + def test_dynamic_xfail(request): + request.node.add_marker(pytest.mark.xfail(reason="late xfail")) + assert 0 + """ + ) + + result = pytester.runpytest("-rx") + result.assert_outcomes(xfailed=1) + result.stdout.fnmatch_lines( + ["*XFAIL*test_dynamic_xfail*", "*late xfail*"] + ) + + +def test_xfail_run_false_prevents_call(pytester): + _disable_plugin_autoload(pytester) + pytester.makepyfile( + """ + import pytest + + + @pytest.mark.xfail(run=False, reason="dismiss") + def test_not_run(): + raise AssertionError("should not run") + """ + ) + + result = pytester.runpytest("-rx") + result.assert_outcomes(xfailed=1) + result.stdout.fnmatch_lines( + ["*XFAIL*test_not_run*", " reason: [NOTRUN] dismiss"] + )