|
52 | 52 | from coverage.misc import NotPython |
53 | 53 |
|
54 | 54 | 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 |
55 | 59 | from django.template import TemplateDoesNotExist |
56 | 60 | from django.template.loaders.base import Loader as BaseLoader |
57 | 61 | from django.test.runner import DiscoverRunner |
@@ -342,6 +346,81 @@ def code_coverage_test(self): |
342 | 346 | else: |
343 | 347 | self.skipTest("Coverage switched off with --skip-coverage") |
344 | 348 |
|
| 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 | + |
345 | 424 | class IetfTestRunner(DiscoverRunner): |
346 | 425 |
|
347 | 426 | @classmethod |
@@ -403,6 +482,10 @@ def setup_test_environment(self, **kwargs): |
403 | 482 | "covered": {}, |
404 | 483 | "format": 1, |
405 | 484 | }, |
| 485 | + "migration": { |
| 486 | + "present": {}, |
| 487 | + "format": 3, |
| 488 | + } |
406 | 489 | } |
407 | 490 |
|
408 | 491 | 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): |
527 | 610 | if self.check_coverage: |
528 | 611 | template_coverage_collection = True |
529 | 612 | extra_tests += [ |
| 613 | + CoverageTest(test_runner=self, methodName='interleaved_migrations_test'), |
530 | 614 | CoverageTest(test_runner=self, methodName='url_coverage_test'), |
531 | 615 | CoverageTest(test_runner=self, methodName='template_coverage_test'), |
532 | 616 | CoverageTest(test_runner=self, methodName='code_coverage_test'), |
|
0 commit comments