diff options
| author | Jayden Kneller <jayden@arrai.com> | 2025-11-03 12:05:59 -0500 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2025-11-04 08:15:22 -0500 |
| commit | e27cff68a32a0183c6b8d110b359c1c858f68cd7 (patch) | |
| tree | ab021c78b5cf8069fefc71ee8329efbb2865d384 /tests/migrations | |
| parent | eaf7b563a5d3861026242fdb503a58f71acf17f6 (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')
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 |
