diff --git a/CHANGELOG.md b/CHANGELOG.md index 7267487..054287f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added +- Issue [#192](https://github.com/reportportal/agent-Python-RobotFramework/issues/192): Robot link markup to Markdown conversion, by @HardNorth + +## [5.5.4] ### Fixed - Issue [#187](https://github.com/reportportal/agent-Python-RobotFramework/issues/187): Distutils in the agent, by @HardNorth ### Added diff --git a/examples/rkie_keyword.robot b/examples/rkie_keyword.robot new file mode 100644 index 0000000..ad74cd5 --- /dev/null +++ b/examples/rkie_keyword.robot @@ -0,0 +1,18 @@ +*** Settings *** +Documentation Example of 'Run Keyword And Ignore Error' keyword reporting + +*** Variables *** +${countval} 0 + +*** Test Cases *** +Rkie test + Run Keyword And Ignore Error Fail on first try + Fail on first try + +*** Keywords *** +Fail on first try + ${counter} Evaluate ${countval} + 1 + Set Suite Variable ${countval} ${counter} + IF ${countval} < 2 + Fail To less executions + END diff --git a/examples/suite_doc_with_urls.robot b/examples/suite_doc_with_urls.robot new file mode 100644 index 0000000..d36e1b6 --- /dev/null +++ b/examples/suite_doc_with_urls.robot @@ -0,0 +1,6 @@ +*** Settings *** +Documentation This is a test suite with URLs: [https://www.google.com | Google] and [https://www.google.com] + +*** Test Cases *** +Simple test + Log Hello, world! diff --git a/robotframework_reportportal/helpers.py b/robotframework_reportportal/helpers.py new file mode 100644 index 0000000..d7f696c --- /dev/null +++ b/robotframework_reportportal/helpers.py @@ -0,0 +1,40 @@ +# Copyright 2024 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. + +"""This module contains functions to ease reporting to ReportPortal.""" + +import re +from typing import Iterable, Tuple + + +def replace_patterns(text: str, patterns: Iterable[Tuple[re.Pattern, str]]) -> str: + """Replace given patterns in the text.""" + result = text + for p, repl in patterns: + result = p.sub(repl, result) + return result + + +BARE_LINK_PATTERN = re.compile(r'\[\s*([^]|]+)]') +NAMED_LINK_PATTERN = re.compile(r'\[\s*([^]|]+)\|\s*([^]]+)]') + +ROBOT_MARKUP_REPLACEMENT_PATTERS = [ + (BARE_LINK_PATTERN, r'<\1>'), + (NAMED_LINK_PATTERN, r'[\2](\1)'), +] + + +def robot_markup_to_markdown(text: str) -> str: + """Convert Robot Framework's text markup to Markdown format.""" + return replace_patterns(text, ROBOT_MARKUP_REPLACEMENT_PATTERS) diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index 4de50fa..85e7f2c 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -25,10 +25,10 @@ from reportportal_client.helpers import LifoQueue, is_binary, guess_content_type_from_bytes -from .model import Keyword, Launch, Test, LogMessage, Suite -from .service import RobotService -from .static import MAIN_SUITE_ID, PABOT_WITHOUT_LAUNCH_ID_MSG -from .variables import Variables +from robotframework_reportportal.model import Keyword, Launch, Test, LogMessage, Suite +from robotframework_reportportal.service import RobotService +from robotframework_reportportal.static import MAIN_SUITE_ID, PABOT_WITHOUT_LAUNCH_ID_MSG +from robotframework_reportportal.variables import Variables logger = logging.getLogger(__name__) VARIABLE_PATTERN = r'^\s*\${[^}]*}\s*=\s*' diff --git a/robotframework_reportportal/logger.py b/robotframework_reportportal/logger.py index 0494971..e895325 100644 --- a/robotframework_reportportal/logger.py +++ b/robotframework_reportportal/logger.py @@ -39,7 +39,7 @@ def log_free_memory(self): from robot.api import logger -from .model import LogMessage +from robotframework_reportportal.model import LogMessage def write(msg: str, level: str = 'INFO', html: bool = False, attachment: Optional[Dict[str, str]] = None, diff --git a/robotframework_reportportal/model.py b/robotframework_reportportal/model.py index d530987..51bdbd7 100644 --- a/robotframework_reportportal/model.py +++ b/robotframework_reportportal/model.py @@ -17,6 +17,7 @@ import os from typing import Any, Dict, List, Optional, Union +from robotframework_reportportal.helpers import robot_markup_to_markdown from reportportal_client.helpers import gen_attributes @@ -48,7 +49,7 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any]): :param robot_attributes: Suite attributes passed through the listener """ self.robot_attributes = robot_attributes - self.doc = robot_attributes['doc'] + self.doc = robot_markup_to_markdown(robot_attributes['doc']) self.end_time = robot_attributes.get('endtime', '') self.longname = robot_attributes['longname'] self.message = robot_attributes.get('message') @@ -145,7 +146,7 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any], test_attributes: self._tags = robot_attributes['tags'] self.test_attributes = gen_attributes(test_attributes) self.robot_attributes = robot_attributes - self.doc = robot_attributes['doc'] + self.doc = robot_markup_to_markdown(robot_attributes['doc']) self.end_time = robot_attributes.get('endtime', '') self.longname = robot_attributes['longname'] self.message = robot_attributes.get('message') @@ -242,7 +243,7 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any], parent_type: Opt self.robot_attributes = robot_attributes self.args = robot_attributes['args'] self.assign = robot_attributes['assign'] - self.doc = robot_attributes['doc'] + self.doc = robot_markup_to_markdown(robot_attributes['doc']) self.end_time = robot_attributes.get('endtime') self.keyword_name = robot_attributes['kwname'] self.keyword_type = robot_attributes['type'] diff --git a/robotframework_reportportal/result_visitor.py b/robotframework_reportportal/result_visitor.py index 23ab5ab..9a0ba10 100644 --- a/robotframework_reportportal/result_visitor.py +++ b/robotframework_reportportal/result_visitor.py @@ -15,20 +15,20 @@ import re import string from datetime import datetime - -from robot.api import ResultVisitor +from typing import List, Pattern, Optional from urllib.parse import unquote -from . import listener -from .time_visitor import corrections -# noinspection PyUnresolvedReferences -from .variables import _variables +from robot.result import ResultVisitor, Result, TestSuite, TestCase, Keyword, Message +from robotframework_reportportal import listener +from robotframework_reportportal.time_visitor import corrections +# noinspection PyUnresolvedReferences +from robotframework_reportportal.variables import _variables listener = listener.listener() -def to_timestamp(time_str): +def to_timestamp(time_str: str) -> Optional[str]: if time_str: dt = datetime.strptime(time_str, '%Y%m%d %H:%M:%S.%f') return str(int(dt.timestamp() * 1000)) @@ -36,15 +36,16 @@ def to_timestamp(time_str): class RobotResultsVisitor(ResultVisitor): - _link_pattern = re.compile("src=[\"\']([^\"\']+)[\"\']") + _link_pattern: Pattern = re.compile("src=[\"\']([^\"\']+)[\"\']") - def start_result(self, result): + def start_result(self, result: Result) -> bool: if "RP_LAUNCH" not in _variables: _variables["RP_LAUNCH"] = result.suite.name if "RP_LAUNCH_DOC" not in _variables: _variables["RP_LAUNCH_DOC"] = result.suite.doc + return True - def start_suite(self, suite): + def start_suite(self, suite: TestSuite) -> bool: ts = to_timestamp(suite.starttime if suite.id not in corrections else corrections[suite.id][0]) attrs = { 'id': suite.id, @@ -58,8 +59,9 @@ def start_suite(self, suite): 'starttime': ts } listener.start_suite(suite.name, attrs, ts) + return True - def end_suite(self, suite): + def end_suite(self, suite: TestSuite) -> None: ts = to_timestamp(suite.endtime if suite.id not in corrections else corrections[suite.id][1]) attrs = { 'id': suite.id, @@ -78,7 +80,7 @@ def end_suite(self, suite): } listener.end_suite(None, attrs, ts) - def start_test(self, test): + def start_test(self, test: TestCase) -> bool: ts = to_timestamp(test.starttime if test.id not in corrections else corrections[test.id][0]) attrs = { 'id': test.id, @@ -95,8 +97,9 @@ def start_test(self, test): 'starttime': ts, } listener.start_test(test.name, attrs, ts) + return True - def end_test(self, test): + def end_test(self, test: TestCase) -> None: ts = to_timestamp(test.endtime if test.id not in corrections else corrections[test.id][1]) attrs = { 'id': test.id, @@ -117,7 +120,7 @@ def end_test(self, test): } listener.end_test(test.name, attrs, ts) - def start_keyword(self, kw): + def start_keyword(self, kw: Keyword) -> bool: ts = to_timestamp(kw.starttime if kw.id not in corrections else corrections[kw.id][0]) attrs = { 'type': string.capwords(kw.type), @@ -130,8 +133,9 @@ def start_keyword(self, kw): 'starttime': ts, } listener.start_keyword(kw.name, attrs, ts) + return True - def end_keyword(self, kw): + def end_keyword(self, kw: Keyword) -> None: ts = to_timestamp(kw.endtime if kw.id not in corrections else corrections[kw.id][1]) attrs = { 'type': string.capwords(kw.type), @@ -147,7 +151,7 @@ def end_keyword(self, kw): } listener.end_keyword(kw.name, attrs, ts) - def start_message(self, msg): + def start_message(self, msg: Message) -> bool: if msg.message: message = { 'message': msg.message, @@ -162,8 +166,9 @@ def start_message(self, msg): try: listener.log_message(message) except Exception: - pass + return False + return True - def parse_message(self, msg): + def parse_message(self, msg: str) -> List[str]: m = self._link_pattern.search(msg) return [m.group(), unquote(m.group(1))] diff --git a/robotframework_reportportal/result_visitor.pyi b/robotframework_reportportal/result_visitor.pyi deleted file mode 100644 index a50d5ed..0000000 --- a/robotframework_reportportal/result_visitor.pyi +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2022 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 typing import Pattern, List -from .listener import listener as ls -from robot.result import ResultVisitor, Result, TestSuite, TestCase, Keyword, Message - -listener: ls - -class RobotResultsVisitor(ResultVisitor): - def __init__(self): - self._link_pattern = Pattern - - def start_result(self, result: Result) -> bool: ... - - def start_suite(self, suite: TestSuite) -> bool: ... - - def end_suite(self, suite: TestSuite) -> None: ... - - def start_test(self, test: TestCase) -> bool: ... - - def end_test(self, test: TestCase) -> None: ... - - def start_keyword(self, kw: Keyword) -> bool: ... - - def end_keyword(self, kw: Keyword) -> None: ... - - def start_message(self, msg: Message) -> bool: ... - - def parse_message(self, param) -> List[str]: ... diff --git a/robotframework_reportportal/service.py b/robotframework_reportportal/service.py index 8cc1869..7f34ce8 100644 --- a/robotframework_reportportal/service.py +++ b/robotframework_reportportal/service.py @@ -14,22 +14,21 @@ """This module is a Robot service for reporting results to ReportPortal.""" +import logging from typing import Optional from dateutil.parser import parse -import logging - +from reportportal_client import RP, create_client from reportportal_client.helpers import ( dict_to_payload, get_launch_sys_attrs, get_package_version, timestamp ) -from reportportal_client import RP, create_client -from .model import Launch, Suite, Test, Keyword, LogMessage -from .variables import Variables -from .static import LOG_LEVEL_MAPPING, STATUS_MAPPING +from robotframework_reportportal.model import Launch, Suite, Test, Keyword, LogMessage +from robotframework_reportportal.static import LOG_LEVEL_MAPPING, STATUS_MAPPING +from robotframework_reportportal.variables import Variables logger = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index 802f7ec..ee8e1b5 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import setup -__version__ = '5.5.4' +__version__ = '5.5.5' def read_file(fname): diff --git a/tests/integration/test_rkie_keyword.py b/tests/integration/test_rkie_keyword.py new file mode 100644 index 0000000..a39dd73 --- /dev/null +++ b/tests/integration/test_rkie_keyword.py @@ -0,0 +1,39 @@ +# 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 unittest import mock + +from tests import REPORT_PORTAL_SERVICE +from tests.helpers import utils + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_before_after_suite_with_steps(mock_client_init): + mock_client = mock_client_init.return_value + mock_client.start_test_item.side_effect = utils.item_id_gen + + result = utils.run_robot_tests(['examples/rkie_keyword.robot']) + assert result == 0 + + launch_start = mock_client.start_launch.call_args_list + launch_finish = mock_client.finish_launch.call_args_list + assert len(launch_start) == len(launch_finish) == 1 + + item_start_calls = mock_client.start_test_item.call_args_list + item_finish_calls = mock_client.finish_test_item.call_args_list + assert len(item_start_calls) == len(item_finish_calls) == 13 + + statuses = [finish[1]['status'] for finish in item_finish_calls] + assert statuses == ['PASSED'] * 2 + ['FAILED'] * 3 + ['PASSED'] * 3 + [ + 'SKIPPED'] * 2 + ['PASSED'] * 3 diff --git a/tests/integration/test_suite_doc_with_urls.py b/tests/integration/test_suite_doc_with_urls.py new file mode 100644 index 0000000..c09dd85 --- /dev/null +++ b/tests/integration/test_suite_doc_with_urls.py @@ -0,0 +1,39 @@ +# Copyright 2022 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 unittest import mock + +from tests import REPORT_PORTAL_SERVICE +from tests.helpers import utils + +SIMPLE_TEST = 'examples/suite_doc_with_urls.robot' + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_suite_doc_with_urls(mock_client_init): + result = utils.run_robot_tests([SIMPLE_TEST]) + assert result == 0 # the test successfully passed + + mock_client = mock_client_init.return_value + launch_start = mock_client.start_launch.call_args_list + launch_finish = mock_client.finish_launch.call_args_list + assert len(launch_start) == len(launch_finish) == 1 + + item_start_calls = mock_client.start_test_item.call_args_list + item_finish_calls = mock_client.finish_test_item.call_args_list + assert len(item_start_calls) == len(item_finish_calls) == 3 + + test_suite = item_start_calls[0] + assert test_suite[1]['description'] == ('This is a test suite with URLs: [Google](https://www.google.com ) and ' + '')