Skip to content

Commit 801b644

Browse files
Annotate warnings (#68)
* Annotate warnings - Refactor workflow command generation * Rename option to `--exclude-warning-annotations` * Suppress windows relpath exception
1 parent 8e001ca commit 801b644

File tree

4 files changed

+188
-37
lines changed

4 files changed

+188
-37
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ env:
1717
jobs:
1818
test:
1919
strategy:
20+
fail-fast: false
2021
matrix:
2122
os: [ubuntu-latest, windows-latest]
2223
python-version:

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,9 @@ If your test is running in a Docker container, you have to install this plugin a
3636

3737
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.
3838

39+
### Warning annotations
40+
41+
This plugin also supports warning annotations when used with Pytest 6.0+. To disable warning annotations, pass `--exclude-warning-annotations` to pytest.
42+
3943
## Screenshot
4044
[![Image from Gyazo](https://i.gyazo.com/b578304465dd1b755ceb0e04692a57d9.png)](https://gyazo.com/b578304465dd1b755ceb0e04692a57d9)

plugin_test.py

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@
55
import pytest
66
from packaging import version
77

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

1011

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

1917

20-
def test_annotation_succeed_no_output(testdir):
18+
def test_annotation_succeed_no_output(testdir: pytest.Testdir):
2119
testdir.makepyfile(
2220
"""
2321
import pytest
@@ -33,7 +31,7 @@ def test_success():
3331
no_fnmatch_line(result, "::error file=test_annotation_succeed_no_output.py")
3432

3533

36-
def test_annotation_pytest_error(testdir):
34+
def test_annotation_pytest_error(testdir: pytest.Testdir):
3735
testdir.makepyfile(
3836
"""
3937
import pytest
@@ -55,7 +53,7 @@ def test_error():
5553
)
5654

5755

58-
def test_annotation_fail(testdir):
56+
def test_annotation_fail(testdir: pytest.Testdir):
5957
testdir.makepyfile(
6058
"""
6159
import pytest
@@ -68,11 +66,13 @@ def test_fail():
6866
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
6967
result = testdir.runpytest_subprocess()
7068
result.stderr.fnmatch_lines(
71-
["::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",]
69+
[
70+
"::error file=test_annotation_fail.py,line=5::test_fail*assert 0*",
71+
]
7272
)
7373

7474

75-
def test_annotation_exception(testdir):
75+
def test_annotation_exception(testdir: pytest.Testdir):
7676
testdir.makepyfile(
7777
"""
7878
import pytest
@@ -86,11 +86,51 @@ def test_fail():
8686
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
8787
result = testdir.runpytest_subprocess()
8888
result.stderr.fnmatch_lines(
89-
["::error file=test_annotation_exception.py,line=5::test_fail*oops*",]
89+
[
90+
"::error file=test_annotation_exception.py,line=5::test_fail*oops*",
91+
]
92+
)
93+
94+
95+
def test_annotation_warning(testdir: pytest.Testdir):
96+
testdir.makepyfile(
97+
"""
98+
import warnings
99+
import pytest
100+
pytest_plugins = 'pytest_github_actions_annotate_failures'
101+
102+
def test_warning():
103+
warnings.warn('beware', Warning)
104+
assert 1
105+
"""
90106
)
107+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
108+
result = testdir.runpytest_subprocess()
109+
result.stderr.fnmatch_lines(
110+
[
111+
"::warning file=test_annotation_warning.py,line=6::beware",
112+
]
113+
)
114+
115+
116+
def test_annotation_exclude_warnings(testdir: pytest.Testdir):
117+
testdir.makepyfile(
118+
"""
119+
import warnings
120+
import pytest
121+
pytest_plugins = 'pytest_github_actions_annotate_failures'
122+
123+
def test_warning():
124+
warnings.warn('beware', Warning)
125+
assert 1
126+
"""
127+
)
128+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
129+
result = testdir.runpytest_subprocess("--exclude-warning-annotations")
130+
assert not result.stderr.lines
91131

92132

93-
def test_annotation_third_party_exception(testdir):
133+
def test_annotation_third_party_exception(testdir: pytest.Testdir):
94134
testdir.makepyfile(
95135
my_module="""
96136
def fn():
@@ -111,11 +151,43 @@ def test_fail():
111151
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
112152
result = testdir.runpytest_subprocess()
113153
result.stderr.fnmatch_lines(
114-
["::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",]
154+
[
155+
"::error file=test_annotation_third_party_exception.py,line=6::test_fail*oops*",
156+
]
157+
)
158+
159+
160+
def test_annotation_third_party_warning(testdir: pytest.Testdir):
161+
testdir.makepyfile(
162+
my_module="""
163+
import warnings
164+
165+
def fn():
166+
warnings.warn('beware', Warning)
167+
"""
168+
)
169+
170+
testdir.makepyfile(
171+
"""
172+
import pytest
173+
from my_module import fn
174+
pytest_plugins = 'pytest_github_actions_annotate_failures'
175+
176+
def test_warning():
177+
fn()
178+
"""
179+
)
180+
testdir.monkeypatch.setenv("GITHUB_ACTIONS", "true")
181+
result = testdir.runpytest_subprocess()
182+
result.stderr.fnmatch_lines(
183+
# ["::warning file=test_annotation_third_party_warning.py,line=6::beware",]
184+
[
185+
"::warning file=my_module.py,line=4::beware",
186+
]
115187
)
116188

117189

118-
def test_annotation_fail_disabled_outside_workflow(testdir):
190+
def test_annotation_fail_disabled_outside_workflow(testdir: pytest.Testdir):
119191
testdir.makepyfile(
120192
"""
121193
import pytest
@@ -132,7 +204,7 @@ def test_fail():
132204
)
133205

134206

135-
def test_annotation_fail_cwd(testdir):
207+
def test_annotation_fail_cwd(testdir: pytest.Testdir):
136208
testdir.makepyfile(
137209
"""
138210
import pytest
@@ -148,11 +220,13 @@ def test_fail():
148220
testdir.makefile(".ini", pytest="[pytest]\ntestpaths=..")
149221
result = testdir.runpytest_subprocess("--rootdir=foo")
150222
result.stderr.fnmatch_lines(
151-
["::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",]
223+
[
224+
"::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*",
225+
]
152226
)
153227

154228

155-
def test_annotation_fail_runpath(testdir):
229+
def test_annotation_fail_runpath(testdir: pytest.Testdir):
156230
testdir.makepyfile(
157231
"""
158232
import pytest
@@ -166,11 +240,13 @@ def test_fail():
166240
testdir.monkeypatch.setenv("PYTEST_RUN_PATH", "some_path")
167241
result = testdir.runpytest_subprocess()
168242
result.stderr.fnmatch_lines(
169-
["::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",]
243+
[
244+
"::error file=some_path/test_annotation_fail_runpath.py,line=5::test_fail*assert 0*",
245+
]
170246
)
171247

172248

173-
def test_annotation_long(testdir):
249+
def test_annotation_long(testdir: pytest.Testdir):
174250
testdir.makepyfile(
175251
"""
176252
import pytest
@@ -202,7 +278,7 @@ def test_fail():
202278
no_fnmatch_line(result, "::*assert x += 1*")
203279

204280

205-
def test_class_method(testdir):
281+
def test_class_method(testdir: pytest.Testdir):
206282
testdir.makepyfile(
207283
"""
208284
import pytest
@@ -224,7 +300,7 @@ def test_method(self):
224300
no_fnmatch_line(result, "::*x = 1*")
225301

226302

227-
def test_annotation_param(testdir):
303+
def test_annotation_param(testdir: pytest.Testdir):
228304
testdir.makepyfile(
229305
"""
230306
import pytest

pytest_github_actions_annotate_failures/plugin.py

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11

22
from __future__ import annotations
33

4+
import contextlib
45
import os
56
import sys
6-
from collections import OrderedDict
77
from typing import TYPE_CHECKING
88

99
import pytest
1010
from _pytest._code.code import ExceptionRepr
11+
from packaging import version
1112

1213
if TYPE_CHECKING:
1314
from _pytest.nodes import Item
@@ -23,6 +24,9 @@
2324
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
2425

2526

27+
PYTEST_VERSION = version.parse(pytest.__version__)
28+
29+
2630
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
2731
def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
2832
# execute all other hooks to obtain the report object
@@ -79,25 +83,91 @@ def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
7983
elif isinstance(report.longrepr, str):
8084
longrepr += "\n\n" + report.longrepr
8185

82-
print(
83-
_error_workflow_command(filesystempath, lineno, longrepr), file=sys.stderr
86+
workflow_command = _build_workflow_command(
87+
"error",
88+
filesystempath,
89+
lineno,
90+
message=longrepr,
8491
)
92+
print(workflow_command, file=sys.stderr)
8593

8694

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
93-
94-
details = ",".join(f"{k}={v}" for k, v in details_dict.items())
95+
class _AnnotateWarnings:
96+
def pytest_warning_recorded(self, warning_message, when, nodeid, location): # noqa: ARG002
97+
# enable only in a workflow of GitHub Actions
98+
# ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
99+
if os.environ.get("GITHUB_ACTIONS") != "true":
100+
return
95101

96-
if longrepr is None:
97-
return f"\n::error {details}"
102+
filesystempath = warning_message.filename
103+
workspace = os.environ.get("GITHUB_WORKFLOW")
98104

99-
longrepr = _escape(longrepr)
100-
return f"\n::error {details}::{longrepr}"
105+
if workspace:
106+
try:
107+
rel_path = os.path.relpath(filesystempath, workspace)
108+
except ValueError:
109+
# os.path.relpath() will raise ValueError on Windows
110+
# when full_path and workspace have different mount points.
111+
rel_path = filesystempath
112+
if not rel_path.startswith(".."):
113+
filesystempath = rel_path
114+
else:
115+
with contextlib.suppress(ValueError):
116+
filesystempath = os.path.relpath(filesystempath)
117+
118+
workflow_command = _build_workflow_command(
119+
"warning",
120+
filesystempath,
121+
warning_message.lineno,
122+
message=warning_message.message.args[0],
123+
)
124+
print(workflow_command, file=sys.stderr)
125+
126+
127+
def pytest_addoption(parser):
128+
group = parser.getgroup("pytest_github_actions_annotate_failures")
129+
group.addoption(
130+
"--exclude-warning-annotations",
131+
action="store_true",
132+
default=False,
133+
help="Annotate failures in GitHub Actions.",
134+
)
135+
136+
def pytest_configure(config):
137+
if not config.option.exclude_warning_annotations:
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):

0 commit comments

Comments
 (0)