Пример #1
0
    def _perform_delete_app_test(self, sql_name, database=None):
        # Simulate deletion of the app.
        self.set_base_model(self.default_base_model,
                            extra_models=self.default_extra_models,
                            db_name=database)

        end_sig = self.start_sig.clone()
        end_sig.remove_app_sig('tests')

        d = Diff(self.start_sig, end_sig)
        self.assertEqual(sorted(six.iterkeys(d.deleted)), ['tests'])
        self.assertEqual(d.deleted['tests'], [
            'TestModel', 'AppDeleteAnchor1', 'AppDeleteAnchor2',
            'CustomTestModel'
        ])

        mutation = DeleteApplication()
        self.perform_simulations([mutation],
                                 end_sig,
                                 ignore_apps=True,
                                 db_name=database)

        test_database_state = self.database_state.clone()
        test_sig = self.start_sig.clone()

        app_mutator = AppMutator(app_label='tests',
                                 project_sig=test_sig,
                                 database_state=test_database_state,
                                 database=database)
        app_mutator.run_mutation(mutation)
        sql = app_mutator.to_sql()

        self.assertSQLMappingEqual(sql, sql_name, database=database)
Пример #2
0
    def _perform_delete_app_test(self, sql_name, database=None):
        # Simulate deletion of the app.
        self.set_base_model(self.default_base_model,
                            extra_models=self.default_extra_models,
                            db_name=database)

        end_sig = self.copy_sig(self.start_sig)
        end_sig.pop('tests')

        d = Diff(self.start_sig, end_sig)
        self.assertEqual(sorted(d.deleted.keys()), ['tests'])
        self.assertEqual(d.deleted['tests'], [
            'TestModel', 'AppDeleteAnchor1', 'AppDeleteAnchor2',
            'CustomTestModel'
        ])

        mutation = DeleteApplication()
        self.perform_simulations([mutation],
                                 end_sig,
                                 ignore_apps=True,
                                 db_name=database)

        test_database_sig = self.copy_sig(self.database_sig)
        test_sig = self.copy_sig(self.start_sig)

        app_mutator = AppMutator('tests', test_sig, test_database_sig,
                                 database)
        app_mutator.run_mutation(mutation)
        sql = app_mutator.to_sql()

        self.assertEqual('\n'.join(sql),
                         self.get_sql_mapping(sql_name, database))
Пример #3
0
    def test_diff_identical_model(self):
        """Testing Diff with identical signatures"""
        class DestModel(BaseTestModel):
            name = models.CharField(max_length=20)
            age = models.IntegerField()
            ref = models.ForeignKey(DiffAnchor1, on_delete=models.CASCADE)

        end_sig = self.make_end_signatures(DestModel, 'TestModel')[1]
        d = Diff(self.start_sig, end_sig)

        self.assertTrue(d.is_empty())
        self.assertEqual(d.evolution(), {})
Пример #4
0
    def perform_simulations(self,
                            evolutions,
                            end_sig,
                            ignore_apps=False,
                            db_name=None):
        """Run simulations and verify that they result in an end signature.

        This will run through an evolution chain, simulating each one on a
        copy of the starting signature, and then verifying that the signature
        is properly transformed into the expected ending signature.

        Args:
            evolutions (list of django_evolution.mutations.BaseMutation):
                The evolution chain to run simulations on.

            end_sig (django_evolution.signature.ProjectSignature):
                The expected ending signature.

            ignore_apps (bool, optional):
                Whether to ignore changes to the list of applications.

            db_name (unicode, optional):
                The name of the database to perform the simulations against.

        Returns:
            django_evolution.signature.ProjectSignature:
            The resulting modified signature.

        Raises:
            AssertionError:
                The modified signature did not match the expected end
                signature.
        """
        db_name = db_name or self.default_database_name

        self.test_database_state = self.database_state.clone()
        test_sig = self.start_sig.clone()

        for mutation in evolutions:
            mutation.run_simulation(app_label='tests',
                                    project_sig=test_sig,
                                    database_state=self.test_database_state,
                                    database=db_name)

        # Check that the simulation's changes results in an empty diff.
        d = Diff(test_sig, end_sig)
        self.assertTrue(d.is_empty(ignore_apps=ignore_apps))

        return test_sig
Пример #5
0
    def diff_evolutions(self):
        """Return a diff between stored and post-evolution project signatures.

        This will run through all queued tasks, preparing them and simulating
        their changes. The returned diff will represent the changes made in
        those tasks.

        This can only be called after all tasks have been queued.

        Returns:
            django_evolution.diff.Diff:
            The diff between the stored signature and the queued changes.
        """
        self._prepare_tasks()

        return Diff(self.project_sig, self.target_project_sig)
Пример #6
0
    def perform_diff_test(self,
                          end_sig,
                          diff_text=None,
                          expected_hint=None,
                          expect_empty=False):
        """Generate a diff between signatures and check for expected results.

        The registered base signature and the provided ending signature will
        be diffed, asserted to be empty/not empty (depending on the arguments),
        and then checked against the provided diff text and hint.

        Args:
            end_sig (django_evolution.signature.ProjectSignature):
                The expected project signature at the end of the evolution.
                This is generated by :py:meth:`make_end_signatures`.

            diff_text (unicode, optional):
                The expected generated text describing a diff that must
                match, if provided.

            expected_hint (unicode, optional):
                The expected generated hint text that must match, if provided.

            expect_empty (bool, optional):
                Whether the diff is expected to be empty.

        Returns:
            django_evolution.diff.Diff:
            The resulting diff.

        Raises:
            AssertionError:
                One of the expectations has failed.
        """
        d = Diff(self.start_sig, end_sig)
        self.assertEqual(d.is_empty(), expect_empty)

        if not expect_empty:
            if diff_text is not None:
                self.assertEqual(str(d), diff_text)

            if expected_hint is not None:
                self.assertEqual([str(e) for e in d.evolution()['tests']],
                                 expected_hint)

        return d
Пример #7
0
    def perform_diff_test(self,
                          end_sig,
                          diff_text,
                          expected_hint,
                          expect_empty=False):
        d = Diff(self.start_sig, end_sig)
        self.assertEqual(d.is_empty(), expect_empty)

        if not expect_empty:
            if diff_text is not None:
                self.assertEqual(str(d), diff_text)

            if expected_hint is not None:
                self.assertEqual([str(e) for e in d.evolution()['tests']],
                                 expected_hint)

        return d
Пример #8
0
    def perform_simulations(self,
                            evolutions,
                            end_sig,
                            ignore_apps=False,
                            db_name=None):
        db_name = db_name or self.default_database_name

        self.test_database_sig = self.copy_sig(self.database_sig)
        test_sig = self.copy_sig(self.start_sig)

        for mutation in evolutions:
            mutation.simulate('tests',
                              test_sig,
                              self.test_database_sig,
                              database=db_name)

        # Check that the simulation's changes results in an empty diff.
        d = Diff(test_sig, end_sig)
        self.assertTrue(d.is_empty(ignore_apps=ignore_apps))
Пример #9
0
    def __init__(self,
                 hinted=False,
                 verbosity=0,
                 interactive=False,
                 database_name=DEFAULT_DB_ALIAS):
        """Initialize the evolver.

        Args:
            hinted (bool, optional):
                Whether to operate against hinted evolutions. This may
                result in changes to the database without there being any
                accompanying evolution files backing those changes.

            verbosity (int, optional):
                The verbosity level for any output. This is passed along to
                signal emissions.

            interactive (bool, optional):
                Whether the evolution operations are being performed in a
                way that allows interactivity on the command line. This is
                passed along to signal emissions.

            database_name (unicode, optional):
                The name of the database to evolve.

        Raises:
            django_evolution.errors.EvolutionBaselineMissingError:
                An initial baseline for the project was not yet installed.
                This is due to ``syncdb``/``migrate`` not having been run.
        """
        self.database_name = database_name
        self.hinted = hinted
        self.verbosity = verbosity
        self.interactive = interactive

        self.evolved = False
        self.initial_diff = None
        self.project_sig = None
        self.version = None
        self.installed_new_database = False

        self.connection = connections[database_name]

        if hasattr(self.connection, 'prepare_database'):
            # Django >= 1.8
            self.connection.prepare_database()

        self.database_state = DatabaseState(self.database_name)
        self.target_project_sig = \
            ProjectSignature.from_database(database_name)

        self._tasks_by_class = OrderedDict()
        self._tasks_by_id = OrderedDict()
        self._tasks_prepared = False

        latest_version = None

        if self.database_state.has_model(Version):
            try:
                latest_version = \
                    Version.objects.current_version(using=database_name)
            except Version.DoesNotExist:
                # We'll populate this next.
                pass

        if latest_version is None:
            # Either the models aren't yet synced to the database, or we
            # don't have a saved project signature, so let's set these up.
            self.installed_new_database = True

            self.project_sig = ProjectSignature()
            app = get_app('django_evolution')

            task = EvolveAppTask(evolver=self, app=app)
            task.prepare(hinted=False)

            with self.sql_executor() as sql_executor:
                task.execute(sql_executor=sql_executor, create_models_now=True)

            self.database_state.rescan_tables()

            app_sig = AppSignature.from_app(app=app, database=database_name)
            self.project_sig.add_app_sig(app_sig)

            # Let's make completely sure that we've only found the models
            # we expect. This is mostly for the benefit of unit tests.
            model_names = set(model_sig.model_name
                              for model_sig in app_sig.model_sigs)
            expected_model_names = set(['Evolution', 'Version'])

            assert model_names == expected_model_names, (
                'Unexpected models found for django_evolution app: %s' %
                ', '.join(model_names - expected_model_names))

            self._save_project_sig(new_evolutions=task.new_evolutions)
            latest_version = self.version

        self.project_sig = latest_version.signature
        self.initial_diff = Diff(self.project_sig, self.target_project_sig)
Пример #10
0
def evolution(app, created_models, verbosity=1, **kwargs):
    """
    A hook into syncdb's post_syncdb signal, that is used to notify the user
    if a model evolution is necessary.
    """
    default_db = None
    if is_multi_db():
        from django.db.utils import DEFAULT_DB_ALIAS
        default_db = DEFAULT_DB_ALIAS

    db = kwargs.get('db', default_db)
    proj_sig = create_project_sig(db)
    signature = pickle.dumps(proj_sig)

    using_args = {}

    if is_multi_db():
        using_args['using'] = db

    try:
        if is_multi_db():
            latest_version = \
                django_evolution.Version.objects.using(db).latest('when')
        else:
            latest_version = django_evolution.Version.objects.latest('when')
    except django_evolution.Version.DoesNotExist:
        # We need to create a baseline version.
        if verbosity > 0:
            print "Installing baseline version"

        latest_version = django_evolution.Version(signature=signature)
        latest_version.save(**using_args)

        for a in get_apps():
            install_baseline(a, latest_version, using_args, verbosity)

    unapplied = get_unapplied_evolutions(app, db)

    if unapplied:
        print style.NOTICE('There are unapplied evolutions for %s.'
                           % app.__name__.split('.')[-2])

    # Evolutions are checked over the entire project, so we only need to check
    # once. We do this check when Django Evolutions itself is synchronized.
    if app == django_evolution:
        old_proj_sig = pickle.loads(str(latest_version.signature))

        # If any models or apps have been added, a baseline must be set
        # for those new models
        changed = False
        new_apps = []

        for app_name, new_app_sig in proj_sig.items():
            if app_name == '__version__':
                # Ignore the __version__ tag
                continue

            old_app_sig = old_proj_sig.get(app_name, None)

            if old_app_sig is None:
                # App has been added
                old_proj_sig[app_name] = proj_sig[app_name]
                new_apps.append(app_name)
                changed = True
            else:
                for model_name, new_model_sig in new_app_sig.items():
                    old_model_sig = old_app_sig.get(model_name, None)

                    if old_model_sig is None:
                        # Model has been added
                        old_proj_sig[app_name][model_name] = \
                            proj_sig[app_name][model_name]
                        changed = True

        if changed:
            if verbosity > 0:
                print "Adding baseline version for new models"

            latest_version = \
                django_evolution.Version(signature=pickle.dumps(old_proj_sig))
            latest_version.save(**using_args)

            for app_name in new_apps:
                install_baseline(get_app(app_name), latest_version, using_args,
                                 verbosity)

        # TODO: Model introspection step goes here.
        # # If the current database state doesn't match the last
        # # saved signature (as reported by latest_version),
        # # then we need to update the Evolution table.
        # actual_sig = introspect_project_sig()
        # acutal = pickle.dumps(actual_sig)
        # if actual != latest_version.signature:
        #     nudge = Version(signature=actual)
        #     nudge.save()
        #     latest_version = nudge

        diff = Diff(old_proj_sig, proj_sig)

        if not diff.is_empty():
            print style.NOTICE(
                'Project signature has changed - an evolution is required')

            if verbosity > 1:
                old_proj_sig = pickle.loads(str(latest_version.signature))
                print diff
Пример #11
0
        evolution_required = False
        simulated = True
        sql = []
        new_evolutions = []

        current_proj_sig = create_project_sig(database)
        current_signature = pickle.dumps(current_proj_sig)

        try:
            if is_multi_db():
                latest_version = Version.objects.using(database).latest('when')
            else:
                latest_version = Version.objects.latest('when')

            database_sig = pickle.loads(str(latest_version.signature))
            diff = Diff(database_sig, current_proj_sig)
        except Evolution.DoesNotExist:
            raise CommandError("Can't evolve yet. Need to set an "
                               "evolution baseline.")

        try:
            for app in app_list:
                app_label = app.__name__.split('.')[-2]
                if hint:
                    evolutions = []
                    hinted_evolution = diff.evolution()
                    temp_mutations = hinted_evolution.get(app_label, [])
                else:
                    evolutions = get_unapplied_evolutions(app, database)
                    temp_mutations = get_mutations(app, evolutions, database)
Пример #12
0
    def test_change_with_add_same_name_other_model(self):
        """Testing ChangeField with same field name as that added in
        another model
        """
        class OtherModel(models.Model):
            int_field = models.IntegerField()
            test_field = models.CharField(max_length=32, null=True)

        class OtherDestModel(models.Model):
            int_field = models.IntegerField()
            test_field = models.CharField(max_length=32, null=False)

        class DestModel(models.Model):
            my_id = models.AutoField(primary_key=True)
            alt_pk = models.IntegerField()
            int_field = models.IntegerField(db_column='custom_db_column')
            int_field1 = models.IntegerField(db_index=True)
            int_field2 = models.IntegerField(db_index=False)
            int_field3 = models.IntegerField(unique=True)
            int_field4 = models.IntegerField(unique=False)
            char_field = models.CharField(max_length=20)
            char_field1 = models.CharField(max_length=25, null=True)
            char_field2 = models.CharField(max_length=30, null=False)
            m2m_field1 = models.ManyToManyField(
                ChangeAnchor1, db_table='change_field_non-default_m2m_table')
            test_field = models.CharField(max_length=32, null=False)

        self.set_base_model(self.default_base_model,
                            pre_extra_models=[('OtherModel', OtherModel),
                                              ('ChangeAnchor1', ChangeAnchor1)
                                              ])

        end, end_sig = self.make_end_signatures(DestModel, 'TestModel')
        end2, end_sig2 = self.make_end_signatures(OtherDestModel, 'OtherModel')

        end.update(end2)

        end_app_sig = end_sig.get_app_sig('tests')
        end_app_sig2 = end_sig2.get_app_sig('tests')

        for model_sig in end_app_sig2.model_sigs:
            end_app_sig.add_model_sig(model_sig.clone())

        d = self.perform_diff_test(
            end_sig, ("In model tests.OtherModel:\n"
                      "    In field 'test_field':\n"
                      "        Property 'null' has changed\n"
                      "In model tests.TestModel:\n"
                      "    Field 'test_field' has been added"),
            [
                "ChangeField('OtherModel', 'test_field',"
                " initial=<<USER VALUE REQUIRED>>, null=False)",
                "AddField('TestModel', 'test_field', models.CharField,"
                " initial=<<USER VALUE REQUIRED>>, max_length=32)",
            ])

        test_sig = self.start_sig.clone()
        app_mutator = AppMutator(app_label='tests',
                                 project_sig=test_sig,
                                 database_state=self.database_state)
        evolutions = d.evolution()['tests']
        app_mutator.run_mutations(evolutions)

        d = Diff(self.start_sig, test_sig)

        self.assertEqual(str(d), ("In model tests.OtherModel:\n"
                                  "    In field 'test_field':\n"
                                  "        Property 'null' has changed\n"
                                  "In model tests.TestModel:\n"
                                  "    Field 'test_field' has been added"))
Пример #13
0
def _on_app_models_updated(app, verbosity=1, using=DEFAULT_DB_ALIAS, **kwargs):
    """Handler for when an app's models were updated.

    This is called in response to a syncdb or migrate operation for an app.
    It will install baselines for any new models, record the changes in the
    evolution history, and notify the user if any of the changes require an
    evolution.

    Args:
        app (module):
            The app models module that was updated.

        verbosity (int, optional):
            The verbosity used to control output. This will have been provided
            by the syncdb or migrate command.

        using (str, optional):
            The database being updated.

        **kwargs (dict):
            Additional keyword arguments provided by the signal handler for
            the syncdb or migrate operation.
    """
    project_sig = ProjectSignature.from_database(using)

    try:
        latest_version = Version.objects.current_version(using=using)
    except Version.DoesNotExist:
        # We need to create a baseline version.
        if verbosity > 0:
            print("Installing baseline version")

        latest_version = Version(signature=project_sig)
        latest_version.save(using=using)

        for a in get_apps():
            _install_baseline(app=a,
                              latest_version=latest_version,
                              using=using,
                              verbosity=verbosity)

    unapplied = get_unapplied_evolutions(app, using)

    if unapplied:
        print(
            style.NOTICE('There are unapplied evolutions for %s.' %
                         get_app_label(app)))

    # Evolutions are checked over the entire project, so we only need to check
    # once. We do this check when Django Evolutions itself is synchronized.

    if app is get_app('django_evolution'):
        old_project_sig = latest_version.signature

        # If any models or apps have been added, a baseline must be set
        # for those new models
        changed = False
        new_apps = []

        for new_app_sig in project_sig.app_sigs:
            app_id = new_app_sig.app_id
            old_app_sig = old_project_sig.get_app_sig(app_id)

            if old_app_sig is None:
                # App has been added
                old_project_sig.add_app_sig(new_app_sig.clone())
                new_apps.append(app_id)
                changed = True
            else:
                for new_model_sig in new_app_sig.model_sigs:
                    model_name = new_model_sig.model_name

                    old_model_sig = old_app_sig.get_model_sig(model_name)

                    if old_model_sig is None:
                        # Model has been added
                        old_app_sig.add_model_sig(
                            project_sig.get_app_sig(app_id).get_model_sig(
                                model_name).clone())
                        changed = True

        if changed:
            if verbosity > 0:
                print("Adding baseline version for new models")

            latest_version = Version(signature=old_project_sig)
            latest_version.save(using=using)

            for app_name in new_apps:
                app = get_app(app_name, True)

                if app:
                    _install_baseline(app=app,
                                      latest_version=latest_version,
                                      using=using,
                                      verbosity=verbosity)

        # TODO: Model introspection step goes here.
        # # If the current database state doesn't match the last
        # # saved signature (as reported by latest_version),
        # # then we need to update the Evolution table.
        # actual_sig = introspect_project_sig()
        # acutal = pickle.dumps(actual_sig)
        # if actual != latest_version.signature:
        #     nudge = Version(signature=actual)
        #     nudge.save()
        #     latest_version = nudge

        diff = Diff(old_project_sig, project_sig)

        if not diff.is_empty():
            print(
                style.NOTICE(
                    'Project signature has changed - an evolution is required')
            )

            if verbosity > 1:
                print(diff)
Пример #14
0
def evolution(app, created_models, verbosity=1, **kwargs):
    """
    A hook into syncdb's post_syncdb signal, that is used to notify the user
    if a model evolution is necessary.
    """
    proj_sig = create_project_sig()
    signature = pickle.dumps(proj_sig)

    try:
        latest_version = django_evolution.Version.objects.latest('when')
    except django_evolution.Version.DoesNotExist:
        # We need to create a baseline version.
        if verbosity > 0:
            print "Installing baseline version"

        latest_version = django_evolution.Version(signature=signature)
        latest_version.save()

        for a in get_apps():
            app_label = a.__name__.split('.')[-2]
            sequence = get_evolution_sequence(a)
            if sequence:
                if verbosity > 0:
                    print 'Evolutions in %s baseline:' % app_label, ', '.join(
                        sequence)
            for evo_label in sequence:
                evolution = django_evolution.Evolution(app_label=app_label,
                                                       label=evo_label,
                                                       version=latest_version)
                evolution.save()

    unapplied = get_unapplied_evolutions(app)
    if unapplied:
        print style.NOTICE('There are unapplied evolutions for %s.' %
                           app.__name__.split('.')[-2])

    # Evolutions are checked over the entire project, so we only need to
    # check once. We do this check when Django Evolutions itself is synchronized.
    if app == django_evolution:
        old_proj_sig = pickle.loads(str(latest_version.signature))

        # If any models have been added, a baseline must be set
        # for those new models
        changed = False
        for app_name, new_app_sig in proj_sig.items():
            if app_name == '__version__':
                # Ignore the __version__ tag
                continue
            old_app_sig = old_proj_sig.get(app_name, None)
            if old_app_sig is None:
                # App has been added
                old_proj_sig[app_name] = proj_sig[app_name]
                changed = True
                continue
            for model_name, new_model_sig in new_app_sig.items():
                old_model_sig = old_app_sig.get(model_name, None)
                if old_model_sig is None:
                    # Model has been added
                    old_proj_sig[app_name][model_name] = proj_sig[app_name][
                        model_name]
                    changed = True

        if changed:
            if verbosity > 0:
                print "Adding baseline version for new models"
            latest_version = django_evolution.Version(
                signature=pickle.dumps(old_proj_sig))
            latest_version.save()

        # TODO: Model introspection step goes here.
        # # If the current database state doesn't match the last
        # # saved signature (as reported by latest_version),
        # # then we need to update the Evolution table.
        # actual_sig = introspect_project_sig()
        # acutal = pickle.dumps(actual_sig)
        # if actual != latest_version.signature:
        #     nudge = Version(signature=actual)
        #     nudge.save()
        #     latest_version = nudge

        diff = Diff(old_proj_sig, proj_sig)
        if not diff.is_empty():
            print style.NOTICE(
                'Project signature has changed - an evolution is required')
            if verbosity > 1:
                old_proj_sig = pickle.loads(str(latest_version.signature))
                print diff