Merge pull request #22304 from netbox-community/21945-duplicate-migration-indexes-test
Closes #21945: Introduce a test for duplicate migration indexes
This commit is contained in:
commit
de5d12860b
|
|
@ -0,0 +1,89 @@
|
|||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
# Migrations which share a numeric prefix with a sibling migration but predate
|
||||
# enforcement of this convention. New duplicates must not be added; these are
|
||||
# grandfathered in to avoid rewriting historical migration history.
|
||||
#
|
||||
# Keyed by app label, valued by the set of duplicated numeric prefixes.
|
||||
EXEMPTIONS = {
|
||||
'dcim': {'0203', '0226'},
|
||||
}
|
||||
|
||||
MIGRATION_PREFIX_RE = re.compile(r'^(\d+)_')
|
||||
|
||||
|
||||
def first_party_app_labels():
|
||||
"""
|
||||
Return the labels of NetBox's own apps (those living within the project
|
||||
source tree), excluding third-party and plugin apps.
|
||||
"""
|
||||
labels = []
|
||||
for app_config in apps.get_app_configs():
|
||||
if app_config.path and app_config.path.startswith(settings.BASE_DIR):
|
||||
labels.append(app_config.label)
|
||||
return labels
|
||||
|
||||
|
||||
class DuplicateMigrationIndexTestCase(SimpleTestCase):
|
||||
"""
|
||||
Ensure that no two migrations within the same app share a numeric prefix
|
||||
(e.g. ``0123_foo.py`` and ``0123_bar.py``). While technically permissible,
|
||||
duplicate indexes invite confusion and have never been NetBox's (or
|
||||
Django's) convention. See netbox-community/netbox#21945.
|
||||
"""
|
||||
|
||||
def find_duplicate_prefixes(self, app_labels):
|
||||
"""
|
||||
Map each given app label to the numeric prefixes shared by more than
|
||||
one of its migrations: ``{app_label: {prefix: [migration_name, ...]}}``.
|
||||
"""
|
||||
per_app = {label: defaultdict(list) for label in app_labels}
|
||||
loader = MigrationLoader(connection=None, ignore_no_migrations=True)
|
||||
|
||||
for (migration_app, migration_name) in loader.disk_migrations:
|
||||
if migration_app not in per_app:
|
||||
continue
|
||||
match = MIGRATION_PREFIX_RE.match(migration_name)
|
||||
if match:
|
||||
per_app[migration_app][match.group(1)].append(migration_name)
|
||||
|
||||
return {
|
||||
label: {prefix: sorted(names) for prefix, names in prefixes.items() if len(names) > 1}
|
||||
for label, prefixes in per_app.items()
|
||||
}
|
||||
|
||||
def test_no_duplicate_migration_indexes(self):
|
||||
errors = []
|
||||
app_labels = first_party_app_labels()
|
||||
duplicates_by_app = self.find_duplicate_prefixes(app_labels)
|
||||
|
||||
for app_label in app_labels:
|
||||
duplicates = duplicates_by_app[app_label]
|
||||
exempted = EXEMPTIONS.get(app_label, set())
|
||||
|
||||
# Flag any newly introduced duplicate prefixes.
|
||||
for prefix, names in sorted(duplicates.items()):
|
||||
if prefix in exempted:
|
||||
continue
|
||||
joined = ', '.join(f'{name}.py' for name in names)
|
||||
errors.append(f' {app_label}: prefix {prefix} is shared by {joined}')
|
||||
|
||||
# Flag stale exemptions whose duplicates have since been resolved,
|
||||
# so the exemption list cannot rot.
|
||||
for prefix in sorted(exempted - set(duplicates)):
|
||||
errors.append(
|
||||
f' {app_label}: stale exemption for prefix {prefix} (no longer duplicated); '
|
||||
f'remove it from EXEMPTIONS'
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
errors,
|
||||
'Duplicate migration indexes detected. Renumber new migrations so each has a unique '
|
||||
'numeric prefix within its app:\n' + '\n'.join(errors),
|
||||
)
|
||||
Loading…
Reference in New Issue