From 3301e724bfbe9c56cb0a3c5a8c9e017453310d82 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Fri, 16 Dec 2022 09:23:25 +0100 Subject: [PATCH 01/31] feat: Add JIRA dependency and server context manager --- requirements.txt | 1 + toolium/jira.py | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 537b6029..81930ea0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ screeninfo~=0.8 lxml~=4.9 Faker~=18.3 phonenumbers~=8.13 +jira==3.5.0 \ No newline at end of file diff --git a/toolium/jira.py b/toolium/jira.py index cf0b8b09..c692a994 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -18,8 +18,8 @@ import logging import re - import requests +from jira import JIRA from toolium.config_driver import get_error_message_from_exception from toolium.driver_wrappers_pool import DriverWrappersPool @@ -40,6 +40,30 @@ build = None only_if_changes = None +logger = logging.getLogger(__name__) + +class JiraServer: + + def __init__(self, execution_url, token=None): + global logger + + logger = logging.getLogger("Jira.Server") + self.url = execution_url + self._token = token + self.server: JIRA = None + + def __enter__(self): + server_url = self.url + headers = JIRA.DEFAULT_OPTIONS["headers"] + headers["Authorization"] = f"Bearer {self._token}" + logger.info("Starting Jira server...") + self.server = JIRA(server=server_url, options={"headers": headers, 'verify': True}, get_server_info=True) + return self.server + + def __exit__(self, exc_type, exc_val, exc_tb): + self.server.close() + logger.info("Jira server closed//") + def jira(test_key): """Decorator to update test status in Jira @@ -127,7 +151,6 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments): :param test_comment: test case comments :param test_attachments: test case attachments """ - logger = logging.getLogger(__name__) if not execution_url: logger.warning("Test Case '%s' can not be updated: execution_url is not configured", test_key) From d412c6cf79c7b27eace655a7d7dee0eb369cab29 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sat, 29 Apr 2023 09:06:18 +0200 Subject: [PATCH 02/31] Updated jira log statements --- toolium/jira.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index c692a994..4ad0eb63 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -20,7 +20,6 @@ import re import requests from jira import JIRA - from toolium.config_driver import get_error_message_from_exception from toolium.driver_wrappers_pool import DriverWrappersPool @@ -103,6 +102,10 @@ def save_jira_conf(): build = config.get_optional('Jira', 'build') only_if_changes = config.getboolean_optional('Jira', 'onlyifchanges') attachments = [] + jira_properties = {"enabled": enabled, "execution_url": execution_url, "summary_prefix": summary_prefix, + "labels": labels, "comments": comments, "fix_version": fix_version, "build": build, + "only_if_changes": only_if_changes, "attachments": attachments} + logger.debug("Jira properties set:" + jira_properties.__str__()) def add_attachment(attachment): @@ -112,6 +115,7 @@ def add_attachment(attachment): """ if attachment: attachments.append(attachment) + logger.info("Attachement Added from: " + attachment) def add_jira_status(test_key, test_status, test_comment): @@ -126,6 +130,8 @@ def add_jira_status(test_key, test_status, test_comment): if test_key in jira_tests_status: # Merge data with previous test status previous_status = jira_tests_status[test_key] + logger.debug("Found previous data for " + test_key.__str__()) + test_status = 'Pass' if previous_status[1] == 'Pass' and test_status == 'Pass' else 'Fail' if previous_status[2] and test_comment: test_comment = '{}\n{}'.format(previous_status[2], test_comment) @@ -134,6 +140,8 @@ def add_jira_status(test_key, test_status, test_comment): attachments += previous_status[3] # Add or update test status jira_tests_status[test_key] = (test_key, test_status, test_comment, attachments) + elif enabled and not test_key: + logger.error("Status not updated, invalid test key") def change_all_jira_status(): @@ -141,6 +149,7 @@ def change_all_jira_status(): for test_status in jira_tests_status.values(): change_jira_status(*test_status) jira_tests_status.clear() + logger.debug("Test cases status updated") def change_jira_status(test_key, test_status, test_comment, test_attachments): @@ -180,7 +189,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments): logger.warning("Error updating Test Case '%s': [%s] %s", test_key, response.status_code, get_error_message(response.content)) else: - logger.debug("%s", response.content.decode().splitlines()[0]) + logger.debug("Response with status " + str(response.status_code) + + " is: '%s'", response.content.decode().splitlines()[0]) def get_error_message(response_content): @@ -193,6 +203,8 @@ def get_error_message(response_content): match = apache_regex.search(response_content) if match: error_message = match.group(1) + logger.debug("Error message extracted from HTTP response with regex:" + apache_regex.__repr__()) + else: local_regex = re.compile(r'.*(.*).*') match = local_regex.search(response_content) @@ -200,4 +212,6 @@ def get_error_message(response_content): error_message = match.group(1) else: error_message = response_content + logger.debug("Error message extracted from HTTP response with regex:" + local_regex.__repr__()) + return error_message From aa9dd98e6d3b7ba9ef1a09e2ea5597291361a574 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 07:53:12 +0200 Subject: [PATCH 03/31] Refactor, switched Jira-API calls To Jira dependency --- toolium/jira.py | 126 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 35 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 4ad0eb63..36e5936a 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -18,34 +18,17 @@ import logging import re -import requests -from jira import JIRA +from jira import JIRA, Issue from toolium.config_driver import get_error_message_from_exception from toolium.driver_wrappers_pool import DriverWrappersPool -# Dict to save tuples with jira keys, their test status, comments and attachments -jira_tests_status = {} - -# List to save temporary test attachments -attachments = [] - -# Jira configuration -enabled = None -execution_url = None -summary_prefix = None -labels = None -comments = None -fix_version = None -build = None -only_if_changes = None - logger = logging.getLogger(__name__) + class JiraServer: def __init__(self, execution_url, token=None): global logger - logger = logging.getLogger("Jira.Server") self.url = execution_url self._token = token @@ -63,6 +46,24 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.server.close() logger.info("Jira server closed//") +# Dict to save tuples with jira keys, their test status, comments and attachments +jira_tests_status = {} + +# List to save temporary test attachments +attachments = [] + +# Jira configuration +enabled = None +jiratoken = None +project_id = 0 +execution_url = '' +summary_prefix = None +labels = None +comments = None +fix_version = None +build = None +only_if_changes = None + def jira(test_key): """Decorator to update test status in Jira @@ -91,9 +92,12 @@ def modified_test(*args, **kwargs): def save_jira_conf(): """Read Jira configuration from properties file and save it""" - global enabled, execution_url, summary_prefix, labels, comments, fix_version, build, only_if_changes, attachments + global enabled, jiratoken, project_id, execution_url, summary_prefix, labels, comments,\ + fix_version, build, only_if_changes, attachments config = DriverWrappersPool.get_default_wrapper().config enabled = config.getboolean_optional('Jira', 'enabled') + jiratoken = config.get_optional('Jira', 'token') + project_id = int(config.get_optional('Jira', 'project_id')) execution_url = config.get_optional('Jira', 'execution_url') summary_prefix = config.get_optional('Jira', 'summary_prefix') labels = config.get_optional('Jira', 'labels') @@ -149,16 +153,16 @@ def change_all_jira_status(): for test_status in jira_tests_status.values(): change_jira_status(*test_status) jira_tests_status.clear() - logger.debug("Test cases status updated") + logger.debug("Update attempt complete, clearing queue") -def change_jira_status(test_key, test_status, test_comment, test_attachments): +def change_jira_status(test_key, test_status, test_comment, test_attachments: list[str]): """Update test status in Jira :param test_key: test case key in Jira :param test_status: test case status :param test_comment: test case comments - :param test_attachments: test case attachments + :param test_attachments: list of absolutes paths of test case attachment files """ if not execution_url: @@ -174,26 +178,78 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments): if only_if_changes: payload['onlyIfStatusChanges'] = 'true' try: - if test_attachments and len(test_attachments) > 0: - files = dict() - for index in range(len(test_attachments)): - files['attachments{}'.format(index)] = open(test_attachments[index], 'rb') - else: - files = None - response = requests.post(execution_url, data=payload, files=files) + with JiraServer(execution_url, jiratoken) as server: + existing_issues = execute_query(server, 'issue = ' + test_key) + if not existing_issues: + logger.warning("Jira Issue not found,...") + return + # TODO issue = new_testcase(server, project_id, summary=scenarioname, description=description) + # test_key = issue.key + + # TODO enforce test case as issue type and call create_test_execution for each scenario as below + # new_execution = create_test_execution(server,test_key, project_id) + + logger.info("Retrieving " + test_key) + issue = server.issue(test_key) + + # TODO massage payload, labels?? + logger.debug("Update skipped for " + test_key) + # issue.update(fields=payload, jira=server) + + # TODO wait to create test execution before transitioning to behave status + logger.debug("Transition skipped for " + test_key) + # transition(server, issue, test_status) + # TODO add attachements + # add_results(server, issue.key, test_attachments) + except Exception as e: - logger.warning("Error updating Test Case '%s': %s", test_key, e) + logger.error("Exception while updating Issue '%s': %s", test_key, e) return + +def execute_query(jira: JIRA, query: str): + logger = logging.getLogger("Jira.Queries") + + logger.info(f"executing query: {query} ...\n") + existing_issues = jira.search_issues(query) + + issuesfound = "" + for issue in existing_issues: + issuesfound += f'\n{issue} {jira.issue(issue).fields.summary}' + if issuesfound: + logger.info("Found issue/s:" + issuesfound) + return existing_issues + + +def create_test_execution(server: JIRA, issueid: str, projectid: int, summary=None, description=None) -> Issue: + """Creates an execution linked to the TestCase provided""" + issue_dict = { + 'project': {'id': projectid}, + 'summary': summary if summary else input("Summary:"), + 'description': description if description else input("Description:"), + 'issuetype': {'name': 'Test Case Execution'}, + 'parent': {'key': issueid} + } + + return server.create_issue(fields=issue_dict) + + +def transition(server: JIRA, issue: Issue, test_status: str): + """ + Transitions the issue to a new state, see issue workflows in the project for available options + param test_status: the new status of the issue + """ + logger.info("Setting new status for " + issue.key + "from " + issue.get_field("status") + " to " + test_status) + response = server.transition_issue(issue.key, transition=test_status.lower()) if response.status_code >= 400: - logger.warning("Error updating Test Case '%s': [%s] %s", test_key, response.status_code, - get_error_message(response.content)) + logger.warning("Error transitioning Test Case '%s': [%s] %s", issue.key, response.status_code, + get_error_message(response.content.decode())) else: - logger.debug("Response with status " + str(response.status_code) + + logger.debug("Transition response with status " + str(response.status_code) + " is: '%s'", response.content.decode().splitlines()[0]) -def get_error_message(response_content): +def get_error_message(response_content: str): """Extract error message from the HTTP response :param response_content: HTTP response from test case execution API From 4354298f4d820b0590796380b10dea2ba5362360 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 07:56:20 +0200 Subject: [PATCH 04/31] Feat: added upload output to jira --- toolium/jira.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 36e5936a..faf43442 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -17,7 +17,9 @@ """ import logging +import os import re +from os import path from jira import JIRA, Issue from toolium.config_driver import get_error_message_from_exception from toolium.driver_wrappers_pool import DriverWrappersPool @@ -199,8 +201,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li # TODO wait to create test execution before transitioning to behave status logger.debug("Transition skipped for " + test_key) # transition(server, issue, test_status) - # TODO add attachements - # add_results(server, issue.key, test_attachments) + + add_results(server, issue.key, test_attachments) except Exception as e: logger.error("Exception while updating Issue '%s': %s", test_key, e) @@ -249,6 +251,64 @@ def transition(server: JIRA, issue: Issue, test_status: str): " is: '%s'", response.content.decode().splitlines()[0]) +def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): + """Adds the results to the execution or the associated test case instead""" + + def addscreenshots(issueid: str, attachements: list[str] = None): + """ + Attach the screenshots found the report folder to the jira issue provided + param issueid: Full Jira ID + param attachements: Absolute paths of attachments + Raises FileNotFound: if an attachement file is not found + """ + + if attachements: + for filepath in attachements: + if os.path.isfile(path.join(filepath)): + with open(filepath, 'rb') as file: + logger.debug("Opened screenshot " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached " + file.name + " into " + issueid) + else: + screenshotspath = path.join(path.dirname(__file__), ".", DriverWrappersPool.screenshots_directory) + logger.debug("Reading screenshot folder " + screenshotspath) + + if len(os.listdir(screenshotspath)) < 1: + logger.warning("Screenshot folder empty...") + return + + for filename in os.listdir(screenshotspath): + if os.path.isfile(path.join(screenshotspath, filename)): + with open(os.path.join(screenshotspath, filename), 'rb') as file: + logger.debug("Opened screenshot " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached " + filename + " into " + issueid) + logger.info("Screenshots uploaded...") + + def addlogs(issueid: str): + """ + Attach the logs in the report folder to the jira issue provided + param issueid Full Jira ID + Raises FileNotFound Error if the log file is not found in reports + """ + logpath = path.join(path.dirname(__file__), + ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") + + with open(logpath, 'rb') as file: + logger.debug("Opened log file " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached log into " + issueid) + + try: + logger.info("Adding results to issue...") + addscreenshots(issueid, attachements) if attachements else addscreenshots(issueid) + addlogs(issueid) + logger.debug("Results added to issue " + issueid) + + except Exception as error: + logger.error("Results not added, exception:" + str(error)) + + def get_error_message(response_content: str): """Extract error message from the HTTP response From ec131fb25a490ce5e4393b5d6440eb34b25275b0 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:02:30 +0200 Subject: [PATCH 05/31] Docs: Jira module --- docs/Jira.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/Jira.rst diff --git a/docs/Jira.rst b/docs/Jira.rst new file mode 100644 index 00000000..03cff8bf --- /dev/null +++ b/docs/Jira.rst @@ -0,0 +1,43 @@ +Jira Configuration +==================== + +First of all Jira must be enabled by setting the enabled property to true in config.cfg:: + + [Jira] + enabled: true + +The rest of the properties can be configured as: + +token: The OAuth token for the Jira account, can be created in Jira web page > Profile > Personal access tokens +project_id: The project id, ,can be seen in Jira web page > Administration > Projects > The Edit html anchor href will have a pid= segment with the project id +execution_url: The root server URL, see example below +onlyifchanges: true, if previous state of the updated issues is similar true will force the update, false will omit it +summary_prefix: # TODO +fixversion: The fixversion field, see example below +labels: # TODO +comments: # TODO +build: # TODO + +Full example:: + + [Jira] + enabled: true + token: My OAuth token + project_id: 99999 + execution_url: https://jira.atlassian.com + onlyifchanges: true + summary_prefix: # TODO + fixversion: 4.12 + labels: # TODO + comments: # TODO + build: # TODO + +See `https://jira.readthedocs.io`_ for the complete Package documentation. + +Jira API +==================== + +See request module for REST API calls `https://requests.readthedocs.io/en/latest/`_ +And the json parsing module `https://docs.python.org/3/library/json.html`_ +See `https://developer.atlassian.com/server/jira/platform/rest-apis/`_ for the the underlying API introduction. +And `https://docs.atlassian.com/software/jira/docs/api/REST/9.5.0/`_ for the underlying API full documentation. From 17362cc51f115ef7d0c1da7bc27fbf8f1b04e277 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:05:41 +0200 Subject: [PATCH 06/31] Refactor: enabled jira create execution and transition --- toolium/jira.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index faf43442..a0a6466b 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -48,6 +48,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.server.close() logger.info("Jira server closed//") + # Dict to save tuples with jira keys, their test status, comments and attachments jira_tests_status = {} @@ -111,7 +112,7 @@ def save_jira_conf(): jira_properties = {"enabled": enabled, "execution_url": execution_url, "summary_prefix": summary_prefix, "labels": labels, "comments": comments, "fix_version": fix_version, "build": build, "only_if_changes": only_if_changes, "attachments": attachments} - logger.debug("Jira properties set:" + jira_properties.__str__()) + logger.debug("Jira properties read:" + jira_properties.__str__()) def add_attachment(attachment): @@ -155,7 +156,10 @@ def change_all_jira_status(): for test_status in jira_tests_status.values(): change_jira_status(*test_status) jira_tests_status.clear() - logger.debug("Update attempt complete, clearing queue") + if enabled: + logger.debug("Update attempt complete, clearing queue") + else: + logger.debug("Jira disabled, upload skipped") def change_jira_status(test_key, test_status, test_comment, test_attachments: list[str]): @@ -188,21 +192,18 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li # TODO issue = new_testcase(server, project_id, summary=scenarioname, description=description) # test_key = issue.key - # TODO enforce test case as issue type and call create_test_execution for each scenario as below - # new_execution = create_test_execution(server,test_key, project_id) - logger.info("Retrieving " + test_key) - issue = server.issue(test_key) + new_execution = create_test_execution(server,test_key, project_id) + logger.info(f"Created execution {new_execution.key} for test " + test_key) # TODO massage payload, labels?? logger.debug("Update skipped for " + test_key) # issue.update(fields=payload, jira=server) - # TODO wait to create test execution before transitioning to behave status - logger.debug("Transition skipped for " + test_key) - # transition(server, issue, test_status) + logger.debug("Transitioning " + new_execution.key) + transition(server, new_execution, test_status) - add_results(server, issue.key, test_attachments) + add_results(server, new_execution.key, test_attachments) except Exception as e: logger.error("Exception while updating Issue '%s': %s", test_key, e) From b94eb448cc5aa356c40ac5ad1218ec25c4312845 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:09:09 +0200 Subject: [PATCH 07/31] Refactor: log jira error stack trace --- toolium/jira.py | 52 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index a0a6466b..4e115d26 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -21,6 +21,7 @@ import re from os import path from jira import JIRA, Issue +from jira.exceptions import JIRAError from toolium.config_driver import get_error_message_from_exception from toolium.driver_wrappers_pool import DriverWrappersPool @@ -122,7 +123,7 @@ def add_attachment(attachment): """ if attachment: attachments.append(attachment) - logger.info("Attachement Added from: " + attachment) + logger.info("Attachement added from: " + attachment) def add_jira_status(test_key, test_status, test_comment): @@ -192,8 +193,10 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li # TODO issue = new_testcase(server, project_id, summary=scenarioname, description=description) # test_key = issue.key - - new_execution = create_test_execution(server,test_key, project_id) + logger.debug("Creating execution for " + test_key) + new_execution = create_test_execution(server, test_key, project_id, + summary_prefix, existing_issues[0].fields.summary, fix_version, + existing_issues[0].fields.labels, labels) logger.info(f"Created execution {new_execution.key} for test " + test_key) # TODO massage payload, labels?? @@ -205,7 +208,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li add_results(server, new_execution.key, test_attachments) - except Exception as e: + except JIRAError as e: + print_trace(logger, e) logger.error("Exception while updating Issue '%s': %s", test_key, e) return @@ -242,14 +246,16 @@ def transition(server: JIRA, issue: Issue, test_status: str): Transitions the issue to a new state, see issue workflows in the project for available options param test_status: the new status of the issue """ - logger.info("Setting new status for " + issue.key + "from " + issue.get_field("status") + " to " + test_status) - response = server.transition_issue(issue.key, transition=test_status.lower()) - if response.status_code >= 400: - logger.warning("Error transitioning Test Case '%s': [%s] %s", issue.key, response.status_code, - get_error_message(response.content.decode())) - else: - logger.debug("Transition response with status " + str(response.status_code) + - " is: '%s'", response.content.decode().splitlines()[0]) + logger.info("Setting new status for " + issue.key + "from " + issue.fields.status.raw['name'] + " to " + test_status) + try: + server.transition_issue(issue.key, transition=test_status.lower()) + except JIRAError: + # TODO Get available transitions + logger.warning("Available transitions for %s are %s", issue.key, server.transitions(issue.key)) + logger.error("Error transitioning Test Case '%s' to status [%s]", issue.key, test_status) + + return + logger.debug("Transitioned issue to status %s", test_status) def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): @@ -306,7 +312,9 @@ def addlogs(issueid: str): addlogs(issueid) logger.debug("Results added to issue " + issueid) - except Exception as error: + except FileNotFoundError as error: + # TODO catch all jira exception types + print_trace(logger, error) logger.error("Results not added, exception:" + str(error)) @@ -332,3 +340,21 @@ def get_error_message(response_content: str): logger.debug("Error message extracted from HTTP response with regex:" + local_regex.__repr__()) return error_message + + +def print_trace(logger, e): + # TODO refactor + # TODO move to utils as stand alone + # TODO set custom JiraError for toolium? + import traceback + + trace = traceback.format_tb(e.__traceback__, 30) + final_stack = '' + for count in range(len(trace)): + final_stack += f'Trace stack {count} {trace[count]}' + logger.error(f'Error Trace:\n{final_stack}') + logger.error(f'Error: {e.__class__}') + if hasattr(e, "msg"): + logger.error(f'Error Message: {e.msg}') + else: + logger.error(e.args) From d2ad14b03053350be982cd164bf0fe80d1dfe38d Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:19:20 +0200 Subject: [PATCH 08/31] Refactor: jira labels --- docs/Jira.rst | 2 +- toolium/jira.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/Jira.rst b/docs/Jira.rst index 03cff8bf..cab69fcd 100644 --- a/docs/Jira.rst +++ b/docs/Jira.rst @@ -28,7 +28,7 @@ Full example:: onlyifchanges: true summary_prefix: # TODO fixversion: 4.12 - labels: # TODO + labels: [QA, label2, 3rdlabel] comments: # TODO build: # TODO diff --git a/toolium/jira.py b/toolium/jira.py index 4e115d26..53be7078 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -104,7 +104,11 @@ def save_jira_conf(): project_id = int(config.get_optional('Jira', 'project_id')) execution_url = config.get_optional('Jira', 'execution_url') summary_prefix = config.get_optional('Jira', 'summary_prefix') - labels = config.get_optional('Jira', 'labels') + labels_raw = config.get_optional('Jira', 'labels') + labels_raw = labels_raw.replace("[", "").replace("]", "") + labels = [] + for label in labels_raw.split(","): + labels.append(label) comments = config.get_optional('Jira', 'comments') fix_version = config.get_optional('Jira', 'fixversion') build = config.get_optional('Jira', 'build') @@ -214,7 +218,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li return -def execute_query(jira: JIRA, query: str): +def execute_query(jira: JIRA, query: str) -> list: logger = logging.getLogger("Jira.Queries") logger.info(f"executing query: {query} ...\n") @@ -228,13 +232,15 @@ def execute_query(jira: JIRA, query: str): return existing_issues -def create_test_execution(server: JIRA, issueid: str, projectid: int, summary=None, description=None) -> Issue: +def create_test_execution(server: JIRA, issueid: str, projectid: int, parent_labels: list, summary=None, description=None, + new_labels: list = None,) -> Issue: """Creates an execution linked to the TestCase provided""" issue_dict = { 'project': {'id': projectid}, 'summary': summary if summary else input("Summary:"), 'description': description if description else input("Description:"), 'issuetype': {'name': 'Test Case Execution'}, + 'labels': parent_labels + new_labels, 'parent': {'key': issueid} } From 4b2d7923916790d568754e9eb68bdca762e029c0 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:23:01 +0200 Subject: [PATCH 09/31] Feat: added check jira args --- toolium/jira.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/toolium/jira.py b/toolium/jira.py index 53be7078..388e7266 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -167,6 +167,27 @@ def change_all_jira_status(): logger.debug("Jira disabled, upload skipped") +def check_jira_args(server: JIRA): + + available_keys = [] + for project_instance in server.projects(): + available_keys.append([project_instance.raw['name'], project_instance.raw['key'],project_instance.raw['id']]) + if project_id not in available_keys: + msg = f"No existe el proyecto '{project_id}'" + logger.warning(f"Available projects for your user: '{available_keys}'") + logger.error(msg) + raise ValueError(msg) + + available_fixversions = [] + for version in server.project(str(project_id)): + available_fixversions.append(version.raw["name"]) + if fix_version not in available_fixversions: + msg = f"No existe la fix version '{fix_version}'" + logger.warning(f"Available fixversions for project {server.project(str(project_id))} are {available_fixversions}") + logger.error(msg) + raise ValueError(msg) + + def change_jira_status(test_key, test_status, test_comment, test_attachments: list[str]): """Update test status in Jira @@ -190,6 +211,9 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li payload['onlyIfStatusChanges'] = 'true' try: with JiraServer(execution_url, jiratoken) as server: + + check_jira_args(server) + existing_issues = execute_query(server, 'issue = ' + test_key) if not existing_issues: logger.warning("Jira Issue not found,...") From af9b665a8a74ee80e50c843bdaa56f1ef936877f Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:30:11 +0200 Subject: [PATCH 10/31] Feat: jira, create test execution added all remaining fields --- toolium/jira.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 388e7266..bb9a6399 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -61,10 +61,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): jiratoken = None project_id = 0 execution_url = '' -summary_prefix = None -labels = None +summary_prefix = "" +labels = [] comments = None -fix_version = None +fix_version = "" build = None only_if_changes = None @@ -106,7 +106,6 @@ def save_jira_conf(): summary_prefix = config.get_optional('Jira', 'summary_prefix') labels_raw = config.get_optional('Jira', 'labels') labels_raw = labels_raw.replace("[", "").replace("]", "") - labels = [] for label in labels_raw.split(","): labels.append(label) comments = config.get_optional('Jira', 'comments') @@ -223,7 +222,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li logger.debug("Creating execution for " + test_key) new_execution = create_test_execution(server, test_key, project_id, - summary_prefix, existing_issues[0].fields.summary, fix_version, + summary_prefix, existing_issues[0].fields.summary, + fix_version, existing_issues[0].fields.labels, labels) logger.info(f"Created execution {new_execution.key} for test " + test_key) @@ -256,16 +256,21 @@ def execute_query(jira: JIRA, query: str) -> list: return existing_issues -def create_test_execution(server: JIRA, issueid: str, projectid: int, parent_labels: list, summary=None, description=None, - new_labels: list = None,) -> Issue: +def create_test_execution(server: JIRA, issueid: str, projectid: int, summary_prefix: str, test_summary: str, + fix_version: str, parent_labels: list, new_labels: list = None, description: str = " ") -> Issue: """Creates an execution linked to the TestCase provided""" issue_dict = { 'project': {'id': projectid}, - 'summary': summary if summary else input("Summary:"), - 'description': description if description else input("Description:"), + 'assignee': {'name': server.current_user()}, 'issuetype': {'name': 'Test Case Execution'}, + 'parent': {'key': issueid}, + 'summary': str(summary_prefix) + " Execution of " + test_summary + " " + issueid, + 'fixVersions': [{'name': fix_version}], + # TODO set priority as config input? + # 'priority': {'name': 'Minor', "id": 10101}, 'labels': parent_labels + new_labels, - 'parent': {'key': issueid} + # 'versions': [{'name': fix_version}], + 'description': description, } return server.create_issue(fields=issue_dict) From cf8cf24e7abee8040e2f5aadc2874cbe9d60eb1e Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Sun, 30 Apr 2023 08:31:46 +0200 Subject: [PATCH 11/31] Feat: jira, added jira_key and jira_name as project identifiers --- toolium/jira.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index bb9a6399..efda1906 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -60,6 +60,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): enabled = None jiratoken = None project_id = 0 +project_key = "" +project_name = "" execution_url = '' summary_prefix = "" labels = [] @@ -96,12 +98,14 @@ def modified_test(*args, **kwargs): def save_jira_conf(): """Read Jira configuration from properties file and save it""" - global enabled, jiratoken, project_id, execution_url, summary_prefix, labels, comments,\ + global enabled, jiratoken, project_id, project_key, project_name, execution_url, summary_prefix, labels, comments,\ fix_version, build, only_if_changes, attachments config = DriverWrappersPool.get_default_wrapper().config enabled = config.getboolean_optional('Jira', 'enabled') jiratoken = config.get_optional('Jira', 'token') project_id = int(config.get_optional('Jira', 'project_id')) + project_key = int(config.get_optional('Jira', 'project_key')) + project_name = int(config.get_optional('Jira', 'project_name')) execution_url = config.get_optional('Jira', 'execution_url') summary_prefix = config.get_optional('Jira', 'summary_prefix') labels_raw = config.get_optional('Jira', 'labels') @@ -167,22 +171,46 @@ def change_all_jira_status(): def check_jira_args(server: JIRA): + global project_name, project_key, project_id available_keys = [] for project_instance in server.projects(): + # TODO turn into dict available_keys.append([project_instance.raw['name'], project_instance.raw['key'],project_instance.raw['id']]) - if project_id not in available_keys: - msg = f"No existe el proyecto '{project_id}'" + + if project_id: + project_option = project_id + elif project_key: + project_option = project_key + else: + project_option = project_name + + # TODO create def + if project_option not in available_keys: + msg = f"No existe el proyecto '{project_option}'" logger.warning(f"Available projects for your user: '{available_keys}'") logger.error(msg) raise ValueError(msg) + # TODO create def + # TODO Refactor duplicate ifs + if project_id: + project_key = server.project(str(project_id)).raw["key"] + elif project_key: + project_id = int(server.project(project_key).raw["id"]) + else: + for version in available_keys: + if project_name in version: + project_key = version[1] + project_id = int(version[2]) + + # TODO create def available_fixversions = [] - for version in server.project(str(project_id)): + for version in server.project_versions(project_name): available_fixversions.append(version.raw["name"]) if fix_version not in available_fixversions: msg = f"No existe la fix version '{fix_version}'" - logger.warning(f"Available fixversions for project {server.project(str(project_id))} are {available_fixversions}") + logger.warning(f"Available fixversions for project {server.project(project_name)} are {available_fixversions}") logger.error(msg) raise ValueError(msg) From 65ec24cc630966ade02bf07f908a2e5651e524c8 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Fri, 5 May 2023 13:50:06 +0200 Subject: [PATCH 12/31] Fix: jira, added jira_key and jira_name as project identifiers --- toolium/jira.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index efda1906..82320a3e 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -110,6 +110,7 @@ def save_jira_conf(): summary_prefix = config.get_optional('Jira', 'summary_prefix') labels_raw = config.get_optional('Jira', 'labels') labels_raw = labels_raw.replace("[", "").replace("]", "") + labels = [] for label in labels_raw.split(","): labels.append(label) comments = config.get_optional('Jira', 'comments') @@ -176,41 +177,46 @@ def check_jira_args(server: JIRA): available_keys = [] for project_instance in server.projects(): # TODO turn into dict - available_keys.append([project_instance.raw['name'], project_instance.raw['key'],project_instance.raw['id']]) + available_keys.append([project_instance.raw['name'], project_instance.raw['key'], project_instance.raw['id']]) - if project_id: - project_option = project_id - elif project_key: - project_option = project_key - else: - project_option = project_name + logger.debug(f"Read project info read name:'{project_name}', key:'{project_key}', id:'{project_id}'") # TODO create def - if project_option not in available_keys: - msg = f"No existe el proyecto '{project_option}'" + project_option = "" + # TODO Update jira_properties + for key in available_keys: + if project_id in key[2]: + project_option = project_id + elif project_key in key[1]: + project_option = project_key + elif project_name in key[0]: + project_option = project_name + + if not project_option: + msg = f"No existe el proyecto especificado name:'{project_name}', key:'{project_key}', id:'{project_id}'" logger.warning(f"Available projects for your user: '{available_keys}'") logger.error(msg) raise ValueError(msg) # TODO create def # TODO Refactor duplicate ifs - if project_id: + if project_option == project_id: project_key = server.project(str(project_id)).raw["key"] - elif project_key: - project_id = int(server.project(project_key).raw["id"]) + elif project_option == project_key: + project_id = str(server.project(project_key).raw["id"]) else: for version in available_keys: if project_name in version: project_key = version[1] - project_id = int(version[2]) + project_id = str(version[2]) # TODO create def available_fixversions = [] - for version in server.project_versions(project_name): + for version in server.project_versions(project_key): available_fixversions.append(version.raw["name"]) if fix_version not in available_fixversions: msg = f"No existe la fix version '{fix_version}'" - logger.warning(f"Available fixversions for project {server.project(project_name)} are {available_fixversions}") + logger.warning(f"Available fixversions for project {server.project(project_key)} are {available_fixversions}") logger.error(msg) raise ValueError(msg) From ee89523c92b0881e393e4eda116a3592f21eb3f6 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 11:28:15 +0200 Subject: [PATCH 13/31] Add jira connection unit tests --- toolium/test/test_jira.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 294f9f69..8a66cc05 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -17,14 +17,16 @@ """ import os - +import unittest import mock import pytest import requests_mock +from jira import JIRAError from requests.exceptions import ConnectionError from toolium import jira from toolium.driver_wrappers_pool import DriverWrappersPool +from toolium.jira import JiraServer @pytest.fixture @@ -243,3 +245,32 @@ def mock_test_pass_2(self): @jira.jira(test_key='TOOLIUM-3') def mock_test_fail(self): raise AssertionError('test error') + + +def test_jira_connection_anonymous(): + with JiraServer("https://issues.apache.org/jira", None) as jira: + assert len(jira.projects()) > 1 + + +@unittest.skip("TOKEN injection pending") +def test_jira_connection_private_user(): + def call_jira_server(): + with JiraServer("https://issues.apache.org/jira", "") as jira: # TODO + assert jira.current_user() + unittest.TestCase().assertRaises(JIRAError, call_jira_server) + + +def test_jira_connection_invalid_url(): + # with JiraServer("http://github.com", None) as jira: + # return + def call_jira_server(): + with JiraServer("http://github.com", None) as jira: + return + unittest.TestCase().assertRaisesRegex(JIRAError, "^JiraError HTTP 406 url", call_jira_server) + + +def test_jira_connection_invalid_token(): + def call_jira_server(): + with JiraServer("https://jira.elevenpaths.com/", "6") as jira: + assert jira.current_user() + unittest.TestCase().assertRaises(JIRAError, call_jira_server) From 56be05301ae251f77693dbc6b354bc8e5f55abfe Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 12:22:30 +0200 Subject: [PATCH 14/31] Fix, changed jira dependency for python 3.7 compatibility --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 79ece0bf..cf5ca6f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ screeninfo~=0.8 lxml~=4.9 Faker~=18.3 phonenumbers~=8.13 -jira==3.5.0 \ No newline at end of file +jira==3.2.0 \ No newline at end of file From 388a11dd84f152e56d6b144bf90061184b7945b4 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 12:25:03 +0200 Subject: [PATCH 15/31] Update, Jira docs --- docs/Jira.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/Jira.rst b/docs/Jira.rst index cab69fcd..05adcba2 100644 --- a/docs/Jira.rst +++ b/docs/Jira.rst @@ -9,9 +9,20 @@ First of all Jira must be enabled by setting the enabled property to true in con The rest of the properties can be configured as: token: The OAuth token for the Jira account, can be created in Jira web page > Profile > Personal access tokens -project_id: The project id, ,can be seen in Jira web page > Administration > Projects > The Edit html anchor href will have a pid= segment with the project id -execution_url: The root server URL, see example below -onlyifchanges: true, if previous state of the updated issues is similar true will force the update, false will omit it + +Project data in order of preference: + +- project_id: The project id, ,can be seen in Jira web page > Administration > Projects > The Edit html anchor href will have a pid= segment with the project id + +- project_key: The shortened project upper case identifier that appears in the issues ids, see example below + +- project_name: Full project name + +- execution_url: The root server URL, see example below + +Note: if the project cannot be located, available project for your user will be printed in toolium.log + +onlyifchanges: true if previous state of the updated issues is similar true will force the update, false will omit it summary_prefix: # TODO fixversion: The fixversion field, see example below labels: # TODO @@ -23,7 +34,9 @@ Full example:: [Jira] enabled: true token: My OAuth token - project_id: 99999 + project_id: 12316620 + project_key: COMMONSRDF + project_name: Apache Commons RDF execution_url: https://jira.atlassian.com onlyifchanges: true summary_prefix: # TODO From 05306e0bfa799b5047d0356b0382d5d7ec4735ad Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 13:26:05 +0200 Subject: [PATCH 16/31] Refactor, Jira check args --- toolium/jira.py | 69 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 82320a3e..3e77d210 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -174,43 +174,74 @@ def change_all_jira_status(): def check_jira_args(server: JIRA): global project_name, project_key, project_id + project_name, project_key, project_id = _check_project_set_ids(server, project_id, project_key, project_name) + _check_fix_version(server, fix_version) + + +def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): + """ + Checks the provided project identifiers returns the values updated if they were empty + Note that the args are provided in order of precedence to prevent mismatches between them + Args: + server (str): jira server instance + p_id (str): The project id + p_key (str): the key shortcut for the project + p_name (str): Project full name as registered when the project was created + Raises: + ValueError: if the project does not exist; will log available project if any + Returns: + p_id (str): The project id + p_key (str): the key shortcut for the project + p_name (str): Project full name as registered when the project was created + + """ available_keys = [] for project_instance in server.projects(): # TODO turn into dict - available_keys.append([project_instance.raw['name'], project_instance.raw['key'], project_instance.raw['id']]) + available_keys.append( + [project_instance.raw['name'], project_instance.raw['key'], project_instance.raw['id']]) - logger.debug(f"Read project info read name:'{project_name}', key:'{project_key}', id:'{project_id}'") + logger.debug(f"Read project info read name:'{p_name}', key:'{p_key}', id:'{p_id}'") - # TODO create def project_option = "" - # TODO Update jira_properties for key in available_keys: - if project_id in key[2]: - project_option = project_id - elif project_key in key[1]: - project_option = project_key - elif project_name in key[0]: - project_option = project_name + if p_id in key[2]: + project_option = p_id + elif p_key in key[1]: + project_option = p_key + elif p_name in key[0]: + project_option = p_name if not project_option: - msg = f"No existe el proyecto especificado name:'{project_name}', key:'{project_key}', id:'{project_id}'" + msg = f"No existe el proyecto especificado name:'{p_name}', key:'{p_key}', id:'{p_id}'" logger.warning(f"Available projects for your user: '{available_keys}'") logger.error(msg) raise ValueError(msg) # TODO create def # TODO Refactor duplicate ifs - if project_option == project_id: - project_key = server.project(str(project_id)).raw["key"] - elif project_option == project_key: - project_id = str(server.project(project_key).raw["id"]) + if project_option == p_id: + p_key = server.project(str(p_id)).raw["key"] + elif project_option == p_key: + p_id = str(server.project(p_key).raw["id"]) else: for version in available_keys: - if project_name in version: - project_key = version[1] - project_id = str(version[2]) + if p_name in version: + p_key = version[1] + p_id = str(version[2]) - # TODO create def + return p_id, p_key, p_name + + +def _check_fix_version(server: JIRA, fix_version: str) -> None: + """ + Retrieves the fix_versions for the current project and ensures the one provided is valid + Args: + server (str): jira server instance + fix_version (str): fix version that will be checked + Raises: + ValueError: if the fix_version is invalid + """ available_fixversions = [] for version in server.project_versions(project_key): available_fixversions.append(version.raw["name"]) From c84bbfefe6a0c51a2eb302a939d860d0c145cbb2 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 15:59:41 +0200 Subject: [PATCH 17/31] Fix, Jira check project --- toolium/jira.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 3e77d210..9b4a67cd 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -174,7 +174,7 @@ def change_all_jira_status(): def check_jira_args(server: JIRA): global project_name, project_key, project_id - project_name, project_key, project_id = _check_project_set_ids(server, project_id, project_key, project_name) + project_id, project_key = _check_project_set_ids(server, project_id, project_key, project_name) _check_fix_version(server, fix_version) @@ -192,7 +192,6 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): Returns: p_id (str): The project id p_key (str): the key shortcut for the project - p_name (str): Project full name as registered when the project was created """ available_keys = [] @@ -207,10 +206,20 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): for key in available_keys: if p_id in key[2]: project_option = p_id + p_key = server.project(str(p_id)).raw["key"] + break elif p_key in key[1]: project_option = p_key + p_id = str(server.project(p_key).raw["id"]) + break elif p_name in key[0]: project_option = p_name + for version in available_keys: + if p_name in version: + p_key = version[1] + p_id = str(version[2]) + break + break if not project_option: msg = f"No existe el proyecto especificado name:'{p_name}', key:'{p_key}', id:'{p_id}'" @@ -218,19 +227,7 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): logger.error(msg) raise ValueError(msg) - # TODO create def - # TODO Refactor duplicate ifs - if project_option == p_id: - p_key = server.project(str(p_id)).raw["key"] - elif project_option == p_key: - p_id = str(server.project(p_key).raw["id"]) - else: - for version in available_keys: - if p_name in version: - p_key = version[1] - p_id = str(version[2]) - - return p_id, p_key, p_name + return p_id, p_key def _check_fix_version(server: JIRA, fix_version: str) -> None: From 8fb5ab3741ffa3d4dcdbc974fb1a3ca355d798c5 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 17:17:48 +0200 Subject: [PATCH 18/31] Refactor, Jira check fix_version --- toolium/jira.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 9b4a67cd..33ce00fa 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -175,7 +175,7 @@ def check_jira_args(server: JIRA): global project_name, project_key, project_id project_id, project_key = _check_project_set_ids(server, project_id, project_key, project_name) - _check_fix_version(server, fix_version) + _check_fix_version(server, project_key, fix_version) def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): @@ -230,11 +230,12 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): return p_id, p_key -def _check_fix_version(server: JIRA, fix_version: str) -> None: +def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None: """ Retrieves the fix_versions for the current project and ensures the one provided is valid Args: server (str): jira server instance + project_key (str): project ket (short version of the name) fix_version (str): fix version that will be checked Raises: ValueError: if the fix_version is invalid From 5a9a0daae5d61092e205572a644c2eb4dcf1b433 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Mon, 26 Jun 2023 17:18:02 +0200 Subject: [PATCH 19/31] Added, Jira check fix_version tests --- toolium/test/test_jira.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 8a66cc05..2e09a88a 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -22,11 +22,12 @@ import pytest import requests_mock from jira import JIRAError +from jira.resources import Version from requests.exceptions import ConnectionError from toolium import jira from toolium.driver_wrappers_pool import DriverWrappersPool -from toolium.jira import JiraServer +from toolium.jira import JiraServer, _check_fix_version @pytest.fixture @@ -264,7 +265,7 @@ def test_jira_connection_invalid_url(): # with JiraServer("http://github.com", None) as jira: # return def call_jira_server(): - with JiraServer("http://github.com", None) as jira: + with JiraServer("http://github.com", None): return unittest.TestCase().assertRaisesRegex(JIRAError, "^JiraError HTTP 406 url", call_jira_server) @@ -274,3 +275,23 @@ def call_jira_server(): with JiraServer("https://jira.elevenpaths.com/", "6") as jira: assert jira.current_user() unittest.TestCase().assertRaises(JIRAError, call_jira_server) + +def test_jira_check_valid_fix_version(): + version = mock.Mock(Version) + version.configure_mock(raw={"name":"1.1"}) + server = mock.Mock(JiraServer("https://jira.elevenpaths.com/", "6")) + server.configure_mock(project= lambda x:"KEY", project_versions = lambda arg: [version]) + _check_fix_version(server, "KEY", "1.1") + + +def test_jira_check_wrong_fix_version(): + version = mock.Mock(Version) + version.configure_mock(raw={"name":"0.5"}) + server = mock.Mock(JiraServer("https://jira.elevenpaths.com/", "6")) + server.configure_mock(project= lambda x:"KEY", project_versions = lambda arg: [version]) + + def call_check_fix_version_wrong(): + _check_fix_version(server, "KEY", "1.1") + unittest.TestCase().assertRaisesRegex(ValueError, + "No existe la fix version '1.1'", + call_check_fix_version_wrong) From 3ba445ccf97b64e2928c6b097cd56323db456038 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Wed, 28 Jun 2023 12:09:06 +0200 Subject: [PATCH 20/31] Added, Jira comments to execution if any --- toolium/jira.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/toolium/jira.py b/toolium/jira.py index 33ce00fa..1ba6257a 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -290,6 +290,9 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li existing_issues[0].fields.labels, labels) logger.info(f"Created execution {new_execution.key} for test " + test_key) + if composed_comments: + server.add_comment(new_execution.key, composed_comments) + # TODO massage payload, labels?? logger.debug("Update skipped for " + test_key) # issue.update(fields=payload, jira=server) @@ -334,6 +337,7 @@ def create_test_execution(server: JIRA, issueid: str, projectid: int, summary_pr 'labels': parent_labels + new_labels, # 'versions': [{'name': fix_version}], 'description': description, + #TODO add build field ?? } return server.create_issue(fields=issue_dict) From 032b598cb713719c8653d1c6b872ce845eed95ae Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Wed, 28 Jun 2023 13:35:57 +0200 Subject: [PATCH 21/31] Minor Refactor Jira Readme --- docs/Jira.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/Jira.rst b/docs/Jira.rst index 05adcba2..e5295327 100644 --- a/docs/Jira.rst +++ b/docs/Jira.rst @@ -23,11 +23,16 @@ Project data in order of preference: Note: if the project cannot be located, available project for your user will be printed in toolium.log onlyifchanges: true if previous state of the updated issues is similar true will force the update, false will omit it + summary_prefix: # TODO + fixversion: The fixversion field, see example below -labels: # TODO -comments: # TODO -build: # TODO + +labels: List of labels to be added + +comments: A comment to be added in each of the test executions + +build: Inactive field # TODO Pending field value confirmation Full example:: @@ -39,11 +44,10 @@ Full example:: project_name: Apache Commons RDF execution_url: https://jira.atlassian.com onlyifchanges: true - summary_prefix: # TODO + summary_prefix: [DEV][QA] fixversion: 4.12 labels: [QA, label2, 3rdlabel] - comments: # TODO - build: # TODO + comments: "A new comment for the execution" See `https://jira.readthedocs.io`_ for the complete Package documentation. From b20b8aca0983920bba811654084ab3ee516cb1f6 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Wed, 28 Jun 2023 13:43:37 +0200 Subject: [PATCH 22/31] Minor Refactor jira --- toolium/jira.py | 105 ++++++++++++++++++++------------------ toolium/test/test_jira.py | 9 ++-- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 1ba6257a..d2da39c6 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -322,8 +322,10 @@ def execute_query(jira: JIRA, query: str) -> list: return existing_issues -def create_test_execution(server: JIRA, issueid: str, projectid: int, summary_prefix: str, test_summary: str, - fix_version: str, parent_labels: list, new_labels: list = None, description: str = " ") -> Issue: +def create_test_execution(server: JIRA, issueid: str, projectid: int, + summary_prefix: str, test_summary: str, + fix_version: str, parent_labels: list, new_labels: list = None, + description: str = " ") -> Issue: """Creates an execution linked to the TestCase provided""" issue_dict = { 'project': {'id': projectid}, @@ -337,7 +339,7 @@ def create_test_execution(server: JIRA, issueid: str, projectid: int, summary_pr 'labels': parent_labels + new_labels, # 'versions': [{'name': fix_version}], 'description': description, - #TODO add build field ?? + # TODO add build field ?? } return server.create_issue(fields=issue_dict) @@ -348,7 +350,8 @@ def transition(server: JIRA, issue: Issue, test_status: str): Transitions the issue to a new state, see issue workflows in the project for available options param test_status: the new status of the issue """ - logger.info("Setting new status for " + issue.key + "from " + issue.fields.status.raw['name'] + " to " + test_status) + logger.info("Setting new status for " + issue.key + + "from " + issue.fields.status.raw['name'] + " to " + test_status) try: server.transition_issue(issue.key, transition=test_status.lower()) except JIRAError: @@ -360,58 +363,60 @@ def transition(server: JIRA, issue: Issue, test_status: str): logger.debug("Transitioned issue to status %s", test_status) -def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): - """Adds the results to the execution or the associated test case instead""" +def _addscreenshots(jira: JIRA, issueid: str, attachements: list[str] = None): + """ + Attach the screenshots found the report folder to the jira issue provided + param issueid: Full Jira ID + param attachements: Absolute paths of attachments + Raises FileNotFound: if an attachement file is not found + """ - def addscreenshots(issueid: str, attachements: list[str] = None): - """ - Attach the screenshots found the report folder to the jira issue provided - param issueid: Full Jira ID - param attachements: Absolute paths of attachments - Raises FileNotFound: if an attachement file is not found - """ - - if attachements: - for filepath in attachements: - if os.path.isfile(path.join(filepath)): - with open(filepath, 'rb') as file: - logger.debug("Opened screenshot " + file.name) - jira.add_attachment(issue=issueid, attachment=file) - logger.info("Attached " + file.name + " into " + issueid) - else: - screenshotspath = path.join(path.dirname(__file__), ".", DriverWrappersPool.screenshots_directory) - logger.debug("Reading screenshot folder " + screenshotspath) + if attachements: + for filepath in attachements: + if os.path.isfile(path.join(filepath)): + with open(filepath, 'rb') as file: + logger.debug("Opened screenshot " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached " + file.name + " into " + issueid) + else: + screenshotspath = path.join(path.dirname(__file__), ".", DriverWrappersPool.screenshots_directory) + logger.debug("Reading screenshot folder " + screenshotspath) - if len(os.listdir(screenshotspath)) < 1: - logger.warning("Screenshot folder empty...") - return + if len(os.listdir(screenshotspath)) < 1: + logger.warning("Screenshot folder empty...") + return + + for filename in os.listdir(screenshotspath): + if os.path.isfile(path.join(screenshotspath, filename)): + with open(os.path.join(screenshotspath, filename), 'rb') as file: + logger.debug("Opened screenshot " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached " + filename + " into " + issueid) + logger.info("Screenshots uploaded...") - for filename in os.listdir(screenshotspath): - if os.path.isfile(path.join(screenshotspath, filename)): - with open(os.path.join(screenshotspath, filename), 'rb') as file: - logger.debug("Opened screenshot " + file.name) - jira.add_attachment(issue=issueid, attachment=file) - logger.info("Attached " + filename + " into " + issueid) - logger.info("Screenshots uploaded...") - - def addlogs(issueid: str): - """ - Attach the logs in the report folder to the jira issue provided - param issueid Full Jira ID - Raises FileNotFound Error if the log file is not found in reports - """ - logpath = path.join(path.dirname(__file__), - ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") - - with open(logpath, 'rb') as file: - logger.debug("Opened log file " + file.name) - jira.add_attachment(issue=issueid, attachment=file) - logger.info("Attached log into " + issueid) + +def _addlogs(jira: JIRA, issueid: str): + """ + Attach the logs in the report folder to the jira issue provided + param issueid Full Jira ID + Raises FileNotFound Error if the log file is not found in reports + """ + logpath = path.join(path.dirname(__file__), + ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") + + with open(logpath, 'rb') as file: + logger.debug("Opened log file " + file.name) + jira.add_attachment(issue=issueid, attachment=file) + logger.info("Attached log into " + issueid) + + +def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): + """Adds the results to the execution or the associated test case instead""" try: logger.info("Adding results to issue...") - addscreenshots(issueid, attachements) if attachements else addscreenshots(issueid) - addlogs(issueid) + _addscreenshots(jira, issueid, attachements) if attachements else addscreenshots(issueid) + _addlogs(jira, issueid) logger.debug("Results added to issue " + issueid) except FileNotFoundError as error: diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 2e09a88a..70f85cd6 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -276,19 +276,20 @@ def call_jira_server(): assert jira.current_user() unittest.TestCase().assertRaises(JIRAError, call_jira_server) + def test_jira_check_valid_fix_version(): version = mock.Mock(Version) - version.configure_mock(raw={"name":"1.1"}) + version.configure_mock(raw={"name": "1.1"}) server = mock.Mock(JiraServer("https://jira.elevenpaths.com/", "6")) - server.configure_mock(project= lambda x:"KEY", project_versions = lambda arg: [version]) + server.configure_mock(project=lambda x: "KEY", project_versions=lambda arg: [version]) _check_fix_version(server, "KEY", "1.1") def test_jira_check_wrong_fix_version(): version = mock.Mock(Version) - version.configure_mock(raw={"name":"0.5"}) + version.configure_mock(raw={"name": "0.5"}) server = mock.Mock(JiraServer("https://jira.elevenpaths.com/", "6")) - server.configure_mock(project= lambda x:"KEY", project_versions = lambda arg: [version]) + server.configure_mock(project=lambda x: "KEY", project_versions=lambda arg: [version]) def call_check_fix_version_wrong(): _check_fix_version(server, "KEY", "1.1") From 26450460dbad27a41010113ac2c718076e3ea7a3 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Wed, 28 Jun 2023 14:38:11 +0200 Subject: [PATCH 23/31] Fix , Jira add screenshot call --- toolium/jira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolium/jira.py b/toolium/jira.py index d2da39c6..551d25a3 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -415,7 +415,7 @@ def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): try: logger.info("Adding results to issue...") - _addscreenshots(jira, issueid, attachements) if attachements else addscreenshots(issueid) + _addscreenshots(jira, issueid, attachements) if attachements else _addscreenshots(jira, issueid) _addlogs(jira, issueid) logger.debug("Results added to issue " + issueid) From ea11eea0b7db566b6b47eca0782a3ba100f05755 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Wed, 28 Jun 2023 14:44:05 +0200 Subject: [PATCH 24/31] Minor fix, typehints for python 3.7 --- toolium/jira.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 551d25a3..9e80d409 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -250,7 +250,7 @@ def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None raise ValueError(msg) -def change_jira_status(test_key, test_status, test_comment, test_attachments: list[str]): +def change_jira_status(test_key, test_status, test_comment, test_attachments: list): """Update test status in Jira :param test_key: test case key in Jira @@ -363,7 +363,7 @@ def transition(server: JIRA, issue: Issue, test_status: str): logger.debug("Transitioned issue to status %s", test_status) -def _addscreenshots(jira: JIRA, issueid: str, attachements: list[str] = None): +def _addscreenshots(jira: JIRA, issueid: str, attachements: list = None): """ Attach the screenshots found the report folder to the jira issue provided param issueid: Full Jira ID @@ -410,7 +410,7 @@ def _addlogs(jira: JIRA, issueid: str): logger.info("Attached log into " + issueid) -def add_results(jira: JIRA, issueid: str, attachements: list[str] = None): +def add_results(jira: JIRA, issueid: str, attachements: list = None): """Adds the results to the execution or the associated test case instead""" try: From 352e9bc3d8a56b00ec6d9a459c177288ee440bef Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 12:37:50 +0200 Subject: [PATCH 25/31] Fix, jira id config params --- toolium/jira.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 9e80d409..12f12a33 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -103,12 +103,12 @@ def save_jira_conf(): config = DriverWrappersPool.get_default_wrapper().config enabled = config.getboolean_optional('Jira', 'enabled') jiratoken = config.get_optional('Jira', 'token') - project_id = int(config.get_optional('Jira', 'project_id')) - project_key = int(config.get_optional('Jira', 'project_key')) - project_name = int(config.get_optional('Jira', 'project_name')) + project_id = int(config.get_optional('Jira', 'project_id', 0)) + project_key = config.get_optional('Jira', 'project_key') + project_name = config.get_optional('Jira', 'project_name') execution_url = config.get_optional('Jira', 'execution_url') summary_prefix = config.get_optional('Jira', 'summary_prefix') - labels_raw = config.get_optional('Jira', 'labels') + labels_raw = config.get_optional('Jira', 'labels', "") labels_raw = labels_raw.replace("[", "").replace("]", "") labels = [] for label in labels_raw.split(","): From a63a82a16b99c5f2dcc902e406ad165de6d22a21 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 12:41:24 +0200 Subject: [PATCH 26/31] Fix, jira Tests --- toolium/jira.py | 17 ++++--- toolium/test/test_jira.py | 101 +++++++++++++------------------------- 2 files changed, 46 insertions(+), 72 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 12f12a33..c10d1d46 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -19,6 +19,9 @@ import logging import os import re +from pathlib import Path + +import requests from os import path from jira import JIRA, Issue from jira.exceptions import JIRAError @@ -250,7 +253,7 @@ def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None raise ValueError(msg) -def change_jira_status(test_key, test_status, test_comment, test_attachments: list): +def change_jira_status(test_key, test_status, test_comment, test_attachments: list, jira_server: JIRA = None): """Update test status in Jira :param test_key: test case key in Jira @@ -272,7 +275,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li if only_if_changes: payload['onlyIfStatusChanges'] = 'true' try: - with JiraServer(execution_url, jiratoken) as server: + server = jira_server if jira_server else JiraServer(execution_url, jiratoken) + with server as server: check_jira_args(server) @@ -302,7 +306,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li add_results(server, new_execution.key, test_attachments) - except JIRAError as e: + except (JIRAError, requests.exceptions.ConnectionError) as e: print_trace(logger, e) logger.error("Exception while updating Issue '%s': %s", test_key, e) return @@ -395,14 +399,15 @@ def _addscreenshots(jira: JIRA, issueid: str, attachements: list = None): logger.info("Screenshots uploaded...") -def _addlogs(jira: JIRA, issueid: str): +def _addlogs(jira: JIRA, issueid: str, logpath: Path = None): """ Attach the logs in the report folder to the jira issue provided param issueid Full Jira ID Raises FileNotFound Error if the log file is not found in reports """ - logpath = path.join(path.dirname(__file__), - ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") + if not logpath: + logpath = path.join(path.dirname(__file__), + ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") with open(logpath, 'rb') as file: logger.debug("Opened log file " + file.name) diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 70f85cd6..d8682c2c 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -15,16 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. """ - +import importlib import os import unittest +from tempfile import TemporaryFile +from unittest.mock import ANY import mock import pytest -import requests_mock from jira import JIRAError from jira.resources import Version from requests.exceptions import ConnectionError +import toolium from toolium import jira from toolium.driver_wrappers_pool import DriverWrappersPool from toolium.jira import JiraServer, _check_fix_version @@ -42,87 +44,52 @@ def logger(): logger_patch.stop() # Clear jira module configuration - jira.enabled = None - jira.execution_url = None - jira.summary_prefix = None - jira.labels = None - jira.comments = None - jira.fix_version = None - jira.build = None - jira.only_if_changes = None - jira.jira_tests_status.clear() - jira.attachments = [] - + importlib.reload(toolium.jira) def test_change_jira_status(logger): - # Test response - response = b"The Test Case Execution 'TOOLIUM-11' has been created\r\n" # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' - jira.summary_prefix = 'prefix' - jira.labels = 'label1 label2' - jira.comments = 'comment' - jira.fix_version = 'Release 1.0' - jira.build = '453' - jira.only_if_changes = True - - with requests_mock.mock() as req_mock: - # Configure mock - req_mock.post(jira.execution_url, content=response) - - # Test method - jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) + jira.logger = mock.MagicMock() + jira.check_jira_args = mock.MagicMock() + jira.add_results = mock.MagicMock() + jira.transition = mock.MagicMock() + jira.execute_query = mock.MagicMock() + jira.execute_query.return_value = [mock.MagicMock()] + def create_test(*args, **kwargs): + mock_test = mock.MagicMock(key="TOOLIUM-TEST") + return mock_test + jira.create_test_execution = create_test - # Check requested url - assert jira.execution_url == req_mock.request_history[0].url - for partial_url in ['jiraStatus=Pass', 'jiraTestCaseId=TOOLIUM-1', 'summaryPrefix=prefix', - 'labels=label1+label2', 'comments=comment', 'version=Release+1.0', 'build=453', - 'onlyIfStatusChanges=true']: - assert partial_url in req_mock.request_history[0].text + # Test method + jira.change_jira_status('TOOLIUM-1', 'Pass', None, [], mock.MagicMock()) # Check logging call - logger.debug.assert_called_once_with("%s", "The Test Case Execution 'TOOLIUM-11' has been created") + jira.logger.debug.assert_any_call("Creating execution for TOOLIUM-1") + jira.logger.info.assert_called_with(f"Created execution TOOLIUM-TEST for test TOOLIUM-1") def test_change_jira_status_attachments(logger): - # Test response - response = b"The Test Case Execution 'TOOLIUM-11' has been created\r\n" # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' - jira.summary_prefix = 'prefix' - jira.labels = 'label1 label2' - jira.comments = 'comment' - jira.fix_version = 'Release 1.0' - jira.build = '453' jira.only_if_changes = True - resources_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'resources') - attachments = [os.path.join(resources_path, 'ios.png'), os.path.join(resources_path, 'ios_web.png')] - - with requests_mock.mock() as req_mock: - # Configure mock - req_mock.post(jira.execution_url, content=response) - - # Test method - jira.change_jira_status('TOOLIUM-1', 'Pass', None, attachments) + attachments = [os.path.dirname(TemporaryFile().name)] + jira.logger = mock.MagicMock() + def addlogs(*args, **kwargs): + jira.logger.info("Attached log into TOOLIUM-1") + jira._addlogs = addlogs - # Get response body - body_bytes = req_mock.last_request.body - - # Check requested url - body = "".join(map(chr, body_bytes)) - for partial_url in ['"jiraStatus"\r\n\r\nPass', '"jiraTestCaseId"\r\n\r\nTOOLIUM-1', - '"summaryPrefix"\r\n\r\nprefix', '"labels"\r\n\r\nlabel1 label2', - '"comments"\r\n\r\ncomment', '"version"\r\n\r\nRelease 1.0', '"build"\r\n\r\n453', - '"onlyIfStatusChanges"\r\n\r\ntrue', '"attachments0"; filename="ios.png"', - '"attachments1"; filename="ios_web.png"']: - assert partial_url in body + # Test method + jira.add_results(mock.MagicMock(), 'TOOLIUM-1', attachments) # Check logging call - logger.debug.assert_called_once_with("%s", "The Test Case Execution 'TOOLIUM-11' has been created") + calls = [mock.call('Adding results to issue...'), + mock.call('Screenshots uploaded...'), + mock.call("Attached log into TOOLIUM-1")] + jira.logger.info.assert_has_calls(calls) @mock.patch('toolium.jira.requests.get') @@ -132,29 +99,31 @@ def test_change_jira_status_empty_url(jira_get, logger): # Configure jira module jira.enabled = True + jira.logger = mock.MagicMock() # Test method jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) # Check logging error message - logger.warning.assert_called_once_with("Test Case '%s' can not be updated: execution_url is not configured", + jira.logger.warning.assert_called_once_with("Test Case '%s' can not be updated: execution_url is not configured", 'TOOLIUM-1') @mock.patch('toolium.jira.requests.post') def test_change_jira_status_exception(jira_post, logger): # Configure jira mock - jira_post.side_effect = ConnectionError('exception error') + jira_post.side_effect = ConnectionError() # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' + jira.logger = mock.MagicMock() # Test method jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) # Check logging error message - logger.warning.assert_called_once_with("Error updating Test Case '%s': %s", 'TOOLIUM-1', jira_post.side_effect) + jira.logger.error.assert_called_with("Exception while updating Issue '%s': %s", 'TOOLIUM-1', ANY) def test_jira_annotation_pass(logger): From 02041d2276a28fbb6f27745c2d3ddc7dc9ab7f41 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 12:41:24 +0200 Subject: [PATCH 27/31] Fix, jira Tests --- toolium/jira.py | 17 ++++--- toolium/test/test_jira.py | 102 ++++++++++++++------------------------ 2 files changed, 48 insertions(+), 71 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index 12f12a33..c10d1d46 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -19,6 +19,9 @@ import logging import os import re +from pathlib import Path + +import requests from os import path from jira import JIRA, Issue from jira.exceptions import JIRAError @@ -250,7 +253,7 @@ def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None raise ValueError(msg) -def change_jira_status(test_key, test_status, test_comment, test_attachments: list): +def change_jira_status(test_key, test_status, test_comment, test_attachments: list, jira_server: JIRA = None): """Update test status in Jira :param test_key: test case key in Jira @@ -272,7 +275,8 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li if only_if_changes: payload['onlyIfStatusChanges'] = 'true' try: - with JiraServer(execution_url, jiratoken) as server: + server = jira_server if jira_server else JiraServer(execution_url, jiratoken) + with server as server: check_jira_args(server) @@ -302,7 +306,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li add_results(server, new_execution.key, test_attachments) - except JIRAError as e: + except (JIRAError, requests.exceptions.ConnectionError) as e: print_trace(logger, e) logger.error("Exception while updating Issue '%s': %s", test_key, e) return @@ -395,14 +399,15 @@ def _addscreenshots(jira: JIRA, issueid: str, attachements: list = None): logger.info("Screenshots uploaded...") -def _addlogs(jira: JIRA, issueid: str): +def _addlogs(jira: JIRA, issueid: str, logpath: Path = None): """ Attach the logs in the report folder to the jira issue provided param issueid Full Jira ID Raises FileNotFound Error if the log file is not found in reports """ - logpath = path.join(path.dirname(__file__), - ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") + if not logpath: + logpath = path.join(path.dirname(__file__), + ".", DriverWrappersPool.screenshots_directory, "..", "..", "toolium.log") with open(logpath, 'rb') as file: logger.debug("Opened log file " + file.name) diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 70f85cd6..69279769 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -15,16 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. """ - +import importlib import os import unittest +from tempfile import TemporaryFile +from unittest.mock import ANY import mock import pytest -import requests_mock from jira import JIRAError from jira.resources import Version from requests.exceptions import ConnectionError +import toolium from toolium import jira from toolium.driver_wrappers_pool import DriverWrappersPool from toolium.jira import JiraServer, _check_fix_version @@ -42,87 +44,55 @@ def logger(): logger_patch.stop() # Clear jira module configuration - jira.enabled = None - jira.execution_url = None - jira.summary_prefix = None - jira.labels = None - jira.comments = None - jira.fix_version = None - jira.build = None - jira.only_if_changes = None - jira.jira_tests_status.clear() - jira.attachments = [] + importlib.reload(toolium.jira) def test_change_jira_status(logger): - # Test response - response = b"The Test Case Execution 'TOOLIUM-11' has been created\r\n" # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' - jira.summary_prefix = 'prefix' - jira.labels = 'label1 label2' - jira.comments = 'comment' - jira.fix_version = 'Release 1.0' - jira.build = '453' - jira.only_if_changes = True - - with requests_mock.mock() as req_mock: - # Configure mock - req_mock.post(jira.execution_url, content=response) - - # Test method - jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) + jira.logger = mock.MagicMock() + jira.check_jira_args = mock.MagicMock() + jira.add_results = mock.MagicMock() + jira.transition = mock.MagicMock() + jira.execute_query = mock.MagicMock() + jira.execute_query.return_value = [mock.MagicMock()] + + def create_test(*args, **kwargs): + mock_test = mock.MagicMock(key="TOOLIUM-TEST") + return mock_test + jira.create_test_execution = create_test - # Check requested url - assert jira.execution_url == req_mock.request_history[0].url - for partial_url in ['jiraStatus=Pass', 'jiraTestCaseId=TOOLIUM-1', 'summaryPrefix=prefix', - 'labels=label1+label2', 'comments=comment', 'version=Release+1.0', 'build=453', - 'onlyIfStatusChanges=true']: - assert partial_url in req_mock.request_history[0].text + # Test method + jira.change_jira_status('TOOLIUM-1', 'Pass', None, [], mock.MagicMock()) # Check logging call - logger.debug.assert_called_once_with("%s", "The Test Case Execution 'TOOLIUM-11' has been created") + jira.logger.debug.assert_any_call("Creating execution for TOOLIUM-1") + jira.logger.info.assert_called_with("Created execution TOOLIUM-TEST for test TOOLIUM-1") def test_change_jira_status_attachments(logger): - # Test response - response = b"The Test Case Execution 'TOOLIUM-11' has been created\r\n" # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' - jira.summary_prefix = 'prefix' - jira.labels = 'label1 label2' - jira.comments = 'comment' - jira.fix_version = 'Release 1.0' - jira.build = '453' jira.only_if_changes = True - resources_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'resources') - attachments = [os.path.join(resources_path, 'ios.png'), os.path.join(resources_path, 'ios_web.png')] - - with requests_mock.mock() as req_mock: - # Configure mock - req_mock.post(jira.execution_url, content=response) + attachments = [os.path.dirname(TemporaryFile().name)] + jira.logger = mock.MagicMock() - # Test method - jira.change_jira_status('TOOLIUM-1', 'Pass', None, attachments) + def addlogs(*args, **kwargs): + jira.logger.info("Attached log into TOOLIUM-1") + jira._addlogs = addlogs - # Get response body - body_bytes = req_mock.last_request.body - - # Check requested url - body = "".join(map(chr, body_bytes)) - for partial_url in ['"jiraStatus"\r\n\r\nPass', '"jiraTestCaseId"\r\n\r\nTOOLIUM-1', - '"summaryPrefix"\r\n\r\nprefix', '"labels"\r\n\r\nlabel1 label2', - '"comments"\r\n\r\ncomment', '"version"\r\n\r\nRelease 1.0', '"build"\r\n\r\n453', - '"onlyIfStatusChanges"\r\n\r\ntrue', '"attachments0"; filename="ios.png"', - '"attachments1"; filename="ios_web.png"']: - assert partial_url in body + # Test method + jira.add_results(mock.MagicMock(), 'TOOLIUM-1', attachments) # Check logging call - logger.debug.assert_called_once_with("%s", "The Test Case Execution 'TOOLIUM-11' has been created") + calls = [mock.call('Adding results to issue...'), + mock.call('Screenshots uploaded...'), + mock.call("Attached log into TOOLIUM-1")] + jira.logger.info.assert_has_calls(calls) @mock.patch('toolium.jira.requests.get') @@ -132,29 +102,31 @@ def test_change_jira_status_empty_url(jira_get, logger): # Configure jira module jira.enabled = True + jira.logger = mock.MagicMock() # Test method jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) # Check logging error message - logger.warning.assert_called_once_with("Test Case '%s' can not be updated: execution_url is not configured", - 'TOOLIUM-1') + jira.logger.warning.assert_called_once_with( + "Test Case '%s' can not be updated: execution_url is not configured", 'TOOLIUM-1') @mock.patch('toolium.jira.requests.post') def test_change_jira_status_exception(jira_post, logger): # Configure jira mock - jira_post.side_effect = ConnectionError('exception error') + jira_post.side_effect = ConnectionError() # Configure jira module jira.enabled = True jira.execution_url = 'http://server/execution_service' + jira.logger = mock.MagicMock() # Test method jira.change_jira_status('TOOLIUM-1', 'Pass', None, []) # Check logging error message - logger.warning.assert_called_once_with("Error updating Test Case '%s': %s", 'TOOLIUM-1', jira_post.side_effect) + jira.logger.error.assert_called_with("Exception while updating Issue '%s': %s", 'TOOLIUM-1', ANY) def test_jira_annotation_pass(logger): From 5bd363ec46064802f0939f1098f61a6b3daa37a4 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 13:02:10 +0200 Subject: [PATCH 28/31] Fix, named temp file in jira Tests --- toolium/test/test_jira.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolium/test/test_jira.py b/toolium/test/test_jira.py index 69279769..9be65e3a 100644 --- a/toolium/test/test_jira.py +++ b/toolium/test/test_jira.py @@ -18,7 +18,7 @@ import importlib import os import unittest -from tempfile import TemporaryFile +from tempfile import NamedTemporaryFile from unittest.mock import ANY import mock import pytest @@ -78,7 +78,7 @@ def test_change_jira_status_attachments(logger): jira.enabled = True jira.execution_url = 'http://server/execution_service' jira.only_if_changes = True - attachments = [os.path.dirname(TemporaryFile().name)] + attachments = [os.path.dirname(os.path.join(NamedTemporaryFile().name))] jira.logger = mock.MagicMock() def addlogs(*args, **kwargs): From b793468e8fa8c03bb242dc677bcd13b9a347592d Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 16:45:54 +0200 Subject: [PATCH 29/31] Refactor, Jira codeclimate recomendations --- toolium/jira.py | 118 ++++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index c10d1d46..fe4eb85d 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -113,7 +113,6 @@ def save_jira_conf(): summary_prefix = config.get_optional('Jira', 'summary_prefix') labels_raw = config.get_optional('Jira', 'labels', "") labels_raw = labels_raw.replace("[", "").replace("]", "") - labels = [] for label in labels_raw.split(","): labels.append(label) comments = config.get_optional('Jira', 'comments') @@ -181,7 +180,7 @@ def check_jira_args(server: JIRA): _check_fix_version(server, project_key, fix_version) -def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): +def _check_project_set_ids(server: JIRA, p_id: int, p_key: str, p_name: str): """ Checks the provided project identifiers returns the values updated if they were empty Note that the args are provided in order of precedence to prevent mismatches between them @@ -199,30 +198,20 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): """ available_keys = [] for project_instance in server.projects(): - # TODO turn into dict available_keys.append( - [project_instance.raw['name'], project_instance.raw['key'], project_instance.raw['id']]) + {"name": project_instance.raw['name'], + "key": project_instance.raw['key'], + "id": project_instance.raw['id']} + ) logger.debug(f"Read project info read name:'{p_name}', key:'{p_key}', id:'{p_id}'") project_option = "" - for key in available_keys: - if p_id in key[2]: - project_option = p_id - p_key = server.project(str(p_id)).raw["key"] - break - elif p_key in key[1]: - project_option = p_key - p_id = str(server.project(p_key).raw["id"]) - break - elif p_name in key[0]: - project_option = p_name - for version in available_keys: - if p_name in version: - p_key = version[1] - p_id = str(version[2]) - break - break + for option in [p_id, p_key, p_name]: + for key in available_keys: + _set_project_data(server, option, str(key["id"]), key["key"]) + if p_id > 0: + break if not project_option: msg = f"No existe el proyecto especificado name:'{p_name}', key:'{p_key}', id:'{p_id}'" @@ -233,6 +222,19 @@ def _check_project_set_ids(server: JIRA, p_id: str, p_key: str, p_name: str): return p_id, p_key +def _set_project_data(server: JIRA, target: str, project_id: str, project_key: str): + if target in project_id: + project_key = server.project(str(project_id)).raw["key"] + + elif target in project_key: + project_id = str(server.project(project_key).raw["id"]) + + else: + return None, None + + return project_id, project_key + + def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None: """ Retrieves the fix_versions for the current project and ensures the one provided is valid @@ -260,6 +262,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li :param test_status: test case status :param test_comment: test case comments :param test_attachments: list of absolutes paths of test case attachment files + :param jira_server: JIRA server instance to use for the upload if any """ if not execution_url: @@ -288,10 +291,10 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li # test_key = issue.key logger.debug("Creating execution for " + test_key) + labels += existing_issues[0].fields.labels + summary = f"{summary_prefix} Execution of {existing_issues[0].fields.summary} {test_key}" new_execution = create_test_execution(server, test_key, project_id, - summary_prefix, existing_issues[0].fields.summary, - fix_version, - existing_issues[0].fields.labels, labels) + summary, fix_version, labels) logger.info(f"Created execution {new_execution.key} for test " + test_key) if composed_comments: @@ -327,8 +330,7 @@ def execute_query(jira: JIRA, query: str) -> list: def create_test_execution(server: JIRA, issueid: str, projectid: int, - summary_prefix: str, test_summary: str, - fix_version: str, parent_labels: list, new_labels: list = None, + summary: str, fix_version: str, labels: list = None, description: str = " ") -> Issue: """Creates an execution linked to the TestCase provided""" issue_dict = { @@ -336,11 +338,11 @@ def create_test_execution(server: JIRA, issueid: str, projectid: int, 'assignee': {'name': server.current_user()}, 'issuetype': {'name': 'Test Case Execution'}, 'parent': {'key': issueid}, - 'summary': str(summary_prefix) + " Execution of " + test_summary + " " + issueid, + 'summary': summary, 'fixVersions': [{'name': fix_version}], # TODO set priority as config input? # 'priority': {'name': 'Minor', "id": 10101}, - 'labels': parent_labels + new_labels, + 'labels': labels, # 'versions': [{'name': fix_version}], 'description': description, # TODO add build field ?? @@ -375,30 +377,48 @@ def _addscreenshots(jira: JIRA, issueid: str, attachements: list = None): Raises FileNotFound: if an attachement file is not found """ - if attachements: - for filepath in attachements: - if os.path.isfile(path.join(filepath)): - with open(filepath, 'rb') as file: - logger.debug("Opened screenshot " + file.name) - jira.add_attachment(issue=issueid, attachment=file) - logger.info("Attached " + file.name + " into " + issueid) - else: - screenshotspath = path.join(path.dirname(__file__), ".", DriverWrappersPool.screenshots_directory) - logger.debug("Reading screenshot folder " + screenshotspath) - - if len(os.listdir(screenshotspath)) < 1: - logger.warning("Screenshot folder empty...") - return - - for filename in os.listdir(screenshotspath): - if os.path.isfile(path.join(screenshotspath, filename)): - with open(os.path.join(screenshotspath, filename), 'rb') as file: - logger.debug("Opened screenshot " + file.name) - jira.add_attachment(issue=issueid, attachment=file) - logger.info("Attached " + filename + " into " + issueid) + if not attachements: + attachements = read_screenshot_folder() + + for filepath in attachements: + if os.path.isfile(path.join(filepath)): + _add_screenshot_as_file(jira, issueid, path.join(filepath)) + logger.info("Screenshots uploaded...") +def read_screenshot_folder() -> list: + """ + Reads the screenshot folder and returns a list with the absolute paths to each file found + """ + screenshotspath = path.join(path.dirname(__file__), ".", DriverWrappersPool.screenshots_directory) + logger.debug("Reading screenshot folder " + screenshotspath) + file_list = [] + + if len(os.listdir(screenshotspath)) < 1: + logger.warning("Screenshot folder empty...") + + for filename in os.listdir(screenshotspath): + if os.path.isfile(path.join(screenshotspath, filename)): + file_list.append(path.dirname(path.join(screenshotspath, filename))) + + return file_list + + +def _add_screenshot_as_file(jira: JIRA, issue_id: str, screenshotspath: str): + """ + Adds the given file as an attachement for the provided issue + :param jira: JIRA server to be used for the attachement + :param issue_id: The id of the target isue + :param screenshotspath: Absolute path of the attachment + Raises OSError if the file cannot be opened + """ + with open(os.path.join(screenshotspath), 'rb') as file: + logger.debug("Opened screenshot " + file.name) + jira.add_attachment(issue=issue_id, attachment=file) + logger.info("Attached " + file.name + " into " + issue_id) + + def _addlogs(jira: JIRA, issueid: str, logpath: Path = None): """ Attach the logs in the report folder to the jira issue provided From 546ef33dfb24aa7621b50d26e3fe51c756f0a843 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 17:45:39 +0200 Subject: [PATCH 30/31] Minor fix + refactor Jira methods --- toolium/jira.py | 79 ++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/toolium/jira.py b/toolium/jira.py index fe4eb85d..2704541e 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -196,43 +196,26 @@ def _check_project_set_ids(server: JIRA, p_id: int, p_key: str, p_name: str): p_key (str): the key shortcut for the project """ - available_keys = [] - for project_instance in server.projects(): - available_keys.append( - {"name": project_instance.raw['name'], - "key": project_instance.raw['key'], - "id": project_instance.raw['id']} - ) - - logger.debug(f"Read project info read name:'{p_name}', key:'{p_key}', id:'{p_id}'") - - project_option = "" - for option in [p_id, p_key, p_name]: - for key in available_keys: - _set_project_data(server, option, str(key["id"]), key["key"]) - if p_id > 0: - break - - if not project_option: - msg = f"No existe el proyecto especificado name:'{p_name}', key:'{p_key}', id:'{p_id}'" - logger.warning(f"Available projects for your user: '{available_keys}'") - logger.error(msg) - raise ValueError(msg) - - return p_id, p_key + if p_id: + p_key = server.project(str(p_id)).raw["key"] + elif p_key: + p_id = str(server.project(p_key).raw["id"]) -def _set_project_data(server: JIRA, target: str, project_id: str, project_key: str): - if target in project_id: - project_key = server.project(str(project_id)).raw["key"] - - elif target in project_key: - project_id = str(server.project(project_key).raw["id"]) + elif project_name: + for project in server.projects(): + if project_name == project["name"]: + p_id = project["id"] + p_key = project["key"] + logger.debug(f"Read project info, name:'{p_name}', key:'{p_key}', id:'{p_id}'") else: - return None, None + msg = f"No existe el proyecto especificado name:'{p_name}', key:'{p_key}', id:'{p_id}'" + logger.warning(f"Available projects for your user: '{server.projects()}'") + logger.error(msg) + raise ValueError(msg) - return project_id, project_key + return p_id, p_key def _check_fix_version(server: JIRA, project_key: str, fix_version: str) -> None: @@ -265,6 +248,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li :param jira_server: JIRA server instance to use for the upload if any """ + if not execution_url: logger.warning("Test Case '%s' can not be updated: execution_url is not configured", test_key) return @@ -273,8 +257,12 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li composed_comments = comments if test_comment: composed_comments = '{}\n{}'.format(comments, test_comment) if comments else test_comment + + global labels + # TODO remove global reference payload = {'jiraTestCaseId': test_key, 'jiraStatus': test_status, 'summaryPrefix': summary_prefix, 'labels': labels, 'comments': composed_comments, 'version': fix_version, 'build': build} + if only_if_changes: payload['onlyIfStatusChanges'] = 'true' try: @@ -293,8 +281,7 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li logger.debug("Creating execution for " + test_key) labels += existing_issues[0].fields.labels summary = f"{summary_prefix} Execution of {existing_issues[0].fields.summary} {test_key}" - new_execution = create_test_execution(server, test_key, project_id, - summary, fix_version, labels) + new_execution = create_test_execution(server, test_key, summary, fix_version, labels) logger.info(f"Created execution {new_execution.key} for test " + test_key) if composed_comments: @@ -329,12 +316,30 @@ def execute_query(jira: JIRA, query: str) -> list: return existing_issues -def create_test_execution(server: JIRA, issueid: str, projectid: int, - summary: str, fix_version: str, labels: list = None, +def search_project_id_from_issue(server: JIRA, issue_id: str) -> int: + """ + Returns the id of the project given an issue identifier + """ + # TODO remove either this or the duplicated set id/key method above + try: + if project_id > 0: + logger.debug("Global project id found") + return project_id + except AttributeError: + for project in server.projects(): + if issue_id.split("-")[0].strip().upper() in project.raw["key"]: + logger.debug(f'Found id {project.raw["id"]} for issue {issue_id}') + return int(project.raw["id"]) + return 0 + + +def create_test_execution(server: JIRA, issueid: str, summary: str, + fix_version: str, labels: list = None, description: str = " ") -> Issue: """Creates an execution linked to the TestCase provided""" + issue_dict = { - 'project': {'id': projectid}, + 'project': {'id': search_project_id_from_issue(server, issueid)}, 'assignee': {'name': server.current_user()}, 'issuetype': {'name': 'Test Case Execution'}, 'parent': {'key': issueid}, From 52cc59f1464203124b2b484dc04d2a84d7ad2989 Mon Sep 17 00:00:00 2001 From: KJAMRAOUI Date: Thu, 29 Jun 2023 17:47:25 +0200 Subject: [PATCH 31/31] Minor refactor Jira --- toolium/jira.py | 1 - 1 file changed, 1 deletion(-) diff --git a/toolium/jira.py b/toolium/jira.py index 2704541e..b0bf641f 100644 --- a/toolium/jira.py +++ b/toolium/jira.py @@ -248,7 +248,6 @@ def change_jira_status(test_key, test_status, test_comment, test_attachments: li :param jira_server: JIRA server instance to use for the upload if any """ - if not execution_url: logger.warning("Test Case '%s' can not be updated: execution_url is not configured", test_key) return