diff --git a/.gitignore b/.gitignore
index b809fa0..8207902 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,7 @@ staticfiles/
# npm
npm-debug.log
+# nose
+nosetests.json
+nosetests.xml
+
diff --git a/Makefile b/Makefile
index a2b7ef9..f61b13e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: test fasttest run lint pep8 eslint manage
+.PHONY: test fasttest run lint pep8 eslint manage report_failed_tests
# Project settings
LEVEL ?= development
@@ -115,3 +115,7 @@ devserver: clean
# Production Server
server: clean pep8
LEVEL=$(LEVEL) PYTHONPATH=$(PROJECT) $(GUNICORN) -b $(SERVER_HOST):$(SERVER_PORT) -w $(GUNICORN_WORKERS) -n $(GUNICORN_NAME) -t 60 --graceful-timeout 60 $(gunicorn_args) $(GUNICORN_ARGS) $(PROJECT).wsgi:application
+
+# Reporting of failed cases:
+jira_check_tests:
+ ezh-jira-test-checker run_check $(COMMAND_ARGS)
diff --git a/circle.yml b/circle.yml
index 1129112..08f5794 100644
--- a/circle.yml
+++ b/circle.yml
@@ -5,6 +5,15 @@ machine:
DATABASE_URL: postgres://ubuntu:@127.0.0.1:5432/circle_test
DJANGO_SETTINGS_MODULE: django_and_angular.settings.development
LEVEL: development
+
+ # JIRA integration
+ REPORT_FAILED_TEST_BRANCHES: develop,master,test1
+ JIRA_ISSUE_TYPE: Bug
+ JIRA_PROJECT_KEY: EZHOME
+ JIRA_SERVER: https://ezhome-test.atlassian.net
+ JIRA_USERNAME: jira_bot
+ JIRA_PASSWORD: P@ssw0rd
+ JIRA_DEFAULT_ASSIGNEE: sshishov.sshishov@yandex.ru
timezone:
America/Los_Angeles
node:
@@ -25,13 +34,13 @@ dependencies:
test:
override:
- - TEST_ARGS='--with-xunit --with-json-extended' make lint test
+ - TEST_ARGS='--with-xunit' make lint test
post:
# - coveralls
- mkdir -p $CIRCLE_TEST_REPORTS/junit/
- "[ -r nosetests.xml ] && mv nosetests.xml $CIRCLE_TEST_REPORTS/junit/ || :"
- - "[ -r nosetests.json ] && mv nosetests.json $CIRCLE_TEST_REPORTS/junit/ || :"
+ - "COMMAND_ARGS='--target-branch ${CIRCLE_BRANCH} --test-results ${CIRCLE_TEST_REPORTS}/junit/nosetests.xml --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) --jira-default-assignee $(JIRA_DEFAULT_ASSIGNEE)' make jira_check_tests || :"
# # Override /etc/hosts
# hosts:
diff --git a/nosetests.xml b/nosetests.xml
deleted file mode 100644
index c33cfd0..0000000
--- a/nosetests.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index eb76aeb..db08fdb 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,12 +1,16 @@
+--extra-index-url=http://ezhome-bot:q8zgdmot3@pypi.ezhome.io/simple/
+
coverage==4.0
coveralls==1.0
django-nose==1.4.2
+# ezhinfra==0.2.?
+# temporary solution for being able to check the PR
+-e git+git@github.com:ezhome/ezh-infrastructure-tools.git@d910af7fdbe2c04e1c258e718e5026992c1ac29d#egg=ezhinfra
flake8==2.3.0
flake8-import-order==0.5.3
flake8-pep257==1.0.3
-https://github.com/zheller/flake8-quotes/tarball/aef86c4f8388e790332757e5921047ad53160a75#egg=flake8-quotes
+flake8-quotes==0.2.4
nose==1.3.7
-nosetests-json-extended==0.1.0
pep257==0.6.0
pep8==1.6.2
pep8-naming==0.3.3
diff --git a/requirements.txt b/requirements.txt
index db1dd86..450ee67 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,6 @@ drf-nested-routers==0.9.0
gunicorn==19.1.1
psycopg2==2.6.1
raven==5.10.2
-six==1.8.0
+six==1.9.0
static3==0.5.1
wsgiref==0.1.2
diff --git a/src/authentication/tests.py b/src/authentication/tests.py
index f9d30b0..437c6fe 100644
--- a/src/authentication/tests.py
+++ b/src/authentication/tests.py
@@ -7,5 +7,4 @@ def test_success(self):
pass
def test_failure(self):
- # self.fail('Failed test')
- pass
+ self.fail('Failed test')
diff --git a/src/django_and_angular/management/__init__.py b/src/django_and_angular/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/django_and_angular/management/commands/__init__.py b/src/django_and_angular/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/django_and_angular/management/commands/report_failed_tests.py b/src/django_and_angular/management/commands/report_failed_tests.py
new file mode 100644
index 0000000..86c4b52
--- /dev/null
+++ b/src/django_and_angular/management/commands/report_failed_tests.py
@@ -0,0 +1,226 @@
+import re
+from optparse import make_option
+from xml.etree import cElementTree as ElementTree
+
+from django.core.management.base import BaseCommand
+from git import Repo
+from jira import JIRA
+from jira.exceptions import JIRAError
+
+
+FAILURE_ROW_RE = re.compile(r'\s*File\s"(.*)",\sline\s(.*),.*')
+
+
+class Command(BaseCommand):
+
+ help = 'Creates JIRA issues for every failed case for specified branch'
+
+ option_list = BaseCommand.option_list + (
+ make_option(
+ '--branches',
+ action='store',
+ dest='branches',
+ help='Affected branches',
+ ),
+ make_option(
+ '--target-branch',
+ action='store',
+ dest='target_branch',
+ help='Target branch',
+ ),
+ make_option(
+ '--issue-type',
+ action='store',
+ dest='issue_type',
+ default='Bug',
+ help='Issue type',
+ ),
+ make_option(
+ '--project-key',
+ action='store',
+ dest='project_key',
+ default='EZHOME',
+ help='Project key',
+ ),
+ make_option(
+ '--jira-server',
+ action='store',
+ dest='jira_server',
+ help='JIRA server',
+ ),
+ make_option(
+ '--jira-username',
+ action='store',
+ dest='jira_username',
+ help='Username for JIRA account',
+ ),
+ make_option(
+ '--jira-password',
+ action='store',
+ dest='jira_password',
+ help='Password for JIRA account',
+ ),
+ make_option(
+ '--test-results',
+ action='store',
+ dest='test_results',
+ default='nosetests.xml',
+ help='Location of test results',
+ ),
+ make_option(
+ '--jira-default-assignee',
+ action='store',
+ dest='jira_default_assignee',
+ default='',
+ help='Default assignee for new user',
+ )
+ )
+
+ def handle(self, *args, **options):
+ self.issue_type = options['issue_type']
+ self.project_key = options['project_key']
+ self.jira_server = options['jira_server']
+ self.jira_username = options['jira_username']
+ self.jira_password = options['jira_password']
+ self.jira_default_assignee = options['jira_default_assignee']
+ test_results = options['test_results']
+
+ self.repo = Repo()
+ branches = []
+ try:
+ for branch in options['branches'].split(','):
+ branches.append(self.repo.heads[branch])
+ except IndexError:
+ return 'Cannot find branch "{0}"'.format(branch)
+ try:
+ self.target_branch = self.repo.heads[options['target_branch']]
+ except IndexError:
+ return 'Cannot find branch "{0}"'.format(options['target_branch'])
+ if self.target_branch != self.repo.head.ref:
+ return (
+ 'Current branch "{0}" does not match '
+ 'provided CircleCI branch "{1}"'
+ .format(self.repo.head.ref, self.target_branch)
+ )
+ elif self.target_branch not in branches:
+ return 'Skipping check for branch "{0}"'.format(self.repo.head.ref)
+
+ try:
+ root = ElementTree.parse(test_results).getroot()
+ if root.attrib['errors']:
+ results = []
+ for testcase in root:
+ if testcase:
+ result = self.handle_testcase(testcase)
+ if result is not None:
+ results.append(result)
+ if results:
+ return '\n'.join(results)
+ else:
+ return 'No errors in tests'
+ else:
+ return 'No errors in tests'
+ except IOError:
+ return 'File "{0}" does not exist'.format(test_results)
+
+ @staticmethod
+ def parse_test_path(path):
+ path, classname = path.rsplit('.', 1)
+ path = path.replace('.', '/')
+ return path, classname
+
+ def handle_testcase(self, testcase):
+ path, classname = self.parse_test_path(
+ testcase.attrib['classname']
+ )
+ for (file_path, line_number) in re.findall(
+ FAILURE_ROW_RE, testcase[0].text
+ ):
+ if path in file_path:
+ # Finding the line of testcase definition
+ authors = {}
+ commit, line = self.repo.blame(
+ '-L/def {}/'.format(testcase.attrib['name']), file_path
+ )[0]
+ if commit.author not in authors:
+ authors['function'] = commit.author
+ # Finding the line of failure
+ commit, line = self.repo.blame(
+ '-L{0},{0}'.format(line_number), file_path
+ )[0]
+ if commit.author not in authors:
+ authors['failure'] = commit.author
+ return self.handle_jira(
+ path=path,
+ authors=authors,
+ classname=classname,
+ testcase=testcase,
+ )
+
+ def handle_jira(self, path, authors, classname, testcase):
+ try:
+ jira = JIRA(
+ server=self.jira_server,
+ basic_auth=(
+ self.jira_username,
+ self.jira_password,
+ )
+ )
+ summary = (
+ 'Fail: {path}:{classname}.{testcase}, '
+ 'branch: {branch}'.format(
+ path=path,
+ classname=classname,
+ testcase=testcase.attrib['name'],
+ branch=self.target_branch,
+ )
+ )
+ open_issues = jira.search_issues(
+ 'summary ~ "{summary}" AND '
+ 'resolution=unresolved'.format(
+ summary=summary
+ ),
+ maxResults=1
+ )
+ if open_issues:
+ # Update priority
+ issue = open_issues[0]
+ new_priority = '1'
+ if int(issue.fields.priority.id) > 1:
+ new_priority = str(int(issue.fields.priority.id) - 1)
+ issue.update(priority={'id': new_priority})
+ return (
+ 'Priority of issue "{issue}" '
+ 'has been set to "{priority}"'.format(
+ issue=issue, priority=jira.priority(new_priority)
+ )
+ )
+ else:
+ # Create issue
+ assignees = []
+ assignees.extend(jira.search_users(
+ user=authors['function'].email,
+ maxResults=1
+ ))
+ assignees.extend(jira.search_users(
+ user=authors['failure'].email,
+ maxResults=1
+ ))
+ if self.jira_default_assignee:
+ assignees.extend(jira.search_users(
+ user=self.jira_default_assignee,
+ maxResults=1
+ ))
+ issue_dict = dict(
+ project={'key': self.project_key},
+ summary=summary,
+ issuetype={'name': self.issue_type},
+ priority={'id': jira.priorities()[-1].id},
+ description='Description here',
+ )
+ if assignees:
+ issue_dict['assignee'] = {'name': assignees[0].name}
+ new_issue = jira.create_issue(fields=issue_dict)
+ return 'New issue "{0}" has been created'.format(new_issue)
+ except JIRAError as e:
+ return 'JIRA ERROR: {}'.format(e.text)
diff --git a/src/django_and_angular/settings/defaults.py b/src/django_and_angular/settings/defaults.py
index e9115e0..7be8898 100644
--- a/src/django_and_angular/settings/defaults.py
+++ b/src/django_and_angular/settings/defaults.py
@@ -41,6 +41,7 @@
'compressor',
'authentication',
'posts',
+ 'django_and_angular',
)
MIDDLEWARE_CLASSES = (