Skip to content

Commit 36a602d

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

File tree

4 files changed

+174
-41
lines changed

4 files changed

+174
-41
lines changed

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ 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_USERNAME ?= jira_bot
43+
JIRA_PASSWORD ?= P@ssw0rd
3844

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

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

circle.yml

Lines changed: 2 additions & 3 deletions
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

Lines changed: 2 additions & 2 deletions
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
Lines changed: 162 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,196 @@
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-username',
47+
action='store',
48+
dest='jira_username',
49+
help='Username for JIRA account',
50+
),
51+
make_option(
52+
'--jira-password',
53+
action='store',
54+
dest='jira_password',
55+
help='Password for JIRA account',
56+
),
57+
make_option(
58+
'--test-results',
59+
action='store',
60+
dest='test_results',
61+
default='nosetests.xml',
62+
help='Location of test results',
63+
),
3264
)
3365

34-
@staticmethod
35-
def parse_test_path(path):
36-
path, classname = path.rsplit('.', 1)
37-
path = path.replace('.', '/')
38-
return path, classname
39-
4066
def handle(self, *args, **options):
41-
repo = Repo()
67+
self.issue_type = options['issue_type']
68+
self.project_key = options['project_key']
69+
self.jira_username = options['jira_username']
70+
self.jira_password = options['jira_password']
71+
test_results = options['test_results']
72+
73+
self.repo = Repo()
74+
branches = []
75+
try:
76+
for branch in options['branches'].split(','):
77+
branches.append(self.repo.heads[branch])
78+
except IndexError:
79+
return 'Cannot find branch "{0}"'.format(branch)
4280
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
81+
self.target_branch = self.repo.heads[options['target_branch']]
82+
except IndexError:
83+
return 'Cannot find branch "{0}"'.format(options['target_branch'])
84+
if self.target_branch != self.repo.head.ref:
85+
return (
86+
'Current branch "{0}" does not match '
87+
'provided CircleCI branch "{1}"'
88+
.format(self.repo.head.ref, self.target_branch)
5089
)
51-
elif target not in branches:
52-
return 'Skipping check for branch "{0}"'.format(repo.head.ref)
90+
elif self.target_branch not in branches:
91+
return 'Skipping check for branch "{0}"'.format(self.repo.head.ref)
5392

5493
try:
55-
root = ElementTree.parse(self.result_file).getroot()
94+
root = ElementTree.parse(test_results).getroot()
5695
if root.attrib['errors']:
96+
results = []
5797
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)
98+
if testcase:
99+
results.append(self.handle_testcase(testcase))
100+
return '\n'.join(results)
65101
else:
66-
return 'No errors'
102+
return 'No errors in tests'
67103
except IOError:
68-
return 'File "{0}" does not exist'.format(self.result_file)
104+
return 'File "{0}" does not exist'.format(test_results)
105+
106+
@staticmethod
107+
def parse_test_path(path):
108+
path, classname = path.rsplit('.', 1)
109+
path = path.replace('.', '/')
110+
return path, classname
111+
112+
def handle_testcase(self, testcase):
113+
path, classname = self.parse_test_path(
114+
testcase.attrib['classname']
115+
)
116+
for (file_path, line_number) in re.findall(
117+
FAILURE_ROW_RE, testcase[0].text
118+
):
119+
if path in file_path:
120+
# Finding the line of testcase definition
121+
authors = {}
122+
commit, line = self.repo.blame(
123+
'-L/def {}/'.format(testcase.attrib['name']), file_path
124+
)[0]
125+
if commit.author not in authors:
126+
authors['function'] = commit.author
127+
# Finding the line of failure
128+
commit, line = self.repo.blame(
129+
'-L{0},{0}'.format(line_number), file_path
130+
)[0]
131+
if commit.author not in authors:
132+
authors['failure'] = commit.author
133+
return self.handle_jira(
134+
path=path,
135+
authors=authors,
136+
classname=classname,
137+
testcase=testcase,
138+
)
139+
140+
def handle_jira(self, path, authors, classname, testcase):
141+
try:
142+
jira = JIRA(
143+
server='https://ezhome-test.atlassian.net',
144+
basic_auth=(
145+
self.jira_username,
146+
self.jira_password,
147+
)
148+
)
149+
summary = (
150+
'Fail: {path}:{classname}.{testcase}, '
151+
'branch: {branch}'.format(
152+
path=path,
153+
classname=classname,
154+
testcase=testcase.attrib['name'],
155+
branch=self.target_branch,
156+
)
157+
)
158+
open_issues = jira.search_issues(
159+
'summary ~ "{summary}" AND '
160+
'resolution=unresolved'.format(
161+
summary=summary
162+
),
163+
maxResults=1
164+
)
165+
if open_issues:
166+
# Update priority
167+
issue = open_issues[0]
168+
new_priority = '1'
169+
if int(issue.fields.priority.id) > 1:
170+
new_priority = str(int(issue.fields.priority.id) - 1)
171+
issue.update(priority={'id': new_priority})
172+
return (
173+
'Priority of issue "{issue}" '
174+
'has been set to "{priority}"'.format(
175+
issue=issue, priority=jira.priority(new_priority)
176+
)
177+
)
178+
else:
179+
# Create issue
180+
assignee = jira.search_users(
181+
user=authors['function'].email,
182+
maxResults=1
183+
)
184+
issue_dict = dict(
185+
project={'key': self.project_key},
186+
summary=summary,
187+
issuetype={'name': self.issue_type},
188+
priority={'id': jira.priorities()[-1].id},
189+
description='Description here',
190+
)
191+
if assignee:
192+
issue_dict['assignee'] = {'name': assignee[0].name}
193+
new_issue = jira.create_issue(fields=issue_dict)
194+
return 'New issue "{0}" has been created'.format(new_issue)
195+
except JIRAError as e:
196+
return 'JIRA ERROR: {}'.format(e.text)

0 commit comments

Comments
 (0)