From d584de697ad69be2fe08ffb3fc5534ca69ae9958 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 14:12:23 +0200 Subject: [PATCH 1/9] UPD: improved tests. --- .coveragerc | 3 +- apps/base/tests/test_urls.py | 10 +++++ apps/base/tests/test_views.py | 71 ++++++++++++++++++++++++++++++----- apps/base/views.py | 17 ++++++--- 4 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 apps/base/tests/test_urls.py 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/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..7178768 100644 --- a/apps/base/tests/test_views.py +++ b/apps/base/tests/test_views.py @@ -1,31 +1,84 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime +import re import socket +import unittest from django.test import SimpleTestCase +from unittest import mock + +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): + v = RootView() + if 'kwargs' in test_data: + c = v.get_context_data(**test_data['kwargs']) + else: + c = v.get_context_data() + + self.assertIsInstance(c, dict) + self.assertIn('hostname', c) + self.assertEqual(c['hostname'], hostname) + self.assertIn('color_hex', c) + self.assertTrue(re.match('[0-9a-f]{6}', c['color_hex'])) + self.assertEqual(c['color_hex'], expected_color_hex) + self.assertIn('time', c) + self.assertIsInstance(c['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/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) From b3744b966ddc32c3b562b757eb4e74efea6a9e9a Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 14:19:33 +0200 Subject: [PATCH 2/9] UPD: added coverage report to buildbots test pipeline. --- .buildbot/test/50-django_testrunner.sh | 2 +- .buildbot/test/60-coverage_report.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .buildbot/test/60-coverage_report.sh diff --git a/.buildbot/test/50-django_testrunner.sh b/.buildbot/test/50-django_testrunner.sh index 7cd7b17..6e3730e 100644 --- a/.buildbot/test/50-django_testrunner.sh +++ b/.buildbot/test/50-django_testrunner.sh @@ -1 +1 @@ -python manage.py test +coverage 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 From c3e61674c5b05fcbd98b79abfc1865ac10d9eb71 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 14:39:14 +0200 Subject: [PATCH 3/9] FIX: #5 --- apps/base/tests/test_views.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/base/tests/test_views.py b/apps/base/tests/test_views.py index 7178768..dc582ff 100644 --- a/apps/base/tests/test_views.py +++ b/apps/base/tests/test_views.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime +import mock import re import socket import unittest from django.test import SimpleTestCase -from unittest import mock from ..views import RootView diff --git a/requirements.txt b/requirements.txt index a4d9b9a..73dee02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ coverage django django-extensions +mock From 55dd1e78a091f0a1cd05dea872f3b3d9ef9da5cf Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 14:43:54 +0200 Subject: [PATCH 4/9] FIX: d584de697ad69be2fe08ffb3fc5534ca69ae9958 --- .buildbot/test/50-django_testrunner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildbot/test/50-django_testrunner.sh b/.buildbot/test/50-django_testrunner.sh index 6e3730e..9ede9ff 100644 --- a/.buildbot/test/50-django_testrunner.sh +++ b/.buildbot/test/50-django_testrunner.sh @@ -1 +1 @@ -coverage manage.py test +coverage run manage.py test From df67767a9af82cc1f22915bbe8bfc1931f4f9259 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 16:05:53 +0200 Subject: [PATCH 5/9] ADD: support for pylint. --- .buildbot/test/01-requirements.sh | 1 + .buildbot/test/70-pylint.sh | 1 + .pylintrc | 25 +++++++++++++++++++++++++ apps/base/tests/test_templates.py | 6 +++--- apps/base/tests/test_views.py | 26 ++++++++++++-------------- bin/coverage-html.py | 8 ++++---- setup.py | 2 ++ 7 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 .buildbot/test/70-pylint.sh create mode 100644 .pylintrc diff --git a/.buildbot/test/01-requirements.sh b/.buildbot/test/01-requirements.sh index 0704da7..b8a4c71 100644 --- a/.buildbot/test/01-requirements.sh +++ b/.buildbot/test/01-requirements.sh @@ -3,3 +3,4 @@ if test "$python_major_version" = "2" ; then pip install 'django<2' fi pip install -r requirements.txt +pip install pylint 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/.pylintrc b/.pylintrc new file mode 100644 index 0000000..a194153 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,25 @@ +[MASTER] + +load-plugins=pylint_django + + +[MESSAGES CONTROL] + +disable=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_views.py b/apps/base/tests/test_views.py index dc582ff..ed5a682 100644 --- a/apps/base/tests/test_views.py +++ b/apps/base/tests/test_views.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime -import mock import re import socket import unittest +import mock from django.test import SimpleTestCase from ..views import RootView @@ -39,20 +39,20 @@ class RootUnitTestCase(unittest.TestCase): expected_color_hex = default_color_hex with mock.patch('socket.gethostname', return_value=hostname): - v = RootView() + view = RootView() if 'kwargs' in test_data: - c = v.get_context_data(**test_data['kwargs']) + ctx = view.get_context_data(**test_data['kwargs']) else: - c = v.get_context_data() + ctx = view.get_context_data() - self.assertIsInstance(c, dict) - self.assertIn('hostname', c) - self.assertEqual(c['hostname'], hostname) - self.assertIn('color_hex', c) - self.assertTrue(re.match('[0-9a-f]{6}', c['color_hex'])) - self.assertEqual(c['color_hex'], expected_color_hex) - self.assertIn('time', c) - self.assertIsInstance(c['time'], datetime.datetime) + 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): @@ -80,5 +80,3 @@ class RootDjangoTestCase(SimpleTestCase): response = self.client.get('/') hostname = socket.gethostname().capitalize() self.assertContains(response, hostname) - - diff --git a/bin/coverage-html.py b/bin/coverage-html.py index 2750291..a7feee4 100755 --- a/bin/coverage-html.py +++ b/bin/coverage-html.py @@ -2,14 +2,14 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import argparse -import coverage import datetime import os import shutil import sys +import coverage -class Command(object): +class Command(object): # pylint: disable=too-few-public-methods default_browser = 'firefox' @staticmethod @@ -64,9 +64,9 @@ class Command(object): sys.stdout.write('Report directory: {}\n'.format(report_dir)) try: self._create_report(report_dir) - except Exception as e1: + except Exception as e: self._remove_report_directory(report_dir) - raise e1 + raise e exitval = self._show_report(report_dir) return exitval 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', From 077e25e57caf8ab690cd745877973342fc6eb85b Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 16:10:40 +0200 Subject: [PATCH 6/9] FIX df67767a9a --- .buildbot/test/01-requirements.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildbot/test/01-requirements.sh b/.buildbot/test/01-requirements.sh index b8a4c71..94ea381 100644 --- a/.buildbot/test/01-requirements.sh +++ b/.buildbot/test/01-requirements.sh @@ -3,4 +3,4 @@ if test "$python_major_version" = "2" ; then pip install 'django<2' fi pip install -r requirements.txt -pip install pylint +pip install pylint-django From e9a43740eeed8934dfa574409799900a01acd403 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 16:33:11 +0200 Subject: [PATCH 7/9] FIX using pylint for python2. --- .buildbot/test/01-requirements.sh | 6 +++++- .pylintrc | 6 ++++-- apps/base/tests/utils.py | 3 ++- apps/base/urls.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.buildbot/test/01-requirements.sh b/.buildbot/test/01-requirements.sh index 94ea381..c1beb58 100644 --- a/.buildbot/test/01-requirements.sh +++ b/.buildbot/test/01-requirements.sh @@ -3,4 +3,8 @@ if test "$python_major_version" = "2" ; then pip install 'django<2' fi pip install -r requirements.txt -pip install pylint-django +if test "$python_major_version" = "2" ; then + pip install 'pylint-django<2' +else + pip install pylint-django +fi diff --git a/.pylintrc b/.pylintrc index a194153..bfd1a4a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,11 +1,13 @@ [MASTER] -load-plugins=pylint_django +persistent=no +#load-plugins=pylint_django [MESSAGES CONTROL] -disable=missing-module-docstring, +disable=missing-docstring, + missing-module-docstring, missing-class-docstring, missing-function-docstring, useless-object-inheritance, diff --git a/apps/base/tests/utils.py b/apps/base/tests/utils.py index cd8727b..4f2d087 100644 --- a/apps/base/tests/utils.py +++ b/apps/base/tests/utils.py @@ -8,5 +8,6 @@ def mkdtemp(prefix): dirname = os.path.dirname pkg_base_dir = dirname(dirname(dirname(__file__))) tmp_dir = os.path.join(pkg_base_dir, 'tmp') - os.makedirs(tmp_dir, exist_ok=True) + # os.makedirs(tmp_dir, exist_ok=True) + os.makedirs(tmp_dir) return _mkdtmp(prefix=prefix, dir=tmp_dir) 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), ] From 42ef136bbea1d006ad4d8b19f8ba99eccec0b786 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 24 Oct 2019 16:37:53 +0200 Subject: [PATCH 8/9] UPD: excluded unused function apps.base.tests.utils.mkdtemp from pylint. --- apps/base/tests/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/base/tests/utils.py b/apps/base/tests/utils.py index 4f2d087..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 @@ -8,6 +9,5 @@ def mkdtemp(prefix): dirname = os.path.dirname pkg_base_dir = dirname(dirname(dirname(__file__))) tmp_dir = os.path.join(pkg_base_dir, 'tmp') - # os.makedirs(tmp_dir, exist_ok=True) - os.makedirs(tmp_dir) + os.makedirs(tmp_dir, exist_ok=True) return _mkdtmp(prefix=prefix, dir=tmp_dir) From 774005d2fca19d83707d740752a656ea1631b88e Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Fri, 25 Oct 2019 11:33:46 +0200 Subject: [PATCH 9/9] UPD: improved requirements management and coverage-html.py --- .buildbot/test/01-requirements.sh | 6 +--- bin/coverage-html.py | 51 ++++++++++++++++++------------- requirements-test.txt | 5 +++ requirements.txt | 2 -- 4 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 requirements-test.txt diff --git a/.buildbot/test/01-requirements.sh b/.buildbot/test/01-requirements.sh index c1beb58..36000db 100644 --- a/.buildbot/test/01-requirements.sh +++ b/.buildbot/test/01-requirements.sh @@ -1,10 +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' -fi -pip install -r requirements.txt -if test "$python_major_version" = "2" ; then pip install 'pylint-django<2' -else - pip install pylint-django fi +pip install -r requirements-test.txt diff --git a/bin/coverage-html.py b/bin/coverage-html.py index a7feee4..e72fb19 100755 --- a/bin/coverage-html.py +++ b/bin/coverage-html.py @@ -6,7 +6,10 @@ 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): # pylint: disable=too-few-public-methods @@ -15,9 +18,11 @@ class Command(object): # pylint: disable=too-few-public-methods @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): # pylint: disable=too-few-public-methods 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 e: - self._remove_report_directory(report_dir) - raise e + 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 73dee02..8a964ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ -coverage django django-extensions -mock