Repository navigation
Add --xfail-flaky to report exhausted flaky failures as xfail #378
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Add ``--xfail-flaky`` flag that reports a test as ``xfailed`` instead of | ||
| ``failed`` when it still fails after all its re-runs are exhausted. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -169,6 +169,14 @@ def pytest_addoption(parser): | |
| dest="fail_on_flaky", | ||
| help="Fail the test run with exit code 7 if a flaky test passes on a rerun.", | ||
| ) | ||
| group._addoption( | ||
| "--xfail-flaky", | ||
| action="store_true", | ||
| dest="xfail_flaky", | ||
| help="Mark tests that still fail after exhausting their reruns as " | ||
| "xfailed instead of failed. Only applies to tests that were actually " | ||
| "rerun, and not to failures excluded by --only-rerun/--rerun-except.", | ||
| ) | ||
| group._addoption( | ||
| "--rerun-show-tracebacks", | ||
| action="store_true", | ||
|
|
@@ -1324,6 +1332,19 @@ def pytest_runtest_protocol(item, nextitem): | |
| # nonmatching failure as a final result first. | ||
| continue | ||
| elif _should_not_rerun(item, report, reruns, condition): | ||
| if ( | ||
| item.session.config.option.xfail_flaky | ||
| and report.failed | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude Teardown failures are converted too, so a test is counted twice and the teardown error is hidden. This check runs on every report: setup, call and teardown. A test whose call passes but whose teardown always fails ends up as
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude Failures that come only from subtests are not converted. When a test fails only through subtests, the |
||
| and item.execution_count > reruns | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude
|
||
| and item.execution_count > 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude Under xdist, a crash on the last attempt is not converted. When a worker crashes, |
||
| and not any(item._terminal_errors.values()) | ||
| ): | ||
| # reruns are exhausted: report the final failure as xfail | ||
| report.outcome = "skipped" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude Using this with |
||
| report.wasxfail = ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude The failure reason is lost. Before, the short summary showed |
||
| f"test failed after {item.execution_count - 1} " | ||
| "rerun(s), marked as xfail" | ||
| ) | ||
| # no rerun needed or one already triggered, log normally | ||
| item.ihook.pytest_runtest_logreport(report=report) | ||
| else: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -617,6 +617,66 @@ def test_pass(): | |
| assert_outcomes(result, passed=1, rerun=2) | ||
|
|
||
|
|
||
| def test_xfail_flaky_marks_exhausted_reruns_as_xfail(testdir): | ||
| testdir.makepyfile("def test_fail(): assert False") | ||
| result = testdir.runpytest("--reruns", "2", "--xfail-flaky") | ||
| assert result.ret == 0 | ||
| assert_outcomes(result, passed=0, failed=0, xfailed=1, rerun=2) | ||
|
|
||
|
|
||
| def test_xfail_flaky_does_not_affect_eventual_pass(testdir): | ||
| testdir.makepyfile( | ||
| f""" | ||
| def test_pass(): | ||
| {temporary_failure()}""" | ||
| ) | ||
| result = testdir.runpytest("--reruns", "2", "--xfail-flaky") | ||
| assert_outcomes(result, passed=1, rerun=1) | ||
|
|
||
|
|
||
| def test_xfail_flaky_does_not_affect_test_without_reruns(testdir): | ||
| testdir.makepyfile("def test_fail(): assert False") | ||
| result = testdir.runpytest("--xfail-flaky") | ||
| assert_outcomes(result, passed=0, failed=1, xfailed=0, rerun=0) | ||
|
|
||
|
|
||
| def test_xfail_flaky_ignores_unmatched_only_rerun_error(testdir): | ||
| testdir.makepyfile("def test_fail(): raise ValueError('nope')") | ||
| result = testdir.runpytest( | ||
| "--reruns", "2", "--only-rerun", "AssertionError", "--xfail-flaky" | ||
| ) | ||
| assert_outcomes(result, passed=0, failed=1, xfailed=0, rerun=0) | ||
|
|
||
|
|
||
| def test_xfail_flaky_with_flaky_marker(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| import pytest | ||
|
|
||
| @pytest.mark.flaky(reruns=1) | ||
| def test_fail(): assert False | ||
| """ | ||
| ) | ||
| result = testdir.runpytest("--xfail-flaky") | ||
| assert result.ret == 0 | ||
| assert_outcomes(result, passed=0, failed=0, xfailed=1, rerun=1) | ||
|
|
||
|
|
||
| def test_xfail_flaky_marks_exhausted_setup_failures_as_xfail(testdir): | ||
| testdir.makepyfile( | ||
| """ | ||
| import pytest | ||
|
|
||
| @pytest.fixture | ||
| def broken_fixture(): | ||
| raise ValueError("setup always fails") | ||
|
|
||
| def test_fail(broken_fixture): pass | ||
| """ | ||
| ) | ||
| result = testdir.runpytest("--reruns", "2", "--xfail-flaky") | ||
| assert result.ret == 0 | ||
| assert_outcomes(result, passed=0, failed=0, xfailed=1, rerun=2) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. — Comment created by Claude Two blank lines are missing before |
||
| def test_flaky_marker_with_zero_reruns_disables_rerun(testdir): | ||
| testdir.makepyfile( | ||
| f""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
— Comment created by Claude
Nit: this repeats checks that
_should_not_rerunalready does (execution_count > reruns,_terminal_errors) and adds another special case to the main protocol loop. A small helper such as_mark_exhausted_failure_as_xfail(item, report, reruns)would be easier to read. The same helper could then be called from the suite-cap branch and limited to the deciding report, which also fixes the findings above.