set up tests to run with sqlite

For most of the tests for storyboard sqlite is around 10 times faster
than MySQL (more for non-SSD systems). An sqlite database does not
support some operations, like modifying constraints or dropping
columns, so we cannot avoid testing with MySQL. We can however use
sqlite for local development to reduce the pain involved with running
tests as part of the development process.

This patch adds a tox environment for running the tests against
sqlite3. The new tox environment is intended to be used by developers
as well as the new check and gate job defined in .zuul.yaml.

The new job ensures that changes to alembic migration scripts continue
to work with sqlite.

This patch also modifies the existing alter scripts to skip steps not
supported under sqlite. Those steps aren't strictly needed, and they
are still tested when the CI system runs the tests with MySQL.

Change-Id: Icb979cb03e10c56519a90ea3976a4da2d9bddb05
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2018-01-30 19:58:21 +00:00
parent ac2b2921d6
commit 2fe2f57b6b
12 changed files with 129 additions and 45 deletions

16
.zuul.yaml Normal file
View File

@ -0,0 +1,16 @@
- job:
name: storyboard-tox-sqlite
parent: openstack-tox
description: |
Run tests using sqlite instead of mysql.
vars:
tox_envlist: sqlite
- project:
name: openstack-infra/storyboard
check:
jobs:
- storyboard-tox-sqlite
gate:
jobs:
- storyboard-tox-sqlite

View File

@ -30,6 +30,11 @@ or for Python 3::
$ tox -e py35
For faster versions of the integration tests using only Python 3,
run::
$ tox -e sqlite
And to run the style-checker and static analysis tool::
$ tox -e pep8

View File

@ -82,10 +82,13 @@ def upgrade(active_plugins=None, options=None):
worklist.id, move_permission, session=session)
session.flush()
op.drop_constraint(u'boards_ibfk_2', 'boards', type_='foreignkey')
op.drop_column(u'boards', 'permission_id')
op.drop_constraint(u'worklists_ibfk_2', 'worklists', type_='foreignkey')
op.drop_column(u'worklists', 'permission_id')
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.drop_constraint(u'boards_ibfk_2', 'boards', type_='foreignkey')
op.drop_column(u'boards', 'permission_id')
op.drop_constraint(u'worklists_ibfk_2', 'worklists',
type_='foreignkey')
op.drop_column(u'worklists', 'permission_id')
def downgrade(active_plugins=None, options=None):

View File

@ -82,8 +82,10 @@ def upgrade(active_plugins=None, options=None):
'worklist_items',
sa.Column('display_due_date', sa.Integer(), nullable=True)
)
op.create_foreign_key(
None, 'worklist_items', 'due_dates', ['display_due_date'], ['id'])
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.create_foreign_key(
None, 'worklist_items', 'due_dates', ['display_due_date'], ['id'])
def downgrade(active_plugins=None, options=None):

View File

@ -31,8 +31,10 @@ import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.add_column(
'comments', sa.Column('in_reply_to', sa.Integer(), nullable=True))
op.create_foreign_key(
'comments_ibfk_1', 'comments', 'comments', ['in_reply_to'], ['id'])
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.create_foreign_key(
'comments_ibfk_1', 'comments', 'comments', ['in_reply_to'], ['id'])
def downgrade(active_plugins=None, options=None):

View File

@ -46,12 +46,16 @@ def upgrade(active_plugins=None, options=None):
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
sa.ForeignKeyConstraint(['story_id'], ['stories.id'], )
)
op.add_column(
u'stories',
sa.Column('private', sa.Boolean(), default=False, nullable=False))
op.alter_column('worklist_items', 'list_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
dialect = op.get_bind().engine.dialect
if dialect.name == 'sqlite':
col = sa.Column('private', sa.Boolean(), default=False)
else:
col = sa.Column('private', sa.Boolean(), default=False, nullable=False)
op.add_column(u'stories', col)
if dialect.supports_alter:
op.alter_column('worklist_items', 'list_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
def downgrade(active_plugins=None, options=None):

View File

@ -33,10 +33,12 @@ new_type_enum = sa.Enum(
def upgrade(active_plugins=None, options=None):
op.alter_column('subscriptions',
'target_type',
existing_type=old_type_enum,
type_=new_type_enum)
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.alter_column('subscriptions',
'target_type',
existing_type=old_type_enum,
type_=new_type_enum)
def downgrade(active_plugins=None, options=None):

View File

@ -33,10 +33,13 @@ def upgrade(active_plugins=None, options=None):
'events', sa.Column('board_id', sa.Integer(), nullable=True))
op.add_column(
'events', sa.Column('worklist_id', sa.Integer(), nullable=True))
op.create_foreign_key(
'fk_event_worklist', 'events', 'worklists', ['worklist_id'], ['id'])
op.create_foreign_key(
'fk_event_board', 'events', 'boards', ['board_id'], ['id'])
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.create_foreign_key(
'fk_event_worklist', 'events', 'worklists',
['worklist_id'], ['id'])
op.create_foreign_key(
'fk_event_board', 'events', 'boards', ['board_id'], ['id'])
def downgrade(active_plugins=None, options=None):

View File

@ -29,9 +29,10 @@ import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.alter_column('project_groups', 'name', type_=sa.Unicode(100))
op.alter_column('projects', 'name', type_=sa.Unicode(100))
dialect = op.get_bind().engine.dialect
if dialect.supports_alter:
op.alter_column('project_groups', 'name', type_=sa.Unicode(100))
op.alter_column('projects', 'name', type_=sa.Unicode(100))
def downgrade(active_plugins=None, options=None):

View File

@ -16,6 +16,7 @@
# under the License.
import os
import os.path
import shutil
import stat
import uuid
@ -142,34 +143,52 @@ class DbTestCase(WorkingDirTestCase):
self.setup_db()
def setup_db(self):
self.db_name = "storyboard_test_db_%s" % uuid.uuid4()
self.db_name = self.db_name.replace("-", "_")
LOG.info('creating database %s', self.db_name)
# The engine w/o db name
engine = sqlalchemy.create_engine(
self.test_connection)
engine.execute("CREATE DATABASE %s" % self.db_name)
alembic_config = get_alembic_config()
alembic_config.storyboard_config = CONF
CONF.set_override(
"connection",
self.test_connection + "/%s"
% self.db_name,
group="database")
self._full_db_name = self.test_connection + '/' + self.db_name
LOG.info('using database %s', CONF.database.connection)
if self.test_connection.startswith('sqlite://'):
self.using_sqlite = True
else:
self.using_sqlite = False
# The engine w/o db name
engine = sqlalchemy.create_engine(
self.test_connection)
engine.execute("CREATE DATABASE %s" % self.db_name)
alembic_config = get_alembic_config()
alembic_config.storyboard_config = CONF
command.upgrade(alembic_config, "head")
self.addCleanup(self._drop_db)
def _drop_db(self):
engine = sqlalchemy.create_engine(
self.test_connection)
try:
engine.execute("DROP DATABASE %s" % self.db_name)
except Exception as err:
LOG.error('failed to drop database %s: %s',
self.db_name, err)
if self.test_connection.startswith('sqlite://'):
filename = self._full_db_name[9:]
if filename[:2] == '//':
filename = filename[1:]
if os.path.exists(filename):
LOG.info('removing database file %s', filename)
try:
os.unlink(filename)
except OSError as err:
LOG.error('could not remove %s: %s',
filename, err)
else:
engine = sqlalchemy.create_engine(
self.test_connection)
try:
engine.execute("DROP DATABASE %s" % self.db_name)
except Exception as err:
LOG.error('failed to drop database %s: %s',
self.db_name, err)
db_api_base.cleanup()
PATH_PREFIX = '/v1'

View File

@ -49,8 +49,26 @@ class TestDBReferenceError(base.BaseDbTestCase):
'story_id': 100
}
self.assertRaises(exc.DBReferenceError,
lambda: tasks.task_create(task))
# TODO(dhellmann): The SQLite database doesn't use foreign key
# constraints instead of getting an error from the database
# when we try to insert the task we get the error later when
# we try to update the story. The behavior difference doesn't
# seem all that important since it only affects this test, but
# at some point we should probably ensure that all database
# reference errors are turned into the same exception class
# for consistency. For now we just test slightly differently.
if self.using_sqlite:
self.assertRaises(
exc.NotFound,
tasks.task_create,
task,
)
else:
self.assertRaises(
exc.DBReferenceError,
tasks.task_create,
task,
)
class TestDbInvalidSortKey(base.BaseDbTestCase):

13
tox.ini
View File

@ -1,5 +1,5 @@
[tox]
minversion = 1.6
minversion = 2.9.1
skipsdist = True
envlist = py34,py27,pep8
@ -10,12 +10,21 @@ setenv =
VIRTUAL_ENV={envdir}
OS_STDERR_CAPTURE=1
OS_STDOUT_CAPTURE=1
passenv = OS_TEST_TIMEOUT
passenv =
OS_TEST_TIMEOUT
STORYBOARD_TEST_DB
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = ostestr '{posargs}'
whitelist_externals = bash
[testenv:sqlite]
basepython = python3
setenv =
STORYBOARD_TEST_DB=sqlite:///{envtmpdir}
OS_STDERR_CAPTURE=1
OS_STDOUT_CAPTURE=1
[testenv:pep8]
commands = flake8