diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0469540..eb2d991 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,20 +47,20 @@ jobs: run: tox env: PYTEST_MAJOR_VERSION: 9 - PYTEST_PLUGINS: pytest_github_actions_annotate_failures,xdist + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,rerunfailures,xdist - name: Run tests with PyTest 8 run: tox env: PYTEST_MAJOR_VERSION: 8 - PYTEST_PLUGINS: pytest_github_actions_annotate_failures,xdist + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,rerunfailures,xdist - name: Run tests with PyTest 7 run: tox if: runner.os != 'Windows' env: PYTEST_MAJOR_VERSION: 7 - PYTEST_PLUGINS: pytest_github_actions_annotate_failures,xdist + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,rerunfailures,xdist post-test: name: All tests passed diff --git a/plugin_test.py b/plugin_test.py index 11cea53..7184717 100644 --- a/plugin_test.py +++ b/plugin_test.py @@ -451,6 +451,86 @@ def test_fails(n): assert {*lines.values()} == {1} +def test_annotation_rerunfailures_all_fail(testdir: pytest.Testdir): + """Intermediate rerun failures should also be annotated.""" + testdir.makepyfile( + """ + import pytest + pytest_plugins = ['pytest_github_actions_annotate_failures', 'rerunfailures'] + + def test_always_fails(): + assert 0 + """ + ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess("--reruns", "2") + lines = [ + line + for line in result.errlines + if line.startswith("::error file=test_annotation_rerunfailures_all_fail.py") + ] + # 1 initial run + 2 reruns = 3 annotations + assert len(lines) == 3 + + +def test_annotation_rerunfailures_eventually_passes(testdir: pytest.Testdir): + """Failures before a test eventually passes should still be annotated.""" + testdir.makepyfile( + """ + import pytest + pytest_plugins = ['pytest_github_actions_annotate_failures', 'rerunfailures'] + + _attempt = 0 + + def test_flaky(): + global _attempt + _attempt += 1 + assert _attempt >= 2 + """ + ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess("--reruns", "2") + lines = [ + line + for line in result.errlines + if line.startswith( + "::error file=test_annotation_rerunfailures_eventually_passes.py" + ) + ] + # 1 initial failure, then passes on second attempt → 1 annotation + assert len(lines) == 1 + + +def test_with_xdist_and_rerunfailures( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch +): + """Rerun annotations are emitted when xdist and rerunfailures are used together. + + Under xdist, workers handle reruns and forward each rerun report to the + controller. The plugin is only registered on the controller, so the + controller's pytest_runtest_logreport sees both the intermediate 'rerun' + outcomes and the final 'failed' outcome — exactly once each. + """ + pytester.makepyfile( + """ + import pytest + pytest_plugins = ['pytest_github_actions_annotate_failures', 'xdist', 'rerunfailures'] + + def test_always_fails(): + assert 0 + """ + ) + monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = pytester.runpytest_subprocess("-n", "1", "--reruns", "2") + lines = [ + line + for line in result.errlines + if line.startswith("::error file=test_with_xdist_and_rerunfailures.py") + ] + # 1 initial run + 2 reruns = 3 annotations, no duplicates + assert len(lines) == 3 + + # Debugging / development tip: # Add a breakpoint() to the place you are going to check, # uncomment this example, and run it with: diff --git a/pyproject.toml b/pyproject.toml index c9e6fe9..67666f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ pytest_github_actions_annotate_failures = "pytest_github_actions_annotate_failur [dependency-groups] dev = [{ include-group = "test" }] -test = ["packaging", "pytest-xdist"] +test = ["packaging", "pytest-rerunfailures", "pytest-xdist"] [build-system] requires = [ diff --git a/pytest_github_actions_annotate_failures/plugin.py b/pytest_github_actions_annotate_failures/plugin.py index f3c6ce1..589c9cb 100644 --- a/pytest_github_actions_annotate_failures/plugin.py +++ b/pytest_github_actions_annotate_failures/plugin.py @@ -32,8 +32,9 @@ def pytest_runtest_logreport(self, report: TestReport): if os.environ.get("GITHUB_ACTIONS") != "true": return - # Only handle failed tests in call phase - if report.when == "call" and report.failed: + # Only handle failed tests in call phase. + # Also handle 'rerun' outcome set by pytest-rerunfailures on intermediate failures. + if report.when == "call" and (report.failed or report.outcome == "rerun"): filesystempath, lineno, _ = report.location if lineno is not None: diff --git a/tox.ini b/tox.ini index 40ad6a8..bff4bd0 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ PYTEST_MAJOR_VERSION = [testenv] requires = - >=4.42 + tox>=4.42 dependency_groups = test deps = pytest7: pytest>=7.0.0,<8.0.0