diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1a5d97..0469540 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 + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,xdist - name: Run tests with PyTest 8 run: tox env: PYTEST_MAJOR_VERSION: 8 - PYTEST_PLUGINS: pytest_github_actions_annotate_failures + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,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 + PYTEST_PLUGINS: pytest_github_actions_annotate_failures,xdist post-test: name: All tests passed diff --git a/plugin_test.py b/plugin_test.py index d148856..53056cc 100644 --- a/plugin_test.py +++ b/plugin_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from collections import Counter import pytest from packaging import version @@ -399,6 +400,30 @@ def test(subtests): ) +def test_with_xdist(testdir: pytest.Testdir): + testdir.makepyfile( + """ + import pytest + import warnings + import sys + pytest_plugins = ['pytest_github_actions_annotate_failures','xdist'] + + @pytest.mark.parametrize("n", range(10)) + def test_fails(n): + warnings.warn(f"running {n}th") + assert n == 100 + """ + ) + testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true") + result = testdir.runpytest_subprocess("-n", "10", "-s") + lines = Counter( + line for line in result.errlines if line.startswith(("::error ", "::warning ")) + ) + + assert len(lines) == 20 + assert {*lines.values()} == {1} + + # 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 1421aad..61aa14a 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"] +test = ["packaging", "pytest-xdist"] [build-system] requires = [ diff --git a/pytest_github_actions_annotate_failures/plugin.py b/pytest_github_actions_annotate_failures/plugin.py index 061f167..130c85b 100644 --- a/pytest_github_actions_annotate_failures/plugin.py +++ b/pytest_github_actions_annotate_failures/plugin.py @@ -26,52 +26,53 @@ PYTEST_VERSION = version.parse(pytest.__version__) -@pytest.hookimpl(tryfirst=True) -def pytest_runtest_logreport(report: TestReport): - """Handle test reporting for all pytest versions.""" +class _AnnotateErrors: + @pytest.hookimpl(tryfirst=True) + def pytest_runtest_logreport(self, report: TestReport): + """Handle test reporting for all pytest versions.""" - # enable only in a workflow of GitHub Actions - # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables - if os.environ.get("GITHUB_ACTIONS") != "true": - return - - # Only handle failed tests in call phase - if report.when == "call" and report.failed: - filesystempath, lineno, _ = report.location - - if lineno is not None: - # 0-index to 1-index - lineno += 1 - - longrepr = report.head_line or "test" - - # get the error message and line number from the actual error - if isinstance(report.longrepr, ExceptionRepr): - if report.longrepr.reprcrash is not None: - longrepr += "\n\n" + report.longrepr.reprcrash.message - tb_entries = report.longrepr.reprtraceback.reprentries - if tb_entries: - entry = tb_entries[0] - # Handle third-party exceptions - if isinstance(entry, ReprEntry) and entry.reprfileloc is not None: - lineno = entry.reprfileloc.lineno - filesystempath = entry.reprfileloc.path - - elif report.longrepr.reprcrash is not None: - lineno = report.longrepr.reprcrash.lineno - elif isinstance(report.longrepr, tuple): - filesystempath, lineno, message = report.longrepr - longrepr += "\n\n" + message - elif isinstance(report.longrepr, str): - longrepr += "\n\n" + report.longrepr + # enable only in a workflow of GitHub Actions + # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables + if os.environ.get("GITHUB_ACTIONS") != "true": + return - workflow_command = _build_workflow_command( - "error", - compute_path(filesystempath), - lineno, - message=longrepr, - ) - print(workflow_command, file=sys.stderr) + # Only handle failed tests in call phase + if report.when == "call" and report.failed: + filesystempath, lineno, _ = report.location + + if lineno is not None: + # 0-index to 1-index + lineno += 1 + + longrepr = report.head_line or "test" + + # get the error message and line number from the actual error + if isinstance(report.longrepr, ExceptionRepr): + if report.longrepr.reprcrash is not None: + longrepr += "\n\n" + report.longrepr.reprcrash.message + tb_entries = report.longrepr.reprtraceback.reprentries + if tb_entries: + entry = tb_entries[0] + # Handle third-party exceptions + if isinstance(entry, ReprEntry) and entry.reprfileloc is not None: + lineno = entry.reprfileloc.lineno + filesystempath = entry.reprfileloc.path + + elif report.longrepr.reprcrash is not None: + lineno = report.longrepr.reprcrash.lineno + elif isinstance(report.longrepr, tuple): + filesystempath, lineno, message = report.longrepr + longrepr += "\n\n" + message + elif isinstance(report.longrepr, str): + longrepr += "\n\n" + report.longrepr + + workflow_command = _build_workflow_command( + "error", + compute_path(filesystempath), + lineno, + message=longrepr, + ) + print(workflow_command, file=sys.stderr) def compute_path(filesystempath: str) -> str: @@ -129,9 +130,17 @@ def pytest_addoption(parser): def pytest_configure(config): + # Plugins should not be registered for workers. + # On xdist workers the controller re-emits reports, + # so register only there to avoid duplicates. + if config.pluginmanager.hasplugin("xdist") and hasattr(config, "workerinput"): + return + if not config.option.exclude_warning_annotations: config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings") + config.pluginmanager.register(_AnnotateErrors(), "annotate_errors") + def _build_workflow_command( command_name: str, diff --git a/tox.ini b/tox.ini index 8f52ddf..c117a3d 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ PYTEST_MAJOR_VERSION = [testenv] min_version = 4.22.0 -groups = test +dependency_groups = test deps = pytest7: pytest>=7.0.0,<8.0.0 pytest8: pytest>=8.0.0,<9.0.0