diff --git a/requirements.txt b/requirements.txt index 32938ab23..a20c4b217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ Django>=1.4.11 pytest-django==2.8.0 pytest==2.6.4 pytest-cov==1.6 +pytest-bench==0.3.0 flake8==2.2.2 # Optional packages diff --git a/runtests.py b/runtests.py index 0008bfae5..c8236ff25 100755 --- a/runtests.py +++ b/runtests.py @@ -8,7 +8,7 @@ import subprocess PYTEST_ARGS = { - 'default': ['tests', '--tb=short'], + 'default': ['tests', '--tb=short', '--bench'], 'fast': ['tests', '--tb=short', '-q'], } diff --git a/tests/fields.py b/tests/fields.py new file mode 100644 index 000000000..dafb21fce --- /dev/null +++ b/tests/fields.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 +from django.db import models + + +class CustomField(models.Field): + """ + A custom model field simply for testing purposes. + """ + pass diff --git a/tests/models.py b/tests/models.py index 456b0a0bb..b5ac1b3cf 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ +from .fields import CustomField class RESTFrameworkModel(models.Model): @@ -68,3 +69,119 @@ class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') + + +class RegularFieldsModel(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + custom_field = CustomField() + + def method(self): + return 'method' + + +class RegularFieldsModel2(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + + def method(self): + return 'method' + + +class RegularFieldsAndFKModel(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + custom_field = CustomField() + fk = models.ForeignKey(RegularFieldsModel) + + def method(self): + return 'method' + + +class RegularFieldsAndFKModel2(models.Model): + """ + A model class for testing regular flat fields. + """ + auto_field = models.AutoField(primary_key=True) + big_integer_field = models.BigIntegerField() + boolean_field = models.BooleanField(default=False) + char_field = models.CharField(max_length=100) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) + date_field = models.DateField() + datetime_field = models.DateTimeField() + decimal_field = models.DecimalField(max_digits=3, decimal_places=1) + email_field = models.EmailField(max_length=100) + float_field = models.FloatField() + integer_field = models.IntegerField() + null_boolean_field = models.NullBooleanField() + positive_integer_field = models.PositiveIntegerField() + positive_small_integer_field = models.PositiveSmallIntegerField() + slug_field = models.SlugField(max_length=100) + small_integer_field = models.SmallIntegerField() + text_field = models.TextField() + time_field = models.TimeField() + url_field = models.URLField(max_length=100) + fk = models.ForeignKey(RegularFieldsModel2) + + def method(self): + return 'method' diff --git a/tests/test_full_stack_benchmarks.py b/tests/test_full_stack_benchmarks.py new file mode 100644 index 000000000..0cef52612 --- /dev/null +++ b/tests/test_full_stack_benchmarks.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# coding: utf-8 +from decimal import Decimal +from datetime import datetime +from django.utils import unittest + +from pytest import mark + +from rest_framework import viewsets, serializers +from rest_framework.filters import DjangoFilterBackend +from rest_framework.reverse import reverse +from rest_framework.routers import DefaultRouter +from rest_framework.test import APITransactionTestCase +from tests.models import RegularFieldsAndFKModel2, RegularFieldsModel2 + + +data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': str(datetime.now().date()), + 'datetime_field': str(datetime.now()), + 'decimal_field': str(Decimal('1.5')), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': str(datetime.now().time()), + 'url_field': 'https://overtherainbow.com' +} + +data_list = [data for _ in range(100)] + + +class TestSerializer(serializers.ModelSerializer): + class Meta: + model = RegularFieldsModel2 + fields = list(data.keys()) + ['method'] + + +class TestNestedSerializer(serializers.ModelSerializer): + fk = TestSerializer() + + class Meta: + model = RegularFieldsAndFKModel2 + fields = list(data.keys()) + ['fk', 'method'] + + def create(self, validated_data): + fk = RegularFieldsModel2.objects.create(**validated_data['fk']) + validated_data['fk'] = fk + + return RegularFieldsAndFKModel2.objects.create(**validated_data) + + def update(self, instance, validated_data): + fk_data = validated_data.pop('fk') + fk_pk = fk_data.pop('auto_field', None) + + if not fk_pk: + fk_pk = instance.fk_id + + RegularFieldsModel2.objects.filter(pk=fk_pk).update(**fk_data) + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.fk_id = fk_pk + instance.save() + + return instance + + +class RegularFieldsAndFKViewSet(viewsets.ModelViewSet): + queryset = RegularFieldsAndFKModel2.objects.all() + serializer_class = TestNestedSerializer + + +class FilteredRegularFieldsAndFKViewSet(RegularFieldsAndFKViewSet): + filter_backends = (DjangoFilterBackend,) + filter_fields = ('big_integer_field',) + + +router = DefaultRouter() +router.register('benchmark', RegularFieldsAndFKViewSet, base_name='benchmark') +router.register('benchmark2', FilteredRegularFieldsAndFKViewSet, base_name='benchmark-filter') + +urlpatterns = router.urls + + +class FullStackBenchmarksTestCase(APITransactionTestCase): + urls = 'tests.test_full_stack_benchmarks' + + def setUp(self): + RegularFieldsModel2.objects.bulk_create([RegularFieldsModel2(**d) for d in data_list]) + + RegularFieldsAndFKModel2.objects.bulk_create( + [RegularFieldsAndFKModel2(fk=o, **data) for o in RegularFieldsModel2.objects.all()]) + + self.first_pk = RegularFieldsAndFKModel2.objects.only('pk').first().pk + self.last_pk = RegularFieldsAndFKModel2.objects.only('pk').last().pk + + @mark.bench('viewsets.ModelViewSet.list', iterations=1000) + def test_viewset_list(self): + url = reverse('benchmark-list') + + response = self.client.get(url) + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.retrieve', iterations=10000) + def test_viewset_retrieve(self): + url = reverse('benchmark-detail', args=[self.first_pk]) + + response = self.client.get(url) + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.create', iterations=1000) + def test_viewset_create(self): + url = reverse('benchmark-list') + + new_data = data.copy() + new_data['fk'] = data.copy() + + response = self.client.post(url, data=new_data, format='json') + assert response.status_code == 201, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.update', iterations=1000) + def test_viewset_update(self): + url = reverse('benchmark-detail', args=[self.first_pk]) + + new_data = data.copy() + new_fk = RegularFieldsModel2.objects.create(**data) + new_fk_data = data.copy() + new_fk_data['auto_field'] = new_fk.pk + new_data['fk'] = new_fk_data + + response = self.client.put(url, data=new_data, format='json') + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.partial_update', iterations=1000) + def test_viewset_partial_update(self): + url = reverse('benchmark-detail', args=[self.first_pk]) + + new_fk = RegularFieldsModel2.objects.create(**data) + new_fk_data = data.copy() + new_fk_data['auto_field'] = new_fk.pk + new_data = {'fk': new_fk_data} + + response = self.client.patch(url, data=new_data) + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.destroy', iterations=10000) + def test_viewset_delete(self): + new_fk = RegularFieldsModel2.objects.create(**data) + new_obj = RegularFieldsAndFKModel2.objects.create(fk=new_fk, **data) + + url = reverse('benchmark-detail', args=[new_obj.pk]) + + response = self.client.delete(url) + assert response.status_code == 204, (response.rendered_content, url) + + +class FullStackFilteredBenchmarksTestCase(APITransactionTestCase): + urls = 'tests.test_full_stack_benchmarks' + + def setUp(self): + RegularFieldsModel2.objects.bulk_create([RegularFieldsModel2(**d) for d in data_list]) + + RegularFieldsAndFKModel2.objects.bulk_create( + [RegularFieldsAndFKModel2(fk=o, **data) for o in RegularFieldsModel2.objects.all()]) + + self.first_pk = RegularFieldsAndFKModel2.objects.only('pk').first().pk + self.last_pk = RegularFieldsAndFKModel2.objects.only('pk').last().pk + + @mark.bench('viewsets.ModelViewSet.list', iterations=1000) + def test_viewset_list(self): + url = reverse('benchmark-filter-list') + + response = self.client.get(url, data={'big_integer_field': 100000}) + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.retrieve', iterations=10000) + def test_viewset_retrieve(self): + url = reverse('benchmark-filter-detail', args=[self.first_pk]) + + response = self.client.get(url, data={'big_integer_field': 100000}) + assert response.status_code == 200, (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.list', iterations=1000) + def test_viewset_list_nothing(self): + url = reverse('benchmark-filter-list') + + response = self.client.get(url, data={'big_integer_field': 100001}) + assert response.rendered_content == '[]', (response.rendered_content, url) + + @mark.bench('viewsets.ModelViewSet.retrieve', iterations=10000) + @unittest.skip('pytest-bench cannot benchmark operations that raise exceptions') + def test_viewset_retrieve_nothing(self): + url = reverse('benchmark-filter-detail', args=[self.first_pk]) + + response = self.client.get(url, data={'big_integer_field': 100001}) + assert response.status_code == 404, (response.rendered_content, url) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 247b309a1..37b578840 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -6,13 +6,16 @@ These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ from __future__ import unicode_literals + from django.core.exceptions import ImproperlyConfigured from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator from django.db import models from django.test import TestCase from django.utils import six + from rest_framework import serializers from rest_framework.compat import unicode_repr +from .models import RegularFieldsModel def dedent(blocktext): @@ -22,46 +25,11 @@ def dedent(blocktext): # Tests for regular field mappings. # --------------------------------- -class CustomField(models.Field): - """ - A custom model field simply for testing purposes. - """ - pass - class OneFieldModel(models.Model): char_field = models.CharField(max_length=100) -class RegularFieldsModel(models.Model): - """ - A model class for testing regular flat fields. - """ - auto_field = models.AutoField(primary_key=True) - big_integer_field = models.BigIntegerField() - boolean_field = models.BooleanField(default=False) - char_field = models.CharField(max_length=100) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) - date_field = models.DateField() - datetime_field = models.DateTimeField() - decimal_field = models.DecimalField(max_digits=3, decimal_places=1) - email_field = models.EmailField(max_length=100) - float_field = models.FloatField() - integer_field = models.IntegerField() - null_boolean_field = models.NullBooleanField() - positive_integer_field = models.PositiveIntegerField() - positive_small_integer_field = models.PositiveSmallIntegerField() - slug_field = models.SlugField(max_length=100) - small_integer_field = models.SmallIntegerField() - text_field = models.TextField() - time_field = models.TimeField() - url_field = models.URLField(max_length=100) - custom_field = CustomField() - - def method(self): - return 'method' - - COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')) @@ -125,7 +93,7 @@ class TestRegularFieldMappings(TestCase): text_field = CharField(style={'base_template': 'textarea.html'}) time_field = TimeField() url_field = URLField(max_length=100) - custom_field = ModelField(model_field=) + custom_field = ModelField(model_field=) """) self.assertEqual(unicode_repr(TestSerializer()), expected) diff --git a/tests/test_renderers_benchmarks.py b/tests/test_renderers_benchmarks.py new file mode 100644 index 000000000..f8ccd5eae --- /dev/null +++ b/tests/test_renderers_benchmarks.py @@ -0,0 +1,33 @@ +from decimal import Decimal +from datetime import datetime + +from pytest import mark + +from rest_framework import renderers + + +data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': datetime.now().date(), + 'datetime_field': datetime.now(), + 'decimal_field': Decimal('1.5'), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': datetime.now().time(), + 'url_field': 'https://overtherainbow.com' +} + + +@mark.bench('renderers.JSONRenderer.render', iterations=1000000) +def test_json_renderer(): + renderer = renderers.JSONRenderer() + renderer.render(data) diff --git a/tests/test_serializers_benchmarks.py b/tests/test_serializers_benchmarks.py new file mode 100644 index 000000000..a235fe74c --- /dev/null +++ b/tests/test_serializers_benchmarks.py @@ -0,0 +1,166 @@ +from decimal import Decimal + +from pytest import mark + +from datetime import datetime + +from rest_framework import serializers +from .models import RegularFieldsModel, RegularFieldsAndFKModel + + +data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': datetime.now().date(), + 'datetime_field': datetime.now(), + 'decimal_field': Decimal('1.5'), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': datetime.now().time(), + 'url_field': 'https://overtherainbow.com' +} + +nested_data = { + 'big_integer_field': 100000, + 'char_field': 'a', + 'comma_separated_integer_field': '1,2', + 'date_field': datetime.now().date(), + 'datetime_field': datetime.now(), + 'decimal_field': Decimal('1.5'), + 'email_field': 'somewhere@overtherainbow.com', + 'float_field': 0.443, + 'integer_field': 55, + 'null_boolean_field': True, + 'positive_integer_field': 1, + 'positive_small_integer_field': 1, + 'slug_field': 'slug-friendly-text', + 'small_integer_field': 1, + 'text_field': 'lorem ipsum', + 'time_field': datetime.now().time(), + 'url_field': 'https://overtherainbow.com', + 'fk': data +} + +data_list = [data for _ in range(100)] + +data_list_with_nesting = [nested_data for _ in range(100)] + +instances_list = [RegularFieldsModel(**data) for _ in range(100)] + +instances_with_nesting = [RegularFieldsAndFKModel(fk=nested_instance, **data) for nested_instance in instances_list] + + +class TestSerializer(serializers.ModelSerializer): + class Meta: + model = RegularFieldsModel + fields = list(data.keys()) + ['method'] + + +class TestNestedSerializer(serializers.ModelSerializer): + fk = TestSerializer() + + class Meta: + model = RegularFieldsAndFKModel + fields = list(data.keys()) + ['method', 'fk'] + + +@mark.bench('serializers.ModelSerializer.get_fields', iterations=1000) +def test_get_fields(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.get_fields() + + +@mark.bench('serializers.ModelSerializer.get_fields', iterations=1000) +def test_get_fields_twice(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.get_fields() + assert serializer.get_fields() + + +@mark.bench('serializers.ModelSerializer.to_representation', iterations=1000) +def test_object_serialization(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation', iterations=1000) +def test_nested_object_serialization(): + nested_instance = RegularFieldsModel(**data) + instance = RegularFieldsAndFKModel(fk=nested_instance, **data) + serializer = TestSerializer(instance=instance) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ListSerializer.to_representation', iterations=1000) +def test_object_list_serialization(): + serializer = TestSerializer(instance=instances_list, many=True) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ListSerializer.to_representation', iterations=1000) +def test_nested_object_list_serialization(): + serializer = TestSerializer(instance=instances_with_nesting, many=True) + + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation', iterations=10000) +def test_object_serialization_with_partial_update(): + instance = RegularFieldsModel(**data) + serializer = TestSerializer(instance=instance, data={'char_field': 'b'}, partial=True) + + assert serializer.is_valid(), serializer.errors + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_representation', iterations=10000) +def test_object_serialization_with_update(): + instance = RegularFieldsModel(**data) + new_data = data.copy() + new_data['char_field'] = 'b' + serializer = TestSerializer(instance=instance, data=new_data) + + assert serializer.is_valid(), serializer.errors + assert serializer.data, serializer.errors + + +@mark.bench('serializers.ModelSerializer.to_internal_value', iterations=1000) +def test_object_deserialization(): + serializer = TestSerializer(data=data) + + assert serializer.is_valid(), serializer.errors + + +@mark.bench('serializers.ListSerializer.to_internal_value', iterations=1000) +def test_object_list_deserialization(): + serializer = TestSerializer(data=data_list, many=True) + + assert serializer.is_valid(), serializer.errors + + +@mark.bench('serializers.ListSerializer.to_internal_value', iterations=1000) +def test_nested_object_list_deserialization(): + serializer = TestSerializer(data=data_list_with_nesting, many=True) + + assert serializer.is_valid(), serializer.errors + + +@mark.bench('serializers.ModelSerializer.__init__', iterations=10000) +def test_serializer_initialization(): + TestSerializer(data=data) diff --git a/tox.ini b/tox.ini index eda92c19b..ddfcdd7d0 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ deps = {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 pytest-django==2.8.0 + pytest-bench==0.3.0 django-filter==0.9.2 defusedxml==0.3 markdown>=2.1.0