summaryrefslogtreecommitdiff
path: root/tests/migrations
diff options
context:
space:
mode:
authorJayden Kneller <jayden@arrai.com>2025-11-03 12:05:59 -0500
committerJacob Walls <jacobtylerwalls@gmail.com>2025-11-04 08:15:22 -0500
commite27cff68a32a0183c6b8d110b359c1c858f68cd7 (patch)
treeab021c78b5cf8069fefc71ee8329efbb2865d384 /tests/migrations
parenteaf7b563a5d3861026242fdb503a58f71acf17f6 (diff)
Fixed #36652 -- Increased determinism when loading migrations from disk.
Ordering still depends on pkgutil.iter_modules, which does not guarantee order, but at least now Django is not introducing additional indeterminism, causing CircularDependencyError to appear or not appear in some edge cases. Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
Diffstat (limited to 'tests/migrations')
-rw-r--r--tests/migrations/test_loader.py69
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/__init__.py0
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app1/0001_initial.py9
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app1/0001_squashed_initial.py13
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app1/0002_squashed_initial.py16
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app1/__init__.py0
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app2/0001_initial.py11
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app2/0001_squashed_initial.py13
-rw-r--r--tests/migrations/test_migrations_squashed_replaced_order/app2/__init__.py0
9 files changed, 131 insertions, 0 deletions
diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py
index 7cf11f7faa..efa7a94166 100644
--- a/tests/migrations/test_loader.py
+++ b/tests/migrations/test_loader.py
@@ -1,7 +1,12 @@
import compileall
import os
+import subprocess
+import sys
+import tempfile
from importlib import import_module
+from pathlib import Path
+from django.conf import settings
from django.db import connection, connections
from django.db.migrations.exceptions import (
AmbiguityError,
@@ -649,6 +654,70 @@ class LoaderTests(TestCase):
test_module.__spec__.origin = module_origin
test_module.__spec__.has_location = module_has_location
+ def test_loading_order_does_not_create_circular_dependency(self):
+ """
+ Before, for these migrations:
+ app1
+ [ ] 0001_squashed_initial <- replaces app1.0001
+ [ ] 0002_squashed_initial <- replaces app1.0001
+ depends on app1.0001_squashed_initial & app2.0001_squashed_initial
+ app2
+ [ ] 0001_squashed_initial <- replaces app2.0001
+
+ When loading app1's migrations, if 0002_squashed_initial was first:
+ {'0002_squashed_initial', '0001_initial', '0001_squashed_initial'}
+ Then CircularDependencyError was raised, but it's resolvable as:
+ {'0001_initial', '0001_squashed_initial', '0002_squashed_initial'}
+ """
+ # Create a test settings file to provide to the subprocess.
+ MIGRATION_MODULES = {
+ "app1": "migrations.test_migrations_squashed_replaced_order.app1",
+ "app2": "migrations.test_migrations_squashed_replaced_order.app2",
+ }
+ INSTALLED_APPS = [
+ "migrations.test_migrations_squashed_replaced_order.app1",
+ "migrations.test_migrations_squashed_replaced_order.app2",
+ ]
+ tests_dir = Path(__file__).parent.parent
+ with tempfile.NamedTemporaryFile(
+ mode="w", encoding="utf-8", suffix=".py", dir=tests_dir, delete=False
+ ) as test_settings:
+ for attr, value in settings._wrapped.__dict__.items():
+ if attr.isupper():
+ test_settings.write(f"{attr} = {value!r}\n")
+ # Provide overrides here, instead of via decorators.
+ test_settings.write(f"DATABASES = {settings.DATABASES}\n")
+ test_settings.write(f"MIGRATION_MODULES = {MIGRATION_MODULES}\n")
+ # Isolate away other test apps.
+ test_settings.write(
+ "INSTALLED_APPS=[a for a in INSTALLED_APPS if a.startswith('django')]\n"
+ )
+ test_settings.write(f"INSTALLED_APPS += {INSTALLED_APPS}\n")
+ test_settings_name = test_settings.name
+ self.addCleanup(os.remove, test_settings_name)
+
+ test_environ = os.environ.copy()
+ test_environ["PYTHONPATH"] = str(tests_dir)
+ # Ensure deterministic failures.
+ test_environ["PYTHONHASHSEED"] = "1"
+
+ args = [
+ sys.executable,
+ "-m",
+ "django",
+ "showmigrations",
+ "app1",
+ "--skip-checks",
+ "--settings",
+ Path(test_settings_name).stem,
+ ]
+ try:
+ subprocess.run(
+ args, capture_output=True, env=test_environ, check=True, text=True
+ )
+ except subprocess.CalledProcessError as err:
+ self.fail(err.stderr)
+
class PycLoaderTests(MigrationTestBase):
def test_valid(self):
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/__init__.py b/tests/migrations/test_migrations_squashed_replaced_order/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/__init__.py
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_initial.py b/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_initial.py
new file mode 100644
index 0000000000..cf311d1f93
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_initial.py
@@ -0,0 +1,9 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = []
+
+ operations = []
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_squashed_initial.py b/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_squashed_initial.py
new file mode 100644
index 0000000000..7a77d339bc
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app1/0001_squashed_initial.py
@@ -0,0 +1,13 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ replaces = [
+ ("app1", "0001_initial"),
+ ]
+
+ dependencies = []
+
+ operations = []
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app1/0002_squashed_initial.py b/tests/migrations/test_migrations_squashed_replaced_order/app1/0002_squashed_initial.py
new file mode 100644
index 0000000000..fc16ed6d6e
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app1/0002_squashed_initial.py
@@ -0,0 +1,16 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ replaces = [
+ ("app1", "0001_initial"),
+ ]
+
+ dependencies = [
+ ("app1", "0001_squashed_initial"),
+ ("app2", "0001_squashed_initial"),
+ ]
+
+ operations = []
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app1/__init__.py b/tests/migrations/test_migrations_squashed_replaced_order/app1/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app1/__init__.py
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_initial.py b/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_initial.py
new file mode 100644
index 0000000000..05d0409496
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_initial.py
@@ -0,0 +1,11 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ ("app1", "0001_initial"),
+ ]
+
+ operations = []
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_squashed_initial.py b/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_squashed_initial.py
new file mode 100644
index 0000000000..de6bfe4135
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app2/0001_squashed_initial.py
@@ -0,0 +1,13 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ replaces = [
+ ("app2", "0001_initial"),
+ ]
+
+ dependencies = []
+
+ operations = []
diff --git a/tests/migrations/test_migrations_squashed_replaced_order/app2/__init__.py b/tests/migrations/test_migrations_squashed_replaced_order/app2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_squashed_replaced_order/app2/__init__.py