From 16affe2d1273426f93772e907154735c0005cf8d Mon Sep 17 00:00:00 2001 From: Vladislav Kuzmin Date: Tue, 17 Feb 2015 15:01:00 +0300 Subject: [PATCH] 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 --- .testr.conf | 2 +- .../alembic/versions/42278d6179b9_init.py | 8 +- refstack/db/sqlalchemy/api.py | 4 +- refstack/tests/api/__init__.py | 133 ++++++++++-------- refstack/tests/api/refstack.test.conf | 2 - setup-mysql-tests.sh | 37 +++++ test-requirements.txt | 9 +- tox.ini | 18 +-- 8 files changed, 130 insertions(+), 83 deletions(-) delete mode 100644 refstack/tests/api/refstack.test.conf create mode 100755 setup-mysql-tests.sh diff --git a/.testr.conf b/.testr.conf index 5b3d5441..bd99ad88 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,4 @@ [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_list_option=--list diff --git a/refstack/db/migrations/alembic/versions/42278d6179b9_init.py b/refstack/db/migrations/alembic/versions/42278d6179b9_init.py index 1c8ff788..802ca589 100644 --- a/refstack/db/migrations/alembic/versions/42278d6179b9_init.py +++ b/refstack/db/migrations/alembic/versions/42278d6179b9_init.py @@ -15,7 +15,7 @@ import sqlalchemy as sa def upgrade(): - ### commands auto generated by Alembic - please adjust! ### + # commands auto generated by Alembic - please adjust! op.create_table( 'test', sa.Column('updated_at', sa.DateTime()), @@ -56,12 +56,12 @@ def upgrade(): sa.UniqueConstraint('test_id', 'name'), sa.UniqueConstraint('test_id', 'uid') ) - ### end Alembic commands ### + # end Alembic commands def downgrade(): - ### commands auto generated by Alembic - please adjust! ### + # commands auto generated by Alembic - please adjust! op.drop_table('results') op.drop_table('meta') op.drop_table('test') - ### end Alembic commands ### + # end Alembic commands diff --git a/refstack/db/sqlalchemy/api.py b/refstack/db/sqlalchemy/api.py index a36f7a17..c475d79a 100644 --- a/refstack/db/sqlalchemy/api.py +++ b/refstack/db/sqlalchemy/api.py @@ -29,9 +29,7 @@ CONF = cfg.CONF _FACADE = None -_DEFAULT_SQL_CONNECTION = 'sqlite://' -db_options.set_defaults(cfg.CONF, - connection=_DEFAULT_SQL_CONNECTION) +db_options.set_defaults(cfg.CONF) def _create_facade_lazily(): diff --git a/refstack/tests/api/__init__.py b/refstack/tests/api/__init__.py index 27446770..10552f17 100644 --- a/refstack/tests/api/__init__.py +++ b/refstack/tests/api/__init__.py @@ -14,26 +14,28 @@ # under the License. """Base classes for API tests.""" -import inspect import os -import alembic -import alembic.config -from oslo_config import cfg -import sqlalchemy as sa -import sqlalchemy.exc -from unittest import TestCase -from webtest import TestApp +from oslo_config import fixture as config_fixture +from oslotest import base +import pecan.testing +from sqlalchemy.engine import reflection +from sqlalchemy import create_engine +from sqlalchemy.schema import ( + MetaData, + Table, + DropTable, + ForeignKeyConstraint, + DropConstraint, +) +from testtools import testcase -import refstack -from refstack.api import app - -CONF = cfg.CONF +from refstack.db import migration -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. literal application and its integration with the framework. @@ -41,60 +43,75 @@ class FunctionalTest(TestCase): def setUp(self): """Test setup.""" - class TestConfig(object): - app = { + super(FunctionalTest, self).setUp() + + # 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', '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( - os.path.dirname(os.path.realpath(__file__)), - 'refstack.test.conf' - ) - 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() + self.app = pecan.testing.load_test_app(self.config) + + self.drop_all_tables_and_constraints() + migration.upgrade('head') def tearDown(self): """Test teardown.""" + super(FunctionalTest, self).tearDown() + pecan.set_config({}, overwrite=True) self.app.reset() - def prepare_test_db(self): - """Create/clear test database.""" - db_url = CONF.database.connection - db_name = db_url.split('/')[-1] - short_db_url = '/'.join(db_url.split('/')[0:-1]) - 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 drop_all_tables_and_constraints(self): + """Drop tables and cyclical constraints between tables""" + engine = create_engine(self.connection) + conn = engine.connect() + trans = conn.begin() - def migrate_test_db(self): - """Apply migrations to test database.""" - alembic_cfg = alembic.config.Config() - alembic_cfg.set_main_option( - "script_location", - os.path.join(self.project_path, 'refstack', 'db', - 'migrations', 'alembic') - ) - alembic_cfg.set_main_option("sqlalchemy.url", - CONF.database.connection) - alembic.command.upgrade(alembic_cfg, 'head') + inspector = reflection.Inspector.from_engine(engine) + metadata = MetaData() + + tbs = [] + all_fks = [] + + try: + for table_name in inspector.get_table_names(): + fks = [] + for fk in inspector.get_foreign_keys(table_name): + 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, status=None, expect_errors=False, **params): diff --git a/refstack/tests/api/refstack.test.conf b/refstack/tests/api/refstack.test.conf deleted file mode 100644 index f1dff688..00000000 --- a/refstack/tests/api/refstack.test.conf +++ /dev/null @@ -1,2 +0,0 @@ -[DEFAULT] -sql_connection = mysql://root:passw0rd@127.0.0.1/refstack diff --git a/setup-mysql-tests.sh b/setup-mysql-tests.sh new file mode 100755 index 00000000..1729a0f4 --- /dev/null +++ b/setup-mysql-tests.sh @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index c508dde5..477c08bd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,8 @@ -pep8==1.4.5 -pyflakes>=0.7.2,<0.7.4 -flake8==2.0 +pep8==1.5.7 +pyflakes==0.8.1 +flake8==2.2.4 +oslotest>=1.2.0 # Apache-2.0 python-subunit>=0.0.18 testrepository>=0.0.18 testtools>=0.9.34 -mysqlclient \ No newline at end of file +mysqlclient diff --git a/tox.ini b/tox.ini index 8236eab9..da707424 100644 --- a/tox.ini +++ b/tox.ini @@ -18,17 +18,13 @@ deps = -r{toxinidir}/requirements.txt commands = python setup.py testr --testr-args='{posargs}' distribute = false -[testenv:func] -usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = python -m unittest discover ./refstack/tests/api -distribute = false +[testenv:py27-func-mysql] +basepython = python2.7 +# Integration/functional tests +# must not be run in parallel (--concurrency=1), +# because each of these tests +# require cleanup of database +commands = {toxinidir}/setup-mysql-tests.sh python setup.py testr --slowest --testr-args='{posargs:--concurrency=1}' [testenv:pep8] commands =