diff --git a/.buildbot/test/01-requirements.sh b/.buildbot/test/01-requirements.sh index 0704da7..36000db 100644 --- a/.buildbot/test/01-requirements.sh +++ b/.buildbot/test/01-requirements.sh @@ -1,5 +1,6 @@ python_major_version=`python -c 'import sys; print sys.version_info.major'` if test "$python_major_version" = "2" ; then pip install 'django<2' + pip install 'pylint-django<2' fi -pip install -r requirements.txt +pip install -r requirements-test.txt diff --git a/.buildbot/test/50-django_testrunner.sh b/.buildbot/test/50-django_testrunner.sh index 7cd7b17..9ede9ff 100644 --- a/.buildbot/test/50-django_testrunner.sh +++ b/.buildbot/test/50-django_testrunner.sh @@ -1 +1 @@ -python manage.py test +coverage run manage.py test diff --git a/.buildbot/test/60-coverage_report.sh b/.buildbot/test/60-coverage_report.sh new file mode 100644 index 0000000..3afc826 --- /dev/null +++ b/.buildbot/test/60-coverage_report.sh @@ -0,0 +1 @@ +coverage report --skip-covered --fail-under=99 diff --git a/.buildbot/test/70-pylint.sh b/.buildbot/test/70-pylint.sh new file mode 100644 index 0000000..4d04efd --- /dev/null +++ b/.buildbot/test/70-pylint.sh @@ -0,0 +1 @@ +pylint apps.base diff --git a/.coveragerc b/.coveragerc index 701fac8..48875c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,3 @@ [run] -source = base +source = apps.base +omit = apps/base/tests/utils.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..bfd1a4a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,27 @@ +[MASTER] + +persistent=no +#load-plugins=pylint_django + + +[MESSAGES CONTROL] + +disable=missing-docstring, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + useless-object-inheritance, + +[BASIC] + +good-names=_, + c, + d, + e, + i, + j, + k, + +[FORMAT] + +max-line-length=120 diff --git a/apps/base/tests/test_templates.py b/apps/base/tests/test_templates.py index 6e38dbf..55c572f 100644 --- a/apps/base/tests/test_templates.py +++ b/apps/base/tests/test_templates.py @@ -6,7 +6,7 @@ from django.test import SimpleTestCase class BaseTemplateTestCase(SimpleTestCase): - def assertInHTML_multi(self, response, needles, format_kwargs=None): + def assertInHTMLMulti(self, response, needles, format_kwargs=None): # pylint: disable=invalid-name content = response.content.decode('utf-8') for needle in needles: if format_kwargs is not None: @@ -35,7 +35,7 @@ class BaseTemplateTestCase(SimpleTestCase): '', ) - self.assertInHTML_multi(response, needles, format_kwargs) + self.assertInHTMLMulti(response, needles, format_kwargs) def test_bootstrap_css_links(self): response = self.response @@ -55,7 +55,7 @@ class BaseTemplateTestCase(SimpleTestCase): '', ) - self.assertInHTML_multi(response, needles, format_kwargs) + self.assertInHTMLMulti(response, needles, format_kwargs) def test_page_footer(self): response = self.response diff --git a/apps/base/tests/test_urls.py b/apps/base/tests/test_urls.py new file mode 100644 index 0000000..a8839ae --- /dev/null +++ b/apps/base/tests/test_urls.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.test import SimpleTestCase +from django.urls import reverse + + +class UrlsTestCase(SimpleTestCase): + def test_root_reverse(self): + response = self.client.get(reverse('root')) + self.assertEqual(response.status_code, 200) diff --git a/apps/base/tests/test_views.py b/apps/base/tests/test_views.py index 6d42e77..ed5a682 100644 --- a/apps/base/tests/test_views.py +++ b/apps/base/tests/test_views.py @@ -1,31 +1,82 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime +import re import socket +import unittest +import mock from django.test import SimpleTestCase +from ..views import RootView -class DjangoAdminTestCase(SimpleTestCase): + +class RootUnitTestCase(unittest.TestCase): + def test_get_context_data(self): + default_color_hex = '47825b' + + test_data_sets = [ + {}, + {'hostname': 'localhost'}, + {'hostname': 'abcde'}, + {'hostname': 'frodo.abcdef.example.com'}, + {'hostname': 'aBc-dEf', 'expected_color_hex': 'abcdef'}, + {'hostname': 'ab34EF12cd56ZZ', 'expected_color_hex': 'ab34ef'}, + {'hostname': 'abcde.example.com', 'expected_color_hex': 'abcdee'}, + {'hostname': 'abcdef', 'kwargs': {'color_hex': '123456'}, 'expected_color_hex': '123456'}, + ] + + real_hostname = socket.gethostname() + + for test_data in test_data_sets: + if 'hostname' in test_data: + hostname = test_data['hostname'] + else: + hostname = real_hostname + + if 'expected_color_hex' in test_data: + expected_color_hex = test_data['expected_color_hex'] + else: + expected_color_hex = default_color_hex + + with mock.patch('socket.gethostname', return_value=hostname): + view = RootView() + if 'kwargs' in test_data: + ctx = view.get_context_data(**test_data['kwargs']) + else: + ctx = view.get_context_data() + + self.assertIsInstance(ctx, dict) + self.assertIn('hostname', ctx) + self.assertEqual(ctx['hostname'], hostname) + self.assertIn('color_hex', ctx) + self.assertTrue(re.match('[0-9a-f]{6}', ctx['color_hex'])) + self.assertEqual(ctx['color_hex'], expected_color_hex) + self.assertIn('time', ctx) + self.assertIsInstance(ctx['time'], datetime.datetime) + + +class DjangoAdminDjangoTestCase(SimpleTestCase): def test_djangoadmin(self): response = self.client.get('/djangoadmin', follow=True) self.assertContains(response, 'Django administration') -class RootTestCase(SimpleTestCase): - def setUp(self): - super(RootTestCase, self).setUp() - self.response = self.client.get('/') - +class RootDjangoTestCase(SimpleTestCase): def test_root_template(self): - response = self.response + response = self.client.get('/') self.assertTemplateUsed(response, 'base/root.html') def test_root_context(self): - response = self.response + response = self.client.get('/') self.assertIn('hostname', response.context) hostname = socket.gethostname() self.assertEqual(response.context['hostname'], hostname) + self.assertIn('color_hex', response.context) + self.assertTrue(re.match('[0-9a-f]{6}', response.context['color_hex'])) + self.assertIn('time', response.context) + self.assertIsInstance(response.context['time'], datetime.datetime) def test_root_content(self): - response = self.response + response = self.client.get('/') hostname = socket.gethostname().capitalize() self.assertContains(response, hostname) diff --git a/apps/base/tests/utils.py b/apps/base/tests/utils.py index cd8727b..7464a0f 100644 --- a/apps/base/tests/utils.py +++ b/apps/base/tests/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: skip-file from __future__ import unicode_literals import os from tempfile import mkdtemp as _mkdtmp diff --git a/apps/base/urls.py b/apps/base/urls.py index 4a69660..9276107 100644 --- a/apps/base/urls.py +++ b/apps/base/urls.py @@ -5,7 +5,7 @@ from django.conf.urls import url from . import views -urlpatterns = [ +urlpatterns = [ # pylint: disable=invalid-name url(r'^$', views.RootView.as_view(), name='root'), url(r'^djangoadmin/', admin.site.urls), ] diff --git a/apps/base/views.py b/apps/base/views.py index 3b0c7a3..ae9f4cc 100644 --- a/apps/base/views.py +++ b/apps/base/views.py @@ -8,16 +8,23 @@ from django.views import generic class RootView(generic.TemplateView): template_name = 'base/root.html' + _default_color_hex = '47825b' + + def _get_color_hex(self, hostname=None): + color_hex = self._default_color_hex + if hostname: + buf = hostname + buf = re.sub('[-.]', '', buf) + buf = buf[:6].lower() + if re.match('[0-9a-f]{6}', buf): + color_hex = buf + return color_hex def get_context_data(self, **kwargs): if 'hostname' not in kwargs: kwargs['hostname'] = socket.gethostname() if 'color_hex' not in kwargs: - buf = kwargs['hostname'] - buf = re.sub('[-.]', '', buf) - buf = buf[:6].lower() - if re.match('[0-9a-f]{6}', buf): - kwargs['color_hex'] = buf + kwargs['color_hex'] = self._get_color_hex(kwargs['hostname']) if 'time' not in kwargs: kwargs['time'] = timezone.now() return super(RootView, self).get_context_data(**kwargs) diff --git a/bin/coverage-html.py b/bin/coverage-html.py index 2750291..e72fb19 100755 --- a/bin/coverage-html.py +++ b/bin/coverage-html.py @@ -2,22 +2,27 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import argparse -import coverage import datetime import os import shutil import sys +import time +import coverage +from selenium import webdriver +from selenium.common.exceptions import WebDriverException -class Command(object): +class Command(object): # pylint: disable=too-few-public-methods default_browser = 'firefox' @staticmethod def _setup_argparser(): kwargs = { - 'description': 'Tool to create html coverage report.', + 'description': 'Create a coverage report from a previous coverage run and show it within a browser window.', } parser = argparse.ArgumentParser(**kwargs) + parser.add_argument('-k', '--keep', action='store_true', dest='keep_report', + help='keep the report after closing the browser') return parser def _parse_args(self, argv=None): @@ -35,41 +40,45 @@ class Command(object): os.makedirs(path) return path - def _remove_report_directory(self, path=None): - if path is None: - path = self._report_directory - if path is not None: - if os.path.isdir(path): - sys.stdout.write('Removing report directory {}\n'.format(path)) - shutil.rmtree(path) + @staticmethod + def _remove_report_directory(path): + if os.path.isdir(path): + sys.stdout.write('Removing report directory {}\n'.format(path)) + shutil.rmtree(path) def _create_report(self, path): return self._coverage.html_report(directory=path, skip_covered=True) - def _show_report(self, path): - start_file = os.path.join(path, 'index.html') - browser = os.environ.get('BROWSER', self.default_browser) - cmd = '{browser} --new-window "{file}"'.format(browser=browser, file=start_file) - return os.system(cmd) + @staticmethod + def _show_report(path): + browser = webdriver.Firefox() + start_file = os.path.abspath(os.path.join(path, 'index.html')) + browser.get('file://{}'.format(start_file)) + while True: + time.sleep(1) + try: + _ = browser.window_handles + except WebDriverException: + break + return True def __init__(self): self._argparser = self._setup_argparser() - self._report_directory = self._create_report_directory() self._coverage = coverage.Coverage() self._coverage.load() def __call__(self, argv=None): - self._parse_args(argv) - report_dir = self._report_directory + cmd_args = self._parse_args(argv) + report_dir = self._create_report_directory() sys.stdout.write('Report directory: {}\n'.format(report_dir)) try: self._create_report(report_dir) - except Exception as e1: - self._remove_report_directory(report_dir) - raise e1 + self._show_report(report_dir) + finally: + if not cmd_args.keep_report: + self._remove_report_directory(report_dir) - exitval = self._show_report(report_dir) - return exitval + return os.EX_OK def main(): diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..79e0c87 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +-r requirements.txt +coverage +mock +pylint-django +selenium diff --git a/requirements.txt b/requirements.txt index a4d9b9a..8a964ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -coverage django django-extensions diff --git a/setup.py b/setup.py index 878f559..3c0c190 100644 --- a/setup.py +++ b/setup.py @@ -110,6 +110,8 @@ class SetupDjangoProject(MyCommand): if not os.path.exists(d): os.makedirs(d) + return os.EX_OK + setup( name='django-test',