Skip to content

Commit 93411ab

Browse files
Annotate warnings
- Refactor workflow command generation - Skip warning annotations in Pytest < 6.0.0
1 parent 2ce469f commit 93411ab

File tree

3 files changed

+185
-22
lines changed

3 files changed

+185
-22
lines changed

plugin_test.py

+100-7
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import pytest
66
from packaging import version
77

8+
PYTEST_VERSION = version.parse(pytest.__version__)
89
pytest_plugins = "pytester"
910

1011

1112
# result.stderr.no_fnmatch_line() is added to testdir on pytest 5.3.0
1213
# https://docs.pytest.org/en/stable/changelog.html#pytest-5-3-0-2019-11-19
1314
def no_fnmatch_line(result, pattern):
14-
if version.parse(pytest.__version__) >= version.parse("5.3.0"):
15-
result.stderr.no_fnmatch_line(pattern + "*",)
15+
if version.parse("5.3.0") <= PYTEST_VERSION:
16+
result.stderr.no_fnmatch_line(
17+
pattern + "*",
18+
)
1619
else:
1720
assert pattern not in result.stderr.str()
1821

@@ -68,7 +71,9 @@ def test_fail():
6871
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
6972
result = testdir.runpytest_subprocess()
7073
result.stderr.fnmatch_lines(
71-
["::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",]
74+
[
75+
"::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",
76+
]
7277
)
7378

7479

@@ -86,8 +91,56 @@ def test_fail():
8691
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
8792
result = testdir.runpytest_subprocess()
8893
result.stderr.fnmatch_lines(
89-
["::error file=test_annotation_exception.py,line=5::test_fail*oops*",]
94+
[
95+
"::error file=test_annotation_exception.py,line=5::test_fail*oops*",
96+
]
97+
)
98+
99+
100+
@pytest.mark.skipif(
101+
version.parse("6.0.0") > PYTEST_VERSION,
102+
reason="requires pytest 6.0.0",
103+
)
104+
def test_annotation_warning(testdir):
105+
testdir.makepyfile(
106+
"""
107+
import warnings
108+
import pytest
109+
pytest_plugins = 'pytest_github_actions_annotate_failures'
110+
111+
def test_warning():
112+
warnings.warn('beware', Warning)
113+
assert 1
114+
"""
90115
)
116+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
117+
result = testdir.runpytest_subprocess()
118+
result.stderr.fnmatch_lines(
119+
[
120+
"::warning file=test_annotation_warning.py,line=6::beware",
121+
]
122+
)
123+
124+
125+
@pytest.mark.skipif(
126+
version.parse("6.0.0") > PYTEST_VERSION,
127+
reason="requires pytest 6.0.0",
128+
)
129+
def test_annotation_exclude_warnings(testdir):
130+
testdir.makepyfile(
131+
"""
132+
import warnings
133+
import pytest
134+
pytest_plugins = 'pytest_github_actions_annotate_failures'
135+
136+
def test_warning():
137+
warnings.warn('beware', Warning)
138+
assert 1
139+
"""
140+
)
141+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
142+
result = testdir.runpytest_subprocess("--exclude-warnings")
143+
assert not result.stderr.lines
91144

92145

93146
def test_annotation_third_party_exception(testdir):
@@ -111,7 +164,43 @@ def test_fail():
111164
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
112165
result = testdir.runpytest_subprocess()
113166
result.stderr.fnmatch_lines(
114-
["::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",]
167+
[
168+
"::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",
169+
]
170+
)
171+
172+
173+
@pytest.mark.skipif(
174+
version.parse("6.0.0") > PYTEST_VERSION,
175+
reason="requires pytest 6.0.0",
176+
)
177+
def test_annotation_third_party_warning(testdir):
178+
testdir.makepyfile(
179+
my_module="""
180+
import warnings
181+
182+
def fn():
183+
warnings.warn('beware', Warning)
184+
"""
185+
)
186+
187+
testdir.makepyfile(
188+
"""
189+
import pytest
190+
from my_module import fn
191+
pytest_plugins = 'pytest_github_actions_annotate_failures'
192+
193+
def test_warning():
194+
fn()
195+
"""
196+
)
197+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
198+
result = testdir.runpytest_subprocess()
199+
result.stderr.fnmatch_lines(
200+
# ["::warning file=test_annotation_third_party_warning.py,line=6::beware",]
201+
[
202+
"::warning file=my_module.py,line=4::beware",
203+
]
115204
)
116205

117206

@@ -148,7 +237,9 @@ def test_fail():
148237
testdir.makefile(".ini", pytest="[pytest]\ntestpaths=..")
149238
result = testdir.runpytest_subprocess("--rootdir=foo")
150239
result.stderr.fnmatch_lines(
151-
["::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",]
240+
[
241+
"::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",
242+
]
152243
)
153244

154245

@@ -166,7 +257,9 @@ def test_fail():
166257
testdir.monkeypatch.setenv("PYTEST_RUN_PATH", "some_path")
167258
result = testdir.runpytest_subprocess()
168259
result.stderr.fnmatch_lines(
169-
["::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",]
260+
[
261+
"::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",
262+
]
170263
)
171264

172265

pytest_github_actions_annotate_failures/plugin.py

+84-14
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
import os
55
import sys
6-
from collections import OrderedDict
76
from typing import TYPE_CHECKING
87

98
import pytest
109
from _pytest._code.code import ExceptionRepr
10+
from packaging import version
1111

1212
if TYPE_CHECKING:
1313
from _pytest.nodes import Item
@@ -23,6 +23,9 @@
2323
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
2424

2525

26+
PYTEST_VERSION = version.parse(pytest.__version__)
27+
28+
2629
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
2730
def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
2831
# execute all other hooks to obtain the report object
@@ -79,25 +82,92 @@ def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
7982
elif isinstance(report.longrepr, str):
8083
longrepr += "\n\n" + report.longrepr
8184

82-
print(
83-
_error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr
85+
workflow_command = _build_workflow_command(
86+
"error",
87+
filesystempath,
88+
lineno,
89+
message=longrepr,
8490
)
91+
print(workflow_command, file=sys.stderr)
8592

8693

87-
def _error_workflow_command(filesystempath, lineno, longrepr):
88-
# Build collection of arguments. Ordering is strict for easy testing
89-
details_dict = OrderedDict()
90-
details_dict["file"] = filesystempath
91-
if lineno is not None:
92-
details_dict["line"] = lineno
94+
class _AnnotateWarnings:
95+
def pytest_warning_recorded(self, warning_message, when, nodeid, location): # noqa: ARG002
96+
# enable only in a workflow of GitHub Actions
97+
# ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
98+
if os.environ.get("GITHUB_ACTIONS") != "true":
99+
return
93100

94-
details = ",".join(f"{k}={v}" for k, v in details_dict.items())
101+
filesystempath = warning_message.filename
102+
workspace = os.environ.get("GITHUB_WORKFLOW")
95103

96-
if longrepr is None:
97-
return f"\n::error {details}"
104+
if workspace:
105+
try:
106+
rel_path = os.path.relpath(filesystempath, workspace)
107+
except ValueError:
108+
# os.path.relpath() will raise ValueError on Windows
109+
# when full_path and workspace have different mount points.
110+
rel_path = filesystempath
111+
if not rel_path.startswith(".."):
112+
filesystempath = rel_path
113+
else:
114+
filesystempath = os.path.relpath(filesystempath)
115+
116+
workflow_command = _build_workflow_command(
117+
"warning",
118+
filesystempath,
119+
warning_message.lineno,
120+
message=warning_message.message.args[0],
121+
)
122+
print(workflow_command, file=sys.stderr)
123+
124+
125+
if version.parse("6.0.0") <= PYTEST_VERSION:
126+
127+
def pytest_addoption(parser):
128+
group = parser.getgroup("pytest_github_actions_annotate_failures")
129+
group.addoption(
130+
"--exclude-warnings",
131+
action="store_true",
132+
default=False,
133+
help="Annotate failures in GitHub Actions.",
134+
)
98135

99-
longrepr = _escape(longrepr)
100-
return f"\n::error {details}::{longrepr}"
136+
def pytest_configure(config):
137+
if not config.option.exclude_warnings:
138+
config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings")
139+
140+
141+
def _build_workflow_command(
142+
command_name,
143+
file,
144+
line,
145+
end_line=None,
146+
column=None,
147+
end_column=None,
148+
title=None,
149+
message=None,
150+
):
151+
"""Build a command to annotate a workflow."""
152+
result = f"::{command_name} "
153+
154+
entries = [
155+
("file", file),
156+
("line", line),
157+
("endLine", end_line),
158+
("col", column),
159+
("endColumn", end_column),
160+
("title", title),
161+
]
162+
163+
result = result + ",".join(
164+
f"{k}={v}" for k, v in entries if v is not None
165+
)
166+
167+
if message is not None:
168+
result = result + "::" + _escape(message)
169+
170+
return result
101171

102172

103173
def _escape(s):

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ deps =
3131
pytest6: pytest>=6.0.0,<7.0.0
3232
pytest7: pytest>=7.0.0,<8.0.0
3333

34-
commands = {envpython} -m pytest
34+
commands = {envpython} -m pytest {posargs}
3535

3636
[testenv:pkg]
3737
skip_install = true

0 commit comments

Comments
 (0)