Skip to content

Commit d16846c

Browse files
committed
Finishing initial implementation of JIRA integration
1 parent a5f0da6 commit d16846c

File tree

4 files changed

+182
-41
lines changed

4 files changed

+182
-41
lines changed

Makefile

+9-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ SERVER_PORT ?= 8008
3434
DJANGO_SERVER ?= runserver
3535
DJANGO_SHELL ?= shell_plus
3636
REPORT_FAILED_TEST ?= report_failed_tests
37-
REPORT_FAILED_TEST_BRANCHES ?= develop,master,test1
37+
REPORT_FAILED_TEST_BRANCHES ?= develop,master
38+
39+
# JIRA Settings
40+
JIRA_ISSUE_TYPE ?= Bug
41+
JIRA_PROJECT_KEY ?= EZHOME
42+
JIRA_SERVER ?= https://ezhome-test.atlassian.net
43+
JIRA_USERNAME ?= jira_bot
44+
JIRA_PASSWORD ?= P@ssw0rd
3845

3946
# Setup bootstrapper & Gunicorn args
4047
has_bootstrapper = $(shell python -m bootstrapper --version 2>&1 | grep -v "No module")
@@ -120,4 +127,4 @@ server: clean pep8
120127

121128
# Reporting of failed cases:
122129
report_failed_tests:
123-
COMMAND="$(REPORT_FAILED_TEST) --branches $(REPORT_FAILED_TEST_BRANCHES) $(COMMAND_ARGS)" $(MAKE) manage
130+
COMMAND="$(REPORT_FAILED_TEST) --branches $(REPORT_FAILED_TEST_BRANCHES) --issue-type $(JIRA_ISSUE_TYPE) --project-key $(JIRA_PROJECT_KEY) --jira-server $(JIRA_SERVER) --jira-username $(JIRA_USERNAME) --jira-password $(JIRA_PASSWORD) $(COMMAND_ARGS)" $(MAKE) manage

circle.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ dependencies:
2525

2626
test:
2727
override:
28-
- TEST_ARGS='--with-xunit --with-json --json-file="./nosetests.json"' make lint test
28+
- TEST_ARGS='--with-xunit' make lint test
2929

3030
post:
3131
# - coveralls
3232
- mkdir -p $CIRCLE_TEST_REPORTS/junit/
3333
- "[ -r nosetests.xml ] && mv nosetests.xml $CIRCLE_TEST_REPORTS/junit/ || :"
34-
- "[ -r nosetests.json ] && mv nosetests.json $CIRCLE_TEST_REPORTS/junit/ || :"
35-
- COMMAND_ARGS='--target-branch develop' make report_failed_tests
34+
- COMMAND_ARGS='--target-branch ${CIRCLE_BRANCH} --test-results ${CIRCLE_TEST_REPORTS}/junit/nosetests.xml' make report_failed_tests
3635

3736
# # Override /etc/hosts
3837
# hosts:

requirements-dev.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ django-nose==1.4.2
44
flake8==2.3.0
55
flake8-import-order==0.5.3
66
flake8-pep257==1.0.3
7-
GitPython==1.0.2
87
https://github.com/zheller/flake8-quotes/tarball/aef86c4f8388e790332757e5921047ad53160a75#egg=flake8-quotes
8+
GitPython==1.0.2 # For Jira integration
9+
jira==1.0.3 # For Jira integreation
910
nose==1.3.7
10-
nose-json==0.2.4
1111
pep257==0.6.0
1212
pep8==1.6.2
1313
pep8-naming==0.3.3
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,203 @@
11
import re
2-
from git import Repo
3-
42
from optparse import make_option
5-
63
from xml.etree import cElementTree as ElementTree
74

85
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(.*),.*')
912

1013

1114
class Command(BaseCommand):
1215

1316
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(.*),.*')
1617

1718
option_list = BaseCommand.option_list + (
1819
make_option(
19-
'-b',
2020
'--branches',
2121
action='store',
2222
dest='branches',
2323
help='Affected branches',
2424
),
2525
make_option(
26-
'-t',
2726
'--target-branch',
2827
action='store',
2928
dest='target_branch',
3029
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+
),
3270
)
3371

34-
@staticmethod
35-
def parse_test_path(path):
36-
path, classname = path.rsplit('.', 1)
37-
path = path.replace('.', '/')
38-
return path, classname
39-
4072
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 = []
4282
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)
5096
)
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)
5399

54100
try:
55-
root = ElementTree.parse(self.result_file).getroot()
101+
root = ElementTree.parse(test_results).getroot()
56102
if root.attrib['errors']:
103+
results = []
57104
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)
65108
else:
66-
return 'No errors'
109+
return 'No errors in tests'
67110
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

Comments
 (0)