diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eabec2..d153312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [5.5.7] ### Added - `Test Case ID` reporting on Item Finish, by @HardNorth ### Changed diff --git a/examples/library/Log.py b/examples/library/Log.py index a120a9b..30e35cb 100644 --- a/examples/library/Log.py +++ b/examples/library/Log.py @@ -14,35 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - from robotframework_reportportal import logger -def screenshot_log(level, message, screenshot_file): - """ - Attach a screenshot file into a log entry on ReportPortal. - - :param level: log entry level - :param message: screenshot description - :param screenshot_file: path to image file - """ - with open(screenshot_file, "rb") as image_file: - file_data = image_file.read() - item_log(level, message, {"name": screenshot_file.split(os.path.sep)[-1], - "data": file_data, - "mime": "image/png"}) - - -def item_log(level, message, attachment=None): +def item_log(level, message, attachment=None, html=False): """ Post a log entry on which will be attached to the current processing item. :param level: log entry level :param message: message to post :param attachment: path to attachment file + :param html: format or not format the message as html """ - logger.write(message, level, attachment=attachment) + logger.write(message, level, attachment=attachment, html=html) def launch_log(level, message, attachment=None): diff --git a/examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png b/examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png new file mode 100644 index 0000000..d4acbf3 Binary files /dev/null and b/examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png differ diff --git a/examples/res/selenium-screenshot-1.png b/examples/res/selenium-screenshot-1.png new file mode 100644 index 0000000..62539c7 Binary files /dev/null and b/examples/res/selenium-screenshot-1.png differ diff --git a/examples/screenshot.robot b/examples/screenshot.robot new file mode 100644 index 0000000..c77a4c1 --- /dev/null +++ b/examples/screenshot.robot @@ -0,0 +1,8 @@ +*** Settings *** +Library library/Log.py + +*** Test Cases *** +Selenium Screenshot test + Item Log INFO None True +Playwright Screenshot test + Item Log INFO None True diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index 3cdc8ea..c65f9db 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -31,7 +31,12 @@ from robotframework_reportportal.variables import Variables logger = logging.getLogger(__name__) -VARIABLE_PATTERN = r'^\s*\${[^}]*}\s*=\s*' +VARIABLE_PATTERN = re.compile(r'^\s*\${[^}]*}\s*=\s*') +IMAGE_PATTERN = re.compile( + r'' + r'') + +DEFAULT_BINARY_FILE_TYPE = 'application/octet-stream' TRUNCATION_SIGN = "...'" @@ -98,7 +103,7 @@ def __init__(self) -> None: self._service = None self._variables = None - def _build_msg_struct(self, message: Dict) -> LogMessage: + def _build_msg_struct(self, message: Dict[str, Any]) -> LogMessage: """Check if the given message comes from our custom logger or not. :param message: Message passed by the Robot Framework @@ -110,6 +115,44 @@ def _build_msg_struct(self, message: Dict) -> LogMessage: msg.level = message['level'] if not msg.launch_log: msg.item_id = getattr(self.current_item, 'rp_item_id', None) + + message_str = msg.message + if is_binary(message_str): + variable_match = VARIABLE_PATTERN.search(message_str) + if variable_match: + # Treat as partial binary data + msg_content = message_str[variable_match.end():] + # remove trailing `'"...`, add `...'` + msg.message = (message_str[variable_match.start():variable_match.end()] + + str(msg_content.encode('utf-8'))[:-5] + TRUNCATION_SIGN) + else: + # Do not log full binary data, since it's usually corrupted + content_type = guess_content_type_from_bytes(_unescape(message_str, 128)) + msg.message = (f'Binary data of type "{content_type}" logging skipped, as it was processed as text and' + ' hence corrupted.') + msg.level = 'WARN' + elif message.get('html', 'no') == 'yes': + image_match = IMAGE_PATTERN.match(message_str) + if image_match: + image_path = image_match.group(1) + msg.message = f'Image attached: {image_path}' + if os.path.exists(image_path): + image_type_by_name = guess_type(image_path)[0] + with open(image_path, 'rb') as fh: + image_data = fh.read() + image_type_by_data = guess_content_type_from_bytes(image_data) + if image_type_by_name and image_type_by_data and image_type_by_name != image_type_by_data: + logger.warning( + f'Image type mismatch: type by file name "{image_type_by_name}" ' + f'!= type by file content "{image_type_by_data}"') + mime_type = DEFAULT_BINARY_FILE_TYPE + else: + mime_type = image_type_by_name or image_type_by_data or DEFAULT_BINARY_FILE_TYPE + msg.attachment = { + 'name': os.path.basename(image_path), + 'data': image_data, + 'mime': mime_type + } return msg def _add_current_item(self, item: Union[Keyword, Launch, Suite, Test]) -> None: @@ -132,20 +175,6 @@ def log_message(self, message: Dict) -> None: :param message: Message passed by the Robot Framework """ msg = self._build_msg_struct(message) - if is_binary(msg.message): - variable_match = re.search(VARIABLE_PATTERN, msg.message) - if variable_match: - # Treat as partial binary data - msg_content = msg.message[variable_match.end():] - # remove trailing `'"...`, add `...'` - msg.message = (msg.message[variable_match.start():variable_match.end()] - + str(msg_content.encode('utf-8'))[:-5] + TRUNCATION_SIGN) - else: - # Do not log full binary data, since it's usually corrupted - content_type = guess_content_type_from_bytes(_unescape(msg.message, 128)) - msg.message = (f'Binary data of type "{content_type}" logging skipped, as it was processed as text and' - ' hence corrupted.') - msg.level = 'WARN' logger.debug(f'ReportPortal - Log Message: {message}') self.service.log(message=msg) @@ -161,7 +190,7 @@ def log_message_with_image(self, msg: Dict, image: str): mes.attachment = { 'name': os.path.basename(image), 'data': fh.read(), - 'mime': guess_type(image)[0] or 'application/octet-stream' + 'mime': guess_type(image)[0] or DEFAULT_BINARY_FILE_TYPE } logger.debug(f'ReportPortal - Log Message with Image: {mes} {image}') self.service.log(message=mes) diff --git a/setup.py b/setup.py index af0ff72..a42b845 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import setup -__version__ = '5.5.7' +__version__ = '5.5.8' def read_file(fname): diff --git a/tests/integration/test_screenshot.py b/tests/integration/test_screenshot.py new file mode 100644 index 0000000..1b2b5b4 --- /dev/null +++ b/tests/integration/test_screenshot.py @@ -0,0 +1,44 @@ +# Copyright 2023 EPAM Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tests.helpers import utils +from unittest import mock + +from tests import REPORT_PORTAL_SERVICE + +EXAMPLE_TEST = 'examples/screenshot.robot' +SELENIUM_SCREENSHOT = 'examples/res/selenium-screenshot-1.png' +PLAYWRIGHT_SCREENSHOT = 'examples/res/Screenshot_test_FAILURE_SCREENSHOT_1.png' +SCREENSHOTS = [SELENIUM_SCREENSHOT, PLAYWRIGHT_SCREENSHOT] + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_screenshot_log(mock_client_init): + result = utils.run_robot_tests([EXAMPLE_TEST]) + assert result == 0 # the test successfully passed + + mock_client = mock_client_init.return_value + calls = utils.get_log_calls(mock_client) + assert len(calls) == 2 + + for i, call in enumerate(calls): + message = call[1]['message'] + assert message == f'Image attached: {SCREENSHOTS[i]}' + + attachment = call[1]['attachment'] + + assert attachment['name'] == SCREENSHOTS[i].split('/')[-1] + assert attachment['mime'] == 'image/png' + with open(SCREENSHOTS[i], 'rb') as file: + assert attachment['data'] == file.read()