diff --git a/.gitignore b/.gitignore index e61748b..6d63e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ db.sqlite3 /dist *.egg-info .eggs -.coverage \ No newline at end of file +.coverage +/.project +/.pydevproject +/vagrant/.vagrant +/.tox diff --git a/README.rst b/README.rst index 6da231e..d0e99ca 100644 --- a/README.rst +++ b/README.rst @@ -13,3 +13,19 @@ django-randomfields .. image:: https://badge.fury.io/py/django-randomfields.svg :target: http://badge.fury.io/py/django-randomfields + +============ +testing +============ + +cd vagrant/ +vagrant up +vagrant ssh +cd /vagrant/ + +# note we move TOX_WORK_DIR outside of the vagrant synced folder to increase performance +TOX_WORK_DIR=/tmp tox -vv + +-- or test one environment and skip the coverage report -- + +SUPPRESS_COVERAGE_REPORT="--suppress-coverage-report" TOX_WORK_DIR="/tmp" tox -vv -e py36-django-20 diff --git a/randomfields/checks.py b/randomfields/checks.py index 452fa0d..9f1e6fb 100644 --- a/randomfields/checks.py +++ b/randomfields/checks.py @@ -1,4 +1,6 @@ from django import VERSION as DJANGO_VERSION DJANGO_VERSION_17 = DJANGO_VERSION < (1, 8) and (1, 7) <= DJANGO_VERSION -DJANGO_VERSION_LT_18 = DJANGO_VERSION < (1, 8) \ No newline at end of file +DJANGO_VERSION_LT_18 = DJANGO_VERSION < (1, 8) +DJANGO_VERSION_LT_19 = DJANGO_VERSION < (1, 9) +DJANGO_VERSION_LT_20 = DJANGO_VERSION < (2, 0) diff --git a/randomfields/models/fields/integer/identifier.py b/randomfields/models/fields/integer/identifier.py index 8b13989..5682bbb 100644 --- a/randomfields/models/fields/integer/identifier.py +++ b/randomfields/models/fields/integer/identifier.py @@ -58,6 +58,14 @@ def __new__(cls, value, possibilities, lower_bound, upper_bound): return self + def __getnewargs__(self): + return ( + self.db_value, + self.possibilities, + self.lower_bound, + self.upper_bound, + ) + def __int__(self): return self.db_value diff --git a/randomfields/tests/models.py b/randomfields/tests/models.py index 3b12d4d..1c58ca3 100644 --- a/randomfields/tests/models.py +++ b/randomfields/tests/models.py @@ -33,29 +33,29 @@ class TestIdentifierData(models.Model): data = RandomIntegerIdentifierField() class TestIdentifierO2OValue(models.Model): - id = models.OneToOneField(TestIdentifierValue, primary_key=True, editable=True) + id = models.OneToOneField(TestIdentifierValue, on_delete=models.CASCADE, primary_key=True, editable=True) class TestIdentifierFKValue(models.Model): - data = models.ForeignKey(TestIdentifierValue) + data = models.ForeignKey(TestIdentifierValue, on_delete=models.CASCADE) class TestIdentifierM2MValue(models.Model): data = models.ManyToManyField(TestIdentifierValue, blank=True) class TestIdentifierAllValue(models.Model): - o2o = models.OneToOneField(TestIdentifierValue, related_name='+') - fk = models.ForeignKey(TestIdentifierValue, related_name='+') + o2o = models.OneToOneField(TestIdentifierValue, on_delete=models.CASCADE, related_name='+') + fk = models.ForeignKey(TestIdentifierValue, on_delete=models.CASCADE, related_name='+') m2m = models.ManyToManyField(TestIdentifierValue, blank=True, related_name=unique_related_name()) class TestIdentifierM2MO2OPKValue(models.Model): - id = models.OneToOneField(TestIdentifierValue, primary_key=True, editable=True, related_name=unique_related_name()) + id = models.OneToOneField(TestIdentifierValue, on_delete=models.CASCADE, primary_key=True, editable=True, related_name=unique_related_name()) m2m = models.ManyToManyField(TestIdentifierValue, blank=True, related_name=unique_related_name()) class TestIdentifierM2MO2OValue(models.Model): - o2o = models.OneToOneField(TestIdentifierValue, related_name='+') + o2o = models.OneToOneField(TestIdentifierValue, on_delete=models.CASCADE, related_name='+') m2m = models.ManyToManyField(TestIdentifierValue, blank=True, related_name=unique_related_name()) class TestIdentifierM2MFKValue(models.Model): - fk = models.ForeignKey(TestIdentifierValue, related_name='+') + fk = models.ForeignKey(TestIdentifierValue, on_delete=models.CASCADE, related_name='+') m2m = models.ManyToManyField(TestIdentifierValue, blank=True, related_name='+') class TestMaskedAttrDetection(models.Model): diff --git a/randomfields/tests/test_identifier_fk.py b/randomfields/tests/test_identifier_fk.py new file mode 100644 index 0000000..3908481 --- /dev/null +++ b/randomfields/tests/test_identifier_fk.py @@ -0,0 +1,45 @@ +import copy +import pickle + +from django.test import TestCase +from randomfields.checks import DJANGO_VERSION_17 +from randomfields.tests.models import TestIdentifierValue, TestIdentifierFKValue +from randomfields.models.fields.integer import IntegerIdentifier +from unittest import skipIf + + +@skipIf(DJANGO_VERSION_17, "Not supported on Django 17") +class IdentifierFKTests(TestCase): + def test_reverse_accessor(self): + obj = TestIdentifierValue.objects.create() + rel = TestIdentifierFKValue.objects.create(data=obj) + + instance = TestIdentifierFKValue.objects.get(pk=rel.pk) + + # ensure we don't raise a type error when accessing the reverse relation + # encountering the following exception when we do at the moment: + # TypeError: __new__() missing 3 required positional arguments: 'possibilities', 'lower_bound', and 'upper_bound' + # this issue began to occur in Django 2.0 and seems to be an issue + # with copy/deepcopy and the IntegerIdentifier class + instance.data + + def test_integer_identifier_copy(self): + value = IntegerIdentifier(1, 3, -1, 1) + value_copy = copy.copy(value) + self.assertEqual(value, value_copy) + + def test_integer_identifier_deepcopy(self): + value = IntegerIdentifier(1, 3, -1, 1) + value_deepcopy = copy.deepcopy(value) + self.assertEqual(value, value_deepcopy) + + def test_integer_identifier_pickle_dumps(self): + value = IntegerIdentifier(1, 3, -1, 1) + pickle.dumps(value) + + def test_integer_identifier_pickle_loads(self): + value = IntegerIdentifier(1, 3, -1, 1) + value_pickled = pickle.dumps(value) + value_unpickled = pickle.loads(value_pickled) + + self.assertEqual(value, value_unpickled) diff --git a/setup.py b/setup.py index 1fc20fb..a381251 100644 --- a/setup.py +++ b/setup.py @@ -6,18 +6,21 @@ class RunTestsCommand(SetuptoolsTestCommand): user_options = [ ('only=', 'o', 'Only run the specified tests'), - ('level=', 'l', 'Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output') + ('level=', 'l', 'Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output'), + ('suppress-coverage-report', None, 'Suppress coverage report'), ] def initialize_options(self): SetuptoolsTestCommand.initialize_options(self) self.test_suite = "override" self.only = "" self.level = "1" + self.suppress_coverage_report = None def finalize_options(self): SetuptoolsTestCommand.finalize_options(self) self.test_suite = None self.level = int(self.level) + self.suppress_coverage_report = self.suppress_coverage_report is not None def run(self): SetuptoolsTestCommand.run(self) @@ -29,7 +32,7 @@ def run_tests(self): import subprocess import sys import time - + owd = os.path.abspath(os.getcwd()) nwd = os.path.abspath(os.path.dirname(__file__)) os.chdir(nwd) @@ -37,8 +40,10 @@ def run_tests(self): if not tests: tests.extend([nwd, os.path.abspath('test_project')]) errno = coverage.cmdline.main(['run', os.path.abspath('test_project/manage.py'), 'test', '--verbosity=%d' % self.level] + tests) - coverage.cmdline.main(['report', '-m']) + if not self.suppress_coverage_report: + coverage.cmdline.main(['report', '-m']) + if None not in [os.getenv("TRAVIS", None), os.getenv("TRAVIS_JOB_ID", None), os.getenv("TRAVIS_BRANCH", None)]: env = os.environ.copy() env["PYTHONPATH"] = os.pathsep.join(sys.path) @@ -52,9 +57,9 @@ def run_tests(self): time.sleep(seconds) else: print("coveralls failed.") - + os.chdir(owd) - + raise SystemExit(errno) tests_require = ['coverage', 'beautifulsoup4', 'html5lib', 'coveralls'] @@ -63,7 +68,7 @@ def run_tests(self): setup( name = "django-randomfields", - version = "0.1.7", + version = "0.1.8", description = "Random fields for django models", url = "https://github.com/thenewguy/django-randomfields", cmdclass={'test': RunTestsCommand}, diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index d293122..f9c2f55 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -45,16 +45,28 @@ 'testadmin', ) -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -# 'django.middleware.security.SecurityMiddleware',# Raises 'ImportError: No module named security' on DJ1.7 (obviously, since added in 1.8) -) +from randomfields.checks import DJANGO_VERSION_LT_20 + +if DJANGO_VERSION_LT_20: + MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # 'django.middleware.security.SecurityMiddleware',# Raises 'ImportError: No module named security' on DJ1.7 (obviously, since added in 1.8) + ) +else: + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ) ROOT_URLCONF = 'test_project.urls' diff --git a/test_project/test_project/urls.py b/test_project/test_project/urls.py index 6ef5b27..4929bff 100644 --- a/test_project/test_project/urls.py +++ b/test_project/test_project/urls.py @@ -15,7 +15,13 @@ """ from django.conf.urls import include, url from django.contrib import admin +from randomfields.checks import DJANGO_VERSION_LT_19 + +def include_compat(included_urls): + if DJANGO_VERSION_LT_19: + included_urls = include(included_urls) + return included_urls urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', include_compat(admin.site.urls)), ] diff --git a/test_project/testadmin/tests.py b/test_project/testadmin/tests.py index 736d859..31eaf29 100644 --- a/test_project/testadmin/tests.py +++ b/test_project/testadmin/tests.py @@ -2,7 +2,12 @@ from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model -from django.core.urlresolvers import reverse +try: + from django.urls import reverse +except ImportError: + # backwards compatibility Django < 2.0 + # https://docs.djangoproject.com/en/2.0/releases/2.0/#features-removed-in-2-0 + from django.core.urlresolvers import reverse from django.test import TestCase from randomfields.checks import DJANGO_VERSION_17 from randomfields.models.fields import RandomCharField, RandomBigIntegerField @@ -79,11 +84,7 @@ def _test_identifier_selected_in_html(self, url, value): options = soup.find_all('option', value=value) self.assertTrue(options, "No options found with value '%s'. All options: %s" % (value, soup.find_all('option'))) for option in options: - try: - selected = option["selected"] - except KeyError: - selected = "Key error. Option html: %s" % option - self.assertEqual(selected, "selected") + self.assertTrue(option.has_attr("selected"), "Key error. Option not selected! Option html: %s" % option) def test_identifier_o2o_html(self): obj = TestIdentifierValue.objects.create() diff --git a/tox.ini b/tox.ini index d406bac..47cc0b6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,11 @@ [tox] args_are_paths = false envlist = - {py27,py34}-django-{17,18,19,110,master} + {py27,py34}-django-{17,18,19,110,111} {py33}-django-{17,18} - {py35}-django-{19,110,master} + {py34}-django-{20} + {py35}-django-{19,110,111,20,master} + {py36}-django-{111,20,master} [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH @@ -12,6 +14,7 @@ basepython = py33: python3.3 py34: python3.4 py35: python3.5 + py36: python3.6 usedevelop = true pip_pre = true deps = @@ -20,6 +23,8 @@ deps = django-18: Django>=1.8,<1.9 django-19: Django>=1.9,<1.10 django-110: Django>=1.10,<1.11 + django-111: Django>=1.11,<2 + django-20: Django>=2.0,<2.1 django-master: https://github.com/django/django/archive/master.tar.gz commands = python --version diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile new file mode 100644 index 0000000..268fdc7 --- /dev/null +++ b/vagrant/Vagrantfile @@ -0,0 +1,62 @@ +host = RbConfig::CONFIG['host_os'] +HOST_IS_MAC = host =~ /darwin/ +HOST_IS_LINUX = host =~ /linux/ +HOST_IS_WINDOWS = host =~ /mswin|mingw|cygwin/ + +if HOST_IS_MAC + HOST_MEM = `sysctl -n hw.memsize`.to_i / 1024 / 1024 + HOST_CPUS = `sysctl -n hw.ncpu`.to_i +elsif HOST_IS_LINUX + HOST_MEM = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 + HOST_CPUS = `nproc`.to_i +elsif HOST_IS_WINDOWS + HOST_MEM = `wmic computersystem Get TotalPhysicalMemory`.split[1].to_i / 1024 / 1024 + HOST_CPUS = `wmic cpu Get NumberOfCores`.split[1].to_i +end + +Vagrant.configure("2") do |config| + config.vm.boot_timeout = 600 + config.vm.box = "bento/ubuntu-14.04" + config.vm.box_url = "https://vagrantcloud.com/bento/boxes/ubuntu-14.04/versions/201802.02.0/providers/virtualbox.box" + + cpus = HOST_CPUS + if 7000 < HOST_MEM + mem = 4096 + else + mem = 2048 + end + + config.vm.provider "virtualbox" do |v| + v.name = "django-randomfields" + v.memory = mem + v.cpus = cpus + if cpus > 1 + v.customize ["modifyvm", :id, "--ioapic", "on"] + end + v.customize ["modifyvm", :id, "--cpuexecutioncap", "75"] + end + + + config.vm.provision :shell, path: "provision.sh" + config.vm.synced_folder ".", "/vagrant", disabled: true + config.vm.synced_folder "../", "/vagrant" + + + # forward ports as listed in vagrant/vagrant/rebuild.sh + # + ## + ## + ## THIS ALLOWS THE WEB BROWSER ON THE HOST MACHINE + ## TO COMMUNICATE VIA '127.0.0.1' or 'localhost' + ## i.e. `curl -i http://127.0.0.1:8080/` + ## + ## THIS ALSO ALLOWS NETWORKED MACHINES TO ACCESS FORWARDED + ## PORTS VIA THE HOST + ## i.e. `curl -i http://host-ip-or-fqdn:8080/ + ## + ## + + # responder http (use 8080 to avoid sudo requirement) + config.vm.network "forwarded_port", guest: 80, host: 8080 + +end diff --git a/vagrant/provision.sh b/vagrant/provision.sh new file mode 100644 index 0000000..0f4650e --- /dev/null +++ b/vagrant/provision.sh @@ -0,0 +1,18 @@ +set -o errexit +set -o pipefail +set -o nounset +shopt -s failglob +set -o xtrace + +export DEBIAN_FRONTEND=noninteractive + +add-apt-repository ppa:deadsnakes/ppa + +apt-get update + +apt-get install -y git python3.5 python3.6 + +curl -O https://bootstrap.pypa.io/get-pip.py +python get-pip.py + +pip install tox