Skip to content

Commit 87145ee

Browse files
Annotate warnings
- Refactor workflow command generation - Skip warning annotations in Pytest < 6.0.0
1 parent b94d206 commit 87145ee

File tree

3 files changed

+187
-23
lines changed

3 files changed

+187
-23
lines changed

plugin_test.py

+100-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
import pytest
77

88

9+
PYTEST_VERSION = version.parse(pytest.__version__)
910
pytest_plugins = "pytester"
1011

1112

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

@@ -47,7 +50,9 @@ def test_fail():
4750
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
4851
result = testdir.runpytest_subprocess()
4952
result.stderr.fnmatch_lines(
50-
["::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",]
53+
[
54+
"::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",
55+
]
5156
)
5257

5358

@@ -65,8 +70,56 @@ def test_fail():
6570
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
6671
result = testdir.runpytest_subprocess()
6772
result.stderr.fnmatch_lines(
68-
["::error file=test_annotation_exception.py,line=5::test_fail*oops*",]
73+
[
74+
"::error file=test_annotation_exception.py,line=5::test_fail*oops*",
75+
]
76+
)
77+
78+
79+
@pytest.mark.skipif(
80+
PYTEST_VERSION < version.parse("6.0.0"),
81+
reason="requires pytest 6.0.0",
82+
)
83+
def test_annotation_warning(testdir):
84+
testdir.makepyfile(
85+
"""
86+
import warnings
87+
import pytest
88+
pytest_plugins = 'pytest_github_actions_annotate_failures'
89+
90+
def test_warning():
91+
warnings.warn('beware', Warning)
92+
assert 1
93+
"""
6994
)
95+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
96+
result = testdir.runpytest_subprocess()
97+
result.stderr.fnmatch_lines(
98+
[
99+
"::warning file=test_annotation_warning.py,line=6::beware",
100+
]
101+
)
102+
103+
104+
@pytest.mark.skipif(
105+
PYTEST_VERSION < version.parse("6.0.0"),
106+
reason="requires pytest 6.0.0",
107+
)
108+
def test_annotation_exclude_warnings(testdir):
109+
testdir.makepyfile(
110+
"""
111+
import warnings
112+
import pytest
113+
pytest_plugins = 'pytest_github_actions_annotate_failures'
114+
115+
def test_warning():
116+
warnings.warn('beware', Warning)
117+
assert 1
118+
"""
119+
)
120+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
121+
result = testdir.runpytest_subprocess("--exclude-warnings")
122+
assert not result.stderr.lines
70123

71124

72125
def test_annotation_third_party_exception(testdir):
@@ -90,7 +143,43 @@ def test_fail():
90143
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
91144
result = testdir.runpytest_subprocess()
92145
result.stderr.fnmatch_lines(
93-
["::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",]
146+
[
147+
"::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",
148+
]
149+
)
150+
151+
152+
@pytest.mark.skipif(
153+
PYTEST_VERSION < version.parse("6.0.0"),
154+
reason="requires pytest 6.0.0",
155+
)
156+
def test_annotation_third_party_warning(testdir):
157+
testdir.makepyfile(
158+
my_module="""
159+
import warnings
160+
161+
def fn():
162+
warnings.warn('beware', Warning)
163+
"""
164+
)
165+
166+
testdir.makepyfile(
167+
"""
168+
import pytest
169+
from my_module import fn
170+
pytest_plugins = 'pytest_github_actions_annotate_failures'
171+
172+
def test_warning():
173+
fn()
174+
"""
175+
)
176+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
177+
result = testdir.runpytest_subprocess()
178+
result.stderr.fnmatch_lines(
179+
# ["::warning file=test_annotation_third_party_warning.py,line=6::beware",]
180+
[
181+
"::warning file=my_module.py,line=4::beware",
182+
]
94183
)
95184

96185

@@ -127,7 +216,9 @@ def test_fail():
127216
testdir.makefile(".ini", pytest="[pytest]\ntestpaths=..")
128217
result = testdir.runpytest_subprocess("--rootdir=foo")
129218
result.stderr.fnmatch_lines(
130-
["::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",]
219+
[
220+
"::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",
221+
]
131222
)
132223

133224

@@ -145,7 +236,9 @@ def test_fail():
145236
testdir.monkeypatch.setenv("PYTEST_RUN_PATH", "some_path")
146237
result = testdir.runpytest_subprocess()
147238
result.stderr.fnmatch_lines(
148-
["::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",]
239+
[
240+
"::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",
241+
]
149242
)
150243

151244

pytest_github_actions_annotate_failures/plugin.py

+86-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
import os
55
import sys
6-
from collections import OrderedDict
6+
7+
from packaging import version
78

89
import pytest
910

@@ -16,6 +17,9 @@
1617
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
1718

1819

20+
PYTEST_VERSION = version.parse(pytest.__version__)
21+
22+
1923
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
2024
def pytest_runtest_makereport(item, call):
2125
# execute all other hooks to obtain the report object
@@ -71,25 +75,92 @@ def pytest_runtest_makereport(item, call):
7175
elif isinstance(report.longrepr, str):
7276
longrepr += "\n\n" + report.longrepr
7377

74-
print(
75-
_error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr
78+
workflow_command = _build_workflow_command(
79+
"error",
80+
filesystempath,
81+
lineno,
82+
message=longrepr,
7683
)
84+
print(workflow_command, file=sys.stderr)
7785

7886

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

86-
details = ",".join("{}={}".format(k, v) for k, v in details_dict.items())
94+
filesystempath = warning_message.filename
95+
workspace = os.environ.get("GITHUB_WORKFLOW")
96+
97+
if workspace:
98+
try:
99+
rel_path = os.path.relpath(filesystempath, workspace)
100+
except ValueError:
101+
# os.path.relpath() will raise ValueError on Windows
102+
# when full_path and workspace have different mount points.
103+
rel_path = filesystempath
104+
if not rel_path.startswith(".."):
105+
filesystempath = rel_path
106+
else:
107+
filesystempath = os.path.relpath(filesystempath)
108+
109+
workflow_command = _build_workflow_command(
110+
"warning",
111+
filesystempath,
112+
warning_message.lineno,
113+
message=warning_message.message.args[0],
114+
)
115+
print(workflow_command, file=sys.stderr)
116+
117+
118+
if PYTEST_VERSION >= version.parse("6.0.0"):
119+
120+
def pytest_addoption(parser):
121+
group = parser.getgroup("pytest_github_actions_annotate_failures")
122+
group.addoption(
123+
"--exclude-warnings",
124+
action="store_true",
125+
default=False,
126+
help="Annotate failures in GitHub Actions.",
127+
)
87128

88-
if longrepr is None:
89-
return "\n::error {}".format(details)
90-
else:
91-
longrepr = _escape(longrepr)
92-
return "\n::error {}::{}".format(details, longrepr)
129+
def pytest_configure(config: pytest.Config):
130+
if not config.option.exclude_warnings:
131+
config.pluginmanager.register(_AnnotateWarnings(), "annotate_warnings")
132+
133+
134+
def _build_workflow_command(
135+
command_name,
136+
file,
137+
line,
138+
end_line=None,
139+
column=None,
140+
end_column=None,
141+
title=None,
142+
message=None,
143+
):
144+
"""Build a command to annotate a workflow."""
145+
result = "::{} ".format(command_name)
146+
147+
entries = [
148+
("file", file),
149+
("line", line),
150+
("endLine", end_line),
151+
("col", column),
152+
("endColumn", end_column),
153+
("title", title),
154+
]
155+
156+
result = result + ",".join(
157+
"{}={}".format(k, v) for k, v in entries if v is not None
158+
)
159+
160+
if message is not None:
161+
result = result + "::" + _escape(message)
162+
163+
return result
93164

94165

95166
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)