Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ If your test is running in a Docker container, you have to install this plugin a

If your tests are run from a subdirectory of the git repository, you have to set the `PYTEST_RUN_PATH` environment variable to the path of that directory relative to the repository root in order for GitHub to identify the files with errors correctly.

To keep GitHub Actions annotations concise, pass `--github-annotation-max-length=N` to truncate annotation messages longer than `N` characters. The default value is `0`, which disables truncation.

### Warning annotations

This plugin also supports warning annotations when used with Pytest 6.0+. To disable warning annotations, pass `--exclude-warning-annotations` to pytest.
Expand Down
56 changes: 55 additions & 1 deletion plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import sys
import warnings
from collections import Counter
from pathlib import Path

import pytest
from packaging import version

from pytest_github_actions_annotate_failures.plugin import _AnnotateWarnings
from pytest_github_actions_annotate_failures.plugin import (
_AnnotateWarnings,
_truncate_message,
)

PYTEST_VERSION = version.parse(pytest.__version__)
REPO_ROOT = Path(__file__).parent
pytest_plugins = "pytester"


Expand Down Expand Up @@ -393,6 +398,55 @@ def test_fail():
result.stderr.no_fnmatch_line("::*assert x += 1*")


def test_truncate_message():
assert _truncate_message("abc", 0) == "abc"
assert _truncate_message("abc", 3) == "abc"
assert _truncate_message("abcdef", 5) == "ab..."
assert _truncate_message("abcdef", 2) == ".."


def test_annotation_fail_with_max_length(
pytester: pytest.Pytester,
monkeypatch: pytest.MonkeyPatch,
):
pytester.makepyfile(
"""
import pytest

def test_fail():
assert False, "x" * 1000
"""
)
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setenv("PYTHONPATH", str(REPO_ROOT))
result = pytester.runpytest_subprocess(
"-p",
"pytest_github_actions_annotate_failures.plugin",
"--github-annotation-max-length=80",
)
lines = [line for line in result.errlines if line.startswith("::error ")]

assert len(lines) == 1
annotation_message = lines[0].split("::", 2)[2].replace("%0A", "\n")
assert len(annotation_message) == 80
assert annotation_message.endswith("...")
assert "x" * 100 not in annotation_message


def test_annotation_max_length_rejects_negative(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("PYTHONPATH", str(REPO_ROOT))
result = pytester.runpytest_subprocess(
"-p",
"pytest_github_actions_annotate_failures.plugin",
"--github-annotation-max-length=-1",
)
result.stderr.fnmatch_lines(
["ERROR: --github-annotation-max-length must be greater than or equal to 0"]
)


def test_class_method(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch):
pytester.makepyfile(
"""
Expand Down
47 changes: 43 additions & 4 deletions pytest_github_actions_annotate_failures/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

from _pytest.reports import TestReport

_ELLIPSIS = "..."


# Reference:
# https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
Expand All @@ -24,6 +26,9 @@


class _AnnotateErrors:
def __init__(self, max_annotation_length: int = 0) -> None:
self.max_annotation_length = max_annotation_length

@pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report: TestReport):
"""Handle test reporting for all pytest versions."""
Expand Down Expand Up @@ -68,7 +73,7 @@ def pytest_runtest_logreport(self, report: TestReport):
"error",
compute_path(filesystempath),
lineno,
message=longrepr,
message=_truncate_message(longrepr, self.max_annotation_length),
)
print(workflow_command, file=sys.stderr)

Expand Down Expand Up @@ -96,6 +101,9 @@ def compute_path(filesystempath: str) -> str:


class _AnnotateWarnings:
def __init__(self, max_annotation_length: int = 0) -> None:
self.max_annotation_length = max_annotation_length

def pytest_warning_recorded(
self,
warning_message: WarningMessage,
Expand All @@ -116,7 +124,9 @@ def pytest_warning_recorded(
"warning",
compute_path(filesystempath),
warning_message.lineno,
message=str(warning_message.message),
message=_truncate_message(
str(warning_message.message), self.max_annotation_length
),
)
print(workflow_command, file=sys.stderr)

Expand All @@ -129,6 +139,16 @@ def pytest_addoption(parser):
default=False,
help="Exclude annotating warnings in GitHub Actions.",
)
group.addoption(
"--github-annotation-max-length",
action="store",
default=0,
type=int,
help=(
"Maximum length of GitHub Actions annotation messages. "
"Use 0 to disable truncation."
),
)


def pytest_configure(config):
Expand All @@ -138,10 +158,19 @@ def pytest_configure(config):
if config.pluginmanager.hasplugin("xdist") and hasattr(config, "workerinput"):
return

max_annotation_length = config.option.github_annotation_max_length
if max_annotation_length < 0:
msg = "--github-annotation-max-length must be greater than or equal to 0"
raise pytest.UsageError(msg)

if not config.option.exclude_warning_annotations:
config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings")
config.pluginmanager.register(
_AnnotateWarnings(max_annotation_length), "annotate_warnings"
)

config.pluginmanager.register(_AnnotateErrors(), "annotate_errors")
config.pluginmanager.register(
_AnnotateErrors(max_annotation_length), "annotate_errors"
)


def _build_workflow_command(
Expand Down Expand Up @@ -176,3 +205,13 @@ def _build_workflow_command(

def _escape(s: str) -> str:
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")


def _truncate_message(message: str, max_length: int) -> str:
if max_length <= 0 or len(message) <= max_length:
return message

if max_length <= len(_ELLIPSIS):
return _ELLIPSIS[:max_length]

return message[: max_length - len(_ELLIPSIS)] + _ELLIPSIS
Loading