| 
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