summaryrefslogtreecommitdiffstats
path: root/taskcluster/scripts/lib
diff options
context:
space:
mode:
Diffstat (limited to 'taskcluster/scripts/lib')
-rw-r--r--taskcluster/scripts/lib/testrail_api.py130
-rw-r--r--taskcluster/scripts/lib/testrail_conn.py109
-rw-r--r--taskcluster/scripts/lib/testrail_utils.py84
3 files changed, 323 insertions, 0 deletions
diff --git a/taskcluster/scripts/lib/testrail_api.py b/taskcluster/scripts/lib/testrail_api.py
new file mode 100644
index 0000000000..44474ebe9d
--- /dev/null
+++ b/taskcluster/scripts/lib/testrail_api.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+This module provides a TestRail class for interfacing with the TestRail API, enabling the creation and management of test milestones, test runs, and updating test cases. It facilitates automation and integration of TestRail functionalities into testing workflows, particularly for projects requiring automated test management and reporting.
+
+The TestRail class encapsulates methods to interact with TestRail's API, including creating milestones and test runs, updating test cases, and checking the existence of milestones. It also features a method to retry API calls, enhancing the robustness of network interactions.
+
+Key Components:
+- TestRail Class: A class providing methods for interacting with TestRail's API.
+ - create_milestone: Create a new milestone in a TestRail project.
+ - create_milestone_and_test_runs: Create a milestone and associated test runs for multiple devices in a project.
+ - create_test_run: Create a test run within a TestRail project.
+ - does_milestone_exist: Check if a milestone already exists in a TestRail project.
+ - update_test_cases_to_passed: Update the status of test cases to 'passed' in a test run.
+- Private Methods: Utility methods for internal use to fetch test cases, update test run results, and retrieve milestones.
+- Retry Mechanism: A method to retry API calls with a specified number of attempts and delay, improving reliability in case of intermittent network issues.
+
+Usage:
+This module is intended to be used as part of a larger automated testing system, where integration with TestRail is required for test management and reporting.
+
+"""
+
+import os
+import sys
+import time
+
+# Ensure the directory containing this script is in Python's search path
+script_directory = os.path.dirname(os.path.abspath(__file__))
+if script_directory not in sys.path:
+ sys.path.append(script_directory)
+
+from testrail_conn import APIClient
+
+
+class TestRail:
+ def __init__(self, host, username, password):
+ self.client = APIClient(host)
+ self.client.user = username
+ self.client.password = password
+
+ # Public Methods
+
+ def create_milestone(self, testrail_project_id, title, description):
+ data = {"name": title, "description": description}
+ return self.client.send_post(f"add_milestone/{testrail_project_id}", data)
+
+ def create_milestone_and_test_runs(
+ self, project_id, milestone_name, milestone_description, devices, test_suite_id
+ ):
+ # Create milestone
+ milestone_id = self._retry_api_call(
+ self.create_milestone, project_id, milestone_name, milestone_description
+ )["id"]
+
+ # Create test runs for each device
+ for device in devices:
+ test_run_id = self._retry_api_call(
+ self.create_test_run, project_id, milestone_id, device, test_suite_id
+ )["id"]
+ self._retry_api_call(
+ self.update_test_cases_to_passed, project_id, test_run_id, test_suite_id
+ )
+
+ return milestone_id
+
+ def create_test_run(
+ self, testrail_project_id, testrail_milestone_id, name_run, testrail_suite_id
+ ):
+ data = {
+ "name": name_run,
+ "milestone_id": testrail_milestone_id,
+ "suite_id": testrail_suite_id,
+ }
+ return self.client.send_post(f"add_run/{testrail_project_id}", data)
+
+ def does_milestone_exist(self, testrail_project_id, milestone_name):
+ num_of_milestones_to_check = 10 # check last 10 milestones
+ milestones = self._get_milestones(
+ testrail_project_id
+ ) # returns reverse chronological order
+ for milestone in milestones[
+ -num_of_milestones_to_check:
+ ]: # check last 10 api responses
+ if milestone_name == milestone["name"]:
+ return True
+ return False
+
+ def update_test_cases_to_passed(
+ self, testrail_project_id, testrail_run_id, testrail_suite_id
+ ):
+ test_cases = self._get_test_cases(testrail_project_id, testrail_suite_id)
+ data = {
+ "results": [
+ {"case_id": test_case["id"], "status_id": 1} for test_case in test_cases
+ ]
+ }
+ return self._update_test_run_results(testrail_run_id, data)
+
+ # Private Methods
+
+ def _get_test_cases(self, testrail_project_id, testrail_test_suite_id):
+ return self.client.send_get(
+ f"get_cases/{testrail_project_id}&suite_id={testrail_test_suite_id}"
+ )
+
+ def _update_test_run_results(self, testrail_run_id, data):
+ return self.client.send_post(f"add_results_for_cases/{testrail_run_id}", data)
+
+ def _get_milestones(self, testrail_project_id):
+ return self.client.send_get(f"get_milestones/{testrail_project_id}")
+
+ def _retry_api_call(self, api_call, *args, max_retries=3, delay=5):
+ """
+ Retries the given API call up to max_retries times with a delay between attempts.
+
+ :param api_call: The API call method to retry.
+ :param args: Arguments to pass to the API call.
+ :param max_retries: Maximum number of retries.
+ :param delay: Delay between retries in seconds.
+ """
+ for attempt in range(max_retries):
+ try:
+ return api_call(*args)
+ except Exception:
+ if attempt == max_retries - 1:
+ raise # Reraise the last exception
+ time.sleep(delay)
diff --git a/taskcluster/scripts/lib/testrail_conn.py b/taskcluster/scripts/lib/testrail_conn.py
new file mode 100644
index 0000000000..92e3aae275
--- /dev/null
+++ b/taskcluster/scripts/lib/testrail_conn.py
@@ -0,0 +1,109 @@
+# flake8: noqa
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""TestRail API binding for Python 3.x.
+
+(API v2, available since TestRail 3.0)
+
+Compatible with TestRail 3.0 and later.
+
+Learn more:
+
+http://docs.gurock.com/testrail-api2/start
+http://docs.gurock.com/testrail-api2/accessing
+
+Copyright Gurock Software GmbH. See license.md for details.
+"""
+
+import base64
+import json
+
+import requests
+
+
+class APIClient:
+ def __init__(self, base_url):
+ self.user = ""
+ self.password = ""
+ if not base_url.endswith("/"):
+ base_url += "/"
+ self.__url = base_url + "index.php?/api/v2/"
+
+ def send_get(self, uri, filepath=None):
+ """Issue a GET request (read) against the API.
+
+ Args:
+ uri: The API method to call including parameters, e.g. get_case/1.
+ filepath: The path and file name for attachment download; used only
+ for 'get_attachment/:attachment_id'.
+
+ Returns:
+ A dict containing the result of the request.
+ """
+ return self.__send_request("GET", uri, filepath)
+
+ def send_post(self, uri, data):
+ """Issue a POST request (write) against the API.
+
+ Args:
+ uri: The API method to call, including parameters, e.g. add_case/1.
+ data: The data to submit as part of the request as a dict; strings
+ must be UTF-8 encoded. If adding an attachment, must be the
+ path to the file.
+
+ Returns:
+ A dict containing the result of the request.
+ """
+ return self.__send_request("POST", uri, data)
+
+ def __send_request(self, method, uri, data):
+ url = self.__url + uri
+
+ auth = str(
+ base64.b64encode(bytes("%s:%s" % (self.user, self.password), "utf-8")),
+ "ascii",
+ ).strip()
+ headers = {"Authorization": "Basic " + auth}
+
+ if method == "POST":
+ if uri[:14] == "add_attachment": # add_attachment API method
+ files = {"attachment": (open(data, "rb"))}
+ response = requests.post(url, headers=headers, files=files)
+ files["attachment"].close()
+ else:
+ headers["Content-Type"] = "application/json"
+ payload = bytes(json.dumps(data), "utf-8")
+ response = requests.post(url, headers=headers, data=payload)
+ else:
+ headers["Content-Type"] = "application/json"
+ response = requests.get(url, headers=headers)
+
+ if response.status_code > 201:
+ try:
+ error = response.json()
+ except (
+ requests.exceptions.HTTPError
+ ): # response.content not formatted as JSON
+ error = str(response.content)
+ raise APIError(
+ "TestRail API returned HTTP %s (%s)" % (response.status_code, error)
+ )
+ else:
+ if uri[:15] == "get_attachment/": # Expecting file, not JSON
+ try:
+ open(data, "wb").write(response.content)
+ return data
+ except FileNotFoundError:
+ return "Error saving attachment."
+ else:
+ try:
+ return response.json()
+ except requests.exceptions.HTTPError:
+ return {}
+
+
+class APIError(Exception):
+ pass
diff --git a/taskcluster/scripts/lib/testrail_utils.py b/taskcluster/scripts/lib/testrail_utils.py
new file mode 100644
index 0000000000..3f502397b8
--- /dev/null
+++ b/taskcluster/scripts/lib/testrail_utils.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+This script contains utility functions designed to support the integration of automated
+testing processes with TestRail, a test case management tool. The primary focus is on
+creating and managing milestones in TestRail based on automated smoke tests for product
+releases. It includes functions for building milestone names and descriptions, determining
+release types, and loading TestRail credentials.
+
+Functions:
+- build_milestone_name(product_type, release_type, version_number): Constructs a formatted
+ milestone name based on the product type, release type, and version number.
+- build_milestone_description(milestone_name): Generates a detailed description for the
+ milestone, including the release date and placeholders for testing status and QA recommendations.
+- get_release_version(): Reads and returns the release version number from a 'version.txt' file.
+- get_release_type(version): Determines the release type (e.g., Alpha, Beta, RC) based on
+ the version string.
+- load_testrail_credentials(json_file_path): Loads TestRail credentials from a JSON file
+ and handles potential errors during the loading process.
+"""
+
+import json
+import os
+import textwrap
+from datetime import datetime
+
+
+def build_milestone_name(product_type, release_type, version_number):
+ return f"Build Validation sign-off - {product_type} {release_type} {version_number}"
+
+
+def build_milestone_description(milestone_name):
+ current_date = datetime.now()
+ formatted_date = current_date = current_date.strftime("%B %d, %Y")
+ return textwrap.dedent(
+ f"""
+ RELEASE: {milestone_name}\n\n\
+ RELEASE_TAG_URL: https://archive.mozilla.org/pub/fenix/releases/\n\n\
+ RELEASE_DATE: {formatted_date}\n\n\
+ TESTING_STATUS: [ TBD ]\n\n\
+ QA_RECOMMENDATION:[ TBD ]\n\n\
+ QA_RECOMENTATION_VERBOSE: \n\n\
+ TESTING_SUMMARY\n\n\
+ Known issues: n/a\n\
+ New issue: n/a\n\
+ Verified issue:
+ """
+ )
+
+
+def get_release_version():
+ # Check if version.txt was found
+ version_file_path = os.path.join(
+ os.environ.get("GECKO_PATH", "."), "mobile", "android", "version.txt"
+ )
+ if not os.path.isfile(version_file_path):
+ raise FileNotFoundError(f"{version_file_path} not found.")
+
+ # Read the version from the file
+ with open(version_file_path, "r") as file:
+ version = file.readline().strip()
+
+ return version
+
+
+def get_release_type(version):
+ release_map = {"a": "Alpha", "b": "Beta"}
+ # use generator expression to check each char for key else default to 'RC'
+ product_type = next(
+ (release_map[char] for char in version if char in release_map), "RC"
+ )
+ return product_type
+
+
+def load_testrail_credentials(json_file_path):
+ try:
+ with open(json_file_path, "r") as file:
+ credentials = json.load(file)
+ return credentials
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Failed to load TestRail credentials: {e}")