Skip to content

Commit 7a6d4b4

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

File tree

4 files changed

+168
-38
lines changed

4 files changed

+168
-38
lines changed

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ DJANGO_SHELL ?= shell_plus
3636
REPORT_FAILED_TEST ?= report_failed_tests
3737
REPORT_FAILED_TEST_BRANCHES ?= develop,master,test1
3838

39+
# JIRA Settings
40+
JIRA_ISSUE_TYPE ?= Bug
41+
JIRA_PROJECT_KEY ?= EZHOME
42+
JIRA_USERNAME ?= jira_bot
43+
JIRA_PASSWORD ?= P@ssw0rd
44+
3945
# Setup bootstrapper & Gunicorn args
4046
has_bootstrapper = $(shell python -m bootstrapper --version 2>&1 | grep -v "No module")
4147
ifeq ($(LEVEL),development)
@@ -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: 157 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,193 @@
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()
4274
try:
43-
branches = [repo.heads[x] for x in options['branches'].split(',')]
44-
target = repo.heads[options['target_branch']]
75+
branches = [
76+
self.repo.heads[x] for x in options['branches'].split(',')
77+
]
78+
self.target_branch = self.repo.heads[options['target_branch']]
4579
except IndexError as e:
4680
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+
if self.target_branch != self.repo.head.ref:
82+
return (
83+
'Current branch "{0}" does not match '
84+
'provided CircleCI branch "{1}"'
85+
.format(self.repo.head.ref, self.target_branch)
5086
)
51-
elif target not in branches:
52-
return 'Skipping check for branch "{0}"'.format(repo.head.ref)
87+
elif self.target_branch not in branches:
88+
return 'Skipping check for branch "{0}"'.format(self.repo.head.ref)
5389

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

0 commit comments

Comments
 (0)