-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathplugin.py
More file actions
184 lines (146 loc) · 5.98 KB
/
plugin.py
File metadata and controls
184 lines (146 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
from __future__ import annotations
import contextlib
import os
import sys
from typing import TYPE_CHECKING
import pytest
from _pytest._code.code import ExceptionRepr, ReprEntry
from packaging import version
if TYPE_CHECKING:
from warnings import WarningMessage
from _pytest.reports import TestReport
# 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
#
# Inspired by:
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
PYTEST_VERSION = version.parse(pytest.__version__)
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(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
workflow_command = _build_workflow_command(
"error",
compute_path(filesystempath),
lineno,
message=longrepr,
)
print(workflow_command, file=sys.stderr)
def compute_path(filesystempath: str) -> str:
"""Extract and process location information from the report."""
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.
rel_path = filesystempath
if not rel_path.startswith(".."):
filesystempath = rel_path
return filesystempath
class _AnnotateWarnings:
def pytest_warning_recorded(
self,
warning_message: WarningMessage,
when: str, # noqa: ARG002
nodeid: str, # noqa: ARG002
location: tuple[str, int, str], # noqa: ARG002
):
# 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
filesystempath = warning_message.filename
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace:
try:
rel_path = os.path.relpath(filesystempath, workspace)
except ValueError:
# os.path.relpath() will raise ValueError on Windows
# when full_path and workspace have different mount points.
rel_path = filesystempath
if not rel_path.startswith(".."):
filesystempath = rel_path
else:
with contextlib.suppress(ValueError):
filesystempath = os.path.relpath(filesystempath)
workflow_command = _build_workflow_command(
"warning",
filesystempath,
warning_message.lineno,
message=str(warning_message.message),
)
print(workflow_command, file=sys.stderr)
def pytest_addoption(parser):
group = parser.getgroup("pytest_github_actions_annotate_failures")
group.addoption(
"--exclude-warning-annotations",
action="store_true",
default=False,
help="Annotate failures in GitHub Actions.",
)
def pytest_configure(config):
if not config.option.exclude_warning_annotations:
config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings")
def _build_workflow_command(
command_name: str,
file: str,
line: int,
end_line: int | None = None,
column: int | None = None,
end_column: int | None = None,
title: str | None = None,
message: str | None = None,
):
"""Build a command to annotate a workflow."""
result = f"::{command_name} "
entries = [
("file", file),
("line", line),
("endLine", end_line),
("col", column),
("endColumn", end_column),
("title", title),
]
result = result + ",".join(f"{k}={v}" for k, v in entries if v is not None)
if message is not None:
result = result + "::" + _escape(message)
return result
def _escape(s: str) -> str:
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")