Skip to content

Commit f59c7b3

Browse files
authored
Merge pull request #203 from reportportal/develop
Release
2 parents ad9e830 + ec64161 commit f59c7b3

File tree

8 files changed

+104
-37
lines changed

8 files changed

+104
-37
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Changelog
22

33
## [Unreleased]
4+
5+
## [5.5.7]
46
### Added
57
- `Test Case ID` reporting on Item Finish, by @HardNorth
68
### Changed

examples/library/Log.py

+3-19
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,19 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
import os
18-
1917
from robotframework_reportportal import logger
2018

2119

22-
def screenshot_log(level, message, screenshot_file):
23-
"""
24-
Attach a screenshot file into a log entry on ReportPortal.
25-
26-
:param level: log entry level
27-
:param message: screenshot description
28-
:param screenshot_file: path to image file
29-
"""
30-
with open(screenshot_file, "rb") as image_file:
31-
file_data = image_file.read()
32-
item_log(level, message, {"name": screenshot_file.split(os.path.sep)[-1],
33-
"data": file_data,
34-
"mime": "image/png"})
35-
36-
37-
def item_log(level, message, attachment=None):
20+
def item_log(level, message, attachment=None, html=False):
3821
"""
3922
Post a log entry on which will be attached to the current processing item.
4023
4124
:param level: log entry level
4225
:param message: message to post
4326
:param attachment: path to attachment file
27+
:param html: format or not format the message as html
4428
"""
45-
logger.write(message, level, attachment=attachment)
29+
logger.write(message, level, attachment=attachment, html=html)
4630

4731

4832
def launch_log(level, message, attachment=None):
Loading
30.1 KB
Loading

examples/screenshot.robot

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*** Settings ***
2+
Library library/Log.py
3+
4+
*** Test Cases ***
5+
Selenium Screenshot test
6+
Item Log INFO </td></tr><tr><td colspan="3"><a href="examples/res/selenium-screenshot-1.png"><img src="examples/res/selenium-screenshot-1.png" width="800px"></a> None True
7+
Playwright Screenshot test
8+
Item Log INFO </td></tr><tr><td colspan="3"><a href="examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png" target="_blank"><img src="examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png" width="800px"/></a> None True

robotframework_reportportal/listener.py

+46-17
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
from robotframework_reportportal.variables import Variables
3232

3333
logger = logging.getLogger(__name__)
34-
VARIABLE_PATTERN = r'^\s*\${[^}]*}\s*=\s*'
34+
VARIABLE_PATTERN = re.compile(r'^\s*\${[^}]*}\s*=\s*')
35+
IMAGE_PATTERN = re.compile(
36+
r'</td></tr><tr><td colspan="\d+"><a href="[^"]+"(?: target="_blank")?>'
37+
r'<img src="([^"]+)" width="\d+(?:px|pt)?"/?></a>')
38+
39+
DEFAULT_BINARY_FILE_TYPE = 'application/octet-stream'
3540
TRUNCATION_SIGN = "...'"
3641

3742

@@ -98,7 +103,7 @@ def __init__(self) -> None:
98103
self._service = None
99104
self._variables = None
100105

101-
def _build_msg_struct(self, message: Dict) -> LogMessage:
106+
def _build_msg_struct(self, message: Dict[str, Any]) -> LogMessage:
102107
"""Check if the given message comes from our custom logger or not.
103108
104109
:param message: Message passed by the Robot Framework
@@ -110,6 +115,44 @@ def _build_msg_struct(self, message: Dict) -> LogMessage:
110115
msg.level = message['level']
111116
if not msg.launch_log:
112117
msg.item_id = getattr(self.current_item, 'rp_item_id', None)
118+
119+
message_str = msg.message
120+
if is_binary(message_str):
121+
variable_match = VARIABLE_PATTERN.search(message_str)
122+
if variable_match:
123+
# Treat as partial binary data
124+
msg_content = message_str[variable_match.end():]
125+
# remove trailing `'"...`, add `...'`
126+
msg.message = (message_str[variable_match.start():variable_match.end()]
127+
+ str(msg_content.encode('utf-8'))[:-5] + TRUNCATION_SIGN)
128+
else:
129+
# Do not log full binary data, since it's usually corrupted
130+
content_type = guess_content_type_from_bytes(_unescape(message_str, 128))
131+
msg.message = (f'Binary data of type "{content_type}" logging skipped, as it was processed as text and'
132+
' hence corrupted.')
133+
msg.level = 'WARN'
134+
elif message.get('html', 'no') == 'yes':
135+
image_match = IMAGE_PATTERN.match(message_str)
136+
if image_match:
137+
image_path = image_match.group(1)
138+
msg.message = f'Image attached: {image_path}'
139+
if os.path.exists(image_path):
140+
image_type_by_name = guess_type(image_path)[0]
141+
with open(image_path, 'rb') as fh:
142+
image_data = fh.read()
143+
image_type_by_data = guess_content_type_from_bytes(image_data)
144+
if image_type_by_name and image_type_by_data and image_type_by_name != image_type_by_data:
145+
logger.warning(
146+
f'Image type mismatch: type by file name "{image_type_by_name}" '
147+
f'!= type by file content "{image_type_by_data}"')
148+
mime_type = DEFAULT_BINARY_FILE_TYPE
149+
else:
150+
mime_type = image_type_by_name or image_type_by_data or DEFAULT_BINARY_FILE_TYPE
151+
msg.attachment = {
152+
'name': os.path.basename(image_path),
153+
'data': image_data,
154+
'mime': mime_type
155+
}
113156
return msg
114157

115158
def _add_current_item(self, item: Union[Keyword, Launch, Suite, Test]) -> None:
@@ -132,20 +175,6 @@ def log_message(self, message: Dict) -> None:
132175
:param message: Message passed by the Robot Framework
133176
"""
134177
msg = self._build_msg_struct(message)
135-
if is_binary(msg.message):
136-
variable_match = re.search(VARIABLE_PATTERN, msg.message)
137-
if variable_match:
138-
# Treat as partial binary data
139-
msg_content = msg.message[variable_match.end():]
140-
# remove trailing `'"...`, add `...'`
141-
msg.message = (msg.message[variable_match.start():variable_match.end()]
142-
+ str(msg_content.encode('utf-8'))[:-5] + TRUNCATION_SIGN)
143-
else:
144-
# Do not log full binary data, since it's usually corrupted
145-
content_type = guess_content_type_from_bytes(_unescape(msg.message, 128))
146-
msg.message = (f'Binary data of type "{content_type}" logging skipped, as it was processed as text and'
147-
' hence corrupted.')
148-
msg.level = 'WARN'
149178
logger.debug(f'ReportPortal - Log Message: {message}')
150179
self.service.log(message=msg)
151180

@@ -161,7 +190,7 @@ def log_message_with_image(self, msg: Dict, image: str):
161190
mes.attachment = {
162191
'name': os.path.basename(image),
163192
'data': fh.read(),
164-
'mime': guess_type(image)[0] or 'application/octet-stream'
193+
'mime': guess_type(image)[0] or DEFAULT_BINARY_FILE_TYPE
165194
}
166195
logger.debug(f'ReportPortal - Log Message with Image: {mes} {image}')
167196
self.service.log(message=mes)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from setuptools import setup
1919

2020

21-
__version__ = '5.5.7'
21+
__version__ = '5.5.8'
2222

2323

2424
def read_file(fname):

tests/integration/test_screenshot.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2023 EPAM Systems
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from tests.helpers import utils
16+
from unittest import mock
17+
18+
from tests import REPORT_PORTAL_SERVICE
19+
20+
EXAMPLE_TEST = 'examples/screenshot.robot'
21+
SELENIUM_SCREENSHOT = 'examples/res/selenium-screenshot-1.png'
22+
PLAYWRIGHT_SCREENSHOT = 'examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png'
23+
SCREENSHOTS = [SELENIUM_SCREENSHOT, PLAYWRIGHT_SCREENSHOT]
24+
25+
26+
@mock.patch(REPORT_PORTAL_SERVICE)
27+
def test_screenshot_log(mock_client_init):
28+
result = utils.run_robot_tests([EXAMPLE_TEST])
29+
assert result == 0 # the test successfully passed
30+
31+
mock_client = mock_client_init.return_value
32+
calls = utils.get_log_calls(mock_client)
33+
assert len(calls) == 2
34+
35+
for i, call in enumerate(calls):
36+
message = call[1]['message']
37+
assert message == f'Image attached: {SCREENSHOTS[i]}'
38+
39+
attachment = call[1]['attachment']
40+
41+
assert attachment['name'] == SCREENSHOTS[i].split('/')[-1]
42+
assert attachment['mime'] == 'image/png'
43+
with open(SCREENSHOTS[i], 'rb') as file:
44+
assert attachment['data'] == file.read()

0 commit comments

Comments
 (0)