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 = (