Przeglądaj źródła

Merge pull request #22304 from netbox-community/21945-duplicate-migration-indexes-test

Closes #21945: Introduce a test for duplicate migration indexes
bctiemann 1 dzień temu
rodzic
commit
de5d12860b
1 zmienionych plików z 89 dodań i 0 usunięć
  1. 89 0
      netbox/netbox/tests/test_migrations.py

+ 89 - 0
netbox/netbox/tests/test_migrations.py

@@ -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),
+        )