From b1374a839aecf257b6bfbe226a14927e9d9a7fb0 Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Tue, 19 Mar 2024 13:06:52 +0000 Subject: [PATCH 01/13] Changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939fa4d..220cefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [5.5.2] ### Added - Binary data escaping in `listener` module (enhancing `Get Binary File` keyword logging), by @HardNorth ### Changed From a9d241bcf62b6ce45dcfc2ad51316dcb5a99c6a9 Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Tue, 19 Mar 2024 13:06:53 +0000 Subject: [PATCH 02/13] Version update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f96de59..865cb4e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import setup -__version__ = '5.5.2' +__version__ = '5.5.3' def read_file(fname): From a0560a643d1594bf6f575855de5a750a125ade79 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 14:14:59 +0300 Subject: [PATCH 03/13] flake8 fixes --- CHANGELOG.md | 2 + robotframework_reportportal/listener.py | 22 +-- robotframework_reportportal/model.py | 197 +++++++++++++++--------- robotframework_reportportal/model.pyi | 101 ------------ robotframework_reportportal/service.py | 2 +- tests/unit/test_model.py | 2 +- 6 files changed, 142 insertions(+), 184 deletions(-) delete mode 100644 robotframework_reportportal/model.pyi diff --git a/CHANGELOG.md b/CHANGELOG.md index 220cefb..a16645b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Removed +- `model.pyi` stub file, as we don't really need it anymore, by @HardNorth ## [5.5.2] ### Added diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index db87617..d7842d0 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -207,18 +207,18 @@ def variables(self) -> Variables: return self._variables @check_rp_enabled - def start_launch(self, attributes: Dict, ts: Optional[Any] = None) -> None: + def start_launch(self, attributes: Dict[str, Any], ts: Optional[Any] = None) -> None: """Start a new launch at the ReportPortal. :param attributes: Dictionary passed by the Robot Framework :param ts: Timestamp(used by the ResultVisitor) """ - launch = Launch(self.variables.launch_name, attributes) - launch.attributes = gen_attributes(self.variables.launch_attributes) + launch = Launch(self.variables.launch_name, attributes, gen_attributes(self.variables.launch_attributes)) + launch.robot_attributes = gen_attributes(self.variables.launch_attributes) launch.doc = self.variables.launch_doc or launch.doc if self.variables.pabot_used: warn(PABOT_WIHOUT_LAUNCH_ID_MSG, stacklevel=2) - logger.debug('ReportPortal - Start Launch: {0}'.format(launch.attributes)) + logger.debug('ReportPortal - Start Launch: {0}'.format(launch.robot_attributes)) self.service.start_launch( launch=launch, mode=self.variables.mode, @@ -255,10 +255,10 @@ def end_suite(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None :param ts: Timestamp(used by the ResultVisitor) """ suite = self._remove_current_item().update(attributes) - logger.debug('ReportPortal - End Suite: {0}'.format(suite.attributes)) + logger.debug('ReportPortal - End Suite: {0}'.format(suite.robot_attributes)) self.service.finish_suite(suite=suite, ts=ts) if attributes['id'] == MAIN_SUITE_ID: - launch = Launch(self.variables.launch_name, attributes) + launch = Launch(self.variables.launch_name, attributes, None) logger.debug( msg='ReportPortal - End Launch: {0}'.format(attributes)) self.service.finish_launch(launch=launch, ts=ts) @@ -275,7 +275,7 @@ def start_test(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> N # no 'source' parameter at this level for Robot versions < 4 attributes = attributes.copy() attributes['source'] = getattr(self.current_item, 'source', None) - test = Test(name=name, attributes=attributes) + test = Test(name=name, robot_attributes=attributes) logger.debug('ReportPortal - Start Test: {0}'.format(attributes)) test.attributes = gen_attributes(self.variables.test_attributes + test.tags) test.rp_parent_item_id = self.parent_id @@ -291,13 +291,13 @@ def end_test(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None) :param ts: Timestamp(used by the ResultVisitor) """ test = self.current_item.update(attributes) - test.attributes = gen_attributes( + test.robot_attributes = gen_attributes( self.variables.test_attributes + test.tags) if not test.critical and test.status == 'FAIL': test.status = 'SKIP' if test.message: self.log_message({'message': test.message, 'level': 'DEBUG'}) - logger.debug('ReportPortal - End Test: {0}'.format(test.attributes)) + logger.debug('ReportPortal - End Test: {0}'.format(test.robot_attributes)) self._remove_current_item() self.service.finish_test(test=test, ts=ts) @@ -309,7 +309,7 @@ def start_keyword(self, name: str, attributes: Dict, ts: Optional[Any] = None) - :param attributes: Dictionary passed by the Robot Framework :param ts: Timestamp(used by the ResultVisitor) """ - kwd = Keyword(name=name, parent_type=self.current_item.type, attributes=attributes) + kwd = Keyword(name=name, parent_type=self.current_item.type, robot_attributes=attributes) kwd.rp_parent_item_id = self.parent_id logger.debug('ReportPortal - Start Keyword: {0}'.format(attributes)) kwd.rp_item_id = self.service.start_keyword(keyword=kwd, ts=ts) @@ -324,7 +324,7 @@ def end_keyword(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = No :param ts: Timestamp(used by the ResultVisitor) """ kwd = self._remove_current_item().update(attributes) - logger.debug('ReportPortal - End Keyword: {0}'.format(kwd.attributes)) + logger.debug('ReportPortal - End Keyword: {0}'.format(kwd.robot_attributes)) self.service.finish_keyword(keyword=kwd, ts=ts) def log_file(self, log_path: str) -> None: diff --git a/robotframework_reportportal/model.py b/robotframework_reportportal/model.py index 969b623..53f0122 100644 --- a/robotframework_reportportal/model.py +++ b/robotframework_reportportal/model.py @@ -1,55 +1,75 @@ -# Copyright (c) 2023 EPAM Systems +# 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 +# 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 +# limitations under the License. """This module contains models representing Robot Framework test items.""" import os +from typing import Any, Dict, List, Optional, Union class Suite: """Class represents Robot Framework test suite.""" - def __init__(self, name, attributes): + robot_attributes: Union[List[str], Dict[str, Any]] + doc: str + end_time: str + longname: str + message: str + metadata: Dict[str, str] + name: str + robot_id: str + rp_item_id: Optional[str] + rp_parent_item_id: Optional[str] + start_time: Optional[str] + statistics: str + status: str + suites: List[str] + tests: List[str] + total_tests: int + type: str = 'SUITE' + + def __init__(self, name: str, robot_attributes: Dict[str, Any]): """Initialize required attributes. :param name: Suite name - :param attributes: Suite attributes passed through the listener + :param robot_attributes: Suite attributes passed through the listener """ - self.attributes = attributes - self.doc = attributes['doc'] - self.end_time = attributes.get('endtime', '') - self.longname = attributes['longname'] - self.message = attributes.get('message') - self.metadata = attributes['metadata'] + self.robot_attributes = robot_attributes + self.doc = robot_attributes['doc'] + self.end_time = robot_attributes.get('endtime', '') + self.longname = robot_attributes['longname'] + self.message = robot_attributes.get('message') + self.metadata = robot_attributes['metadata'] self.name = name - self.robot_id = attributes['id'] + self.robot_id = robot_attributes['id'] self.rp_item_id = None self.rp_parent_item_id = None - self.start_time = attributes.get('starttime') - self.statistics = attributes.get('statistics') - self.status = attributes.get('status') - self.suites = attributes['suites'] - self.tests = attributes['tests'] - self.total_tests = attributes['totaltests'] + self.start_time = robot_attributes.get('starttime') + self.statistics = robot_attributes.get('statistics') + self.status = robot_attributes.get('status') + self.suites = robot_attributes['suites'] + self.tests = robot_attributes['tests'] + self.total_tests = robot_attributes['totaltests'] self.type = 'SUITE' @property - def source(self): + def source(self) -> str: """Return the test case source file path.""" - if self.attributes.get('source') is not None: - return os.path.relpath(self.attributes['source'], os.getcwd()) + if self.robot_attributes.get('source') is not None: + return os.path.relpath(self.robot_attributes['source'], os.getcwd()) - def update(self, attributes): + def update(self, attributes: Dict[str, Any]) -> 'Suite': """Update suite attributes on suite finish. :param attributes: Suite attributes passed through the listener @@ -64,74 +84,92 @@ def update(self, attributes): class Launch(Suite): """Class represents Robot Framework test suite.""" - def __init__(self, name, attributes): + type: str = 'LAUNCH' + + def __init__(self, name: str, robot_attributes: Dict[str, Any], launch_attributes: Optional[List[Dict[str, str]]]): """Initialize required attributes. :param name: Launch name - :param attributes: Suite attributes passed through the listener + :param robot_attributes: Suite attributes passed through the listener """ - # noinspection PySuperArguments - super(Launch, self).__init__(name, attributes) + super().__init__(name, robot_attributes) self.type = 'LAUNCH' class Test: """Class represents Robot Framework test case.""" - def __init__(self, name, attributes): + _critical: str + _tags: List[str] + robot_attributes: Dict[str, Any] + attributes: List[Dict[str, str]] + doc: str + end_time: str + longname: str + message: str + name: str + robot_id: str + rp_item_id: Optional[str] + rp_parent_item_id: Optional[str] + start_time: str + status: str + template: str + type: str = 'TEST' + + def __init__(self, name: str, robot_attributes: Dict[str, Any]): """Initialize required attributes. :param name: Name of the test - :param attributes: Test attributes passed through the listener + :param robot_attributes: Test attributes passed through the listener """ # for backward compatibility with Robot < 4.0 mark every test case # as critical if not set - self._critical = attributes.get('critical', 'yes') - self._tags = attributes['tags'] - self._attributes = attributes - self.doc = attributes['doc'] - self.end_time = attributes.get('endtime', '') - self.longname = attributes['longname'] - self.message = attributes.get('message') + self._critical = robot_attributes.get('critical', 'yes') + self._tags = robot_attributes['tags'] + self.robot_attributes = robot_attributes + self.doc = robot_attributes['doc'] + self.end_time = robot_attributes.get('endtime', '') + self.longname = robot_attributes['longname'] + self.message = robot_attributes.get('message') self.name = name - self.robot_id = attributes['id'] + self.robot_id = robot_attributes['id'] self.rp_item_id = None self.rp_parent_item_id = None - self.start_time = attributes['starttime'] - self.status = attributes.get('status') - self.template = attributes['template'] + self.start_time = robot_attributes['starttime'] + self.status = robot_attributes.get('status') + self.template = robot_attributes['template'] self.type = 'TEST' @property - def critical(self): + def critical(self) -> bool: """Form unique value for RF 4.0+ and older versions.""" return self._critical in ('yes', True) @property - def tags(self): + def tags(self) -> List[str]: """Get list of test tags excluding test_case_id.""" return [ tag for tag in self._tags if not tag.startswith('test_case_id')] @property - def source(self): + def source(self) -> str: """Return the test case source file path.""" - if self._attributes['source'] is not None: - return os.path.relpath(self._attributes['source'], os.getcwd()) + if self.robot_attributes['source'] is not None: + return os.path.relpath(self.robot_attributes['source'], os.getcwd()) @property - def code_ref(self): + def code_ref(self) -> str: """Return the test case code reference. The result line should be exactly how it appears in '.robot' file. """ - line_number = self._attributes.get("lineno") + line_number = self.robot_attributes.get("lineno") if line_number is not None: return '{0}:{1}'.format(self.source, line_number) return '{0}:{1}'.format(self.source, self.name) @property - def test_case_id(self): + def test_case_id(self) -> Optional[str]: """Get test case ID through the tags.""" # use test case id from tags if specified for tag in self._tags: @@ -140,7 +178,7 @@ def test_case_id(self): # generate it if not return '{0}:{1}'.format(self.source, self.name) - def update(self, attributes): + def update(self, attributes: Dict[str, Any]) -> 'Test': """Update test attributes on test finish. :param attributes: Suite attributes passed through the listener @@ -155,31 +193,48 @@ def update(self, attributes): class Keyword: """Class represents Robot Framework keyword.""" - def __init__(self, name, attributes, parent_type=None): + robot_attributes: Dict[str, Any] + args: List[str] + assign: List[str] + doc: str + end_time: str + keyword_name: str + keyword_type: str + libname: str + name: str + rp_item_id: Optional[str] + rp_parent_item_id: Optional[str] + parent_type: str + start_time: str + status: str + tags: List[str] + type: str = 'KEYWORD' + + def __init__(self, name: str, robot_attributes: Dict[str, Any], parent_type: Optional[str] = None): """Initialize required attributes. :param name: Name of the keyword - :param attributes: Keyword attributes passed through the listener + :param robot_attributes: Keyword attributes passed through the listener :param parent_type: Type of the parent test item """ - self.attributes = attributes - self.args = attributes['args'] - self.assign = attributes['assign'] - self.doc = attributes['doc'] - self.end_time = attributes.get('endtime') - self.keyword_name = attributes['kwname'] - self.keyword_type = attributes['type'] - self.libname = attributes['libname'] + self.robot_attributes = robot_attributes + self.args = robot_attributes['args'] + self.assign = robot_attributes['assign'] + self.doc = robot_attributes['doc'] + self.end_time = robot_attributes.get('endtime') + self.keyword_name = robot_attributes['kwname'] + self.keyword_type = robot_attributes['type'] + self.libname = robot_attributes['libname'] self.name = name self.rp_item_id = None self.rp_parent_item_id = None self.parent_type = parent_type - self.start_time = attributes['starttime'] - self.status = attributes.get('status') - self.tags = attributes['tags'] + self.start_time = robot_attributes['starttime'] + self.status = robot_attributes.get('status') + self.tags = robot_attributes['tags'] self.type = 'KEYWORD' - def get_name(self): + def get_name(self) -> str: """Get name of the keyword suitable for ReportPortal.""" assign = ', '.join(self.assign) assignment = '{0} = '.format(assign) if self.assign else '' @@ -187,7 +242,7 @@ def get_name(self): full_name = f'{assignment}{self.name} ({arguments})' return full_name[:256] - def get_type(self): + def get_type(self) -> str: """Get keyword type.""" if self.keyword_type.lower() in ('setup', 'teardown'): if self.parent_type.lower() == 'keyword': @@ -199,7 +254,7 @@ def get_type(self): else: return 'STEP' - def update(self, attributes): + def update(self, attributes: Dict[str, Any]) -> 'Keyword': """Update keyword attributes on keyword finish. :param attributes: Suite attributes passed through the listener @@ -209,17 +264,19 @@ def update(self, attributes): return self -class LogMessage(str): +class LogMessage: """Class represents Robot Framework messages.""" - def __init__(self, message): + attachment: Optional[Dict[str, str]] + launch_log: bool + item_id: Optional[str] + level: str + message: str + + def __init__(self, message: str): """Initialize required attributes.""" self.attachment = None self.item_id = None self.level = 'INFO' self.launch_log = False self.message = message - - def __repr__(self): - """Return string representation of the object.""" - return self.message diff --git a/robotframework_reportportal/model.pyi b/robotframework_reportportal/model.pyi deleted file mode 100644 index 79fd73d..0000000 --- a/robotframework_reportportal/model.pyi +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) 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 typing import Any, Dict, List, Optional, Tuple, Union - -class Suite: - attributes: Union[List[str], Dict[str, Any]] = ... - doc: str = ... - end_time: str = ... - longname: str = ... - message: str = ... - metadata: Dict[str, str] = ... - name: str = ... - robot_id: str = ... - rp_item_id: Optional[str] = ... - rp_parent_item_id: Optional[str] = ... - start_time: Optional[str] = ... - statistics: str = ... - status: str = ... - suites: List[str] = ... - tests: List[str] = ... - total_tests: int = ... - type: str = 'SUITE' - def __init__(self, name: str, attributes: Dict[str, Any]) -> None: ... - @property - def source(self) -> str: ... - def update(self, attributes: Dict[str, Any]) -> Union[Launch, Suite]: ... - -class Launch(Suite): - type: str = 'LAUNCH' - def __init__(self, name: str, attributes: Dict[str, Any]) -> None: ... - -class Test: - _critical: str = ... - _tags: List[str] = ... - _attributes: Dict[str, Any] = ... - attributes: List[Dict[str, str]] = ... - doc: str = ... - end_time: str = ... - longname: str = ... - message: str = ... - name: str = ... - robot_id: str = ... - rp_item_id: Optional[str] = ... - rp_parent_item_id: Optional[str] = ... - start_time: str = ... - status: str = ... - template: str = ... - type: str = 'TEST' - def __init__(self, name: str, attributes: Dict[str, Any]) -> None: ... - @property - def critical(self) -> bool: ... - @property - def tags(self) -> List[str]: ... - @property - def source(self) -> str: ... - @property - def code_ref(self) -> str: ... - @property - def test_case_id(self) -> Optional[str]: ... - def update(self, attributes: Dict[str, Any]) -> Test: ... - -class Keyword: - attributes: Dict[str, Any] = ... - args: List[str] = ... - assign: List[str] = ... - doc: str = ... - end_time: str = ... - keyword_name: str = ... - keyword_type: str = ... - libname: str = ... - name: str = ... - rp_item_id: Optional[str] = ... - rp_parent_item_id: Optional[str] = ... - parent_type: str = ... - start_time: str = ... - status: str = ... - tags: List[str] = ... - type: str = 'KEYWORD' - def __init__(self, name: str, attributes: Dict[str, Any], parent_type: Optional[str] = None) -> None: ... - def get_name(self) -> str: ... - def get_type(self) -> str: ... - def update(self, attributes: Dict[str, Any]) -> Keyword: ... - -class LogMessage(str): - attachment: Optional[Dict[str, str]] = ... - launch_log: bool = ... - item_id: Optional[str] = ... - level: str = ... - message: str = ... - def __init__(self, *args: Tuple, **kwargs: Dict) -> None: ... diff --git a/robotframework_reportportal/service.py b/robotframework_reportportal/service.py index b5bd69a..7aa16fb 100644 --- a/robotframework_reportportal/service.py +++ b/robotframework_reportportal/service.py @@ -120,7 +120,7 @@ def start_launch(self, launch: Launch, mode: Optional[str] = None, rerun: bool = :return: launch UUID """ sl_pt = { - 'attributes': self._get_launch_attributes(launch.attributes), + 'attributes': self._get_launch_attributes(launch.robot_attributes), 'description': launch.doc, 'name': launch.name, 'mode': mode, diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py index 5d3ea6d..12a8cc7 100644 --- a/tests/unit/test_model.py +++ b/tests/unit/test_model.py @@ -25,7 +25,7 @@ ]) def test_keyword_get_type(kwd_attributes, self_type, parent_type, expected): """Test for the get_type() method of the Keyword model.""" - kwd = Keyword(name='Test keyword', attributes=kwd_attributes, + kwd = Keyword(name='Test keyword', robot_attributes=kwd_attributes, parent_type=parent_type) kwd.keyword_type = self_type assert kwd.get_type() == expected From a46276e5ea154900a528f67266d77c7bb3c792d8 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 14:20:10 +0300 Subject: [PATCH 04/13] Remove static.pyi --- CHANGELOG.md | 2 +- robotframework_reportportal/listener.py | 4 ++-- robotframework_reportportal/static.py | 13 +++++++------ robotframework_reportportal/static.pyi | 20 -------------------- 4 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 robotframework_reportportal/static.pyi diff --git a/CHANGELOG.md b/CHANGELOG.md index a16645b..69e2a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] ### Removed -- `model.pyi` stub file, as we don't really need it anymore, by @HardNorth +- `model.pyi`, `static.pyi` stub files, as we don't really need them anymore, by @HardNorth ## [5.5.2] ### Added diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index d7842d0..1b6753c 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -28,7 +28,7 @@ from .model import Keyword, Launch, Test, LogMessage, Suite from .service import RobotService -from .static import MAIN_SUITE_ID, PABOT_WIHOUT_LAUNCH_ID_MSG +from .static import MAIN_SUITE_ID, PABOT_WITHOUT_LAUNCH_ID_MSG from .variables import Variables logger = logging.getLogger(__name__) @@ -217,7 +217,7 @@ def start_launch(self, attributes: Dict[str, Any], ts: Optional[Any] = None) -> launch.robot_attributes = gen_attributes(self.variables.launch_attributes) launch.doc = self.variables.launch_doc or launch.doc if self.variables.pabot_used: - warn(PABOT_WIHOUT_LAUNCH_ID_MSG, stacklevel=2) + warn(PABOT_WITHOUT_LAUNCH_ID_MSG, stacklevel=2) logger.debug('ReportPortal - Start Launch: {0}'.format(launch.robot_attributes)) self.service.start_launch( launch=launch, diff --git a/robotframework_reportportal/static.py b/robotframework_reportportal/static.py index 856c465..05b578d 100644 --- a/robotframework_reportportal/static.py +++ b/robotframework_reportportal/static.py @@ -14,7 +14,9 @@ """This module includes static variables of the agent.""" -LOG_LEVEL_MAPPING = { +from typing import Dict + +LOG_LEVEL_MAPPING: Dict[str, str] = { 'INFO': 'INFO', 'FAIL': 'ERROR', 'TRACE': 'TRACE', @@ -24,11 +26,10 @@ 'ERROR': 'ERROR', 'SKIP': 'INFO' } -MAIN_SUITE_ID = 's1' -PABOT_WIHOUT_LAUNCH_ID_MSG = ( - 'Pabot library is used but RP_LAUNCH_UUID was not provided. Please, ' - 'initialize listener with the RP_LAUNCH_UUID argument.') -STATUS_MAPPING = { +MAIN_SUITE_ID: str = 's1' +PABOT_WITHOUT_LAUNCH_ID_MSG: str = ('Pabot library is used but RP_LAUNCH_UUID was not provided. Please, ' + 'initialize listener with the RP_LAUNCH_UUID argument.') +STATUS_MAPPING: Dict[str, str] = { 'PASS': 'PASSED', 'FAIL': 'FAILED', 'NOT RUN': 'SKIPPED', diff --git a/robotframework_reportportal/static.pyi b/robotframework_reportportal/static.pyi deleted file mode 100644 index 4f6a19e..0000000 --- a/robotframework_reportportal/static.pyi +++ /dev/null @@ -1,20 +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 Text, Dict - -LOG_LEVEL_MAPPING: Dict -MAIN_SUITE_ID: Text -PABOT_WIHOUT_LAUNCH_ID_MSG: Text -STATUS_MAPPING: Dict From a8e750554eab42f053b95f8a186aebe8f4fc2bfa Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 15:31:12 +0300 Subject: [PATCH 05/13] Refactor attributes handling --- robotframework_reportportal/listener.py | 42 +++++++--------- robotframework_reportportal/logger.py | 37 +++++++------- robotframework_reportportal/model.py | 41 ++++++++++++---- robotframework_reportportal/service.py | 18 +++---- robotframework_reportportal/variables.py | 61 +++++++++++------------- 5 files changed, 104 insertions(+), 95 deletions(-) diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index 1b6753c..2918d56 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -24,7 +24,7 @@ from typing import Optional, Dict, Union, Any from warnings import warn -from reportportal_client.helpers import gen_attributes, LifoQueue, is_binary, guess_content_type_from_bytes +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 @@ -106,9 +106,9 @@ def wrap(*args, **kwargs): class listener: """Robot Framework listener interface for reporting to ReportPortal.""" - _items: LifoQueue = ... - _service: Optional[RobotService] = ... - _variables: Optional[Variables] = ... + _items: LifoQueue + _service: Optional[RobotService] + _variables: Optional[Variables] ROBOT_LISTENER_API_VERSION = 2 def __init__(self) -> None: @@ -165,7 +165,7 @@ def log_message(self, message: Dict) -> None: 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('ReportPortal - Log Message: {0}'.format(message)) + logger.debug(f'ReportPortal - Log Message: {message}') self.service.log(message=msg) @check_rp_enabled @@ -182,8 +182,7 @@ def log_message_with_image(self, msg: Dict, image: str): 'data': fh.read(), 'mime': guess_type(image)[0] or 'application/octet-stream' } - logger.debug('ReportPortal - Log Message with Image: {0} {1}' - .format(mes, image)) + logger.debug(f'ReportPortal - Log Message with Image: {mes} {image}') self.service.log(message=mes) @property @@ -213,12 +212,11 @@ def start_launch(self, attributes: Dict[str, Any], ts: Optional[Any] = None) -> :param attributes: Dictionary passed by the Robot Framework :param ts: Timestamp(used by the ResultVisitor) """ - launch = Launch(self.variables.launch_name, attributes, gen_attributes(self.variables.launch_attributes)) - launch.robot_attributes = gen_attributes(self.variables.launch_attributes) + launch = Launch(self.variables.launch_name, attributes, self.variables.launch_attributes) launch.doc = self.variables.launch_doc or launch.doc if self.variables.pabot_used: warn(PABOT_WITHOUT_LAUNCH_ID_MSG, stacklevel=2) - logger.debug('ReportPortal - Start Launch: {0}'.format(launch.robot_attributes)) + logger.debug(f'ReportPortal - Start Launch: {launch.robot_attributes}') self.service.start_launch( launch=launch, mode=self.variables.mode, @@ -237,10 +235,10 @@ def start_suite(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> if attributes['id'] == MAIN_SUITE_ID: self.start_launch(attributes, ts) if self.variables.pabot_used: - name += '.{0}'.format(self.variables.pabot_pool_id) - logger.debug('ReportPortal - Create global Suite: {0}'.format(attributes)) + name = f'{name}.{self.variables.pabot_pool_id}' + logger.debug(f'ReportPortal - Create global Suite: {attributes}') else: - logger.debug('ReportPortal - Start Suite: {0}'.format(attributes)) + logger.debug(f'ReportPortal - Start Suite: {attributes}') suite = Suite(name, attributes) suite.rp_parent_item_id = self.parent_id suite.rp_item_id = self.service.start_suite(suite=suite, ts=ts) @@ -255,12 +253,11 @@ def end_suite(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None :param ts: Timestamp(used by the ResultVisitor) """ suite = self._remove_current_item().update(attributes) - logger.debug('ReportPortal - End Suite: {0}'.format(suite.robot_attributes)) + logger.debug(f'ReportPortal - End Suite: {suite.robot_attributes}') self.service.finish_suite(suite=suite, ts=ts) if attributes['id'] == MAIN_SUITE_ID: launch = Launch(self.variables.launch_name, attributes, None) - logger.debug( - msg='ReportPortal - End Launch: {0}'.format(attributes)) + logger.debug(msg=f'ReportPortal - End Launch: {attributes}') self.service.finish_launch(launch=launch, ts=ts) @check_rp_enabled @@ -275,9 +272,8 @@ def start_test(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> N # no 'source' parameter at this level for Robot versions < 4 attributes = attributes.copy() attributes['source'] = getattr(self.current_item, 'source', None) - test = Test(name=name, robot_attributes=attributes) - logger.debug('ReportPortal - Start Test: {0}'.format(attributes)) - test.attributes = gen_attributes(self.variables.test_attributes + test.tags) + test = Test(name=name, robot_attributes=attributes, test_attributes=self.variables.test_attributes) + logger.debug(f'ReportPortal - Start Test: {attributes}') test.rp_parent_item_id = self.parent_id test.rp_item_id = self.service.start_test(test=test, ts=ts) self._add_current_item(test) @@ -291,13 +287,11 @@ def end_test(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None) :param ts: Timestamp(used by the ResultVisitor) """ test = self.current_item.update(attributes) - test.robot_attributes = gen_attributes( - self.variables.test_attributes + test.tags) if not test.critical and test.status == 'FAIL': test.status = 'SKIP' if test.message: self.log_message({'message': test.message, 'level': 'DEBUG'}) - logger.debug('ReportPortal - End Test: {0}'.format(test.robot_attributes)) + logger.debug(f'ReportPortal - End Test: {test.robot_attributes}') self._remove_current_item() self.service.finish_test(test=test, ts=ts) @@ -311,7 +305,7 @@ def start_keyword(self, name: str, attributes: Dict, ts: Optional[Any] = None) - """ kwd = Keyword(name=name, parent_type=self.current_item.type, robot_attributes=attributes) kwd.rp_parent_item_id = self.parent_id - logger.debug('ReportPortal - Start Keyword: {0}'.format(attributes)) + logger.debug(f'ReportPortal - Start Keyword: {attributes}') kwd.rp_item_id = self.service.start_keyword(keyword=kwd, ts=ts) self._add_current_item(kwd) @@ -324,7 +318,7 @@ def end_keyword(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = No :param ts: Timestamp(used by the ResultVisitor) """ kwd = self._remove_current_item().update(attributes) - logger.debug('ReportPortal - End Keyword: {0}'.format(kwd.robot_attributes)) + logger.debug(f'ReportPortal - End Keyword: {kwd.robot_attributes}') self.service.finish_keyword(keyword=kwd, ts=ts) def log_file(self, log_path: str) -> None: diff --git a/robotframework_reportportal/logger.py b/robotframework_reportportal/logger.py index dfffccf..00194b0 100644 --- a/robotframework_reportportal/logger.py +++ b/robotframework_reportportal/logger.py @@ -35,29 +35,29 @@ def log_free_memory(self): }, ) """ +from typing import Literal, Optional, Dict from robot.api import logger from .model import LogMessage -def write(msg, level='INFO', html=False, attachment=None, launch_log=False): +def write(msg: str, level: logger.LOGLEVEL = 'INFO', html: bool = False, attachment: Optional[Dict[str, str]] = None, + launch_log: bool = False): """Write the message to the log file using the given level. - Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default since RF - 2.9.1), ``WARN``, and ``ERROR`` (new in RF 2.9). Additionally it is - possible to use ``HTML`` pseudo log level that logs the message as HTML - using the ``INFO`` level. + Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default since RF 2.9.1), ``WARN``, + and ``ERROR`` (new in RF 2.9). Additionally, it is possible to use ``HTML`` pseudo log level that logs the message + as HTML using the ``INFO`` level. - Attachment should contain a dict with "name", "data" and "mime" values - defined. See module example. + Attachment should contain a dict with "name", "data" and "mime" values defined. See module example. - Instead of using this method, it is generally better to use the level - specific methods such as ``info`` and ``debug`` that have separate + Instead of using this method, it is generally better to use the level specific methods such as ``info`` and + ``debug`` that have separate - :param msg: argument to control the message format. - :param level: log level - :param html: format or not format the message as html. + :param msg: argument to control the message format. + :param level: log level + :param html: format or not format the message as html. :param attachment: a binary content to attach to the log entry :param launch_log: put the log entry on Launch level """ @@ -82,8 +82,7 @@ def info(msg, html=False, also_console=False, attachment=None, launch_log=False): """Write the message to the log file using the ``INFO`` level. - If ``also_console`` argument is set to ``True``, the message is - written both to the log file and to the console. + If ``also_console`` argument is set to ``True``, the message is written both to the log file and to the console. """ write(msg, "INFO", html, attachment, launch_log) if also_console: @@ -100,14 +99,12 @@ def error(msg, html=False, attachment=None, launch_log=False): write(msg, "ERROR", html, attachment, launch_log) -def console(msg, newline=True, stream="stdout"): +def console(msg: str, newline: bool = True, stream: Literal['stdout', 'stderr'] = 'stdout'): """Write the message to the console. - If the ``newline`` argument is ``True``, a newline character is - automatically added to the message. + If the ``newline`` argument is ``True``, a newline character is automatically added to the message. - By default the message is written to the standard output stream. - Using the standard error stream is possibly by giving the ``stream`` - argument value ``'stderr'``. + By default, the message is written to the standard output stream. + Using the standard error stream is possibly by giving the ``stream`` argument value ``'stderr'``. """ logger.console(msg, newline, stream) diff --git a/robotframework_reportportal/model.py b/robotframework_reportportal/model.py index 53f0122..22fa519 100644 --- a/robotframework_reportportal/model.py +++ b/robotframework_reportportal/model.py @@ -17,6 +17,8 @@ import os from typing import Any, Dict, List, Optional, Union +from reportportal_client.helpers import gen_attributes + class Suite: """Class represents Robot Framework test suite.""" @@ -63,6 +65,13 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any]): self.total_tests = robot_attributes['totaltests'] self.type = 'SUITE' + @property + def attributes(self) -> Optional[List[Dict[str, str]]]: + """Get Suite attributes.""" + if self.metadata is None or not self.metadata: + return None + return [{'key': key, 'value': value} for key, value in self.metadata.items()] + @property def source(self) -> str: """Return the test case source file path.""" @@ -84,17 +93,25 @@ def update(self, attributes: Dict[str, Any]) -> 'Suite': class Launch(Suite): """Class represents Robot Framework test suite.""" + launch_attributes: Optional[List[Dict[str, str]]] type: str = 'LAUNCH' - def __init__(self, name: str, robot_attributes: Dict[str, Any], launch_attributes: Optional[List[Dict[str, str]]]): + def __init__(self, name: str, robot_attributes: Dict[str, Any], launch_attributes: Optional[List[str]]): """Initialize required attributes. :param name: Launch name :param robot_attributes: Suite attributes passed through the listener + :param launch_attributes: Launch attributes from variables """ super().__init__(name, robot_attributes) + self.launch_attributes = gen_attributes(launch_attributes or []) self.type = 'LAUNCH' + @property + def attributes(self) -> Optional[List[Dict[str, str]]]: + """Get Launch attributes.""" + return self.launch_attributes + class Test: """Class represents Robot Framework test case.""" @@ -102,7 +119,7 @@ class Test: _critical: str _tags: List[str] robot_attributes: Dict[str, Any] - attributes: List[Dict[str, str]] + test_attributes: Optional[List[Dict[str, str]]] doc: str end_time: str longname: str @@ -116,16 +133,17 @@ class Test: template: str type: str = 'TEST' - def __init__(self, name: str, robot_attributes: Dict[str, Any]): + def __init__(self, name: str, robot_attributes: Dict[str, Any], test_attributes: List[str]): """Initialize required attributes. - :param name: Name of the test - :param robot_attributes: Test attributes passed through the listener + :param name: Name of the test + :param robot_attributes: Attributes passed through the listener """ # for backward compatibility with Robot < 4.0 mark every test case # as critical if not set self._critical = robot_attributes.get('critical', 'yes') self._tags = robot_attributes['tags'] + self.test_attributes = gen_attributes(test_attributes) self.robot_attributes = robot_attributes self.doc = robot_attributes['doc'] self.end_time = robot_attributes.get('endtime', '') @@ -140,6 +158,11 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any]): self.template = robot_attributes['template'] self.type = 'TEST' + @property + def attributes(self) -> Optional[List[Dict[str, str]]]: + """Get Test attributes.""" + return self.test_attributes + gen_attributes(self._tags) + @property def critical(self) -> bool: """Form unique value for RF 4.0+ and older versions.""" @@ -213,9 +236,9 @@ class Keyword: def __init__(self, name: str, robot_attributes: Dict[str, Any], parent_type: Optional[str] = None): """Initialize required attributes. - :param name: Name of the keyword - :param robot_attributes: Keyword attributes passed through the listener - :param parent_type: Type of the parent test item + :param name: Name of the keyword + :param robot_attributes: Attributes passed through the listener + :param parent_type: Type of the parent test item """ self.robot_attributes = robot_attributes self.args = robot_attributes['args'] @@ -264,7 +287,7 @@ def update(self, attributes: Dict[str, Any]) -> 'Keyword': return self -class LogMessage: +class LogMessage(str): """Class represents Robot Framework messages.""" attachment: Optional[Dict[str, str]] diff --git a/robotframework_reportportal/service.py b/robotframework_reportportal/service.py index 7aa16fb..8cc1869 100644 --- a/robotframework_reportportal/service.py +++ b/robotframework_reportportal/service.py @@ -111,16 +111,16 @@ def start_launch(self, launch: Launch, mode: Optional[str] = None, rerun: bool = ts: Optional[str] = None) -> Optional[str]: """Call start_launch method of the common client. - :param launch: Instance of the Launch class - :param mode: Launch mode - :param rerun: Rerun mode. Allowable values 'True' of 'False' - :param rerun_of: Rerun mode. Specifies launch to be re-runned. - Should be used with the 'rerun' option. - :param ts: Start time - :return: launch UUID + :param launch: Instance of the Launch class + :param mode: Launch mode + :param rerun: Rerun mode. Allowable values 'True' of 'False' + :param rerun_of: Rerun mode. Specifies launch to be re-run. + Should be used with the 'rerun' option. + :param ts: Start time + :return: launch UUID """ sl_pt = { - 'attributes': self._get_launch_attributes(launch.robot_attributes), + 'attributes': self._get_launch_attributes(launch.attributes), 'description': launch.doc, 'name': launch.name, 'mode': mode, @@ -152,7 +152,7 @@ def start_suite(self, suite: Suite, ts: Optional[str] = None) -> Optional[str]: :return: Suite UUID """ start_rq = { - 'attributes': None, + 'attributes': suite.attributes, 'description': suite.doc, 'item_type': suite.type, 'name': suite.name, diff --git a/robotframework_reportportal/variables.py b/robotframework_reportportal/variables.py index 2b62f1a..a12727b 100644 --- a/robotframework_reportportal/variables.py +++ b/robotframework_reportportal/variables.py @@ -15,7 +15,7 @@ from distutils.util import strtobool from os import path -from typing import Optional, Union, Dict, Tuple, Any +from typing import Optional, Union, Dict, Tuple, Any, List from warnings import warn from reportportal_client import OutputType, ClientType @@ -42,27 +42,27 @@ def get_variable(name: str, default: Optional[str] = None) -> Optional[str]: class Variables: """This class stores Robot Framework variables related to ReportPortal.""" - enabled: bool = ... - endpoint: Optional[str] = ... - launch_name: Optional[str] = ... - _pabot_pool_id: Optional[int] = ... - _pabot_used: Optional[str] = ... - project: Optional[str] = ... - api_key: Optional[str] = ... - attach_log: bool = ... - attach_report: bool = ... - attach_xunit: bool = ... - launch_attributes: list = ... - launch_id: Optional[str] = ... - launch_doc: Optional[str] = ... - log_batch_size: Optional[int] = ... - mode: Optional[str] = ... - pool_size: Optional[int] = ... - rerun: bool = ... - rerun_of: Optional[str] = ... - test_attributes: Optional[list] = ... - skipped_issue: bool = ... - log_batch_payload_size: int = ... + enabled: bool + endpoint: Optional[str] + launch_name: Optional[str] + _pabot_pool_id: Optional[int] + _pabot_used: Optional[str] + project: Optional[str] + api_key: Optional[str] + attach_log: bool + attach_report: bool + attach_xunit: bool + launch_attributes: List[str] + launch_id: Optional[str] + launch_doc: Optional[str] + log_batch_size: Optional[int] + mode: Optional[str] + pool_size: Optional[int] + rerun: bool + rerun_of: Optional[str] + test_attributes: List[str] + skipped_issue: bool + log_batch_payload_size: int launch_uuid_print: bool launch_uuid_print_output: Optional[OutputType] client_type: ClientType @@ -78,12 +78,9 @@ def __init__(self) -> None: self._pabot_used = None self.attach_log = bool(strtobool(get_variable( 'RP_ATTACH_LOG', default='False'))) - self.attach_report = bool(strtobool(get_variable( - 'RP_ATTACH_REPORT', default='False'))) - self.attach_xunit = bool(strtobool(get_variable( - 'RP_ATTACH_XUNIT', default='False'))) - self.launch_attributes = get_variable( - 'RP_LAUNCH_ATTRIBUTES', default='').split() + self.attach_report = bool(strtobool(get_variable('RP_ATTACH_REPORT', default='False'))) + self.attach_xunit = bool(strtobool(get_variable('RP_ATTACH_XUNIT', default='False'))) + self.launch_attributes = get_variable('RP_LAUNCH_ATTRIBUTES', default='').split() self.launch_id = get_variable('RP_LAUNCH_UUID') self.launch_doc = get_variable('RP_LAUNCH_DOC') self.log_batch_size = int(get_variable( @@ -95,11 +92,9 @@ def __init__(self) -> None: self.rerun_of = get_variable('RP_RERUN_OF', default=None) self.skipped_issue = bool(strtobool(get_variable( 'RP_SKIPPED_ISSUE', default='True'))) - self.test_attributes = get_variable( - 'RP_TEST_ATTRIBUTES', default='').split() - self.log_batch_payload_size = int(get_variable( - 'RP_LOG_BATCH_PAYLOAD_SIZE', - default=str(MAX_LOG_BATCH_PAYLOAD_SIZE))) + self.test_attributes = get_variable('RP_TEST_ATTRIBUTES', default='').split() + self.log_batch_payload_size = int(get_variable('RP_LOG_BATCH_PAYLOAD_SIZE', + default=str(MAX_LOG_BATCH_PAYLOAD_SIZE))) self.launch_uuid_print = bool(strtobool(get_variable('RP_LAUNCH_UUID_PRINT', default='False'))) output_type = get_variable('RP_LAUNCH_UUID_PRINT_OUTPUT') self.launch_uuid_print_output = OutputType[output_type.upper()] if output_type else None From 88fe51fad174526a345d41297094d4edd0568857 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 15:32:54 +0300 Subject: [PATCH 06/13] CHANGELOG.md update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e2a8a..01fc344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Added +- Issue [#178](https://github.com/reportportal/agent-Python-RobotFramework/issues/178) Metadata attributes handling, by @HardNorth ### Removed - `model.pyi`, `static.pyi` stub files, as we don't really need them anymore, by @HardNorth From 79cd074241e1f349516b8e335081d7d1b7365b3a Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:05:41 +0300 Subject: [PATCH 07/13] Type fixing --- robotframework_reportportal/logger.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/robotframework_reportportal/logger.py b/robotframework_reportportal/logger.py index 00194b0..cc54dab 100644 --- a/robotframework_reportportal/logger.py +++ b/robotframework_reportportal/logger.py @@ -35,15 +35,15 @@ def log_free_memory(self): }, ) """ -from typing import Literal, Optional, Dict +from typing import Optional, Dict from robot.api import logger from .model import LogMessage -def write(msg: str, level: logger.LOGLEVEL = 'INFO', html: bool = False, attachment: Optional[Dict[str, str]] = None, - launch_log: bool = False): +def write(msg: str, level: str = 'INFO', html: bool = False, attachment: Optional[Dict[str, str]] = None, + launch_log: bool = False) -> None: """Write the message to the log file using the given level. Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default since RF 2.9.1), ``WARN``, @@ -68,18 +68,18 @@ def write(msg: str, level: logger.LOGLEVEL = 'INFO', html: bool = False, attachm logger.write(log_message, level, html) -def trace(msg, html=False, attachment=None, launch_log=False): +def trace(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None: """Write the message to the log file using the ``TRACE`` level.""" write(msg, "TRACE", html, attachment, launch_log) -def debug(msg, html=False, attachment=None, launch_log=False): +def debug(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None: """Write the message to the log file using the ``DEBUG`` level.""" write(msg, "DEBUG", html, attachment, launch_log) -def info(msg, html=False, also_console=False, attachment=None, - launch_log=False): +def info(msg: str, html: bool = False, also_console: bool = False, attachment: Optional[Dict[str, str]] = None, + launch_log: bool = False): """Write the message to the log file using the ``INFO`` level. If ``also_console`` argument is set to ``True``, the message is written both to the log file and to the console. @@ -89,17 +89,17 @@ def info(msg, html=False, also_console=False, attachment=None, console(msg) -def warn(msg, html=False, attachment=None, launch_log=False): +def warn(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None: """Write the message to the log file using the ``WARN`` level.""" - write(msg, "WARN", html, attachment, launch_log) + write(msg, 'WARN', html, attachment, launch_log) -def error(msg, html=False, attachment=None, launch_log=False): +def error(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None: """Write the message to the log file using the ``ERROR`` level.""" - write(msg, "ERROR", html, attachment, launch_log) + write(msg, 'ERROR', html, attachment, launch_log) -def console(msg: str, newline: bool = True, stream: Literal['stdout', 'stderr'] = 'stdout'): +def console(msg: str, newline: bool = True, stream: str = 'stdout') -> None: """Write the message to the console. If the ``newline`` argument is ``True``, a newline character is automatically added to the message. From 5fdaf19e3980d7d7b3a17ad335b7534b76eba608 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:06:01 +0300 Subject: [PATCH 08/13] Format fix --- robotframework_reportportal/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robotframework_reportportal/logger.py b/robotframework_reportportal/logger.py index cc54dab..0494971 100644 --- a/robotframework_reportportal/logger.py +++ b/robotframework_reportportal/logger.py @@ -78,7 +78,7 @@ def debug(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = N write(msg, "DEBUG", html, attachment, launch_log) -def info(msg: str, html: bool = False, also_console: bool = False, attachment: Optional[Dict[str, str]] = None, +def info(msg: str, html: bool = False, also_console: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False): """Write the message to the log file using the ``INFO`` level. From 6b430940d0eaf0d318a939264b2777eee9f02377 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:30:14 +0300 Subject: [PATCH 09/13] Client version update --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6286d8a..83cee74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # Basic dependencies python-dateutil~=2.8.1 -reportportal-client~=5.5.5 +reportportal-client~=5.5.6 robotframework From 6514abf1e8a4d1f90dcdd9409ca866bdd636f760 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:30:38 +0300 Subject: [PATCH 10/13] Client version update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01fc344..35ce4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added - Issue [#178](https://github.com/reportportal/agent-Python-RobotFramework/issues/178) Metadata attributes handling, by @HardNorth +### Changed +- Client version updated on [5.5.6](https://github.com/reportportal/client-Python/releases/tag/5.5.6), by @HardNorth ### Removed - `model.pyi`, `static.pyi` stub files, as we don't really need them anymore, by @HardNorth From ab57017c8846f98a3f951330034a0a4dcee02a5b Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:32:47 +0300 Subject: [PATCH 11/13] Fix test_case_id tag passing to attributes --- robotframework_reportportal/model.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/robotframework_reportportal/model.py b/robotframework_reportportal/model.py index 22fa519..d530987 100644 --- a/robotframework_reportportal/model.py +++ b/robotframework_reportportal/model.py @@ -158,11 +158,6 @@ def __init__(self, name: str, robot_attributes: Dict[str, Any], test_attributes: self.template = robot_attributes['template'] self.type = 'TEST' - @property - def attributes(self) -> Optional[List[Dict[str, str]]]: - """Get Test attributes.""" - return self.test_attributes + gen_attributes(self._tags) - @property def critical(self) -> bool: """Form unique value for RF 4.0+ and older versions.""" @@ -171,8 +166,12 @@ def critical(self) -> bool: @property def tags(self) -> List[str]: """Get list of test tags excluding test_case_id.""" - return [ - tag for tag in self._tags if not tag.startswith('test_case_id')] + return [tag for tag in self._tags if not tag.startswith('test_case_id')] + + @property + def attributes(self) -> Optional[List[Dict[str, str]]]: + """Get Test attributes.""" + return self.test_attributes + gen_attributes(self.tags) @property def source(self) -> str: From 18826792a3f842da764c701ee9b937b5467db53d Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:32:59 +0300 Subject: [PATCH 12/13] Remove redundant constant --- robotframework_reportportal/listener.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index 2918d56..4de50fa 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -20,7 +20,6 @@ import re from functools import wraps from mimetypes import guess_type -from types import MappingProxyType from typing import Optional, Dict, Union, Any from warnings import warn @@ -34,24 +33,6 @@ logger = logging.getLogger(__name__) VARIABLE_PATTERN = r'^\s*\${[^}]*}\s*=\s*' TRUNCATION_SIGN = "...'" -CONTENT_TYPE_TO_EXTENSIONS = MappingProxyType({ - 'application/pdf': 'pdf', - 'application/zip': 'zip', - 'application/java-archive': 'jar', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/gif': 'gif', - 'image/bmp': 'bmp', - 'image/vnd.microsoft.icon': 'ico', - 'image/webp': 'webp', - 'audio/mpeg': 'mp3', - 'audio/wav': 'wav', - 'video/mpeg': 'mpeg', - 'video/avi': 'avi', - 'video/webm': 'webm', - 'text/plain': 'txt', - 'application/octet-stream': 'bin' -}) def _unescape(binary_string: str, stop_at: int = -1): From 0b43da911019175c5fdf546a91595163584a2706 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 20 Mar 2024 16:44:57 +0300 Subject: [PATCH 13/13] Add suite metadata tests --- examples/suite_metadata.robot | 6 +++ tests/integration/test_suite_metadata.py | 60 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 examples/suite_metadata.robot create mode 100644 tests/integration/test_suite_metadata.py diff --git a/examples/suite_metadata.robot b/examples/suite_metadata.robot new file mode 100644 index 0000000..6ffb626 --- /dev/null +++ b/examples/suite_metadata.robot @@ -0,0 +1,6 @@ +*** Settings *** +Metadata Author John Doe + +*** Test Cases *** +Simple test + Log Hello, world! diff --git a/tests/integration/test_suite_metadata.py b/tests/integration/test_suite_metadata.py new file mode 100644 index 0000000..2eaa7e7 --- /dev/null +++ b/tests/integration/test_suite_metadata.py @@ -0,0 +1,60 @@ +# 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 tests.helpers import utils +from unittest import mock + +from tests import REPORT_PORTAL_SERVICE + + +SIMPLE_TEST = 'examples/suite_metadata.robot' + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_suite_metadata_simple(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]['attributes'] == [{'key': 'Author', 'value': 'John Doe'}] + + +@mock.patch(REPORT_PORTAL_SERVICE) +def test_suite_metadata_command_line_simple(mock_client_init): + result = utils.run_robot_tests([SIMPLE_TEST], arguments={'--metadata': 'Scope:Smoke'}) + 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] + attributes = test_suite[1]['attributes'] + assert len(attributes) == 2 + assert {'value': 'Smoke', 'key': 'Scope'} in attributes + assert {'key': 'Author', 'value': 'John Doe'} in attributes