Skip to content

Commit

Permalink
Use pytest_runtest_logreport hook again
Browse files Browse the repository at this point in the history
Fixes #76
  • Loading branch information
amezin authored and henryiii committed May 15, 2024
1 parent 8358442 commit 4e7af6f
Showing 1 changed file with 83 additions and 64 deletions.
147 changes: 83 additions & 64 deletions pytest_github_actions_annotate_failures/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,85 @@
import os
import sys
from collections import OrderedDict
from typing import TYPE_CHECKING

import pytest
from _pytest._code.code import ExceptionRepr

if TYPE_CHECKING:
from _pytest.nodes import Item
from _pytest.reports import CollectReport
try:
from xdist import is_xdist_worker

except ImportError:
def is_xdist_worker(request_or_session):
return hasattr(request_or_session.config, "workerinput")


# Reference:
# https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
# https://docs.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example
# https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_makereport
# https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_logreport
#
# Inspired by:
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
# execute all other hooks to obtain the report object
outcome = yield
report: CollectReport = outcome.get_result()

# 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

if report.when == "call" and report.failed:
# collect information to be annotated
filesystempath, lineno, _ = report.location

runpath = os.environ.get("PYTEST_RUN_PATH")
if runpath:
filesystempath = os.path.join(runpath, filesystempath)

# try to convert to absolute path in GitHub Actions
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace:
full_path = os.path.abspath(filesystempath)
try:
rel_path = os.path.relpath(full_path, workspace)
except ValueError:
# os.path.relpath() will raise ValueError on Windows
# when full_path and workspace have different mount points.
# https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
rel_path = filesystempath
if not rel_path.startswith(".."):
filesystempath = rel_path

if lineno is not None:
# 0-index to 1-index
lineno += 1

# get the name of the current failed test, with parametrize info
longrepr = report.head_line or item.name

# 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 len(tb_entries) > 1 and tb_entries[0].reprfileloc is not None:
# Handle third-party exceptions
lineno = tb_entries[0].reprfileloc.lineno
elif report.longrepr.reprcrash is not None:
lineno = report.longrepr.reprcrash.lineno
elif isinstance(report.longrepr, tuple):
_, lineno, message = report.longrepr
longrepr += "\n\n" + message
elif isinstance(report.longrepr, str):
longrepr += "\n\n" + report.longrepr

print(
_error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr
)
class Reporter:
def pytest_runtest_logreport(self, report: pytest.TestReport):
if report.when == "call" and report.failed:
# collect information to be annotated
filesystempath, lineno, domaininfo = report.location

runpath = os.environ.get("PYTEST_RUN_PATH")
if runpath:
filesystempath = os.path.join(runpath, filesystempath)

# try to convert to absolute path in GitHub Actions
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace:
full_path = os.path.abspath(filesystempath)
try:
rel_path = os.path.relpath(full_path, workspace)
except ValueError:
# os.path.relpath() will raise ValueError on Windows
# when full_path and workspace have different mount points.
# https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
rel_path = filesystempath
if not rel_path.startswith(".."):
filesystempath = rel_path

if lineno is not None:
# 0-index to 1-index
lineno += 1

# get the name of the current failed test, with parametrize info
longrepr = getattr(report, 'head_line', None)

if not longrepr:
# BaseReport.head_line currently does this
longrepr = domaininfo

if not longrepr:
# Should not happen
longrepr = _remove_prefix(report.nodeid, f'{report.fspath}::')

# 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 len(tb_entries) > 1 and tb_entries[0].reprfileloc is not None:
# Handle third-party exceptions
lineno = tb_entries[0].reprfileloc.lineno
elif report.longrepr.reprcrash is not None:
lineno = report.longrepr.reprcrash.lineno
elif isinstance(report.longrepr, tuple):
_, lineno, message = report.longrepr
longrepr += "\n\n" + message
elif isinstance(report.longrepr, str):
longrepr += "\n\n" + report.longrepr

print(
_error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr
)


def _error_workflow_command(filesystempath, lineno, longrepr):
Expand All @@ -102,3 +103,21 @@ def _error_workflow_command(filesystempath, lineno, longrepr):

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


def _remove_prefix(s, prefix):
# Replace with built-in `.removeprefix()` when Python 3.8 support is dropped
return s[len(prefix):] if s.startswith(prefix) else s


def pytest_sessionstart(session: pytest.Session):
# 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

# print commands only from the main xdist process
if is_xdist_worker(session):
return

session.config.pluginmanager.register(Reporter())

0 comments on commit 4e7af6f

Please sign in to comment.