From 3dcbc1caaeb7513093e05d3c0e359395d7abdce2 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 9 Feb 2021 14:00:56 -0500 Subject: [PATCH 1/4] Fix django 2.2 jquery/media bug --- cropduster/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cropduster/forms.py b/cropduster/forms.py index 91238387..a148b0f8 100644 --- a/cropduster/forms.py +++ b/cropduster/forms.py @@ -26,6 +26,7 @@ class CropDusterWidget(GenericForeignFileWidget): class Media: css = {'all': ('cropduster/css/cropduster.css',)} js = ( + 'admin/js/jquery.init.js', 'cropduster/js/jsrender.js', 'cropduster/js/cropduster.js', ) From d1240874acc2bff3bdd3b5ccd881c442b42dd7fa Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 12 Jun 2020 10:57:55 -0400 Subject: [PATCH 2/4] Switch to pytest as test runner --- .coveragerc | 4 ++ .gitignore | 1 + .travis.yml | 2 +- cropduster/settings.py | 2 +- cropduster/tests/admin.py | 8 ---- cropduster/tests/test_standalone/__init__.py | 0 cropduster/utils/image.py | 2 +- pytest.ini | 6 +++ runtests.py | 20 -------- setup.py | 2 +- {cropduster/tests => tests}/__init__.py | 0 tests/admin.py | 8 ++++ tests/conftest.py | 13 ++++++ .../data/animated-duration.gif | Bin {cropduster/tests => tests}/data/animated.gif | Bin .../data/best-fit-off-by-one-bug.png | Bin {cropduster/tests => tests}/data/cmyk.jpg | Bin {cropduster/tests => tests}/data/img.jpg | Bin {cropduster/tests => tests}/data/img.png | Bin {cropduster/tests => tests}/data/img2.jpg | Bin .../tests => tests}/data/size-order-bug.png | Bin .../tests => tests}/data/transparent.png | Bin {cropduster/tests => tests}/helpers.py | 0 {cropduster/tests => tests}/models.py | 26 +++++------ {cropduster/tests => tests}/settings.py | 6 +-- tests/standalone/__init__.py | 1 + .../standalone}/admin.py | 0 tests/standalone/apps.py | 6 +++ .../standalone}/models.py | 0 .../standalone}/test_admin.py | 8 ++-- .../tests => tests}/templates/404.html | 0 .../tests => tests}/templates/500.html | 0 {cropduster/tests => tests}/test_admin.py | 16 +++---- {cropduster/tests => tests}/test_gifsicle.py | 2 +- {cropduster/tests => tests}/test_models.py | 44 +++++++++--------- {cropduster/tests => tests}/test_resizing.py | 0 {cropduster/tests => tests}/test_utils.py | 14 +++--- {cropduster/tests => tests}/test_views.py | 0 {cropduster/tests => tests}/urls.py | 0 {cropduster/tests => tests}/utils.py | 0 tox.ini | 23 +++++---- 41 files changed, 116 insertions(+), 98 deletions(-) create mode 100644 .coveragerc delete mode 100644 cropduster/tests/admin.py delete mode 100644 cropduster/tests/test_standalone/__init__.py create mode 100644 pytest.ini delete mode 100755 runtests.py rename {cropduster/tests => tests}/__init__.py (100%) create mode 100644 tests/admin.py create mode 100644 tests/conftest.py rename {cropduster/tests => tests}/data/animated-duration.gif (100%) rename {cropduster/tests => tests}/data/animated.gif (100%) rename {cropduster/tests => tests}/data/best-fit-off-by-one-bug.png (100%) rename {cropduster/tests => tests}/data/cmyk.jpg (100%) rename {cropduster/tests => tests}/data/img.jpg (100%) rename {cropduster/tests => tests}/data/img.png (100%) rename {cropduster/tests => tests}/data/img2.jpg (100%) rename {cropduster/tests => tests}/data/size-order-bug.png (100%) rename {cropduster/tests => tests}/data/transparent.png (100%) rename {cropduster/tests => tests}/helpers.py (100%) rename {cropduster/tests => tests}/models.py (78%) rename {cropduster/tests => tests}/settings.py (94%) create mode 100644 tests/standalone/__init__.py rename {cropduster/tests/test_standalone => tests/standalone}/admin.py (100%) create mode 100644 tests/standalone/apps.py rename {cropduster/tests/test_standalone => tests/standalone}/models.py (100%) rename {cropduster/tests/test_standalone => tests/standalone}/test_admin.py (97%) rename {cropduster/tests => tests}/templates/404.html (100%) rename {cropduster/tests => tests}/templates/500.html (100%) rename {cropduster/tests => tests}/test_admin.py (95%) rename {cropduster/tests => tests}/test_gifsicle.py (92%) rename {cropduster/tests => tests}/test_models.py (91%) rename {cropduster/tests => tests}/test_resizing.py (100%) rename {cropduster/tests => tests}/test_utils.py (90%) rename {cropduster/tests => tests}/test_views.py (100%) rename {cropduster/tests => tests}/urls.py (100%) rename {cropduster/tests => tests}/utils.py (100%) diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..15bf4b90 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +omit = + cropduster/*migrations/* + diff --git a/.gitignore b/.gitignore index 366c33b0..034f6c41 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build ghostdriver.log test/media dist/ +.coverage diff --git a/.travis.yml b/.travis.yml index 669ed76c..e70c07ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,5 +37,5 @@ install: - chmod +x ~/bin/chromedriver script: - - travis_retry tox -- --selenium=chrome-headless + - travis_retry tox -- --selenosis-driver=chrome-headless diff --git a/cropduster/settings.py b/cropduster/settings.py index be8c93ed..86a55a3c 100644 --- a/cropduster/settings.py +++ b/cropduster/settings.py @@ -45,7 +45,7 @@ def get_jpeg_quality(width, height): "CROPDUSTER_JPEG_QUALITY setting must be either a callable " "or a numeric value, got type %s" % (type(CROPDUSTER_JPEG_QUALITY).__name__)) -JPEG_SAVE_ICC_SUPPORTED = (LooseVersion(getattr(PIL, 'PILLOW_VERSION', '0')) +JPEG_SAVE_ICC_SUPPORTED = (LooseVersion(getattr(PIL, '__version__', '0')) >= LooseVersion('2.2.1')) CROPDUSTER_GIFSICLE_PATH = getattr(settings, 'CROPDUSTER_GIFSICLE_PATH', None) diff --git a/cropduster/tests/admin.py b/cropduster/tests/admin.py deleted file mode 100644 index deed2d13..00000000 --- a/cropduster/tests/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin -from .models import Author, Article, TestForOptionalSizes, TestForOrphanedThumbs - - -admin.site.register(Author) -admin.site.register(Article) -admin.site.register(TestForOptionalSizes) -admin.site.register(TestForOrphanedThumbs) diff --git a/cropduster/tests/test_standalone/__init__.py b/cropduster/tests/test_standalone/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cropduster/utils/image.py b/cropduster/utils/image.py index e2ff3166..724d94c9 100644 --- a/cropduster/utils/image.py +++ b/cropduster/utils/image.py @@ -178,7 +178,7 @@ def smart_resize(im, final_w, final_h): # Pillow 2.7.0 greatly improved the bicubic resize algorithm, which makes # our multiple-step resizing unnecessary - pillow_version = getattr(PIL, 'PILLOW_VERSION', None) + pillow_version = getattr(PIL, '__version__', None) if pillow_version and LooseVersion(pillow_version) >= LooseVersion('2.7.0'): return im.resize((final_w, final_h), PIL.Image.BICUBIC) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..1bf0341f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +DJANGO_SETTINGS_MODULE = tests.settings +addopts = --tb=short --create-db --cov=cropduster +django_find_project = false +python_files = tests.py test_*.py *_tests.py +testpaths = tests diff --git a/runtests.py b/runtests.py deleted file mode 100755 index e21dbc7b..00000000 --- a/runtests.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -import warnings -import selenosis - - -class RunTests(selenosis.RunTests): - - def __call__(self, *args, **kwargs): - warnings.simplefilter("error", Warning) - warnings.filterwarnings('ignore', message='.*?ckeditor') - super(RunTests, self).__call__(*args, **kwargs) - - -def main(): - runtests = RunTests("cropduster.tests.settings", "cropduster") - runtests() - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index c561a484..a02c1caa 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ author_email='programmers@theatlantic.com', url='https://github.com/theatlantic/django-cropduster', description='Django image uploader and cropping tool', - packages=find_packages(), + packages=find_packages(exclude=["tests"]), zip_safe=False, long_description=open('README.rst').read(), license='BSD', diff --git a/cropduster/tests/__init__.py b/tests/__init__.py similarity index 100% rename from cropduster/tests/__init__.py rename to tests/__init__.py diff --git a/tests/admin.py b/tests/admin.py new file mode 100644 index 00000000..56e318b5 --- /dev/null +++ b/tests/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import Author, Article, OptionalSizes, OrphanedThumbs + + +admin.site.register(Author) +admin.site.register(Article) +admin.site.register(OptionalSizes) +admin.site.register(OrphanedThumbs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..c5a089ea --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import warnings + +import pytest +from django.test import TestCase + + +TestCase.pytestmark = pytest.mark.django_db(transaction=True, reset_sequences=True) + + +@pytest.fixture(autouse=True) +def suppress_warnings(): + warnings.simplefilter("error", Warning) + warnings.filterwarnings('ignore', message='.*?ckeditor') diff --git a/cropduster/tests/data/animated-duration.gif b/tests/data/animated-duration.gif similarity index 100% rename from cropduster/tests/data/animated-duration.gif rename to tests/data/animated-duration.gif diff --git a/cropduster/tests/data/animated.gif b/tests/data/animated.gif similarity index 100% rename from cropduster/tests/data/animated.gif rename to tests/data/animated.gif diff --git a/cropduster/tests/data/best-fit-off-by-one-bug.png b/tests/data/best-fit-off-by-one-bug.png similarity index 100% rename from cropduster/tests/data/best-fit-off-by-one-bug.png rename to tests/data/best-fit-off-by-one-bug.png diff --git a/cropduster/tests/data/cmyk.jpg b/tests/data/cmyk.jpg similarity index 100% rename from cropduster/tests/data/cmyk.jpg rename to tests/data/cmyk.jpg diff --git a/cropduster/tests/data/img.jpg b/tests/data/img.jpg similarity index 100% rename from cropduster/tests/data/img.jpg rename to tests/data/img.jpg diff --git a/cropduster/tests/data/img.png b/tests/data/img.png similarity index 100% rename from cropduster/tests/data/img.png rename to tests/data/img.png diff --git a/cropduster/tests/data/img2.jpg b/tests/data/img2.jpg similarity index 100% rename from cropduster/tests/data/img2.jpg rename to tests/data/img2.jpg diff --git a/cropduster/tests/data/size-order-bug.png b/tests/data/size-order-bug.png similarity index 100% rename from cropduster/tests/data/size-order-bug.png rename to tests/data/size-order-bug.png diff --git a/cropduster/tests/data/transparent.png b/tests/data/transparent.png similarity index 100% rename from cropduster/tests/data/transparent.png rename to tests/data/transparent.png diff --git a/cropduster/tests/helpers.py b/tests/helpers.py similarity index 100% rename from cropduster/tests/helpers.py rename to tests/helpers.py diff --git a/cropduster/tests/models.py b/tests/models.py similarity index 78% rename from cropduster/tests/models.py rename to tests/models.py index 400eab83..2cdf4c37 100644 --- a/cropduster/tests/models.py +++ b/tests/models.py @@ -39,7 +39,7 @@ class Article(models.Model): field_identifier="alt") -class TestForOptionalSizes(models.Model): +class OptionalSizes(models.Model): TEST_SIZES = [ Size('main', w=600, h=480, auto=[ @@ -50,7 +50,7 @@ class TestForOptionalSizes(models.Model): image = CropDusterField(upload_to="test", sizes=TEST_SIZES) -class TestForOrphanedThumbs(models.Model): +class OrphanedThumbs(models.Model): TEST_SIZES = [ Size('main', w=600, h=480, auto=[ @@ -64,22 +64,22 @@ class TestForOrphanedThumbs(models.Model): image = CropDusterField(upload_to="test", sizes=TEST_SIZES) -class TestMultipleFieldsInheritanceParent(models.Model): +class MultipleFieldsInheritanceParent(models.Model): slug = models.SlugField() image = CropDusterField(upload_to="test", sizes=[Size(u'main', w=600, h=480)]) -class TestMultipleFieldsInheritanceChild(TestMultipleFieldsInheritanceParent): +class MultipleFieldsInheritanceChild(MultipleFieldsInheritanceParent): image2 = CropDusterField(upload_to="test", sizes=[Size(u'main', w=600, h=480)], field_identifier="2") @python_2_unicode_compatible -class TestReverseForeignRelA(models.Model): +class ReverseForeignRelA(models.Model): slug = models.SlugField() - c = models.ForeignKey('TestReverseForeignRelC', on_delete=models.CASCADE) + c = models.ForeignKey('ReverseForeignRelC', on_delete=models.CASCADE) a_type = models.CharField(max_length=10, choices=( ("x", "X"), ("y", "Y"), @@ -91,29 +91,29 @@ def __str__(self): @python_2_unicode_compatible -class TestReverseForeignRelB(models.Model): +class ReverseForeignRelB(models.Model): slug = models.SlugField() - c = models.ForeignKey('TestReverseForeignRelC', on_delete=models.CASCADE) + c = models.ForeignKey('ReverseForeignRelC', on_delete=models.CASCADE) def __str__(self): return self.slug @python_2_unicode_compatible -class TestReverseForeignRelC(models.Model): +class ReverseForeignRelC(models.Model): slug = models.SlugField() rel_a = ReverseForeignRelation( - TestReverseForeignRelA, field_name='c', limit_choices_to={'a_type': 'x'}) - rel_b = ReverseForeignRelation(TestReverseForeignRelB, field_name='c') + ReverseForeignRelA, field_name='c', limit_choices_to={'a_type': 'x'}) + rel_b = ReverseForeignRelation(ReverseForeignRelB, field_name='c') def __str__(self): return self.slug @python_2_unicode_compatible -class TestReverseForeignRelM2M(models.Model): +class ReverseForeignRelM2M(models.Model): slug = models.SlugField() - m2m = models.ManyToManyField(TestReverseForeignRelC) + m2m = models.ManyToManyField(ReverseForeignRelC) def __str__(self): return self.slug diff --git a/cropduster/tests/settings.py b/tests/settings.py similarity index 94% rename from cropduster/tests/settings.py rename to tests/settings.py index ef7bd484..440250d6 100755 --- a/cropduster/tests/settings.py +++ b/tests/settings.py @@ -25,12 +25,12 @@ 'generic_plus', 'cropduster', 'cropduster.standalone', - 'cropduster.tests', - 'cropduster.tests.test_standalone', + 'tests', + 'tests.standalone', 'ckeditor', ) -ROOT_URLCONF = 'cropduster.tests.urls' +ROOT_URLCONF = 'tests.urls' TEMPLATES[0]['OPTIONS']['debug'] = True diff --git a/tests/standalone/__init__.py b/tests/standalone/__init__.py new file mode 100644 index 00000000..28166264 --- /dev/null +++ b/tests/standalone/__init__.py @@ -0,0 +1 @@ +default_app_config = 'tests.standalone.apps.StandaloneTestConfig' diff --git a/cropduster/tests/test_standalone/admin.py b/tests/standalone/admin.py similarity index 100% rename from cropduster/tests/test_standalone/admin.py rename to tests/standalone/admin.py diff --git a/tests/standalone/apps.py b/tests/standalone/apps.py new file mode 100644 index 00000000..8e1501dc --- /dev/null +++ b/tests/standalone/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StandaloneTestConfig(AppConfig): + name = 'tests.standalone' + label = 'standalone_test' diff --git a/cropduster/tests/test_standalone/models.py b/tests/standalone/models.py similarity index 100% rename from cropduster/tests/test_standalone/models.py rename to tests/standalone/models.py diff --git a/cropduster/tests/test_standalone/test_admin.py b/tests/standalone/test_admin.py similarity index 97% rename from cropduster/tests/test_standalone/test_admin.py rename to tests/standalone/test_admin.py index bea8b4ef..17216f94 100644 --- a/cropduster/tests/test_standalone/test_admin.py +++ b/tests/standalone/test_admin.py @@ -14,14 +14,14 @@ from selenosis import AdminSelenosisTestCase from cropduster.models import Image, Thumb -from cropduster.tests.helpers import CropdusterTestCaseMediaMixin +from tests.helpers import CropdusterTestCaseMediaMixin from .models import Article class TestStandaloneAdmin(CropdusterTestCaseMediaMixin, AdminSelenosisTestCase): - root_urlconf = 'cropduster.tests.urls' + root_urlconf = 'tests.urls' @property def available_apps(self): @@ -36,8 +36,8 @@ def available_apps(self): 'generic_plus', 'cropduster', 'cropduster.standalone', - 'cropduster.tests', - 'cropduster.tests.test_standalone', + 'tests', + 'tests.standalone', 'ckeditor', 'selenosis', ] diff --git a/cropduster/tests/templates/404.html b/tests/templates/404.html similarity index 100% rename from cropduster/tests/templates/404.html rename to tests/templates/404.html diff --git a/cropduster/tests/templates/500.html b/tests/templates/500.html similarity index 100% rename from cropduster/tests/templates/500.html rename to tests/templates/500.html diff --git a/cropduster/tests/test_admin.py b/tests/test_admin.py similarity index 95% rename from cropduster/tests/test_admin.py rename to tests/test_admin.py index 7d7a8edc..289b17ed 100644 --- a/cropduster/tests/test_admin.py +++ b/tests/test_admin.py @@ -7,12 +7,12 @@ from cropduster.models import Image, Size from .helpers import CropdusterTestCaseMediaMixin -from .models import Article, Author, TestForOptionalSizes +from .models import Article, Author, OptionalSizes class TestAdmin(CropdusterTestCaseMediaMixin, AdminSelenosisTestCase): - root_urlconf = 'cropduster.tests.urls' + root_urlconf = 'tests.urls' @property def available_apps(self): @@ -26,8 +26,8 @@ def available_apps(self): 'django.contrib.admin', 'generic_plus', 'cropduster', - 'cropduster.tests', - 'cropduster.tests.test_standalone', + 'tests', + 'tests.standalone', 'selenosis', ] if self.has_grappelli: @@ -191,7 +191,7 @@ def test_changeform_multiple_images(self): self.assertEqual(len(article.alt_image.related_object.thumbs.all()), 1) def test_changeform_with_optional_sizes_small_image(self): - test_a = TestForOptionalSizes.objects.create(slug='a') + test_a = OptionalSizes.objects.create(slug='a') self.load_admin(test_a) @@ -212,13 +212,13 @@ def test_changeform_with_optional_sizes_small_image(self): self.save_form() - test_a = TestForOptionalSizes.objects.get(slug='a') + test_a = OptionalSizes.objects.get(slug='a') image = test_a.image.related_object num_thumbs = len(image.thumbs.all()) self.assertEqual(num_thumbs, 1, "Expected one thumb; instead got %d" % num_thumbs) def test_changeform_with_optional_sizes_large_image(self): - test_a = TestForOptionalSizes.objects.create(slug='a') + test_a = OptionalSizes.objects.create(slug='a') self.load_admin(test_a) # Upload and crop image @@ -238,7 +238,7 @@ def test_changeform_with_optional_sizes_large_image(self): self.save_form() - test_a = TestForOptionalSizes.objects.get(slug='a') + test_a = OptionalSizes.objects.get(slug='a') image = test_a.image.related_object num_thumbs = len(image.thumbs.all()) self.assertEqual(num_thumbs, 2, "Expected one thumb; instead got %d" % num_thumbs) diff --git a/cropduster/tests/test_gifsicle.py b/tests/test_gifsicle.py similarity index 92% rename from cropduster/tests/test_gifsicle.py rename to tests/test_gifsicle.py index f7090fd7..619a5f5d 100644 --- a/cropduster/tests/test_gifsicle.py +++ b/tests/test_gifsicle.py @@ -19,7 +19,7 @@ def _get_img(self, filename): return Image.open(os.path.join(self.TEST_IMG_DIR, filename)) def test_is_animated_gif(self): - from ..utils import is_animated_gif + from cropduster.utils import is_animated_gif yes = self._get_img('animated.gif') no = self._get_img('img.jpg') self.assertTrue(is_animated_gif(yes)) diff --git a/cropduster/tests/test_models.py b/tests/test_models.py similarity index 91% rename from cropduster/tests/test_models.py rename to tests/test_models.py index 8fd68ed0..f10279f5 100644 --- a/cropduster/tests/test_models.py +++ b/tests/test_models.py @@ -11,9 +11,9 @@ from .helpers import CropdusterTestCaseMediaMixin from .models import ( - Article, Author, TestForOptionalSizes, TestMultipleFieldsInheritanceChild, - TestReverseForeignRelA, TestReverseForeignRelB, TestReverseForeignRelC, - TestReverseForeignRelM2M, TestForOrphanedThumbs) + Article, Author, OptionalSizes, MultipleFieldsInheritanceChild, + ReverseForeignRelA, ReverseForeignRelB, ReverseForeignRelC, + ReverseForeignRelM2M, OrphanedThumbs) from cropduster.models import Size, Image, Thumb from cropduster.exceptions import CropDusterResizeException from cropduster import settings as cropduster_settings @@ -307,7 +307,7 @@ def test_change_image_on_model_with_two_images(self): self.assertEqual(article.alt_image.name, alt_image) def test_optional_sizes(self): - test_a = TestForOptionalSizes.objects.create(slug='a') + test_a = OptionalSizes.objects.create(slug='a') test_a.image = self.create_unique_image('img.jpg') test_a.save() test_a.image.generate_thumbs() @@ -317,7 +317,7 @@ def test_optional_sizes(self): num_thumbs = len(image.thumbs.all()) self.assertEqual(num_thumbs, 1, "Expected one thumb; instead got %d" % num_thumbs) - test_b = TestForOptionalSizes.objects.create(slug='b') + test_b = OptionalSizes.objects.create(slug='b') test_b.image = self.create_unique_image('img2.jpg') test_b.save() test_b.image.generate_thumbs() @@ -328,15 +328,15 @@ def test_optional_sizes(self): self.assertEqual(num_thumbs, 2, "Expected one thumb; instead got %d" % num_thumbs) def test_multiple_fields_with_inheritance(self): - child_fields = [f.name for f in TestMultipleFieldsInheritanceChild._meta.local_fields] + child_fields = [f.name for f in MultipleFieldsInheritanceChild._meta.local_fields] self.assertNotIn('image', child_fields, "Field 'image' from parent model should not be in the child model's local_fields") def test_orphaned_thumbs_after_delete(self): - test_a = TestForOrphanedThumbs.objects.create( + test_a = OrphanedThumbs.objects.create( slug='a', image=self.create_unique_image('img2.jpg')) test_a.image.generate_thumbs() - test_a = TestForOrphanedThumbs.objects.get(slug='a') + test_a = OrphanedThumbs.objects.get(slug='a') test_a.delete() num_thumbs = len(Thumb.objects.all()) @@ -346,22 +346,22 @@ def test_orphaned_thumbs_after_delete(self): class TestReverseForeignRelation(TestCase): def test_standard_manager(self): - c = TestReverseForeignRelC.objects.create(slug='c-1') + c = ReverseForeignRelC.objects.create(slug='c-1') for i in range(0, 3): - TestReverseForeignRelB.objects.create(slug="b-%d" % i, c=c) + ReverseForeignRelB.objects.create(slug="b-%d" % i, c=c) c.refresh_from_db() self.assertEqual(len(c.rel_b.all()), 3) def test_standard_prefetch_related(self): for i in range(0, 2): - m2m = TestReverseForeignRelM2M.objects.create(slug='standard-m2m-%d' % i) + m2m = ReverseForeignRelM2M.objects.create(slug='standard-m2m-%d' % i) for j in range(0, 2): - c = TestReverseForeignRelC.objects.create(slug='c-%d-%d' % (i, j)) + c = ReverseForeignRelC.objects.create(slug='c-%d-%d' % (i, j)) m2m.m2m.add(c) for k in range(0, 3): - TestReverseForeignRelB.objects.create(slug="b-%d-%d-%d" % (i, j, k), c=c) - objs = TestReverseForeignRelM2M.objects.prefetch_related('m2m__rel_b') + ReverseForeignRelB.objects.create(slug="b-%d-%d-%d" % (i, j, k), c=c) + objs = ReverseForeignRelM2M.objects.prefetch_related('m2m__rel_b') with self.assertNumQueries(3): for obj in objs: for m2m_obj in obj.m2m.all(): @@ -371,10 +371,10 @@ def test_manager_with_limit_choices_to(self): """ A ReverseForeignRelation with limit_choices_to applies the filter to the manager """ - c = TestReverseForeignRelC.objects.create(slug='c-1') + c = ReverseForeignRelC.objects.create(slug='c-1') for i in range(0, 3): - TestReverseForeignRelA.objects.create(slug="a-%d" % i, c=c, a_type="x") - TestReverseForeignRelA.objects.create(slug="a-4", c=c, a_type="y") + ReverseForeignRelA.objects.create(slug="a-%d" % i, c=c, a_type="x") + ReverseForeignRelA.objects.create(slug="a-4", c=c, a_type="y") c.refresh_from_db() a_len = len(c.rel_a.all()) @@ -384,16 +384,16 @@ def test_manager_with_limit_choices_to(self): def test_prefetch_related_with_limit_choices_to(self): for i in range(0, 2): - m2m = TestReverseForeignRelM2M.objects.create(slug='standard-m2m-%d' % i) + m2m = ReverseForeignRelM2M.objects.create(slug='standard-m2m-%d' % i) for j in range(0, 2): - c = TestReverseForeignRelC.objects.create(slug='c-%d-%d' % (i, j)) + c = ReverseForeignRelC.objects.create(slug='c-%d-%d' % (i, j)) m2m.m2m.add(c) for k in range(0, 3): - TestReverseForeignRelA.objects.create( + ReverseForeignRelA.objects.create( slug="a-%d-%d-%d" % (i, j, k), c=c, a_type='x') - TestReverseForeignRelA.objects.create( + ReverseForeignRelA.objects.create( slug="a-%d-%d-%d" % (i, j, 4), c=c, a_type='y') - objs = TestReverseForeignRelM2M.objects.prefetch_related('m2m__rel_a') + objs = ReverseForeignRelM2M.objects.prefetch_related('m2m__rel_a') with self.assertNumQueries(3): for obj in objs: for m2m_obj in obj.m2m.all(): diff --git a/cropduster/tests/test_resizing.py b/tests/test_resizing.py similarity index 100% rename from cropduster/tests/test_resizing.py rename to tests/test_resizing.py diff --git a/cropduster/tests/test_utils.py b/tests/test_utils.py similarity index 90% rename from cropduster/tests/test_utils.py rename to tests/test_utils.py index b9101b9f..6767600a 100644 --- a/cropduster/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,7 +21,7 @@ def test_that_test_work(self): self.assertEqual(True, True) def test_get_image_extension(self): - from ..utils import get_image_extension + from cropduster.utils import get_image_extension tmp_jpg_bad_ext_pdf = tempfile.NamedTemporaryFile(suffix='.pdf') tmp_png_bad_ext_jpg = tempfile.NamedTemporaryFile(suffix='.png') @@ -45,7 +45,7 @@ def test_get_image_extension(self): self.assertEqual(get_image_extension(img), ext) def test_is_transparent(self): - from ..utils import is_transparent + from cropduster.utils import is_transparent yes = self._get_img('transparent.png') no = self._get_img('img.png') @@ -53,7 +53,7 @@ def test_is_transparent(self): self.assertFalse(is_transparent(no)) def test_correct_colorspace(self): - from ..utils import correct_colorspace + from cropduster.utils import correct_colorspace img = self._get_img('cmyk.jpg') self.assertEqual(img.mode, 'CMYK') converted = correct_colorspace(img) @@ -61,7 +61,7 @@ def test_correct_colorspace(self): self.assertEqual(converted.mode, 'RGB') def test_is_animated_gif(self): - from ..utils import is_animated_gif + from cropduster.utils import is_animated_gif yes = self._get_img('animated.gif') no = self._get_img('img.jpg') self.assertTrue(is_animated_gif(yes)) @@ -72,7 +72,7 @@ class TestUtilsPaths(CropdusterTestCaseMediaMixin, test.TestCase): def test_get_upload_foldername(self): import uuid - from ..utils import get_upload_foldername + from cropduster.utils import get_upload_foldername path = random = uuid.uuid4().hex folder_path = get_upload_foldername('my img.jpg', upload_to=path) @@ -82,8 +82,8 @@ def test_get_upload_foldername(self): os.path.join(path, 'my_img-1')) def test_get_min_size(self): - from ..utils import get_min_size - from ..resizing import Size + from cropduster.utils import get_min_size + from cropduster.resizing import Size sizes = [ Size('a', w=200, h=200), diff --git a/cropduster/tests/test_views.py b/tests/test_views.py similarity index 100% rename from cropduster/tests/test_views.py rename to tests/test_views.py diff --git a/cropduster/tests/urls.py b/tests/urls.py similarity index 100% rename from cropduster/tests/urls.py rename to tests/urls.py diff --git a/cropduster/tests/utils.py b/tests/utils.py similarity index 100% rename from cropduster/tests/utils.py rename to tests/utils.py diff --git a/tox.ini b/tox.ini index 978007bd..fe10dbad 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,13 @@ [tox] envlist = py{27,36}-dj111{,-grp} - py36-dj20{,-grp} - py36-dj21 + py{36,37,38}-dj{22,30,31}{,-grp} +skipsdist = True [testenv] commands = - python runtests.py {posargs} --noinput + pytest {posargs} +usedevelop = True passenv = CI TRAVIS @@ -16,16 +17,22 @@ passenv = AWS_SECRET_ACCESS_KEY S3 deps = + pytest + pytest-cov + pytest-django selenium django-selenosis boto3==1.12.18 django-storages==1.9.1 - dj18: Django>=1.8,<1.8.99 dj111: Django>=1.11a1,<1.11.99 dj20: Django>=2.0.0,<2.0.99 - dj18-grp: django-grappelli==2.7.3 - dj111-grp: django-grappelli==2.10.2 - dj20-grp: django-grappelli==2.11.1 - dj21: https://github.com/django/django/archive/master.tar.gz + dj21: Django>=2.1.0,<2.1.99 + dj22: Django>=2.2a1,<2.2.99 + dj30: Django>=3.0,<3.1 + dj31: Django>=3.1a0,<3.2 + dj111-grp: django-grappelli==2.10.4 + dj22-grp: django-grappelli==2.13.4 + dj30-grp: django-grappelli==2.14.3 + dj31-grp: django-grappelli==2.14.3 lxml -e git+https://github.com/theatlantic/django-ckeditor.git@v4.5.7+atl.6.1#egg=django-ckeditor From b0717019a64ccfbd0913fcd8da78e86c6c1e84d6 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 9 Feb 2021 12:45:10 -0500 Subject: [PATCH 3/4] Replace travis with github actions --- .github/workflows/apt-get-update.sh | 19 +++++ .github/workflows/test.yml | 114 ++++++++++++++++++++++++++++ .gitignore | 1 + .travis.yml | 41 ---------- tox.ini | 44 ++++++++++- 5 files changed, 175 insertions(+), 44 deletions(-) create mode 100755 .github/workflows/apt-get-update.sh create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/apt-get-update.sh b/.github/workflows/apt-get-update.sh new file mode 100755 index 00000000..677aac43 --- /dev/null +++ b/.github/workflows/apt-get-update.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -eo pipefail + +aptget_update() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying apt-get update..." + echo "" + fi + output=`sudo apt-get update 2>&1` + echo "$output" + if [[ $output == *[WE]:\ * ]]; then + return 1 + fi +} + +aptget_update || aptget_update retry || aptget_update retry diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c307d506 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,114 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + python-version: ["2.7", "3.6", "3.7"] + django-version: ["1.11", "2.2"] + grappelli: ["0", "1"] + s3: ["0", "1"] + exclude: + - python-version: "2.7" + django-version: "2.2" + - grappelli: "1" + s3: "1" + - python-version: "3.7" + django-version: "1.11" + - python-version: "3.8" + django-version: "1.11" + - python-version: "3.6" + s3: "1" + include: + - python-version: "2.7" + python-bin: python2 + - python-version: "3.6" + python-bin: python3 + - python-version: "3.7" + python-bin: python3 + - s3: "1" + name-suffix: " with S3 storage" + - grappelli: "1" + name-suffix: " with grappelli" + - s3: "0" + grappelli: "0" + name-suffix: "" + + runs-on: ubuntu-latest + name: Django ${{ matrix.django-version }}${{ matrix.name-suffix }} (Python ${{ matrix.python-version }}) + + env: + DJANGO: ${{ matrix.django-version }} + GRAPPELLI: ${{ matrix.grappelli }} + S3: ${{ matrix.s3 }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup chromedriver + uses: nanasess/setup-chromedriver@v1.0.5 + + - name: Install system dependencies + run: | + sudo .github/workflows/apt-get-update.sh + sudo apt-get install -y exempi gifsicle + + - name: Install tox + run: | + ${{ matrix.python-bin }} -m pip install tox tox-gh-actions + + - name: Run tests + run: | + tox -- -v --selenosis-driver=chrome-headless || \ + tox -- -v --selenosis-driver=chrome-headless || \ + tox -- -v --selenosis-driver=chrome-headless + + - name: Upload junit xml + if: always() + uses: actions/upload-artifact@v2 + with: + name: junit-reports + path: reports/*.xml + + - name: Combine coverage + run: tox -e coverage-report + + - name: Upload coverage + run: tox -e codecov + env: + CODECOV_NAME: ${{ github.workflow }} + + report: + if: always() + needs: build + runs-on: ubuntu-latest + name: "Report Test Results" + steps: + - uses: actions/download-artifact@v2 + with: + name: junit-reports + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1.8 + if: always() + with: + files: ./*.xml + report_individual_runs: true + + success: + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.gitignore b/.gitignore index 034f6c41..d169890a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ ghostdriver.log test/media dist/ .coverage +/reports/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e70c07ef..00000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: python - -sudo: false - -addons: - apt: - packages: - - libexempi3 - - gifsicle - chrome: stable - -env: - global: - - PATH=$HOME/bin:$PATH - -matrix: - include: - - { python: 2.7, env: S3=0 TOXENV=py27-dj111 } - - { python: 2.7, env: S3=1 TOXENV=py27-dj111 } - - { python: 2.7, env: S3=0 TOXENV=py27-dj111-grp } - - { python: 3.6, env: S3=0 TOXENV=py36-dj111 } - - { python: 3.6, env: S3=0 TOXENV=py36-dj20 } - - { python: 3.6, env: S3=1 TOXENV=py36-dj20 } - - { python: 3.6, env: S3=0 TOXENV=py36-dj20-grp } - - { python: 3.6, env: S3=0 TOXENV=py36-dj21 } - allow_failures: - - env: S3=0 TOXENV=py36-dj21 - -cache: pip - -install: - - pip install tox - - mkdir -p ~/bin - - wget -N http://chromedriver.storage.googleapis.com/$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip -P ~/ - - unzip ~/chromedriver_linux64.zip -d ~/bin - - rm ~/chromedriver_linux64.zip - - chmod +x ~/bin/chromedriver - -script: - - travis_retry tox -- --selenosis-driver=chrome-headless - diff --git a/tox.ini b/tox.ini index fe10dbad..10a42b23 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,32 @@ [tox] envlist = - py{27,36}-dj111{,-grp} - py{36,37,38}-dj{22,30,31}{,-grp} + py{27,36}-dj111-{grp,nogrp} + py{36,37,38}-dj{22,30,31}-{grp,nogrp} skipsdist = True +[gh-actions] +python = + 2.7: py27 + 3.6: py36 + 3.7: py37 + 3.8: py38 + +[gh-actions:env] +DJANGO = + 1.11: dj111 + 2.2: dj22 + 3.0: dj30 + 3.1: dj31 +GRAPPELLI = + 0: nogrp + 1: grp + [testenv] commands = - pytest {posargs} + pytest --junitxml={toxinidir}/reports/test-{envname}.xml {posargs} usedevelop = True +setenv = + COVERAGE_FILE={toxworkdir}/coverage/.coverage.{envname} passenv = CI TRAVIS @@ -36,3 +55,22 @@ deps = dj31-grp: django-grappelli==2.14.3 lxml -e git+https://github.com/theatlantic/django-ckeditor.git@v4.5.7+atl.6.1#egg=django-ckeditor + +[testenv:coverage-report] +skip_install = true +deps = coverage +setenv=COVERAGE_FILE=.coverage +changedir = {toxworkdir}/coverage +commands = + coverage combine + coverage report + coverage xml + +[testenv:codecov] +skip_install = true +deps = codecov +depends = coverage-report +passenv = CODECOV_TOKEN +changedir = {toxinidir} +commands = + codecov --file {toxworkdir}/coverage/*.xml {posargs} From 680ee78647b816cc9c37f3d571cef970e2328862 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Wed, 10 Feb 2021 14:44:34 -0500 Subject: [PATCH 4/4] tests: Use timeout in assertImageColorEqual to allow for S3 latency --- tests/helpers.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index e88bdcea..79f1bb8c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -44,20 +44,29 @@ def assertImageColorEqual(self, element, image): scroll_top = -1 * self.selenium.execute_script( 'return document.body.getBoundingClientRect().top') tmp_file = tempfile.NamedTemporaryFile(suffix='.png') - if not self.selenium.save_screenshot(tmp_file.name): - raise Exception("Failed to save screenshot") pixel_density = self.selenium.execute_script('return window.devicePixelRatio') or 1 - im1 = PIL.Image.open(tmp_file.name).convert('RGB') x1 = int(round(element.location['x'] + (element.size['width'] // 2.0))) y1 = int(round(element.location['y'] - scroll_top + (element.size['height'] // 2.0))) - rgb1 = im1.getpixel((x1 * pixel_density, y1 * pixel_density)) - im2 = PIL.Image.open(os.path.join(os.path.dirname(__file__), 'data', image)).convert('RGB') - w, h = im2.size + + image_path = os.path.join(os.path.dirname(__file__), 'data', image) + ref_im = PIL.Image.open(image_path).convert('RGB') + w, h = ref_im.size x2, y2 = int(round(w // 2.0)), int(round(h // 2.0)) - rgb2 = im2.getpixel((x2, y2)) - if rgb1 != rgb2: - msg = "Colors differ: %s != %s" % (repr_rgb(rgb1), repr_rgb(rgb2)) - self.fail(msg) + ref_rgb = ref_im.getpixel((x2, y2)) + ref_im.close() + + def get_screenshot_rgb(): + if not self.selenium.save_screenshot(tmp_file.name): + raise Exception("Failed to save screenshot") + im = PIL.Image.open(tmp_file.name).convert('RGB') + rgb = im.getpixel((x1 * pixel_density, y1 * pixel_density)) + im.close() + return rgb + + self.wait_until( + lambda d: get_screenshot_rgb() == ref_rgb, + message=( + "Colors differ: %s != %s" % (repr_rgb(ref_rgb), repr_rgb(get_screenshot_rgb())))) def create_unique_image(self, image): image_uuid = uuid.uuid4().hex