The API unit test suite has been failing consistently on the Python 3.11 matrix in Depot CI, and a failure has now reproduced in GitHub Actions: https://github.com/Flagsmith/flagsmith/actions/runs/25070433056/job/73448935538 (run on commit 5c5ebc4).
Symptoms
Two failure clusters in the same job:
-
tests/unit/core/management/test_unit_core_management_makemigrations.py::test_makemigrations__with_check_changes__runs_without_error exits with SystemExit: 1. Captured stdout:
Migrations for 'app_analytics':
app_analytics/migrations/0009_analyticsmodel_mymodel_mymodel1_mymodel2.py
+ Create model AnalyticsModel
+ Create model MyModel
+ Create model MyModel1
+ Create model MyModel2
-
Roughly a dozen migration tests using django_test_migrations.Migrator fail at teardown across multiple xdist workers with:
psycopg2.errors.DuplicateTable: relation "app_analytics_analyticsmodel" already exists
Affected files include tests/unit/segments/test_unit_segments_migrations.py, tests/unit/features/test_migrations.py, tests/unit/organisations/permissions/test_unit_organisations_migrations.py, tests/unit/environments/permissions/test_unit_environments_permissions_migrations.py, tests/unit/features/versioning/test_unit_versioning_migrations.py, tests/unit/projects/test_migrations.py.
Root cause
api/tests/unit/app/test_unit_app_routers.py defines four django.db.models.Model subclasses inline inside parametrised test functions:
AnalyticsModel (line 19) with app_label = "app_analytics" (parametrised) or "another_app"
MyModel (line 44) with the same parametrisation
MyModel1 (line 70) and MyModel2 (line 74) with the same parametrisation
Defining a Django model class registers it in the apps registry for the lifetime of the worker process. There is no apps registry isolation around these tests. Once these tests run with app_label="app_analytics", the four model classes are permanently registered against the real app_analytics app for that worker.
That registration is exactly what we see in cluster 1: makemigrations --check finds four models with no migrations and exits non-zero, listing the four polluting model names verbatim.
It also explains cluster 2: when Migrator.apply_initial_migration rebuilds migration state, the polluted apps registry causes the migration executor to attempt CREATE TABLE \"app_analytics_analyticsmodel\", which collides with prior state in the worker's test database.
Why it's flaky
pytest-xdist distributes tests across workers, and the failure only manifests on a worker that runs test_unit_app_routers.py before any victim test in the same process. Whether that ordering occurs depends on collection and load distribution, hence the intermittence. Recently it has been firing reliably on the 3.11 matrix in Depot, and now in GHA, suggesting collection or scheduling has shifted such that the bad ordering is the common case.
Fix options
- Drop the inline
models.Model subclasses and exercise the routers with simple objects exposing _meta.app_label (the routers only inspect app_label).
- Or wrap the tests in
django.test.utils.isolate_apps(\"tests\") so model registration is reverted at the end of each test.
- Or set
app_label to a throwaway label (e.g. \"tests\") — this would stop polluting app_analytics, but the tests are parametrised on app_label itself, so the first option is cleaner.
Job artefacts
The API unit test suite has been failing consistently on the Python 3.11 matrix in Depot CI, and a failure has now reproduced in GitHub Actions: https://github.com/Flagsmith/flagsmith/actions/runs/25070433056/job/73448935538 (run on commit 5c5ebc4).
Symptoms
Two failure clusters in the same job:
tests/unit/core/management/test_unit_core_management_makemigrations.py::test_makemigrations__with_check_changes__runs_without_errorexits withSystemExit: 1. Captured stdout:Roughly a dozen migration tests using
django_test_migrations.Migratorfail at teardown across multiple xdist workers with:Affected files include
tests/unit/segments/test_unit_segments_migrations.py,tests/unit/features/test_migrations.py,tests/unit/organisations/permissions/test_unit_organisations_migrations.py,tests/unit/environments/permissions/test_unit_environments_permissions_migrations.py,tests/unit/features/versioning/test_unit_versioning_migrations.py,tests/unit/projects/test_migrations.py.Root cause
api/tests/unit/app/test_unit_app_routers.pydefines fourdjango.db.models.Modelsubclasses inline inside parametrised test functions:AnalyticsModel(line 19) withapp_label = "app_analytics"(parametrised) or"another_app"MyModel(line 44) with the same parametrisationMyModel1(line 70) andMyModel2(line 74) with the same parametrisationDefining a Django model class registers it in the apps registry for the lifetime of the worker process. There is no apps registry isolation around these tests. Once these tests run with
app_label="app_analytics", the four model classes are permanently registered against the realapp_analyticsapp for that worker.That registration is exactly what we see in cluster 1:
makemigrations --checkfinds four models with no migrations and exits non-zero, listing the four polluting model names verbatim.It also explains cluster 2: when
Migrator.apply_initial_migrationrebuilds migration state, the polluted apps registry causes the migration executor to attemptCREATE TABLE \"app_analytics_analyticsmodel\", which collides with prior state in the worker's test database.Why it's flaky
pytest-xdistdistributes tests across workers, and the failure only manifests on a worker that runstest_unit_app_routers.pybefore any victim test in the same process. Whether that ordering occurs depends on collection and load distribution, hence the intermittence. Recently it has been firing reliably on the 3.11 matrix in Depot, and now in GHA, suggesting collection or scheduling has shifted such that the bad ordering is the common case.Fix options
models.Modelsubclasses and exercise the routers with simple objects exposing_meta.app_label(the routers only inspectapp_label).django.test.utils.isolate_apps(\"tests\")so model registration is reverted at the end of each test.app_labelto a throwaway label (e.g.\"tests\") — this would stop pollutingapp_analytics, but the tests are parametrised onapp_labelitself, so the first option is cleaner.Job artefacts
5c5ebc4f5d6587585d45772236e857651db3f0f6(main)api/tests/unit/app/test_unit_app_routers.py