|
1 | 1 | import re
|
2 |
| -from git import Repo |
3 |
| - |
4 | 2 | from optparse import make_option
|
5 |
| - |
6 | 3 | from xml.etree import cElementTree as ElementTree
|
7 | 4 |
|
8 | 5 | from django.core.management.base import BaseCommand
|
| 6 | +from git import Repo |
| 7 | +from jira import JIRA |
| 8 | +from jira.exceptions import JIRAError |
| 9 | + |
| 10 | + |
| 11 | +FAILURE_ROW_RE = re.compile(r'\s*File\s"(.*)",\sline\s(.*),.*') |
9 | 12 |
|
10 | 13 |
|
11 | 14 | class Command(BaseCommand):
|
12 | 15 |
|
13 | 16 | help = 'Creates JIRA issues for every failed case for specified branch'
|
14 |
| - result_file = 'nosetests.xml' |
15 |
| - failure_row_re = re.compile(r'\s*File\s"(.*)",\sline\s(.*),.*') |
16 | 17 |
|
17 | 18 | option_list = BaseCommand.option_list + (
|
18 | 19 | make_option(
|
19 |
| - '-b', |
20 | 20 | '--branches',
|
21 | 21 | action='store',
|
22 | 22 | dest='branches',
|
23 | 23 | help='Affected branches',
|
24 | 24 | ),
|
25 | 25 | make_option(
|
26 |
| - '-t', |
27 | 26 | '--target-branch',
|
28 | 27 | action='store',
|
29 | 28 | dest='target_branch',
|
30 | 29 | help='Target branch',
|
31 |
| - ) |
| 30 | + ), |
| 31 | + make_option( |
| 32 | + '--issue-type', |
| 33 | + action='store', |
| 34 | + dest='issue_type', |
| 35 | + default='Bug', |
| 36 | + help='Issue type', |
| 37 | + ), |
| 38 | + make_option( |
| 39 | + '--project-key', |
| 40 | + action='store', |
| 41 | + dest='project_key', |
| 42 | + default='EZHOME', |
| 43 | + help='Project key', |
| 44 | + ), |
| 45 | + make_option( |
| 46 | + '--jira-server', |
| 47 | + action='store', |
| 48 | + dest='jira_server', |
| 49 | + help='JIRA server', |
| 50 | + ), |
| 51 | + make_option( |
| 52 | + '--jira-username', |
| 53 | + action='store', |
| 54 | + dest='jira_username', |
| 55 | + help='Username for JIRA account', |
| 56 | + ), |
| 57 | + make_option( |
| 58 | + '--jira-password', |
| 59 | + action='store', |
| 60 | + dest='jira_password', |
| 61 | + help='Password for JIRA account', |
| 62 | + ), |
| 63 | + make_option( |
| 64 | + '--test-results', |
| 65 | + action='store', |
| 66 | + dest='test_results', |
| 67 | + default='nosetests.xml', |
| 68 | + help='Location of test results', |
| 69 | + ), |
32 | 70 | )
|
33 | 71 |
|
34 |
| - @staticmethod |
35 |
| - def parse_test_path(path): |
36 |
| - path, classname = path.rsplit('.', 1) |
37 |
| - path = path.replace('.', '/') |
38 |
| - return path, classname |
39 |
| - |
40 | 72 | def handle(self, *args, **options):
|
41 |
| - repo = Repo() |
| 73 | + self.issue_type = options['issue_type'] |
| 74 | + self.project_key = options['project_key'] |
| 75 | + self.jira_server = options['jira_server'] |
| 76 | + self.jira_username = options['jira_username'] |
| 77 | + self.jira_password = options['jira_password'] |
| 78 | + test_results = options['test_results'] |
| 79 | + |
| 80 | + self.repo = Repo() |
| 81 | + branches = [] |
42 | 82 | try:
|
43 |
| - branches = [repo.heads[x] for x in options['branches'].split(',')] |
44 |
| - target = repo.heads[options['target_branch']] |
45 |
| - except IndexError as e: |
46 |
| - return 'Cannot find branch with error "{0}"'.format(e) |
47 |
| - if target != repo.head.ref: |
48 |
| - return 'Current branch "{0}" does not match provided CircleCI branch "{1}"'.format( |
49 |
| - repo.head.ref, target |
| 83 | + for branch in options['branches'].split(','): |
| 84 | + branches.append(self.repo.heads[branch]) |
| 85 | + except IndexError: |
| 86 | + return 'Cannot find branch "{0}"'.format(branch) |
| 87 | + try: |
| 88 | + self.target_branch = self.repo.heads[options['target_branch']] |
| 89 | + except IndexError: |
| 90 | + return 'Cannot find branch "{0}"'.format(options['target_branch']) |
| 91 | + if self.target_branch != self.repo.head.ref: |
| 92 | + return ( |
| 93 | + 'Current branch "{0}" does not match ' |
| 94 | + 'provided CircleCI branch "{1}"' |
| 95 | + .format(self.repo.head.ref, self.target_branch) |
50 | 96 | )
|
51 |
| - elif target not in branches: |
52 |
| - return 'Skipping check for branch "{0}"'.format(repo.head.ref) |
| 97 | + elif self.target_branch not in branches: |
| 98 | + return 'Skipping check for branch "{0}"'.format(self.repo.head.ref) |
53 | 99 |
|
54 | 100 | try:
|
55 |
| - root = ElementTree.parse(self.result_file).getroot() |
| 101 | + root = ElementTree.parse(test_results).getroot() |
56 | 102 | if root.attrib['errors']:
|
| 103 | + results = [] |
57 | 104 | for testcase in root:
|
58 |
| - for failure in testcase: |
59 |
| - print('Failure for {}'. format(testcase.attrib['name'])) |
60 |
| - path, classname = self.parse_test_path(testcase.attrib['classname']) |
61 |
| - print path, classname |
62 |
| - for (file_path, line_number) in re.findall(self.failure_row_re, failure.text): |
63 |
| - if path in file_path: |
64 |
| - print repo.git.blame('HEAD', file_path) |
| 105 | + if testcase: |
| 106 | + results.append(self.handle_testcase(testcase)) |
| 107 | + return '\n'.join(results) |
65 | 108 | else:
|
66 |
| - return 'No errors' |
| 109 | + return 'No errors in tests' |
67 | 110 | except IOError:
|
68 |
| - return 'File "{0}" does not exist'.format(self.result_file) |
| 111 | + return 'File "{0}" does not exist'.format(test_results) |
| 112 | + |
| 113 | + @staticmethod |
| 114 | + def parse_test_path(path): |
| 115 | + path, classname = path.rsplit('.', 1) |
| 116 | + path = path.replace('.', '/') |
| 117 | + return path, classname |
| 118 | + |
| 119 | + def handle_testcase(self, testcase): |
| 120 | + path, classname = self.parse_test_path( |
| 121 | + testcase.attrib['classname'] |
| 122 | + ) |
| 123 | + for (file_path, line_number) in re.findall( |
| 124 | + FAILURE_ROW_RE, testcase[0].text |
| 125 | + ): |
| 126 | + if path in file_path: |
| 127 | + # Finding the line of testcase definition |
| 128 | + authors = {} |
| 129 | + commit, line = self.repo.blame( |
| 130 | + '-L/def {}/'.format(testcase.attrib['name']), file_path |
| 131 | + )[0] |
| 132 | + if commit.author not in authors: |
| 133 | + authors['function'] = commit.author |
| 134 | + # Finding the line of failure |
| 135 | + commit, line = self.repo.blame( |
| 136 | + '-L{0},{0}'.format(line_number), file_path |
| 137 | + )[0] |
| 138 | + if commit.author not in authors: |
| 139 | + authors['failure'] = commit.author |
| 140 | + return self.handle_jira( |
| 141 | + path=path, |
| 142 | + authors=authors, |
| 143 | + classname=classname, |
| 144 | + testcase=testcase, |
| 145 | + ) |
| 146 | + |
| 147 | + def handle_jira(self, path, authors, classname, testcase): |
| 148 | + try: |
| 149 | + jira = JIRA( |
| 150 | + server=self.jira_server, |
| 151 | + basic_auth=( |
| 152 | + self.jira_username, |
| 153 | + self.jira_password, |
| 154 | + ) |
| 155 | + ) |
| 156 | + summary = ( |
| 157 | + 'Fail: {path}:{classname}.{testcase}, ' |
| 158 | + 'branch: {branch}'.format( |
| 159 | + path=path, |
| 160 | + classname=classname, |
| 161 | + testcase=testcase.attrib['name'], |
| 162 | + branch=self.target_branch, |
| 163 | + ) |
| 164 | + ) |
| 165 | + open_issues = jira.search_issues( |
| 166 | + 'summary ~ "{summary}" AND ' |
| 167 | + 'resolution=unresolved'.format( |
| 168 | + summary=summary |
| 169 | + ), |
| 170 | + maxResults=1 |
| 171 | + ) |
| 172 | + if open_issues: |
| 173 | + # Update priority |
| 174 | + issue = open_issues[0] |
| 175 | + new_priority = '1' |
| 176 | + if int(issue.fields.priority.id) > 1: |
| 177 | + new_priority = str(int(issue.fields.priority.id) - 1) |
| 178 | + issue.update(priority={'id': new_priority}) |
| 179 | + return ( |
| 180 | + 'Priority of issue "{issue}" ' |
| 181 | + 'has been set to "{priority}"'.format( |
| 182 | + issue=issue, priority=jira.priority(new_priority) |
| 183 | + ) |
| 184 | + ) |
| 185 | + else: |
| 186 | + # Create issue |
| 187 | + assignee = jira.search_users( |
| 188 | + user=authors['function'].email, |
| 189 | + maxResults=1 |
| 190 | + ) |
| 191 | + issue_dict = dict( |
| 192 | + project={'key': self.project_key}, |
| 193 | + summary=summary, |
| 194 | + issuetype={'name': self.issue_type}, |
| 195 | + priority={'id': jira.priorities()[-1].id}, |
| 196 | + description='Description here', |
| 197 | + ) |
| 198 | + if assignee: |
| 199 | + issue_dict['assignee'] = {'name': assignee[0].name} |
| 200 | + new_issue = jira.create_issue(fields=issue_dict) |
| 201 | + return 'New issue "{0}" has been created'.format(new_issue) |
| 202 | + except JIRAError as e: |
| 203 | + return 'JIRA ERROR: {}'.format(e.text) |
0 commit comments