def test__find_milestone_revisions_branchless(self, walk_mock): revisions = [FakeRevision() for r in range(5)] revisions[2].module.neutron_milestone = [migration.LIBERTY] walk_mock.return_value = revisions m = cli._find_milestone_revisions(self.configs[0], 'liberty') self.assertEqual(1, len(m)) m = cli._find_milestone_revisions(self.configs[0], 'mitaka') self.assertEqual(0, len(m))
def test__find_milestone_revisions_one_branch(self, walk_mock): c_revs = [FakeRevision(labels={cli.CONTRACT_BRANCH}) for r in range(5)] c_revs[1].module.neutron_milestone = [migration.LIBERTY] walk_mock.return_value = c_revs m = cli._find_milestone_revisions(self.configs[0], 'liberty', cli.CONTRACT_BRANCH) self.assertEqual(1, len(m)) m = cli._find_milestone_revisions(self.configs[0], 'liberty', cli.EXPAND_BRANCH) self.assertEqual(0, len(m))
def test__find_milestone_revisions_two_branches(self, walk_mock): c_revs = [FakeRevision(labels={cli.CONTRACT_BRANCH}) for r in range(5)] c_revs[1].module.neutron_milestone = [migration.MITAKA] e_revs = [FakeRevision(labels={cli.EXPAND_BRANCH}) for r in range(5)] e_revs[3].module.neutron_milestone = [migration.MITAKA] walk_mock.return_value = c_revs + e_revs m = cli._find_milestone_revisions(self.configs[0], 'mitaka') self.assertEqual(2, len(m)) m = cli._find_milestone_revisions(self.configs[0], 'newton') self.assertEqual(0, len(m))
def _test__find_milestone_revisions_one_branch(self, contract_label, expand_label, walk_mock): c_revs = [FakeRevision(labels={contract_label}) for r in range(5)] c_revs[1].module.neutron_milestone = [migration.LIBERTY] walk_mock.return_value = c_revs m = cli._find_milestone_revisions(self.configs[0], 'liberty', contract_label) self.assertEqual(1, len(m)) m = cli._find_milestone_revisions(self.configs[0], 'liberty', expand_label) self.assertEqual(0, len(m))
def test_branches(self): drop_exceptions = collections.defaultdict(list) creation_exceptions = collections.defaultdict(list) def find_migration_exceptions(): # Due to some misunderstandings and some conscious decisions, # there may be some expand migrations which drop elements and # some contract migrations which create elements. These excepted # elements must be returned by a method in the script itself. # The names of the method must be 'contract_creation_exceptions' # or 'expand_drop_exceptions'. The methods must have a docstring # explaining the reason for the exception. # # Here we build lists of the excepted elements and verify that # they are documented. script = alembic_script.ScriptDirectory.from_config( self.alembic_config) for m in list(script.walk_revisions(base='base', head='heads')): branches = m.branch_labels or [] if migration.CONTRACT_BRANCH in branches: method_name = 'contract_creation_exceptions' exceptions_dict = creation_exceptions elif migration.EXPAND_BRANCH in branches: method_name = 'expand_drop_exceptions' exceptions_dict = drop_exceptions else: continue get_excepted_elements = getattr(m.module, method_name, None) if not get_excepted_elements: continue explanation = getattr(get_excepted_elements, '__doc__', "") if len(explanation) < 1: self.fail( "%s() requires docstring with explanation" % '.'.join([ m.module.__name__, get_excepted_elements.__name__ ])) for sa_type, elements in get_excepted_elements().items(): exceptions_dict[sa_type].extend(elements) def is_excepted_sqla(clauseelement, exceptions): """Identify excepted operations that are allowed for the branch.""" element = clauseelement.element element_name = element.name if isinstance(element, sqlalchemy.Index): element_name = element.table.name for sa_type_, excepted_names in exceptions.items(): if isinstance(element, sa_type_): if element_name in excepted_names: return True def is_excepted_alembic(clauseelement, exceptions): """Identify excepted operations that are allowed for the branch.""" # For alembic the clause is AddColumn or DropColumn column = clauseelement.column.name table = clauseelement.column.table.name element_name = '.'.join([table, column]) for alembic_type, excepted_names in exceptions.items(): if alembic_type == sqlalchemy.Column: if element_name in excepted_names: return True def is_allowed(clauseelement, exceptions, disallowed_ops): if (isinstance(clauseelement, disallowed_ops['sqla']) and hasattr(clauseelement, 'element')): return is_excepted_sqla(clauseelement, exceptions) if isinstance(clauseelement, disallowed_ops['alembic']): return is_excepted_alembic(clauseelement, exceptions) return True def check_expand_branch(conn, clauseelement, multiparams, params): if not is_allowed(clauseelement, drop_exceptions, DROP_OPERATIONS): self.fail("Migration in expand branch contains drop command") def check_contract_branch(conn, clauseelement, multiparams, params): if not is_allowed(clauseelement, creation_exceptions, CREATION_OPERATIONS): self.fail("Migration in contract branch contains create " "command") find_migration_exceptions() engine = self.engine cfg.CONF.set_override('connection', engine.url, group='database') with engine.begin() as connection: self.alembic_config.attributes['connection'] = connection # upgrade to latest release first; --expand users are expected to # apply all alembic scripts from previous releases before applying # the new ones for release in migration_root.NEUTRON_MILESTONES: release_revisions = migration._find_milestone_revisions( self.alembic_config, release) for rev in release_revisions: migration.do_alembic_command(self.alembic_config, 'upgrade', rev[0]) with self._listener(engine, check_expand_branch): migration.do_alembic_command( self.alembic_config, 'upgrade', '%s@head' % migration.EXPAND_BRANCH) with self._listener(engine, check_contract_branch): migration.do_alembic_command( self.alembic_config, 'upgrade', '%s@head' % migration.CONTRACT_BRANCH)
def test_branches(self): drop_exceptions = collections.defaultdict(list) creation_exceptions = collections.defaultdict(list) def find_migration_exceptions(): # Due to some misunderstandings and some conscious decisions, # there may be some expand migrations which drop elements and # some contract migrations which create elements. These excepted # elements must be returned by a method in the script itself. # The names of the method must be 'contract_creation_exceptions' # or 'expand_drop_exceptions'. The methods must have a docstring # explaining the reason for the exception. # # Here we build lists of the excepted elements and verify that # they are documented. script = alembic_script.ScriptDirectory.from_config( self.alembic_config) for m in list(script.walk_revisions(base='base', head='heads')): branches = m.branch_labels or [] if migration.CONTRACT_BRANCH in branches: method_name = 'contract_creation_exceptions' exceptions_dict = creation_exceptions elif migration.EXPAND_BRANCH in branches: method_name = 'expand_drop_exceptions' exceptions_dict = drop_exceptions else: continue get_excepted_elements = getattr(m.module, method_name, None) if not get_excepted_elements: continue explanation = getattr(get_excepted_elements, '__doc__', "") if len(explanation) < 1: self.fail("%s() requires docstring with explanation" % '.'.join([m.module.__name__, get_excepted_elements.__name__])) for sa_type, elements in get_excepted_elements().items(): exceptions_dict[sa_type].extend(elements) def is_excepted_sqla(clauseelement, exceptions): """Identify excepted operations that are allowed for the branch.""" element = clauseelement.element element_name = element.name if isinstance(element, sqlalchemy.Index): element_name = element.table.name for sa_type_, excepted_names in exceptions.items(): if isinstance(element, sa_type_): if element_name in excepted_names: return True def is_excepted_alembic(clauseelement, exceptions): """Identify excepted operations that are allowed for the branch.""" # For alembic the clause is AddColumn or DropColumn column = clauseelement.column.name table = clauseelement.column.table.name element_name = '.'.join([table, column]) for alembic_type, excepted_names in exceptions.items(): if alembic_type == sqlalchemy.Column: if element_name in excepted_names: return True def is_allowed(clauseelement, exceptions, disallowed_ops): if (isinstance(clauseelement, disallowed_ops['sqla']) and hasattr(clauseelement, 'element')): return is_excepted_sqla(clauseelement, exceptions) if isinstance(clauseelement, disallowed_ops['alembic']): return is_excepted_alembic(clauseelement, exceptions) return True def check_expand_branch(conn, clauseelement, multiparams, params): if not is_allowed(clauseelement, drop_exceptions, DROP_OPERATIONS): self.fail("Migration in expand branch contains drop command") def check_contract_branch(conn, clauseelement, multiparams, params): if not is_allowed(clauseelement, creation_exceptions, CREATION_OPERATIONS): self.fail("Migration in contract branch contains create " "command") find_migration_exceptions() engine = self.engine cfg.CONF.set_override('connection', engine.url, group='database') with engine.begin() as connection: self.alembic_config.attributes['connection'] = connection # upgrade to latest release first; --expand users are expected to # apply all alembic scripts from previous releases before applying # the new ones for release in migration_root.NEUTRON_MILESTONES: release_revisions = migration._find_milestone_revisions( self.alembic_config, release) for rev in release_revisions: migration.do_alembic_command( self.alembic_config, 'upgrade', rev[0]) with self._listener(engine, check_expand_branch): migration.do_alembic_command( self.alembic_config, 'upgrade', '%s@head' % migration.EXPAND_BRANCH) with self._listener(engine, check_contract_branch): migration.do_alembic_command( self.alembic_config, 'upgrade', '%s@head' % migration.CONTRACT_BRANCH)