"""This script uploads a cucumber feature file to a test set ticket in JIRA"""
import argparse
import getpass
import os
import sys
import pytest_bdd
import requests
from requests.auth import HTTPBasicAuth
JIRA_ISSUE_URL = "https://jira.skatelescope.org/browse/{}"
XTP_TESTSET_URL = "https://jira.skatelescope.org/rest/raven/1.0/api/testset/{}/test"
FEATURE_UPLOAD_URL = (
"https://jira.skatelescope.org/rest/raven/1.0/import/feature?projectKey=XTP"
)
HEADERS = {"Accept": "application/json"}
[docs]def associate_test_with_test_set(
args: argparse.Namespace,
test_set_xtp_ticket: str,
new_test_xtp_ticket: str,
):
"""Associate a Jira test ticket to a Jira test set ticket
:param args: The passed paramaters
:param test_set_xtp_ticket: The XTP ticket number of the test set
:param new_test_xtp_ticket: The XTP ticket number of the test
"""
if args.verbose:
print(
f"Associating test ticket {new_test_xtp_ticket} to test set "
f"{test_set_xtp_ticket}"
)
data = {"add": [f"{new_test_xtp_ticket}"], "remove": []}
url = XTP_TESTSET_URL.format(test_set_xtp_ticket)
response = requests.post(
url, auth=HTTPBasicAuth(args.username, args.password), json=data
)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as err:
if response.status_code == 400:
raise SystemExit(f"A bad request was made to {url}.") from err
if response.status_code == 401:
raise SystemExit(
"Authentication failure, either the credentials are incorrect or the "
"Xray license has expired."
) from err
if response.status_code == 500:
raise SystemExit(
"An internal error occurred when associating the tests."
) from err
raise SystemExit(err) from err
if args.verbose:
print(f"Result: {response.content}")
[docs]def upload_feature_file(xtp_ticket: str, args: argparse.Namespace) -> list:
"""POST the .feature file to JIRA
If the `Scenario Outline` in the feature file matches the Summary of a Jira Test
ticket, then the ticket will be updated.
If not then a new ticket will be created.
:param xtp_ticket: the test set XTP ticket
:param args: the passed paramaters
:return: details of the new/updated test ticket
"""
jira_issue_url = JIRA_ISSUE_URL.format(xtp_ticket)
response = None
if not args.dry_run:
if args.verbose:
print(f"Uploading feature file {args.feature_file.name}...")
with open(args.feature_file.name, "rb") as open_file:
files = {"file": open_file}
response = requests.post(
FEATURE_UPLOAD_URL,
auth=HTTPBasicAuth(args.username, args.password),
files=files,
)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as err:
if response.status_code == 400:
raise SystemExit("No .feature file was provided.") from err
if response.status_code == 401:
raise SystemExit(
"Authentication failure, either the credentials are incorrect "
"or the Xray license has expired."
) from err
if response.status_code == 500:
raise SystemExit(
"An internal error occurred when uploading the .feature file."
) from err
raise SystemExit(err) from err
response = response.json()
if args.verbose:
print(f"Result: {response}")
print(
f"Uploaded feature file {args.feature_file.name} to "
f"{jira_issue_url}."
)
else:
print(f"[DRY RUN] Uploading feature file {args.feature_file.name}...")
print(
f"[DRY RUN] Uploaded feature file {args.feature_file.name} to "
f"{jira_issue_url}."
)
return response
[docs]def fetch_test_set(test_set_xtp: str, args: argparse.Namespace) -> list:
"""Make sure a Jira test set with the parameter exists
:param test_set_xtp: The test set XTP ticket
:param args: The passed paramaters
:return: Details of the tests assciated with the test set, if any
"""
url = XTP_TESTSET_URL.format(test_set_xtp)
jira_issue_url = JIRA_ISSUE_URL.format(test_set_xtp)
response = None
if not args.dry_run:
if args.verbose:
print(f"Checking that test set {test_set_xtp} exists in {jira_issue_url}")
try:
response = requests.get(
url,
auth=HTTPBasicAuth(args.username, args.password),
headers=HEADERS,
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
if response.status_code == 404:
raise SystemExit(
f"No test set found in Jira with key {test_set_xtp}"
) from err
if response.status_code == 401:
raise SystemExit(
"Authentication failure, either the credentials are incorrect or "
"the Xray license has expired."
) from err
if response.status_code == 400:
raise SystemExit("Returns the error") from err
if response.status_code == 500:
raise SystemExit(
"An internal error occurred when generating the feature file(s)."
) from err
raise SystemExit(err) from err
response = response.json()
if args.verbose:
print(f"OK. Result: {response}")
else:
print(
f"[DRY RUN] Checking that test set {test_set_xtp} exists in "
f"{jira_issue_url}"
)
print(f"[DRY RUN] OK. Test set {test_set_xtp} found")
return response
[docs]def get_xtp_ticket_tag(
args: argparse.Namespace, feature_file: pytest_bdd.parser.Feature
) -> str:
"""Making use of the parsed feature file, get the test set XTP ticket
Also ensures that there is only one XTP ticket tag
:param args: The passed paramaters
:param feature_file: The parsed feature file
:return: The test set XTP ticket
"""
if args.verbose:
print(f"\nGetting the test set XTP ticket from {feature_file.filename}")
if args.dry_run:
print(
f"\n[DRY RUN] Getting the test set XTP ticket from {feature_file.filename}"
)
xtp_ticket_tag = None
tags = [tag for tag in feature_file.tags if tag.startswith("XTP")]
if len(tags) == 0:
if args.dry_run:
print(
"\n[DRY RUN] The feature file should have a tag with a XTP ticket "
"number. E.g:\n\n\t@XTP-1234\n"
"\tFeature: Some feature description\n\t...\n"
)
else:
print(
"\nThe feature file should have a tag with a XTP ticket number. "
"E.g:\n\n\t@XTP-1234\n\tFeature: Some feature description\n\t...\n"
)
sys.exit(1)
if len(tags) > 1:
if args.dry_run:
print("[DRY RUN] Only one XTP ticket tag is supported.")
else:
print("Only one XTP ticket tag is supported.")
sys.exit(1)
xtp_ticket_tag = tags[0]
if args.verbose:
print(f"OK. Test set XTP ticket is {xtp_ticket_tag}")
if args.dry_run:
print(f"[DRY RUN] OK. Test set XTP ticket is {xtp_ticket_tag}.")
return xtp_ticket_tag
[docs]def parse_feature_file(args: argparse.Namespace):
"""Parse the feature file
:param args: The passed paramaters
:return: The parsed feature file
"""
if args.verbose:
print(f"\nChecking feature file {args.feature_file.name}")
feature_file = None
try:
feature_file = pytest_bdd.feature.get_feature("", args.feature_file.name)
except pytest_bdd.exceptions.FeatureError as err:
print("\nThere is a problem with the feature file:\n")
print(err)
sys.exit(1)
if args.verbose:
print("OK")
return feature_file
[docs]def main():
"""Script entrypoint"""
parser = argparse.ArgumentParser(
description=(
"Uploads a cucumber test file to JIRA by way of the XRAY extension. Either "
"use username/password or set the environment variable JIRA_AUTH for "
"authentication."
)
)
parser.add_argument(
"-f",
"--feature-file",
type=argparse.FileType("r"),
required=True,
help="Path to the feature file.",
)
parser.add_argument("-u", "--username", type=str, help="JIRA account username")
parser.add_argument(
"-p",
"--password",
type=str,
default="",
help=(
"Password for specified user. If not specified you will be prompted for one"
),
)
parser.add_argument(
"-v",
"--verbose",
required=False,
action="store_true",
help="Verbose output",
)
parser.add_argument(
"--dry-run",
required=False,
action="store_true",
help="Run the script without uploading the XTP-*.feature file.",
)
args = parser.parse_args()
setattr(args, "jira_auth_token", os.environ.get("JIRA_AUTH", None))
if not args.jira_auth_token:
if not args.username:
print(
"A username is required when the environment variable JIRA_AUTH is not "
"set"
)
sys.exit(0)
if not args.password:
stdin_password = getpass.getpass()
setattr(args, "password", stdin_password)
feature_file = parse_feature_file(args)
test_set_xtp = get_xtp_ticket_tag(args, feature_file)
# If the test set has no associated tests,
# then the response will be an empty list
test_set = fetch_test_set(test_set_xtp, args)
test_set_associated_tests = None
if not args.dry_run:
test_set_associated_tests = [ticket["key"] for ticket in test_set]
new_test_result = upload_feature_file(test_set_xtp, args)
if new_test_result:
for test_xtp in new_test_result:
new_test_xtp = test_xtp["key"]
if new_test_xtp not in test_set_associated_tests:
associate_test_with_test_set(args, test_set_xtp, new_test_xtp)
if args.verbose:
print(f"Test {new_test_xtp} associated with {test_set_xtp}")
else:
if args.verbose:
print(
f"Test {new_test_xtp} has already been associated with test "
f"set {test_set_xtp}"
)
if __name__ == "__main__":
main()