Skip to content

Commit d0bfb85

Browse files
author
Alexander.Iljushkin
authored
Merge pull request #13 from frizzby/async_requests
Async requests and log attachements
2 parents 696f8e1 + f0de9b0 commit d0bfb85

File tree

4 files changed

+131
-93
lines changed

4 files changed

+131
-93
lines changed

README.rst

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ logging handler privided by plugin like bellow:
6060
.. code-block:: python
6161
6262
import logging
63-
# Import Report Portal handler to the test module.
64-
from pytest_reportportal import RPlogHandler
63+
# Import Report Portal logger and handler to the test module.
64+
from pytest_reportportal import RPLogger, RPLogHandler
6565
# Setting up a logging.
66+
logging.setLoggerClass(RPLogger)
6667
logger = logging.getLogger(__name__)
6768
logger.setLevel(logging.DEBUG)
6869
# Create handler for Report Portal.
@@ -79,6 +80,19 @@ logging handler privided by plugin like bellow:
7980
x = "this"
8081
logger.info("x is: %s", x)
8182
assert 'h' in x
83+
84+
# Message with an attachment.
85+
import subprocess
86+
free_memory = subprocess.check_output("free -h".split())
87+
logger.info(
88+
"Case1. Memory consumption",
89+
attachment={
90+
"name": "free_memory.txt",
91+
"data": free_memory,
92+
"mime": "application/octet-stream",
93+
},
94+
)
95+
8296
# This debug message will not be sent to the Report Portal.
8397
logger.debug("Case1. Debug message")
8498
@@ -98,4 +112,4 @@ Copyright Notice
98112

99113
Licensed under the GPLv3_ license (see the LICENSE file).
100114

101-
.. _GPLv3: https://www.gnu.org/licenses/quick-guide-gplv3.html
115+
.. _GPLv3: https://www.gnu.org/licenses/quick-guide-gplv3.html

pytest_reportportal/__init__.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
1+
import sys
12
import logging
23

34
from .service import PyTestService
45

56

6-
class RPlogHandler(logging.Handler):
7+
class RPLogger(logging.getLoggerClass()):
8+
9+
def __init__(self, name, level=0):
10+
super(RPLogger, self).__init__(name, level=level)
11+
12+
def _log(self, level, msg, args, exc_info=None, extra=None,
13+
attachment=None):
14+
"""
15+
Low-level logging routine which creates a LogRecord and then calls
16+
all the handlers of this logger to handle the record.
17+
"""
18+
if logging._srcfile:
19+
# IronPython doesn't track Python frames, so findCaller raises an
20+
# exception on some versions of IronPython. We trap it here so that
21+
# IronPython can use logging.
22+
try:
23+
fn, lno, func = self.findCaller()
24+
except ValueError:
25+
fn, lno, func = "(unknown file)", 0, "(unknown function)"
26+
else:
27+
fn, lno, func = "(unknown file)", 0, "(unknown function)"
28+
29+
if exc_info and not isinstance(exc_info, tuple):
30+
exc_info = sys.exc_info()
31+
32+
record = self.makeRecord(
33+
self.name, level, fn, lno, msg, args, exc_info, func, extra
34+
)
35+
record.attachment = attachment
36+
self.handle(record)
37+
38+
39+
class RPLogHandler(logging.Handler):
740

841
# Map loglevel codes from `logging` module to ReportPortal text names:
942
_loglevel_map = {
@@ -17,7 +50,7 @@ class RPlogHandler(logging.Handler):
1750
_sorted_levelnos = sorted(_loglevel_map.keys(), reverse=True)
1851

1952
def __init__(self, level=logging.NOTSET):
20-
super(RPlogHandler, self).__init__(level)
53+
super(RPLogHandler, self).__init__(level)
2154

2255
def emit(self, record):
2356
try:
@@ -31,4 +64,8 @@ def emit(self, record):
3164
if level <= record.levelno:
3265
break
3366

34-
return PyTestService.post_log(msg, loglevel=self._loglevel_map[level])
67+
return PyTestService.post_log(
68+
msg,
69+
loglevel=self._loglevel_map[level],
70+
attachment=record.__dict__.get("attachment", None),
71+
)

pytest_reportportal/plugin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ def pytest_runtest_makereport(self, item, call):
2323
report = (yield).get_result()
2424

2525
if report.longrepr:
26-
PyTestService.post_log(cgi.escape(report.longreprtext), loglevel='ERROR')
26+
PyTestService.post_log(
27+
cgi.escape(report.longreprtext),
28+
loglevel='ERROR',
29+
)
2730

2831
if report.when == "setup":
2932

@@ -91,6 +94,10 @@ def pytest_configure(config):
9194

9295

9396
def pytest_unconfigure(config):
97+
rp_launch = config.getoption("rp_launch")
98+
if rp_launch:
99+
PyTestService.terminate_service()
100+
94101
if hasattr(config, "_reporter"):
95102
reporter = config._reporter
96103
del config._reporter

pytest_reportportal/service.py

Lines changed: 66 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import logging
2+
import traceback
23
from time import time
4+
35
from six import with_metaclass
4-
from reportportal_client import (
5-
ReportPortalService, FinishExecutionRQ, StartLaunchRQ, StartTestItemRQ,
6-
FinishTestItemRQ, SaveLogRQ)
6+
7+
from reportportal_client import ReportPortalServiceAsync
8+
9+
10+
def async_error_handler(exc_info):
11+
exc, msg, tb = exc_info
12+
traceback.print_exception(exc, msg, tb)
713

814

915
def timestamp():
@@ -26,125 +32,99 @@ class PyTestServiceClass(with_metaclass(Singleton, object)):
2632

2733
def __init__(self):
2834
self.RP = None
29-
self.TEST_ITEM_STACK = []
30-
self.launch_id = None
3135

3236
def init_service(self, endpoint, project, uuid):
33-
3437
if self.RP is None:
3538
logging.debug(
3639
msg="ReportPortal - Init service: "
3740
"endpoint={0}, project={1}, uuid={2}".
3841
format(endpoint, project, uuid))
39-
self.RP = ReportPortalService(
42+
self.RP = ReportPortalServiceAsync(
4043
endpoint=endpoint,
4144
project=project,
42-
token=uuid)
45+
token=uuid,
46+
error_handler=async_error_handler)
4347
else:
4448
logging.debug("The pytest is already initialized")
4549
return self.RP
4650

51+
def terminate_service(self):
52+
if self.RP is not None:
53+
self.RP.terminate()
54+
4755
def start_launch(
48-
self, launch_name=None, mode=None, tags=None, launch=None):
56+
self, launch_name, mode=None, tags=None):
4957
# In next versions launch object(suite, testcase)
5058
# could be set as parameter
51-
sl_pt = StartLaunchRQ(
52-
name=launch_name,
53-
start_time=timestamp(),
54-
description='Pytest Launch',
55-
mode=mode,
56-
tags=tags)
57-
logging.debug(msg="ReportPortal - Start launch: "
58-
"request_body={0}".format(sl_pt.data))
59-
req_data = self.RP.start_launch(sl_pt)
60-
logging.debug(msg="ReportPortal - Launch started: "
61-
"response_body={0}".format(req_data.raw))
62-
self.launch_id = req_data.id
63-
64-
self.TEST_ITEM_STACK.append((None, "SUITE"))
65-
logging.debug(
66-
msg="ReportPortal - Stack: {0}".
67-
format(self.TEST_ITEM_STACK))
59+
sl_pt = {
60+
"name": launch_name,
61+
"start_time": timestamp(),
62+
"description": 'Pytest Launch',
63+
"mode": mode,
64+
"tags": tags
65+
}
66+
logging.debug("ReportPortal - Start launch: "
67+
"request_body=%s", sl_pt)
68+
req_data = self.RP.start_launch(**sl_pt)
69+
logging.debug("ReportPortal - Launch started: "
70+
"response_body=%s", req_data)
6871

6972
def start_pytest_item(self, test_item=None):
7073
try:
7174
# for common items
7275
item_description = test_item.function.__doc__
7376
except AttributeError:
74-
# doctest has no `function` attribute
77+
# doctest has no `function` attribute
7578
item_description = test_item.reportinfo()[2]
76-
start_rq = StartTestItemRQ(
77-
name=test_item.name,
78-
description=item_description,
79-
tags=['PyTest Item Tag'],
80-
start_time=timestamp(),
81-
launch_id=self.launch_id,
82-
type="TEST")
83-
84-
parent_item_id = self._get_top_id_from_stack()
79+
start_rq = {
80+
"name": test_item.name,
81+
"description": item_description,
82+
"tags": ['PyTest Item Tag'],
83+
"start_time": timestamp(),
84+
"item_type": "TEST"
85+
}
8586

8687
logging.debug(
87-
msg="ReportPortal - Start TestItem: "
88-
"request_body={0}, parent_item={1}".format(
89-
start_rq.data, parent_item_id))
88+
"ReportPortal - Start TestItem: "
89+
"request_body=%s", start_rq)
9090

91-
req_data = self.RP.start_test_item(
92-
parent_item_id=parent_item_id, start_test_item_rq=start_rq)
93-
94-
self.TEST_ITEM_STACK.append((req_data.id, "TEST"))
95-
logging.debug(
96-
msg="ReportPortal - Stack: {0}".
97-
format(self.TEST_ITEM_STACK))
91+
self.RP.start_test_item(**start_rq)
9892

9993
def finish_pytest_item(self, status, issue=None):
100-
fta_rq = FinishTestItemRQ(end_time=timestamp(),
101-
status=status,
102-
issue=issue)
94+
fta_rq = {
95+
"end_time": timestamp(),
96+
"status": status,
97+
"issue": issue
98+
}
10399

104-
test_item_id = self._get_top_id_from_stack()
105100
logging.debug(
106-
msg="ReportPortal - Finish TetsItem:"
107-
" request_body={0}, test_id={1}".
108-
format(fta_rq.data, test_item_id))
109-
self.RP.finish_test_item(
110-
item_id=test_item_id,
111-
finish_test_item_rq=fta_rq)
112-
self.TEST_ITEM_STACK.pop()
113-
logging.debug(
114-
msg="ReportPortal - Stack: {0}".
115-
format(self.TEST_ITEM_STACK))
101+
"ReportPortal - Finish TestItem:"
102+
" request_body=%s", fta_rq)
103+
self.RP.finish_test_item(**fta_rq)
116104

117105
def finish_launch(self, launch=None, status="rp_launch"):
118106
# TO finish launch session str parameter is needed
119-
fl_rq = FinishExecutionRQ(
120-
end_time=timestamp(),
121-
status=status)
122-
launch_id = self.launch_id
123-
logging.debug(msg="ReportPortal - Finish launch: "
124-
"request_body={0}, launch_id={1}".format(fl_rq.data,
125-
launch_id))
126-
self.RP.finish_launch(launch_id, fl_rq)
127-
self.TEST_ITEM_STACK.pop()
128-
logging.debug(
129-
msg="ReportPortal - Stack: {0}".
130-
format(self.TEST_ITEM_STACK))
131-
132-
def _get_top_id_from_stack(self):
133-
try:
134-
return self.TEST_ITEM_STACK[-1][0]
135-
except IndexError:
136-
return None
137-
138-
def post_log(self, message, loglevel='INFO'):
107+
fl_rq = {
108+
"end_time": timestamp(),
109+
"status": status
110+
}
111+
logging.debug("ReportPortal - Finish launch: "
112+
"request_body=%s", fl_rq)
113+
self.RP.finish_launch(**fl_rq)
114+
115+
def post_log(self, message, loglevel='INFO', attachment=None):
139116
if loglevel not in self._loglevels:
140-
logging.warning('Incorrect loglevel = {}. Force set to INFO. Avaliable levels: '
141-
'{}.'.format(loglevel, self._loglevels))
117+
logging.warning('Incorrect loglevel = %s. Force set to INFO. '
118+
'Avaliable levels: %s.', loglevel, self._loglevels)
142119
loglevel = 'INFO'
143120

144-
sl_rq = SaveLogRQ(item_id=self._get_top_id_from_stack(),
145-
time=timestamp(), message=message,
146-
level=loglevel)
147-
self.RP.log(sl_rq)
121+
sl_rq = {
122+
"time": timestamp(),
123+
"message": message,
124+
"level": loglevel,
125+
"attachment": attachment,
126+
}
127+
self.RP.log(**sl_rq)
148128

149129

150130
PyTestService = PyTestServiceClass()

0 commit comments

Comments
 (0)