A more reliable test, check against expected keys, rather than received
[mediagoblin.git] / mediagoblin / tests / test_sql_migrations.py
index ccaf60ba3866c39773cb7905d86b38f0b16f9ba2..7e0569adcb349327f8943a9973f2d9fa1230d208 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import six
+import pytest
+
+pytestmark = pytest.mark.skipif(six.PY3, reason='needs sqlalchemy.migrate')
+
 import copy
 
 from sqlalchemy import (
-    Table, Column, MetaData, Index
+    Table, Column, MetaData, Index,
     Integer, Float, Unicode, UnicodeText, DateTime, Boolean,
-    ForeignKey, UniqueConstraint, PickleType)
+    ForeignKey, UniqueConstraint, PickleType, VARCHAR)
 from sqlalchemy.orm import sessionmaker, relationship
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.sql import select, insert
-from migrate import changeset
+if six.PY2:
+    from migrate import changeset
 
-from mediagoblin.db.sql.base import GMGTableBase
+from mediagoblin.db.base import GMGTableBase
+from mediagoblin.db.migration_tools import MigrationManager, RegisterMigration
+from mediagoblin.tools.common import CollectingPrinter
 
 
 # This one will get filled with local migrations
@@ -50,13 +58,17 @@ class Level1(Base1):
     __tablename__ = "level"
 
     id = Column(Unicode, primary_key=True)
-    name = Column(Unicode, unique=True, nullable=False, index=True)
-    description = Column(UnicodeText)
+    name = Column(Unicode)
+    description = Column(Unicode)
     exits = Column(PickleType)
 
 SET1_MODELS = [Creature1, Level1]
 
-SET1_MIGRATIONS = []
+FOUNDATIONS = {Creature1:[{'name':u'goblin','num_legs':2,'is_demon':False},
+                          {'name':u'cerberus','num_legs':4,'is_demon':True}]
+              }
+
+SET1_MIGRATIONS = {}
 
 #######################################################
 # Migration set 2: A few migrations and new model
@@ -87,7 +99,7 @@ class Level2(Base2):
 
     id = Column(Unicode, primary_key=True)
     name = Column(Unicode)
-    description = Column(UnicodeText)
+    description = Column(Unicode)
 
 class LevelExit2(Base2):
     __tablename__ = "level_exit"
@@ -108,12 +120,16 @@ def creature_remove_is_demon(db_conn):
     Remove the is_demon field from the creature model.  We don't need
     it!
     """
-    metadata = MetaData(bind=db_conn.engine)
-    creature_table = Table(
-        'creature', metadata,
-        autoload=True, autoload_with=db_conn.engine)
-    creature_table.drop_column('is_demon')
-    
+    # :( Commented out 'cuz of:
+    # http://code.google.com/p/sqlalchemy-migrate/issues/detail?id=143&thanks=143&ts=1327882242
+
+    # metadata = MetaData(bind=db_conn.bind)
+    # creature_table = Table(
+    #     'creature', metadata,
+    #     autoload=True, autoload_with=db_conn.bind)
+    # creature_table.drop_column('is_demon')
+    pass
+
 
 @RegisterMigration(2, FULL_MIGRATIONS)
 def creature_powers_new_table(db_conn):
@@ -122,7 +138,14 @@ def creature_powers_new_table(db_conn):
     yet though as there wasn't anything that previously held this
     information
     """
-    metadata = MetaData(bind=db_conn.engine)
+    metadata = MetaData(bind=db_conn.bind)
+
+    # We have to access the creature table so sqlalchemy can make the
+    # foreign key relationship
+    creature_table = Table(
+        'creature', metadata,
+        autoload=True, autoload_with=db_conn.bind)
+
     creature_powers = Table(
         'creature_power', metadata,
         Column('id', Integer, primary_key=True),
@@ -131,7 +154,7 @@ def creature_powers_new_table(db_conn):
         Column('name', Unicode),
         Column('description', Unicode),
         Column('hitpower', Integer, nullable=False))
-    metadata.create_all(db_conn.engine)
+    metadata.create_all(db_conn.bind)
 
 
 @RegisterMigration(3, FULL_MIGRATIONS)
@@ -142,41 +165,44 @@ def level_exits_new_table(db_conn):
     """
     # First, create the table
     # -----------------------
-    metadata = MetaData(bind=db_conn.engine)
+    metadata = MetaData(bind=db_conn.bind)
+
+    # Minimal representation of level table.
+    # Not auto-introspecting here because of pickle table.  I'm not
+    # sure sqlalchemy can auto-introspect pickle columns.
+    levels = Table(
+        'level', metadata,
+        Column('id', Unicode, primary_key=True),
+        Column('name', Unicode),
+        Column('description', Unicode),
+        Column('exits', PickleType))
+
     level_exits = Table(
         'level_exit', metadata,
         Column('id', Integer, primary_key=True),
         Column('name', Unicode),
         Column('from_level',
-               Integer, ForeignKey('level.id'), nullable=False),
+               Unicode, ForeignKey('level.id'), nullable=False),
         Column('to_level',
-               Integer, ForeignKey('level.id'), nullable=False))
-    metadata.create_all(db_conn.engine)
+               Unicode, ForeignKey('level.id'), nullable=False))
+    metadata.create_all(db_conn.bind)
 
     # And now, convert all the old exit pickles to new level exits
     # ------------------------------------------------------------
 
-    # Minimal representation of level table.
-    # Not auto-introspecting here because of pickle table.  I'm not
-    # sure sqlalchemy can auto-introspect pickle columns.
-    levels = Table(
-        'level', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('exits', PickleType))
-
     # query over and insert
     result = db_conn.execute(
         select([levels], levels.c.exits!=None))
 
     for level in result:
-        this_exit = level['exits']
-        
-        # Insert the level exit
-        db_conn.execute(
-            level_exits.insert().values(
-                name=this_exit['name'],
-                from_level=this_exit['from_level'],
-                to_level=this_exit['to_level']))
+
+        for exit_name, to_level in six.iteritems(level['exits']):
+            # Insert the level exit
+            db_conn.execute(
+                level_exits.insert().values(
+                    name=exit_name,
+                    from_level=level.id,
+                    to_level=to_level))
 
     # Finally, drop the old level exits pickle table
     # ----------------------------------------------
@@ -200,6 +226,7 @@ class Creature3(Base3):
     id = Column(Integer, primary_key=True)
     name = Column(Unicode, unique=True, nullable=False, index=True)
     num_limbs= Column(Integer, nullable=False)
+    magical_powers = relationship("CreaturePower3")
 
 class CreaturePower3(Base3):
     __tablename__ = "creature_power"
@@ -210,14 +237,13 @@ class CreaturePower3(Base3):
     name = Column(Unicode)
     description = Column(Unicode)
     hitpower = Column(Float, nullable=False)
-    magical_powers = relationship("CreaturePower3")
 
 class Level3(Base3):
     __tablename__ = "level"
 
     id = Column(Unicode, primary_key=True)
     name = Column(Unicode)
-    description = Column(UnicodeText)
+    description = Column(Unicode)
 
 class LevelExit3(Base3):
     __tablename__ = "level_exit"
@@ -231,79 +257,137 @@ class LevelExit3(Base3):
 
 
 SET3_MODELS = [Creature3, CreaturePower3, Level3, LevelExit3]
+SET3_MIGRATIONS = FULL_MIGRATIONS
 
 
 @RegisterMigration(4, FULL_MIGRATIONS)
 def creature_num_legs_to_num_limbs(db_conn):
-    metadata = MetaData(bind=db_conn.engine)
+    """
+    Turns out we're tracking all sorts of limbs, not "legs"
+    specifically.  Humans would be 4 here, for instance.  So we
+    renamed the column.
+    """
+    metadata = MetaData(bind=db_conn.bind)
     creature_table = Table(
         'creature', metadata,
-        autoload=True, autoload_with=db_conn.engine)
-    creature_table.c.num_legs.alter(name="num_limbs")
+        autoload=True, autoload_with=db_conn.bind)
+    creature_table.c.num_legs.alter(name=u"num_limbs")
 
 
 @RegisterMigration(5, FULL_MIGRATIONS)
 def level_exit_index_from_and_to_level(db_conn):
-    metadata = MetaData(bind=db_conn.engine)
+    """
+    Index the from and to levels of the level exit table.
+    """
+    metadata = MetaData(bind=db_conn.bind)
     level_exit = Table(
         'level_exit', metadata,
-        autoload=True, autoload_with=db_conn.engine)
-    Index('ix_from_level', level_exit.c.from_level).create(engine)
-    Index('ix_to_exit', level_exit.c.to_exit).create(engine)
+        autoload=True, autoload_with=db_conn.bind)
+    Index('ix_level_exit_from_level',
+          level_exit.c.from_level).create(db_conn.bind)
+    Index('ix_level_exit_to_level',
+          level_exit.c.to_level).create(db_conn.bind)
 
 
 @RegisterMigration(6, FULL_MIGRATIONS)
 def creature_power_index_creature(db_conn):
-    metadata = MetaData(bind=db_conn.engine)
+    """
+    Index our foreign key relationship to the creatures
+    """
+    metadata = MetaData(bind=db_conn.bind)
     creature_power = Table(
         'creature_power', metadata,
-        autoload=True, autoload_with=db_conn.engine)
-    Index('ix_creature', creature_power.c.creature).create(engine)
+        autoload=True, autoload_with=db_conn.bind)
+    Index('ix_creature_power_creature',
+          creature_power.c.creature).create(db_conn.bind)
 
 
 @RegisterMigration(7, FULL_MIGRATIONS)
 def creature_power_hitpower_to_float(db_conn):
-    metadata = MetaData(bind=db_conn.engine)
+    """
+    Convert hitpower column on creature power table from integer to
+    float.
+
+    Turns out we want super precise values of how much hitpower there
+    really is.
+    """
+    metadata = MetaData(bind=db_conn.bind)
+
+    # We have to access the creature table so sqlalchemy can make the
+    # foreign key relationship
+    creature_table = Table(
+        'creature', metadata,
+        autoload=True, autoload_with=db_conn.bind)
+
     creature_power = Table(
         'creature_power', metadata,
-        autoload=True, autoload_with=db_conn.engine)
+        Column('id', Integer, primary_key=True),
+        Column('creature', Integer,
+               ForeignKey('creature.id'), nullable=False,
+               index=True),
+        Column('name', Unicode),
+        Column('description', Unicode),
+        Column('hitpower', Integer, nullable=False))
+
     creature_power.c.hitpower.alter(type=Float)
 
 
+@RegisterMigration(8, FULL_MIGRATIONS)
+def creature_power_name_creature_unique(db_conn):
+    """
+    Add a unique constraint to name and creature on creature_power.
+
+    We don't want multiple creature powers with the same name per creature!
+    """
+    # Note: We don't actually check to see if this constraint is set
+    # up because at present there's no way to do so in sqlalchemy :\
+
+    metadata = MetaData(bind=db_conn.bind)
+
+    creature_power = Table(
+        'creature_power', metadata,
+        autoload=True, autoload_with=db_conn.bind)
+        
+    cons = changeset.constraint.UniqueConstraint(
+        'name', 'creature', table=creature_power)
+
+    cons.create()
+
+
 def _insert_migration1_objects(session):
     """
     Test objects to insert for the first set of things
     """
     # Insert creatures
     session.add_all(
-        [Creature1(name='centipede',
+        [Creature1(name=u'centipede',
                    num_legs=100,
                    is_demon=False),
-         Creature1(name='wolf',
+         Creature1(name=u'wolf',
                    num_legs=4,
                    is_demon=False),
          # don't ask me what a wizardsnake is.
-         Creature1(name='wizardsnake',
+         Creature1(name=u'wizardsnake',
                    num_legs=0,
                    is_demon=True)])
 
     # Insert levels
     session.add_all(
-        [Level1(id='necroplex',
-                name='The Necroplex',
-                description='A complex full of pure deathzone.',
+        [Level1(id=u'necroplex',
+                name=u'The Necroplex',
+                description=u'A complex full of pure deathzone.',
                 exits={
-                    'deathwell': 'evilstorm',
-                    'portal': 'central_park'}),
-         Level1(id='evilstorm',
-                name='Evil Storm',
-                description='A storm full of pure evil.',
+                    u'deathwell': u'evilstorm',
+                    u'portal': u'central_park'}),
+         Level1(id=u'evilstorm',
+                name=u'Evil Storm',
+                description=u'A storm full of pure evil.',
                 exits={}), # you can't escape the evilstorm
-         Level1(id='central_park'
-                name='Central Park, NY, NY',
-                description="New York's friendly Central Park.",
+         Level1(id=u'central_park',
+                name=u'Central Park, NY, NY',
+                description=u"New York's friendly Central Park.",
                 exits={
-                    'portal': 'necroplex'})])
+                    u'portal': u'necroplex'})])
 
     session.commit()
 
@@ -315,70 +399,532 @@ def _insert_migration2_objects(session):
     # Insert creatures
     session.add_all(
         [Creature2(
-                name='centipede',
+                name=u'centipede',
                 num_legs=100),
          Creature2(
-                name='wolf',
+                name=u'wolf',
                 num_legs=4,
                 magical_powers = [
                     CreaturePower2(
-                        name="ice breath",
-                        description="A blast of icy breath!",
+                        name=u"ice breath",
+                        description=u"A blast of icy breath!",
                         hitpower=20),
                     CreaturePower2(
-                        name="death stare",
-                        description="A frightening stare, for sure!",
+                        name=u"death stare",
+                        description=u"A frightening stare, for sure!",
                         hitpower=45)]),
          Creature2(
-                name='wizardsnake',
+                name=u'wizardsnake',
                 num_legs=0,
                 magical_powers=[
                     CreaturePower2(
-                        name='death_rattle',
-                        description='A rattle... of DEATH!',
+                        name=u'death_rattle',
+                        description=u'A rattle... of DEATH!',
                         hitpower=1000),
                     CreaturePower2(
-                        name='sneaky_stare',
-                        description="The sneakiest stare you've ever seen!"
+                        name=u'sneaky_stare',
+                        description=u"The sneakiest stare you've ever seen!",
                         hitpower=300),
                     CreaturePower2(
-                        name='slithery_smoke',
-                        description="A blast of slithery, slithery smoke.",
+                        name=u'slithery_smoke',
+                        description=u"A blast of slithery, slithery smoke.",
                         hitpower=10),
                     CreaturePower2(
-                        name='treacherous_tremors',
-                        description="The ground shakes beneath footed animals!",
+                        name=u'treacherous_tremors',
+                        description=u"The ground shakes beneath footed animals!",
                         hitpower=0)])])
 
     # Insert levels
     session.add_all(
-        [Level2(id='necroplex',
-                name='The Necroplex',
-                description='A complex full of pure deathzone.'),
-         Level2(id='evilstorm',
-                name='Evil Storm',
-                description='A storm full of pure evil.',
+        [Level2(id=u'necroplex',
+                name=u'The Necroplex',
+                description=u'A complex full of pure deathzone.'),
+         Level2(id=u'evilstorm',
+                name=u'Evil Storm',
+                description=u'A storm full of pure evil.',
+                exits=[]), # you can't escape the evilstorm
+         Level2(id=u'central_park',
+                name=u'Central Park, NY, NY',
+                description=u"New York's friendly Central Park.")])
+
+    # necroplex exits
+    session.add_all(
+        [LevelExit2(name=u'deathwell',
+                    from_level=u'necroplex',
+                    to_level=u'evilstorm'),
+         LevelExit2(name=u'portal',
+                    from_level=u'necroplex',
+                    to_level=u'central_park')])
+
+    # there are no evilstorm exits because there is no exit from the
+    # evilstorm
+      
+    # central park exits
+    session.add_all(
+        [LevelExit2(name=u'portal',
+                    from_level=u'central_park',
+                    to_level=u'necroplex')])
+
+    session.commit()
+
+
+def _insert_migration3_objects(session):
+    """
+    Test objects to insert for the third set of things
+    """
+    # Insert creatures
+    session.add_all(
+        [Creature3(
+                name=u'centipede',
+                num_limbs=100),
+         Creature3(
+                name=u'wolf',
+                num_limbs=4,
+                magical_powers = [
+                    CreaturePower3(
+                        name=u"ice breath",
+                        description=u"A blast of icy breath!",
+                        hitpower=20.0),
+                    CreaturePower3(
+                        name=u"death stare",
+                        description=u"A frightening stare, for sure!",
+                        hitpower=45.0)]),
+         Creature3(
+                name=u'wizardsnake',
+                num_limbs=0,
+                magical_powers=[
+                    CreaturePower3(
+                        name=u'death_rattle',
+                        description=u'A rattle... of DEATH!',
+                        hitpower=1000.0),
+                    CreaturePower3(
+                        name=u'sneaky_stare',
+                        description=u"The sneakiest stare you've ever seen!",
+                        hitpower=300.0),
+                    CreaturePower3(
+                        name=u'slithery_smoke',
+                        description=u"A blast of slithery, slithery smoke.",
+                        hitpower=10.0),
+                    CreaturePower3(
+                        name=u'treacherous_tremors',
+                        description=u"The ground shakes beneath footed animals!",
+                        hitpower=0.0)])],
+        # annnnnd one more to test a floating point hitpower
+        Creature3(
+                name=u'deity',
+                numb_limbs=30,
+                magical_powers=[
+                    CreaturePower3(
+                        name=u'smite',
+                        description=u'Smitten by holy wrath!',
+                        hitpower=9999.9)]))
+
+    # Insert levels
+    session.add_all(
+        [Level3(id=u'necroplex',
+                name=u'The Necroplex',
+                description=u'A complex full of pure deathzone.'),
+         Level3(id=u'evilstorm',
+                name=u'Evil Storm',
+                description=u'A storm full of pure evil.',
                 exits=[]), # you can't escape the evilstorm
-         Level2(id='central_park'
-                name='Central Park, NY, NY',
-                description="New York's friendly Central Park.")])
+         Level3(id=u'central_park',
+                name=u'Central Park, NY, NY',
+                description=u"New York's friendly Central Park.")])
 
     # necroplex exits
     session.add_all(
-        [LevelExit2(name='deathwell',
-                    from_level='necroplex',
-                    to_level='evilstorm'),
-         LevelExit2(name='portal',
-                    from_level='necroplex',
-                    to_level='central_park')])
+        [LevelExit3(name=u'deathwell',
+                    from_level=u'necroplex',
+                    to_level=u'evilstorm'),
+         LevelExit3(name=u'portal',
+                    from_level=u'necroplex',
+                    to_level=u'central_park')])
 
     # there are no evilstorm exits because there is no exit from the
     # evilstorm
       
     # central park exits
     session.add_all(
-        [LevelExit2(name='portal',
-                    from_level='central_park',
-                    to_level='necroplex')]
+        [LevelExit3(name=u'portal',
+                    from_level=u'central_park',
+                    to_level=u'necroplex')])
 
     session.commit()
+
+def create_test_engine():
+    from sqlalchemy import create_engine
+    engine = create_engine('sqlite:///:memory:', echo=False)
+    Session = sessionmaker(bind=engine)
+    return engine, Session
+
+
+def assert_col_type(column, this_class):
+    assert isinstance(column.type, this_class)
+
+
+def _get_level3_exits(session, level):
+    return dict(
+        [(level_exit.name, level_exit.to_level)
+         for level_exit in
+         session.query(LevelExit3).filter_by(from_level=level.id)])
+
+
+def test_set1_to_set3():
+    # Create / connect to database
+    # ----------------------------
+
+    engine, Session = create_test_engine()
+
+    # Create tables by migrating on empty initial set
+    # -----------------------------------------------
+
+    printer = CollectingPrinter()
+    migration_manager = MigrationManager(
+        u'__main__', SET1_MODELS, FOUNDATIONS, SET1_MIGRATIONS, Session(),
+        printer)
+
+    # Check latest migration and database current migration
+    assert migration_manager.latest_migration == 0
+    assert migration_manager.database_current_migration == None
+
+    result = migration_manager.init_or_migrate()
+
+    # Make sure output was "inited"
+    assert result == u'inited'
+    # Check output
+    assert printer.combined_string == (
+        "-> Initializing main mediagoblin tables... done.\n" + \
+        "   + Laying foundations for Creature1 table\n"    )
+    # Check version in database
+    assert migration_manager.latest_migration == 0
+    assert migration_manager.database_current_migration == 0
+
+
+    # Install the initial set
+    # -----------------------
+
+    _insert_migration1_objects(Session())
+
+    # Try to "re-migrate" with same manager settings... nothing should happen
+    migration_manager = MigrationManager(
+        u'__main__', SET1_MODELS, FOUNDATIONS, SET1_MIGRATIONS, 
+        Session(), printer)
+    assert migration_manager.init_or_migrate() == None
+
+    # Check version in database
+    assert migration_manager.latest_migration == 0
+    assert migration_manager.database_current_migration == 0
+
+    # Sanity check a few things in the database...
+    metadata = MetaData(bind=engine)
+
+    # Check the structure of the creature table
+    creature_table = Table(
+        'creature', metadata,
+        autoload=True, autoload_with=engine)
+    assert set(creature_table.c.keys()) == set(
+        ['id', 'name', 'num_legs', 'is_demon'])
+    assert_col_type(creature_table.c.id, Integer)
+    assert_col_type(creature_table.c.name, VARCHAR)
+    assert creature_table.c.name.nullable is False
+    #assert creature_table.c.name.index is True
+    #assert creature_table.c.name.unique is True
+    assert_col_type(creature_table.c.num_legs, Integer)
+    assert creature_table.c.num_legs.nullable is False
+    assert_col_type(creature_table.c.is_demon, Boolean)
+
+    # Check the structure of the level table
+    level_table = Table(
+        'level', metadata,
+        autoload=True, autoload_with=engine)
+    assert set(level_table.c.keys()) == set(
+        ['id', 'name', 'description', 'exits'])
+    assert_col_type(level_table.c.id, VARCHAR)
+    assert level_table.c.id.primary_key is True
+    assert_col_type(level_table.c.name, VARCHAR)
+    assert_col_type(level_table.c.description, VARCHAR)
+    # Skipping exits... Not sure if we can detect pickletype, not a
+    # big deal regardless.
+
+    # Now check to see if stuff seems to be in there.
+    session = Session()
+
+    # Check the creation of the foundation rows on the creature table
+    creature = session.query(Creature1).filter_by(
+        name=u'goblin').one()
+    assert creature.num_legs == 2
+    assert creature.is_demon == False
+
+    creature = session.query(Creature1).filter_by(
+        name=u'cerberus').one()
+    assert creature.num_legs == 4
+    assert creature.is_demon == True
+
+    
+    # Check the creation of the inserted rows on the creature and levels tables
+  
+    creature = session.query(Creature1).filter_by(
+        name=u'centipede').one()
+    assert creature.num_legs == 100
+    assert creature.is_demon == False
+
+    creature = session.query(Creature1).filter_by(
+        name=u'wolf').one()
+    assert creature.num_legs == 4
+    assert creature.is_demon == False
+
+    creature = session.query(Creature1).filter_by(
+        name=u'wizardsnake').one()
+    assert creature.num_legs == 0
+    assert creature.is_demon == True
+
+    level = session.query(Level1).filter_by(
+        id=u'necroplex').one()
+    assert level.name == u'The Necroplex'
+    assert level.description == u'A complex full of pure deathzone.'
+    assert level.exits == {
+        'deathwell': 'evilstorm',
+        'portal': 'central_park'}
+
+    level = session.query(Level1).filter_by(
+        id=u'evilstorm').one()
+    assert level.name == u'Evil Storm'
+    assert level.description == u'A storm full of pure evil.'
+    assert level.exits == {}  # You still can't escape the evilstorm!
+
+    level = session.query(Level1).filter_by(
+        id=u'central_park').one()
+    assert level.name == u'Central Park, NY, NY'
+    assert level.description == u"New York's friendly Central Park."
+    assert level.exits == {
+        'portal': 'necroplex'}
+
+    # Create new migration manager, but make sure the db migration
+    # isn't said to be updated yet
+    printer = CollectingPrinter()
+    migration_manager = MigrationManager(
+        u'__main__', SET3_MODELS, FOUNDATIONS, SET3_MIGRATIONS, Session(),
+        printer)
+
+    assert migration_manager.latest_migration == 8
+    assert migration_manager.database_current_migration == 0
+
+    # Migrate
+    result = migration_manager.init_or_migrate()
+
+    # Make sure result was "migrated"
+    assert result == u'migrated'
+
+    # TODO: Check output to user
+    assert printer.combined_string == """\
+-> Updating main mediagoblin tables:
+   + Running migration 1, "creature_remove_is_demon"... done.
+   + Running migration 2, "creature_powers_new_table"... done.
+   + Running migration 3, "level_exits_new_table"... done.
+   + Running migration 4, "creature_num_legs_to_num_limbs"... done.
+   + Running migration 5, "level_exit_index_from_and_to_level"... done.
+   + Running migration 6, "creature_power_index_creature"... done.
+   + Running migration 7, "creature_power_hitpower_to_float"... done.
+   + Running migration 8, "creature_power_name_creature_unique"... done.
+"""
+    
+    # Make sure version matches expected
+    migration_manager = MigrationManager(
+        u'__main__', SET3_MODELS, FOUNDATIONS, SET3_MIGRATIONS, Session(),
+        printer)
+    assert migration_manager.latest_migration == 8
+    assert migration_manager.database_current_migration == 8
+
+    # Check all things in database match expected
+
+    # Check the creature table
+    metadata = MetaData(bind=engine)
+    creature_table = Table(
+        'creature', metadata,
+        autoload=True, autoload_with=engine)
+    # assert set(creature_table.c.keys()) == set(
+    #     ['id', 'name', 'num_limbs'])
+    assert set(creature_table.c.keys()) == set(
+        [u'id', 'name', u'num_limbs', u'is_demon'])
+    assert_col_type(creature_table.c.id, Integer)
+    assert_col_type(creature_table.c.name, VARCHAR)
+    assert creature_table.c.name.nullable is False
+    #assert creature_table.c.name.index is True
+    #assert creature_table.c.name.unique is True
+    assert_col_type(creature_table.c.num_limbs, Integer)
+    assert creature_table.c.num_limbs.nullable is False
+
+    # Check the CreaturePower table
+    creature_power_table = Table(
+        'creature_power', metadata,
+        autoload=True, autoload_with=engine)
+    assert set(creature_power_table.c.keys()) == set(
+        ['id', 'creature', 'name', 'description', 'hitpower'])
+    assert_col_type(creature_power_table.c.id, Integer)
+    assert_col_type(creature_power_table.c.creature, Integer)
+    assert creature_power_table.c.creature.nullable is False
+    assert_col_type(creature_power_table.c.name, VARCHAR)
+    assert_col_type(creature_power_table.c.description, VARCHAR)
+    assert_col_type(creature_power_table.c.hitpower, Float)
+    assert creature_power_table.c.hitpower.nullable is False
+
+    # Check the structure of the level table
+    level_table = Table(
+        'level', metadata,
+        autoload=True, autoload_with=engine)
+    assert set(level_table.c.keys()) == set(
+        ['id', 'name', 'description'])
+    assert_col_type(level_table.c.id, VARCHAR)
+    assert level_table.c.id.primary_key is True
+    assert_col_type(level_table.c.name, VARCHAR)
+    assert_col_type(level_table.c.description, VARCHAR)
+
+    # Check the structure of the level_exits table
+    level_exit_table = Table(
+        'level_exit', metadata,
+        autoload=True, autoload_with=engine)
+    assert set(level_exit_table.c.keys()) == set(
+        ['id', 'name', 'from_level', 'to_level'])
+    assert_col_type(level_exit_table.c.id, Integer)
+    assert_col_type(level_exit_table.c.name, VARCHAR)
+    assert_col_type(level_exit_table.c.from_level, VARCHAR)
+    assert level_exit_table.c.from_level.nullable is False
+    #assert level_exit_table.c.from_level.index is True
+    assert_col_type(level_exit_table.c.to_level, VARCHAR)
+    assert level_exit_table.c.to_level.nullable is False
+    #assert level_exit_table.c.to_level.index is True
+
+    # Now check to see if stuff seems to be in there.
+    session = Session()
+
+
+    # Start with making sure that the foundations did not run again
+    assert session.query(Creature3).filter_by(
+        name=u'goblin').count() == 1
+    assert session.query(Creature3).filter_by(
+        name=u'cerberus').count() == 1
+
+    # Then make sure the models have been migrated correctly
+    creature = session.query(Creature3).filter_by(
+        name=u'centipede').one()
+    assert creature.num_limbs == 100.0
+    assert creature.magical_powers == []
+
+    creature = session.query(Creature3).filter_by(
+        name=u'wolf').one()
+    assert creature.num_limbs == 4.0
+    assert creature.magical_powers == []
+
+    creature = session.query(Creature3).filter_by(
+        name=u'wizardsnake').one()
+    assert creature.num_limbs == 0.0
+    assert creature.magical_powers == []
+
+    level = session.query(Level3).filter_by(
+        id=u'necroplex').one()
+    assert level.name == u'The Necroplex'
+    assert level.description == u'A complex full of pure deathzone.'
+    level_exits = _get_level3_exits(session, level)
+    assert level_exits == {
+        u'deathwell': u'evilstorm',
+        u'portal': u'central_park'}
+
+    level = session.query(Level3).filter_by(
+        id=u'evilstorm').one()
+    assert level.name == u'Evil Storm'
+    assert level.description == u'A storm full of pure evil.'
+    level_exits = _get_level3_exits(session, level)
+    assert level_exits == {}  # You still can't escape the evilstorm!
+
+    level = session.query(Level3).filter_by(
+        id=u'central_park').one()
+    assert level.name == u'Central Park, NY, NY'
+    assert level.description == u"New York's friendly Central Park."
+    level_exits = _get_level3_exits(session, level)
+    assert level_exits == {
+        'portal': 'necroplex'}
+
+
+#def test_set2_to_set3():
+    # Create / connect to database
+    # Create tables by migrating on empty initial set
+
+    # Install the initial set
+    # Check version in database
+    # Sanity check a few things in the database
+
+    # Migrate
+    # Make sure version matches expected
+    # Check all things in database match expected
+    # pass
+
+
+#def test_set1_to_set2_to_set3():
+    # Create / connect to database
+    # Create tables by migrating on empty initial set
+
+    # Install the initial set
+    # Check version in database
+    # Sanity check a few things in the database
+
+    # Migrate
+    # Make sure version matches expected
+    # Check all things in database match expected
+
+    # Migrate again
+    # Make sure version matches expected again
+    # Check all things in database match expected again
+
+    ##### Set2
+    # creature_table = Table(
+    #     'creature', metadata,
+    #     autoload=True, autoload_with=db_conn.bind)
+    # assert set(creature_table.c.keys()) == set(
+    #     ['id', 'name', 'num_legs'])
+    # assert_col_type(creature_table.c.id, Integer)
+    # assert_col_type(creature_table.c.name, VARCHAR)
+    # assert creature_table.c.name.nullable is False
+    # assert creature_table.c.name.index is True
+    # assert creature_table.c.name.unique is True
+    # assert_col_type(creature_table.c.num_legs, Integer)
+    # assert creature_table.c.num_legs.nullable is False
+
+    # # Check the CreaturePower table
+    # creature_power_table = Table(
+    #     'creature_power', metadata,
+    #     autoload=True, autoload_with=db_conn.bind)
+    # assert set(creature_power_table.c.keys()) == set(
+    #     ['id', 'creature', 'name', 'description', 'hitpower'])
+    # assert_col_type(creature_power_table.c.id, Integer)
+    # assert_col_type(creature_power_table.c.creature, Integer)
+    # assert creature_power_table.c.creature.nullable is False
+    # assert_col_type(creature_power_table.c.name, VARCHAR)
+    # assert_col_type(creature_power_table.c.description, VARCHAR)
+    # assert_col_type(creature_power_table.c.hitpower, Integer)
+    # assert creature_power_table.c.hitpower.nullable is False
+
+    # # Check the structure of the level table
+    # level_table = Table(
+    #     'level', metadata,
+    #     autoload=True, autoload_with=db_conn.bind)
+    # assert set(level_table.c.keys()) == set(
+    #     ['id', 'name', 'description'])
+    # assert_col_type(level_table.c.id, VARCHAR)
+    # assert level_table.c.id.primary_key is True
+    # assert_col_type(level_table.c.name, VARCHAR)
+    # assert_col_type(level_table.c.description, VARCHAR)
+
+    # # Check the structure of the level_exits table
+    # level_exit_table = Table(
+    #     'level_exit', metadata,
+    #     autoload=True, autoload_with=db_conn.bind)
+    # assert set(level_exit_table.c.keys()) == set(
+    #     ['id', 'name', 'from_level', 'to_level'])
+    # assert_col_type(level_exit_table.c.id, Integer)
+    # assert_col_type(level_exit_table.c.name, VARCHAR)
+    # assert_col_type(level_exit_table.c.from_level, VARCHAR)
+    # assert level_exit_table.c.from_level.nullable is False
+    # assert_col_type(level_exit_table.c.to_level, VARCHAR)
+
+    # pass