Skip to content

Commit 59e55b9

Browse files
author
Dmitriy Gumeniuk
authored
Merge pull request #147 from bigbZik/issue-id-marks
Adding support for issue-id marks
2 parents 9ac4a91 + 3535ed1 commit 59e55b9

File tree

5 files changed

+213
-42
lines changed

5 files changed

+213
-42
lines changed

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The following parameters are optional:
7878
- :code:`rp_hierarchy_dirs_level = 0` - Directory starting hierarchy level (from pytest.ini level) (default `0`)
7979
- :code:`rp_issue_marks = 'xfail' 'issue'` - Pytest marks that could be used to get issue information (id, type, reason)
8080
- :code:`rp_issue_system_url = http://bugzilla.some.com/show_bug.cgi?id={%issue_id}` - issue URL (issue_id will be filled by parameter from pytest mark)
81+
- :code:`rp_issue_id_marks = True` - Enables adding marks for issue ids (e.g. "issue:123456")
8182
- :code:`rp_verify_ssl = True` - Verify SSL when connecting to the server
8283
- :code:`rp_display_suite_test_file = True` In case of True, include the suite's relative file path in the launch name as a convention of "<RELATIVE_FILE_PATH>::<SUITE_NAME>". In case of False, set the launch name to be the suite name only - this flag is relevant only when "rp_hierarchy_module" flag is set to False
8384

pytest_reportportal/listener.py

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ def __init__(self, py_test_service,
3232

3333
@pytest.hookimpl(hookwrapper=True)
3434
def pytest_runtest_protocol(self, item):
35+
# Adding issues id marks to the test item
36+
# if client doesn't support item updates
37+
update_supported = self.PyTestService.is_item_update_supported()
38+
if not update_supported:
39+
self._add_issue_id_marks(item)
40+
3541
self.PyTestService.start_pytest_item(item)
3642
if PYTEST_HAS_LOGGING_PLUGIN:
3743
# This check can go away once we support pytest >= 3.3
@@ -41,6 +47,12 @@ def pytest_runtest_protocol(self, item):
4147
yield
4248
else:
4349
yield
50+
# Updating item in RP (tags and description)
51+
# if client supports
52+
if update_supported:
53+
self._add_issue_id_marks(item)
54+
self.PyTestService.update_pytest_item(item)
55+
# Finishing item in RP
4456
self.PyTestService.finish_pytest_item(item, self.result or 'SKIPPED', self.issue or None)
4557

4658
@pytest.hookimpl(hookwrapper=True)
@@ -53,67 +65,88 @@ def pytest_runtest_makereport(self, item):
5365
loglevel='ERROR',
5466
)
5567

68+
# Defining test result
5669
if report.when == 'setup':
5770
self.result = None
5871
self.issue = {}
59-
if report.failed:
60-
# This happens for example when a fixture fails to run
61-
# causing the test to error
62-
self.result = 'FAILED'
63-
self._add_issue_info(item, report)
64-
elif report.skipped:
65-
# This happens when a testcase is marked "skip". It will
66-
# show in reportportal as not requiring investigation.
72+
73+
if report.failed:
74+
self.result = 'FAILED'
75+
elif report.skipped:
76+
if self.result in (None, 'PASSED'):
6777
self.result = 'SKIPPED'
68-
self._add_issue_info(item, report)
78+
else:
79+
if self.result is None:
80+
self.result = 'PASSED'
6981

70-
if report.when == 'call':
71-
if report.passed:
72-
item_result = 'PASSED'
73-
elif report.skipped:
74-
item_result = 'SKIPPED'
75-
self._add_issue_info(item, report)
76-
else:
77-
item_result = 'FAILED'
78-
self._add_issue_info(item, report)
79-
self.result = item_result
82+
# Adding test comment and issue type
83+
self._add_issue_info(item, report)
8084

85+
def _add_issue_id_marks(self, item):
86+
"""Add marks with issue id.
87+
88+
:param item: pytest test item
89+
"""
90+
issue_marks = item.session.config.getini('rp_issue_marks')
91+
if item.session.config.getini('rp_issue_id_marks'):
92+
for mark_name in issue_marks:
93+
for mark in item.iter_markers(name=mark_name):
94+
if mark:
95+
issue_ids = mark.kwargs.get("issue_id", [])
96+
if not isinstance(issue_ids, list):
97+
issue_ids = [issue_ids]
98+
for issue_id in issue_ids:
99+
mark_issue = "{}:{}".format(mark.name, issue_id)
100+
try:
101+
pytest.mark._markers.add(mark_issue) # register mark in pytest,
102+
except AttributeError: # for pytest >= 4.5.0
103+
pass
104+
item.add_marker(mark_issue)
81105

82106
def _add_issue_info(self, item, report):
107+
"""Add issues description and issue_type to the test item.
83108
84-
issue_type = None
85-
comment = ""
109+
:param item: pytest test item
110+
:param report: pytest report instance
111+
"""
86112
url = item.session.config.getini('rp_issue_system_url')
87113
issue_marks = item.session.config.getini('rp_issue_marks')
114+
issue_type = None
115+
comment = ""
88116

89117
for mark_name in issue_marks:
90-
try:
91-
mark = item.get_closest_marker(mark_name)
92-
except AttributeError:
93-
# pytest < 3.6
94-
mark = item.get_marker(mark_name)
95-
96-
if mark:
97-
if "reason" in mark.kwargs:
98-
comment += "\n" if comment else ""
99-
comment += mark.kwargs["reason"]
100-
if "issue_id" in mark.kwargs:
101-
issue_ids = mark.kwargs["issue_id"]
102-
if not isinstance(issue_ids, list):
103-
issue_ids = [issue_ids]
104-
comment += "\n" if comment else ""
105-
comment += "Issues:"
118+
for mark in item.iter_markers(name=mark_name):
119+
if not mark:
120+
continue
121+
122+
mark_comment = ""
123+
mark_url = mark.kwargs.get("url", None) or url
124+
issue_ids = mark.kwargs.get("issue_id", [])
125+
if not isinstance(issue_ids, list):
126+
issue_ids = [issue_ids]
127+
128+
if issue_ids:
129+
mark_comment = mark.kwargs.get("reason", mark.name)
130+
mark_comment += ":"
106131
for issue_id in issue_ids:
107-
template = (" [{issue_id}]" + "({})".format(url)) if url else " {issue_id}"
108-
comment += template.format(issue_id=issue_id)
132+
issue_url = mark_url.format(issue_id=issue_id) if mark_url else None
133+
template = " [{issue_id}]({url})" if issue_url else " {issue_id}"
134+
mark_comment += template.format(issue_id=issue_id, url=issue_url)
135+
elif "reason" in mark.kwargs:
136+
mark_comment = mark.kwargs["reason"]
137+
138+
if mark_comment:
139+
comment += ("\n* " if comment else "* ") + mark_comment
109140

110-
if "issue_type" in mark.kwargs:
141+
# Set issue_type only for first issue mark
142+
if "issue_type" in mark.kwargs and issue_type is None:
111143
issue_type = mark.kwargs["issue_type"]
112144

145+
# default value
113146
issue_type = "TI" if issue_type is None else issue_type
114147

115-
if issue_type and getattr(self.PyTestService, 'issue_types', False) \
116-
and (issue_type in self.PyTestService.issue_types):
148+
if issue_type and \
149+
(issue_type in getattr(self.PyTestService, 'issue_types', ())):
117150
if comment:
118151
self.issue['comment'] = comment
119152
self.issue['issue_type'] = self.PyTestService.issue_types[issue_type]

pytest_reportportal/plugin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,12 @@ def pytest_addoption(parser):
317317
"'<RELATIVE_FILE_PATH>::<SUITE_NAME>'. In case of False, set the launch name to be the suite name "
318318
"only - this flag is relevant only when 'rp_hierarchy_module' flag is set to False")
319319

320+
parser.addini(
321+
'rp_issue_id_marks',
322+
type='bool',
323+
default=True,
324+
help='Adding tag with issue id to the test')
325+
320326
parser.addini(
321327
'retries',
322328
default='0',

pytest_reportportal/service.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ def start_pytest_item(self, test_item=None):
240240
log.debug('ReportPortal - Start TestItem: request_body=%s', start_rq)
241241
self.RP.start_test_item(**start_rq)
242242

243+
def is_item_update_supported(self):
244+
"""Check item update API call client support."""
245+
return hasattr(self.RP, "update_test_item")
246+
247+
def update_pytest_item(self, test_item=None):
248+
"""Make item update API call.
249+
250+
:param test_item: pytest test item
251+
"""
252+
self._stop_if_necessary()
253+
if self.RP is None:
254+
return
255+
256+
# if update_test_item is not supported in client
257+
if not self.is_item_update_supported():
258+
log.debug('ReportPortal - Update TestItem: method is not defined')
259+
return
260+
261+
start_rq = {
262+
'description': self._get_item_description(test_item),
263+
'tags': self._get_item_tags(test_item),
264+
}
265+
log.debug('ReportPortal - Update TestItem: request_body=%s', start_rq)
266+
self.RP.update_test_item(**start_rq)
267+
243268
def finish_pytest_item(self, test_item, status, issue=None):
244269
self._stop_if_necessary()
245270
if self.RP is None:

tests/test_plugin.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import pytest
1111
from requests.exceptions import RequestException
1212

13+
from pytest_reportportal.listener import RPReportListener
1314
from pytest_reportportal.plugin import pytest_configure
15+
from pytest_reportportal.service import PyTestServiceClass
1416
from pytest_reportportal import RPLogger
1517

1618

@@ -63,3 +65,107 @@ def test_logger_handle_no_attachment(mock_handler, logger, log_level):
6365
expect(getattr(mock_handler.call_args[0][0], "attachment") is None,
6466
"record.attachment in args is not None")
6567
assert_expectations()
68+
69+
70+
def test_pytest_runtest_protocol(request):
71+
"""Test listener pytest_runtest_protocol hook."""
72+
rp_service = Mock()
73+
rp_service.is_item_update_supported = Mock(return_value=False)
74+
rp_listener = RPReportListener(rp_service)
75+
rp_listener._add_issue_id_marks = Mock()
76+
test_item = Mock()
77+
78+
next(rp_listener.pytest_runtest_protocol(test_item))
79+
80+
expect(rp_listener._add_issue_id_marks.call_count == 1,
81+
"_add_issue_id_marks called more than 1 time")
82+
assert_expectations()
83+
84+
85+
@patch('reportportal_client.service.ReportPortalService.get_project_settings')
86+
def test_is_item_update_supported(request):
87+
"""Test listener public is_client_support_item_update method."""
88+
func = None
89+
rp_service = PyTestServiceClass()
90+
rp_service.init_service("endpoint", "project", "uuid", 20, False, [])
91+
92+
if hasattr(rp_service.RP, "update_test_item"):
93+
rp_service.RP.supported_methods.remove("update_test_item")
94+
func = rp_service.RP.update_test_item
95+
delattr(type(rp_service.RP), "update_test_item")
96+
97+
98+
result = rp_service.is_item_update_supported()
99+
expect(result == False,
100+
"incorrect result for is_client_support_item_update method")
101+
102+
rp_service.RP.update_test_item = func
103+
rp_service.RP.supported_methods.append("update_test_item")
104+
105+
result = rp_service.is_item_update_supported()
106+
expect(result == True,
107+
"incorrect result for is_client_support_item_update method")
108+
assert_expectations()
109+
110+
111+
def test_add_issue_info(request):
112+
"""Test listener helper _add_issue_info method."""
113+
rp_service = Mock()
114+
rp_listener = RPReportListener(rp_service)
115+
rp_service.issue_types = {"TST": "TEST"}
116+
117+
report = Mock()
118+
report.when = "call"
119+
report.skipped = False
120+
121+
def getini(option):
122+
if option == "rp_issue_system_url":
123+
return "https://bug.com/{issue_id}"
124+
elif option == "rp_issue_marks":
125+
return ["issue"]
126+
return None
127+
128+
def iter_markers(name=None):
129+
for mark in [pytest.mark.issue(issue_id="456823", issue_type="TST")]:
130+
yield mark
131+
132+
test_item = Mock()
133+
test_item.session.config.getini = getini
134+
test_item.iter_markers = iter_markers
135+
136+
rp_listener._add_issue_info(test_item, report)
137+
138+
expect(rp_listener.issue['issue_type'] == "TEST",
139+
"incorrect test issue_type")
140+
expect(rp_listener.issue['comment'] == "* issue: [456823](https://bug.com/456823)",
141+
"incorrect test comment")
142+
assert_expectations()
143+
144+
145+
def test_add_issue_id_marks(request):
146+
"""Test listener helper _add_issue_id_marks method."""
147+
rp_service = Mock()
148+
rp_listener = RPReportListener(rp_service)
149+
150+
def getini(option):
151+
if option == "rp_issue_id_marks":
152+
return True
153+
elif option == "rp_issue_marks":
154+
return ["issue"]
155+
return None
156+
157+
def iter_markers(name=None):
158+
for mark in [pytest.mark.issue(issue_id="456823")]:
159+
yield mark
160+
161+
test_item = Mock()
162+
test_item.session.config.getini = getini
163+
test_item.iter_markers = iter_markers
164+
165+
rp_listener._add_issue_id_marks(test_item)
166+
167+
expect(test_item.add_marker.call_count == 1,
168+
"item.add_marker called more than 1 time")
169+
expect(test_item.add_marker.call_args[0][0] == "issue:456823",
170+
"item.add_marker called with incorrect parameters")
171+
assert_expectations()

0 commit comments

Comments
 (0)