def test_has_table(self):
        """Testing DatabaseState.has_table"""
        database_state = DatabaseState(db_name='default', scan=False)
        self.assertFalse(database_state.has_table('my_test_table'))

        database_state.add_table('my_test_table')
        self.assertTrue(database_state.has_table('my_test_table'))
    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_get_index_with_invalid_name(self):
        """Testing DatabaseState.get_index with invalid name"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        self.assertIsNone(database_state.get_index(table_name='my_test_table',
                                                   index_name='my_index'),)
Ejemplo n.º 4
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_add_table(self):
        """Testing DatabaseState.add_table"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        self.assertEqual(database_state._tables['my_test_table'], {
            'indexes': {},
        })
Ejemplo n.º 6
0
    def test_get_index_with_invalid_name(self):
        """Testing DatabaseState.get_index with invalid name"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        self.assertIsNone(
            database_state.get_index(table_name='my_test_table',
                                     index_name='my_index'), )
    def test_add_table(self):
        """Testing DatabaseState.add_table"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        self.assertEqual(database_state._tables['my_test_table'], {
            'indexes': {},
        })
    def test_find_index_with_not_found(self):
        """Testing DatabaseState.find_index with index no found"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        index = database_state.find_index(table_name='my_test_table',
                                          columns=['col1', 'col2'],
                                          unique=True)
        self.assertIsNone(index)
Ejemplo n.º 9
0
    def test_find_index_with_not_found(self):
        """Testing DatabaseState.find_index with index no found"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        index = database_state.find_index(table_name='my_test_table',
                                          columns=['col1', 'col2'],
                                          unique=True)
        self.assertIsNone(index)
    def test_add_index_with_existing_index(self):
        """Testing DatabaseState.add_index with existing index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='existing_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        expected_message = (
            'Unable to add index "existing_index" to table "my_test_table". '
            'This index already exists.'
        )

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.add_index(table_name='my_test_table',
                                     index_name='existing_index',
                                     columns=['col1', 'col2'],
                                     unique=True)

        # It's fine if it has a new name.
        database_state.add_index(table_name='my_test_table',
                                 index_name='new_index',
                                 columns=['col1', 'col2'],
                                 unique=True)
    def clear_indexes(self):
        """Testing DatabaseState.clear_indexes"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        self.assertEqual(database_state._tables['my_test_table']['indexes'],
                         {})
Ejemplo n.º 12
0
    def test_remove_index_with_untracked_table(self):
        """Testing DatabaseState.remove_index with untracked table"""
        database_state = DatabaseState(db_name='default', scan=False)

        expected_message = (
            'Unable to remove index "my_index" from table "my_test_table". '
            'The table is not being tracked in the database state.')

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.remove_index(table_name='my_test_table',
                                        index_name='my_index')
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    def test_add_index_with_untracked_table(self):
        """Testing DatabaseState.add_index with untracked table"""
        database_state = DatabaseState(db_name='default', scan=False)

        expected_message = (
            'Unable to add index "my_index" to table "my_test_table". The '
            'table is not being tracked in the database state.')

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.add_index(table_name='my_test_table',
                                     index_name='my_index',
                                     columns=['col1', 'col2'],
                                     unique=True)
    def test_remove_index_with_untracked_table(self):
        """Testing DatabaseState.remove_index with untracked table"""
        database_state = DatabaseState(db_name='default', scan=False)

        expected_message = (
            'Unable to remove index "my_index" from table "my_test_table". '
            'The table is not being tracked in the database state.'
        )

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.remove_index(table_name='my_test_table',
                                        index_name='my_index',
                                        unique=True)
    def test_remove_index_with_invalid_index_name(self):
        """Testing DatabaseState.remove_index with invalid index name"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')

        expected_message = (
            'Unable to remove index "my_index" from table "my_test_table". '
            'The index could not be found.'
        )

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.remove_index(table_name='my_test_table',
                                        index_name='my_index',
                                        unique=True)
    def test_add_index_with_untracked_table(self):
        """Testing DatabaseState.add_index with untracked table"""
        database_state = DatabaseState(db_name='default', scan=False)

        expected_message = (
            'Unable to add index "my_index" to table "my_test_table". The '
            'table is not being tracked in the database state.'
        )

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.add_index(table_name='my_test_table',
                                     index_name='my_index',
                                     columns=['col1', 'col2'],
                                     unique=True)
Ejemplo n.º 18
0
    def __init__(self, db_name, database_state=None):
        """Initialize the instance.

        Args:
            db_name (unicode):
                The name of the database.

            database_state (django_evolution.db.state.DatabaseState):
                The database state to track information through.
        """
        if database_state is None:
            from django_evolution.db.state import DatabaseState
            database_state = DatabaseState(db_name, scan=False)

        try:
            from django.db import connections
            engine = settings.DATABASES[db_name]['ENGINE'].split('.')[-1]
            connection = connections[db_name]
            module_name = ['django_evolution.db', engine]
            module = __import__('.'.join(module_name), {}, {}, [''])
            self.evolver = module.EvolutionOperations(database_state,
                                                      connection)
        except ImportError:
            if hasattr(settings, 'DATABASE_ENGINE'):
                module_name = ['django_evolution.db', settings.DATABASE_ENGINE]
                module = __import__('.'.join(module_name), {}, {}, [''])
                self.evolver = module.EvolutionOperations(database_state)
            else:
                raise
Ejemplo n.º 19
0
    def test_rescan_indexes(self):
        """Testing DatabaseState.rescan_indexes"""
        database_state = DatabaseState(db_name='default')

        # Check that a few known tables are in the list, to make sure
        # the scan worked.
        for table_name in ('django_content_type', 'django_evolution',
                           'django_project_version'):
            self.assertTrue(database_state.has_table(table_name))

        # Check the Evolution model.
        indexes = [
            (index_state.columns, index_state.unique)
            for index_state in database_state.iter_indexes('django_evolution')
        ]

        self.assertIn((['version_id'], False), indexes)
    def test_rescan_indexes(self):
        """Testing DatabaseState.rescan_indexes"""
        database_state = DatabaseState(db_name='default')

        # Check that a few known tables are in the list, to make sure
        # the scan worked.
        for table_name in ('django_content_type',
                           'django_evolution',
                           'django_project_version'):
            self.assertTrue(database_state.has_table(table_name))

        # Check the Evolution model.
        indexes = [
            (index_state.columns, index_state.unique)
            for index_state in database_state.iter_indexes('django_evolution')
        ]

        self.assertIn((['version_id'], False), indexes)
Ejemplo n.º 21
0
    def setUp(self):
        super(BaseRelationFieldsTestCase, self).setUp()

        register_models(database_state=DatabaseState(DEFAULT_DB_ALIAS),
                        models=[
                            ('CompatModelsTestModel', CompatModelsTestModel),
                            ('CompatModelsAnchor', CompatModelsAnchor),
                        ],
                        new_app_label='tests')
Ejemplo n.º 22
0
    def test_has_model(self):
        """Testing DatabaseState.has_model"""
        database_state = DatabaseState(db_name='default', scan=False)
        self.assertFalse(database_state.has_model(Evolution))

        database_state.rescan_tables()
        self.assertTrue(database_state.has_model(Evolution))
Ejemplo n.º 23
0
    def test_has_table(self):
        """Testing DatabaseState.has_table"""
        database_state = DatabaseState(db_name='default', scan=False)
        self.assertFalse(database_state.has_table('my_test_table'))

        database_state.add_table('my_test_table')
        self.assertTrue(database_state.has_table('my_test_table'))
Ejemplo n.º 24
0
    def test_remove_index(self):
        """Testing DatabaseState.remove_index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'])

        database_state.remove_index(table_name='my_test_table',
                                    index_name='my_index')

        self.assertEqual(database_state._tables['my_test_table'], {
            'indexes': {},
            'unique_indexes': {},
        })
    def test_iter_indexes(self):
        """Testing DatabaseState.iter_indexes"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index1',
                                 columns=['col1', 'col2'],
                                 unique=True)
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index2',
                                 columns=['col3'])

        indexes = list(database_state.iter_indexes('my_test_table'))

        self.assertEqual(indexes, [
            IndexState(name='my_index1', columns=['col1', 'col2'],
                       unique=True),
            IndexState(name='my_index2', columns=['col3'], unique=False),
        ])
Ejemplo n.º 26
0
    def test_find_index(self):
        """Testing DatabaseState.find_index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'])

        index = database_state.find_index(table_name='my_test_table',
                                          columns=['col1', 'col2'])
        self.assertEqual(index,
                         IndexState(name='my_index', columns=['col1', 'col2']))
    def test_get_index(self):
        """Testing DatabaseState.get_index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        self.assertEqual(
            database_state.get_index(table_name='my_test_table',
                                     index_name='my_index'),
            IndexState(name='my_index', columns=['col1', 'col2'], unique=True))
Ejemplo n.º 28
0
    def test_remove_index_with_invalid_index_type(self):
        """Testing DatabaseState.remove_index with invalid index type"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        expected_message = (
            'Unable to remove index "my_index" from table "my_test_table". '
            'The index could not be found.')

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.remove_index(table_name='my_test_table',
                                        index_name='my_index',
                                        unique=False)
Ejemplo n.º 29
0
    def test_add_index_with_existing_index(self):
        """Testing DatabaseState.add_index with existing index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='existing_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        expected_message = (
            'Unable to add index "existing_index" to table "my_test_table". '
            'This index already exists.')

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.add_index(table_name='my_test_table',
                                     index_name='existing_index',
                                     columns=['col1', 'col2'],
                                     unique=True)

        # It's fine if it has a new name.
        database_state.add_index(table_name='my_test_table',
                                 index_name='new_index',
                                 columns=['col1', 'col2'],
                                 unique=True)
Ejemplo n.º 30
0
    def test_has_model_with_auto_created(self):
        """Testing DatabaseState.has_model with auto-created model"""
        model = get_remote_field(User._meta.get_field('groups')).through
        self.assertTrue(model._meta.auto_created)

        database_state = DatabaseState(db_name='default', scan=False)
        self.assertFalse(database_state.has_model(model))

        database_state.rescan_tables()
        self.assertTrue(database_state.has_model(model))
    def clear_indexes(self):
        """Testing DatabaseState.clear_indexes"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        self.assertEqual(database_state._tables['my_test_table']['indexes'],
                         {})
Ejemplo n.º 32
0
    def test_find_index_with_unique_true(self):
        """Testing DatabaseState.find_index with unique=True"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        index = database_state.find_index(table_name='my_test_table',
                                          columns=['col1', 'col2'],
                                          unique=True)
        self.assertEqual(
            index,
            IndexState(name='my_index', columns=['col1', 'col2'], unique=True))
Ejemplo n.º 33
0
    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 test_get_index(self):
        """Testing DatabaseState.get_index"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        self.assertEqual(
            database_state.get_index(table_name='my_test_table',
                                     index_name='my_index'),
            IndexState(name='my_index',
                       columns=['col1', 'col2'],
                       unique=True))
    def test_remove_index_with_invalid_index_type(self):
        """Testing DatabaseState.remove_index with invalid index type"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        expected_message = (
            'Unable to remove index "my_index" from table "my_test_table". '
            'The specified index type (unique=False) does not match the '
            'existing type (unique=True).'
        )

        with self.assertRaisesMessage(DatabaseStateError, expected_message):
            database_state.remove_index(table_name='my_test_table',
                                        index_name='my_index',
                                        unique=False)
    def test_iter_indexes(self):
        """Testing DatabaseState.iter_indexes"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index1',
                                 columns=['col1', 'col2'],
                                 unique=True)
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index2',
                                 columns=['col3'])

        indexes = list(database_state.iter_indexes('my_test_table'))

        self.assertEqual(
            indexes,
            [
                IndexState(name='my_index1',
                           columns=['col1', 'col2'],
                           unique=True),
                IndexState(name='my_index2',
                           columns=['col3'],
                           unique=False),
            ])
Ejemplo n.º 37
0
    def test_apply_migrations(self):
        """Testing apply_migrations"""
        database_state = DatabaseState(db_name=DEFAULT_DB_ALIAS)
        register_models(database_state=database_state,
                        models=[('MigrationTestModel', MigrationTestModel)])

        app_migrations = [
            InitialMigration('0001_initial', 'tests'),
            AddFieldMigration('0002_add_field', 'tests'),
        ]

        targets = [
            ('tests', '0001_initial'),
            ('tests', '0002_add_field'),
        ]

        custom_migrations = MigrationList()
        custom_migrations.add_migration(app_migrations[0])
        custom_migrations.add_migration(app_migrations[1])

        connection = connections[DEFAULT_DB_ALIAS]
        executor = MigrationExecutor(connection,
                                     custom_migrations=custom_migrations)

        migrate_state = apply_migrations(
            executor=executor,
            targets=targets,
            plan=[
                (app_migrations[0], False),
                (app_migrations[1], False),
            ],
            pre_migrate_state=create_pre_migrate_state(executor))
        finalize_migrations(migrate_state)

        # Make sure this is in the database now.
        MigrationTestModel.objects.create(field1=123,
                                          field2='abc',
                                          field3=True)
Ejemplo n.º 38
0
    def test_add_index_with_unique_true(self):
        """Testing DatabaseState.add_index with unique=True"""
        database_state = DatabaseState(db_name='default', scan=False)
        database_state.add_table('my_test_table')
        database_state.add_index(table_name='my_test_table',
                                 index_name='my_index',
                                 columns=['col1', 'col2'],
                                 unique=True)

        self.assertEqual(
            database_state._tables['my_test_table'], {
                'indexes': {},
                'unique_indexes': {
                    'my_index':
                    IndexState(name='my_index',
                               columns=['col1', 'col2'],
                               unique=True),
                },
            })
Ejemplo n.º 39
0
class Evolver(object):
    """The main class for managing database evolutions.

    The evolver is used to queue up tasks that modify the database. These
    allow for evolving database models and purging applications across an
    entire Django project or only for specific applications. Custom tasks
    can even be written by an application if very specific database
    operations need to be made outside of what's available in an evolution.

    Tasks are executed in order, but batched by the task type. That is, if
    two instances of ``TaskType1`` are queued, followed by an instance of
    ``TaskType2``, and another of ``TaskType1``, all 3 tasks of ``TaskType1``
    will be executed at once, with the ``TaskType2`` task following.

    Callers are expected to create an instance and queue up one or more tasks.
    Once all tasks are queued, the changes can be made using :py:meth:`evolve`.
    Alternatively, evolution hints can be generated using
    :py:meth:`generate_hints`.

    Projects will generally utilize this through the existing ``evolve``
    Django management command.

    Attributes:
        connection (django.db.backends.base.base.BaseDatabaseWrapper):
            The database connection object being used for the evolver.

        database_name (unicode):
            The name of the database being evolved.

        database_state (django_evolution.db.state.DatabaseState):
            The state of the database, for evolution purposes.

        evolved (bool):
            Whether the evolver has already performed its evolutions. These
            can only be done once per evolver.

        hinted (bool):
            Whether the evolver is operating against hinted evolutions. This
            may result in changes to the database without there being any
            accompanying evolution files backing those changes.

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

        initial_diff (django_evolution.diff.Diff):
            The initial diff between the stored project signature and the
            current project signature.

        project_sig (django_evolution.signature.ProjectSignature):
            The project signature. This will start off as the previous
            signature stored in the database, but will be modified when
            mutations are simulated.

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

        version (django_evolution.models.Version):
            The project version entry saved as the result of any evolution
            operations. This contains the current version of the project
            signature. It may be ``None`` until :py:meth:`evolve` is called.
    """
    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)

    @property
    def tasks(self):
        """A list of all tasks that will be performed.

        This can only be accessed after all necessary tasks have been queued.
        """
        # If a caller is interested in the list of tasks, then it's likely
        # interested in state on those tasks. That means we'll need to prepare
        # all the tasks before we can return any of them.
        self._prepare_tasks()

        return six.itervalues(self._tasks_by_id)

    def can_simulate(self):
        """Return whether all queued tasks can be simulated.

        If any tasks cannot be simulated (for instance, a hinted evolution
        requiring manually-entered values), then this will return ``False``.

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

        Returns:
            bool:
            ``True`` if all queued tasks can be simulated. ``False`` if any
            cannot.
        """
        return all(task.can_simulate or not task.evolution_required
                   for task in self.tasks)

    def get_evolution_required(self):
        """Return whether there are any evolutions required.

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

        Returns:
            bool:
            ``True`` if any tasks require evolution. ``False`` if none do.
        """
        return any(task.evolution_required for task in self.tasks)

    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)

    def iter_evolution_content(self):
        """Generate the evolution content for all queued tasks.

        This will loop through each tasks and yield any evolution content
        provided.

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

        Yields:
            tuple:
            A tuple of ``(task, evolution_content)``.
        """
        for task in self.tasks:
            content = task.get_evolution_content()

            if content:
                yield task, content

    def queue_evolve_all_apps(self):
        """Queue an evolution of all registered Django apps.

        This cannot be used if :py:meth:`queue_evolve_app` is also being used.

        Raises:
            django_evolution.errors.EvolutionTaskAlreadyQueuedError:
                An evolution for an app was already queued.

            django_evolution.errors.QueueEvolverTaskError:
                Error queueing a non-duplicate task. Tasks may have already
                been prepared and finalized.
        """
        for app in get_apps():
            self.queue_evolve_app(app)

    def queue_evolve_app(self, app):
        """Queue an evolution of a registered Django app.

        Args:
            app (module):
                The Django app to queue an evolution for.

        Raises:
            django_evolution.errors.EvolutionTaskAlreadyQueuedError:
                An evolution for this app was already queued.

            django_evolution.errors.QueueEvolverTaskError:
                Error queueing a non-duplicate task. Tasks may have already
                been prepared and finalized.
        """
        try:
            self.queue_task(EvolveAppTask(self, app))
        except EvolutionTaskAlreadyQueuedError:
            raise EvolutionTaskAlreadyQueuedError(
                _('"%s" is already being tracked for evolution') %
                get_app_label(app))

    def queue_purge_old_apps(self):
        """Queue the purging of all old, stale Django apps.

        This will purge any apps that exist in the stored project signature
        but that are no longer registered in Django.

        This generally should not be used if :py:meth:`queue_purge_app` is also
        being used.

        Raises:
            django_evolution.errors.EvolutionTaskAlreadyQueuedError:
                A purge of an app was already queued.

            django_evolution.errors.QueueEvolverTaskError:
                Error queueing a non-duplicate task. Tasks may have already
                been prepared and finalized.
        """
        for app_label in self.initial_diff.deleted:
            self.queue_purge_app(app_label)

    def queue_purge_app(self, app_label):
        """Queue the purging of a Django app.

        Args:
            app_label (unicode):
                The label of the app to purge.

        Raises:
            django_evolution.errors.EvolutionTaskAlreadyQueuedError:
                A purge of this app was already queued.

            django_evolution.errors.QueueEvolverTaskError:
                Error queueing a non-duplicate task. Tasks may have already
                been prepared and finalized.
        """
        try:
            self.queue_task(PurgeAppTask(evolver=self, app_label=app_label))
        except EvolutionTaskAlreadyQueuedError:
            raise EvolutionTaskAlreadyQueuedError(
                _('"%s" is already being tracked for purging') % app_label)

    def queue_task(self, task):
        """Queue a task to run during evolution.

        This should only be directly called if working with custom tasks.
        Otherwise, use a more specific queue method.

        Args:
            task (BaseEvolutionTask):
                The task to queue.

        Raises:
            django_evolution.errors.EvolutionTaskAlreadyQueuedError:
                A purge of this app was already queued.

            django_evolution.errors.QueueEvolverTaskError:
                Error queueing a non-duplicate task. Tasks may have already
                been prepared and finalized.

        """
        assert task.id

        if self._tasks_prepared:
            raise QueueEvolverTaskError(
                _('Evolution tasks have already been prepared. New tasks '
                  'cannot be added.'))

        if task.id in self._tasks_by_id:
            raise EvolutionTaskAlreadyQueuedError(
                _('A task with ID "%s" is already queued.') % task.id)

        self._tasks_by_id[task.id] = task
        self._tasks_by_class.setdefault(type(task), []).append(task)

    def evolve(self):
        """Perform the evolution.

        This will run through all queued tasks and attempt to apply them in
        a database transaction, tracking each new batch of evolutions as the
        tasks finish.

        This can only be called once per evolver instance.

        Raises:
            django_evolution.errors.EvolutionException:
                Something went wrong during the evolution process. Details
                are in the error message. Note that a more specific exception
                may be raised.

            django_evolution.errors.EvolutionExecutionError:
                A specific evolution task failed. Details are in the error.
        """
        if self.evolved:
            raise EvolutionException(
                _('Evolver.evolve() has already been run once. It cannot be '
                  'run again.'))

        self._prepare_tasks()

        evolving.send(sender=self)

        try:
            new_evolutions = []

            for task_cls, tasks in six.iteritems(self._tasks_by_class):
                # Perform the evolution for the app. This is responsible
                # for raising any exceptions.
                task_cls.execute_tasks(evolver=self, tasks=tasks)

                for task in tasks:
                    new_evolutions += task.new_evolutions

                # Things may have changed, so rescan the database.
                self.database_state.rescan_tables()

            self._save_project_sig(new_evolutions=new_evolutions)
            self.evolved = True
        except Exception as e:
            evolving_failed.send(sender=self, exception=e)
            raise

        evolved.send(sender=self)

    def _prepare_tasks(self):
        """Prepare all queued tasks for further operations.

        Once prepared, no new tasks can be added. This will be done before
        performing any operations requiring state from queued tasks.
        """
        if not self._tasks_prepared:
            self._tasks_prepared = True

            for task_cls, tasks in six.iteritems(self._tasks_by_class):
                task_cls.prepare_tasks(evolver=self,
                                       tasks=tasks,
                                       hinted=self.hinted)

    def sql_executor(self, **kwargs):
        """Return an SQLExecutor for executing SQL.

        This is a convenience method for creating an
        :py:class:`~django_evolution.utils.sql.SQLExecutor` to operate using
        the evolver's current database.

        Version Added:
            2.1

        Args:
            **kwargs (dict):
                Additional keyword arguments used to construct the executor.

        Returns:
            django_evolution.utils.sql.SQLExecutor:
            The new SQLExecutor.
        """
        return SQLExecutor(database=self.database_name, **kwargs)

    @contextmanager
    def transaction(self):
        """Execute database operations in a transaction.

        This is a convenience method for executing in a transaction using
        the evolver's current database.

        Deprecated:
            2.1:
            This has been replaced with manual calls to
            :py:class:`~django_evolution.utils.sql.SQLExecutor`.

        Context:
            django.db.backends.util.CursorWrapper:
            The cursor used to execute statements.
        """
        with atomic(using=self.database_name):
            cursor = self.connection.cursor()

            try:
                yield cursor
            finally:
                cursor.close()

    def _save_project_sig(self, new_evolutions):
        """Save the project signature and any new evolutions.

        This will serialize the current modified project signature to the
        database and write any new evolutions, attaching them to the current
        project version.

        This can be called many times for one evolver instance. After the
        first time, the version already saved will simply be updated.

        Args:
            new_evolutions (list of django_evolution.models.Evolution):
                The list of new evolutions to save to the database.

        Raises:
            django_evolution.errors.EvolutionExecutionError:
                There was an error saving to the database.
        """
        version = self.version

        if version is None:
            version = Version(signature=self.project_sig)
            self.version = version

        try:
            version.save(using=self.database_name)

            if new_evolutions:
                for evolution in new_evolutions:
                    evolution.version = version

                Evolution.objects.using(
                    self.database_name).bulk_create(new_evolutions)
        except Exception as e:
            raise EvolutionExecutionError(
                _('Error saving new evolution version information: %s') % e,
                detailed_error=six.text_type(e))
Ejemplo n.º 40
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)
Ejemplo n.º 41
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
Ejemplo n.º 42
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