From f851a954ac605c4a525761a18d2dcbfb6f8deeca Mon Sep 17 00:00:00 2001
From: Huu Nguyen <huu@prismskylabs.com>
Date: Tue, 20 May 2014 07:29:32 -0700
Subject: [PATCH] Fixed #22602 -- Improved code coverage of makemigrations
 command tests.

---
 tests/migrations/test_commands.py             | 151 ++++++++++++++++++
 .../0001_initial.py                           |  30 ++++
 .../0002_conflicting_second.py                |  28 ++++
 .../0002_second.py                            |  29 ++++
 .../test_migrations_no_ancestor/__init__.py   |   0
 .../0001_initial.py                           |  30 ++++
 .../test_migrations_no_changes/0002_second.py |  29 ++++
 .../test_migrations_no_changes/0003_third.py  |  29 ++++
 .../test_migrations_no_changes/__init__.py    |   0
 9 files changed, 326 insertions(+)
 create mode 100644 tests/migrations/test_migrations_no_ancestor/0001_initial.py
 create mode 100644 tests/migrations/test_migrations_no_ancestor/0002_conflicting_second.py
 create mode 100644 tests/migrations/test_migrations_no_ancestor/0002_second.py
 create mode 100644 tests/migrations/test_migrations_no_ancestor/__init__.py
 create mode 100644 tests/migrations/test_migrations_no_changes/0001_initial.py
 create mode 100644 tests/migrations/test_migrations_no_changes/0002_second.py
 create mode 100644 tests/migrations/test_migrations_no_changes/0003_third.py
 create mode 100644 tests/migrations/test_migrations_no_changes/__init__.py

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