diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index c612b3812e..757d69995b 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -7,6 +7,7 @@ import shutil from django.apps import apps from django.core.management import call_command, CommandError +from django.db.migrations import questioner from django.test import override_settings, override_system_checks from django.utils import six from django.utils._os import upath @@ -211,3 +212,153 @@ class MakeMigrationsTests(MigrationTestBase): call_command("makemigrations", merge=True, verbosity=0) except CommandError: self.fail("Makemigrations errored in merge mode with conflicts") + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) + def test_makemigrations_merge_no_conflict(self): + """ + Makes sure that makemigrations exits if in merge mode with no conflicts. + """ + stdout = six.StringIO() + try: + call_command("makemigrations", merge=True, stdout=stdout) + except CommandError: + self.fail("Makemigrations errored in merge mode with no conflicts") + self.assertIn("No conflicts detected to merge.", stdout.getvalue()) + + @override_system_checks([]) + def test_makemigrations_no_app_sys_exit(self): + """ + Makes sure that makemigrations exits if a non-existent app is specified. + """ + stderr = six.StringIO() + with self.assertRaises(SystemExit): + call_command("makemigrations", "this_app_does_not_exist", stderr=stderr) + self.assertIn("'this_app_does_not_exist' could not be found.", stderr.getvalue()) + + @override_system_checks([]) + def test_makemigrations_empty_no_app_specified(self): + """ + Makes sure that makemigrations exits if no app is specified with 'empty' mode. + """ + with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): + self.assertRaises(CommandError, call_command, "makemigrations", empty=True) + + @override_system_checks([]) + def test_makemigrations_empty_migration(self): + """ + Makes sure that makemigrations properly constructs an empty migration. + """ + with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): + try: + call_command("makemigrations", "migrations", empty=True, verbosity=0) + except CommandError: + self.fail("Makemigrations errored in creating empty migration for a proper app.") + + initial_file = os.path.join(self.migration_dir, "0001_initial.py") + + # Check for existing 0001_initial.py file in migration folder + self.assertTrue(os.path.exists(initial_file)) + + with codecs.open(initial_file, 'r', encoding='utf-8') as fp: + content = fp.read() + self.assertTrue('# -*- coding: utf-8 -*-' in content) + + # Remove all whitespace to check for empty dependencies and operations + content = content.replace(' ', '') + self.assertIn('dependencies=[\n]', content) + self.assertIn('operations=[\n]', content) + + @override_system_checks([]) + def test_makemigrations_no_changes_no_apps(self): + """ + Makes sure that makemigrations exits when there are no changes and no apps are specified. + """ + stdout = six.StringIO() + call_command("makemigrations", stdout=stdout) + self.assertIn("No changes detected", stdout.getvalue()) + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_no_changes"}) + def test_makemigrations_no_changes(self): + """ + Makes sure that makemigrations exits when there are no changes to an app. + """ + stdout = six.StringIO() + call_command("makemigrations", "migrations", stdout=stdout) + self.assertIn("No changes detected in app 'migrations'", stdout.getvalue()) + + @override_system_checks([]) + def test_makemigrations_migrations_announce(self): + """ + Makes sure that makemigrations announces the migration at the default verbosity level. + """ + stdout = six.StringIO() + with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): + call_command("makemigrations", "migrations", stdout=stdout) + self.assertIn("Migrations for 'migrations'", stdout.getvalue()) + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_no_ancestor"}) + def test_makemigrations_no_common_ancestor(self): + """ + Makes sure that makemigrations fails to merge migrations with no common ancestor. + """ + with self.assertRaises(ValueError) as context: + call_command("makemigrations", "migrations", merge=True) + exception_message = str(context.exception) + self.assertIn("Could not find common ancestor of", exception_message) + self.assertIn("0002_second", exception_message) + self.assertIn("0002_conflicting_second", exception_message) + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) + def test_makemigrations_interactive_reject(self): + """ + Makes sure that makemigrations enters and exits interactive mode properly. + """ + # Monkeypatch interactive questioner to auto reject + old_input = questioner.input + questioner.input = lambda _: "N" + try: + call_command("makemigrations", "migrations", merge=True, interactive=True, verbosity=0) + merge_file = os.path.join(self.test_dir, 'test_migrations_conflict', '0003_merge.py') + self.assertFalse(os.path.exists(merge_file)) + except CommandError: + self.fail("Makemigrations failed while running interactive questioner") + finally: + questioner.input = old_input + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) + def test_makemigrations_interactive_accept(self): + """ + Makes sure that makemigrations enters interactive mode and merges properly. + """ + # Monkeypatch interactive questioner to auto accept + old_input = questioner.input + questioner.input = lambda _: "y" + stdout = six.StringIO() + try: + call_command("makemigrations", "migrations", merge=True, interactive=True, stdout=stdout) + merge_file = os.path.join(self.test_dir, 'test_migrations_conflict', '0003_merge.py') + self.assertTrue(os.path.exists(merge_file)) + os.remove(merge_file) + self.assertFalse(os.path.exists(merge_file)) + except CommandError: + self.fail("Makemigrations failed while running interactive questioner") + finally: + questioner.input = old_input + self.assertIn("Created new merge migration", stdout.getvalue()) + + @override_system_checks([]) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) + def test_makemigrations_handle_merge(self): + """ + Makes sure that makemigrations properly merges the conflicting migrations. + """ + stdout = six.StringIO() + call_command("makemigrations", "migrations", merge=True, stdout=stdout) + self.assertIn("Merging migrations", stdout.getvalue()) + self.assertIn("Branch 0002_second", stdout.getvalue()) + self.assertIn("Branch 0002_conflicting_second", stdout.getvalue()) diff --git a/tests/migrations/test_migrations_no_ancestor/0001_initial.py b/tests/migrations/test_migrations_no_ancestor/0001_initial.py new file mode 100644 index 0000000000..581d536814 --- /dev/null +++ b/tests/migrations/test_migrations_no_ancestor/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + operations = [ + + migrations.CreateModel( + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(null=True)), + ("age", models.IntegerField(default=0)), + ("silly_field", models.BooleanField(default=False)), + ], + ), + + migrations.CreateModel( + "Tribble", + [ + ("id", models.AutoField(primary_key=True)), + ("fluffy", models.BooleanField(default=True)), + ], + ) + + ] diff --git a/tests/migrations/test_migrations_no_ancestor/0002_conflicting_second.py b/tests/migrations/test_migrations_no_ancestor/0002_conflicting_second.py new file mode 100644 index 0000000000..e5909bd08b --- /dev/null +++ b/tests/migrations/test_migrations_no_ancestor/0002_conflicting_second.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + + migrations.DeleteModel("Tribble"), + + migrations.RemoveField("Author", "silly_field"), + + migrations.AddField("Author", "rating", models.IntegerField(default=0)), + + migrations.CreateModel( + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("migrations.Author", null=True)), + ], + ) + + ] diff --git a/tests/migrations/test_migrations_no_ancestor/0002_second.py b/tests/migrations/test_migrations_no_ancestor/0002_second.py new file mode 100644 index 0000000000..9dd60fade2 --- /dev/null +++ b/tests/migrations/test_migrations_no_ancestor/0002_second.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("migrations", "0001_initial"), + ] + + operations = [ + + migrations.DeleteModel("Tribble"), + + migrations.RemoveField("Author", "silly_field"), + + migrations.AddField("Author", "rating", models.IntegerField(default=0)), + + migrations.CreateModel( + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("migrations.Author", null=True)), + ], + ) + + ] diff --git a/tests/migrations/test_migrations_no_ancestor/__init__.py b/tests/migrations/test_migrations_no_ancestor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/test_migrations_no_changes/0001_initial.py b/tests/migrations/test_migrations_no_changes/0001_initial.py new file mode 100644 index 0000000000..581d536814 --- /dev/null +++ b/tests/migrations/test_migrations_no_changes/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + operations = [ + + migrations.CreateModel( + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(null=True)), + ("age", models.IntegerField(default=0)), + ("silly_field", models.BooleanField(default=False)), + ], + ), + + migrations.CreateModel( + "Tribble", + [ + ("id", models.AutoField(primary_key=True)), + ("fluffy", models.BooleanField(default=True)), + ], + ) + + ] diff --git a/tests/migrations/test_migrations_no_changes/0002_second.py b/tests/migrations/test_migrations_no_changes/0002_second.py new file mode 100644 index 0000000000..9dd60fade2 --- /dev/null +++ b/tests/migrations/test_migrations_no_changes/0002_second.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("migrations", "0001_initial"), + ] + + operations = [ + + migrations.DeleteModel("Tribble"), + + migrations.RemoveField("Author", "silly_field"), + + migrations.AddField("Author", "rating", models.IntegerField(default=0)), + + migrations.CreateModel( + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("migrations.Author", null=True)), + ], + ) + + ] diff --git a/tests/migrations/test_migrations_no_changes/0003_third.py b/tests/migrations/test_migrations_no_changes/0003_third.py new file mode 100644 index 0000000000..f8f3db9386 --- /dev/null +++ b/tests/migrations/test_migrations_no_changes/0003_third.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('migrations', '0002_second'), + ] + + operations = [ + migrations.CreateModel( + name='ModelWithCustomBase', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.DeleteModel( + name='Author', + ), + migrations.DeleteModel( + name='Book', + ), + ] diff --git a/tests/migrations/test_migrations_no_changes/__init__.py b/tests/migrations/test_migrations_no_changes/__init__.py new file mode 100644 index 0000000000..e69de29bb2