test_writer.py 9.66 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3
from __future__ import unicode_literals

4
import datetime
5
import os
6
import tokenize
7
import unittest
8

9
from django.core.validators import RegexValidator, EmailValidator
Andrew Godwin's avatar
Andrew Godwin committed
10
from django.db import models, migrations
11
from django.db.migrations.writer import MigrationWriter, SettingsReference
12
from django.test import TestCase
13
from django.conf import settings
14
from django.utils import datetime_safe, six
15
from django.utils.deconstruct import deconstructible
16
from django.utils.translation import ugettext_lazy as _
17
from django.utils.timezone import get_default_timezone
18 19


20 21 22 23 24 25
class TestModel1(object):
    def upload_to(self):
        return "somewhere dynamic"
    thing = models.FileField(upload_to=upload_to)


26
class WriterTests(TestCase):
27 28 29 30
    """
    Tests the migration writer (makes migration files from Migration instances)
    """

31
    def safe_exec(self, string, value=None):
32 33
        l = {}
        try:
34 35 36 37 38 39
            exec(string, globals(), l)
        except Exception as e:
            if value:
                self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e))
            else:
                self.fail("Could not exec %r: %s" % (string.strip(), e))
40 41
        return l

42
    def serialize_round_trip(self, value):
43
        string, imports = MigrationWriter.serialize(value)
44 45 46 47
        return self.safe_exec("%s\ntest_value_result = %s" % ("\n".join(imports), string), value)['test_value_result']

    def assertSerializedEqual(self, value):
        self.assertEqual(self.serialize_round_trip(value), value)
48

49 50
    def assertSerializedResultEqual(self, value, target):
        self.assertEqual(MigrationWriter.serialize(value), target)
51 52 53 54 55 56 57

    def assertSerializedFieldEqual(self, value):
        new_value = self.serialize_round_trip(value)
        self.assertEqual(value.__class__, new_value.__class__)
        self.assertEqual(value.max_length, new_value.max_length)
        self.assertEqual(value.null, new_value.null)
        self.assertEqual(value.unique, new_value.unique)
58 59 60 61 62 63 64 65 66 67

    def test_serialize(self):
        """
        Tests various different forms of the serializer.
        This does not care about formatting, just that the parsed result is
        correct, so we always exec() the result and check that.
        """
        # Basic values
        self.assertSerializedEqual(1)
        self.assertSerializedEqual(None)
68
        self.assertSerializedEqual(b"foobar")
69 70
        string, imports = MigrationWriter.serialize(b"foobar")
        self.assertEqual(string, "b'foobar'")
71
        self.assertSerializedEqual("föobár")
72 73
        string, imports = MigrationWriter.serialize("foobar")
        self.assertEqual(string, "'foobar'")
74 75 76 77
        self.assertSerializedEqual({1: 2})
        self.assertSerializedEqual(["a", 2, True, None])
        self.assertSerializedEqual(set([2, 3, "eighty"]))
        self.assertSerializedEqual({"lalalala": ["yeah", "no", "maybe"]})
78
        self.assertSerializedEqual(_('Hello'))
79 80 81
        # Functions
        with six.assertRaisesRegex(self, ValueError, 'Cannot serialize function: lambda'):
            self.assertSerializedEqual(lambda x: 42)
82 83 84 85
        self.assertSerializedEqual(models.SET_NULL)
        string, imports = MigrationWriter.serialize(models.SET(42))
        self.assertEqual(string, 'models.SET(42)')
        self.serialize_round_trip(models.SET(42))
86 87 88
        # Datetime stuff
        self.assertSerializedEqual(datetime.datetime.utcnow())
        self.assertSerializedEqual(datetime.datetime.utcnow)
89 90
        self.assertSerializedEqual(datetime.datetime.today())
        self.assertSerializedEqual(datetime.datetime.today)
91 92
        self.assertSerializedEqual(datetime.date.today())
        self.assertSerializedEqual(datetime.date.today)
93 94
        with self.assertRaises(ValueError):
            self.assertSerializedEqual(datetime.datetime(2012, 1, 1, 1, 1, tzinfo=get_default_timezone()))
95 96 97 98 99 100 101 102
        safe_date = datetime_safe.date(2014, 3, 31)
        string, imports = MigrationWriter.serialize(safe_date)
        self.assertEqual(string, repr(datetime.date(2014, 3, 31)))
        self.assertEqual(imports, {'import datetime'})
        safe_datetime = datetime_safe.datetime(2014, 3, 31, 16, 4, 31)
        string, imports = MigrationWriter.serialize(safe_datetime)
        self.assertEqual(string, repr(datetime.datetime(2014, 3, 31, 16, 4, 31)))
        self.assertEqual(imports, {'import datetime'})
103 104 105
        # Classes
        validator = RegexValidator(message="hello")
        string, imports = MigrationWriter.serialize(validator)
106
        self.assertEqual(string, "django.core.validators.RegexValidator(message='hello')")
107 108 109
        self.serialize_round_trip(validator)
        validator = EmailValidator(message="hello")  # Test with a subclass.
        string, imports = MigrationWriter.serialize(validator)
110
        self.assertEqual(string, "django.core.validators.EmailValidator(message='hello')")
111 112 113
        self.serialize_round_trip(validator)
        validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello")
        string, imports = MigrationWriter.serialize(validator)
114
        self.assertEqual(string, "custom.EmailValidator(message='hello')")
115 116 117
        # Django fields
        self.assertSerializedFieldEqual(models.CharField(max_length=255))
        self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))
118 119 120 121 122 123 124 125 126
        # Setting references
        self.assertSerializedEqual(SettingsReference(settings.AUTH_USER_MODEL, "AUTH_USER_MODEL"))
        self.assertSerializedResultEqual(
            SettingsReference("someapp.model", "AUTH_USER_MODEL"),
            (
                "settings.AUTH_USER_MODEL",
                set(["from django.conf import settings"]),
            )
        )
127
        self.assertSerializedResultEqual(
128
            ((x, x * x) for x in range(3)),
129 130 131 132 133
            (
                "((0, 0), (1, 1), (2, 4))",
                set(),
            )
        )
134

135 136 137 138 139 140
    def test_serialize_empty_nonempty_tuple(self):
        """
        Ticket #22679: makemigrations generates invalid code for (an empty
        tuple) default_permissions = ()
        """
        empty_tuple = ()
141
        one_item_tuple = ('a',)
142 143 144 145 146
        many_items_tuple = ('a', 'b', 'c')
        self.assertSerializedEqual(empty_tuple)
        self.assertSerializedEqual(one_item_tuple)
        self.assertSerializedEqual(many_items_tuple)

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    @unittest.skipUnless(six.PY2, "Only applies on Python 2")
    def test_serialize_direct_function_reference(self):
        """
        Ticket #22436: You cannot use a function straight from its body
        (e.g. define the method and use it in the same body)
        """
        with self.assertRaises(ValueError):
            self.serialize_round_trip(TestModel1.thing)

    def test_serialize_local_function_reference(self):
        """
        Neither py2 or py3 can serialize a reference in a local scope.
        """
        class TestModel2(object):
            def upload_to(self):
                return "somewhere dynamic"
            thing = models.FileField(upload_to=upload_to)
        with self.assertRaises(ValueError):
            self.serialize_round_trip(TestModel2.thing)

167 168 169 170
    def test_simple_migration(self):
        """
        Tests serializing a simple migration.
        """
171 172 173 174 175 176 177 178 179 180
        fields = {
            'charfield': models.DateTimeField(default=datetime.datetime.utcnow),
            'datetimefield': models.DateTimeField(default=datetime.datetime.utcnow),
        }

        options = {
            'verbose_name': 'My model',
            'verbose_name_plural': 'My models',
        }

181
        migration = type(str("Migration"), (migrations.Migration,), {
182
            "operations": [
183 184 185
                migrations.CreateModel("MyModel", tuple(fields.items()), options, (models.Model,)),
                migrations.CreateModel("MyModel2", tuple(fields.items()), bases=(models.Model,)),
                migrations.CreateModel(name="MyModel3", fields=tuple(fields.items()), options=options, bases=(models.Model,)),
186
                migrations.DeleteModel("MyModel"),
187
                migrations.AddField("OtherModel", "datetimefield", fields["datetimefield"]),
188 189 190 191 192
            ],
            "dependencies": [("testapp", "some_other_one")],
        })
        writer = MigrationWriter(migration)
        output = writer.as_string()
193 194 195 196 197 198
        # It should NOT be unicode.
        self.assertIsInstance(output, six.binary_type, "Migration as_string returned unicode")
        # We don't test the output formatting - that's too fragile.
        # Just make sure it runs for now, and that things look alright.
        result = self.safe_exec(output)
        self.assertIn("Migration", result)
199 200 201
        # In order to preserve compatibility with Python 3.2 unicode literals
        # prefix shouldn't be added to strings.
        tokens = tokenize.generate_tokens(six.StringIO(str(output)).readline)
202
        for token_type, token_source, (srow, scol), __, line in tokens:
203 204 205 206 207 208 209
            if token_type == tokenize.STRING:
                self.assertFalse(
                    token_source.startswith('u'),
                    "Unicode literal prefix found at %d:%d: %r" % (
                        srow, scol, line.strip()
                    )
                )
210 211 212 213 214 215 216 217 218

    def test_migration_path(self):
        test_apps = [
            'migrations.migrations_test_apps.normal',
            'migrations.migrations_test_apps.with_package_model',
        ]

        base_dir = os.path.dirname(os.path.dirname(__file__))

219
        for app in test_apps:
220
            with self.modify_settings(INSTALLED_APPS={'append': app}):
221 222 223 224
                migration = migrations.Migration('0001_initial', app.split('.')[-1])
                expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py']))
                writer = MigrationWriter(migration)
                self.assertEqual(writer.path, expected_path)