diff --git a/tests/functional/test_makemigrations_command.py b/tests/functional/test_makemigrations_command.py new file mode 100644 index 0000000..a7b6bcb --- /dev/null +++ b/tests/functional/test_makemigrations_command.py @@ -0,0 +1,195 @@ +import io +import os +import shutil +import sys +import tempfile +from contextlib import contextmanager +from importlib import import_module + +if sys.version_info >= (3, 3): + import unittest.mock as mock +else: + import mock + +from django.apps import apps +from django.conf import settings +from django.core.management import call_command +from django.test import TransactionTestCase +from django.test.utils import extend_sys_path, override_settings +from django.utils.module_loading import module_dir + +MAKEMIGRATIONS_INSTALLED_APPS = settings.INSTALLED_APPS + [ + "tests.test_project.makemigrations_correct_migration_missing", + "tests.test_project.makemigrations_backward_incompatible_migration_missing", +] + + +@override_settings(INSTALLED_APPS=MAKEMIGRATIONS_INSTALLED_APPS) +class BaseMakeMigrationsTestCase(TransactionTestCase): + @contextmanager + def temporary_migration_module(self, app_label="migrations", module=None): + """ + Shamelessly copied from Django. + See django.tests.migrations.test_base.MigrationTestBase.temporary_migration_module + + Allows testing management commands in a temporary migrations module. + + Wrap all invocations to makemigrations and squashmigrations with this + context manager in order to avoid creating migration files in your + source tree inadvertently. + """ + with tempfile.TemporaryDirectory() as temp_dir: + target_dir = tempfile.mkdtemp(dir=temp_dir) + with open(os.path.join(target_dir, "__init__.py"), "w"): + pass + target_migrations_dir = os.path.join(target_dir, "migrations") + + if module is None: + module = apps.get_app_config(app_label).name + ".migrations" + + try: + source_migrations_dir = module_dir(import_module(module)) + except (ImportError, ValueError): + pass + else: + shutil.copytree(source_migrations_dir, target_migrations_dir) + + with extend_sys_path(temp_dir): + new_module = os.path.basename(target_dir) + ".migrations" + with self.settings(MIGRATION_MODULES={app_label: new_module}): + yield target_migrations_dir + + +class MakeMigrationsCorrectTestCase(BaseMakeMigrationsTestCase): + available_apps = [ + "django_migration_linter", + "tests.test_project.makemigrations_correct_migration_missing", + ] + databases = {"default", "postgresql"} + + def test_correct_linted_makemigrations(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_correct_migration_missing" + ) as migration_dir: + call_command("makemigrations", lint=True, database="postgresql", stdout=out) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertTrue(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Linting for 'makemigrations_correct_migration_missing':", output) + + @override_settings(MIGRATION_LINTER_OVERRIDE_MAKEMIGRATIONS=True) + def test_correct_linted_makemigrations_using_settings(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_correct_migration_missing" + ) as migration_dir: + call_command("makemigrations", database="postgresql", stdout=out) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertTrue(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Linting for 'makemigrations_correct_migration_missing':", output) + + def test_no_linting_when_no_option(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_correct_migration_missing" + ) as migration_dir: + call_command("makemigrations", database="postgresql", stdout=out) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertTrue(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertNotIn("Linting", output) + + def test_correct_linted_makemigrations_dry_run(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_correct_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", + lint=True, + database="postgresql", + dry_run=True, + stdout=out, + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertFalse(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertNotIn("Linting", output) + + +@override_settings(MIGRATION_LINTER_OVERRIDE_MAKEMIGRATIONS=True) +class MakeMigrationsBackwardIncompatibleTestCase(BaseMakeMigrationsTestCase): + available_apps = [ + "django_migration_linter", + "tests.test_project.makemigrations_backward_incompatible_migration_missing", + ] + databases = {"default", "sqlite", "mysql", "postgresql"} + + def test_backward_incompatible_migration_postgresql(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_backward_incompatible_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", interactive=False, database="postgresql", stdout=out + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertFalse(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Deleted", output) + + def test_backward_incompatible_migration_mysql(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_backward_incompatible_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", interactive=False, database="mysql", stdout=out + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertFalse(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Deleted", output) + + def test_backward_incompatible_migration_sqlite(self): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_backward_incompatible_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", interactive=False, database="sqlite", stdout=out + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertFalse(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Deleted", output) + + @mock.patch("builtins.input", return_value="yes") + def test_backward_incompatible_migration_interactive_keep_migration(self, *args): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_backward_incompatible_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", interactive=True, database="postgresql", stdout=out + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertTrue(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertNotIn("Deleted", output) + + @mock.patch("builtins.input", return_value="no") + def test_backward_incompatible_migration_interactive_delete_migration(self, *args): + out = io.StringIO() + with self.temporary_migration_module( + app_label="makemigrations_backward_incompatible_migration_missing" + ) as migration_dir: + call_command( + "makemigrations", interactive=True, database="postgresql", stdout=out + ) + new_migration_file = os.path.join(migration_dir, "0002_a_new_field.py") + self.assertFalse(os.path.exists(new_migration_file)) + output = out.getvalue() + self.assertIn("Deleted", output) diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/__init__.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/apps.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/apps.py new file mode 100644 index 0000000..18cea59 --- /dev/null +++ b/tests/test_project/makemigrations_backward_incompatible_migration_missing/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MakemigrationsBackwardIncompatibleMigrationMissingConfig(AppConfig): + name = "tests.test_project.makemigrations_backward_incompatible_migration_missing" diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py new file mode 100644 index 0000000..ff05a7c --- /dev/null +++ b/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2 on 2020-06-26 17:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="A", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("x", models.IntegerField()), + ], + ), + ] diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/__init__.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py new file mode 100644 index 0000000..3ffc3e6 --- /dev/null +++ b/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class A(models.Model): + x = models.IntegerField() + new_field = models.CharField(null=False, default="empty", max_length=255) diff --git a/tests/test_project/makemigrations_correct_migration_missing/__init__.py b/tests/test_project/makemigrations_correct_migration_missing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/makemigrations_correct_migration_missing/apps.py b/tests/test_project/makemigrations_correct_migration_missing/apps.py new file mode 100644 index 0000000..86549c3 --- /dev/null +++ b/tests/test_project/makemigrations_correct_migration_missing/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MakemigrationsCorrectMigrationMissingConfig(AppConfig): + name = "tests.test_project.makemigrations_correct_migration_missing" diff --git a/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py b/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py new file mode 100644 index 0000000..9aea7ed --- /dev/null +++ b/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2 on 2020-06-26 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="A", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("x", models.IntegerField()), + ], + ), + ] diff --git a/tests/test_project/makemigrations_correct_migration_missing/migrations/__init__.py b/tests/test_project/makemigrations_correct_migration_missing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/makemigrations_correct_migration_missing/models.py b/tests/test_project/makemigrations_correct_migration_missing/models.py new file mode 100644 index 0000000..101fd38 --- /dev/null +++ b/tests/test_project/makemigrations_correct_migration_missing/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class A(models.Model): + x = models.IntegerField() + new_field = models.CharField(null=True, max_length=255)