From 0bf36d7f8aecec126c1adae9641ae59c77e3bb53 Mon Sep 17 00:00:00 2001 From: Ivan Ivanou <8349078+iivanou@users.noreply.github.com> Date: Mon, 6 Dec 2021 17:14:13 +0300 Subject: [PATCH] New client implementation --- reportportal_client/client.py | 200 +++++++++++++++++------ reportportal_client/client.pyi | 18 +- reportportal_client/core/rp_requests.py | 62 ++++--- reportportal_client/core/rp_requests.pyi | 24 +-- reportportal_client/core/rp_responses.py | 4 +- reportportal_client/helpers.py | 105 ++---------- setup.py | 2 +- 7 files changed, 230 insertions(+), 185 deletions(-) diff --git a/reportportal_client/client.py b/reportportal_client/client.py index 301319bf..5d3cac9d 100644 --- a/reportportal_client/client.py +++ b/reportportal_client/client.py @@ -15,18 +15,18 @@ limitations under the License. """ import logging - import requests from requests.adapters import HTTPAdapter from reportportal_client.core.log_manager import LogManager -from reportportal_client.core.test_manager import TestManager from reportportal_client.core.rp_requests import ( HttpRequest, + ItemStartRequest, + ItemFinishRequest, LaunchStartRequest, LaunchFinishRequest ) -from reportportal_client.helpers import uri_join +from reportportal_client.helpers import uri_join, verify_value_length logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -83,8 +83,6 @@ def __init__(self, self._log_manager = LogManager( self.endpoint, self.session, self.api_v2, self.launch_id, self.project, log_batch_size=log_batch_size) - self._test_manager = TestManager( - self.session, self.endpoint, project, self.launch_id) def finish_launch(self, end_time, @@ -101,14 +99,15 @@ def finish_launch(self, """ url = uri_join(self.base_url_v2, 'launch', self.launch_id, 'finish') request_payload = LaunchFinishRequest( - end_time=end_time, + end_time, status=status, attributes=attributes, - **kwargs + description=kwargs.get('description') ).payload response = HttpRequest(self.session.put, url=url, json=request_payload, verify_ssl=self.verify_ssl).make() logger.debug('finish_launch - ID: %s', self.launch_id) + logger.debug('response message: %s', response.message) return response.message def finish_test_item(self, @@ -117,33 +116,100 @@ def finish_test_item(self, status, issue=None, attributes=None, + description=None, + retry=False, **kwargs): """Finish suite/case/step/nested step item. - :param item_id: id of the test item - :param end_time: time in UTC format - :param status: status of the test - :param issue: description of an issue - :param attributes: list of attributes - :param kwargs: other parameters - :return: json message + :param item_id: ID of the test item + :param end_time: Test item end time + :param status: Test status. Allowable values: "passed", + "failed", "stopped", "skipped", "interrupted", + "cancelled" + :param attributes: Test item attributes(tags). Pairs of key and value. + Overrides attributes on start + :param description: Test item description. Overrides description + from start request. + :param issue: Issue of the current test item + :param retry: Used to report retry of the test. Allowable values: + "True" or "False" + """ + url = uri_join(self.base_url_v2, 'item', item_id) + request_payload = ItemFinishRequest( + end_time, + self.launch_id, + status, + attributes=attributes, + description=description, + is_skipped_an_issue=self.is_skipped_an_issue, + issue=issue, + retry=retry + ).payload + response = HttpRequest(self.session.put, url=url, json=request_payload, + verify_ssl=self.verify_ssl).make() + logger.debug('finish_test_item - ID: %s', item_id) + logger.debug('response message: %s', response.message) + return response.message + + def get_item_id_by_uuid(self, uuid): + """Get test item ID by the given UUID. + + :param uuid: UUID returned on the item start + :return: Test item ID + """ + url = uri_join(self.base_url_v1, 'item', 'uuid', uuid) + response = HttpRequest(self.session.get, url=url, + verify_ssl=self.verify_ssl).make() + return response.id + + def get_launch_info(self): + """Get the current launch information. + + :return dict: Launch information in dictionary + """ + if self.launch_id is None: + return {} + url = uri_join(self.base_url_v1, 'launch', 'uuid', self.launch_id) + logger.debug('get_launch_info - ID: %s', self.launch_id) + response = HttpRequest(self.session.get, url=url, + verify_ssl=self.verify_ssl).make() + if response.is_success: + launch_info = response.json + logger.debug( + 'get_launch_info - Launch info: %s', response.json) + else: + logger.warning('get_launch_info - Launch info: ' + 'Failed to fetch launch ID from the API.') + launch_info = {} + return launch_info + + def get_launch_ui_id(self): + """Get UI ID of the current launch. + + :return: UI ID of the given launch. None if UI ID has not been found. + """ + return self.get_launch_info().get('id') + + def get_launch_ui_url(self): + """Get UI URL of the current launch. + + :return: launch URL or all launches URL. """ - self._test_manager.finish_test_item(self.api_v2, - item_id, - end_time, - status, - issue=issue, - attributes=attributes, - **kwargs) + ui_id = self.get_launch_ui_id() or '' + path = 'ui/#{0}/launches/all/{1}'.format(self.project, ui_id) + url = uri_join(self.endpoint, path) + logger.debug('get_launch_ui_url - ID: %s', self.launch_id) + return url def get_project_settings(self): - """Get settings from project. + """Get project settings. - :return: json body + :return: HTTP response in dictionary """ url = uri_join(self.base_url_v1, 'settings') - r = self.session.get(url=url, json={}, verify=self.verify_ssl) - return r.json() + response = HttpRequest(self.session.get, url=url, json={}, + verify_ssl=self.verify_ssl).make() + return response.json def log(self, time, message, level=None, attachment=None, item_id=None): """Send log message to the Report Portal. @@ -168,8 +234,7 @@ def start_launch(self, mode=None, rerun=False, rerun_of=None, - **kwargs - ): + **kwargs): """Start a new launch with the given parameters. :param name: Launch name @@ -189,14 +254,14 @@ def start_launch(self, description=description, mode=mode, rerun=rerun, - rerun_of=rerun_of, + rerun_of=rerun_of or kwargs.get('rerunOf'), **kwargs ).payload response = HttpRequest(self.session.post, url=url, json=request_payload, verify_ssl=self.verify_ssl).make() - self._test_manager.launch_id = self.launch_id = response.id + self._log_manager.launch_id = self.launch_id = response.id logger.debug('start_launch - ID: %s', self.launch_id) return self.launch_id @@ -210,31 +275,68 @@ def start_test_item(self, parent_item_id=None, has_stats=True, code_ref=None, + retry=False, **kwargs): """Start case/step/nested step item. - :param name: Name of test item - :param start_time: Test item start time - :param item_type: Type of test item - :param description: Test item description - :param attributes: Test item attributes - :param parameters: Test item parameters - :param parent_item_id: Parent test item UUID - :param has_stats: Does test item has stats or not - :param code_ref: Test item code reference + :param name: Name of the test item + :param start_time: Test item start time + :param item_type: Type of the test item. Allowable values: "suite", + "story", "test", "scenario", "step", + "before_class", "before_groups", "before_method", + "before_suite", "before_test", "after_class", + "after_groups", "after_method", "after_suite", + "after_test" + :param attributes: Test item attributes + :param code_ref: Physical location of the test item + :param description: Test item description + :param has_stats: Set to False if test item is nested step + :param parameters: Set of parameters (for parametrized test items) + :param retry: Used to report retry of the test. Allowable values: + "True" or "False" """ - return self._test_manager.start_test_item(self.api_v2, - name, - start_time, - item_type, - description=description, - attributes=attributes, - parameters=parameters, - parent_uuid=parent_item_id, - has_stats=has_stats, - code_ref=code_ref, - **kwargs) + if parent_item_id: + url = uri_join(self.base_url_v2, 'item', parent_item_id) + else: + url = uri_join(self.base_url_v2, 'item') + request_payload = ItemStartRequest( + name, + start_time, + item_type, + self.launch_id, + attributes=verify_value_length(attributes), + code_ref=code_ref, + description=description, + has_stats=has_stats, + parameters=parameters, + retry=retry + ).payload + response = HttpRequest(self.session.post, + url=url, + json=request_payload, + verify_ssl=self.verify_ssl).make() + logger.debug('start_test_item - ID: %s', response.id) + return response.id def terminate(self, *args, **kwargs): """Call this to terminate the client.""" self._log_manager.stop() + + def update_test_item(self, item_uuid, attributes=None, description=None): + """Update existing test item at the Report Portal. + + :param str item_uuid: Test item UUID returned on the item start + :param str description: Test item description + :param list attributes: Test item attributes + [{'key': 'k_name', 'value': 'k_value'}, ...] + """ + data = { + 'description': description, + 'attributes': verify_value_length(attributes), + } + item_id = self.get_item_id_by_uuid(item_uuid) + url = uri_join(self.base_url_v1, 'item', item_id, 'update') + response = HttpRequest(self.session.put, url=url, json=data, + verify_ssl=self.verify_ssl).make() + logger.debug('update_test_item - Item: %s', item_id) + return response.message diff --git a/reportportal_client/client.pyi b/reportportal_client/client.pyi index 44169690..730d9bf3 100644 --- a/reportportal_client/client.pyi +++ b/reportportal_client/client.pyi @@ -1,11 +1,10 @@ from requests import Session from typing import Any, Dict, List, Optional, Text, Tuple, Union from reportportal_client.core.log_manager import LogManager as LogManager -from reportportal_client.core.test_manager import TestManager as TestManager +from reportportal_client.core.rp_issues import Issue as Issue class RPClient: _log_manager: LogManager = ... - _test_manager: TestManager = ... api_v1: Text = ... api_v2: Text = ... base_url_v1: Text = ... @@ -30,15 +29,19 @@ class RPClient: def finish_launch(self, end_time: Text, status: Text = ..., - attributes: List = ..., + attributes: Optional[Union[List, Dict]] = ..., **kwargs: Any) -> Dict: ... def finish_test_item(self, item_id: Text, end_time: Text, status: Text, - issue: Text = ..., + issue: Optional[Issue] = ..., attributes: List = ..., **kwargs: Any) -> None: ... + def get_item_id_by_uuid(self, uuid: Text) -> Text: ... + def get_launch_info(self) -> Dict: ... + def get_launch_ui_id(self) -> Optional[Dict]: ... + def get_launch_ui_url(self) -> Text: ... def get_project_settings(self) -> Dict: ... def log(self, time: Text, @@ -50,7 +53,7 @@ class RPClient: name: Text, start_time: Text, description: Text = ..., - attributes: List = ..., + attributes: Optional[Union[List, Dict]] = ..., mode: Text = ..., rerun: bool = ..., rerun_of: Text = ..., @@ -60,10 +63,11 @@ class RPClient: start_time: Text, item_type: Text, description: Text = ..., - attributes: List = ..., - parameters: dict = ..., + attributes: Optional[Union[List, Dict]] = ..., + parameters: Dict = ..., parent_item_id: Text = ..., has_stats: bool = ..., code_ref: Text = ..., **kwargs: Any) -> Text: ... def terminate(self, *args: Tuple, **kwargs: Any) -> None: ... + def update_test_item(self, item_uuid: Text, attributes: Optional[Union[List, Dict]], description: Optional[Text]): diff --git a/reportportal_client/core/rp_requests.py b/reportportal_client/core/rp_requests.py index c57f9d41..284565be 100644 --- a/reportportal_client/core/rp_requests.py +++ b/reportportal_client/core/rp_requests.py @@ -24,6 +24,7 @@ from reportportal_client.core.rp_file import RPFile from reportportal_client.core.rp_issues import Issue +from reportportal_client.helpers import dict_to_payload from reportportal_client.static.abstract import ( AbstractBaseClass, abstractmethod @@ -136,8 +137,7 @@ def __init__(self, description=None, mode='default', rerun=False, - rerun_of=None, - uuid=None): + rerun_of=None): """Initialize instance attributes. :param name: Name of the launch @@ -148,7 +148,6 @@ def __init__(self, :param rerun: Rerun mode. Allowable values 'True' of 'False' :param rerun_of: Rerun mode. Specifies launch to be re-runned. Uses with the 'rerun' attribute. - :param uuid: Launch uuid (string identifier) """ super(LaunchStartRequest, self).__init__() self.attributes = attributes @@ -158,11 +157,12 @@ def __init__(self, self.rerun = rerun self.rerun_of = rerun_of self.start_time = start_time - self.uuid = uuid @property def payload(self): """Get HTTP payload for the request.""" + if self.attributes and isinstance(self.attributes, dict): + self.attributes = dict_to_payload(self.attributes) return { 'attributes': self.attributes, 'description': self.description, @@ -170,8 +170,7 @@ def payload(self): 'name': self.name, 'rerun': self.rerun, 'rerunOf': self.rerun_of, - 'startTime': self.start_time, - 'uuid': self.uuid + 'startTime': self.start_time } @@ -205,6 +204,8 @@ def __init__(self, @property def payload(self): """Get HTTP payload for the request.""" + if self.attributes and isinstance(self.attributes, dict): + self.attributes = dict_to_payload(self.attributes) return { 'attributes': self.attributes, 'description': self.description, @@ -230,6 +231,7 @@ def __init__(self, has_stats=True, parameters=None, retry=False, + test_case_id=None, uuid=None, unique_id=None): """Initialize instance attributes. @@ -250,6 +252,7 @@ def __init__(self, :param parameters: Set of parameters (for parametrized test items) :param retry: Used to report retry of the test. Allowable values: "True" or "False" + :param test_case_id:Test case ID from integrated TMS :param uuid: Test item UUID (auto generated) :param unique_id: Test item ID (auto generated) """ @@ -263,6 +266,7 @@ def __init__(self, self.parameters = parameters self.retry = retry self.start_time = start_time + self.test_case_id = test_case_id self.type_ = type_ self.uuid = uuid self.unique_id = unique_id @@ -270,6 +274,10 @@ def __init__(self, @property def payload(self): """Get HTTP payload for the request.""" + if self.attributes and isinstance(self.attributes, dict): + self.attributes = dict_to_payload(self.attributes) + if self.parameters: + self.parameters = dict_to_payload(self.parameters) return { 'attributes': self.attributes, 'codeRef': self.code_ref, @@ -280,7 +288,8 @@ def payload(self): 'parameters': self.parameters, 'retry': self.retry, 'startTime': self.start_time, - 'type': self.type_, + 'testCaseId': self.test_case_id, + 'type': self.type_ } @@ -296,27 +305,31 @@ def __init__(self, status, attributes=None, description=None, + is_skipped_an_issue=True, issue=None, retry=False): """Initialize instance attributes. - :param end_time: Test item end time - :param launch_uuid: Parent launch UUID - :param status: Test status. Allowable values: "passed", - "failed", "stopped", "skipped", "interrupted", - "cancelled" - :param attributes: Test item attributes(tags). Pairs of key and value. - Overrides attributes on start - :param description: Test item description. Overrides description - from start request. - :param issue: Issue of the current test item - :param retry: Used to report retry of the test. Allowable values: - "True" or "False" + :param end_time: Test item end time + :param launch_uuid: Parent launch UUID + :param status: Test status. Allowable values: "passed", + "failed", "stopped", "skipped", + "interrupted", "cancelled". + :param attributes: Test item attributes(tags). Pairs of key + and value. Overrides attributes on start + :param description: Test item description. Overrides + description from start request. + :param is_skipped_an_issue: Option to mark skipped tests as not + 'To Investigate' items in UI + :param issue: Issue of the current test item + :param retry: Used to report retry of the test. + Allowable values: "True" or "False" """ super(ItemFinishRequest, self).__init__() self.attributes = attributes self.description = description self.end_time = end_time + self.is_skipped_an_issue = is_skipped_an_issue self.issue = issue # type: Issue self.launch_uuid = launch_uuid self.status = status @@ -325,12 +338,19 @@ def __init__(self, @property def payload(self): """Get HTTP payload for the request.""" + if self.attributes and isinstance(self.attributes, dict): + self.attributes = dict_to_payload(self.attributes) + if self.issue is None and self.status.lower() == 'skipped' and not \ + self.is_skipped_an_issue: + issue_payload = {'issue_type': 'NOT_ISSUE'} + else: + issue_payload = None return { 'attributes': self.attributes, 'description': self.description, 'endTime': self.end_time, - 'issue': self.issue.payload, - 'launch_uuid': self.launch_uuid, + 'issue': getattr(self.issue, 'payload', issue_payload), + 'launchUuid': self.launch_uuid, 'status': self.status, 'retry': self.retry } diff --git a/reportportal_client/core/rp_requests.pyi b/reportportal_client/core/rp_requests.pyi index 82025585..0ad61b5d 100644 --- a/reportportal_client/core/rp_requests.pyi +++ b/reportportal_client/core/rp_requests.pyi @@ -44,7 +44,7 @@ class RPRequestBase(metaclass=AbstractBaseClass): def payload(self) -> Dict: ... class LaunchStartRequest(RPRequestBase): - attributes: List = ... + attributes: Optional[Union[List, Dict]] = ... description: Text = ... mode: Text = ... name: Text = ... @@ -55,7 +55,7 @@ class LaunchStartRequest(RPRequestBase): def __init__(self, name: Text, start_time: Text, - attributes: Optional[List] = ..., + attributes: Optional[Union[List, Dict]] = ..., description: Optional[Text] = ..., mode: Text = ..., rerun: bool = ..., @@ -65,28 +65,29 @@ class LaunchStartRequest(RPRequestBase): def payload(self) -> Dict: ... class LaunchFinishRequest(RPRequestBase): - attributes: List = ... + attributes: Optional[Union[List, Dict]] = ... description: Text = ... end_time: Text = ... status: Text = ... def __init__(self, end_time: Text, status: Optional[Text] = ..., - attributes: Optional[List] = ..., + attributes: Optional[Union[List, Dict]] = ..., description: Optional[Text] = ...) -> None: ... @property def payload(self) -> Dict: ... class ItemStartRequest(RPRequestBase): - attributes: List = ... + attributes: Optional[Union[List, Dict]] = ... code_ref: Text = ... description: Text = ... has_stats: bool = ... launch_uuid: Text = ... name: Text = ... - parameters: List = ... + parameters: Optional[Union[List, Dict]] = ... retry: bool = ... start_time: Text = ... + test_case_id: Optional[Text] = ... type_: Text = ... uuid: Text = ... unique_id: Text = ... @@ -95,21 +96,23 @@ class ItemStartRequest(RPRequestBase): start_time: Text, type_: Text, launch_uuid: Text, - attributes: Optional[List] = ..., + attributes: Optional[Union[List, Dict]] = ..., code_ref: Optional[Text] = ..., description: Optional[Text] = ..., has_stats: bool = ..., - parameters: Optional[List] = ..., + parameters: Optional[Union[List, Dict]] = ..., retry: bool = ..., + test_case_id: Optional[Text] = ..., uuid: Optional[Any] = ..., unique_id: Optional[Any] = ...) -> None: ... @property def payload(self) -> Dict: ... class ItemFinishRequest(RPRequestBase): - attributes: List = ... + attributes: Optional[Union[List, Dict]] = ... description: Text = ... end_time: Text = ... + is_skipped_an_issue: bool = ... issue: Issue = ... launch_uuid: Text = ... status: Text = ... @@ -118,8 +121,9 @@ class ItemFinishRequest(RPRequestBase): end_time: Text, launch_uuid: Text, status: Text, - attributes: Optional[List] = ..., + attributes: Optional[Union[List, Dict]] = ..., description: Optional[Any] = ..., + is_skipped_an_issue: bool = ..., issue: Optional[Issue] = ..., retry: bool = ...) -> None: ... @property diff --git a/reportportal_client/core/rp_responses.py b/reportportal_client/core/rp_responses.py index cffcbb33..696291f2 100644 --- a/reportportal_client/core/rp_responses.py +++ b/reportportal_client/core/rp_responses.py @@ -102,8 +102,8 @@ def json(self): @property def message(self): - """Get value of the 'msg' key.""" - return self.json.get('msg', NOT_FOUND) + """Get value of the 'message' key.""" + return self.json.get('message', NOT_FOUND) @property def messages(self): diff --git a/reportportal_client/helpers.py b/reportportal_client/helpers.py index e3073507..3956391a 100644 --- a/reportportal_client/helpers.py +++ b/reportportal_client/helpers.py @@ -19,14 +19,13 @@ import six from pkg_resources import DistributionNotFound, get_distribution -from .errors import ResponseError, EntryCreatedError, OperationCompletionError from .static.defines import ATTRIBUTE_LENGTH_LIMIT logger = logging.getLogger(__name__) def generate_uuid(): - """Generate Uuid.""" + """Generate UUID.""" return str(uuid.uuid4()) @@ -43,14 +42,18 @@ def convert_string(value): def dict_to_payload(dictionary): - """Convert dict to list of dicts. + """Convert incoming dictionary to the list of dictionaries. - :param dictionary: initial dict - :return list: list of dicts + This function transforms the given dictionary of tags/attributes into + the format required by the Report Portal API. Also, we add the system + key to every tag/attribute that indicates that the key should be hidden + from the user in UI. + :param dictionary: Dictionary containing tags/attributes + :return list: List of tags/attributes in the required format """ - system = dictionary.pop('system', False) + hidden = dictionary.pop('system', False) return [ - {'key': key, 'value': convert_string(value), 'system': system} + {'key': key, 'value': convert_string(value), 'system': hidden} for key, value in sorted(dictionary.items()) ] @@ -146,98 +149,10 @@ def uri_join(*uri_parts): Avoiding usage of urlparse.urljoin and os.path.join as it does not clearly join parts. - Args: *uri_parts: tuple of values for join, can contain back and forward slashes (will be stripped up). - Returns: An uri string. - """ return '/'.join(str(s).strip('/').strip('\\') for s in uri_parts) - - -def get_id(response): - """Get id from Response. - - :param response: Response object - :return id: int value of id - """ - try: - return get_data(response)["id"] - except KeyError: - raise EntryCreatedError( - "No 'id' in response: {0}".format(response.text)) - - -def get_msg(response): - """ - Get message from Response. - - :param response: Response object - :return: data: json data - """ - try: - return get_data(response) - except KeyError: - raise OperationCompletionError( - "No 'message' in response: {0}".format(response.text)) - - -def get_data(response): - """ - Get data from Response. - - :param response: Response object - :return: json data - """ - data = get_json(response) - error_messages = get_error_messages(data) - error_count = len(error_messages) - - if error_count == 1: - raise ResponseError(error_messages[0]) - elif error_count > 1: - raise ResponseError( - "\n - ".join(["Multiple errors:"] + error_messages)) - elif not response.ok: - response.raise_for_status() - elif not data: - raise ResponseError("Empty response") - else: - return data - - -def get_json(response): - """ - Get json from Response. - - :param response: Response object - :return: data: json object - """ - try: - if response.text: - return response.json() - else: - return {} - except ValueError as value_error: - raise ResponseError( - "Invalid response: {0}: {1}".format(value_error, response.text)) - - -def get_error_messages(data): - """ - Get messages (ErrorCode) from Response. - - :param data: dict of datas - :return list: Empty list or list of errors - """ - error_messages = [] - for ret in data.get("responses", [data]): - if "errorCode" in ret: - error_messages.append( - "{0}: {1}".format(ret["errorCode"], ret.get("message")) - ) - - return error_messages diff --git a/setup.py b/setup.py index 7613f9b3..2d742c37 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -__version__ = '5.0.12' +__version__ = '5.1.0' with open('requirements.txt') as f: requirements = f.read().splitlines()