Skip to content

Commit e209a1e

Browse files
authored
Merge pull request #14 from henryiii/henryiii/short
feat: better annotation structure
2 parents 3e7600b + 81f57a4 commit e209a1e

File tree

2 files changed

+138
-31
lines changed

2 files changed

+138
-31
lines changed

plugin_test.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_fail():
4141
testdir.monkeypatch.setenv('GITHUB_ACTIONS', 'true')
4242
result = testdir.runpytest_subprocess()
4343
result.stdout.fnmatch_lines([
44-
'::error file=test_annotation_fail.py,line=4::def test_fail():*',
44+
'::error file=test_annotation_fail.py,line=5::test_fail*assert 0*',
4545
])
4646

4747
def test_annotation_exception(testdir):
@@ -58,7 +58,7 @@ def test_fail():
5858
testdir.monkeypatch.setenv('GITHUB_ACTIONS', 'true')
5959
result = testdir.runpytest_subprocess()
6060
result.stdout.fnmatch_lines([
61-
'::error file=test_annotation_exception.py,line=4::def test_fail():*',
61+
'::error file=test_annotation_exception.py,line=5::test_fail*oops*',
6262
])
6363

6464
def test_annotation_fail_disabled_outside_workflow(testdir):
@@ -73,7 +73,7 @@ def test_fail():
7373
)
7474
testdir.monkeypatch.setenv('GITHUB_ACTIONS', '')
7575
result = testdir.runpytest_subprocess()
76-
no_fnmatch_line(result, '::error file=test_annotation_fail_disabled_outside_workflow.py')
76+
no_fnmatch_line(result, '::error file=test_annotation_fail_disabled_outside_workflow.py*')
7777

7878
def test_annotation_fail_cwd(testdir):
7979
testdir.makepyfile(
@@ -91,5 +91,87 @@ def test_fail():
9191
testdir.makefile('.ini', pytest='[pytest]\ntestpaths=..')
9292
result = testdir.runpytest_subprocess('--rootdir=foo')
9393
result.stdout.fnmatch_lines([
94-
'::error file=test_annotation_fail_cwd.py,line=4::def test_fail():*',
94+
'::error file=test_annotation_fail_cwd.py,line=5::test_fail*assert 0*',
9595
])
96+
97+
def test_annotation_long(testdir):
98+
testdir.makepyfile(
99+
'''
100+
import pytest
101+
pytest_plugins = 'pytest_github_actions_annotate_failures'
102+
103+
def f(x):
104+
return x
105+
106+
def test_fail():
107+
x = 1
108+
x += 1
109+
x += 1
110+
x += 1
111+
x += 1
112+
x += 1
113+
x += 1
114+
x += 1
115+
116+
assert f(x) == 3
117+
'''
118+
)
119+
testdir.monkeypatch.setenv('GITHUB_ACTIONS', 'true')
120+
result = testdir.runpytest_subprocess()
121+
result.stdout.fnmatch_lines([
122+
'::error file=test_annotation_long.py,line=17::test_fail*assert 8 == 3*where 8 = f(8)*',
123+
])
124+
no_fnmatch_line(result, '::*assert x += 1*')
125+
126+
def test_class_method(testdir):
127+
testdir.makepyfile(
128+
'''
129+
import pytest
130+
pytest_plugins = 'pytest_github_actions_annotate_failures'
131+
132+
class TestClass(object):
133+
def test_method(self):
134+
x = 1
135+
assert x == 2
136+
'''
137+
)
138+
testdir.monkeypatch.setenv('GITHUB_ACTIONS', 'true')
139+
result = testdir.runpytest_subprocess()
140+
result.stdout.fnmatch_lines([
141+
'::error file=test_class_method.py,line=7::TestClass.test_method*assert 1 == 2*',
142+
])
143+
no_fnmatch_line(result, '::*x = 1*')
144+
145+
146+
147+
148+
def test_annotation_param(testdir):
149+
testdir.makepyfile(
150+
'''
151+
import pytest
152+
pytest_plugins = 'pytest_github_actions_annotate_failures'
153+
154+
@pytest.mark.parametrize("a", [1])
155+
@pytest.mark.parametrize("b", [2], ids=["other"])
156+
def test_param(a, b):
157+
158+
a += 1
159+
b += 1
160+
161+
assert a == b
162+
'''
163+
)
164+
testdir.monkeypatch.setenv('GITHUB_ACTIONS', 'true')
165+
result = testdir.runpytest_subprocess()
166+
result.stdout.fnmatch_lines([
167+
'::error file=test_annotation_param.py,line=11::test_param?other?1*assert 2 == 3*',
168+
])
169+
170+
# Debugging / development tip:
171+
# Add a breakpoint() to the place you are going to check,
172+
# uncomment this example, and run it with:
173+
# GITHUB_ACTIONS=true pytest -k test_example
174+
# def test_example():
175+
# x = 3
176+
# y = 4
177+
# assert x == y
Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,71 @@
11
from __future__ import print_function
22
import os
3+
import pytest
4+
from collections import OrderedDict
5+
6+
# Reference:
7+
# https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
8+
# https://docs.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example
9+
# https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_makereport
10+
#
11+
# Inspired by:
12+
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
13+
14+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
15+
def pytest_runtest_makereport(item, call):
16+
# execute all other hooks to obtain the report object
17+
outcome = yield
18+
report = outcome.get_result()
319

4-
def pytest_runtest_logreport(report):
520
# enable only in a workflow of GitHub Actions
621
# ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
722
if os.environ.get('GITHUB_ACTIONS') != 'true':
823
return
924

10-
if report.outcome != 'failed':
11-
return
25+
if report.when == "call" and report.failed:
26+
# collect information to be annotated
27+
filesystempath, lineno, _ = report.location
28+
29+
# try to convert to absolute path in GitHub Actions
30+
workspace = os.environ.get('GITHUB_WORKSPACE')
31+
if workspace:
32+
full_path = os.path.abspath(filesystempath)
33+
rel_path = os.path.relpath(full_path, workspace)
34+
if not rel_path.startswith('..'):
35+
filesystempath = rel_path
1236

13-
# collect information to be annotated
14-
filesystempath, lineno, _ = report.location
37+
# 0-index to 1-index
38+
lineno += 1
1539

16-
# try to convert to absolute path in GitHub Actions
17-
workspace = os.environ.get('GITHUB_WORKSPACE')
18-
if workspace:
19-
full_path = os.path.abspath(filesystempath)
20-
rel_path = os.path.relpath(full_path, workspace)
21-
if not rel_path.startswith('..'):
22-
filesystempath = rel_path
2340

24-
# 0-index to 1-index
25-
lineno += 1
41+
# get the name of the current failed test, with parametrize info
42+
longrepr = report.head_line or item.name
2643

27-
longrepr = str(report.longrepr)
44+
# get the error message and line number from the actual error
45+
try:
46+
longrepr += "\n\n" + report.longrepr.reprcrash.message
47+
lineno = report.longrepr.reprcrash.lineno
48+
49+
except AttributeError:
50+
pass
51+
52+
print(_error_workflow_command(filesystempath, lineno, longrepr))
2853

29-
print(_error_workflow_command(filesystempath, lineno, longrepr))
3054

3155
def _error_workflow_command(filesystempath, lineno, longrepr):
32-
if lineno is None:
33-
if longrepr is None:
34-
return '\n::error file={}'.format(filesystempath)
35-
else:
36-
longrepr = _escape(longrepr)
37-
return '\n::error file={}::{}'.format(filesystempath, longrepr)
56+
# Build collection of arguments. Ordering is strict for easy testing
57+
details_dict = OrderedDict()
58+
details_dict["file"] = filesystempath
59+
if lineno is not None:
60+
details_dict["line"] = lineno
61+
62+
details = ",".join("{}={}".format(k,v) for k,v in details_dict.items())
63+
64+
if longrepr is None:
65+
return '\n::error {}'.format(details)
3866
else:
39-
if longrepr is None:
40-
return '\n::error file={},line={}'.format(filesystempath, lineno)
41-
else:
42-
longrepr = _escape(longrepr)
43-
return '\n::error file={},line={}::{}'.format(filesystempath, lineno, longrepr)
67+
longrepr = _escape(longrepr)
68+
return '\n::error {}::{}'.format(details, longrepr)
4469

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

0 commit comments

Comments
 (0)