Improve database creation for testing

Create MySQL database before starting tests and cleanup after that.
MySQL database is able to work in standalone mode where establish
connection using a Unix domain socket.
This patch makes it possible to perform functional tests with `tox -e py27-func-mysql`.
If an enviroment variable REFSTACK_TEST_MYSQL_URL is set then tests will run with it
and a database will not be created.
Update test requirements.

Change-Id: I0cdba19701bbf06109e78f78f275038caeecd881
This commit is contained in:
Vladislav Kuzmin 2015-02-17 15:01:00 +03:00
parent 8ea0a1ace9
commit 16affe2d12
8 changed files with 130 additions and 83 deletions

View File

@ -1,4 +1,4 @@
[DEFAULT] [DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./refstack -s ./refstack/tests/unit $LISTOPT $IDOPTION test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./refstack -s ./refstack/tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE test_id_option=--load-list $IDFILE
test_list_option=--list test_list_option=--list

View File

@ -15,7 +15,7 @@ import sqlalchemy as sa
def upgrade(): def upgrade():
### commands auto generated by Alembic - please adjust! ### # commands auto generated by Alembic - please adjust!
op.create_table( op.create_table(
'test', 'test',
sa.Column('updated_at', sa.DateTime()), sa.Column('updated_at', sa.DateTime()),
@ -56,12 +56,12 @@ def upgrade():
sa.UniqueConstraint('test_id', 'name'), sa.UniqueConstraint('test_id', 'name'),
sa.UniqueConstraint('test_id', 'uid') sa.UniqueConstraint('test_id', 'uid')
) )
### end Alembic commands ### # end Alembic commands
def downgrade(): def downgrade():
### commands auto generated by Alembic - please adjust! ### # commands auto generated by Alembic - please adjust!
op.drop_table('results') op.drop_table('results')
op.drop_table('meta') op.drop_table('meta')
op.drop_table('test') op.drop_table('test')
### end Alembic commands ### # end Alembic commands

View File

@ -29,9 +29,7 @@ CONF = cfg.CONF
_FACADE = None _FACADE = None
_DEFAULT_SQL_CONNECTION = 'sqlite://' db_options.set_defaults(cfg.CONF)
db_options.set_defaults(cfg.CONF,
connection=_DEFAULT_SQL_CONNECTION)
def _create_facade_lazily(): def _create_facade_lazily():

View File

@ -14,26 +14,28 @@
# under the License. # under the License.
"""Base classes for API tests.""" """Base classes for API tests."""
import inspect
import os import os
import alembic from oslo_config import fixture as config_fixture
import alembic.config from oslotest import base
from oslo_config import cfg import pecan.testing
import sqlalchemy as sa from sqlalchemy.engine import reflection
import sqlalchemy.exc from sqlalchemy import create_engine
from unittest import TestCase from sqlalchemy.schema import (
from webtest import TestApp MetaData,
Table,
DropTable,
ForeignKeyConstraint,
DropConstraint,
)
from testtools import testcase
import refstack from refstack.db import migration
from refstack.api import app
CONF = cfg.CONF
class FunctionalTest(TestCase): class FunctionalTest(base.BaseTestCase):
"""Functional test case. """Base class for functional test case.
Used for functional tests where you need to test your. Used for functional tests where you need to test your.
literal application and its integration with the framework. literal application and its integration with the framework.
@ -41,60 +43,75 @@ class FunctionalTest(TestCase):
def setUp(self): def setUp(self):
"""Test setup.""" """Test setup."""
class TestConfig(object): super(FunctionalTest, self).setUp()
app = {
# Skip integration/functional tests
# if database has not been created
self.connection = os.environ.get("REFSTACK_TEST_MYSQL_URL")
if self.connection is None:
raise testcase.TestSkipped("Database connection url was not found")
self.config = {
'app': {
'root': 'refstack.api.controllers.root.RootController', 'root': 'refstack.api.controllers.root.RootController',
'modules': ['refstack.api'], 'modules': ['refstack.api'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/${package}/templates',
} }
}
self.config_fixture = config_fixture.Config()
self.CONF = self.useFixture(self.config_fixture).conf
self.CONF.set_override('connection',
self.connection,
'database')
test_config = os.path.join( self.app = pecan.testing.load_test_app(self.config)
os.path.dirname(os.path.realpath(__file__)),
'refstack.test.conf' self.drop_all_tables_and_constraints()
) migration.upgrade('head')
os.environ['REFSTACK_OSLO_CONFIG'] = test_config
self.project_path = os.path.abspath(
os.path.join(inspect.getabsfile(refstack), '..', '..'))
self.app = TestApp(app.setup_app(TestConfig()))
self.prepare_test_db()
self.migrate_test_db()
def tearDown(self): def tearDown(self):
"""Test teardown.""" """Test teardown."""
super(FunctionalTest, self).tearDown()
pecan.set_config({}, overwrite=True)
self.app.reset() self.app.reset()
def prepare_test_db(self): def drop_all_tables_and_constraints(self):
"""Create/clear test database.""" """Drop tables and cyclical constraints between tables"""
db_url = CONF.database.connection engine = create_engine(self.connection)
db_name = db_url.split('/')[-1] conn = engine.connect()
short_db_url = '/'.join(db_url.split('/')[0:-1]) trans = conn.begin()
try:
engine = sa.create_engine(db_url)
conn = engine.connect()
conn.execute('commit')
conn.execute('drop database %s' % db_name)
conn.close()
except sqlalchemy.exc.OperationalError:
pass
finally:
engine = sa.create_engine('/'.join((short_db_url, 'mysql')))
conn = engine.connect()
conn.execute('commit')
conn.execute('create database %s' % db_name)
conn.close()
def migrate_test_db(self): inspector = reflection.Inspector.from_engine(engine)
"""Apply migrations to test database.""" metadata = MetaData()
alembic_cfg = alembic.config.Config()
alembic_cfg.set_main_option( tbs = []
"script_location", all_fks = []
os.path.join(self.project_path, 'refstack', 'db',
'migrations', 'alembic') try:
) for table_name in inspector.get_table_names():
alembic_cfg.set_main_option("sqlalchemy.url", fks = []
CONF.database.connection) for fk in inspector.get_foreign_keys(table_name):
alembic.command.upgrade(alembic_cfg, 'head') if not fk['name']:
continue
fks.append(
ForeignKeyConstraint((), (), name=fk['name']))
t = Table(table_name, metadata, *fks)
tbs.append(t)
all_fks.extend(fks)
for fkc in all_fks:
conn.execute(DropConstraint(fkc))
for table in tbs:
conn.execute(DropTable(table))
trans.commit()
trans.close()
conn.close()
except:
trans.rollback()
conn.close()
raise
def get_json(self, url, headers=None, extra_environ=None, def get_json(self, url, headers=None, extra_environ=None,
status=None, expect_errors=False, **params): status=None, expect_errors=False, **params):

View File

@ -1,2 +0,0 @@
[DEFAULT]
sql_connection = mysql://root:passw0rd@127.0.0.1/refstack

37
setup-mysql-tests.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash -x
wait_for_line () {
while read line
do
echo "$line" | grep -q "$1" && break
done < "$2"
# Read the fifo for ever otherwise process would block
cat "$2" >/dev/null &
}
# If test DB url is provided, run tests with it
if [[ "$REFSTACK_TEST_MYSQL_URL" ]]
then
$*
exit $?
fi
# Else setup mysql base for tests.
# Start MySQL process for tests
MYSQL_DATA=`mktemp -d /tmp/refstack-mysql-XXXXX`
mkfifo ${MYSQL_DATA}/out
# On systems like Fedora here's where mysqld can be found
PATH=$PATH:/usr/libexec
mysqld --no-defaults --datadir=${MYSQL_DATA} --pid-file=${MYSQL_DATA}/mysql.pid --socket=${MYSQL_DATA}/mysql.socket --skip-networking --skip-grant-tables &> ${MYSQL_DATA}/out &
# Wait for MySQL to start listening to connections
wait_for_line "mysqld: ready for connections." ${MYSQL_DATA}/out
export REFSTACK_TEST_MYSQL_URL="mysql://root@localhost/test?unix_socket=${MYSQL_DATA}/mysql.socket&charset=utf8"
mysql --no-defaults -S ${MYSQL_DATA}/mysql.socket -e 'CREATE DATABASE test;'
# Yield execution to venv command
$*
# Cleanup after tests
ret=$?
kill $(jobs -p)
rm -rf "${MYSQL_DATA}"
exit $ret

View File

@ -1,7 +1,8 @@
pep8==1.4.5 pep8==1.5.7
pyflakes>=0.7.2,<0.7.4 pyflakes==0.8.1
flake8==2.0 flake8==2.2.4
oslotest>=1.2.0 # Apache-2.0
python-subunit>=0.0.18 python-subunit>=0.0.18
testrepository>=0.0.18 testrepository>=0.0.18
testtools>=0.9.34 testtools>=0.9.34
mysqlclient mysqlclient

18
tox.ini
View File

@ -18,17 +18,13 @@ deps = -r{toxinidir}/requirements.txt
commands = python setup.py testr --testr-args='{posargs}' commands = python setup.py testr --testr-args='{posargs}'
distribute = false distribute = false
[testenv:func] [testenv:py27-func-mysql]
usedevelop = True basepython = python2.7
install_command = pip install -U {opts} {packages} # Integration/functional tests
setenv = VIRTUAL_ENV={envdir} # must not be run in parallel (--concurrency=1),
LANG=en_US.UTF-8 # because each of these tests
LANGUAGE=en_US:en # require cleanup of database
LC_ALL=C commands = {toxinidir}/setup-mysql-tests.sh python setup.py testr --slowest --testr-args='{posargs:--concurrency=1}'
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python -m unittest discover ./refstack/tests/api
distribute = false
[testenv:pep8] [testenv:pep8]
commands = commands =