Пример #1
0
    def test_clone(self):
        """Testing DatabaseState.clone"""
        database_state = DatabaseState(db_name='default')
        cloned_state = database_state.clone()

        self.assertEqual(cloned_state.db_name, database_state.db_name)
        self.assertEqual(cloned_state._tables, database_state._tables)
    def test_clone(self):
        """Testing DatabaseState.clone"""
        database_state = DatabaseState(db_name='default')
        cloned_state = database_state.clone()

        self.assertEqual(cloned_state.db_name, database_state.db_name)
        self.assertEqual(cloned_state._tables, database_state._tables)
Пример #3
0
class EvolutionTestCase(TestCase):
    """Base class for test cases that need to evolve the database."""

    default_model_name = 'TestModel'
    default_base_model = None
    default_pre_extra_models = []
    default_extra_models = []

    def setUp(self):
        super(EvolutionTestCase, self).setUp()

        self.base_model = None
        self.pre_extra_models = []
        self.extra_models = []
        self.start = None
        self.start_sig = None
        self.database_state = None
        self.test_database_state = None
        self._models_registered = False

        if self.default_base_model:
            self.set_base_model(base_model=self.default_base_model,
                                pre_extra_models=self.default_pre_extra_models,
                                extra_models=self.default_extra_models)

    def tearDown(self):
        if self._models_registered:
            unregister_app('tests')

        super(EvolutionTestCase, self).tearDown()

    def default_create_test_data(self, db_name):
        """Default function for creating test data for base models.

        By default, this won't do anything.

        Args:
            db_name (unicode):
                The name of the database to create models on.
        """
        pass

    def set_base_model(self,
                       base_model,
                       name=None,
                       extra_models=[],
                       pre_extra_models=[],
                       db_name=None):
        """Set the base model(s) that will be mutated in a test.

        These models will be registered in Django's model registry and
        queued up to be written to the database. Starting signatures based
        on these models will be provided, which the test is expected to
        mutate.

        Args:
            base_model (type):
                The base :py:class:`~django.db.models.Model` to register and
                write to the database that the test will then mutate.

            name (unicode, optional):
                The name to register for the model. This defaults to
                :py:attr:`default_model_name`.

            extra_models (list of type, optional):
                The list of extra models to register and write to the
                database after writing ``base_model``. These may form
                relations to ``base_model``.

            pre_extra_models (list of type, optional):
                The list of extra models to write to the database before
                writing ``base_model``. ``base_model`` may form relations to
                these models.

            db_name (unicode, optional):
                The name of the database to write the models to. This
                defaults to :py:attr:`default_database_name`.
        """
        name = name or self.default_model_name
        db_name = db_name or self.default_database_name

        if self.base_model:
            unregister_app('tests')

        self.base_model = base_model
        self.pre_extra_models = pre_extra_models
        self.extra_models = extra_models
        self.database_state = DatabaseState(db_name)

        self.start = self.register_model(model=base_model,
                                         name=name,
                                         register_indexes=True,
                                         db_name=db_name)
        self.start_sig = self.create_test_proj_sig(model=base_model, name=name)

    def make_end_signatures(self, dest_model, model_name, db_name=None):
        """Return signatures for a model representing the end of a mutation.

        Callers should construct a model that reflects the expected result
        of any mutations and provide that. This will register that model
        and construct a signature from it.

        Args:
            dest_model (type):
                The destination :py:class:`~django.db.models.Model`
                representing the expected result of an evolution.

            model_name (unicode):
                The name to register for the model.

            db_name (unicode, optional):
                The name of the database to write the models to. This
                defaults to :py:attr:`default_database_name`.

        Returns:
            tuple:
            A tuple containing:

            1. A :py:class:`collections.OrderedDict` mapping the model name
               to the model class.
            2. A :py:class:`django_evolution.signature.ProjectSignature`
               for the provided model.
        """
        db_name = db_name or self.default_database_name

        end = self.register_model(model=dest_model,
                                  name=model_name,
                                  db_name=db_name)
        end_sig = self.create_test_proj_sig(model=dest_model, name=model_name)

        return end, end_sig

    def perform_evolution_tests(self,
                                dest_model,
                                evolutions,
                                diff_text=None,
                                expected_hint=None,
                                sql_name=None,
                                model_name=None,
                                end=None,
                                end_sig=None,
                                expect_noop=False,
                                rescan_indexes=True,
                                use_hinted_evolutions=False,
                                perform_simulations=True,
                                perform_mutations=True,
                                db_name=None,
                                create_test_data_func=None):
        """Perform test evolutions and validate results.

        This is used for most common evolution-related tests. It handles
        generating signatures for a base model and an expected post-evolution
        model, ensuring that the mutations result in an empty diff.

        It then optionally simulates the evolutions on the signatures
        (using :py:meth:`perform_simulations)`, and then optionally performs
        the actual evolutions on the database (using
        :py:meth:`perform_mutations`), verifying all results.

        Args:
            dest_model (type):
                The destination :py:class:`~django.db.models.Model`
                representing the expected result of an evolution.

            evolutions (list of django_evolution.mutations.BaseMutation):
                The combined series of evolutions (list of mutations) to apply
                to the base model.

            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.

            sql_name (unicode, optional):
                The name of the registered SQL content for the database being
                tested.

                This must be provided if ``perform_mutations`` is ``True`.

            model_name (unicode, optional):
                The name of the model to register. This defaults to
                :py:attr:`default_model_name`.

            end (collections.OrderedDict, optional):
                The expected model map at the end of the evolution. This
                is generated by :py:meth:`make_end_signatures`.

                If not provided, one will be generated.

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

                If not provided, one will be generated.

            expect_noop (bool, optional):
                Whether the evolution is expected not to change anything.

            rescan_indexes (bool, optional):
                Whether to re-scan the list of table indexes after performing
                mutations.

                This is ignored if ``perform_mutations`` is ``False``.

            use_hinted_evolutions (bool, optional):
                Whether to use the hinted evolutions generated by the
                signatures. This cannot be used if ``evolutions`` is
                non-empty.

            perform_simulations (bool, optional):
                Whether to simulate the evolution and compare results.

            perform_mutations (bool, optional):
                Whether to apply the mutations and compare results.

            db_name (unicode, optional):
                The name of the database to apply evolutions to. This
                defaults to :py:attr:`default_database_name`.

            create_test_data_func (callable, optional):
                A function to call in order to create data in the database for
                the initial models, before applying mutations. It must take
                a database name.

                If not provided, :py:meth:`default_create_test_data` will be
                used.

        Raises:
            AssertionError:
                A diff, simulation, or mutation test has failed.
        """
        model_name = model_name or self.default_model_name
        db_name = db_name or self.default_database_name

        if end is None or end_sig is None:
            end, end_sig = self.make_end_signatures(dest_model=dest_model,
                                                    model_name=model_name,
                                                    db_name=db_name)

        # See if the diff between signatures contains the contents we expect.
        d = self.perform_diff_test(end_sig=end_sig,
                                   diff_text=diff_text,
                                   expected_hint=expected_hint,
                                   expect_empty=expect_noop)

        if use_hinted_evolutions:
            assert not evolutions, (
                'The evolutions= argument cannot be provided when providing '
                'use_hinted_evolutions=True')

            evolutions = d.evolution()['tests']

        if perform_simulations:
            self.perform_simulations(evolutions=evolutions,
                                     end_sig=end_sig,
                                     db_name=db_name)

        if perform_mutations:
            self.perform_mutations(
                evolutions=evolutions,
                end=end,
                end_sig=end_sig,
                sql_name=sql_name,
                rescan_indexes=rescan_indexes,
                db_name=db_name,
                create_test_data_func=(create_test_data_func
                                       or self.default_create_test_data))

    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

    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

    def perform_mutations(self,
                          evolutions,
                          end,
                          end_sig,
                          sql_name=None,
                          rescan_indexes=True,
                          db_name=None,
                          create_test_data_func=None):
        """Apply mutations that and verify the results.

        This will run through the evolution chain, applying each mutation
        on the database and against the signature, and then verifying the
        resulting signature and generated SQL.

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

            end (collections.OrderedDict):
                The expected model map at the end of the evolution. This
                is generated by :py:meth:`make_end_signatures`.

            end_sig (django_evolution.signature.ProjectSignature):
                The expected ending signature. This is generated by
                :py:meth:`make_end_signatures`.

            sql_name (unicode, optional):
                The name of the registered SQL content for the database being
                tested. If not provided, the SQL won't be compared.

            rescan_indexes (bool, optional):
                Whether to re-scan the list of table indexes after applying
                the mutations.

            db_name (unicode, optional):
                The name of the database to apply the evolutions against.

            create_test_data_func (callable, optional):
                A function to call in order to create data in the database for
                the initial models, before applying mutations. It must take
                a database name.

        Raises:
            AssertionError:
                The resulting SQL did not match.

            django.db.utils.OperationalError:
                There was an error executing SQL.
        """
        app_label = 'tests'

        def run_mutations():
            if rescan_indexes:
                self.test_database_state.rescan_tables()

            app_mutator = AppMutator(app_label=app_label,
                                     project_sig=test_sig,
                                     database_state=self.test_database_state,
                                     database=db_name)
            app_mutator.run_mutations(evolutions)

            return app_mutator.to_sql()

        db_name = db_name or self.default_database_name

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

        with ensure_test_db(model_entries=six.iteritems(self.start),
                            end_model_entries=six.iteritems(end),
                            app_label=app_label,
                            database=db_name):
            if create_test_data_func:
                create_test_data_func(db_name)

            sql = execute_test_sql(run_mutations(), database=db_name)

        if sql_name is not None:
            self.assertSQLMappingEqual(sql, sql_name, database=db_name)

    def register_model(self, model, name, db_name=None, **kwargs):
        """Register a model for the test.

        This will register not only this model, but any models in
        :py:attr:`pre_extra_models` and :py:attr:`extra_models`. It will
        not include :py:attr:`base_model`.

        Args:
            model (type):
                The main :py:class:`~django.db.models.Model` to register.

            name (unicode):
                The name to use when for the model when registering. This
                doesn't have to match the model's actual name.

            db_name (unicode, optional):
                The name of the database to register this model on.

            **kwargs (dict):
                Additional keyword arguments to pass to
                :py:func:`~django_evolution.tests.utils.register_models`.

        Returns:
            collections.OrderedDict:
            A dictionary of registered models. The keys are model names, and
            the values are the models.
        """
        self._models_registered = True

        models = self.pre_extra_models + [(name, model)] + self.extra_models

        return register_models(database_state=self.database_state,
                               models=models,
                               new_app_label='tests',
                               db_name=db_name or self.default_database_name,
                               **kwargs)

    def create_test_proj_sig(self,
                             model,
                             name,
                             extra_models=[],
                             pre_extra_models=[]):
        """Create a project signature for the given models.

        The signature will include not only these models, but any models in
        :py:attr:`pre_extra_models` and :py:attr:`extra_models`. It will
        not include :py:attr:`base_model`.

        Args:
            model (type):
                The main :py:class:`~django.db.models.Model` to include
                in the signature.

            name (unicode):
                The name to use when for the model. This doesn't have to match
                the model's actual name.

            extra_models (list of type, optional):
                An additional list of extra models to register after ``model``
                (but before the class-defined :py:attr:`extra_models`).

            pre_extra_models (list of type, optional):
                An additional list of extra models to register before ``model``
                (but after :py:attr:`pre_extra_models`).

        Returns:
            django_evolution.signature.ProjectSignature:
            The generated project signature.
        """
        return create_test_project_sig(
            models=(self.pre_extra_models + pre_extra_models +
                    [(name, model)] + extra_models + self.extra_models))

    def copy_models(self, models):
        """Copy a list of models.

        This will be a deep copy, allowing any of the copied models to be
        altered without affecting the originals.

        Args:
            models (list of type):
                The list of :py:class:`~django.db.models.Model` classes.

        Returns:
            list of type:
            The copied list of :py:class:`~django.db.models.Model` classes.
        """
        return copy.deepcopy(models)

    @contextmanager
    def override_db_routers(self, routers):
        """Override database routers for a test.

        This clears the router cache before and after the test, allowing
        custom routers to be used during unit tests.

        Args:
            routers (list):
                The list of router class paths or instances.

        Yields:
            The context.
        """
        try:
            with override_settings(DATABASE_ROUTERS=routers):
                self.clear_routers_cache()
                yield
        finally:
            self.clear_routers_cache()

    def clear_routers_cache(self):
        """Clear the router cache."""
        router.routers = ConnectionRouter().routers
Пример #4
0
class EvolutionTestCase(TestCase):
    """Base class for test cases that need to evolve the database."""

    sql_mapping_key = None
    default_database_name = 'default'
    default_model_name = 'TestModel'
    default_base_model = None
    default_pre_extra_models = []
    default_extra_models = []

    # Allow for diffs for large dictionary structures, to help debug
    # signature failures.
    maxDiff = 10000

    def setUp(self):
        self.base_model = None
        self.pre_extra_models = []
        self.extra_models = []
        self.start = None
        self.start_sig = None
        self.database_state = None
        self.test_database_state = None
        self._models_registered = False

        if self.default_base_model:
            self.set_base_model(self.default_base_model,
                                pre_extra_models=self.default_pre_extra_models,
                                extra_models=self.default_extra_models)

    def tearDown(self):
        if self._models_registered:
            unregister_app('tests')

    def set_base_model(self, model, name=None, extra_models=[],
                       pre_extra_models=[], db_name=None):
        name = name or self.default_model_name
        db_name = db_name or self.default_database_name

        if self.base_model:
            unregister_app('tests')

        self.base_model = model
        self.pre_extra_models = pre_extra_models
        self.extra_models = extra_models
        self.database_state = DatabaseState(db_name)

        self.start = self.register_model(model, name,
                                         register_indexes=True,
                                         db_name=db_name)
        self.start_sig = self.create_test_proj_sig(model, name)

    def make_end_signatures(self, model, model_name, db_name=None):
        db_name = db_name or self.default_database_name

        end = self.register_model(model, name=model_name, db_name=db_name)
        end_sig = self.create_test_proj_sig(model, name=model_name)

        return end, end_sig

    def perform_evolution_tests(self, model, evolutions, diff_text=None,
                                expected_hint=None, sql_name=None,
                                model_name=None,
                                end=None, end_sig=None,
                                expect_noop=False,
                                rescan_indexes=True,
                                use_hinted_evolutions=False,
                                perform_simulations=True,
                                perform_mutations=True,
                                db_name=None):
        model_name = model_name or self.default_model_name
        db_name = db_name or self.default_database_name

        if end is None or end_sig is None:
            end, end_sig = self.make_end_signatures(model, model_name, db_name)

        # See if the diff between signatures contains the contents we expect.
        d = self.perform_diff_test(end_sig, diff_text, expected_hint,
                                   expect_empty=expect_noop)

        if use_hinted_evolutions:
            assert not evolutions
            evolutions = d.evolution()['tests']

        if perform_simulations:
            self.perform_simulations(evolutions, end_sig, db_name=db_name)

        if perform_mutations:
            self.perform_mutations(evolutions, end, end_sig, sql_name,
                                   rescan_indexes=rescan_indexes,
                                   db_name=db_name)

    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

    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_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))

    def perform_mutations(self, evolutions, end, end_sig, sql_name,
                          rescan_indexes=True, db_name=None):
        def run_mutations():
            if rescan_indexes:
                self.test_database_state.rescan_indexes()

            app_mutator = AppMutator(app_label='tests',
                                     project_sig=test_sig,
                                     database_state=self.test_database_state,
                                     database=db_name)
            app_mutator.run_mutations(evolutions)

            return app_mutator.to_sql()

        db_name = db_name or self.default_database_name

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

        sql = execute_test_sql(self.start, end, run_mutations,
                               database=db_name)

        if sql_name is not None:
            # Normalize the generated and expected SQL so that we are
            # guaranteed to have a list with one item per line.
            generated_sql = '\n'.join(sql).splitlines()
            expected_sql = self.get_sql_mapping(sql_name, db_name).splitlines()

            # Output the statements one-by-one, to help with diagnosing
            # differences.

            print()
            print("** Comparing SQL against '%s'" % sql_name)
            print('** Generated:')
            print()

            for line in generated_sql:
                print('    %s' % line)

            print()
            print('** Expected:')
            print()

            for line in expected_sql:
                print('    %s' % line)

            print()

            # Compare as lists, so that we can better spot inconsistencies.
            self.assertListEqual(generated_sql, expected_sql)

    def get_sql_mapping(self, name, db_name=None):
        db_name = db_name or self.default_database_name

        return get_sql_mappings(self.sql_mapping_key, db_name)[name]

    def register_model(self, model, name, db_name=None, **kwargs):
        self._models_registered = True

        models = self.pre_extra_models + [(name, model)] + self.extra_models

        return register_models(database_state=self.database_state,
                               models=models,
                               new_app_label='tests',
                               db_name=db_name or self.default_database_name,
                               **kwargs)

    def create_test_proj_sig(self, model, name, extra_models=[],
                             pre_extra_models=[]):
        return create_test_project_sig(models=(
            self.pre_extra_models + pre_extra_models + [(name, model)] +
            extra_models + self.extra_models
        ))

    def copy_models(self, models):
        return copy.deepcopy(models)

    @contextmanager
    def override_db_routers(self, routers):
        """Override database routers for a test.

        This clears the router cache before and after the test, allowing
        custom routers to be used during unit tests.

        Args:
            routers (list):
                The list of router class paths or instances.

        Yields:
            The context.
        """
        with override_settings(DATABASE_ROUTERS=routers):
            self.clear_routers_cache()
            yield

        self.clear_routers_cache()

    def clear_routers_cache(self):
        """Clear the router cache."""
        router.routers = ConnectionRouter().routers