Skip to content

Commit 4dcdda0

Browse files
committed
Added a test to the test suite which checks if there are interleaved schema and data migrations that have not been released yet. Having split migrations, first all schema and then all data, will permit doing 2 sequential releases; but if the data and schema migrations are interleaved (beyond this) it is problematic to arrange for release without ending up with a prolonged period when running code and table structure is out of sync (while the normally more time-consuming data migrations run).
- Legacy-Id: 13576
1 parent df2d057 commit 4dcdda0

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

ietf/utils/test_runner.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
from coverage.misc import NotPython
5353

5454
from django.conf import settings
55+
from django.db.migrations.loader import MigrationLoader
56+
from django.db.migrations.operations.fields import FieldOperation
57+
from django.db.migrations.operations.models import ModelOperation
58+
from django.db.migrations.operations.base import Operation
5559
from django.template import TemplateDoesNotExist
5660
from django.template.loaders.base import Loader as BaseLoader
5761
from django.test.runner import DiscoverRunner
@@ -342,6 +346,81 @@ def code_coverage_test(self):
342346
else:
343347
self.skipTest("Coverage switched off with --skip-coverage")
344348

349+
def interleaved_migrations_test(self):
350+
# from django.apps import apps
351+
# unreleased = {}
352+
# for appconf in apps.get_app_configs():
353+
# mpath = Path(appconf.path) / 'migrations'
354+
# for pyfile in mpath.glob('*.py'):
355+
# if pyfile.name == '__init__.py':
356+
# continue
357+
# mmod = import_module('%s.migrations.%s' % (appconf.name, pyfile.stem))
358+
# for n,v in mmod.__dict__.items():
359+
# if isinstance(v, type) and issubclass(v, migrations.Migration):
360+
# migration = v
361+
# self.runner.coverage_data['migration']['present'][migration.__module__] = {'operations':[]}
362+
# d = self.runner.coverage_data['migration']['present'][migration.__module__]
363+
# for n,v in migration.__dict__.items():
364+
# if n == 'operations':
365+
# for op in v:
366+
# cl = op.__class__
367+
# if issubclass(cl, ModelOperation) or issubclass(cl, FieldOperation):
368+
# d['operations'].append('schema')
369+
# elif issubclass(cl, Operation):
370+
# d['operations'].append('data')
371+
# else:
372+
# raise RuntimeError("Found unexpected operation type in migration: %s" % (op))
373+
374+
# Clear this setting, otherwise we won't see any migrations
375+
settings.MIGRATION_MODULES = {}
376+
# Save information here, for later write to file
377+
info = self.runner.coverage_data['migration']['present']
378+
# Get migrations
379+
loader = MigrationLoader(None, ignore_no_migrations=True)
380+
graph = loader.graph
381+
targets = graph.leaf_nodes()
382+
seen = set()
383+
opslist = []
384+
for target in targets:
385+
#debug.show('target')
386+
for migration in graph.forwards_plan(target):
387+
if migration not in seen:
388+
node = graph.node_map[migration]
389+
#debug.show('node')
390+
seen.add(migration)
391+
ops = []
392+
# get the actual migration object
393+
migration = loader.graph.nodes[migration]
394+
for op in migration.operations:
395+
cl = op.__class__
396+
if issubclass(cl, ModelOperation) or issubclass(cl, FieldOperation):
397+
ops.append(('schema', cl.__name__))
398+
elif issubclass(cl, Operation):
399+
ops.append(('data', cl.__name__))
400+
else:
401+
raise RuntimeError("Found unexpected operation type in migration: %s" % (op))
402+
info[migration.__module__] = {'operations': ops}
403+
opslist.append((migration, node, ops))
404+
# Compare the migrations we found to those present in the latest
405+
# release, to see if we have any unreleased migrations
406+
latest_coverage_version = self.runner.coverage_master["version"]
407+
if 'migration' in self.runner.coverage_master[latest_coverage_version]:
408+
release_data = self.runner.coverage_master[latest_coverage_version]['migration']['present']
409+
else:
410+
release_data = {}
411+
unreleased = []
412+
for migration, node, ops in opslist:
413+
if not migration.__module__ in release_data:
414+
for op, nm in ops:
415+
unreleased.append((node, op, nm))
416+
# gather the transitions in operation types. We'll allow 1
417+
# transition, but not 2 or more.
418+
mixed = [ unreleased[i] for i in range(1,len(unreleased)) if unreleased[i][1] != unreleased[i-1][1] ]
419+
if len(mixed) > 1:
420+
raise self.failureException('Found interleaved schema and data operations in unreleased migrations;'
421+
' please see if they can be re-ordered with all schema migrations before the data migrations:\n'
422+
+('\n'.join([' %-6s: %-12s, %s (%s)'% (op, node.key[0], node.key[1], nm) for (node, op, nm) in unreleased ])))
423+
345424
class IetfTestRunner(DiscoverRunner):
346425

347426
@classmethod
@@ -403,6 +482,10 @@ def setup_test_environment(self, **kwargs):
403482
"covered": {},
404483
"format": 1,
405484
},
485+
"migration": {
486+
"present": {},
487+
"format": 3,
488+
}
406489
}
407490

408491
settings.TEMPLATES[0]['OPTIONS']['loaders'] = ('ietf.utils.test_runner.TemplateCoverageLoader',) + settings.TEMPLATES[0]['OPTIONS']['loaders']
@@ -527,6 +610,7 @@ def run_tests(self, test_labels, extra_tests=[], **kwargs):
527610
if self.check_coverage:
528611
template_coverage_collection = True
529612
extra_tests += [
613+
CoverageTest(test_runner=self, methodName='interleaved_migrations_test'),
530614
CoverageTest(test_runner=self, methodName='url_coverage_test'),
531615
CoverageTest(test_runner=self, methodName='template_coverage_test'),
532616
CoverageTest(test_runner=self, methodName='code_coverage_test'),

release-coverage.json.gz

15.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)