diff --git a/release-tools/testing/.gitignore b/release-tools/testing/.gitignore new file mode 100644 index 0000000000..0d20b6487c --- /dev/null +++ b/release-tools/testing/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/release-tools/testing/README.md b/release-tools/testing/README.md new file mode 100644 index 0000000000..0a1fbf8551 --- /dev/null +++ b/release-tools/testing/README.md @@ -0,0 +1,83 @@ +# Release Tools - Test Tracker and Generator + +## Introduction + +This command-line tool lets the user create and track tests in the [R3T](https://r3-cev.atlassian.net/projects/R3T) JIRA project. All generic test cases are captured as tickets of type **Platform Test Template** with a label "OS" for tests pertaining to **Corda Open Source**, "ENT" for **Corda Enterprise**, and "NS" for **Corda Network Services**. These tickets can be set to **Active** or **Inactive** status based on their relevance for a particular release. + +The tool creates a set of new release tests by cloning the current set of active test templates into a set of **Platform Test** tickets. These will each get assigned to the appropriate target version. Further, the tool lets the user create sub-tasks for each of the release tests, one for each release candidate. These steps are described in more detail further down. + +## List Test Cases + +To list the active test cases for a product, run the following command: + +```bash +$ ./test-manager list-tests +``` + +Where `` is either `OS`, `ENT` or `NS`. This will list the test cases that are currently applicable to Corda Open Source, Corda Enterprise and Corda Network Services, respectively. + +## Show Test Status + +To show the status of all test runs for a specific release or release candidate, run: + +```bash +$ ./test-manager status +``` + +Here, `` represents the target version, e.g., product `OS` and version `3.3` would represent Corda Open Source 3.3. `` is optional and will narrow down the report to only show the provided candidate version, e.g., `1` for `RC01`. + +## Create JIRA Version + +To create a new release version in JIRA, you can run the following command: + +```bash +$ ./test-manager create-version +``` + +Note that `` is optional. This command will create new versions in the following JIRA projects: `CORDA`, `ENT`, `ENM`, `CID` and `R3T`. + +## Create Release Tests + +To create the set of parent tests for a new release, you can run: + +```bash +$ ./test-manager create-release-tests +``` + +This will create the test cases, but none of the test run tickets for respective release candidates. Note also that "blocks"-links between active test templates will be carried across to the created test tickets. + +## Create Release Candidate Tests + +To create a set of test run tickets for a new release candidate, you can run: + +```bash +$ ./test-manager create-release-candidate-tests +``` + +This will create a new sub-task under each of the test tickets for `` ``, for release candidate ``. + +## Options + +Each command described above has a set of additional options. More specifically, if you want to use a particular JIRA user instead of being prompted for a user name every time, you can specify `--user `. For verbose logging, you can supply `--verbose` or `-v`. And to auto-reply to the prompt of whether to proceed or not, provide `--yes` or `-y`. + +There is also a useful dry-run option, `--dry-run` or `-d`, that lets you run through the command without creating any tickets or applying any changes to JIRA. + +## Example + +As an example, say you want to create test cases for Corda Network Services 1.0 RC01. You would then follow the following steps: + +```bash +$ ./test-manager create-version NS 1.0 # Create "Corda Network Services 1.0" - if it doesn't exist +$ ./test-manager create-version NS 1.0 1 # Create "Corda Network Services 1.0 RC01" - if it doesn't exist +$ ./test-manager create-release-tests NS 1.0 # Create test cases +$ ./test-manager create-release-candidate-tests NS 1.0 1 # Create test run for release candidate +``` + +Later, when it's time to test RC02, you simply run the following: + +```bash +$ ./test-manager create-version NS 1.0 2 +$ ./test-manager create-release-candidate-tests NS 1.0 2 +``` + +That's it. Voila, you've got yourself a whole new set of JIRA tickets :-) diff --git a/release-tools/testing/args.py b/release-tools/testing/args.py new file mode 100644 index 0000000000..9eb92a8a59 --- /dev/null +++ b/release-tools/testing/args.py @@ -0,0 +1,71 @@ +from __future__ import print_function +from argparse import Action, ArgumentParser +import sys, traceback + +# {{{ Representation of a command-line program +class Program: + + # Create a new command-line program represenation, provided an optional name and description + def __init__(self, name=None, description=None): + self.parser = ArgumentParser(name, description=description) + self.subparsers = self.parser.add_subparsers(title='commands') + self.arguments = [] + + # Enter program definition block + def __enter__(self): + return self + + # Add argument to the top-level command-line interface and all registered sub-commands + def add(self, name, *args, **kwargs): + self.parser.add_argument(name, *args, **kwargs) + self.arguments.append(([name] + list(args), kwargs)) + + # Add sub-command to the set of command-line options + def command(self, name, description, handler): + return Command(self, self.subparsers, name, description, handler) + + # Parse arguments from the command line, and run the associated command handler + def __exit__(self, type, value, tb): + args = self.parser.parse_args() + try: + if 'func' in args: + args.func(args) + else: + self.parser.print_help() + except KeyboardInterrupt: + print() + except Exception as error: + if args.verbose: + t, exception, tb = sys.exc_info() + self.parser.error('{}\n\n{}'.format(error.message, '\n'.join(traceback.format_tb(tb)))) + else: + self.parser.error(error.message) +# }}} + +# {{{ Representation of a sub-command of a command-line program +class Command: + + # Create a sub-command, provided a name, description and command handler + def __init__(self, program, subparsers, name, description, handler): + self.program = program + self.subparsers = subparsers + self.name = name + self.description = description + self.handler = handler + + # Enter sub-command definition block + def __enter__(self): + self.parser = self.subparsers.add_parser(self.name, description=self.description) + return self + + # Add argument to the CLI command + def add(self, name, *args, **kwargs): + self.parser.add_argument(name, *args, **kwargs) + + # Exit sub-command definition block and register default handler + def __exit__(self, type, value, traceback): + for (args, kwargs) in self.program.arguments: + self.parser.add_argument(*args, **kwargs) + self.parser.set_defaults(func=self.handler) + +# }}} diff --git a/release-tools/testing/jira_manager.py b/release-tools/testing/jira_manager.py new file mode 100644 index 0000000000..af57865398 --- /dev/null +++ b/release-tools/testing/jira_manager.py @@ -0,0 +1,187 @@ +# {{{ JIRA dependency +from jira import JIRA +from jira.exceptions import JIRAError +# }}} + +# {{{ Class for interacting with a hosted JIRA system +class Jira: + + # {{{ Constants + BLOCKS = 'Blocks' + DUPLICATE = 'Duplicate' + RELATES = 'Relates' + # }}} + + # {{{ init(address) - Initialise JIRA class, pointing it to the JIRA endpoint + def __init__(self, address='https://r3-cev.atlassian.net'): + self.address = address + self.jira = None + self.mock_key = 1 + self.custom_fields_by_name, self.custom_fields_by_key = {}, {} + # }}} + + # {{{ login(user, password) - Log in as a specific JIRA user + def login(self, user, password): + try: + self.jira = JIRA(self.address, auth=(user, password)) + for x in self.jira.fields(): + if x['custom']: + self.custom_fields_by_name[x['name']] = x['key'] + self.custom_fields_by_key[x['key']] = x['name'] + return self + except Exception as error: + message = error.message + if isinstance(error, JIRAError): + message = error.text if error.text and len(error.text) > 0 and not error.text.startswith(' 0 else query + while count == max_count: + try: + issues = self.jira.search_issues(query, maxResults=max_count, startAt=offset) + count = len(issues) + offset += count + for issue in issues: + index += 1 + yield Issue(self, index=index, issue=issue) + except JIRAError as error: + raise Exception('failed to run query "{}": {}'.format(query, error.text)) + # }}} + + # {{{ find(key) - Look up issue by key + def find(self, key): + try: + issue = self.jira.issue(key) + return Issue(self, issue=issue) + except JIRAError as error: + raise Exception('failed to look up issue "{}": {}'.format(key, error.text)) + # }}} + + # {{{ create(fields, dry_run) - Create a new issue + def create(self, fields, dry_run=False): + if dry_run: + return Issue(self, fields=fields) + try: + issue = self.jira.create_issue(fields) + return Issue(self, issue=issue) + except JIRAError as error: + raise Exception('failed to create issue: {}'.format(error.text)) + # }}} + + # {{{ link(issue_key, other_issue_key, relationship, dry_run) - Link one issue to another + def link(self, issue_key, other_issue_key, relationship=RELATES, dry_run=False): + if dry_run: + return + try: + self.jira.create_issue_link( + type=relationship, + inwardIssue=issue_key, + outwardIssue=other_issue_key, + comment={ + 'body': 'Linked {} to {}'.format(issue_key, other_issue_key), + } + ) + except JIRAError as error: + raise Exception('failed to link {} and {}: {}'.format(issue_key, other_issue_key, error.text)) + # }}} + +# }}} + +# {{{ Representation of a JIRA issue +class Issue: + + mock_index = 1 + + # {{{ init(jira, index, issue, key, fields) - Instantiate an abstract representation of an issue + def __init__(self, jira, index=0, issue=None, key=None, fields=None): + self._jira = jira + self._index = index + self._issue = issue + self._fields = fields + self._key = key if key else u'DRY-{:03d}'.format(Issue.mock_index) + if not key and not issue: + Issue.mock_index += 1 + # }}} + + # {{{ getattr(key) - Get attribute from issue + def __getattr__(self, key): + if key == 'index': + return self._index + if self._issue: + value = self._issue.__getattr__(key) + return WrappedDictionary(value) if key == 'fields' else value + elif self._fields: + if key == 'key': + return self._key + elif key == 'fields': + return WrappedDictionary(self._fields) + return None + # }}} + + # {{{ getitem(key) - Get item from issue + def __getitem__(self, key): + return self.__getattr__(key) + # }}} + + # {{{ str() - Get a string representation of the issue + def __str__(self): + summary = self.fields.summary.strip() + labels = self.fields.labels + if len(labels) > 0: + return u'[{}] {} ({})'.format(self.key, summary, ', '.join(labels)) + else: + return u'[{}] {}'.format(self.key, summary) + # }}} + + # {{{ clone(..., dry_run) - Create a clone of the issue, resetting any provided fields) + def clone(self, **kwargs): + dry_run = kwargs['dry_run'] if 'dry_run' in kwargs else False + fields = self.fields.to_dict() + whitelisted_fields = [ + 'project', 'summary', 'description', 'issuetype', 'labels', 'parent', 'priority', + self._jira.custom_fields_by_name['Target Version/s'], + ] + if 'parent' not in kwargs: + whitelisted_fields += [ + self._jira.custom_fields_by_name['Epic Link'], + self._jira.custom_fields_by_name['Preconditions'], + self._jira.custom_fields_by_name['Test Steps'], + self._jira.custom_fields_by_name['Acceptance Criteria'], + self._jira.custom_fields_by_name['Primary Test Environment'], + ] + for key in kwargs: + value = kwargs[key] + if key == 'parent' and type(value) in (str, unicode): + fields[key] = { 'key' : value } + elif key == 'version': + fields[self._jira.custom_fields_by_name['Target Version/s']] = [{ 'name' : value }] + else: + fields[key] = value + for key in [key for key in fields if key not in whitelisted_fields]: + del fields[key] + new_issue = self._jira.create(fields, dry_run=dry_run) + return new_issue + # }}} + +# }}} + +# {{{ Dictionary with attribute getters +class WrappedDictionary: + def __init__(self, dictionary): + self.dictionary = dictionary + + def __getattr__(self, key): + return self.__getitem__(key) + + def __getitem__(self, key): + return self.dictionary[key] + + def to_dict(self): + return dict(self.dictionary) + +# }}} diff --git a/release-tools/testing/login_manager.py b/release-tools/testing/login_manager.py new file mode 100644 index 0000000000..08a4e1412a --- /dev/null +++ b/release-tools/testing/login_manager.py @@ -0,0 +1,60 @@ +# {{{ Dependencies + +from __future__ import print_function +import sys + +try: + from getpass import getpass +except: + def getpass(message): return raw_input(message) + +try: + from keyring import get_password, set_password +except: + def get_password(account, user): return None + def set_password(account, user, password): pass + +# Python 2.x fix; raw_input was renamed to input in Python 3 +try: input = raw_input +except NameError: pass + +# }}} + +# {{{ prompt(message, secret) - Get input from user; if secret is true, hide the input +def prompt(message, secret=False): + try: + return getpass(message) if secret else input(message) + except: + print() + return '' +# }}} + +# {{{ confirm(message, auto_yes) - Request confirmation from user and proceed if the response is 'yes' +def confirm(message, auto_yes=False): + if auto_yes: + print(message.replace('?', '.')) + return + if not prompt(u'{} (y/N) '.format(message)).lower().strip().startswith('y'): + sys.exit(1) +# }}} + +# {{{ login(account, user, password, use_keyring) - Present user with login prompt and return the provided username and password. If use_keyring is true, use previously provided password (if any) +def login(account, user=None, password=None, use_keyring=True): + if not user: + user = prompt('Username: ') + user = u'{}@r3.com'.format(user) if '@' not in user else user + if not user: return (None, None) + else: + user = u'{}@r3.com'.format(user) if '@' not in user else user + print('Username: {}'.format(user)) + password = get_password(account, user) if password is None and use_keyring else password + if not password: + password = prompt('Password: ', secret=True) + if not password: return (None, None) + else: + print('Password: ********') + if use_keyring: + set_password(account, user, password) + print() + return (user, password) +# }}} diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt new file mode 100644 index 0000000000..b4bc32760d --- /dev/null +++ b/release-tools/testing/requirements.txt @@ -0,0 +1,3 @@ +jira==2.0.0 +keyring==13.1.0 +termcolor==1.1.0 diff --git a/release-tools/testing/test-manager b/release-tools/testing/test-manager new file mode 100755 index 0000000000..96bfd97a36 --- /dev/null +++ b/release-tools/testing/test-manager @@ -0,0 +1,312 @@ +#!/usr/bin/env python + +# {{{ Dependencies +from __future__ import print_function +import sys +from args import Program +from login_manager import login, confirm +from jira_manager import Jira +# }}} + +# {{{ Dependencies for printing coloured content to the console +try: + from termcolor import colored + def blue(message): return colored(message, 'blue') + def green(message): return colored(message, 'green') + def red(message): return colored(message, 'red') + def yellow(message): return colored(message, 'yellow') + def faint(message): return colored(message, 'white', attrs=['dark']) + def on_green(message): return colored(message, 'white', 'on_green') + def on_red(message): return colored(message, 'white', 'on_red') + def blue_on_white(message): return colored(message, 'blue', 'on_white') + def yellow_on_white(message): return colored(message, 'yellow', 'on_white') +except: + def blue(message): return u'[{}]'.format(message) + def green(message): return message + def red(message): return message + def yellow(message): return message + def faint(message): return message + def on_green(message): return message + def on_red(message): return message + def blue_on_white(message): return message + def yellow_on_white(message): return message +# }}} + +# {{{ Mapping from product code to product name +product_map = { + 'OS' : 'Corda', + 'ENT' : 'Corda Enterprise', + 'NS' : 'Corda Network Services', + 'TEST' : 'Corda', # for demo and test purposes +} +# }}} + +# {{{ JIRA queries +QUERY_LIST_TEST_CASES = \ + u'project = R3T AND type = "Platform Test Template" AND status = Active AND labels = "{}" ORDER BY key' +QUERY_LIST_TEST_INSTANCES = \ + u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" ORDER BY key' +QUERY_LIST_TEST_INSTANCE_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test" AND labels = "{}" AND "Target Version/s" = "{}" AND issue IN linkedIssues({})' +QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND parent = {} ORDER BY "Target Version/S"' +QUERY_LIST_TEST_RUN_FOR_TICKET = \ + u'project = R3T AND type = "Platform Test Run" AND labels = "{}" AND "Target Version/s" = "{}" AND parent = {}' +QUERY_LIST_BLOCKING_TEST_CASES = \ + u'project = R3T AND type = "Platform Test Template" AND labels = "{}" AND issue IN linkedIssues({}, "Blocks")' +# }}} + +# {{{ list_test_cases() - List active test cases for a specific product +def list_test_cases(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + print(u'List of active test cases for {}:'.format(yellow(product_map[args.PRODUCT]))) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + has_tests = True + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ show_status() - Show the status of all test runs for a specific release or release candidate +def show_status(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + candidate = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version + if args.CANDIDATE: + print(u'Status of test runs for {} version {} release candidate {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(args.CANDIDATE)))) + else: + print(u'Status of test runs for {} version {}:'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION))) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version): + status = issue.fields.status['name'].lower() + if status == 'pass': + status = on_green('Pass') + elif status == 'fail': + status = on_red('Fail') + elif status == 'descope': + status = on_green('Descoped') + else: + status = '' + print(u' - {} {} {}'.format(blue(issue.key), issue.fields.summary, status)) + has_test_runs = False + if args.CANDIDATE: + if args.verbose: + print(faint(' [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + run_list = jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET, args.PRODUCT, candidate, issue.key) + else: + if args.verbose: + print(faint(' [{}]'.format(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET.format(args.PRODUCT, issue.key)))) + run_list = jira.search(QUERY_LIST_ALL_TEST_RUNS_FOR_TICKET, args.PRODUCT, issue.key) + for run in run_list: + has_test_runs = True + print() + status = run.fields.status['name'].lower() + if status == 'pass': + status = on_green('Pass ') + elif status == 'fail': + status = on_red('Fail ') + elif status == 'descope': + status = on_green('Descoped') + elif status == 'in progress': + status = yellow_on_white('Active ') + else: + status = blue_on_white('Open ') + print(u' {} {} ({})'.format(status, faint(run.fields[jira.custom_fields_by_name['Target Version/s']][0]['name']), blue(run.key))) + if not has_test_runs: + print() + print(u' - No release candidate tests found') + print() + has_tests = True + if not has_tests: + print(u' - No test cases found for the specified release') + print() +# }}} + +# {{{ create_version() - Create a new JIRA version +def create_version(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + version = '{} RC{:02d}'.format(version, args.CANDIDATE) if args.CANDIDATE else version + confirm(u'Create new version {}?'.format(yellow(version)), auto_yes=args.yes or args.dry_run) + print() + if not args.dry_run: + for project in ['CORDA', 'ENT', 'ENM', 'R3T', 'CID']: + print(u' - Creating version {} for project {} ...'.format(yellow(version), blue(project))) + try: + jira.jira.create_version(name=version, project=project, description=version) + print(u' {} - Created version for project {}'.format(green('SUCCESS'), blue(project))) + except Exception as error: + print(u' {} - Failed to version: {}'.format(red('FAIL'), error)) + print() + + +# }}} + +# {{{ create_release() - Create test cases for a specific version of a product +def create_release(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + confirm(u'Create test cases for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION)), auto_yes=args.yes or args.dry_run) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_CASES.format(args.PRODUCT)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + print() + has_tests = True + print(u' - Creating test case for version {} ...'.format(yellow(args.VERSION))) + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key)))) + has_test_case_for_version = len(list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key)))) + if has_test_case_for_version: + print(u' {} - Test case for version already exists'.format(yellow('SKIPPED'))) + else: + try: + test_case = issue.clone(issuetype='Platform Test', version=version, dry_run=args.dry_run) + print(u' {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to create ticket: {}'.format(red('FAIL'), error)) + print() + print(u' - Linking test case to template ...') + try: + jira.link(issue.key, test_case.key, dry_run=args.dry_run) + print(u' {} - Linked {} to {}'.format(green('SUCCESS'), blue(issue.key), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to link tickets: {}'.format(red('FAIL'), error)) + print() + print(u'Copying links from test templates for {} version {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION))) + print() + for issue in jira.search(QUERY_LIST_TEST_CASES, args.PRODUCT): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + print() + print(u' - Copying links for test case {} ...'.format(blue(issue.key))) + has_links = False + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_BLOCKING_TEST_CASES.format(args.PRODUCT, issue.key)))) + for blocking_issue in jira.search(QUERY_LIST_BLOCKING_TEST_CASES, args.PRODUCT, issue.key): + from_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, issue.key))) + to_ticket = list(jira.search(QUERY_LIST_TEST_INSTANCE_FOR_TICKET.format(args.PRODUCT, version, blocking_issue.key))) + if len(from_ticket) == 0 or len(to_ticket) == 0: + continue + has_links = True + from_key = from_ticket[0].key + to_key = to_ticket[0].key + try: + jira.link(from_key, to_key, Jira.BLOCKS, dry_run=args.dry_run) + print(u' {} - Linked {} to {}'.format(green('SUCCESS'), blue(from_key), blue(to_key))) + except Exception as error: + print(u' {} - Failed to link tickets {} and {}: {}'.format(red('FAIL'), blue(from_key), blue(to_key), error)) + if not has_links: + print(u' {} - No relevant links found for ticket {}'.format(yellow('SKIPPED'), blue(issue.key))) + print() + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ create_release_candidate() - Create test run tickets for a specific release candidate of a product +def create_release_candidate(args): + user, password = login('jira', args.user) + if not user or not password: sys.exit(1) + jira = Jira().login(user, password) + version = '{} {}'.format(product_map[args.PRODUCT], args.VERSION).replace('.0', '') + CANDIDATE = args.CANDIDATE[0] + candidate = '{} RC{:02d}'.format(version, CANDIDATE) + confirm(u'Create test run tickets for {} version {} release candidate {}?'.format(yellow(product_map[args.PRODUCT]), yellow(args.VERSION), yellow('RC{:02d}'.format(CANDIDATE))), auto_yes=args.yes or args.dry_run) + if args.verbose: + print(faint('[{}]'.format(QUERY_LIST_TEST_INSTANCES.format(args.PRODUCT, version)))) + print() + has_tests = False + for issue in jira.search(QUERY_LIST_TEST_INSTANCES, args.PRODUCT, version): + print(u' - {} {}'.format(blue(issue.key), issue.fields.summary)) + epic_field = jira.custom_fields_by_name['Epic Link'] + epic = issue.fields[epic_field] if epic_field in issue.fields.to_dict() else '' + labels = issue.fields.labels + [epic] + print() + has_tests = True + print(u' - Creating test run ticket for release candidate {} ...'.format(yellow('RC{:02d}'.format(CANDIDATE)))) + if args.verbose: + print(faint(u' [{}]'.format(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + has_test_instance_for_version = len(list(jira.search(QUERY_LIST_TEST_RUN_FOR_TICKET.format(args.PRODUCT, candidate, issue.key)))) + if has_test_instance_for_version: + print(u' {} - Ticket for release candidate already exists'.format(yellow('SKIPPED'))) + else: + try: + test_case = issue.clone(issuetype='Platform Test Run', version=candidate, parent=issue.key, labels=labels, dry_run=args.dry_run) + print(u' {} - Created ticket {}'.format(green('SUCCESS'), blue(test_case.key))) + except Exception as error: + print(u' {} - Failed to create ticket: {}'.format(red('FAIL'), error)) + print() + if not has_tests: + print(u' - No active test cases found') + print() +# }}} + +# {{{ main() - Entry point +def main(): + with Program(description='tool for managing test cases and test runs in JIRA') as program: + + PRODUCTS = ['OS', 'ENT', 'NS', 'TEST'] + + program.add('--verbose', '-v', help='turn on verbose logging', action='store_true') + program.add('--yes', '-y', help='automatically answer "yes" to all prompts', action='store_true') + program.add('--user', '-u', help='the user name or email address used to log in to JIRA', type=str, metavar='USER') + program.add('--no-keyring', help='do not retrieve passwords persisted in the keyring', action='store_true') + + def mixin_dry_run(command): + command.add('--dry-run', '-d', help='run action without applying any changes to JIRA', action='store_true') + + def mixin_product(command): + command.add('PRODUCT', help='the product under test (OS, ENT, NS)', choices=PRODUCTS, metavar='PRODUCT') + + def mixin_version_and_product(command): + mixin_product(command) + command.add('VERSION', help='the target version of the release, e.g., 4.0', type=float) + + def mixin_candidate(command, optional=False): + if optional: + nargs = '?' + else: + nargs = 1 + command.add('CANDIDATE', help='the number of the release candidate, e.g., 1 for RC01', type=int, nargs=nargs) + + with program.command('list-tests', 'list test cases applicable to the provided specification', list_test_cases) as command: + mixin_product(command) + + with program.command('status', 'show the status of all test runs for a specific release or release candidate', show_status) as command: + mixin_version_and_product(command) + mixin_candidate(command, True) + + with program.command('create-version', 'create a new version in JIRA', create_version) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + mixin_candidate(command, True) + + with program.command('create-release-tests', 'create test cases for a new release in JIRA', create_release) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + + with program.command('create-release-candidate-tests', 'create test runs for a new release candidate in JIRA', create_release_candidate) as command: + mixin_dry_run(command) + mixin_version_and_product(command) + mixin_candidate(command) +# }}} + +if __name__ == '__main__': main()