Author: astaric Date: Fri Apr 26 09:10:14 2013 New Revision: 1476118 URL: http://svn.apache.org/r1476118 Log: Added more tests for multiproduct upgrade, refactoring.
Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/api.py bloodhound/trunk/bloodhound_multiproduct/tests/upgrade.py Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/api.py URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/api.py?rev=1476118&r1=1476117&r2=1476118&view=diff ============================================================================== --- bloodhound/trunk/bloodhound_multiproduct/multiproduct/api.py (original) +++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/api.py Fri Apr 26 09:10:14 2013 @@ -30,6 +30,7 @@ from trac.attachment import Attachment from trac.config import Option, PathOption from trac.core import Component, TracError, implements, Interface from trac.db import Table, Column, DatabaseManager, Index +import trac.db_default from trac.env import IEnvironmentSetupParticipant, Environment from trac.perm import IPermissionRequestor, PermissionCache from trac.resource import IExternalResourceConnector, IResourceChangeListener,\ @@ -97,7 +98,7 @@ class MultiProductSystem(Component): global environment configuration. """) - SCHEMA = [mcls._get_schema() \ + SCHEMA = [mcls._get_schema() for mcls in (Product, ProductResourceMap)] # Tables which should be migrated (extended with 'product' column) @@ -202,171 +203,207 @@ class MultiProductSystem(Component): db_installed_version = self.get_version() with self.env.db_direct_transaction as db: if db_installed_version < 1: - # Initial installation - db("ALTER TABLE ticket ADD COLUMN product TEXT") - self.log.debug("creating initial db tables for %s plugin." % - PLUGIN_NAME) - db_connector, dummy = DatabaseManager(self.env)._get_connector() - for table in self.SCHEMA: - for statement in db_connector.to_sql(table): - db(statement) + self._add_column_product_to_ticket(db) + self._create_multiproduct_tables(db) db_installed_version = self._update_db_version(db, 1) if db_installed_version < 2: - from multiproduct.model import Product - products = Product.select(self.env) - for prod in products: - db("""UPDATE ticket SET product=%s - WHERE product=%s""", (prod.prefix, prod.name)) + self._replace_product_on_ticket_with_product_prefix(db) db_installed_version = self._update_db_version(db, 2) if db_installed_version < 3: - from multiproduct.model import Product - import trac.db_default - - def create_temp_table(table): - """creates temporary table with the new schema and - drops original table""" - table_temp_name = '%s_temp' % table - if table == 'report': - cols = ','.join([c for c in table_columns[table] if c != 'id']) - else: - cols = ','.join(table_columns[table]) - self.log.info("Migrating table '%s' to a new schema", table) - db("""CREATE TABLE %s AS SELECT %s FROM %s""" % - (table_temp_name, cols, table)) - db("""DROP TABLE %s""" % table) - db_connector, _ = DatabaseManager(self.env)._get_connector() - table_schema = [t for t in table_defs if t.name == table][0] - for sql in db_connector.to_sql(table_schema): - db(sql) - return table_temp_name, cols - - def drop_temp_table(table): - """drops specified temporary table""" - db("""DROP TABLE %s""" % table) - - TICKET_TABLES = ['ticket_change', 'ticket_custom', - 'attachment', - ] SYSTEM_TABLES = ['system'] + TICKET_TABLES = [ + 'ticket_change', 'ticket_custom', 'attachment', + ] + table_defs = self._add_product_column_to_tables( + self.MIGRATE_TABLES + TICKET_TABLES + SYSTEM_TABLES, + db_installed_version) + table_columns = self._get_table_columns(table_defs) + create_temp_table = lambda table: self._create_temp_table( + db, table, table_columns, table_defs) + + self._insert_default_product(db) + self._upgrade_tickets(db, TICKET_TABLES, create_temp_table) + self._upgrade_wikis(db, create_temp_table, table_columns) + self._upgrade_system_tables(db, create_temp_table) + self._soft_link_repositories_to_default_product(db) + self._upgrade_table_system(SYSTEM_TABLES, create_temp_table, db) + self._enable_multiproduct_hooks() - # extend trac default schema by adding product column and extending key with product - table_defs = [copy.deepcopy(t) for t in trac.db_default.schema - if t.name in self.MIGRATE_TABLES + TICKET_TABLES + SYSTEM_TABLES] - for t in table_defs: - t.columns.append(Column('product')) - if isinstance(t.key, list): - t.key = tuple(t.key) + tuple(['product']) - elif isinstance(t.key, tuple): - t.key = t.key + tuple(['product']) - else: - raise TracError("Invalid table '%s' schema key '%s' while upgrading " - "plugin '%s' from version %d to %d'" % - (t.name, t.key, PLUGIN_NAME, db_installed_version, 3)) - table_columns = dict() - for table in table_defs: - table_columns[table.name] = [c for c in [column.name for column in - [t for t in table_defs if t.name == table.name][0].columns] - if c != 'product'] - # create default product - self.log.info("Creating default product") - db("""INSERT INTO bloodhound_product (prefix, name, description, owner) - VALUES ('%s', '%s', '%s', '')""" % - (DEFAULT_PRODUCT, 'Default', 'Default product')) - - # fetch all products - all_products = Product.select(self.env) - - # migrate tickets that don't have product assigned to default product - # - update ticket table product column - # - update ticket related tables by: - # - upgrading schema - # - update product column to match ticket's product - self.log.info("Migrating tickets w/o product to default product") - db("""UPDATE ticket SET product='%s' - WHERE (product IS NULL OR product='')""" % DEFAULT_PRODUCT) - self._migrate_attachments( - db("""SELECT a.type, a.id, a.filename + db_installed_version = self._update_db_version(db, 3) + + if db_installed_version < 4: + self._create_product_tables_for_plugins(db) + db_installed_version = self._update_db_version(db, 4) + + self.env.enable_multiproduct_schema(True) + + def _add_column_product_to_ticket(self, db): + self.log.debug("Adding field product to ticket table") + db("ALTER TABLE ticket ADD COLUMN product TEXT") + + def _create_multiproduct_tables(self, db): + self.log.debug("Creating initial db tables for %s plugin." % + PLUGIN_NAME) + db_connector, dummy = DatabaseManager(self.env)._get_connector() + for table in self.SCHEMA: + for statement in db_connector.to_sql(table): + db(statement) + + def _replace_product_on_ticket_with_product_prefix(self, db): + for prod in Product.select(self.env): + db("""UPDATE ticket SET product=%s + WHERE product=%s""", (prod.prefix, prod.name)) + + def _create_temp_table(self, db, table, table_columns, table_defs): + """creates temporary table with the new schema and + drops original table""" + table_temp_name = '%s_temp' % table + if table == 'report': + cols = ','.join([c for c in table_columns[table] if c != 'id']) + else: + cols = ','.join(table_columns[table]) + self.log.info("Migrating table '%s' to a new schema", table) + db("""CREATE TABLE %s AS SELECT %s FROM %s""" % + (table_temp_name, cols, table)) + db("""DROP TABLE %s""" % table) + db_connector, _ = DatabaseManager(self.env)._get_connector() + table_schema = [t for t in table_defs if t.name == table][0] + for sql in db_connector.to_sql(table_schema): + db(sql) + return table_temp_name, cols + + def _drop_temp_table(self, db, table): + db("""DROP TABLE %s""" % table) + + def _add_product_column_to_tables(self, tables, current_version): + """Extend trac default schema by adding product column + and extending key with product. + """ + table_defs = [copy.deepcopy(t) for t in trac.db_default.schema + if + t.name in tables] + for t in table_defs: + t.columns.append(Column('product')) + if isinstance(t.key, list): + t.key = tuple(t.key) + tuple(['product']) + elif isinstance(t.key, tuple): + t.key = t.key + tuple(['product']) + else: + raise TracError( + "Invalid table '%s' schema key '%s' while upgrading " + "plugin '%s' from version %d to %d'" % + (t.name, t.key, PLUGIN_NAME, current_version, 3)) + return table_defs + + def _get_table_columns(self, table_defs): + table_columns = dict() + for table in table_defs: + table_definition = \ + [t for t in table_defs if t.name == table.name][0] + column_names = \ + [column.name for column in table_definition.columns] + table_columns[table.name] = \ + [c for c in column_names if c != 'product'] + return table_columns + + def _insert_default_product(self, db): + self.log.info("Creating default product") + db("""INSERT INTO bloodhound_product (prefix, name, description, owner) + VALUES ('%s', '%s', '%s', '') + """ % (DEFAULT_PRODUCT, 'Default', 'Default product')) + + def _upgrade_tickets(self, db, TICKET_TABLES, create_temp_table): + # migrate tickets that don't have product assigned to default product + # - update ticket table product column + # - update ticket related tables by: + # - upgrading schema + # - update product column to match ticket's product + self.log.info("Migrating tickets w/o product to default product") + db("""UPDATE ticket SET product='%s' + WHERE (product IS NULL OR product='') + """ % DEFAULT_PRODUCT) + self._migrate_attachments( + db("""SELECT a.type, a.id, a.filename FROM attachment a INNER JOIN ticket t ON a.id = %(t.id)s WHERE a.type='ticket' - """ % {'t.id': db.cast('t.id', 'text')}), - to_product=DEFAULT_PRODUCT - ) - - self.log.info("Migrating ticket tables to a new schema") - for table in TICKET_TABLES: - temp_table_name, cols = create_temp_table(table) - db("""INSERT INTO %s (%s, product) + """ % {'t.id': db.cast('t.id', 'text')}), + to_product=DEFAULT_PRODUCT + ) + self.log.info("Migrating ticket tables to a new schema") + for table in TICKET_TABLES: + temp_table_name, cols = create_temp_table(table) + db("""INSERT INTO %s (%s, product) SELECT %s, '' FROM %s""" % - (table, cols, cols, temp_table_name)) - drop_temp_table(temp_table_name) - - for table in TICKET_TABLES: - if table == 'attachment': - db(""" - UPDATE attachment - SET product=(SELECT ticket.product - FROM ticket - WHERE %(ticket_id)s=attachment.id - LIMIT 1) - WHERE attachment.type='ticket' - AND EXISTS(SELECT ticket.product - FROM ticket - WHERE %(ticket_id)s=attachment.id) - """ % dict( - ticket_id=db.cast('attachment.id', 'text'))) - else: - db("""UPDATE %s - SET product=(SELECT ticket.product FROM ticket WHERE ticket.id=%s.ticket)""" % - (table, table)) - - # migrate system table (except wiki which is handled separately) to a - # new schema - # - create tables with the new schema - # - populate system tables with global configuration for each product - # - exception is permission table where permissions are also populated in - # global scope - self.log.info("Migrating system tables to a new schema") - for table in self.MIGRATE_TABLES: - if table == 'wiki': - continue - temp_table_name, cols = create_temp_table(table) - for product in all_products: - self.log.info("Populating table '%s' for product '%s' ('%s')", - table, product.name, product.prefix) - db("""INSERT INTO %s (%s, product) SELECT %s,'%s' FROM %s""" % - (table, cols, cols, product.prefix, temp_table_name)) - if table == 'permission': - self.log.info("Populating table '%s' for global scope", table) - db("""INSERT INTO %s (%s, product) SELECT %s,'%s' FROM %s""" % - (table, cols, cols, '', temp_table_name)) - drop_temp_table(temp_table_name) - - # migrate wiki table - # - populate system wikis to all products + global scope - # - update wiki attachment product to match wiki product - table = 'wiki' - temp_table_name, cols = create_temp_table(table) - self.log.info("Migrating wikis to global context") - db("""INSERT INTO %s (%s, product) SELECT %s, '' FROM %s""" % - (table, cols, cols, temp_table_name)) + (table, cols, cols, temp_table_name)) + self._drop_temp_table(db, temp_table_name) + if table == 'attachment': db("""UPDATE attachment - SET product='' - WHERE attachment.type='wiki'""") - - for wiki_name, wiki_version, wiki_product in db(""" + SET product=(SELECT ticket.product + FROM ticket + WHERE %(ticket.id)s=attachment.id + LIMIT 1) + WHERE attachment.type='ticket' + AND EXISTS(SELECT ticket.product + FROM ticket + WHERE %(ticket.id)s=attachment.id) + """ % {'ticket.id': db.cast('ticket.id', 'text')}) + else: + db("""UPDATE %(table)s + SET product=(SELECT ticket.product + FROM ticket + WHERE ticket.id=%(table)s.ticket) + """ % {'table': table}) + + def _upgrade_system_tables(self, db, create_temp_table): + # migrate system table (except wiki which is handled separately) + # to a new schema + # - create tables with the new schema + # - populate system tables with global configuration for each product + # - exception is permission table where permissions + # are also populated in global scope + self.log.info("Migrating system tables to a new schema") + for table in self.MIGRATE_TABLES: + if table == 'wiki': + continue + temp_table_name, cols = create_temp_table(table) + for product in Product.select(self.env): + self.log.info("Populating table '%s' for product '%s' ('%s')", + table, product.name, product.prefix) + db("""INSERT INTO %s (%s, product) SELECT %s,'%s' FROM %s""" % + (table, cols, cols, product.prefix, temp_table_name)) + if table == 'permission': + self.log.info("Populating table '%s' for global scope", table) + db("""INSERT INTO %s (%s, product) SELECT %s,'%s' FROM %s""" % + (table, cols, cols, '', temp_table_name)) + self._drop_temp_table(db, temp_table_name) + + def _upgrade_wikis(self, db, create_temp_table, + table_columns): + # migrate wiki table + # - populate system wikis to all products + global scope + # - update wiki attachment product to match wiki product + table = 'wiki' + temp_table_name, cols = create_temp_table(table) + self.log.info("Migrating wikis to global context") + db("""INSERT INTO %s (%s, product) SELECT %s, '' FROM %s""" % + (table, cols, cols, temp_table_name)) + db("""UPDATE attachment + SET product='' + WHERE attachment.type='wiki'""") + for wiki_name, wiki_version, wiki_product in db(""" SELECT name, version, product FROM %s""" % table): - attachment_cols = ','.join(table_columns['attachment']) - if wiki_name in self.system_wiki_list: - for product in all_products: - db("""INSERT INTO %s (%s, product) + attachment_cols = ','.join(table_columns['attachment']) + if wiki_name in self.system_wiki_list: + for product in Product.select(self.env): + db("""INSERT INTO %s (%s, product) SELECT %s, '%s' FROM %s WHERE name='%s' AND version=%s AND product='%s'""" % - (table, cols, cols, product.prefix, table, - wiki_name, wiki_version, wiki_product)) - db("""INSERT INTO attachment (%(cols)s, product) + (table, cols, cols, product.prefix, table, + wiki_name, wiki_version, wiki_product)) + db("""INSERT INTO attachment (%(cols)s, product) SELECT %(cols)s, '%(new_product)s' FROM attachment a WHERE type='wiki' @@ -380,89 +417,40 @@ class MultiProductSystem(Component): wiki_name=wiki_name, old_product=wiki_product, new_product=product.prefix)) - self._migrate_attachments( - db("""SELECT type, id, filename + self._migrate_attachments( + db("""SELECT type, id, filename FROM attachment WHERE type='wiki' AND id='%s' AND product='%s' """ % (wiki_name, DEFAULT_PRODUCT)), - to_product=DEFAULT_PRODUCT, - copy=True - ) - else: - self.log.info("Moving wiki page '%s' to default product", wiki_name) - db("""UPDATE wiki + to_product=DEFAULT_PRODUCT, + copy=True + ) + else: + self.log.info("Moving wiki page '%s' to default product", + wiki_name) + db("""UPDATE wiki SET product='%s' WHERE name='%s' AND version=%s AND product='%s' """ % (DEFAULT_PRODUCT, wiki_name, wiki_version, wiki_product)) - db("""UPDATE attachment + db("""UPDATE attachment SET product='%s' WHERE type='wiki' AND id='%s' AND product='%s' """ % (DEFAULT_PRODUCT, wiki_name, wiki_product)) - self._migrate_attachments( - db("""SELECT type, id, filename + self._migrate_attachments( + db("""SELECT type, id, filename FROM attachment WHERE type='wiki' AND id='%s' AND product='%s' """ % (wiki_name, DEFAULT_PRODUCT)), - to_product=DEFAULT_PRODUCT, - ) - - drop_temp_table(temp_table_name) - - # soft link existing repositories to default product - repositories_linked = [] - for id, name in db("""SELECT id, value FROM repository - WHERE name='name'"""): - if id in repositories_linked: - continue - db("""INSERT INTO repository (id, name, value) - VALUES (%s, 'product', '%s')""" % - (id, DEFAULT_PRODUCT)) - repositories_linked.append(id) - self.log.info("Repository '%s' (%s) soft linked to default product", name, id) - - # Update system tables - # Upgrade schema - self.log.info("Migrating system tables to a new schema") - for table in SYSTEM_TABLES: - temp_table_name, cols = create_temp_table(table) - db("""INSERT INTO %s (%s, product) - SELECT %s,'' FROM %s""" % - (table, cols, cols, temp_table_name)) - drop_temp_table(temp_table_name) - - # enable multi product hooks in environment configuration - import multiproduct.hooks - import inspect - config_update = False - hook_path = os.path.realpath(inspect.getsourcefile(multiproduct.hooks)) - if not 'environment_factory' in self.env.config['trac']: - self.env.config['trac'].set('environment_factory', hook_path) - config_update = True - if not 'request_factory' in self.env.config['trac']: - self.env.config['trac'].set('request_factory', hook_path) - config_update = True - if config_update: - self.log.info("Enabling multi product hooks in environment configuration") - self.env.config.save() - - db_installed_version = self._update_db_version(db, 3) - - if db_installed_version < 4: - self.log.debug("creating additional db tables for %s plugin." % - PLUGIN_NAME) - db_connector, dummy = DatabaseManager(self.env)._get_connector() - for statement in db_connector.to_sql(ProductSetting._get_schema()): - db(statement) - db_installed_version = self._update_db_version(db, 4) - - self.env.enable_multiproduct_schema(True) + to_product=DEFAULT_PRODUCT, + ) + self._drop_temp_table(db, temp_table_name) def _migrate_attachments(self, attachments, to_product=None, copy=False): for type, id, filename in attachments: @@ -498,6 +486,56 @@ class MultiProductSystem(Component): filename, type, id, str(err) ) + def _soft_link_repositories_to_default_product(self, db): + # soft link existing repositories to default product + repositories_linked = [] + for id, name in db("""SELECT id, value FROM repository + WHERE name='name'"""): + if id in repositories_linked: + continue + db("""INSERT INTO repository (id, name, value) + VALUES (%s, 'product', '%s')""" % + (id, DEFAULT_PRODUCT)) + repositories_linked.append(id) + self.log.info("Repository '%s' (%s) soft linked to default product", + name, id) + + def _upgrade_table_system(self, SYSTEM_TABLES, create_temp_table, db): + # Update system tables + # Upgrade schema + self.log.info("Migrating system tables to a new schema") + for table in SYSTEM_TABLES: + temp_table_name, cols = create_temp_table(table) + db("""INSERT INTO %s (%s, product) + SELECT %s,'' FROM %s""" % + (table, cols, cols, temp_table_name)) + self._drop_temp_table(db, temp_table_name) + + def _enable_multiproduct_hooks(self): + # enable multi product hooks in environment configuration + import multiproduct.hooks + import inspect + + config_update = False + hook_path = os.path.realpath(inspect.getsourcefile(multiproduct.hooks)) + if not 'environment_factory' in self.env.config['trac']: + self.env.config['trac'].set('environment_factory', hook_path) + config_update = True + if not 'request_factory' in self.env.config['trac']: + self.env.config['trac'].set('request_factory', hook_path) + config_update = True + if config_update: + self.log.info( + "Enabling multi product hooks in environment configuration") + self.env.config.save() + + def _create_product_tables_for_plugins(self, db): + self.log.debug("creating additional db tables for %s plugin." % + PLUGIN_NAME) + db_connector, dummy = DatabaseManager(self.env)._get_connector() + for statement in db_connector.to_sql(ProductSetting._get_schema()): + db(statement) + # IResourceChangeListener methods def match_resource(self, resource): return isinstance(resource, Product) Modified: bloodhound/trunk/bloodhound_multiproduct/tests/upgrade.py URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/tests/upgrade.py?rev=1476118&r1=1476117&r2=1476118&view=diff ============================================================================== --- bloodhound/trunk/bloodhound_multiproduct/tests/upgrade.py (original) +++ bloodhound/trunk/bloodhound_multiproduct/tests/upgrade.py Fri Apr 26 09:10:14 2013 @@ -33,6 +33,7 @@ from trac.test import Environment from trac.ticket import Ticket from trac.wiki import WikiPage +from multiproduct.api import MultiProductSystem from multiproduct.env import ProductEnvironment from multiproduct.model import Product @@ -43,13 +44,9 @@ BLOODHOUND_TABLES = ( ) TABLES_WITH_PRODUCT_FIELD = ( - 'component', - 'milestone', - 'version', - 'enum', - 'permission', - 'wiki', - 'report', + 'ticket', 'ticket_change', 'ticket_custom', 'attachment', 'component', + 'milestone', 'wiki', 'report', + 'version', 'enum', 'permission', 'system', ) @@ -57,12 +54,12 @@ class EnvironmentUpgradeTestCase(unittes def setUp(self): self.env_path = tempfile.mkdtemp('multiproduct-tempenv') self.env = Environment(self.env_path, create=True) - self.enabled_components = [] DummyPlugin.version = 1 - def test_upgrade_environment(self): + def test_can_upgrade_environment_with_multi_product_disabled(self): self.env.upgrade() + # Multiproduct was not enabled so multiproduct tables should not exist with self.env.db_direct_transaction as db: for table in BLOODHOUND_TABLES: with self.assertFailsWithMissingTable(): @@ -72,7 +69,7 @@ class EnvironmentUpgradeTestCase(unittes with self.assertFailsWithMissingColumn(): db("SELECT product FROM %s" % table) - def test_upgrade_environment_to_multiproduct(self): + def test_upgrade_creates_multi_product_tables_and_adds_product_column(self): self._enable_multiproduct() self.env.upgrade() @@ -83,89 +80,38 @@ class EnvironmentUpgradeTestCase(unittes for table in TABLES_WITH_PRODUCT_FIELD: db("SELECT product FROM %s" % table) - def test_upgrade_plugin(self): - self._enable_component(DummyPlugin) - self.env.upgrade() - - with self.env.db_direct_transaction as db: - db("SELECT v1 FROM dummy_table") - with self.assertFailsWithMissingColumn(): - db("SELECT v2 FROM dummy_table") - - DummyPlugin.version = 2 - self.env.upgrade() - - with self.env.db_direct_transaction as db: - db("SELECT v2 FROM dummy_table") - - def test_upgrade_plugin_to_multiproduct(self): + def test_upgrade_creates_default_product(self): self._enable_multiproduct() - self._enable_component(DummyPlugin) self.env.upgrade() - with self.env.db_direct_transaction as db: - db("SELECT * FROM dummy_table") - db("""SELECT * FROM "@_dummy_table" """) + products = Product.select(self.env) + self.assertEqual(len(products), 1) - def test_upgrade_existing_plugin_to_multiproduct(self): - self._enable_component(DummyPlugin) - self.env.upgrade() - with self.env.db_direct_transaction as db: - with self.assertFailsWithMissingTable(): - db("""SELECT * FROM "@_dummy_table" """) - - self._enable_multiproduct() - self.env.upgrade() - with self.env.db_direct_transaction as db: - db("SELECT * FROM dummy_table") - db("""SELECT * FROM "@_dummy_table" """) - - def test_upgrading_existing_plugin_leaves_data_in_global_env(self): - self._enable_component(DummyPlugin) - self.env.upgrade() - with self.env.db_direct_transaction as db: - for i in range(5): - db("INSERT INTO dummy_table (v1) VALUES ('%d')" % i) - self.assertEqual( - len(db("SELECT * FROM dummy_table")), 5) - - self._enable_multiproduct() - self.env.upgrade() - with self.env.db_direct_transaction as db: - self.assertEqual( - len(db('SELECT * FROM "dummy_table"')), 5) - self.assertEqual( - len(db('SELECT * FROM "@_dummy_table"')), 0) - - def test_creating_new_product_calls_environment_created(self): - self._enable_component(DummyPlugin) - self._enable_multiproduct() - self.env.upgrade() - - prod = Product(self.env) - prod.update_field_dict(dict(prefix='p1')) - ProductEnvironment(self.env, prod, create=True) - with self.env.db_direct_transaction as db: - db('SELECT * FROM "p1_dummy_table"') - - def test_upgrade_moves_tickets_to_default_product(self): + def test_upgrade_moves_tickets_and_related_objects_to_default_prod(self): + self._add_custom_field('custom_field') with self.env.db_direct_transaction as db: db("""INSERT INTO ticket (id) VALUES (1)""") db("""INSERT INTO attachment (type, id) - VALUES ('ticket', '1')""") + VALUES ('ticket', '1')""") + db("""INSERT INTO ticket_custom (ticket, name, value) + VALUES (1, 'custom_field', '42')""") + db("""INSERT INTO ticket_change (ticket, time, field) + VALUES (1, 42, 'summary')""") self._enable_multiproduct() self.env.upgrade() - with self.env.db_direct_transaction as db: - self.assertEqual( - len(db("""SELECT * FROM ticket WHERE product='@'""")), 1) - self.assertEqual( - len(db("""SELECT * FROM attachment - WHERE product='@' - AND type='ticket'""")), 1) + with self.product('@'): + ticket = Ticket(self.env, 1) + attachments = list(Attachment.select(self.env, + ticket.resource.realm, + ticket.resource.id)) + self.assertEqual(len(attachments), 1) + self.assertEqual(ticket['custom_field'], '42') + changes = ticket.get_changelog() + self.assertEqual(len(changes), 3) - def test_upgrade_moves_wikis_to_default_product(self): + def test_upgrade_moves_custom_wikis_to_default_product(self): with self.env.db_direct_transaction as db: db("""INSERT INTO wiki (name, version) VALUES ('MyPage', 1)""") db("""INSERT INTO attachment (type, id) @@ -205,23 +151,44 @@ class EnvironmentUpgradeTestCase(unittes WHERE product='' AND type='wiki'""")), 1) - def test_can_upgrade_database_with_orphaned_attachments(self): + def test_upgrade_copies_content_of_system_tables_to_all_products(self): + mp = MultiProductSystem(self.env) with self.env.db_direct_transaction as db: - db("""INSERT INTO attachment (id, type) - VALUES ('5', 'ticket')""") - db("""INSERT INTO attachment (id, type) - VALUES ('MyWiki', 'wiki')""") + mp._add_column_product_to_ticket(db) + mp._create_multiproduct_tables(db) + mp._update_db_version(db, 1) + for i in range(1, 6): + db("""INSERT INTO bloodhound_product (prefix, name) + VALUES ('p%d', 'Product 1')""" % i) + for table in ('component', 'milestone', 'enum', 'version', + 'permission', 'report'): + db("""DELETE FROM %s""" % table) + db("""INSERT INTO component (name) VALUES ('foobar')""") + db("""INSERT INTO milestone (name) VALUES ('foobar')""") + db("""INSERT INTO version (name) VALUES ('foobar')""") + db("""INSERT INTO enum (type, name) VALUES ('a', 'b')""") + db("""INSERT INTO permission VALUES ('x', 'TICKET_VIEW')""") + db("""INSERT INTO wiki (name, version) VALUES ('WikiStart', 1)""") + db("""INSERT INTO report (title) VALUES ('x')""") self._enable_multiproduct() self.env.upgrade() - def test_can_upgrade_database_with_text_attachment_ids(self): with self.env.db_direct_transaction as db: - db("""INSERT INTO attachment (id, type) - VALUES ('abc', 'ticket')""") - - self._enable_multiproduct() - self.env.upgrade() + for table in ('component', 'milestone', 'version', 'enum', + 'report'): + rows = db("SELECT * FROM %s" % table) + self.assertEqual( + len(rows), 6, + "Wrong number of lines in %s (%d instead of %d)\n%s" + % (table, len(rows), 6, rows)) + for table in ('wiki', 'permission'): + # Permissions and wikis also hold rows for global product. + rows = db("SELECT * FROM %s" % table) + self.assertEqual( + len(rows), 7, + "Wrong number of lines in %s (%d instead of %d)\n%s" + % (table, len(rows), 7, rows)) def test_upgrading_database_moves_attachment_to_correct_product(self): ticket = self.insert_ticket('ticket') @@ -258,26 +225,148 @@ class EnvironmentUpgradeTestCase(unittes for attachment in attachments: self.assertEqual(attachment.open().read(), 'Hello World!') + def test_can_upgrade_database_with_ticket_attachment_with_text_ids(self): + with self.env.db_direct_transaction as db: + db("""INSERT INTO attachment (id, type) + VALUES ('abc', 'ticket')""") + + self._enable_multiproduct() + self.env.upgrade() + + def test_can_upgrade_database_with_orphaned_attachments(self): + with self.env.db_direct_transaction as db: + db("""INSERT INTO attachment (id, type) + VALUES ('5', 'ticket')""") + db("""INSERT INTO attachment (id, type) + VALUES ('MyWiki', 'wiki')""") + + self._enable_multiproduct() + self.env.upgrade() + + def test_can_upgrade_multi_product_from_v1(self): + mp = MultiProductSystem(self.env) + with self.env.db_direct_transaction as db: + mp._add_column_product_to_ticket(db) + mp._create_multiproduct_tables(db) + mp._update_db_version(db, 1) + + db("""INSERT INTO bloodhound_product (prefix, name) + VALUES ('p1', 'Product 1')""") + db("""INSERT INTO ticket (id, product) + VALUES (1, 'Product 1')""") + + self._enable_multiproduct() + self.env.upgrade() + + with self.product('p1'): + Ticket(self.env, 1) + + def test_can_upgrade_multi_product_from_v2(self): + mp = MultiProductSystem(self.env) + with self.env.db_direct_transaction as db: + mp._add_column_product_to_ticket(db) + mp._create_multiproduct_tables(db) + mp._replace_product_on_ticket_with_product_prefix(db) + mp._update_db_version(db, 2) + + db("""INSERT INTO bloodhound_product (prefix, name) + VALUES ('p1', 'Product 1')""") + db("""INSERT INTO ticket (id, product) + VALUES (1, 'p1')""") + db("""INSERT INTO ticket (id) + VALUES (2)""") + + self._enable_multiproduct() + self.env.upgrade() + + with self.product('p1'): + Ticket(self.env, 1) + with self.product('@'): + Ticket(self.env, 2) + + def test_upgrade_plugin(self): + self._enable_component(DummyPlugin) + self.env.upgrade() + + with self.env.db_direct_transaction as db: + db("SELECT v1 FROM dummy_table") + with self.assertFailsWithMissingColumn(): + db("SELECT v2 FROM dummy_table") + + DummyPlugin.version = 2 + self.env.upgrade() + + with self.env.db_direct_transaction as db: + db("SELECT v2 FROM dummy_table") + + def test_upgrade_plugin_to_multiproduct(self): + self._enable_multiproduct() + self._enable_component(DummyPlugin) + self.env.upgrade() + + with self.env.db_direct_transaction as db: + db("SELECT * FROM dummy_table") + db("""SELECT * FROM "@_dummy_table" """) + + def test_upgrade_existing_plugin_to_multiproduct(self): + self._enable_component(DummyPlugin) + self.env.upgrade() + with self.env.db_direct_transaction as db: + with self.assertFailsWithMissingTable(): + db("""SELECT * FROM "@_dummy_table" """) + + self._enable_multiproduct() + self.env.upgrade() + with self.env.db_direct_transaction as db: + db("SELECT * FROM dummy_table") + db("""SELECT * FROM "@_dummy_table" """) + + def test_upgrading_existing_plugin_leaves_data_in_global_env(self): + self._enable_component(DummyPlugin) + self.env.upgrade() + with self.env.db_direct_transaction as db: + for i in range(5): + db("INSERT INTO dummy_table (v1) VALUES ('%d')" % i) + self.assertEqual( + len(db("SELECT * FROM dummy_table")), 5) + + self._enable_multiproduct() + self.env.upgrade() + with self.env.db_direct_transaction as db: + self.assertEqual( + len(db('SELECT * FROM "dummy_table"')), 5) + self.assertEqual( + len(db('SELECT * FROM "@_dummy_table"')), 0) + + def test_creating_new_product_calls_environment_created(self): + self._enable_component(DummyPlugin) + self._enable_multiproduct() + self.env.upgrade() + + prod = Product(self.env) + prod.update_field_dict(dict(prefix='p1')) + ProductEnvironment(self.env, prod, create=True) + with self.env.db_direct_transaction as db: + db('SELECT * FROM "p1_dummy_table"') + def _enable_multiproduct(self): - self.env.config.set('components', 'multiproduct.*', 'enabled') - self.env.config.save() - self._reload_environment() - self._reenable_components() + self._update_config('components', 'multiproduct.*', 'enabled') + + def _add_custom_field(self, field_name): + self._update_config('ticket-custom', field_name, 'text') def _enable_component(self, cls): - self.env.config.set('components', - '%s.%s' % (cls.__module__, cls.__name__), - 'enabled') - self.enabled_components.append(cls) - self.env.compmgr.enabled[cls] = True + self._update_config( + 'components', + '%s.%s' % (cls.__module__, cls.__name__), + 'enabled' + ) - def _reload_environment(self): + def _update_config(self, section, key, value): + self.env.config.set(section, key, value) + self.env.config.save() self.env = Environment(self.env_path) - def _reenable_components(self): - for cls in self.enabled_components: - self.env.compmgr.enabled[cls] = True - def _create_file_with_content(self, content): filename = str(uuid.uuid4())[:6] path = os.path.join(self.env_path, filename)